@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
JavaScript
/**-----------------------------------------------------------------------------------------
* 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
}] } });