UNPKG

igniteui-angular-sovn

Version:

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

734 lines (643 loc) 25.6 kB
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, Directive, ElementRef, EventEmitter, forwardRef, Host, HostBinding, Inject, Input, OnDestroy, Optional, Output, TemplateRef, ViewChild, ViewRef } from '@angular/core'; import { FilteringExpressionsTree, IFilteringExpressionsTree } from '../../../data-operations/filtering-expressions-tree'; import { PlatformUtil, formatDate, formatCurrency } from '../../../core/utils'; import { GridColumnDataType } from '../../../data-operations/data-util'; import { Subscription } from 'rxjs'; import { DisplayDensity } from '../../../core/density'; import { GridSelectionMode } from '../../common/enums'; import { IgxFilterItem } from '../../../data-operations/filtering-strategy'; import { formatNumber, formatPercent, getLocaleCurrencyCode, NgIf, NgClass } from '@angular/common'; import { BaseFilteringComponent } from './base-filtering.component'; import { ExpressionUI, FilterListItem, generateExpressionsList } from './common'; import { ColumnType, GridType, IGX_GRID_BASE } from '../../common/grid.interface'; import { IgxOverlayService } from '../../../services/overlay/overlay'; import { SortingDirection } from '../../../data-operations/sorting-strategy'; import { IgxExcelStyleSearchComponent } from './excel-style-search.component'; import { IgxExcelStyleConditionalFilterComponent } from './excel-style-conditional-filter.component'; import { IgxExcelStyleClearFiltersComponent } from './excel-style-clear-filters.component'; import { IgxExcelStyleSelectingComponent } from './excel-style-selecting.component'; import { IgxExcelStyleHidingComponent } from './excel-style-hiding.component'; import { IgxExcelStylePinningComponent } from './excel-style-pinning.component'; import { IgxExcelStyleMovingComponent } from './excel-style-moving.component'; import { IgxExcelStyleSortingComponent } from './excel-style-sorting.component'; import { IgxExcelStyleHeaderComponent } from './excel-style-header.component'; @Directive({ selector: 'igx-excel-style-column-operations,[igxExcelStyleColumnOperations]', standalone: true }) export class IgxExcelStyleColumnOperationsTemplateDirective { } @Directive({ selector: 'igx-excel-style-filter-operations,[igxExcelStyleFilterOperations]', standalone: true }) export class IgxExcelStyleFilterOperationsTemplateDirective { } /** * A component used for presenting Excel style filtering UI for a specific column. * It is used internally in the Grid, but could also be hosted in a container outside of it. * * Example: * ```html * <igx-grid-excel-style-filtering * [column]="grid1.columns[0]"> * </igx-grid-excel-style-filtering> * ``` */ @Component({ changeDetection: ChangeDetectionStrategy.OnPush, providers: [{ provide: BaseFilteringComponent, useExisting: forwardRef(() => IgxGridExcelStyleFilteringComponent) }], selector: 'igx-grid-excel-style-filtering', templateUrl: './excel-style-filtering.component.html', standalone: true, imports: [IgxExcelStyleHeaderComponent, NgIf, IgxExcelStyleSortingComponent, IgxExcelStyleMovingComponent, IgxExcelStylePinningComponent, IgxExcelStyleHidingComponent, IgxExcelStyleSelectingComponent, IgxExcelStyleClearFiltersComponent, IgxExcelStyleConditionalFilterComponent, IgxExcelStyleSearchComponent, NgClass] }) export class IgxGridExcelStyleFilteringComponent extends BaseFilteringComponent implements OnDestroy { /** * @hidden @internal */ @HostBinding('class.igx-excel-filter') public defaultClass = true; /** * @hidden @internal */ @HostBinding('class.igx-excel-filter--inline') public inline = true; /** * @hidden @internal */ @Output() public loadingStart = new EventEmitter(); /** * @hidden @internal */ @Output() public loadingEnd = new EventEmitter(); /** * @hidden @internal */ @Output() public initialized = new EventEmitter(); /** * @hidden @internal */ @Output() public sortingChanged = new EventEmitter(); /** * @hidden @internal */ @Output() public columnChange = new EventEmitter<ColumnType>(); /** * @hidden @internal */ @Output() public listDataLoaded = new EventEmitter(); @ViewChild('mainDropdown', { read: ElementRef }) public mainDropdown: ElementRef<HTMLElement>; /** * @hidden @internal */ @ContentChild(IgxExcelStyleColumnOperationsTemplateDirective, { read: IgxExcelStyleColumnOperationsTemplateDirective }) public excelColumnOperationsDirective: IgxExcelStyleColumnOperationsTemplateDirective; /** * @hidden @internal */ @ContentChild(IgxExcelStyleFilterOperationsTemplateDirective, { read: IgxExcelStyleFilterOperationsTemplateDirective }) public excelFilterOperationsDirective: IgxExcelStyleFilterOperationsTemplateDirective; /** * @hidden @internal */ @ViewChild('defaultExcelColumnOperations', { read: TemplateRef, static: true }) protected defaultExcelColumnOperations: TemplateRef<any>; /** * @hidden @internal */ @ViewChild('defaultExcelFilterOperations', { read: TemplateRef, static: true }) protected defaultExcelFilterOperations: TemplateRef<any>; /** * An @Input property that sets the column. */ @Input() public set column(value: ColumnType) { this._column = value; this.listData = new Array<FilterListItem>(); this.columnChange.emit(this._column); this.subscriptions?.unsubscribe(); if (this._column) { this.grid.filteringService.registerSVGIcons(); this.init(); this.sortingChanged.emit(); this.subscriptions = this.grid.columnPin.subscribe(() => { requestAnimationFrame(() => { if (!(this.cdr as ViewRef).destroyed) { this.cdr.detectChanges(); } }); }); this.subscriptions.add(this.grid.columnVisibilityChanged.subscribe(() => this.detectChanges())); this.subscriptions.add(this.grid.sortingExpressionsChange.subscribe(() => this.sortingChanged.emit())); this.subscriptions.add(this.grid.filteringExpressionsTreeChange.subscribe(() => this.init())); this.subscriptions.add(this.grid.densityChanged.subscribe(() => this.detectChanges())); this.subscriptions.add(this.grid.columnMovingEnd.subscribe(() => this.cdr.markForCheck())); } } /** * Returns the current column. */ public get column(): ColumnType { return this._column; } /** * @hidden @internal */ public expressionsList = new Array<ExpressionUI>(); /** * @hidden @internal */ public listData = new Array<FilterListItem>(); /** * @hidden @internal */ public uniqueValues: IgxFilterItem[] = []; /** * @hidden @internal */ public overlayService: IgxOverlayService; /** * @hidden @internal */ public overlayComponentId: string; /** * @hidden @internal */ public isHierarchical = false; private _minHeight; /** * Gets the minimum height. */ @Input() public get minHeight(): string { if (this._minHeight || this._minHeight === 0) { return this._minHeight; } if (!this.inline) { let minHeight = 645; switch (this.displayDensity) { case DisplayDensity.cosy: minHeight = 465; break; case DisplayDensity.compact: minHeight = 330; break; default: break; } return `${minHeight}px`; } } /** * Sets the minimum height. */ public set minHeight(value: string) { this._minHeight = value; } private _maxHeight: string; private containsNullOrEmpty = false; private selectAllSelected = true; private selectAllIndeterminate = false; private filterValues = new Set<any>(); private _column: ColumnType; private subscriptions: Subscription; private _originalDisplay: string; /** * Gets the maximum height. */ @Input() @HostBinding('style.max-height') public get maxHeight(): string { if (this._maxHeight) { return this._maxHeight; } if (!this.inline) { let maxHeight = 775; switch (this.displayDensity) { case DisplayDensity.cosy: maxHeight = 565; break; case DisplayDensity.compact: maxHeight = 405; break; default: break; } return `${maxHeight}px`; } } /** * Sets the maximum height. */ public set maxHeight(value: string) { this._maxHeight = value; } /** * @hidden @internal */ public get grid(): GridType { return this.column?.grid ?? this.gridAPI; } /** * @hidden @internal */ public get displayDensity() { return this.grid?.displayDensity; } constructor( cdr: ChangeDetectorRef, element: ElementRef<HTMLElement>, platform: PlatformUtil, @Host() @Optional() @Inject(IGX_GRID_BASE) protected gridAPI?: GridType) { super(cdr, element, platform); } /** * @hidden @internal */ public ngOnDestroy(): void { this.subscriptions?.unsubscribe(); delete this.overlayComponentId; } /** * @hidden @internal */ public initialize(column: ColumnType, overlayService: IgxOverlayService) { this.inline = false; this.column = column; this.overlayService = overlayService; if (this._originalDisplay) { this.element.nativeElement.style.display = this._originalDisplay; } this.initialized.emit(); this.subscriptions.add(this.grid.columnMoving.subscribe(() => this.closeDropdown())); } /** * @hidden @internal */ public onPin() { this.closeDropdown(); this.column.pinned = !this.column.pinned; } /** * @hidden @internal */ public onSelect() { if (!this.column.selected) { this.grid.selectionService.selectColumn(this.column.field, this.grid.columnSelection === GridSelectionMode.single); } else { this.grid.selectionService.deselectColumn(this.column.field); } this.grid.notifyChanges(); } /** * @hidden @internal */ public columnSelectable() { return this.grid?.columnSelection !== GridSelectionMode.none && this.column?.selectable; } /** * @hidden @internal */ public onHideToggle() { this.column.toggleVisibility(); this.closeDropdown(); } /** * @hidden @internal */ public cancel() { if (!this.overlayComponentId) { this.init(); } this.closeDropdown(); } /** * @hidden @internal */ public closeDropdown() { if (this.overlayComponentId) { this.overlayService.hide(this.overlayComponentId); this.overlayComponentId = null; } } /** * @hidden @internal */ public onKeyDown(eventArgs: KeyboardEvent) { if (this.platform.isFilteringKeyCombo(eventArgs)) { eventArgs.preventDefault(); this.closeDropdown(); } eventArgs.stopPropagation(); } /** * @hidden @internal */ public hide() { this._originalDisplay = document.defaultView.getComputedStyle(this.element.nativeElement).display; this.element.nativeElement.style.display = 'none'; } /** * @hidden @internal */ public detectChanges() { this.cdr.detectChanges(); } private init() { this.expressionsList = new Array<ExpressionUI>(); generateExpressionsList(this.column.filteringExpressionsTree, this.grid.filteringLogic, this.expressionsList); this.populateColumnData(); } private areExpressionsSelectable() { if (this.expressionsList.length === 1 && (this.expressionsList[0].expression.condition.name === 'equals' || this.expressionsList[0].expression.condition.name === 'at' || this.expressionsList[0].expression.condition.name === 'true' || this.expressionsList[0].expression.condition.name === 'false' || this.expressionsList[0].expression.condition.name === 'empty' || this.expressionsList[0].expression.condition.name === 'in')) { return true; } const selectableExpressionsCount = this.expressionsList.filter(exp => (exp.beforeOperator === 1 || exp.afterOperator === 1) && (exp.expression.condition.name === 'equals' || exp.expression.condition.name === 'at' || exp.expression.condition.name === 'true' || exp.expression.condition.name === 'false' || exp.expression.condition.name === 'empty' || exp.expression.condition.name === 'in')).length; return selectableExpressionsCount === this.expressionsList.length; } private populateColumnData() { this.cdr.detectChanges(); if (this.grid.uniqueColumnValuesStrategy) { this.renderColumnValuesRemotely(); } else { this.renderColumnValuesFromData(); } } private renderColumnValuesRemotely() { this.loadingStart.emit(); const expressionsTree: FilteringExpressionsTree = this.getColumnFilterExpressionsTree(); const prevColumn = this.column; this.grid.uniqueColumnValuesStrategy(this.column, expressionsTree, (values: any[]) => { if (!this.column || this.column !== prevColumn) { return; } const items = values.map(v => ({ value: v })); this.uniqueValues = this.column.sortStrategy.sort(items, 'value', SortingDirection.Asc, this.column.sortingIgnoreCase, (obj, key) => { let resolvedValue = obj[key]; if (this.column.dataType === GridColumnDataType.Time) { resolvedValue = new Date().setHours( resolvedValue.getHours(), resolvedValue.getMinutes(), resolvedValue.getSeconds(), resolvedValue.getMilliseconds()); } return resolvedValue; }); this.renderValues(); this.loadingEnd.emit(); }); } private renderColumnValuesFromData() { this.loadingStart.emit(); const expressionsTree = this.getColumnFilterExpressionsTree(); const promise = this.grid.filterStrategy.getFilterItems(this.column, expressionsTree); promise.then((items) => { this.isHierarchical = items.length > 0 && items.some(i => i.children && i.children.length > 0); this.uniqueValues = items; this.renderValues(); this.loadingEnd.emit(); this.sortingChanged.emit(); }); } private renderValues() { this.filterValues = this.generateFilterValues(this.column.dataType === GridColumnDataType.Date || this.column.dataType === GridColumnDataType.DateTime); this.generateListData(); } private generateFilterValues(isDateColumn = false) { let filterValues; if (isDateColumn) { filterValues = new Set<any>(this.expressionsList.reduce((arr, e) => { if (e.expression.condition.name === 'in') { return [...arr, ...Array.from((e.expression.searchVal as Set<any>).values()).map(v => new Date(v).toISOString())]; } return [...arr, ...[e.expression.searchVal ? e.expression.searchVal.toISOString() : e.expression.searchVal]]; }, [])); } else if (this.column.dataType === GridColumnDataType.Time) { filterValues = new Set<any>(this.expressionsList.reduce((arr, e) => { if (e.expression.condition.name === 'in') { return [...arr, ...Array.from((e.expression.searchVal as Set<any>).values()).map(v => typeof v === 'string' ? v : new Date(v).toLocaleTimeString())]; } return [...arr, ...[e.expression.searchVal ? e.expression.searchVal.toLocaleTimeString() : e.expression.searchVal]]; }, [])); } else { filterValues = new Set<any>(this.expressionsList.reduce((arr, e) => { if (e.expression.condition.name === 'in') { return [...arr, ...Array.from((e.expression.searchVal as Set<any>).values())]; } return [...arr, ...[e.expression.searchVal]]; }, [])); } return filterValues; } private generateListData() { this.listData = new Array<FilterListItem>(); const shouldUpdateSelection = this.areExpressionsSelectable(); if (this.column.dataType === GridColumnDataType.Boolean) { this.addBooleanItems(); } else { this.addItems(shouldUpdateSelection); } if (!this.isHierarchical && this.containsNullOrEmpty) { const blanksItem = this.generateBlanksItem(shouldUpdateSelection); this.listData.unshift(blanksItem); } if (this.listData.length > 0) { this.addSelectAllItem(); } if (!(this.cdr as any).destroyed) { this.cdr.detectChanges(); } this.listDataLoaded.emit(); } private getColumnFilterExpressionsTree() { const gridExpressionsTree: IFilteringExpressionsTree = this.grid.filteringExpressionsTree; const expressionsTree = new FilteringExpressionsTree(gridExpressionsTree.operator, gridExpressionsTree.fieldName); for (const operand of gridExpressionsTree.filteringOperands) { if (operand instanceof FilteringExpressionsTree) { const columnExprTree = operand as FilteringExpressionsTree; if (columnExprTree.fieldName === this.column.field) { continue; } } expressionsTree.filteringOperands.push(operand); } return expressionsTree; } private addBooleanItems() { this.selectAllSelected = true; this.selectAllIndeterminate = false; this.uniqueValues.forEach(element => { const value = element.value; const filterListItem = new FilterListItem(); if (value !== undefined && value !== null && value !== '') { if (this.column.filteringExpressionsTree) { if (value === true && this.expressionsList.find(exp => exp.expression.condition.name === 'true')) { filterListItem.isSelected = true; filterListItem.isFiltered = true; this.selectAllIndeterminate = true; } else if (value === false && this.expressionsList.find(exp => exp.expression.condition.name === 'false')) { filterListItem.isSelected = true; filterListItem.isFiltered = true; this.selectAllIndeterminate = true; } else { filterListItem.isSelected = false; filterListItem.isFiltered = false; } } else { filterListItem.isSelected = true; filterListItem.isFiltered = true; } filterListItem.value = value; filterListItem.label = value ? this.grid.resourceStrings.igx_grid_filter_true : this.grid.resourceStrings.igx_grid_filter_false; filterListItem.indeterminate = false; this.listData.push(filterListItem); } else { this.containsNullOrEmpty = true; } }); } private addItems(shouldUpdateSelection: boolean) { this.selectAllSelected = true; this.selectAllIndeterminate = false; this.containsNullOrEmpty = false; this.listData = this.generateFilterListItems(this.uniqueValues, shouldUpdateSelection); this.containsNullOrEmpty = this.uniqueValues.length > this.listData.length; } private generateFilterListItems(values: IgxFilterItem[], shouldUpdateSelection: boolean, parent?: FilterListItem) { const filterListItems = []; values?.forEach(element => { const value = element.value; const hasValue = value !== undefined && value !== null && value !== ''; if (hasValue) { const filterListItem = new FilterListItem(); filterListItem.parent = parent; filterListItem.value = value; filterListItem.label = element.label !== undefined ? element.label : this.getFilterItemLabel(value); filterListItem.indeterminate = false; filterListItem.isSelected = true; filterListItem.isFiltered = true; if (this.column.filteringExpressionsTree) { filterListItem.isSelected = false; filterListItem.isFiltered = false; if (shouldUpdateSelection) { const exprValue = this.getExpressionValue(value); if (this.filterValues.has(exprValue)) { filterListItem.isSelected = true; filterListItem.isFiltered = true; } this.selectAllIndeterminate = true; } else { this.selectAllSelected = false; } } filterListItem.children = this.generateFilterListItems(element.children ?? element.value?.children, shouldUpdateSelection, filterListItem); filterListItems.push(filterListItem); } }); return filterListItems; } private addSelectAllItem() { const selectAll = new FilterListItem(); selectAll.isSelected = this.selectAllSelected; selectAll.value = this.grid.resourceStrings.igx_grid_excel_select_all; selectAll.label = this.grid.resourceStrings.igx_grid_excel_select_all; selectAll.indeterminate = this.selectAllIndeterminate; selectAll.isSpecial = true; selectAll.isFiltered = this.selectAllSelected; this.listData.unshift(selectAll); } private generateBlanksItem(shouldUpdateSelection) { const blanks = new FilterListItem(); if (this.column.filteringExpressionsTree) { if (shouldUpdateSelection) { if (this.filterValues.has(null)) { blanks.isSelected = true; blanks.isFiltered = true; } else { blanks.isSelected = false; blanks.isFiltered = false; } } } else { blanks.isSelected = true; blanks.isFiltered = true; } blanks.value = null; blanks.label = this.grid.resourceStrings.igx_grid_excel_blanks; blanks.indeterminate = false; blanks.isSpecial = true; blanks.isBlanks = true; return blanks; } private getFilterItemLabel(value: any, applyFormatter = true, data?: any) { if (this.column.formatter) { if (applyFormatter) { return this.column.formatter(value, data); } return value; } const { display, format, digitsInfo, currencyCode, timezone } = this.column.pipeArgs; const locale = this.grid.locale; switch (this.column.dataType) { case GridColumnDataType.Date: case GridColumnDataType.DateTime: case GridColumnDataType.Time: return formatDate(value, format, locale, timezone); case GridColumnDataType.Currency: return formatCurrency(value, currencyCode || getLocaleCurrencyCode(locale), display, digitsInfo, locale); case GridColumnDataType.Number: return formatNumber(value, locale, digitsInfo); case GridColumnDataType.Percent: return formatPercent(value, locale, digitsInfo); default: return value; } } private getExpressionValue(value: any): string { if (this.column.dataType === GridColumnDataType.Date) { value = value ? new Date(value).toISOString() : value; } else if (this.column.dataType === GridColumnDataType.DateTime) { value = value ? new Date(value).toISOString() : value; } else if (this.column.dataType === GridColumnDataType.Time) { value = value ? new Date(value).toLocaleTimeString() : value; } return value; } }