@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
JavaScript
/**-----------------------------------------------------------------------------------------
* 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">
(hasTemplate) {
(false) {
(!isMultiFilter) {
<ng-container
kendoFilterMenuHost
[filterService]="childService"
[column]="column"
[filter]="childFilter"
[menuTabbingService]="menuTabbingService">
</ng-container>
} {
<kendo-grid-multicheckbox-filter style="display: contents;" [column]="column" (filterChange)="onCheckboxFilterChange($event)"></kendo-grid-multicheckbox-filter>
}
}
(true) {
(column.filterMenuTemplateRef) {
<ng-template
[ngTemplateOutlet]="column.filterMenuTemplateRef"
[ngTemplateOutletContext]="templateContext"
>
</ng-template>
}
}
}
(!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">
(hasTemplate) {
(false) {
(!isMultiFilter) {
<ng-container
kendoFilterMenuHost
[filterService]="childService"
[column]="column"
[filter]="childFilter"
[menuTabbingService]="menuTabbingService">
</ng-container>
} {
<kendo-grid-multicheckbox-filter style="display: contents;" [column]="column" (filterChange)="onCheckboxFilterChange($event)"></kendo-grid-multicheckbox-filter>
}
}
(true) {
(column.filterMenuTemplateRef) {
<ng-template
[ngTemplateOutlet]="column.filterMenuTemplateRef"
[ngTemplateOutletContext]="templateContext"
>
</ng-template>
}
}
}
(!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 }]
}] } });