add filter service and utils functions
This commit is contained in:
parent
1633911e13
commit
3625e29516
@ -5,4 +5,8 @@ export * from './lib/base/auto-unsubscribe.component';
|
||||
export * from './lib/utils/decorators/required.decorator';
|
||||
export * from './lib/buttons/circle-button/circle-button.component';
|
||||
export * from './lib/buttons/circle-button/circle-button.type';
|
||||
export * from './lib/types/tooltip-positions.type';
|
||||
export * from './lib/utils/types/tooltip-positions.type';
|
||||
export * from './lib/filtering/filter.service';
|
||||
export * from './lib/filtering/filter-utils';
|
||||
export * from './lib/filtering/models/filter-group.model';
|
||||
export * from './lib/filtering/models/filter.model';
|
||||
|
||||
@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, On
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { CircleButtonType, CircleButtonTypes } from './circle-button.type';
|
||||
import { Required } from '../../utils/decorators/required.decorator';
|
||||
import { TooltipPositionsType, TooltipPositionsTypes } from '../../types/tooltip-positions.type';
|
||||
import { TooltipPositionsType, TooltipPositionsTypes } from '../../utils/types/tooltip-positions.type';
|
||||
|
||||
@Component({
|
||||
selector: 'iqser-circle-button',
|
||||
@ -24,7 +24,7 @@ export class CircleButtonComponent implements OnInit {
|
||||
@Input() isSubmit = false;
|
||||
@Input() size = 34;
|
||||
@Input() iconSize = 14;
|
||||
@Output() action = new EventEmitter<any>();
|
||||
@Output() action = new EventEmitter<MouseEvent>();
|
||||
|
||||
@ViewChild(MatTooltip) private readonly _matTooltip!: MatTooltip;
|
||||
|
||||
@ -35,7 +35,7 @@ export class CircleButtonComponent implements OnInit {
|
||||
this._elementRef.nativeElement.style.setProperty('--iconSize', this.iconSize + 'px');
|
||||
}
|
||||
|
||||
performAction($event: any) {
|
||||
performAction($event: MouseEvent) {
|
||||
if (this.disabled) return;
|
||||
|
||||
if (this.removeTooltip) {
|
||||
|
||||
@ -16,5 +16,5 @@ export class IconButtonComponent {
|
||||
@Input() showDot = false;
|
||||
@Input() disabled = false;
|
||||
@Input() type: IconButtonType = IconButtonTypes.default;
|
||||
@Output() action = new EventEmitter<any>();
|
||||
@Output() action = new EventEmitter<MouseEvent>();
|
||||
}
|
||||
|
||||
82
src/lib/filtering/filter-utils.ts
Normal file
82
src/lib/filtering/filter-utils.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { FilterModel } from './models/filter.model';
|
||||
import { FilterGroup } from './models/filter-group.model';
|
||||
|
||||
export function processFilters(oldFilters: FilterModel[], newFilters: FilterModel[]) {
|
||||
copySettings(oldFilters, newFilters);
|
||||
if (newFilters) {
|
||||
newFilters.forEach(filter => {
|
||||
handleCheckedValue(filter);
|
||||
});
|
||||
}
|
||||
return newFilters;
|
||||
}
|
||||
|
||||
function copySettings(oldFilters: FilterModel[], newFilters: FilterModel[]) {
|
||||
if (oldFilters && newFilters) {
|
||||
for (const oldFilter of oldFilters) {
|
||||
const newFilter = newFilters.find(f => f.key === oldFilter.key);
|
||||
if (newFilter) {
|
||||
newFilter.checked = oldFilter.checked;
|
||||
newFilter.indeterminate = oldFilter.indeterminate;
|
||||
if (oldFilter.filters && newFilter.filters) copySettings(oldFilter.filters, newFilter.filters);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function handleCheckedValue(filter: FilterModel) {
|
||||
if (filter.filters && filter.filters.length) {
|
||||
filter.checked = filter.filters.reduce<boolean>((acc, next) => acc && !!next.checked, true);
|
||||
if (filter.checked) {
|
||||
filter.indeterminate = false;
|
||||
} else {
|
||||
filter.indeterminate = filter.filters.reduce<boolean>((acc, next) => acc || !!next.checked, false);
|
||||
}
|
||||
} else {
|
||||
filter.indeterminate = false;
|
||||
}
|
||||
}
|
||||
|
||||
export function checkFilter(entity: any, filters: FilterModel[], validate: Function, validateArgs: any = [], matchAll: boolean = false) {
|
||||
const hasChecked = filters.find(f => f.checked);
|
||||
|
||||
if (validateArgs) {
|
||||
if (!Array.isArray(validateArgs)) {
|
||||
validateArgs = [validateArgs];
|
||||
}
|
||||
} else {
|
||||
validateArgs = [];
|
||||
}
|
||||
if (!hasChecked) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let filterMatched = matchAll;
|
||||
for (const filter of filters) {
|
||||
if (filter.checked) {
|
||||
if (matchAll) {
|
||||
filterMatched = filterMatched && validate(entity, filter, ...validateArgs);
|
||||
} else {
|
||||
filterMatched = filterMatched || validate(entity, filter, ...validateArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filterMatched;
|
||||
}
|
||||
|
||||
export const keyChecker = (key: string) => (entity: any, filter: FilterModel) => entity[key] === filter.key;
|
||||
|
||||
export function getFilteredEntities<T>(entities: T[], filters: FilterGroup[]) {
|
||||
const filteredEntities: T[] = [];
|
||||
for (const entity of entities) {
|
||||
let add = true;
|
||||
for (const filter of filters) {
|
||||
add = add && checkFilter(entity, filter.filters, filter.checker, filter.checkerArgs, filter.matchAll);
|
||||
}
|
||||
if (add) {
|
||||
filteredEntities.push(entity);
|
||||
}
|
||||
}
|
||||
return filteredEntities;
|
||||
}
|
||||
88
src/lib/filtering/filter.service.ts
Normal file
88
src/lib/filtering/filter.service.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { processFilters } from './filter-utils';
|
||||
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
||||
import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators';
|
||||
import { FilterGroup } from './models/filter-group.model';
|
||||
import { FilterModel } from './models/filter.model';
|
||||
|
||||
@Injectable()
|
||||
export class FilterService {
|
||||
private readonly _filterGroups$ = new BehaviorSubject<FilterGroup[]>([]);
|
||||
private readonly _refresh$ = new Subject();
|
||||
readonly filterGroups$ = this._refresh$.pipe(
|
||||
startWith(''),
|
||||
switchMap(() => this._filterGroups$.asObservable())
|
||||
);
|
||||
readonly showResetFilters$ = this._showResetFilters$;
|
||||
|
||||
get filterGroups(): FilterGroup[] {
|
||||
return Object.values(this._filterGroups$.getValue());
|
||||
}
|
||||
|
||||
private get _showResetFilters$(): Observable<boolean> {
|
||||
return this.filterGroups$.pipe(
|
||||
map(all => this._toFlatFilters(all)),
|
||||
map(f => !!f.find(el => el.checked)),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this._refresh$.next();
|
||||
}
|
||||
|
||||
toggleFilter(filterGroupSlug: string, key: string): void {
|
||||
const filters = this.filterGroups.find(group => group.slug === filterGroupSlug)?.filters;
|
||||
if (!filters) return console.error(`Cannot find filter group "${filterGroupSlug}"`);
|
||||
|
||||
let found = filters.find(f => f.key === key);
|
||||
if (!found) found = filters.map(f => f.filters?.find(ff => ff.key === key))[0];
|
||||
if (!found) return console.error(`Cannot find filter with key "${key}" in group "${filterGroupSlug}"`);
|
||||
|
||||
found.checked = !found.checked;
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
addFilterGroup(value: FilterGroup): void {
|
||||
const oldFilters = this.getFilterGroup(value.slug)?.filters;
|
||||
if (!oldFilters) return this._filterGroups$.next([...this.filterGroups, value]);
|
||||
|
||||
value.filters = processFilters(oldFilters, value.filters);
|
||||
this._filterGroups$.next([...this.filterGroups.filter(f => f.slug !== value.slug), value]);
|
||||
}
|
||||
|
||||
getFilterGroup(slug: string): FilterGroup | undefined {
|
||||
return this.filterGroups.find(group => group.slug === slug);
|
||||
}
|
||||
|
||||
getFilterModels$(filterGroupSlug: string): Observable<FilterModel[] | undefined> {
|
||||
return this.getFilterGroup$(filterGroupSlug).pipe(map(f => f?.filters));
|
||||
}
|
||||
|
||||
getFilterGroup$(slug: string): Observable<FilterGroup | undefined> {
|
||||
return this.filterGroups$.pipe(map(all => all.find(f => f.slug === slug)));
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.filterGroups.forEach(item => {
|
||||
item.filters.forEach(child => {
|
||||
child.checked = false;
|
||||
child.indeterminate = false;
|
||||
child.filters?.forEach(f => {
|
||||
f.checked = false;
|
||||
f.indeterminate = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private _toFlatFilters(entities: FilterGroup[]): FilterModel[] {
|
||||
const flatChildren = (filters: FilterModel[]) =>
|
||||
(filters ?? []).reduce((acc: FilterModel[], f) => [...acc, ...(f?.filters ?? [])], []);
|
||||
|
||||
return entities.reduce((acc: FilterModel[], f) => [...acc, ...f.filters, ...flatChildren(f.filters)], []);
|
||||
}
|
||||
}
|
||||
14
src/lib/filtering/models/filter-group.model.ts
Normal file
14
src/lib/filtering/models/filter-group.model.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { FilterModel } from './filter.model';
|
||||
import { TemplateRef } from '@angular/core';
|
||||
|
||||
export interface FilterGroup {
|
||||
filters: FilterModel[];
|
||||
readonly slug: string;
|
||||
readonly label?: string;
|
||||
readonly icon?: string;
|
||||
readonly filterTemplate?: TemplateRef<any>;
|
||||
readonly hide?: boolean;
|
||||
readonly checker: Function;
|
||||
readonly matchAll?: boolean;
|
||||
readonly checkerArgs?: any;
|
||||
}
|
||||
13
src/lib/filtering/models/filter.model.ts
Normal file
13
src/lib/filtering/models/filter.model.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export interface FilterModel {
|
||||
readonly key: string;
|
||||
checked?: boolean;
|
||||
indeterminate?: boolean;
|
||||
expanded?: boolean;
|
||||
matches?: number;
|
||||
readonly label?: string;
|
||||
readonly icon?: string;
|
||||
readonly topLevelFilter?: boolean;
|
||||
readonly filters?: FilterModel[];
|
||||
readonly checker?: (obj?: unknown) => boolean;
|
||||
readonly required?: boolean;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user