From feebed2c567898cd2d125d0c839ecea239cd02d5 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Thu, 19 Aug 2021 17:24:03 +0300 Subject: [PATCH] add popup filters --- src/assets/styles/_variables.scss | 1 + src/index.ts | 2 + src/lib/common-ui.module.ts | 15 ++- src/lib/filtering/filter.service.ts | 3 +- .../popup-filter/popup-filter.component.html | 109 +++++++++++++++++ .../popup-filter/popup-filter.component.scss | 41 +++++++ .../popup-filter/popup-filter.component.ts | 111 ++++++++++++++++++ .../quick-filters.component.scss | 2 +- src/lib/utils/operators.ts | 10 ++ 9 files changed, 290 insertions(+), 4 deletions(-) create mode 100644 src/lib/filtering/popup-filter/popup-filter.component.html create mode 100644 src/lib/filtering/popup-filter/popup-filter.component.scss create mode 100644 src/lib/filtering/popup-filter/popup-filter.component.ts create mode 100644 src/lib/utils/operators.ts diff --git a/src/assets/styles/_variables.scss b/src/assets/styles/_variables.scss index 96c6bcb..073ea5d 100644 --- a/src/assets/styles/_variables.scss +++ b/src/assets/styles/_variables.scss @@ -7,3 +7,4 @@ $warn: #fdbd00 !default; $white: white !default; $separator: rgba(226, 228, 233, 0.9) !default; $quick-filter-border: #d3d5da !default; +$filter-bg: #f4f5f7 !default; diff --git a/src/index.ts b/src/index.ts index 1ceca69..c9c5448 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ export * from './lib/common-ui.module'; export * from './lib/buttons/icon-button/icon-button.type'; export * from './lib/buttons/icon-button/icon-button.component'; export * from './lib/utils/functions'; +export * from './lib/utils/operators'; export * from './lib/utils/auto-unsubscribe.directive'; export * from './lib/utils/pipes/humanize.pipe'; export * from './lib/utils/types/utility-types'; @@ -16,6 +17,7 @@ export * from './lib/filtering/filter.service'; export * from './lib/filtering/models/filter.model'; export * from './lib/filtering/models/filter-group.model'; export * from './lib/filtering/models/nested-filter.model'; +export * from './lib/filtering/popup-filter/popup-filter.component'; export * from './lib/filtering/quick-filters/quick-filters.component'; export * from './lib/sorting/sort-by.pipe'; export * from './lib/sorting/sorting.service'; diff --git a/src/lib/common-ui.module.ts b/src/lib/common-ui.module.ts index a0d40b8..f74b5c3 100644 --- a/src/lib/common-ui.module.ts +++ b/src/lib/common-ui.module.ts @@ -6,6 +6,8 @@ import { DomSanitizer } from '@angular/platform-browser'; import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslateModule } from '@ngx-translate/core'; import { FormsModule } from '@angular/forms'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { IconButtonComponent } from './buttons/icon-button/icon-button.component'; import { ChevronButtonComponent } from './buttons/chevron-button/chevron-button.component'; import { CircleButtonComponent } from './buttons/circle-button/circle-button.component'; @@ -18,16 +20,25 @@ import { TableHeaderComponent } from './tables/table-header/table-header.compone import { SyncWidthDirective } from './tables/sync-width.directive'; import { StatusBarComponent } from './misc/status-bar/status-bar.component'; import { EditableInputComponent } from './inputs/editable-input/editable-input.component'; +import { PopupFilterComponent } from './filtering/popup-filter/popup-filter.component'; const buttons = [IconButtonComponent, ChevronButtonComponent, CircleButtonComponent]; const inputs = [RoundCheckboxComponent, EditableInputComponent]; -const matModules = [MatIconModule, MatButtonModule, MatTooltipModule]; +const matModules = [MatIconModule, MatButtonModule, MatTooltipModule, MatMenuModule, MatCheckboxModule]; const modules = [...matModules, FormsModule, TranslateModule]; -const components = [...buttons, ...inputs, TableColumnNameComponent, QuickFiltersComponent, TableHeaderComponent, StatusBarComponent]; +const components = [ + ...buttons, + ...inputs, + TableColumnNameComponent, + QuickFiltersComponent, + PopupFilterComponent, + TableHeaderComponent, + StatusBarComponent +]; const utils = [SortByPipe, HumanizePipe, SyncWidthDirective]; diff --git a/src/lib/filtering/filter.service.ts b/src/lib/filtering/filter.service.ts index 60de9d2..e8bdd48 100644 --- a/src/lib/filtering/filter.service.ts +++ b/src/lib/filtering/filter.service.ts @@ -4,6 +4,7 @@ import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators' import { processFilters, toFlatFilters } from './filter-utils'; import { FilterGroup } from './models/filter-group.model'; import { NestedFilter } from './models/nested-filter.model'; +import { get } from '../utils/operators'; @Injectable() export class FilterService { @@ -61,7 +62,7 @@ export class FilterService { } getGroup$(slug: string): Observable { - return this.filterGroups$.pipe(map(all => all.find(f => f.slug === slug))); + return this.filterGroups$.pipe(get(group => group.slug === slug)); } reset(): void { diff --git a/src/lib/filtering/popup-filter/popup-filter.component.html b/src/lib/filtering/popup-filter/popup-filter.component.html new file mode 100644 index 0000000..335429b --- /dev/null +++ b/src/lib/filtering/popup-filter/popup-filter.component.html @@ -0,0 +1,109 @@ + + + + + + + +
+
+
+
+
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ + + {{ filter?.label }} + + + +
+
+ + +
+
 
