diff --git a/resources/i18n/en.json b/resources/i18n/en.json index c803cda073..bb02c8051c 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -96,14 +96,18 @@ "reset": "Reset filters", "facet-filter": { "show-more": "Show more", + "show-less": "Show less", "author": { - "placeholder": "Author name" + "placeholder": "Author name", + "head": "Author" }, "scope": { - "placeholder": "Scope filter" + "placeholder": "Scope filter", + "head": "Scope" }, "subject": { - "placeholder": "Subject" + "placeholder": "Subject", + "head": "Subject" } } } diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html index 7d1f7f2e89..f54883011a 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.html @@ -1,8 +1,15 @@ - - - {{value.value}} - ({{value.count}}) + + + + {{value.value}} + ({{value.count}}) + -{{"search.filters.facet-filter.show-more" | translate}} +{{"search.filters.facet-filter.show-more" + | translate}} +{{"search.filters.facet-filter.show-less" | + translate}} - \ No newline at end of file + \ No newline at end of file diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index 2565c11945..a817eb0d9b 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -1,9 +1,10 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { FacetValue } from '../../../search-service/facet-value.model'; import { SearchFilterConfig } from '../../../search-service/search-filter-config.model'; import { SearchService } from '../../../search-service/search.service'; -import { ActivatedRoute } from '@angular/router'; +import { Params } from '@angular/router'; import { Observable } from 'rxjs/Observable'; +import { SearchFilterService } from '../search-filter.service'; /** * This component renders a simple item page. @@ -17,25 +18,48 @@ import { Observable } from 'rxjs/Observable'; templateUrl: './search-facet-filter.component.html', }) -export class SidebarFacetFilterComponent { +export class SidebarFacetFilterComponent implements OnInit { @Input() filterValues: FacetValue[]; @Input() filterConfig: SearchFilterConfig; + currentPage: Observable; - constructor(private searchService: SearchService, private route: ActivatedRoute) { + constructor(private filterService: SearchFilterService) { + } + + ngOnInit(): void { + this.currentPage = this.filterService.getPage(this.filterConfig.name); } isChecked(value: FacetValue) { - return this.searchService.isFilterActive(this.filterConfig.name, value.value); + return this.filterService.isFilterActive(this.filterConfig.name, value.value); } getSearchLink() { - return this.searchService.getSearchLink(); + return this.filterService.searchLink; } - getQueryParams(value: FacetValue): Observable { - const params = {}; - params[this.filterConfig.paramName] = value.value; - return this.route.queryParams.map((p) => Object.assign({}, p, params)) + getQueryParams(value: FacetValue): Params { + return this.filterService.switchFilterInURL(this.filterConfig, value.value); } + get facetCount(): Observable { + const resultCount = this.filterValues.length; + return this.currentPage.map((page: number) => { + const max = page * this.filterConfig.pageSize; + return max > resultCount ? resultCount : max; + }); + } + + showMore() { + this.filterService.increasePage(this.filterConfig.name); + } + + showLess() { + this.filterService.decreasePage(this.filterConfig.name); + } + + getCurrentPage(): Observable { + return this.filterService.getPage(this.filterConfig.name); + + } } diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.actions.ts b/src/app/+search-page/search-filters/search-filter/search-filter.actions.ts new file mode 100644 index 0000000000..89a29de527 --- /dev/null +++ b/src/app/+search-page/search-filters/search-filter/search-filter.actions.ts @@ -0,0 +1,58 @@ +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 SearchFilterActionTypes = { + COLLAPSE: type('dspace/search-filter/COLLAPSE'), + INITIAL_COLLAPSE: type('dspace/search-filter/INITIAL_COLLAPSE'), + EXPAND: type('dspace/search-filter/EXPAND'), + INITIAL_EXPAND: type('dspace/search-filter/INITIAL_EXPAND'), + TOGGLE: type('dspace/search-filter/TOGGLE'), + DECREASE_PAGE: type('dspace/search-filter/DECREASE_PAGE'), + INCREASE_PAGE: type('dspace/search-filter/INCREASE_PAGE') +}; + +export class SearchFilterAction implements Action { + filterName: string; + type; + constructor(name: string) { + this.filterName = name; + } +} + +/* tslint:disable:max-classes-per-file */ +export class SearchFilterCollapseAction extends SearchFilterAction { + type = SearchFilterActionTypes.COLLAPSE; +} + +export class SearchFilterExpandAction extends SearchFilterAction { + type = SearchFilterActionTypes.EXPAND; +} + +export class SearchFilterToggleAction extends SearchFilterAction { + type = SearchFilterActionTypes.TOGGLE; +} + +export class SearchFilterInitialCollapseAction extends SearchFilterAction { + type = SearchFilterActionTypes.INITIAL_COLLAPSE; +} + +export class SearchFilterInitialExpandAction extends SearchFilterAction { + type = SearchFilterActionTypes.INITIAL_EXPAND; +} +export class SearchFilterDecreasePageAction extends SearchFilterAction { + type = SearchFilterActionTypes.DECREASE_PAGE; +} + +export class SearchFilterIncreasePageAction extends SearchFilterAction { + type = SearchFilterActionTypes.INCREASE_PAGE; +} +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-filter.component.html index 2279564359..c8fd3ff10c 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.component.html +++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.html @@ -1,4 +1,8 @@
-
{{filter.name}}
- +
{{filter.name}}
+
+ +
\ No newline at end of file diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.scss b/src/app/+search-page/search-filters/search-filter/search-filter.component.scss index 3e77826dc2..93be5d1c05 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.component.scss +++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.scss @@ -1,2 +1,6 @@ @import '../../../../styles/variables.scss'; -@import '../../../../styles/mixins.scss'; \ No newline at end of file +@import '../../../../styles/mixins.scss'; + +.search-filter-wrapper { + overflow: hidden; +} \ No newline at end of file diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts index c26efd41ad..cbffe6a8e9 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts @@ -3,6 +3,9 @@ import { SearchFilterConfig } from '../../search-service/search-filter-config.mo import { SearchService } from '../../search-service/search.service'; import { RemoteData } from '../../../core/data/remote-data'; import { FacetValue } from '../../search-service/facet-value.model'; +import { SearchFilterService } from './search-filter.service'; +import { Observable } from 'rxjs/Observable'; +import { slide } from '../../../shared/animations/slide'; /** * This component renders a simple item page. @@ -14,21 +17,38 @@ import { FacetValue } from '../../search-service/facet-value.model'; selector: 'ds-search-filter', styleUrls: ['./search-filter.component.scss'], templateUrl: './search-filter.component.html', + animations: [slide] }) export class SidebarFilterComponent implements OnInit { @Input() filter: SearchFilterConfig; filterValues: RemoteData; - isCollapsed = false; - constructor(private searchService: SearchService) { + constructor(private searchService: SearchService, private filterService: SearchFilterService) { } ngOnInit() { this.filterValues = this.searchService.getFacetValuesFor(this.filter.name); + if (this.filter.isOpenByDefault) { + this.initialExpand(); + } else { + this.initialCollapse(); + } } toggle() { - this.isCollapsed = !this.isCollapsed; + this.filterService.toggle(this.filter.name); } -} \ No newline at end of file + + isCollapsed(): Observable { + return this.filterService.isCollapsed(this.filter.name); + } + + initialCollapse() { + this.filterService.initialCollapse(this.filter.name); + } + + initialExpand() { + this.filterService.initialExpand(this.filter.name); + } +} diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.reducer.ts b/src/app/+search-page/search-filters/search-filter/search-filter.reducer.ts new file mode 100644 index 0000000000..30892c4c97 --- /dev/null +++ b/src/app/+search-page/search-filters/search-filter/search-filter.reducer.ts @@ -0,0 +1,95 @@ +import { SearchFilterAction, SearchFilterActionTypes } from './search-filter.actions'; +import { isEmpty } from '../../../shared/empty.util'; + +export interface SearchFilterState { + filterCollapsed: boolean, + page: number +} + +export interface SearchFiltersState { + [name: string]: SearchFilterState +} + +const initialState: SearchFiltersState = Object.create(null); + +export function filterReducer(state = initialState, action: SearchFilterAction): SearchFiltersState { + + switch (action.type) { + + case SearchFilterActionTypes.INITIAL_COLLAPSE: { + if (isEmpty(state) || isEmpty(state[action.filterName])) { + return Object.assign({}, state, { + [action.filterName]: { + filterCollapsed: true, + page: 1 + } + }); + } + return state; + } + + case SearchFilterActionTypes.INITIAL_EXPAND: { + if (isEmpty(state) || isEmpty(state[action.filterName])) { + return Object.assign({}, state, { + [action.filterName]: { + filterCollapsed: false, + page: 1 + } + }); + } + return state; + } + + case SearchFilterActionTypes.COLLAPSE: { + return Object.assign({}, state, { + [action.filterName]: { + filterCollapsed: true, + page: state[action.filterName].page + } + }); + } + + case SearchFilterActionTypes.EXPAND: { + return Object.assign({}, state, { + [action.filterName]: { + filterCollapsed: false, + page: state[action.filterName].page + } + }); + + } + + case SearchFilterActionTypes.DECREASE_PAGE: { + return Object.assign({}, state, { + [action.filterName]: { + filterCollapsed: state[action.filterName].filterCollapsed, + page: state[action.filterName].page - 1 + } + }); + } + + case SearchFilterActionTypes.INCREASE_PAGE: { + return Object.assign({}, state, { + [action.filterName]: { + filterCollapsed: state[action.filterName].filterCollapsed, + page: state[action.filterName].page + 1 + } + }); + + } + + case SearchFilterActionTypes.TOGGLE: { + return Object.assign({}, state, { + [action.filterName]: { + filterCollapsed: !state[action.filterName].filterCollapsed, + page: state[action.filterName].page + } + }); + + } + + default: { + return state; + } + } +} diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts new file mode 100644 index 0000000000..258f54aa78 --- /dev/null +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts @@ -0,0 +1,136 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { SearchFiltersState, SearchFilterState } from './search-filter.reducer'; +import { createSelector, MemoizedSelector, Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import { AppState } from '../../../app.reducer'; +import { + SearchFilterCollapseAction, SearchFilterDecreasePageAction, SearchFilterIncreasePageAction, + SearchFilterInitialCollapseAction, + SearchFilterInitialExpandAction, + SearchFilterToggleAction +} from './search-filter.actions'; +import { hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { ActivatedRoute, Params, Router } from '@angular/router'; +import { SearchFilterConfig } from '../../search-service/search-filter-config.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { FacetValue } from '../../search-service/facet-value.model'; +import { FilterType } from '../../search-service/filter-type.model'; +import { SearchService } from '../../search-service/search.service'; + +const filterStateSelector = (state: SearchFiltersState) => state.searchFilter; + +@Injectable() +export class SearchFilterService implements OnDestroy { + private sub; + + constructor(private store: Store, + private route: ActivatedRoute, + private router: Router, + private searchService: SearchService) { + } + + isFilterActive(filterName: string, filterValue: string): boolean { + let filterConfig: SearchFilterConfig; + this.sub = this.searchService.getConfig().payload + .subscribe((configuration) => filterConfig = configuration + .find((config: SearchFilterConfig) => config.name === filterName)); + return isNotEmpty(this.route.snapshot.queryParams[filterConfig.paramName]) && [...this.route.snapshot.queryParams[filterConfig.paramName]].indexOf(filterValue, 0) > -1; + } + + switchFilterInURL(filterConfig: SearchFilterConfig, value: string) { + console.log(this.route.snapshot.queryParams); + if (this.isFilterActive(filterConfig.name, value)) { + return this.removeQueryParameter(filterConfig.paramName, value); + } else { + return this.addQueryParameter(filterConfig.paramName, value); + } + } + + addQueryParameter(paramName: string, value: string): Params { + const currentParams = this.route.snapshot.queryParams; + const newParam = {}; + if ((currentParams[paramName])) { + newParam[paramName] = [...currentParams[paramName], value]; + } else { + newParam[paramName] = [value]; + } + return Object.assign({}, currentParams, newParam); + } + + removeQueryParameter(paramName: string, value: string): Params { + const currentParams = this.route.snapshot.queryParams; + const newParam = {}; + let currentFilterParams = [...currentParams[paramName]]; + if (isNotEmpty(currentFilterParams)) { + const index = currentFilterParams.indexOf(value, 0); + if (index > -1) { + currentFilterParams = currentFilterParams.splice(index, 1); + } + newParam[paramName] = currentFilterParams; + } + return Object.assign({}, currentParams, newParam); + } + + get searchLink() { + return this.searchService.searchLink; + } + + isCollapsed(filterName: string): Observable { + return this.store.select(filterByNameSelector(filterName)) + .map((object: SearchFilterState) => object.filterCollapsed); + } + + getPage(filterName: string): Observable { + return this.store.select(filterByNameSelector(filterName)) + .map((object: SearchFilterState) => object.page); + } + + public collapse(filterName: string): void { + this.store.dispatch(new SearchFilterCollapseAction(filterName)); + } + + public expand(filterName: string): void { + this.store.dispatch(new SearchFilterCollapseAction(filterName)); + } + + public toggle(filterName: string): void { + this.store.dispatch(new SearchFilterToggleAction(filterName)); + } + + public initialCollapse(filterName: string): void { + this.store.dispatch(new SearchFilterInitialCollapseAction(filterName)); + } + + public initialExpand(filterName: string): void { + this.store.dispatch(new SearchFilterInitialExpandAction(filterName)); + } + + public decreasePage(filterName: string): void { + this.store.dispatch(new SearchFilterDecreasePageAction(filterName)); + } + + public increasePage(filterName: string): void { + this.store.dispatch(new SearchFilterIncreasePageAction(filterName)); + } + + ngOnDestroy(): void { + if (this.sub !== undefined) { + this.sub.unsubscribe(); + } + } +} + +function filterByNameSelector(name: string): MemoizedSelector { + return keySelector(name); +} + +export function keySelector(key: string): MemoizedSelector { + return createSelector(filterStateSelector, (state: SearchFilterState) => { + if (hasValue(state)) { + return state[key]; + } else { + return undefined; + } + }); +} diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html index 25bd741415..881efbf0c1 100644 --- a/src/app/+search-page/search-page.component.html +++ b/src/app/+search-page/search-page.component.html @@ -13,7 +13,7 @@
+ [@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'"> config.name === filterName); - return isNotEmpty(this.router.url.match(filterConfig.paramName + '=' + encodeURI(filterValue) + '(&(.*))?$')); - } - getSearchLink() { return this.searchLink; } @@ -249,5 +244,4 @@ export class SearchService implements OnDestroy { this.sub.unsubscribe(); } } - } diff --git a/src/app/+search-page/search-sidebar/search-sidebar.effects.ts b/src/app/+search-page/search-sidebar/search-sidebar.effects.ts index 71d928bd5f..2894796695 100644 --- a/src/app/+search-page/search-sidebar/search-sidebar.effects.ts +++ b/src/app/+search-page/search-sidebar/search-sidebar.effects.ts @@ -2,7 +2,6 @@ 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() diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index b01fd62f60..72b29519de 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -7,12 +7,17 @@ import { SearchSidebarState, sidebarReducer } from './+search-page/search-sidebar/search-sidebar.reducer'; +import { + filterReducer, + SearchFiltersState +} from './+search-page/search-filters/search-filter/search-filter.reducer'; export interface AppState { router: fromRouter.RouterReducerState; hostWindow: HostWindowState; header: HeaderState; searchSidebar: SearchSidebarState; + searchFilter: SearchFiltersState; } export const appReducers: ActionReducerMap = { @@ -20,4 +25,5 @@ export const appReducers: ActionReducerMap = { hostWindow: hostWindowReducer, header: headerReducer, searchSidebar: sidebarReducer, + searchFilter: filterReducer }; diff --git a/src/app/shared/animations/push.ts b/src/app/shared/animations/push.ts new file mode 100644 index 0000000000..124e3289e7 --- /dev/null +++ b/src/app/shared/animations/push.ts @@ -0,0 +1,16 @@ +import { animate, state, transition, trigger, style } from '@angular/animations'; + +export const pushInOut = trigger('pushInOut', [ + + /* + 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/animations/slide.ts b/src/app/shared/animations/slide.ts index e4a1fcbb59..381f592168 100644 --- a/src/app/shared/animations/slide.ts +++ b/src/app/shared/animations/slide.ts @@ -1,16 +1,10 @@ import { animate, state, transition, trigger, style } from '@angular/animations'; -export const slideInOut = trigger('slideInOut', [ +export const slide = trigger('slide', [ - /* - state('expanded', style({ right: '100%' })); + state('expanded', style({ height: '*' })), - state('collapsed', style({ right: 0 })); -*/ - - state('expanded', style({ left: '100%' })), - - state('collapsed', style({ left: 0 })), + state('collapsed', style({ height: 0 })), transition('expanded <=> collapsed', animate(250)), ]);