@progress/kendo-angular-treeview
Version:
Kendo UI TreeView for Angular
321 lines (320 loc) • 13.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, EventEmitter, Input, Output, NgZone } from '@angular/core';
import { TreeViewComponent } from './treeview.component';
import { fetchLoadedDescendants, isBoolean, isPresent, noop } from './utils';
import { Subscription } from 'rxjs';
import { filter, take, switchMap, tap } from 'rxjs/operators';
import { isChanged } from '@progress/kendo-angular-common';
import * as i0 from "@angular/core";
import * as i1 from "./treeview.component";
const indexChecked = (keys, index) => keys.filter(k => k === index).length > 0;
const matchKey = index => k => {
if (index === k) {
return true;
}
if (!k.split) {
return false;
}
return k.split('_').reduce(({ key, result }, part) => {
key += part;
if (index === key || result) {
return { result: true };
}
key += "_";
return { key, result: false };
}, { key: "", result: false }).result;
};
/**
* A directive which manages the in-memory checked state of the TreeView node
* ([see example]({% slug checkboxes_treeview %})).
*/
export class CheckDirective {
treeView;
zone;
/**
* @hidden
*/
set isChecked(value) {
this.treeView.isChecked = value;
}
/**
* Defines the item key that will be stored in the `checkedKeys` collection.
*/
checkKey;
/**
* Defines the collection that will store the checked keys
* ([see example]({% slug checkboxes_treeview %})).
*/
checkedKeys;
/**
* Defines the checkable settings ([see example]({% slug checkboxes_treeview %}#toc-setup)).
* If no value is provided, the default [`CheckableSettings`]({% slug api_treeview_checkablesettings %}) are applied.
*/
checkable;
/**
* Fires when the `checkedKeys` collection was updated.
*/
checkedKeysChange = new EventEmitter();
subscriptions = new Subscription();
get options() {
const defaultOptions = {
checkChildren: true,
checkParents: true,
enabled: true,
mode: "multiple",
uncheckCollapsedChildren: false
};
if (!isPresent(this.checkable) || typeof this.checkable === 'string') {
return defaultOptions;
}
const checkSettings = isBoolean(this.checkable)
? { enabled: this.checkable }
: this.checkable;
return Object.assign(defaultOptions, checkSettings);
}
checkActions = {
'multiple': (e) => this.checkMultiple(e),
'single': (e) => this.checkSingle(e)
};
/**
* Reflectes the internal `checkedKeys` state.
*/
state = new Set();
clickSubscription;
/**
* Holds the last emitted `checkedKeys` collection.
*/
lastChange;
constructor(treeView, zone) {
this.treeView = treeView;
this.zone = zone;
this.subscriptions.add(this.treeView.checkedChange
.subscribe((e) => this.check(e)));
const expandedItems = [];
this.subscriptions.add(this.treeView.childrenLoaded
.pipe(filter(() => this.options.checkChildren && this.treeView.loadOnDemand), tap(item => expandedItems.push(item)), switchMap(() => this.zone.onStable.pipe(take(1))))
.subscribe(() => this.addCheckedItemsChildren(expandedItems)));
this.treeView.isChecked = this.isItemChecked.bind(this);
}
ngOnChanges(changes) {
if (changes.checkable) {
this.treeView.checkboxes = this.options.enabled;
this.toggleCheckOnClick();
}
if (isChanged('checkedKeys', changes, false) && changes.checkedKeys.currentValue !== this.lastChange) {
this.state = new Set(changes.checkedKeys.currentValue);
}
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
this.unsubscribeClick();
}
isItemChecked(dataItem, index) {
if (!this.checkKey) {
return this.isIndexChecked(index);
}
const hasKey = this.state.has(this.itemKey({ dataItem, index }));
return hasKey ? 'checked' : 'none';
}
isIndexChecked(index) {
const checkedKeys = Array.from(this.state).filter(matchKey(index));
if (indexChecked(checkedKeys, index)) {
return 'checked';
}
const { mode, checkParents } = this.options;
if (mode === 'multiple' && checkParents && checkedKeys.length) {
return 'indeterminate';
}
return 'none';
}
itemKey(item) {
if (!isPresent(this.checkKey)) {
return item.index;
}
if (typeof this.checkKey === "string" && isPresent(item.dataItem)) {
return item.dataItem[this.checkKey];
}
if (typeof this.checkKey === "function") {
return this.checkKey(item);
}
}
check(e) {
const { enabled, mode } = this.options;
const performSelection = this.checkActions[mode] || noop;
if (!enabled) {
return;
}
performSelection(e);
}
checkSingle(node) {
const key = this.itemKey(node.item);
const hasKey = this.state.has(key);
this.state.clear();
if (!hasKey) {
this.state.add(key);
}
this.notify();
}
checkMultiple(node) {
this.checkNode(node);
if (this.options.checkParents) {
this.checkParents(node.parent);
}
this.notify();
}
toggleCheckOnClick() {
this.unsubscribeClick();
if (this.options.checkOnClick) {
this.clickSubscription = this.treeView.nodeClick.subscribe(args => {
if (args.type === 'click') {
const lookup = this.treeView.itemLookup(args.item.index);
this.check(lookup);
}
});
}
}
unsubscribeClick() {
if (this.clickSubscription) {
this.clickSubscription.unsubscribe();
this.clickSubscription = null;
}
}
checkNode(node) {
if (!isPresent(node.item.dataItem) ||
this.treeView.isDisabled(node.item.dataItem, node.item.index) ||
!this.treeView.hasCheckbox(node.item.dataItem, node.item.index)) {
return;
}
const currentKey = this.itemKey(node.item);
if (!isPresent(currentKey)) {
return;
}
const pendingCheck = [currentKey];
if (this.options.checkChildren) {
const descendants = fetchLoadedDescendants(node, ({ item }) => (this.treeView.disableParentNodesOnly || this.options.checkDisabledChildren ?
this.treeView.isVisible(item.dataItem, item.index) :
this.treeView.isVisible(item.dataItem, item.index) &&
!this.treeView.isDisabled(item.dataItem, item.index) &&
this.treeView.hasCheckbox(item.dataItem, item.index)));
pendingCheck.push(...descendants.filter((item) => this.options.checkDisabledChildren ||
!this.treeView.isDisabled(item.item.dataItem, item.item.index) ||
this.treeView.hasCheckbox(item.item.dataItem, item.item.index))
.map(({ item }) => this.itemKey(item)));
}
const shouldCheck = !this.state.has(currentKey);
pendingCheck.forEach(key => {
if (shouldCheck) {
this.state.add(key);
}
else {
this.state.delete(key);
if (this.options.uncheckCollapsedChildren &&
this.options.mode === 'multiple' &&
this.treeView.loadOnDemand) {
if (this.checkKey && this.treeView.hasChildren(node.item.dataItem)) {
this.uncheckChildren(node.item.dataItem, node.item.index);
return;
}
const checkedKeys = Array.from(this.state).filter(matchKey(node.item.index));
checkedKeys.forEach(key => this.state.delete(key));
}
}
});
}
uncheckChildren(dataItem, parentNodeIndex) {
this.treeView.children(dataItem).subscribe(children => children.forEach((item, index) => {
const nodeIndex = `${parentNodeIndex}_${index}`;
this.state.delete(this.itemKey({ dataItem: item, index: nodeIndex }));
if (this.treeView.hasChildren(item)) {
this.uncheckChildren(item, nodeIndex);
}
}));
}
checkParents(parent) {
if (!isPresent(parent)) {
return;
}
let currentParent = parent;
while (currentParent) {
const parentKey = this.itemKey(currentParent.item);
const isDisabled = this.treeView.isDisabled(currentParent.item.dataItem, currentParent.item.index);
const allChildrenSelected = currentParent.children.every(item => this.state.has(this.itemKey(item)));
const hasCheckbox = this.treeView.hasCheckbox(currentParent.item.dataItem, currentParent.item.index);
if (hasCheckbox && (!isDisabled || this.options.checkDisabledChildren) && allChildrenSelected) {
this.state.add(parentKey);
}
else {
this.state.delete(parentKey);
}
currentParent = currentParent.parent;
}
}
allChildrenSelected(children) {
return children.every(item => {
const childrenSel = this.allChildrenSelected(item.children);
return this.state.has(this.itemKey(item.item)) && childrenSel;
});
}
notify() {
this.lastChange = Array.from(this.state);
this.checkedKeysChange.emit(this.lastChange);
}
addCheckedItemsChildren(lookups) {
if (!isPresent(lookups) || lookups.length === 0) {
return;
}
const initiallyCheckedItemsCount = this.state.size;
const disabledItems = new Set();
lookups.forEach(lookup => {
const itemKey = this.itemKey(lookup.item);
if (!this.state.has(itemKey)) {
return;
}
lookup.children.forEach(item => {
// ensure both the parent item and each child node is enabled
if ((!this.treeView.isDisabled(lookup.item.dataItem, lookup.item.index) &&
!this.treeView.isDisabled(item.dataItem, item.index)) &&
(this.treeView.hasCheckbox(lookup.item.dataItem, lookup.item.index) &&
this.treeView.hasCheckbox(item.dataItem, item.index)) ||
this.treeView.disableParentNodesOnly || this.options.checkDisabledChildren) {
this.state.add(this.itemKey(item));
}
if (this.treeView.disableParentNodesOnly &&
!this.options.checkDisabledChildren &&
(this.treeView.isDisabled(item.dataItem, item.index) ||
!this.treeView.hasCheckbox(item.dataItem, item.index))) {
disabledItems.add(this.itemKey(item));
}
});
});
disabledItems.forEach(item => this.state.delete(item));
const hasNewlyCheckedItems = initiallyCheckedItemsCount !== this.state.size;
if (hasNewlyCheckedItems) {
this.zone.run(() => this.notify());
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: CheckDirective, deps: [{ token: i1.TreeViewComponent }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: CheckDirective, isStandalone: true, selector: "[kendoTreeViewCheckable]", inputs: { isChecked: "isChecked", checkKey: ["checkBy", "checkKey"], checkedKeys: "checkedKeys", checkable: ["kendoTreeViewCheckable", "checkable"] }, outputs: { checkedKeysChange: "checkedKeysChange" }, usesOnChanges: true, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: CheckDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoTreeViewCheckable]',
standalone: true
}]
}], ctorParameters: function () { return [{ type: i1.TreeViewComponent }, { type: i0.NgZone }]; }, propDecorators: { isChecked: [{
type: Input
}], checkKey: [{
type: Input,
args: ["checkBy"]
}], checkedKeys: [{
type: Input
}], checkable: [{
type: Input,
args: ['kendoTreeViewCheckable']
}], checkedKeysChange: [{
type: Output
}] } });