+ + + + +
+ +
+
+ + + + + +
+
+
+
diff --git a/src/lib/filtering/popup-filter/popup-filter.component.scss b/src/lib/filtering/popup-filter/popup-filter.component.scss new file mode 100644 index 0000000..1a48d2f --- /dev/null +++ b/src/lib/filtering/popup-filter/popup-filter.component.scss @@ -0,0 +1,41 @@ +@import '../../../assets/styles/variables'; + +.filter-menu-options, +.filter-menu-header { + display: flex; + justify-content: space-between; + padding: 8px 16px 16px 16px; + width: 350px; + + .actions { + display: flex; + + > *:not(:last-child) { + margin-right: 8px; + } + } +} + +.filter-content { + max-height: 570px; + overflow: auto; +} + +.filter-menu-options { + margin-top: 8px; + padding: 16px 16px 3px; +} + +.filter-options { + background-color: $filter-bg; + padding-bottom: 8px; +} + +::ng-deep .filter-menu-checkbox { + width: 100%; + + label { + width: 100%; + height: 100%; + } +} diff --git a/src/lib/filtering/popup-filter/popup-filter.component.ts b/src/lib/filtering/popup-filter/popup-filter.component.ts new file mode 100644 index 0000000..f504f4b --- /dev/null +++ b/src/lib/filtering/popup-filter/popup-filter.component.ts @@ -0,0 +1,111 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit, TemplateRef } from '@angular/core'; +import { FilterGroup, FilterService, handleCheckedValue, NestedFilter } from '@iqser/common-ui'; +import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox'; +import { BehaviorSubject, combineLatest, Observable, pipe } from 'rxjs'; +import { delay, distinctUntilChanged, map, shareReplay } from 'rxjs/operators'; +import { any } from '../../utils/operators'; + +const areExpandable = (nestedFilter: NestedFilter) => !!nestedFilter?.children?.length; +const atLeastOneIsExpandable = pipe( + map(group => !!group?.filters.some(areExpandable)), + distinctUntilChanged(), + shareReplay() +); + +@Component({ + selector: 'iqser-popup-filter [primaryFiltersSlug]', + templateUrl: './popup-filter.component.html', + styleUrls: ['./popup-filter.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: MAT_CHECKBOX_DEFAULT_OPTIONS, + useValue: { + clickAction: 'noop', + color: 'primary' + } + } + ] +}) +export class PopupFilterComponent implements OnInit { + @Input() primaryFiltersSlug!: string; + @Input() actionsTemplate?: TemplateRef; + @Input() secondaryFiltersSlug = ''; + + atLeastOneFilterIsExpandable$?: Observable; + atLeastOneSecondaryFilterIsExpandable$?: Observable; + hasActiveFilters$?: Observable; + readonly expanded = new BehaviorSubject(false); + readonly expanded$ = this.expanded.asObservable().pipe(delay(200)); + + primaryFilterGroup$!: Observable; + secondaryFilterGroup$!: Observable; + + constructor(readonly filterService: FilterService) {} + + ngOnInit(): void { + this.primaryFilterGroup$ = this.filterService.getGroup$(this.primaryFiltersSlug); + this.secondaryFilterGroup$ = this.filterService.getGroup$(this.secondaryFiltersSlug); + + this.hasActiveFilters$ = this._hasActiveFilters$; + this.atLeastOneFilterIsExpandable$ = atLeastOneIsExpandable(this.primaryFilterGroup$); + this.atLeastOneSecondaryFilterIsExpandable$ = atLeastOneIsExpandable(this.secondaryFilterGroup$); + } + + filterCheckboxClicked($event: MouseEvent, nestedFilter: NestedFilter, parent?: NestedFilter): void { + $event.stopPropagation(); + + // eslint-disable-next-line no-param-reassign + nestedFilter.checked = !nestedFilter.checked; + + if (parent) { + handleCheckedValue(parent); + } else { + // eslint-disable-next-line no-param-reassign + if (nestedFilter.indeterminate) nestedFilter.checked = false; + // eslint-disable-next-line no-param-reassign + nestedFilter.indeterminate = false; + // eslint-disable-next-line no-return-assign,no-param-reassign + nestedFilter.children?.forEach(f => (f.checked = nestedFilter.checked)); + } + + this.filterService.refresh(); + } + + activatePrimaryFilters(): void { + this._setFilters(this.primaryFiltersSlug, true); + } + + deactivateFilters(): void { + this._setFilters(this.primaryFiltersSlug); + if (this.secondaryFiltersSlug) this._setFilters(this.secondaryFiltersSlug); + } + + toggleFilterExpanded($event: MouseEvent, nestedFilter: NestedFilter): void { + $event.stopPropagation(); + // eslint-disable-next-line no-param-reassign + nestedFilter.expanded = !nestedFilter.expanded; + this.filterService.refresh(); + } + + private get _hasActiveFilters$() { + return combineLatest([this.primaryFilterGroup$, this.secondaryFilterGroup$]).pipe( + map(([primary, secondary]) => [...(primary?.filters || []), ...(secondary?.filters || [])]), + any(f => f.checked || !!f.indeterminate), + distinctUntilChanged() + ); + } + + private _setFilters(filterGroup: string, checked = false) { + const filters = this.filterService.getGroup(filterGroup)?.filters; + filters?.forEach(f => { + // eslint-disable-next-line no-param-reassign + f.checked = checked; + // eslint-disable-next-line no-param-reassign + f.indeterminate = false; + // eslint-disable-next-line no-return-assign,no-param-reassign + f.children?.forEach(ff => (ff.checked = checked)); + }); + this.filterService.refresh(); + } +} diff --git a/src/lib/filtering/quick-filters/quick-filters.component.scss b/src/lib/filtering/quick-filters/quick-filters.component.scss index 879adf8..13ef48d 100644 --- a/src/lib/filtering/quick-filters/quick-filters.component.scss +++ b/src/lib/filtering/quick-filters/quick-filters.component.scss @@ -1,4 +1,4 @@ -@import '../../../assets/styles/common'; +@import '../../../assets/styles/variables'; :host { display: flex; diff --git a/src/lib/utils/operators.ts b/src/lib/utils/operators.ts new file mode 100644 index 0000000..3e6c9d0 --- /dev/null +++ b/src/lib/utils/operators.ts @@ -0,0 +1,10 @@ +import { map } from 'rxjs/operators'; +import { OperatorFunction } from 'rxjs'; + +export function get(predicate: (value: T, index: number) => boolean): OperatorFunction { + return map(entities => entities.find(predicate)); +} + +export function any(predicate: (value: T, index: number) => boolean): OperatorFunction { + return map(entities => entities.some(predicate)); +}