UNPKG

@progress/kendo-angular-grid

Version:

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

340 lines (339 loc) 12.3 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Directive, Input, ChangeDetectorRef, isDevMode } from '@angular/core'; import { DataBindingDirective } from "../databinding.directive"; import { filterBy, process } from "@progress/kendo-data-query"; import { isPresent, isArray } from "../utils"; import { diffFilters, cloneFilters } from "../common/filter-descriptor-differ"; import { LocalDataChangesService } from "../editing/local-data-changes.service"; import { GridConfigurationErrorMessages } from '../common/error-messages'; import { ContextService } from '../common/provider.service'; import { GroupsService } from './groups.service'; import { Subscription } from 'rxjs'; import * as i0 from "@angular/core"; import * as i1 from "../editing/local-data-changes.service"; import * as i2 from "../common/provider.service"; import * as i3 from "./groups.service"; const hasGroups = (items) => items && items.length && items[0].field && items[0].items; const groupDescriptorsPresent = (descriptors) => isPresent(descriptors) && descriptors.length > 0; const processGroups = (data, state) => process(data, state).data; const removeParentDescriptors = (parents, owner) => g => g.field !== owner.field && !parents.some(y => y.field === g.field); const findGroup = (groupIndex, groups) => { const parents = []; return { group: groupIndex.split("_").reduce((acc, x) => { const idx = parseInt(x, 10); if (acc.items) { parents.push(acc); return acc.items[idx]; } return isArray(acc) ? acc[idx] : acc; }, groups), parents }; }; const findChildren = (data, parents) => { const filters = parents.map(p => ({ field: p.field, operator: "eq", value: p.value })); return filterBy(data, { filters: filters, logic: "and" }); }; /** * @hidden */ export const count = (groups, includeFooters = false) => (groups.reduce((acc, group) => { if (!group.skipHeader) { acc++; } if (group.items) { const children = count(group.items, includeFooters); if (includeFooters && children && !group.hideFooter) { acc++; } acc += children; } return acc; }, 0)); /** * @hidden */ export const noDescriptors = (descriptors) => !isPresent(descriptors) || !descriptors.length; /** * @hidden */ export const slice = (groups, skip, take, includeFooters = false) => { if (!isPresent(take)) { return groups; } const result = []; for (let idx = 0, length = groups.length; idx < length; idx++) { if (take <= 0) { break; } const group = groups[idx]; const groupItems = group.items; let itemCount = count(groupItems, includeFooters); if (includeFooters && groupItems.length) { itemCount++; } const skipHeader = skip > 0; if (skip) { skip--; if (itemCount && skip >= itemCount) { skip -= itemCount; continue; } } if (!skipHeader || itemCount) { const items = []; let hideFooter = true; if (!skipHeader) { take--; } if (take) { if (hasGroups(groupItems)) { const children = slice(groupItems, skip, take, includeFooters); items.push(...children); take -= count(children, includeFooters); } else { items.push(...groupItems.slice(skip, Math.min(skip + take, groupItems.length))); take -= items.length; } if (take && includeFooters) { hideFooter = false; take--; } skip = 0; } result.push({ aggregates: group.aggregates, field: group.field, hideFooter, items, offset: idx, skipHeader, value: group.value }); } } return result; }; const skippedHeaders = (groupItem) => { let total = 0; while (groupItem) { if (groupItem.skipHeader) { total++; } groupItem = groupItem.items && groupItem.items[0] || null; } return total; }; /** * A directive which encapsulates the in-memory handling of grouping with virtual scrolling. */ export class GroupBindingDirective extends DataBindingDirective { groupsService; /** * The array of data which will be used to populate the Grid. */ set kendoGridGroupBinding(value) { this.groups = null; this.grid.resetGroupsState(); this.data = value; } /** * @hidden */ set data(value) { this.originalData = value || []; this.dataChanged = true; } /** * Defines the descriptors by which the data will be sorted. */ set sort(value) { const noCurrentDescriptors = noDescriptors(this.state.sort); const noIncomingDescriptors = noDescriptors(value); const clear = this.state.sort !== value && !(noCurrentDescriptors && noIncomingDescriptors); this.grid.sort = this.state.sort = value; if (clear) { this.groups = null; this.grid.resetGroupsState(); } } /** * Defines the descriptor by which the data will be filtered. */ set filter(value) { const clear = diffFilters(this.state.filter, value); if (clear) { this.state.filter = value; this.grid.filter = cloneFilters(value); this.groups = null; this.grid.resetGroupsState(); } } /** * Defines the descriptors by which the data will be grouped. */ set group(value) { // don't clear if no groups are present in previous and current value const groupsPresent = groupDescriptorsPresent(this.state.group) || groupDescriptorsPresent(value); const clear = this.state.group !== value && groupsPresent; this.grid.group = this.state.group = value; if (clear) { this.groups = null; this.grid.resetGroupsState(); this.skip = 0; } } groups; gridSubs = new Subscription(); constructor(changeDetector, localDataChangesService, ctxService, groupsService) { super(ctxService.grid, changeDetector, localDataChangesService); this.groupsService = groupsService; ctxService.groupBindingDirective = this; } ngOnInit() { super.ngOnInit(); this.gridSubs.add(this.grid.groupExpand.subscribe(this.groupExpand.bind(this))); this.gridSubs.add(this.grid.groupCollapse.subscribe(this.groupCollapse.bind(this))); } ngAfterContentInit() { if (isDevMode() && this.grid.isGroupExpanded) { throw new Error(GridConfigurationErrorMessages.groupBindingDirectives); } } ngOnDestroy() { this.gridSubs.unsubscribe(); } /** * @hidden */ toggleAll(expand) { this.skip = 0; this.grid.scrollTo({ row: 0, column: 0 }); this.groups.forEach((gr, idx) => { const expanded = this.groupsService.isExpanded({ group: gr, groupIndex: idx.toString(), parentGroup: undefined }); const performToggle = (expand && !expanded) || (!expand && expanded); if (performToggle) { this.grid.groupsService.toggleRow({ type: 'group', data: gr, index: idx.toString(), level: 0, parentGroup: undefined }); this[expand ? 'groupExpand' : 'groupCollapse']({ groupIndex: idx.toString() }); } }); } /** * Collapses all expanded root level groups. */ collapseAll() { this.toggleAll(false); } /** * Expands all expanded root level groups. */ expandAll() { this.toggleAll(true); } /** * @hidden */ groupExpand({ groupIndex }) { const { group, parents } = findGroup(groupIndex, this.groups); if (!group) { return; } this.groupsService.expandChildren(groupIndex); if (!group.items.length) { const descriptors = this.state.group.filter(removeParentDescriptors(parents, group)); const children = findChildren(this.originalData, parents.concat(group)); group.items = processGroups(children, { filter: this.state.filter, group: descriptors, sort: this.state.sort }); } this.grid.data = this.dataResult(this.state.skip, this.state.take); } /** * @hidden */ groupCollapse({ groupIndex }) { const { group } = findGroup(groupIndex, this.groups); if (group) { group.items = []; } else { return; } this.grid.data = this.dataResult(this.state.skip, this.state.take); } process(state) { if (state.group && state.group.length) { const groups = this.processGroups(state); this.grid.skip -= skippedHeaders(groups.data[0]); return groups; } else { this.groups = null; } return super.process(state); } processGroups(state) { if (!this.groups || !this.groups.length) { this.groups = processGroups(this.originalData, { filter: state.filter, group: state.group, sort: state.sort }); } return this.dataResult(state.skip, state.take); } dataResult(skip, take) { const includeFooters = this.grid.showGroupFooters; return { data: slice(this.groups, skip, take, includeFooters), total: count(this.groups, includeFooters) }; } applyState({ skip, take, sort, group, filter }) { this.skip = skip; this.state.take = take; // this.pageSize = take; // do need to update take as the process with slice correctly this.sort = sort; this.group = group; this.filter = filter; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: GroupBindingDirective, deps: [{ token: i0.ChangeDetectorRef }, { token: i1.LocalDataChangesService }, { token: i2.ContextService }, { token: i3.GroupsService }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: GroupBindingDirective, isStandalone: true, selector: "[kendoGridGroupBinding]", inputs: { kendoGridGroupBinding: "kendoGridGroupBinding", sort: "sort", filter: "filter", group: "group" }, exportAs: ["kendoGridGroupBinding"], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: GroupBindingDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoGridGroupBinding]', exportAs: 'kendoGridGroupBinding', standalone: true }] }], ctorParameters: function () { return [{ type: i0.ChangeDetectorRef }, { type: i1.LocalDataChangesService }, { type: i2.ContextService }, { type: i3.GroupsService }]; }, propDecorators: { kendoGridGroupBinding: [{ type: Input, args: ["kendoGridGroupBinding"] }], sort: [{ type: Input }], filter: [{ type: Input }], group: [{ type: Input }] } });