-
-
-
-
{
+fdescribe('SearchPageComponent', () => {
let comp: SearchPageComponent;
let fixture: ComponentFixture;
let searchServiceObject: SearchService;
@@ -39,14 +43,15 @@ describe('SearchPageComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
- // imports: [ SearchPageModule ],
- declarations: [SearchPageComponent],
+ imports: [ SearchPageModule ],
+ // declarations: [SearchPageComponent],
providers: [
{ provide: SearchService, useValue: searchServiceStub },
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: CommunityDataService, useValue: communityDataServiceStub },
- { provide: Router, useClass: RouterStub }
- ],
+ { provide: Router, useClass: RouterStub },
+ { provide: Store, useClass: {} }
+ ],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts
index 01569711ab..a4a37162e2 100644
--- a/src/app/+search-page/search-page.component.ts
+++ b/src/app/+search-page/search-page.component.ts
@@ -6,11 +6,20 @@ import { SearchResult } from './search-result.model';
import { DSpaceObject } from '../core/shared/dspace-object.model';
import { SortOptions } from '../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
-import { ViewModeSwitchComponent } from '../shared/view-mode-switch/view-mode-switch.component';
import { SearchOptions } from './search-options.model';
import { CommunityDataService } from '../core/data/community-data.service';
import { isNotEmpty } from '../shared/empty.util';
import { Community } from '../core/shared/community.model';
+import { createSelector, Store } from '@ngrx/store';
+import { AppState } from '../app.reducer';
+import { Observable } from 'rxjs/Observable';
+import { SearchSidebarState } from './search-sidebar/search-sidebar.reducer';
+import { SearchSidebarToggleAction } from './search-sidebar/search-sidebar.actions';
+import { slideInOut } from '../shared/animations/slide';
+import { HostWindowService } from '../shared/host-window.service';
+
+const sidebarStateSelector = (state: AppState) => state.searchSidebar;
+const sidebarCollapsedSelector = createSelector(sidebarStateSelector, (sidebar: SearchSidebarState) => sidebar.sidebarCollapsed);
/**
* This component renders a simple item page.
@@ -22,6 +31,7 @@ import { Community } from '../core/shared/community.model';
selector: 'ds-search-page',
styleUrls: ['./search-page.component.scss'],
templateUrl: './search-page.component.html',
+ animations: [slideInOut]
})
export class SearchPageComponent implements OnInit, OnDestroy {
@@ -34,14 +44,14 @@ export class SearchPageComponent implements OnInit, OnDestroy {
currentParams = {};
searchOptions: SearchOptions;
scopeList: RemoteData;
- isSidebarActive = false;
- isListView = true;
+ isSidebarCollapsed: Observable;
+ isMobileView: Observable;
- constructor(
- private service: SearchService,
- private route: ActivatedRoute,
- private communityService: CommunityDataService
- ) {
+ constructor(private service: SearchService,
+ private route: ActivatedRoute,
+ private communityService: CommunityDataService,
+ private store: Store,
+ private hostWindowService: HostWindowService) {
this.scopeList = communityService.findAll();
// Initial pagination config
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
@@ -50,9 +60,14 @@ export class SearchPageComponent implements OnInit, OnDestroy {
pagination.pageSize = 10;
const sort: SortOptions = new SortOptions();
this.searchOptions = { pagination: pagination, sort: sort };
+ this.isMobileView = Observable.combineLatest(
+ this.hostWindowService.isXs(),
+ this.hostWindowService.isSm(),
+ (isXs, isSm) => isXs || isSm);
}
ngOnInit(): void {
+ this.isSidebarCollapsed = this.store.select(sidebarCollapsedSelector);
this.sub = this.route
.queryParams
.subscribe((params) => {
@@ -93,11 +108,7 @@ export class SearchPageComponent implements OnInit, OnDestroy {
this.sub.unsubscribe();
}
- setSidebarActive(show: boolean) {
- this.isSidebarActive = show;
- }
-
- setListView(isList: boolean) {
- this.isListView = isList;
+ public toggle(): void {
+ this.store.dispatch(new SearchSidebarToggleAction());
}
}
diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts
index 0df318cbdc..6345582de9 100644
--- a/src/app/+search-page/search-page.module.ts
+++ b/src/app/+search-page/search-page.module.ts
@@ -19,8 +19,6 @@ import { LayoutControlsComponent } from './layout-controls/layout-controls.compo
imports: [
SearchPageRoutingModule,
CommonModule,
- TranslateModule,
- RouterModule,
SharedModule
],
declarations: [
diff --git a/src/app/+search-page/search-sidebar/search-sidebar.actions.ts b/src/app/+search-page/search-sidebar/search-sidebar.actions.ts
new file mode 100644
index 0000000000..f393bc10b3
--- /dev/null
+++ b/src/app/+search-page/search-sidebar/search-sidebar.actions.ts
@@ -0,0 +1,40 @@
+import { Action } from '@ngrx/store';
+
+import { type } from '../../shared/ngrx/type';
+
+/**
+ * For each action type in an action group, make a simple
+ * enum object for all of this group's action types.
+ *
+ * The 'type' utility function coerces strings into string
+ * literal types and runs a simple check to guarantee all
+ * action types in the application are unique.
+ */
+export const SearchSidebarActionTypes = {
+ COLLAPSE: type('dspace/search-sidebar/COLLAPSE'),
+ EXPAND: type('dspace/search-sidebar/EXPAND'),
+ TOGGLE: type('dspace/search-sidebar/TOGGLE')
+};
+
+/* tslint:disable:max-classes-per-file */
+export class SearchSidebarCollapseAction implements Action {
+ type = SearchSidebarActionTypes.COLLAPSE;
+}
+
+export class SearchSidebarExpandAction implements Action {
+ type = SearchSidebarActionTypes.EXPAND;
+}
+
+export class SearchSidebarToggleAction implements Action {
+ type = SearchSidebarActionTypes.TOGGLE;
+}
+/* tslint:enable:max-classes-per-file */
+
+/**
+ * Export a type alias of all actions in this action group
+ * so that reducers can easily compose action types
+ */
+export type SearchSidebarAction
+ = SearchSidebarCollapseAction
+ | SearchSidebarExpandAction
+ | SearchSidebarToggleAction
diff --git a/src/app/+search-page/search-sidebar/search-sidebar.component.html b/src/app/+search-page/search-sidebar/search-sidebar.component.html
index 9ba8677313..9c0b7b5f3d 100644
--- a/src/app/+search-page/search-sidebar/search-sidebar.component.html
+++ b/src/app/+search-page/search-sidebar/search-sidebar.component.html
@@ -7,6 +7,7 @@
\ No newline at end of file
diff --git a/src/app/+search-page/search-sidebar/search-sidebar.effects.ts b/src/app/+search-page/search-sidebar/search-sidebar.effects.ts
new file mode 100644
index 0000000000..c6f04dbcb1
--- /dev/null
+++ b/src/app/+search-page/search-sidebar/search-sidebar.effects.ts
@@ -0,0 +1,23 @@
+import { Injectable } from '@angular/core';
+import { Effect, Actions } from '@ngrx/effects'
+import * as fromRouter from '@ngrx/router-store';
+
+import { HostWindowActionTypes } from '../../shared/host-window.actions';
+import { SearchSidebarCollapseAction } from './search-sidebar.actions';
+
+@Injectable()
+export class SearchSidebarEffects {
+
+ @Effect() resize$ = this.actions$
+ .ofType(HostWindowActionTypes.RESIZE)
+ .map(() => new SearchSidebarCollapseAction());
+
+ @Effect() routeChange$ = this.actions$
+ .ofType(fromRouter.ROUTER_NAVIGATION)
+ .map(() => new SearchSidebarCollapseAction());
+
+ constructor(private actions$: Actions) {
+
+ }
+
+}
diff --git a/src/app/+search-page/search-sidebar/search-sidebar.reducer.spec.ts b/src/app/+search-page/search-sidebar/search-sidebar.reducer.spec.ts
new file mode 100644
index 0000000000..0976d2f3e8
--- /dev/null
+++ b/src/app/+search-page/search-sidebar/search-sidebar.reducer.spec.ts
@@ -0,0 +1,91 @@
+import * as deepFreeze from 'deep-freeze';
+
+import { sidebarReducer } from './search-sidebar.reducer';
+import {
+ SearchSidebarCollapseAction, SearchSidebarExpandAction,
+ SearchSidebarToggleAction
+} from './search-sidebar.actions';
+import { async, TestBed } from '@angular/core/testing';
+import { TranslateModule } from '@ngx-translate/core';
+
+class NullAction extends SearchSidebarCollapseAction {
+ type = null;
+
+ constructor() {
+ super();
+ }
+}
+
+describe('sidebarReducer', () => {
+
+ it('should return the current state when no valid actions have been made', () => {
+ const state = { sidebarCollapsed: false };
+ const action = new NullAction();
+ const newState = sidebarReducer(state, action);
+
+ expect(newState).toEqual(state);
+ });
+
+ it('should start with sidebarCollapsed = true', () => {
+ const action = new NullAction();
+ const initialState = sidebarReducer(undefined, action);
+
+ // The search sidebar starts collapsed
+ expect(initialState.sidebarCollapsed).toEqual(true);
+ });
+
+ it('should set sidebarCollapsed to true in response to the COLLAPSE action', () => {
+ const state = { sidebarCollapsed: false };
+ const action = new SearchSidebarCollapseAction();
+ const newState = sidebarReducer(state, action);
+
+ expect(newState.sidebarCollapsed).toEqual(true);
+ });
+
+ it('should perform the COLLAPSE action without affecting the previous state', () => {
+ const state = { sidebarCollapsed: false };
+ deepFreeze([state]);
+
+ const action = new SearchSidebarCollapseAction();
+ sidebarReducer(state, action);
+
+ // no expect required, deepFreeze will ensure an exception is thrown if the state
+ // is mutated, and any uncaught exception will cause the test to fail
+ });
+
+ it('should set sidebarCollapsed to false in response to the EXPAND action', () => {
+ const state = { sidebarCollapsed: true };
+ const action = new SearchSidebarExpandAction();
+ const newState = sidebarReducer(state, action);
+
+ expect(newState.sidebarCollapsed).toEqual(false);
+ });
+
+ it('should perform the EXPAND action without affecting the previous state', () => {
+ const state = { sidebarCollapsed: true };
+ deepFreeze([state]);
+
+ const action = new SearchSidebarExpandAction();
+ sidebarReducer(state, action);
+ });
+
+ it('should flip the value of sidebarCollapsed in response to the TOGGLE action', () => {
+ const state1 = { sidebarCollapsed: true };
+ const action = new SearchSidebarToggleAction();
+
+ const state2 = sidebarReducer(state1, action);
+ const state3 = sidebarReducer(state2, action);
+
+ expect(state2.sidebarCollapsed).toEqual(false);
+ expect(state3.sidebarCollapsed).toEqual(true);
+ });
+
+ it('should perform the TOGGLE action without affecting the previous state', () => {
+ const state = { sidebarCollapsed: true };
+ deepFreeze([state]);
+
+ const action = new SearchSidebarToggleAction();
+ sidebarReducer(state, action);
+ });
+
+});
diff --git a/src/app/+search-page/search-sidebar/search-sidebar.reducer.ts b/src/app/+search-page/search-sidebar/search-sidebar.reducer.ts
new file mode 100644
index 0000000000..81d9069238
--- /dev/null
+++ b/src/app/+search-page/search-sidebar/search-sidebar.reducer.ts
@@ -0,0 +1,38 @@
+import { SearchSidebarAction, SearchSidebarActionTypes } from './search-sidebar.actions';
+
+export interface SearchSidebarState {
+ sidebarCollapsed: boolean;
+}
+
+const initialState: SearchSidebarState = {
+ sidebarCollapsed: true
+};
+
+export function sidebarReducer(state = initialState, action: SearchSidebarAction): SearchSidebarState {
+ switch (action.type) {
+
+ case SearchSidebarActionTypes.COLLAPSE: {
+ return Object.assign({}, state, {
+ sidebarCollapsed: true
+ });
+ }
+
+ case SearchSidebarActionTypes.EXPAND: {
+ return Object.assign({}, state, {
+ sidebarCollapsed: false
+ });
+
+ }
+
+ case SearchSidebarActionTypes.TOGGLE: {
+ return Object.assign({}, state, {
+ sidebarCollapsed: !state.sidebarCollapsed
+ });
+
+ }
+
+ default: {
+ return state;
+ }
+ }
+}
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 221c1c37d1..fe9ab3d7db 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -62,12 +62,17 @@ export class AppComponent implements OnInit {
const env: string = this.config.production ? 'Production' : 'Development';
const color: string = this.config.production ? 'red' : 'green';
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
+ this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
}
@HostListener('window:resize', ['$event'])
private onResize(event): void {
+ this.dispatchWindowSize(event.target.innerWidth, event.target.innerHeight);
+ }
+
+ private dispatchWindowSize(width, height): void {
this.store.dispatch(
- new HostWindowResizeAction(event.target.innerWidth, event.target.innerHeight)
+ new HostWindowResizeAction(width, height)
);
}
diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts
index baa3250549..b01fd62f60 100644
--- a/src/app/app.reducer.ts
+++ b/src/app/app.reducer.ts
@@ -3,15 +3,21 @@ import * as fromRouter from '@ngrx/router-store';
import { headerReducer, HeaderState } from './header/header.reducer';
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
+import {
+ SearchSidebarState,
+ sidebarReducer
+} from './+search-page/search-sidebar/search-sidebar.reducer';
export interface AppState {
router: fromRouter.RouterReducerState;
hostWindow: HostWindowState;
header: HeaderState;
+ searchSidebar: SearchSidebarState;
}
export const appReducers: ActionReducerMap
= {
router: fromRouter.routerReducer,
hostWindow: hostWindowReducer,
- header: headerReducer
+ header: headerReducer,
+ searchSidebar: sidebarReducer,
};
diff --git a/src/app/shared/animations/slide.ts b/src/app/shared/animations/slide.ts
new file mode 100644
index 0000000000..e4a1fcbb59
--- /dev/null
+++ b/src/app/shared/animations/slide.ts
@@ -0,0 +1,16 @@
+import { animate, state, transition, trigger, style } from '@angular/animations';
+
+export const slideInOut = trigger('slideInOut', [
+
+ /*
+ state('expanded', style({ right: '100%' }));
+
+ state('collapsed', style({ right: 0 }));
+*/
+
+ state('expanded', style({ left: '100%' })),
+
+ state('collapsed', style({ left: 0 })),
+
+ transition('expanded <=> collapsed', animate(250)),
+]);
diff --git a/src/app/shared/mocks/mock-host-window-service.ts b/src/app/shared/mocks/mock-host-window-service.ts
index 104e712682..e7d108407b 100644
--- a/src/app/shared/mocks/mock-host-window-service.ts
+++ b/src/app/shared/mocks/mock-host-window-service.ts
@@ -16,4 +16,8 @@ export class MockHostWindowService {
isXs(): Observable {
return Observable.of(this.width < 576);
}
+
+ isSm(): Observable {
+ return Observable.of(this.width < 768);
+ }
}
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 5b6146b7a4..6e05647d53 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -30,6 +30,7 @@ import { SearchResultListElementComponent } from '../object-list/search-result-l
import { SearchFormComponent } from './search-form/search-form.component';
import { WrapperListElementComponent } from '../object-list/wrapper-list-element/wrapper-list-element.component';
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
+import { ScrollAndStickDirective } from './utils/scroll-and-stick.directive';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -66,6 +67,10 @@ const COMPONENTS = [
ViewModeSwitchComponent
];
+const DIRECTIVES = [
+ ScrollAndStickDirective,
+];
+
const ENTRY_COMPONENTS = [
// put shared entry components (components that are created dynamically) here
CollectionListElementComponent,
@@ -81,12 +86,14 @@ const ENTRY_COMPONENTS = [
declarations: [
...PIPES,
...COMPONENTS,
- ...ENTRY_COMPONENTS
+ ...ENTRY_COMPONENTS,
+ ...DIRECTIVES
],
exports: [
...MODULES,
...PIPES,
- ...COMPONENTS
+ ...COMPONENTS,
+ ...DIRECTIVES
],
entryComponents: [
...ENTRY_COMPONENTS
diff --git a/src/app/shared/utils/scroll-and-stick.directive.ts b/src/app/shared/utils/scroll-and-stick.directive.ts
new file mode 100644
index 0000000000..4ec99d8db6
--- /dev/null
+++ b/src/app/shared/utils/scroll-and-stick.directive.ts
@@ -0,0 +1,40 @@
+
+import { NativeWindowRef, NativeWindowService } from '../window.service';
+import { Observable } from 'rxjs/Observable';
+import { AfterViewInit, Directive, ElementRef, Inject } from '@angular/core';
+
+@Directive({
+ selector: '[dsStick]'
+})
+export class ScrollAndStickDirective implements AfterViewInit {
+
+ private initialY: number;
+
+ constructor(private _element: ElementRef, @Inject(NativeWindowService) private _window: NativeWindowRef) {
+ this.subscribeForScrollEvent();
+ }
+
+ ngAfterViewInit(): void {
+ this.initialY = this._element.nativeElement.getBoundingClientRect().top;
+ }
+
+ subscribeForScrollEvent() {
+
+ const obs = Observable.fromEvent(window, 'scroll');
+
+ obs.subscribe((e) => this.handleScrollEvent(e));
+ }
+
+ handleScrollEvent(e) {
+
+ if (this._window.nativeWindow.pageYOffset >= this.initialY) {
+
+ this._element.nativeElement.classList.add('stick');
+
+ } else {
+
+ this._element.nativeElement.classList.remove('stick');
+
+ }
+ }
+}