UNPKG

@progress/kendo-angular-filter

Version:
612 lines (539 loc) 28.5 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Component, Input, Output, EventEmitter, HostBinding, isDevMode, ContentChildren, QueryList, HostListener, ElementRef, ViewChildren, Renderer2 } from '@angular/core'; import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n'; import { FilterService } from './filter.service'; import { nullOperators, isPresent } from './util'; import { ChangeDetectorRef } from '@angular/core'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from './package-metadata'; import { FilterFieldComponent } from './filter-field.component'; import { FilterItem } from './util'; import { NavigationService } from './navigation.service'; import { Keys } from '@progress/kendo-angular-common'; import { FilterErrorMessages } from './error-messages'; import { FilterGroupComponent } from './filter-group.component'; import { LocalizedMessagesDirective } from './localization/localized-messages.directive'; import * as i0 from "@angular/core"; import * as i1 from "./filter.service"; import * as i2 from "@progress/kendo-angular-l10n"; import * as i3 from "./navigation.service"; /** * Represents the [Kendo UI Filter component for Angular]({% slug overview_filter %}). * The Filter component enables users to define and apply complex filter criteria. * * @example * ```ts * @Component({ * selector: 'my-app', * template: ` * <kendo-filter [filters]="filters" (valueChange)="onValueChange($event)"></kendo-filter> * ` * }) * export class AppComponent { * public filters: FilterExpression[] = [ * { * name: 'country', * label: 'Country', * filter: 'string', * operators: ['eq', 'neq'], * }, * { * name: 'budget', * filter: 'number' * } * ]; * * onValueChange(e: CompositeFilterDescriptor) { * console.log(e); * } * } * ``` * * @remarks * Supported children components are: {@link CustomMessagesComponent}, {@link FilterFieldComponent}. */ export class FilterComponent { filterService; localization; cdr; element; navigationService; renderer; /** * @hidden */ focusout() { setTimeout(() => { if (!(document.activeElement.closest('.k-filter'))) { this.renderer.setAttribute(this.navigationService.currentlyFocusedElement, 'tabindex', '-1'); this.navigationService.currentlyFocusedElement = this.navigationService.flattenFilterItems[this.navigationService.currentToolbarItemIndex].focusableChildren[0]; this.renderer.setAttribute(this.navigationService.currentlyFocusedElement, 'tabindex', '0'); this.navigationService.isFilterNavigationActivated = false; this.navigationService.isFilterExpressionComponentFocused = false; } }); } /** * @hidden */ focusin() { if (this.navigationService.isFilterNavigationActivated) { return; } this.navigationService.isFilterNavigationActivated = true; this.navigationService.currentToolbarItemChildrenIndex = 0; } /** * @hidden */ onKeydown(event) { const keyCode = event.keyCode; const keys = [Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowLeft, Keys.ArrowRight, Keys.Enter, Keys.Escape, Keys.Tab]; if (keys.indexOf(keyCode) > -1) { this.navigationService.processKeyDown(keyCode, event); } } direction; /** * Specifies the available user-defined filters. At least one filter should be provided. */ set filters(_filters) { if (_filters.length > 0) { this.filterService.filters = _filters.map(filterExpression => { const clonedFilter = Object.assign({}, filterExpression); if (!clonedFilter.title) { clonedFilter.title = clonedFilter.field; } return clonedFilter; }); this.setValue(this.value); } } get filters() { return this.filterService.filters; } /** * Sets the initial `value` of the Filter component. */ set value(value) { const clonedValue = structuredClone(value); if (this.filtersTypeChanged(this.value, clonedValue)) { // due to tracking group items by index, the filters should first be set to [] when the value is changed programmatically this.currentFilter.filters = []; this.cdr.detectChanges(); } this._value = clonedValue; if (this.filters.length > 0) { this.setValue(this.value); } } get value() { return this._value; } /** * Fires every time the Filter component value is updated. * That is each time a Filter Group or Filter Expression is added, removed, or updated. */ valueChange = new EventEmitter(); localizationSubscription; filterFieldsSubscription; _value = { filters: [], logic: 'and' }; filterFields; _filterItems; get filterItems() { return this._filterItems.toArray(); } get toolbarElement() { return this.element.nativeElement.querySelector('.k-toolbar'); } constructor(filterService, localization, cdr, element, navigationService, renderer) { this.filterService = filterService; this.localization = localization; this.cdr = cdr; this.element = element; this.navigationService = navigationService; this.renderer = renderer; validatePackage(packageMetadata); this.direction = localization.rtl ? 'rtl' : 'ltr'; } ngOnInit() { this.localizationSubscription = this.localization.changes.subscribe(({ rtl }) => { this.direction = rtl ? 'rtl' : 'ltr'; this.cdr.detectChanges(); }); } ngAfterViewInit() { this.filterFieldsChanged(); this.filterFieldsSubscription = this.filterFields.changes.subscribe(this.filterFieldsChanged.bind(this)); } ngOnDestroy() { if (this.localizationSubscription) { this.localizationSubscription.unsubscribe(); } if (this.filterFieldsSubscription) { this.filterFieldsSubscription.unsubscribe(); } } filterFieldsChanged() { if (this.filterFields && this.filterFields.length > 0) { this.filters = this.filterFields.map((filterField) => ({ ...filterField, title: filterField.title, editorTemplate: filterField.editorTemplate?.templateRef })); } if (this.filters.length === 0) { throw new Error(FilterErrorMessages.missingFilters); } this.navigationService.reset(this.filterItems); if (!this.navigationService.currentlyFocusedElement) { const firstElement = this.navigationService.flattenFilterItems[0].focusableChildren[0]; this.navigationService.currentlyFocusedElement = firstElement; this.renderer.setAttribute(firstElement, 'tabindex', '0'); } } _currentFilter = { logic: 'and', filters: [] }; /** * @hidden */ get currentFilter() { return this._currentFilter; } /** * @hidden */ set currentFilter(value) { this._currentFilter = value; } /** * @hidden */ onValueChange(isRemoveOperation) { this.cdr.detectChanges(); this.valueChange.emit(this.filterService.normalizedValue); this.navigationService.reset(this.filterItems); if (isRemoveOperation) { if (this.navigationService.currentToolbarItemIndex === this.navigationService.flattenFilterItems.length) { this.navigationService.currentToolbarItemIndex -= 1; } this.navigationService.isFilterExpressionComponentFocused = false; const itemIndex = this.navigationService.currentToolbarItemIndex; const toolbarItem = this.navigationService.flattenFilterItems[itemIndex]; const activeChildIndex = toolbarItem.focusableChildren.length - 1; this.navigationService.currentlyFocusedElement = toolbarItem.focusableChildren[activeChildIndex]; this.renderer.setAttribute(this.navigationService.currentlyFocusedElement, 'tabindex', '0'); this.renderer.addClass(this.navigationService.currentlyFocusedElement, 'k-focus'); this.navigationService.currentlyFocusedElement.focus(); } } normalizeFilter(filterDescriptor) { const foundFilter = this.filterService.filters.find((filter) => filter.field === filterDescriptor.field); if (isDevMode() && !foundFilter) { throw new Error(FilterErrorMessages.missingFilterForUsedField(filterDescriptor.field)); } if (isDevMode() && foundFilter.editor === 'boolean' && (!filterDescriptor.value && filterDescriptor.value !== false)) { console.warn(FilterErrorMessages.missingValueForBooleanField(filterDescriptor.field)); } if (isDevMode() && foundFilter.editor === 'boolean' && filterDescriptor.operator !== 'eq') { console.warn(FilterErrorMessages.operatorBooleanField(filterDescriptor.field)); } if (filterDescriptor.operator && foundFilter.operators && !foundFilter.operators.some(operator => operator === filterDescriptor.operator)) { throw new Error(FilterErrorMessages.filterMissingUsedOperator(filterDescriptor.field, filterDescriptor.operator)); } if (foundFilter.editor === 'boolean') { filterDescriptor.operator = 'eq'; } if (foundFilter.editor === 'date' && filterDescriptor.value) { filterDescriptor.value = new Date(filterDescriptor.value); } if (!isPresent(filterDescriptor.value)) { filterDescriptor.value = null; } if (nullOperators.indexOf(filterDescriptor.operator) >= 0) { filterDescriptor.value = null; } } setValue(items) { this.normalizeValue(items); this.filterService.normalizedValue = items; this.currentFilter = items; this.cdr.detectChanges(); this.navigationService.reset(this.filterItems); } normalizeValue(items) { if (!this.filterService.filters.length) { return; } items.filters.forEach((item) => { if (item.filters) { this.normalizeValue(item); } else { this.normalizeFilter(item); } }); } /** * @hidden */ messageFor(key) { return this.localization.get(key); } /** * @hidden */ filtersTypeChanged(previousValue, newValue) { if (!(previousValue?.filters) && !(newValue?.filters)) { return previousValue?.operator != newValue?.operator || previousValue?.field != newValue?.field; } if (!(previousValue?.filters) || !(newValue?.filters)) { return true; } const previousFilters = previousValue.filters; const newFilters = newValue.filters; if (previousFilters.length !== newFilters.length) { return true; } for (let i = 0; i < previousFilters.length; i++) { if (this.filtersTypeChanged(previousFilters[i], newFilters[i])) { return true; } } return false; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FilterComponent, deps: [{ token: i1.FilterService }, { token: i2.LocalizationService }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i3.NavigationService }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: FilterComponent, isStandalone: true, selector: "kendo-filter", inputs: { filters: "filters", value: "value" }, outputs: { valueChange: "valueChange" }, host: { listeners: { "focusout": "focusout($event)", "focusin": "focusin($event)", "keydown": "onKeydown($event)" }, properties: { "attr.dir": "this.direction" } }, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.filter' }, FilterService, NavigationService ], queries: [{ propertyName: "filterFields", predicate: FilterFieldComponent }], viewQueries: [{ propertyName: "_filterItems", predicate: FilterItem, descendants: true }], ngImport: i0, template: ` <ng-container kendoFilterLocalizedMessages i18n-editorDateTodayText="kendo.filter.editorDateTodayText|The text of the Today button of the Date editor" editorDateTodayText="Today" i18n-editorDateToggleText="kendo.filter.editorDateToggleText|The title of the Toggle button of the Date editor." editorDateToggleText="Toggle calendar" i18n-editorNumericDecrement="kendo.filter.editorNumericDecrement|The title of the Decrement button of the Numeric editor" editorNumericDecrement="Decrement" i18n-editorNumericIncrement="kendo.filter.editorNumericIncrement|The title of the Increment button of the Numeric editor" editorNumericIncrement="Increment" i18n-filterExpressionOperators="kendo.filter.filterExpressionOperators|The text of the Filter Expression Operators drop down" filterExpressionOperators="Operators" i18n-filterExpressionFilters="kendo.filter.filterExpressionFilters|The text of the Filter Expression filters drop down" filterExpressionFilters="Fields" i18n-remove="kendo.filter.remove|The text of the Remove button" remove="Remove" i18n-addFilter="kendo.filter.addFilter|The text of the Add Filter button" addFilter="Add Filter" i18n-addGroup="kendo.filter.addGroup|The text of the Add Group button" addGroup="Add Group" i18n-filterAndLogic="kendo.filter.filterAndLogic|The text of the And filter logic" filterAndLogic="And" i18n-filterOrLogic="kendo.filter.filterOrLogic|The text of the Or filter logic" filterOrLogic="Or" i18n-filterEqOperator="kendo.filter.filterEqOperator|The text of the equal filter operator" filterEqOperator="Is equal to" i18n-filterNotEqOperator="kendo.filter.filterNotEqOperator|The text of the not equal filter operator" filterNotEqOperator="Is not equal to" i18n-filterIsNullOperator="kendo.filter.filterIsNullOperator|The text of the is null filter operator" filterIsNullOperator="Is null" i18n-filterIsNotNullOperator="kendo.filter.filterIsNotNullOperator|The text of the is not null filter operator" filterIsNotNullOperator="Is not null" i18n-filterIsEmptyOperator="kendo.filter.filterIsEmptyOperator|The text of the is empty filter operator" filterIsEmptyOperator="Is empty" i18n-filterIsNotEmptyOperator="kendo.filter.filterIsNotEmptyOperator|The text of the is not empty filter operator" filterIsNotEmptyOperator="Is not empty" i18n-filterStartsWithOperator="kendo.filter.filterStartsWithOperator|The text of the starts with filter operator" filterStartsWithOperator="Starts with" i18n-filterContainsOperator="kendo.filter.filterContainsOperator|The text of the contains filter operator" filterContainsOperator="Contains" i18n-filterNotContainsOperator="kendo.filter.filterNotContainsOperator|The text of the does not contain filter operator" filterNotContainsOperator="Does not contain" i18n-filterEndsWithOperator="kendo.filter.filterEndsWithOperator|The text of the ends with filter operator" filterEndsWithOperator="Ends with" i18n-filterGteOperator="kendo.filter.filterGteOperator|The text of the greater than or equal filter operator" filterGteOperator="Is greater than or equal to" i18n-filterGtOperator="kendo.filter.filterGtOperator|The text of the greater than filter operator" filterGtOperator="Is greater than" i18n-filterLteOperator="kendo.filter.filterLteOperator|The text of the less than or equal filter operator" filterLteOperator="Is less than or equal to" i18n-filterLtOperator="kendo.filter.filterLtOperator|The text of the less than filter operator" filterLtOperator="Is less than" i18n-filterIsTrue="kendo.filter.filterIsTrue|The text of the IsTrue boolean filter option" filterIsTrue="Is True" i18n-filterIsFalse="kendo.filter.filterIsFalse|The text of the IsFalse boolean filter option" filterIsFalse="Is False" i18n-filterBooleanAll="kendo.filter.filterBooleanAll|The text of the (All) boolean filter option" filterBooleanAll="(All)" i18n-filterAfterOrEqualOperator="kendo.filter.filterAfterOrEqualOperator|The text of the after or equal date filter operator" filterAfterOrEqualOperator="Is after or equal to" i18n-filterAfterOperator="kendo.filter.filterAfterOperator|The text of the after date filter operator" filterAfterOperator="Is after" i18n-filterBeforeOperator="kendo.filter.filterBeforeOperator|The text of the before date filter operator" filterBeforeOperator="Is before" i18n-filterBeforeOrEqualOperator="kendo.filter.filterBeforeOrEqualOperator|The text of the before or equal date filter operator" filterBeforeOrEqualOperator="Is before or equal to" i18n-filterFieldAriaLabel="kendo.filter.filterFieldAriaLabel|The text of the filter field aria label" filterFieldAriaLabel="field" i18n-filterOperatorAriaLabel="kendo.filter.filterOperatorAriaLabel|The text of the filter operator aria label" filterOperatorAriaLabel="operator" i18n-filterValueAriaLabel="kendo.filter.filterValueAriaLabel|The text of the filter value aria label" filterValueAriaLabel="value" i18n-filterToolbarAriaLabel="kendo.filter.filterToolbarAriaLabel|The text of the filter row aria label" filterToolbarAriaLabel="filter row settings" i18n-filterComponentAriaLabel="kendo.filter.filterComponentAriaLabel|The text of the filter component aria label" filterComponentAriaLabel="filter component" > </ng-container> <div class="k-filter" [attr.dir]="direction"> <ul class='k-filter-container' role="tree" [attr.aria-label]="messageFor('filterComponentAriaLabel')"> <li class='k-filter-group-main' role="treeitem"> <kendo-filter-group [currentItem]="currentFilter" (valueChange)="onValueChange($event)" > </kendo-filter-group> </li> </ul> </div> `, isInline: true, dependencies: [{ kind: "directive", type: LocalizedMessagesDirective, selector: "[kendoFilterLocalizedMessages]" }, { kind: "component", type: FilterGroupComponent, selector: "kendo-filter-group", inputs: ["currentItem"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FilterComponent, decorators: [{ type: Component, args: [{ providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.filter' }, FilterService, NavigationService ], selector: 'kendo-filter', template: ` <ng-container kendoFilterLocalizedMessages i18n-editorDateTodayText="kendo.filter.editorDateTodayText|The text of the Today button of the Date editor" editorDateTodayText="Today" i18n-editorDateToggleText="kendo.filter.editorDateToggleText|The title of the Toggle button of the Date editor." editorDateToggleText="Toggle calendar" i18n-editorNumericDecrement="kendo.filter.editorNumericDecrement|The title of the Decrement button of the Numeric editor" editorNumericDecrement="Decrement" i18n-editorNumericIncrement="kendo.filter.editorNumericIncrement|The title of the Increment button of the Numeric editor" editorNumericIncrement="Increment" i18n-filterExpressionOperators="kendo.filter.filterExpressionOperators|The text of the Filter Expression Operators drop down" filterExpressionOperators="Operators" i18n-filterExpressionFilters="kendo.filter.filterExpressionFilters|The text of the Filter Expression filters drop down" filterExpressionFilters="Fields" i18n-remove="kendo.filter.remove|The text of the Remove button" remove="Remove" i18n-addFilter="kendo.filter.addFilter|The text of the Add Filter button" addFilter="Add Filter" i18n-addGroup="kendo.filter.addGroup|The text of the Add Group button" addGroup="Add Group" i18n-filterAndLogic="kendo.filter.filterAndLogic|The text of the And filter logic" filterAndLogic="And" i18n-filterOrLogic="kendo.filter.filterOrLogic|The text of the Or filter logic" filterOrLogic="Or" i18n-filterEqOperator="kendo.filter.filterEqOperator|The text of the equal filter operator" filterEqOperator="Is equal to" i18n-filterNotEqOperator="kendo.filter.filterNotEqOperator|The text of the not equal filter operator" filterNotEqOperator="Is not equal to" i18n-filterIsNullOperator="kendo.filter.filterIsNullOperator|The text of the is null filter operator" filterIsNullOperator="Is null" i18n-filterIsNotNullOperator="kendo.filter.filterIsNotNullOperator|The text of the is not null filter operator" filterIsNotNullOperator="Is not null" i18n-filterIsEmptyOperator="kendo.filter.filterIsEmptyOperator|The text of the is empty filter operator" filterIsEmptyOperator="Is empty" i18n-filterIsNotEmptyOperator="kendo.filter.filterIsNotEmptyOperator|The text of the is not empty filter operator" filterIsNotEmptyOperator="Is not empty" i18n-filterStartsWithOperator="kendo.filter.filterStartsWithOperator|The text of the starts with filter operator" filterStartsWithOperator="Starts with" i18n-filterContainsOperator="kendo.filter.filterContainsOperator|The text of the contains filter operator" filterContainsOperator="Contains" i18n-filterNotContainsOperator="kendo.filter.filterNotContainsOperator|The text of the does not contain filter operator" filterNotContainsOperator="Does not contain" i18n-filterEndsWithOperator="kendo.filter.filterEndsWithOperator|The text of the ends with filter operator" filterEndsWithOperator="Ends with" i18n-filterGteOperator="kendo.filter.filterGteOperator|The text of the greater than or equal filter operator" filterGteOperator="Is greater than or equal to" i18n-filterGtOperator="kendo.filter.filterGtOperator|The text of the greater than filter operator" filterGtOperator="Is greater than" i18n-filterLteOperator="kendo.filter.filterLteOperator|The text of the less than or equal filter operator" filterLteOperator="Is less than or equal to" i18n-filterLtOperator="kendo.filter.filterLtOperator|The text of the less than filter operator" filterLtOperator="Is less than" i18n-filterIsTrue="kendo.filter.filterIsTrue|The text of the IsTrue boolean filter option" filterIsTrue="Is True" i18n-filterIsFalse="kendo.filter.filterIsFalse|The text of the IsFalse boolean filter option" filterIsFalse="Is False" i18n-filterBooleanAll="kendo.filter.filterBooleanAll|The text of the (All) boolean filter option" filterBooleanAll="(All)" i18n-filterAfterOrEqualOperator="kendo.filter.filterAfterOrEqualOperator|The text of the after or equal date filter operator" filterAfterOrEqualOperator="Is after or equal to" i18n-filterAfterOperator="kendo.filter.filterAfterOperator|The text of the after date filter operator" filterAfterOperator="Is after" i18n-filterBeforeOperator="kendo.filter.filterBeforeOperator|The text of the before date filter operator" filterBeforeOperator="Is before" i18n-filterBeforeOrEqualOperator="kendo.filter.filterBeforeOrEqualOperator|The text of the before or equal date filter operator" filterBeforeOrEqualOperator="Is before or equal to" i18n-filterFieldAriaLabel="kendo.filter.filterFieldAriaLabel|The text of the filter field aria label" filterFieldAriaLabel="field" i18n-filterOperatorAriaLabel="kendo.filter.filterOperatorAriaLabel|The text of the filter operator aria label" filterOperatorAriaLabel="operator" i18n-filterValueAriaLabel="kendo.filter.filterValueAriaLabel|The text of the filter value aria label" filterValueAriaLabel="value" i18n-filterToolbarAriaLabel="kendo.filter.filterToolbarAriaLabel|The text of the filter row aria label" filterToolbarAriaLabel="filter row settings" i18n-filterComponentAriaLabel="kendo.filter.filterComponentAriaLabel|The text of the filter component aria label" filterComponentAriaLabel="filter component" > </ng-container> <div class="k-filter" [attr.dir]="direction"> <ul class='k-filter-container' role="tree" [attr.aria-label]="messageFor('filterComponentAriaLabel')"> <li class='k-filter-group-main' role="treeitem"> <kendo-filter-group [currentItem]="currentFilter" (valueChange)="onValueChange($event)" > </kendo-filter-group> </li> </ul> </div> `, standalone: true, imports: [LocalizedMessagesDirective, FilterGroupComponent] }] }], ctorParameters: function () { return [{ type: i1.FilterService }, { type: i2.LocalizationService }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i3.NavigationService }, { type: i0.Renderer2 }]; }, propDecorators: { focusout: [{ type: HostListener, args: ['focusout', ['$event']] }], focusin: [{ type: HostListener, args: ['focusin', ['$event']] }], onKeydown: [{ type: HostListener, args: ['keydown', ['$event']] }], direction: [{ type: HostBinding, args: ['attr.dir'] }], filters: [{ type: Input }], value: [{ type: Input }], valueChange: [{ type: Output }], filterFields: [{ type: ContentChildren, args: [FilterFieldComponent] }], _filterItems: [{ type: ViewChildren, args: [FilterItem] }] } });