UNPKG

@progress/kendo-angular-grid

Version:

Kendo UI Grid for Angular - high performance data grid with paging, filtering, virtualization, CRUD, and more.

419 lines (418 loc) 18.2 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { MenuTabbingService } from './menu-tabbing.service'; import { Component, Input, SkipSelf, Output, EventEmitter, ChangeDetectorRef, ElementRef, ViewChild } from '@angular/core'; import { isCompositeFilterDescriptor } from "@progress/kendo-data-query"; import { ColumnComponent } from "../../columns/column.component"; import { FilterService } from "../filter.service"; import { removeFilter, filtersByField } from "../base-filter-cell.component"; import { isPresent, isNullOrEmptyString } from "../../utils"; import { cloneFilters } from '../../common/filter-descriptor-differ'; import { ContextService } from '../../common/provider.service'; import { FilterMenuHostDirective } from './filter-menu-host.directive'; import { NgTemplateOutlet, NgClass } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AdaptiveGridService } from '../../common/adaptiveness.service'; import { MultiCheckboxFilterComponent } from '../multicheckbox-filter.component'; import { ButtonComponent } from '@progress/kendo-angular-buttons'; import { filterClearIcon, filterIcon } from '@progress/kendo-svg-icons'; import { areObjectsEqual } from '@progress/kendo-angular-common'; import * as i0 from "@angular/core"; import * as i1 from "../filter.service"; import * as i2 from "../../common/provider.service"; import * as i3 from "./menu-tabbing.service"; import * as i4 from "../../common/adaptiveness.service"; import * as i5 from "@angular/forms"; const isNoValueOperator = operator => (operator === "isnull" || operator === "isnotnull" || operator === "isempty" || operator === "isnotempty"); /** * @hidden */ export const validFilters = ({ value, operator }) => !isNullOrEmptyString(value) || isNoValueOperator(operator); const trimFilters = (filter) => { const trimComposite = (node) => { const trimmed = []; for (const f of node.filters || []) { if (isCompositeFilterDescriptor(f)) { const child = trimComposite(f); if (child.filters.length) { trimmed.push(child); } } else if (validFilters(f)) { trimmed.push(f); } } return { logic: node.logic || 'and', filters: trimmed }; }; return trimComposite(filter); }; const findParent = (filters, field, parent) => { return filters.reduce((acc, filter) => { if (acc) { return acc; } if (filter.filters) { return findParent(filter.filters, field, filter); } else if (filter.field === field) { return parent; } return acc; }, undefined); }; /** * @hidden */ export const parentLogicOfDefault = (filter, field, def = "and") => { const parent = findParent(((filter || {}).filters || []), field); return isPresent(parent) ? parent.logic : def; }; /** * @hidden */ export class FilterMenuContainerComponent { parentService; childService; ctx; cd; adaptiveGridService; close = new EventEmitter(); /** * The column with which the filter is associated. * @type {ColumnComponent} */ column; /** * @hidden */ isLast; /** * @hidden */ isExpanded; /** * @hidden */ menuTabbingService; /** * The current root filter. * @type {CompositeFilterDescriptor} */ set filter(value) { this._filter = cloneFilters(value); } get filter() { return this._filter; } /** * @hidden */ actionsClass = 'k-actions k-actions-stretched k-actions-horizontal'; get childFilter() { if (!isPresent(this._childFilter)) { this._childFilter = { filters: filtersByField(this.filter, (this.column || {}).field), logic: parentLogicOfDefault(this.filter, (this.column || {}).field) }; } return this._childFilter; } resetButton; _childFilter; subscription; _templateContext = {}; _filter; checkboxFilter; constructor(parentService, childService, ctx, cd, menuTabbingService, adaptiveGridService) { this.parentService = parentService; this.childService = childService; this.ctx = ctx; this.cd = cd; this.adaptiveGridService = adaptiveGridService; this.menuTabbingService = menuTabbingService; this.adaptiveGridService.filterMenuContainer = this; } ngOnInit() { this.subscription = this.childService.changes.subscribe(filter => this._childFilter = filter); this.subscription.add(this.ctx.localization.changes.subscribe(() => this.cd.markForCheck())); } ngAfterViewChecked() { if (!this.menuTabbingService.isColumnMenu || (this.isLast && this.isExpanded)) { this.menuTabbingService.lastFocusable = this.resetButton?.nativeElement; } } ngOnDestroy() { if (this.subscription) { this.subscription.unsubscribe(); } this.menuTabbingService.lastFocusable = undefined; } get disabled() { return this.isMultiFilter ? this.areFiltersEqual : !this.childFilter.filters.some(validFilters); } get templateContext() { this._templateContext.column = this.column; this._templateContext.filter = this.childFilter; this._templateContext.filterService = this.childService; this._templateContext["$implicit"] = this.childFilter; return this._templateContext; } get hasTemplate() { return isPresent(this.column) && isPresent(this.column.filterMenuTemplateRef); } submit() { if (this.isMultiFilter) { this.parentService.filter(this.checkboxFilter); } else { const filter = trimFilters(this.childFilter); if (filter.filters.length) { const root = this.filter || { filters: [], logic: "and" }; removeFilter(root, this.column.field); root.filters.push(filter); this.parentService.filter(root); } } this.close.emit(); return false; } reset() { const root = this.filter || { filters: [], logic: "and" }; removeFilter(root, this.column.field); this.parentService.filter(root); this.close.emit(); } resetChildFilters() { this._childFilter = null; } onTab(e, buttonType) { if (this.menuTabbingService.firstFocusable && (!this.menuTabbingService.isColumnMenu || this.isLast)) { e.preventDefault(); if (buttonType === 'reset') { this.menuTabbingService.firstFocusable.focus(); } else { this.disabled ? this.menuTabbingService.firstFocusable.focus() : this.resetButton.nativeElement.focus(); } } } onCheckboxFilterChange(filter) { this.checkboxFilter = filter; } getButtonIcon(buttonType, iconType) { if (!this.isMultiFilter) { return; } const icons = { filter: { icon: 'filter', svgIcon: filterIcon }, reset: { icon: 'filter-clear', svgIcon: filterClearIcon } }; return icons[buttonType]?.[iconType]; } get clearText() { return this.ctx.localization.get("filterClearButton"); } get filterText() { return this.ctx.localization.get("filterFilterButton"); } get isMultiFilter() { if (!isPresent(this.column?.filterVariant)) { return false; } const filterVariant = this.column?.filterVariant; return isPresent(filterVariant) && (filterVariant === 'multiCheckbox' || typeof filterVariant === 'object' && filterVariant.variant === 'multiCheckbox'); } get areFiltersEqual() { const checkboxFilter = this.checkboxFilter; const gridFilter = this.filter; const isComposite = (f) => !!f && Array.isArray(f.filters); // Treat undefined and "empty (no inner filters)" as equivalent const isEmptyComposite = (f) => isComposite(f) && f.filters.length === 0; if (!checkboxFilter && !gridFilter) { return true; } if ((!checkboxFilter && isEmptyComposite(gridFilter)) || (!gridFilter && isEmptyComposite(checkboxFilter))) { return true; } if (!checkboxFilter || !gridFilter) { return false; } const eq = (x, y) => { const xIsComp = isComposite(x); const yIsComp = isComposite(y); if (xIsComp !== yIsComp) { return false; } if (xIsComp) { const xLogic = x.logic || 'and'; const yLogic = y.logic || 'and'; if (xLogic !== yLogic) { return false; } if (x.filters.length !== y.filters.length) { return false; } for (let i = 0; i < x.filters.length; i++) { if (!eq(x.filters[i], y.filters[i])) { return false; } } return true; } return areObjectsEqual(x, y); }; return eq(checkboxFilter, gridFilter); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FilterMenuContainerComponent, deps: [{ token: i1.FilterService, skipSelf: true }, { token: i1.FilterService }, { token: i2.ContextService }, { token: i0.ChangeDetectorRef }, { token: i3.MenuTabbingService }, { token: i4.AdaptiveGridService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FilterMenuContainerComponent, isStandalone: true, selector: "kendo-grid-filter-menu-container", inputs: { column: "column", isLast: "isLast", isExpanded: "isExpanded", menuTabbingService: "menuTabbingService", filter: "filter", actionsClass: "actionsClass" }, outputs: { close: "close" }, providers: [ FilterService, MenuTabbingService ], viewQueries: [{ propertyName: "resetButton", first: true, predicate: ["resetButton"], descendants: true }], ngImport: i0, template: ` <form (submit)="submit()" (reset)="reset()" class="k-filter-menu" [ngClass]="{'k-popup k-group k-reset': isMultiFilter && !ctx.grid?.isActionSheetExpanded}"> <div class="k-filter-menu-container"> @switch (hasTemplate) { @case (false) { @if (!isMultiFilter) { <ng-container kendoFilterMenuHost [filterService]="childService" [column]="column" [filter]="childFilter" [menuTabbingService]="menuTabbingService"> </ng-container> } @else { <kendo-grid-multicheckbox-filter style="display: contents;" [column]="column" (filterChange)="onCheckboxFilterChange($event)"></kendo-grid-multicheckbox-filter> } } @case (true) { @if (column.filterMenuTemplateRef) { <ng-template [ngTemplateOutlet]="column.filterMenuTemplateRef" [ngTemplateOutletContext]="templateContext" > </ng-template> } } } @if (!ctx.grid?.isActionSheetExpanded) { <div [ngClass]="actionsClass"> <button #filterButton kendoButton themeColor="primary" type="submit" [ngClass]="{'k-button-rectangle': !isMultiFilter}" [disabled]="disabled" [icon]="getButtonIcon('filter', 'icon')" [svgIcon]="getButtonIcon('filter', 'svgIcon')" (keydown.tab)="onTab($event, 'filter')">{{filterText}}</button> <button #resetButton kendoButton type="reset" [ngClass]="{'k-button-rectangle': !isMultiFilter}" [icon]="getButtonIcon('reset', 'icon')" [svgIcon]="getButtonIcon('reset', 'svgIcon')" (keydown.tab)="onTab($event, 'reset')">{{clearText}}</button> </div> } </div> </form> `, isInline: true, dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i5.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i5.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i5.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: FilterMenuHostDirective, selector: "[kendoFilterMenuHost]", inputs: ["filterService", "menuTabbingService"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MultiCheckboxFilterComponent, selector: "kendo-grid-multicheckbox-filter", inputs: ["column"], outputs: ["filterChange"] }, { kind: "component", type: ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FilterMenuContainerComponent, decorators: [{ type: Component, args: [{ providers: [ FilterService, MenuTabbingService ], selector: 'kendo-grid-filter-menu-container', template: ` <form (submit)="submit()" (reset)="reset()" class="k-filter-menu" [ngClass]="{'k-popup k-group k-reset': isMultiFilter && !ctx.grid?.isActionSheetExpanded}"> <div class="k-filter-menu-container"> @switch (hasTemplate) { @case (false) { @if (!isMultiFilter) { <ng-container kendoFilterMenuHost [filterService]="childService" [column]="column" [filter]="childFilter" [menuTabbingService]="menuTabbingService"> </ng-container> } @else { <kendo-grid-multicheckbox-filter style="display: contents;" [column]="column" (filterChange)="onCheckboxFilterChange($event)"></kendo-grid-multicheckbox-filter> } } @case (true) { @if (column.filterMenuTemplateRef) { <ng-template [ngTemplateOutlet]="column.filterMenuTemplateRef" [ngTemplateOutletContext]="templateContext" > </ng-template> } } } @if (!ctx.grid?.isActionSheetExpanded) { <div [ngClass]="actionsClass"> <button #filterButton kendoButton themeColor="primary" type="submit" [ngClass]="{'k-button-rectangle': !isMultiFilter}" [disabled]="disabled" [icon]="getButtonIcon('filter', 'icon')" [svgIcon]="getButtonIcon('filter', 'svgIcon')" (keydown.tab)="onTab($event, 'filter')">{{filterText}}</button> <button #resetButton kendoButton type="reset" [ngClass]="{'k-button-rectangle': !isMultiFilter}" [icon]="getButtonIcon('reset', 'icon')" [svgIcon]="getButtonIcon('reset', 'svgIcon')" (keydown.tab)="onTab($event, 'reset')">{{clearText}}</button> </div> } </div> </form> `, standalone: true, imports: [FormsModule, FilterMenuHostDirective, NgTemplateOutlet, NgClass, MultiCheckboxFilterComponent, ButtonComponent] }] }], ctorParameters: () => [{ type: i1.FilterService, decorators: [{ type: SkipSelf }] }, { type: i1.FilterService }, { type: i2.ContextService }, { type: i0.ChangeDetectorRef }, { type: i3.MenuTabbingService }, { type: i4.AdaptiveGridService }], propDecorators: { close: [{ type: Output }], column: [{ type: Input }], isLast: [{ type: Input }], isExpanded: [{ type: Input }], menuTabbingService: [{ type: Input }], filter: [{ type: Input }], actionsClass: [{ type: Input }], resetButton: [{ type: ViewChild, args: ['resetButton', { static: false }] }] } });