45621: filter components for sidebar

This commit is contained in:
Lotte Hofstede
2017-11-06 14:27:48 +01:00
parent e14e8fac37
commit 532741d073
17 changed files with 195 additions and 107 deletions

View File

@@ -93,7 +93,19 @@
},
"filters": {
"head": "Filters",
"reset": "Reset filters"
"reset": "Reset filters",
"facet-filter": {
"show-more": "Show more",
"author": {
"placeholder": "Author name"
},
"scope": {
"placeholder": "Scope filter"
},
"subject": {
"placeholder": "Subject"
}
}
}
},
"loading": {

View File

@@ -0,0 +1,8 @@
<a *ngFor="let value of filterValues" class="d-block" [routerLink]="[getSearchLink()]" [queryParams]="getQueryParams(value) | async">
<input type="checkbox" [checked]="isChecked(value)"/>
<span class="filter-value">{{value.value}}</span>
<span class="filter-value-count float-right">({{value.count}})</span>
</a>
<a href="">{{"search.filters.facet-filter.show-more" | translate}}</a>
<input type="text" [placeholder]="'search.filters.facet-filter.' + filterConfig.name + '.placeholder'| translate"/>

View File

@@ -0,0 +1,2 @@
@import '../../../../../styles/variables.scss';
@import '../../../../../styles/mixins.scss';

View File

@@ -0,0 +1,41 @@
import { Component, Input } 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 { Observable } from 'rxjs/Observable';
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template.
*/
@Component({
selector: 'ds-search-facet-filter',
styleUrls: ['./search-facet-filter.component.scss'],
templateUrl: './search-facet-filter.component.html',
})
export class SidebarFacetFilterComponent {
@Input() filterValues: FacetValue[];
@Input() filterConfig: SearchFilterConfig;
constructor(private searchService: SearchService, private route: ActivatedRoute) {
}
isChecked(value: FacetValue) {
return this.searchService.isFilterActive(this.filterConfig.name, value.value);
}
getSearchLink() {
return this.searchService.getSearchLink();
}
getQueryParams(value: FacetValue): Observable<any> {
const params = {};
params[this.filterConfig.paramName] = value.value;
return this.route.queryParams.map((p) => Object.assign({}, p, params))
}
}

View File

@@ -0,0 +1,4 @@
<div>
<div (click)="toggle()" class="filter-name">{{filter.name}} <span class="fa float-right" [ngClass]="isCollapsed ? 'fa-plus' : 'fa-minus'"></span></div>
<ds-search-facet-filter [filterConfig]="filter" [filterValues]="filterValues.payload | async"></ds-search-facet-filter>
</div>

View File

@@ -0,0 +1,2 @@
@import '../../../../styles/variables.scss';
@import '../../../../styles/mixins.scss';

View File

@@ -0,0 +1,34 @@
import { Component, Input, OnInit } from '@angular/core';
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
import { SearchService } from '../../search-service/search.service';
import { RemoteData } from '../../../core/data/remote-data';
import { FacetValue } from '../../search-service/facet-value.model';
/**
* This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template.
*/
@Component({
selector: 'ds-search-filter',
styleUrls: ['./search-filter.component.scss'],
templateUrl: './search-filter.component.html',
})
export class SidebarFilterComponent implements OnInit {
@Input() filter: SearchFilterConfig;
filterValues: RemoteData<FacetValue[]>;
isCollapsed = false;
constructor(private searchService: SearchService) {
}
ngOnInit() {
this.filterValues = this.searchService.getFacetValuesFor(this.filter.name);
}
toggle() {
this.isCollapsed = !this.isCollapsed;
}
}

View File

@@ -1,7 +1,7 @@
<h2>{{"search.filters.head" | translate}}</h2>
<div>
<div *ngFor="let filter of filters">
<div *ngIf="filters.hasSucceeded | async">
<div *ngFor="let filter of (filters.payload | async)">
<ds-search-filter [filter]="filter"></ds-search-filter>
</div>
</div>
<a class="btn btn-primary" [href]="getClearFiltersLink() | async" role="button">{{"search.filters.reset" | translate}}</a>
<a class="btn btn-primary" [routerLink]="[getSearchLink()]" [queryParams]="getClearFiltersQueryParams()" role="button">{{"search.filters.reset" | translate}}</a>

View File

@@ -1,6 +1,7 @@
import { Component, Input } from '@angular/core';
import { SearchService } from '../search-service/search.service';
import { Observable } from 'rxjs/Observable';
import { RemoteData } from '../../core/data/remote-data';
import { SearchFilterConfig } from '../search-service/search-filter-config.model';
/**
* This component renders a simple item page.
@@ -15,10 +16,16 @@ import { Observable } from 'rxjs/Observable';
})
export class SidebarFiltersComponent {
@Input() filters;
constructor(private searchService: SearchService) {}
filters: RemoteData<SearchFilterConfig[]>;
constructor(private searchService: SearchService) {
this.filters = searchService.getConfig();
}
getClearFiltersLink(): Observable<string> {
return this.searchService.getClearFiltersLink();
getClearFiltersQueryParams(): any {
return this.searchService.getClearFiltersQueryParams();
}
getSearchLink() {
return this.searchService.getSearchLink();
}
}

View File

@@ -1,26 +1,26 @@
<div class="container">
<div class="search-page">
<ds-search-sidebar dsStick *ngIf="!(isMobileView | async)" class="col-3 sidebar-sm-fixed"
<div class="search-page row">
<ds-search-sidebar *ngIf="!(isMobileView | async)" class="col-3 sidebar-sm-sticky"
id="search-sidebar"
resultCount="{{(results.pageInfo | async)?.totalElements}}"></ds-search-sidebar>
<div id="search-header" class="row">
<ds-search-form id="search-form" class="col-12 col-sm-9 ml-sm-auto"
<div class="col-12 col-sm-9">
<ds-search-form id="search-form"
[query]="query"
[scope]="scopeObject?.payload | async"
[currentParams]="currentParams"
[scopes]="scopeList?.payload">
</ds-search-form>
</div>
<div class="row">
<div id="search-body"
class="row-offcanvas row-offcanvas-left"
[@slideInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
<ds-search-sidebar *ngIf="(isMobileView | async)" class="col-12" id="search-sidebar-xs"
<ds-search-sidebar *ngIf="(isMobileView | async)" class="col-12"
id="search-sidebar-xs"
resultCount="{{(results.pageInfo | async)?.totalElements}}"
(toggleSidebar)="closeSidebar()" [ngClass]="{'active': !(isSidebarCollapsed() | async)}"></ds-search-sidebar>
<div id="search-content" class="col-12 col-sm-9 ml-sm-auto">
(toggleSidebar)="closeSidebar()"
[ngClass]="{'active': !(isSidebarCollapsed() | async)}"></ds-search-sidebar>
<div id="search-content" class="col-12">
<div class="d-block d-sm-none search-controls clearfix">
<ds-view-mode-switch></ds-view-mode-switch>
<button (click)="openSidebar()" aria-controls="#search-body"
class="btn btn-outline-primary float-right open-sidebar"><i
@@ -34,5 +34,6 @@
</div>
</div>
</div>
</div>
</div>

View File

@@ -5,12 +5,6 @@
position: relative;
}
#search-content, #search-form {
display: block;
@include media-breakpoint-down(xs) {
margin-left: 0;
}
}
/deep/ .search-controls {
margin-bottom: $spacer;
@@ -43,15 +37,15 @@
}
}
.sidebar-sm-fixed {
.sidebar-sm-sticky{
@include media-breakpoint-up(sm) {
position: absolute;
margin-top: -$content-spacing;
padding-top: $content-spacing;
&.stick {
position: sticky;
position: -webkit-sticky;
top: 0;
margin-top: 0px;
position: fixed;
}
z-index: $zindex-sticky;
padding-top: $content-spacing;
margin-top: -$content-spacing;
align-self: flex-start;
display: block;
}
}

View File

@@ -13,6 +13,8 @@ import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
import { SearchSidebarEffects } from './search-sidebar/search-sidebar.effects';
import { EffectsModule } from '@ngrx/effects';
import { SidebarFiltersComponent } from './search-filters/search-filters.component';
import { SidebarFilterComponent } from './search-filters/search-filter/search-filter.component';
import { SidebarFacetFilterComponent } from './search-filters/search-filter/search-facet-filter/search-facet-filter.component';
const effects = [
SearchSidebarEffects
@@ -32,7 +34,9 @@ const effects = [
ItemSearchResultListElementComponent,
CollectionSearchResultListElementComponent,
CommunitySearchResultListElementComponent,
SidebarFiltersComponent
SidebarFiltersComponent,
SidebarFilterComponent,
SidebarFacetFilterComponent
],
providers: [
SearchService,

View File

@@ -3,7 +3,6 @@ import { RouterTestingModule } from '@angular/router/testing';
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { SearchService } from './search.service';
import { ItemDataService } from './../../core/data/item-data.service';

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { Observable } from 'rxjs/Observable';
import { SearchResult } from '../search-result.model';
@@ -31,7 +31,7 @@ function shuffle(array: any[]) {
}
@Injectable()
export class SearchService {
export class SearchService implements OnDestroy {
totalPages = 5;
mockedHighlights: string[] = new Array(
@@ -46,6 +46,8 @@ export class SearchService {
'<em>This was blank in the actual item, no abstract</em>',
'<em>The QSAR DataBank (QsarDB) repository</em>',
);
private sub;
searchLink = '/search';
config: SearchFilterConfig[] = [
Object.assign(new SearchFilterConfig(),
@@ -170,13 +172,16 @@ export class SearchService {
}
getFacetValuesFor(searchFilterConfigName: string): RemoteData<FacetValue[]> {
const filterConfig = this.config.find((config: SearchFilterConfig) => config.name === searchFilterConfigName);
const values: FacetValue[] = [];
for (let i = 0; i < 5; i++) {
const value = searchFilterConfigName + ' ' + (i + 1);
values.push({
value: value,
count: Math.floor(Math.random() * 20) + 20 * (5 - i), // make sure first results have the highest (random) count
search: 'https://dspace7.4science.it/dspace-spring-rest/api/search?f.' + searchFilterConfigName + '=' + encodeURI(value)
search: decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value
});
}
const requestPending = Observable.of(false);
@@ -213,17 +218,36 @@ export class SearchService {
queryParamsHandling: 'merge'
};
this.router.navigate(['/search'], navigationExtras);
this.router.navigate([this.searchLink], navigationExtras);
}
getClearFiltersLink(): Observable<string> {
const url = '/search?';
return this.route.queryParamMap
.map((map) => { return url.concat(map.keys
getClearFiltersQueryParams(): any {
const params = {};
this.sub = this.route.queryParamMap
.subscribe((map) => {
map.keys
.filter((key) => this.config
.findIndex((conf: SearchFilterConfig) => conf.paramName === key) < 0)
.map((key) => { return key + '=' + map.get(key) })
.join('&'))})
.first();
.forEach((key) => {
params[key] = map.get(key);
})
});
return params;
}
isFilterActive(filterName: string, filterValue: string): boolean {
const filterConfig = this.config.find((config: SearchFilterConfig) => config.name === filterName);
return isNotEmpty(this.router.url.match(filterConfig.paramName + '=' + encodeURI(filterValue) + '(&(.*))?$'));
}
getSearchLink() {
return this.searchLink;
}
ngOnDestroy(): void {
if (this.sub !== undefined) {
this.sub.unsubscribe();
}
}
}

View File

@@ -1,4 +1,4 @@
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)" class="row">
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)" class="row" action="/search">
<div *ngIf="isNotEmpty(scopes | async)" class="col-12 col-sm-3">
<select [(ngModel)]="selectedId" name="scope" class="form-control" aria-label="Search scope" [compareWith]="byId">
<option value>{{'search.form.search_dspace' | translate}}</option>

View File

@@ -30,7 +30,6 @@ 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
@@ -68,7 +67,6 @@ const COMPONENTS = [
];
const DIRECTIVES = [
ScrollAndStickDirective,
];
const ENTRY_COMPONENTS = [

View File

@@ -1,42 +0,0 @@
import { NativeWindowRef, NativeWindowService } from '../window.service';
import { Observable } from 'rxjs/Observable';
import { AfterViewInit, Directive, ElementRef, Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Directive({
selector: '[dsStick]'
})
export class ScrollAndStickDirective implements AfterViewInit {
private initialY: number;
constructor(private _element: ElementRef, @Inject(NativeWindowService) private _window: NativeWindowRef, @Inject(PLATFORM_ID) private platformId) {
if (isPlatformBrowser(platformId)) {
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');
}
}
}