UNPKG

igniteui-angular-sovn

Version:

Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps

599 lines (504 loc) 23.7 kB
import { Injectable, OnDestroy, } from '@angular/core'; import { FilteringExpressionsTree, IFilteringExpressionsTree } from '../../data-operations/filtering-expressions-tree'; import { IFilteringExpression, FilteringLogic } from '../../data-operations/filtering-expression.interface'; import { Subject } from 'rxjs'; import { takeUntil, first } from 'rxjs/operators'; import { IForOfState } from '../../directives/for-of/for_of.directive'; import { IFilteringOperation } from '../../data-operations/filtering-condition'; import { IColumnResizeEventArgs, IFilteringEventArgs } from '../common/events'; import { OverlaySettings, VerticalAlignment } from '../../services/overlay/utilities'; import { IgxOverlayService } from '../../services/overlay/overlay'; import { useAnimation } from '@angular/animations'; import { fadeIn } from '../../animations/main'; import { AbsoluteScrollStrategy } from '../../services/overlay/scroll/absolute-scroll-strategy'; import { IgxIconService } from '../../icon/icon.service'; import { editor, pinLeft, unpinLeft } from '@igniteui/material-icons-extended'; import { ExpressionUI, generateExpressionsList } from './excel-style/common'; import { ColumnType, GridType } from '../common/grid.interface'; import { formatDate } from '../../core/utils'; import { ExcelStylePositionStrategy } from './excel-style/excel-style-position-strategy'; /** * @hidden */ @Injectable() export class IgxFilteringService implements OnDestroy { public isFilterRowVisible = false; public filteredColumn: ColumnType = null; public selectedExpression: IFilteringExpression = null; public columnToMoreIconHidden = new Map<string, boolean>(); public activeFilterCell = 0; public grid: GridType; private columnsWithComplexFilter = new Set<string>(); private areEventsSubscribed = false; protected destroy$ = new Subject<boolean>(); private isFiltering = false; private columnToExpressionsMap = new Map<string, ExpressionUI[]>(); private columnStartIndex = -1; protected _filterMenuOverlaySettings: OverlaySettings = { closeOnEscape: true, closeOnOutsideClick: true, modal: false, positionStrategy: new ExcelStylePositionStrategy({ verticalStartPoint: VerticalAlignment.Bottom, openAnimation: useAnimation(fadeIn, { params: { duration: '250ms' }}), closeAnimation: null }), scrollStrategy: new AbsoluteScrollStrategy() }; protected lastActiveNode; constructor( private iconService: IgxIconService, protected _overlayService: IgxOverlayService, ) { } public ngOnDestroy(): void { this.destroy$.next(true); this.destroy$.complete(); } public toggleFilterDropdown(element: HTMLElement, column: ColumnType) { const filterIcon = column.filteringExpressionsTree ? 'igx-excel-filter__icon--filtered' : 'igx-excel-filter__icon'; const filterIconTarget = element.querySelector(`.${filterIcon}`) as HTMLElement || element; const { id, ref } = this.grid.createFilterDropdown(column, { ...this._filterMenuOverlaySettings, ...{ target: filterIconTarget } }); this._overlayService.opening .pipe( first(overlay => overlay.id === id), takeUntil(this.destroy$) ) .subscribe(() => this.lastActiveNode = this.grid.navigation.activeNode); this._overlayService.closed .pipe( first(overlay => overlay.id === id), takeUntil(this.destroy$) ) .subscribe(() => { this._overlayService.detach(id); ref?.destroy(); this.grid.navigation.activeNode = this.lastActiveNode; this.grid.theadRow.nativeElement.focus(); }); this.grid.columnPinned.pipe(first()).subscribe(() => ref?.destroy()); this._overlayService.show(id); } /** * Subscribe to grid's events. */ public subscribeToEvents() { if (!this.areEventsSubscribed) { this.areEventsSubscribed = true; this.grid.columnResized.pipe(takeUntil(this.destroy$)).subscribe((eventArgs: IColumnResizeEventArgs) => { this.updateFilteringCell(eventArgs.column); }); this.grid.parentVirtDir.chunkLoad.pipe(takeUntil(this.destroy$)).subscribe((eventArgs: IForOfState) => { if (eventArgs.startIndex !== this.columnStartIndex) { this.columnStartIndex = eventArgs.startIndex; this.grid.filterCellList.forEach((filterCell) => { filterCell.updateFilterCellArea(); }); } }); this.grid.columnMovingEnd.pipe(takeUntil(this.destroy$)).subscribe(() => { this.grid.filterCellList.forEach((filterCell) => { filterCell.updateFilterCellArea(); }); }); } } /** * Close filtering row if a column is hidden. */ public hideFilteringRowOnColumnVisibilityChange(col: ColumnType) { const filteringRow = this.grid.filteringRow; if (filteringRow && filteringRow.column && filteringRow.column === col) { filteringRow.close(); } } /** * Internal method to create expressionsTree and filter grid used in both filter modes. */ public filterInternal(field: string, expressions: FilteringExpressionsTree | Array<ExpressionUI> = null): void { this.isFiltering = true; let expressionsTree; if (expressions instanceof FilteringExpressionsTree) { expressionsTree = expressions; } else { expressionsTree = this.createSimpleFilteringTree(field, expressions); } if (expressionsTree.filteringOperands.length === 0) { this.clearFilter(field); } else { this.filter(field, null, expressionsTree); } this.isFiltering = false; } /** * Execute filtering on the grid. */ public filter(field: string, value: any, conditionOrExpressionTree?: IFilteringOperation | IFilteringExpressionsTree, ignoreCase?: boolean) { const grid = this.grid; const col = grid.getColumnByName(field); const filteringIgnoreCase = ignoreCase || (col ? col.filteringIgnoreCase : false); const filteringTree = grid.filteringExpressionsTree; const columnFilteringExpressionsTree = filteringTree.find(field) as IFilteringExpressionsTree; conditionOrExpressionTree = conditionOrExpressionTree ?? columnFilteringExpressionsTree; const fieldFilterIndex = filteringTree.findIndex(field); const newFilteringTree: FilteringExpressionsTree = this.prepare_filtering_expression(filteringTree, field, value, conditionOrExpressionTree, filteringIgnoreCase, fieldFilterIndex, true); const eventArgs: IFilteringEventArgs = { owner: grid, filteringExpressions: newFilteringTree.find(field) as FilteringExpressionsTree, cancel: false }; this.grid.filtering.emit(eventArgs); if (eventArgs.cancel) { return; } if (conditionOrExpressionTree) { this.filter_internal(field, value, conditionOrExpressionTree, filteringIgnoreCase); } else { const expressionsTreeForColumn = this.grid.filteringExpressionsTree.find(field); if (!expressionsTreeForColumn) { throw new Error('Invalid condition or Expression Tree!'); } else if (expressionsTreeForColumn instanceof FilteringExpressionsTree) { this.filter_internal(field, value, expressionsTreeForColumn, filteringIgnoreCase); } else { const expressionForColumn = expressionsTreeForColumn as IFilteringExpression; this.filter_internal(field, value, expressionForColumn.condition, filteringIgnoreCase); } } const doneEventArgs = this.grid.filteringExpressionsTree.find(field) as FilteringExpressionsTree; // Wait for the change detection to update filtered data through the pipes and then emit the event. requestAnimationFrame(() => this.grid.filteringDone.emit(doneEventArgs)); } public filter_global(term, condition, ignoreCase) { if (!condition) { return; } const filteringTree = this.grid.filteringExpressionsTree; this.grid.crudService.endEdit(false); this.grid.page = 0; filteringTree.filteringOperands = []; for (const column of this.grid.columns) { this.prepare_filtering_expression(filteringTree, column.field, term, condition, ignoreCase || column.filteringIgnoreCase); } this.grid.filteringExpressionsTree = filteringTree; } /** * Clears the filter of a given column if name is provided. Otherwise clears the filters of all columns. */ public clearFilter(field: string): void { if (field) { const column = this.grid.getColumnByName(field); if (!column) { return; } } const emptyFilter = new FilteringExpressionsTree(null, field); const onFilteringEventArgs: IFilteringEventArgs = { owner: this.grid, filteringExpressions: emptyFilter, cancel: false }; this.grid.filtering.emit(onFilteringEventArgs); if (onFilteringEventArgs.cancel) { return; } this.isFiltering = true; this.clear_filter(field); // Wait for the change detection to update filtered data through the pipes and then emit the event. requestAnimationFrame(() => this.grid.filteringDone.emit(emptyFilter)); if (field) { const expressions = this.getExpressions(field); expressions.length = 0; } else { this.grid.columns.forEach(c => { const expressions = this.getExpressions(c.field); expressions.length = 0; }); } this.isFiltering = false; } public clear_filter(fieldName: string) { const grid = this.grid; grid.crudService.endEdit(false); const filteringState = grid.filteringExpressionsTree; const index = filteringState.findIndex(fieldName); if (index > -1) { filteringState.filteringOperands.splice(index, 1); } else if (!fieldName) { filteringState.filteringOperands = []; } grid.filteringExpressionsTree = filteringState; } /** * Filters all the `IgxColumnComponent` in the `IgxGridComponent` with the same condition. */ public filterGlobal(value: any, condition, ignoreCase?) { if (!condition) { return; } const filteringTree = this.grid.filteringExpressionsTree; const newFilteringTree = new FilteringExpressionsTree(filteringTree.operator, filteringTree.fieldName); for (const column of this.grid.columns) { this.prepare_filtering_expression(newFilteringTree, column.field, value, condition, ignoreCase || column.filteringIgnoreCase); } const eventArgs: IFilteringEventArgs = { owner: this.grid, filteringExpressions: newFilteringTree, cancel: false }; this.grid.filtering.emit(eventArgs); if (eventArgs.cancel) { return; } this.grid.crudService.endEdit(false); this.grid.page = 0; this.grid.filteringExpressionsTree = newFilteringTree; // Wait for the change detection to update filtered data through the pipes and then emit the event. requestAnimationFrame(() => this.grid.filteringDone.emit(this.grid.filteringExpressionsTree)); } /** * Register filtering SVG icons in the icon service. */ public registerSVGIcons(): void { const editorIcons = editor as any[]; editorIcons.forEach(icon => this.iconService.addSvgIconFromText(icon.name, icon.value, 'imx-icons')); this.iconService.addSvgIconFromText(pinLeft.name, pinLeft.value, 'imx-icons'); this.iconService.addSvgIconFromText(unpinLeft.name, unpinLeft.value, 'imx-icons'); } /** * Returns the ExpressionUI array for a given column. */ public getExpressions(columnId: string): ExpressionUI[] { if (!this.columnToExpressionsMap.has(columnId)) { const column = this.grid.columns.find((col) => col.field === columnId); const expressionUIs = new Array<ExpressionUI>(); if (column) { this.generateExpressionsList(column.filteringExpressionsTree, this.grid.filteringExpressionsTree.operator, expressionUIs); this.columnToExpressionsMap.set(columnId, expressionUIs); } return expressionUIs; } return this.columnToExpressionsMap.get(columnId); } /** * Recreates all ExpressionUIs for all columns. Executed after filtering to refresh the cache. */ public refreshExpressions() { if (!this.isFiltering) { this.columnsWithComplexFilter.clear(); this.columnToExpressionsMap.forEach((value: ExpressionUI[], key: string) => { const column = this.grid.columns.find((col) => col.field === key); if (column) { value.length = 0; this.generateExpressionsList(column.filteringExpressionsTree, this.grid.filteringExpressionsTree.operator, value); const isComplex = this.isFilteringTreeComplex(column.filteringExpressionsTree); if (isComplex) { this.columnsWithComplexFilter.add(key); } this.updateFilteringCell(column); } else { this.columnToExpressionsMap.delete(key); } }); } } /** * Remove an ExpressionUI for a given column. */ public removeExpression(columnId: string, indexToRemove: number) { const expressionsList = this.getExpressions(columnId); if (indexToRemove === 0 && expressionsList.length > 1) { expressionsList[1].beforeOperator = null; } else if (indexToRemove === expressionsList.length - 1) { expressionsList[indexToRemove - 1].afterOperator = null; } else { expressionsList[indexToRemove - 1].afterOperator = expressionsList[indexToRemove + 1].beforeOperator; expressionsList[0].beforeOperator = null; expressionsList[expressionsList.length - 1].afterOperator = null; } expressionsList.splice(indexToRemove, 1); } /** * Generate filtering tree for a given column from existing ExpressionUIs. */ public createSimpleFilteringTree(columnId: string, expressionUIList = null): FilteringExpressionsTree { const expressionsList = expressionUIList ? expressionUIList : this.getExpressions(columnId); const expressionsTree = new FilteringExpressionsTree(FilteringLogic.Or, columnId); let currAndBranch: FilteringExpressionsTree; for (const currExpressionUI of expressionsList) { if (!currExpressionUI.expression.condition.isUnary && currExpressionUI.expression.searchVal === null) { if (currExpressionUI.afterOperator === FilteringLogic.And && !currAndBranch) { currAndBranch = new FilteringExpressionsTree(FilteringLogic.And, columnId); expressionsTree.filteringOperands.push(currAndBranch); } continue; } if ((currExpressionUI.beforeOperator === undefined || currExpressionUI.beforeOperator === null || currExpressionUI.beforeOperator === FilteringLogic.Or) && currExpressionUI.afterOperator === FilteringLogic.And) { currAndBranch = new FilteringExpressionsTree(FilteringLogic.And, columnId); expressionsTree.filteringOperands.push(currAndBranch); currAndBranch.filteringOperands.push(currExpressionUI.expression); } else if (currExpressionUI.beforeOperator === FilteringLogic.And) { currAndBranch.filteringOperands.push(currExpressionUI.expression); } else { expressionsTree.filteringOperands.push(currExpressionUI.expression); currAndBranch = null; } } return expressionsTree; } /** * Returns whether a complex filter is applied to a given column. */ public isFilterComplex(columnId: string) { if (this.columnsWithComplexFilter.has(columnId)) { return true; } const column = this.grid.columns.find((col) => col.field === columnId); const isComplex = column && this.isFilteringTreeComplex(column.filteringExpressionsTree); if (isComplex) { this.columnsWithComplexFilter.add(columnId); } return isComplex; } /** * Returns the string representation of the FilteringLogic operator. */ public getOperatorAsString(operator: FilteringLogic): any { if (operator === 0) { return this.grid.resourceStrings.igx_grid_filter_operator_and; } else { return this.grid.resourceStrings.igx_grid_filter_operator_or; } } /** * Generate the label of a chip from a given filtering expression. */ public getChipLabel(expression: IFilteringExpression): any { if (expression.condition.isUnary) { return this.grid.resourceStrings[`igx_grid_filter_${expression.condition.name}`] || expression.condition.name; } else if (expression.searchVal instanceof Date) { const column = this.grid.getColumnByName(expression.fieldName); const formatter = column.formatter; if (formatter) { return formatter(expression.searchVal, undefined); } const pipeArgs = column.pipeArgs; return formatDate(expression.searchVal, pipeArgs.format, this.grid.locale); } else { return expression.searchVal; } } /** * Updates the content of a filterCell. */ public updateFilteringCell(column: ColumnType) { const filterCell = column.filterCell; if (filterCell) { filterCell.updateFilterCellArea(); } } public generateExpressionsList(expressions: IFilteringExpressionsTree | IFilteringExpression, operator: FilteringLogic, expressionsUIs: ExpressionUI[]): void { generateExpressionsList(expressions, operator, expressionsUIs); } public isFilteringExpressionsTreeEmpty(expressionTree: IFilteringExpressionsTree): boolean { if (FilteringExpressionsTree.empty(expressionTree)) { return true; } for (const expr of expressionTree.filteringOperands) { if ((expr instanceof FilteringExpressionsTree)) { const exprTree = expr as FilteringExpressionsTree; if (exprTree.filteringOperands && exprTree.filteringOperands.length) { return false; } } else { return false; } } return true; } protected filter_internal(fieldName: string, term, conditionOrExpressionsTree: IFilteringOperation | IFilteringExpressionsTree, ignoreCase: boolean) { const filteringTree = this.grid.filteringExpressionsTree; this.grid.crudService.endEdit(false); this.grid.page = 0; const fieldFilterIndex = filteringTree.findIndex(fieldName); this.prepare_filtering_expression(filteringTree, fieldName, term, conditionOrExpressionsTree, ignoreCase, fieldFilterIndex); this.grid.filteringExpressionsTree = filteringTree; } /** Modifies the filteringState object to contain the newly added filtering conditions/expressions. * If createNewTree is true, filteringState will not be modified (because it directly affects the grid.filteringExpressionsTree), * but a new object is created and returned. */ protected prepare_filtering_expression( filteringState: IFilteringExpressionsTree, fieldName: string, searchVal, conditionOrExpressionsTree: IFilteringOperation | IFilteringExpressionsTree, ignoreCase: boolean, insertAtIndex = -1, createNewTree = false): FilteringExpressionsTree { let expressionsTree = conditionOrExpressionsTree instanceof FilteringExpressionsTree ? conditionOrExpressionsTree as IFilteringExpressionsTree : null; const condition = conditionOrExpressionsTree instanceof FilteringExpressionsTree ? null : conditionOrExpressionsTree as IFilteringOperation; let newExpressionsTree = filteringState as FilteringExpressionsTree; if (createNewTree) { newExpressionsTree = new FilteringExpressionsTree(filteringState.operator, filteringState.fieldName); newExpressionsTree.filteringOperands = [...filteringState.filteringOperands]; } if (condition) { const newExpression: IFilteringExpression = { fieldName, searchVal, condition, ignoreCase }; expressionsTree = new FilteringExpressionsTree(filteringState.operator, fieldName); expressionsTree.filteringOperands.push(newExpression); } if (expressionsTree) { if (insertAtIndex > -1) { newExpressionsTree.filteringOperands[insertAtIndex] = expressionsTree; } else { newExpressionsTree.filteringOperands.push(expressionsTree); } } return newExpressionsTree; } private isFilteringTreeComplex(expressions: IFilteringExpressionsTree | IFilteringExpression): boolean { if (!expressions) { return false; } if (expressions instanceof FilteringExpressionsTree) { const expressionsTree = expressions as FilteringExpressionsTree; if (expressionsTree.operator === FilteringLogic.Or) { const andOperatorsCount = this.getChildAndOperatorsCount(expressionsTree); // having more than one 'And' operator in the sub-tree means that the filter could not be represented without parentheses. return andOperatorsCount > 1; } let isComplex = false; for (const operand of expressionsTree.filteringOperands) { isComplex = isComplex || this.isFilteringTreeComplex(operand); } return isComplex; } return false; } private getChildAndOperatorsCount(expressions: IFilteringExpressionsTree): number { let count = 0; let operand; for (let i = 0; i < expressions.filteringOperands.length; i++) { operand = expressions[i]; if (operand instanceof FilteringExpressionsTree) { if (operand.operator === FilteringLogic.And) { count++; } count = count + this.getChildAndOperatorsCount(operand); } } return count; } }