add popup filters
This commit is contained in:
parent
bb51a90738
commit
feebed2c56
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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];
|
||||
|
||||
|
||||
@ -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<FilterGroup | undefined> {
|
||||
return this.filterGroups$.pipe(map(all => all.find(f => f.slug === slug)));
|
||||
return this.filterGroups$.pipe(get(group => group.slug === slug));
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
|
||||
109
src/lib/filtering/popup-filter/popup-filter.component.html
Normal file
109
src/lib/filtering/popup-filter/popup-filter.component.html
Normal file
@ -0,0 +1,109 @@
|
||||
<ng-container *ngIf="primaryFilterGroup$ | async as primaryGroup">
|
||||
<iqser-icon-button
|
||||
*ngIf="primaryGroup.icon"
|
||||
[icon]="primaryGroup.icon"
|
||||
[label]="primaryGroup.label || ('filter-menu.label' | translate)"
|
||||
[matMenuTriggerFor]="filterMenu"
|
||||
[showDot]="hasActiveFilters$ | async"
|
||||
[attr.aria-expanded]="expanded$ | async"
|
||||
></iqser-icon-button>
|
||||
|
||||
<iqser-chevron-button
|
||||
*ngIf="!primaryGroup.icon"
|
||||
[label]="primaryGroup.label || ('filter-menu.label' | translate)"
|
||||
[matMenuTriggerFor]="filterMenu"
|
||||
[showDot]="hasActiveFilters$ | async"
|
||||
[attr.aria-expanded]="expanded$ | async"
|
||||
></iqser-chevron-button>
|
||||
|
||||
<mat-menu
|
||||
#filterMenu="matMenu"
|
||||
(close)="expanded.next(false)"
|
||||
[class]="(secondaryFilterGroup$ | async)?.filters.length > 0 ? 'padding-bottom-0' : ''"
|
||||
xPosition="before"
|
||||
>
|
||||
<ng-template matMenuContent>
|
||||
<div class="filter-menu-header">
|
||||
<div class="all-caps-label" translate="filter-menu.filter-types"></div>
|
||||
<div class="actions">
|
||||
<div
|
||||
(click)="activatePrimaryFilters(); $event.stopPropagation()"
|
||||
class="all-caps-label primary pointer"
|
||||
translate="actions.all"
|
||||
></div>
|
||||
<div
|
||||
(click)="deactivateFilters(); $event.stopPropagation()"
|
||||
class="all-caps-label primary pointer"
|
||||
translate="actions.none"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-content">
|
||||
<ng-container
|
||||
*ngFor="let filter of primaryGroup.filters"
|
||||
[ngTemplateOutletContext]="{
|
||||
filter: filter,
|
||||
atLeastOneIsExpandable: atLeastOneFilterIsExpandable$ | async
|
||||
}"
|
||||
[ngTemplateOutlet]="defaultFilterTemplate"
|
||||
></ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="secondaryFilterGroup$ | async as secondaryGroup" class="filter-options">
|
||||
<div class="filter-menu-options">
|
||||
<div class="all-caps-label" translate="filter-menu.filter-options"></div>
|
||||
</div>
|
||||
|
||||
<div *ngFor="let filter of secondaryGroup.filters">
|
||||
<ng-container
|
||||
[ngTemplateOutletContext]="{
|
||||
filter: filter,
|
||||
atLeastOneIsExpandable: atLeastOneSecondaryFilterIsExpandable$ | async
|
||||
}"
|
||||
[ngTemplateOutlet]="defaultFilterTemplate"
|
||||
></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-menu>
|
||||
|
||||
<ng-template #defaultFilterLabelTemplate let-filter="filter">
|
||||
{{ filter?.label }}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #defaultFilterTemplate let-atLeastOneIsExpandable="atLeastOneIsExpandable" let-filter="filter">
|
||||
<div (click)="toggleFilterExpanded($event, filter)" class="mat-menu-item flex">
|
||||
<div *ngIf="filter.children?.length > 0" class="arrow-wrapper">
|
||||
<mat-icon *ngIf="filter.expanded" color="accent" svgIcon="iqser:arrow-down"></mat-icon>
|
||||
<mat-icon *ngIf="!filter.expanded" color="accent" svgIcon="red:arrow-right"></mat-icon>
|
||||
</div>
|
||||
<div *ngIf="atLeastOneIsExpandable && filter.children?.length === 0" class="arrow-wrapper spacer"> </div>
|
||||
<mat-checkbox
|
||||
(click)="filterCheckboxClicked($event, filter)"
|
||||
[checked]="filter.checked"
|
||||
[indeterminate]="filter.indeterminate"
|
||||
class="filter-menu-checkbox"
|
||||
>
|
||||
<ng-container
|
||||
[ngTemplateOutletContext]="{ filter: filter }"
|
||||
[ngTemplateOutlet]="primaryGroup.filterTemplate ?? defaultFilterLabelTemplate"
|
||||
></ng-container>
|
||||
</mat-checkbox>
|
||||
<ng-container [ngTemplateOutletContext]="{ filter: filter }" [ngTemplateOutlet]="actionsTemplate"></ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="filter.children?.length && filter.expanded">
|
||||
<div (click)="$event.stopPropagation()" *ngFor="let child of filter.children" class="padding-left mat-menu-item">
|
||||
<mat-checkbox (click)="filterCheckboxClicked($event, child, filter)" [checked]="child.checked">
|
||||
<ng-container
|
||||
[ngTemplateOutletContext]="{ filter: child }"
|
||||
[ngTemplateOutlet]="primaryGroup.filterTemplate ?? defaultFilterLabelTemplate"
|
||||
></ng-container>
|
||||
</mat-checkbox>
|
||||
|
||||
<ng-container [ngTemplateOutletContext]="{ filter: child }" [ngTemplateOutlet]="actionsTemplate"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
41
src/lib/filtering/popup-filter/popup-filter.component.scss
Normal file
41
src/lib/filtering/popup-filter/popup-filter.component.scss
Normal file
@ -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%;
|
||||
}
|
||||
}
|
||||
111
src/lib/filtering/popup-filter/popup-filter.component.ts
Normal file
111
src/lib/filtering/popup-filter/popup-filter.component.ts
Normal file
@ -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<FilterGroup | undefined, boolean>(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<unknown>;
|
||||
@Input() secondaryFiltersSlug = '';
|
||||
|
||||
atLeastOneFilterIsExpandable$?: Observable<boolean>;
|
||||
atLeastOneSecondaryFilterIsExpandable$?: Observable<boolean>;
|
||||
hasActiveFilters$?: Observable<boolean>;
|
||||
readonly expanded = new BehaviorSubject<boolean>(false);
|
||||
readonly expanded$ = this.expanded.asObservable().pipe(delay(200));
|
||||
|
||||
primaryFilterGroup$!: Observable<FilterGroup | undefined>;
|
||||
secondaryFilterGroup$!: Observable<FilterGroup | undefined>;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
@import '../../../assets/styles/common';
|
||||
@import '../../../assets/styles/variables';
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
|
||||
10
src/lib/utils/operators.ts
Normal file
10
src/lib/utils/operators.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { map } from 'rxjs/operators';
|
||||
import { OperatorFunction } from 'rxjs';
|
||||
|
||||
export function get<T>(predicate: (value: T, index: number) => boolean): OperatorFunction<T[], T | undefined> {
|
||||
return map(entities => entities.find(predicate));
|
||||
}
|
||||
|
||||
export function any<T>(predicate: (value: T, index: number) => boolean): OperatorFunction<T[], boolean> {
|
||||
return map(entities => entities.some(predicate));
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user