UNPKG

@progress/kendo-angular-treeview

Version:
321 lines (320 loc) 13.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, 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 }] } });