UNPKG

@progress/kendo-angular-treeview

Version:
1,401 lines (1,381 loc) 241 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import * as i0 from '@angular/core'; import { EventEmitter, Injectable, Directive, Optional, Input, HostBinding, Component, forwardRef, isDevMode, ViewContainerRef, ChangeDetectionStrategy, ViewChild, Output, ContentChild, Host, NgModule } from '@angular/core'; import { isDocumentAvailable, Keys, normalizeKeys, anyChanged, hasObservers, isChanged, guid, ResizeBatchService } from '@progress/kendo-angular-common'; import * as i1 from '@progress/kendo-angular-l10n'; import { ComponentMessages, LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n'; import { Subject, Subscription, of, EMPTY, BehaviorSubject, merge } from 'rxjs'; import { validatePackage } from '@progress/kendo-licensing'; import { getter, setter } from '@progress/kendo-common'; import { caretAltDownIcon, caretAltRightIcon, caretAltLeftIcon, searchIcon, cancelIcon, insertMiddleIcon, insertBottomIcon, insertTopIcon, plusIcon } from '@progress/kendo-svg-icons'; import { CheckBoxComponent, TextBoxComponent, TextBoxPrefixTemplateDirective } from '@progress/kendo-angular-inputs'; import { trigger, transition, style, animate } from '@angular/animations'; import { filter, tap, switchMap, delay, takeUntil, catchError, finalize, take, map } from 'rxjs/operators'; import { NgClass, NgTemplateOutlet } from '@angular/common'; import { IconWrapperComponent, IconsService } from '@progress/kendo-angular-icons'; import { Draggable } from '@progress/kendo-draggable'; import { DialogContainerService, DialogService, WindowService, WindowContainerService } from '@progress/kendo-angular-dialog'; import { PopupService } from '@progress/kendo-angular-popup'; /** * @hidden */ const packageMetadata = { name: '@progress/kendo-angular-treeview', productName: 'Kendo UI for Angular', productCode: 'KENDOUIANGULAR', productCodes: ['KENDOUIANGULAR'], publishDate: 1765467927, version: '21.3.0', licensingDocsUrl: 'https://www.telerik.com/kendo-angular-ui/my-license/' }; /** * @hidden */ class DataChangeNotificationService { changes = new EventEmitter(); notify() { this.changes.emit(); } } /** * @hidden */ const hasChildren = () => false; /** * @hidden */ const isChecked = () => 'none'; /** * @hidden */ const isDisabled = () => false; /** * @hidden */ const hasCheckbox = () => true; /** * @hidden */ const isExpanded = () => true; /** * @hidden */ const isSelected = () => false; /** * @hidden */ const isVisible = () => true; /** * @hidden */ const trackBy = (_, item) => item; /** * @hidden */ class ExpandStateService { changes = new Subject(); expand(index, dataItem) { this.changes.next({ dataItem, index, expand: true }); } collapse(index, dataItem) { this.changes.next({ dataItem, index, expand: false }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ExpandStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ExpandStateService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ExpandStateService, decorators: [{ type: Injectable }] }); /** * @hidden */ class IndexBuilderService { INDEX_SEPARATOR = '_'; nodeIndex(index = '', parentIndex = '') { return `${parentIndex}${parentIndex ? this.INDEX_SEPARATOR : ''}${index}`; } indexForLevel(index, level) { return index.split(this.INDEX_SEPARATOR).slice(0, level).join(this.INDEX_SEPARATOR); } lastLevelIndex(index = '') { const parts = index.split(this.INDEX_SEPARATOR); if (!parts.length) { return NaN; } return parseInt(parts[parts.length - 1], 10); } level(index) { return index.split(this.INDEX_SEPARATOR).length; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: IndexBuilderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: IndexBuilderService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: IndexBuilderService, decorators: [{ type: Injectable }] }); /** * @hidden */ class LoadingNotificationService { changes = new Subject(); notifyLoaded(index) { this.changes.next(index); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoadingNotificationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoadingNotificationService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoadingNotificationService, decorators: [{ type: Injectable }] }); const focusableRegex = /^(?:a|input|select|option|textarea|button|object)$/i; /** * @hidden */ const match = (element, selector) => { const matcher = element.matches || element.msMatchesSelector || element.webkitMatchesSelector; if (!matcher) { return false; } return matcher.call(element, selector.toLowerCase()); }; /** * @hidden */ const closestWithMatch = (element, selector) => { if (!document.documentElement.contains(element)) { return null; } let parent = element; while (parent !== null && parent.nodeType === 1) { if (match(parent, selector)) { return parent; } parent = parent.parentElement || parent.parentNode; } return null; }; /** * @hidden */ const noop = () => { }; /** * @hidden */ const isPresent = (value) => value !== null && value !== undefined; /** * @hidden */ const isBlank = (value) => value === null || value === undefined; /** * @hidden */ const isArray = (value) => Array.isArray(value); /** * @hidden */ const isNullOrEmptyString = (value) => isBlank(value) || value.trim().length === 0; /** * @hidden */ const isBoolean = (value) => typeof value === 'boolean'; /** * @hidden */ const closestNode = (element) => { const selector = 'li.k-treeview-item'; if (!isDocumentAvailable()) { return null; } if (element.closest) { return element.closest(selector); } else { return closestWithMatch(element, selector); } }; /** * @hidden */ const isFocusable = (element) => { if (element.tagName) { const tagName = element.tagName.toLowerCase(); const tabIndex = element.getAttribute('tabIndex'); const skipTab = tabIndex === '-1'; let focusable = tabIndex !== null && !skipTab; if (focusableRegex.test(tagName)) { focusable = !element.disabled && !skipTab; } return focusable; } return false; }; /** * @hidden */ const isContent = (element) => { const scopeSelector = '.k-treeview-leaf:not(.k-treeview-load-more-button),.k-treeview-item,.k-treeview'; if (!isDocumentAvailable()) { return null; } let node = element; while (node && !match(node, scopeSelector)) { node = node.parentNode; } if (node) { return match(node, '.k-treeview-leaf:not(.k-treeview-load-more-button)'); } }; /** * @hidden * * Returns the nested .k-treeview-leaf:not(.k-treeview-load-more-button) element. * If the passed parent item is itself a content node, it is returned. */ const getContentElement = (parent) => { if (!isPresent(parent)) { return null; } const selector = '.k-treeview-leaf:not(.k-treeview-load-more-button)'; if (match(parent, selector)) { return parent; } return parent.querySelector(selector); }; /** * @hidden */ const isLoadMoreButton = (element) => { return isPresent(closestWithMatch(element, '.k-treeview-leaf.k-treeview-load-more-button')); }; /** * @hidden */ const closest = (node, predicate) => { while (node && !predicate(node)) { node = node.parentNode; } return node; }; /** * @hidden */ const hasParent = (element, container) => { return Boolean(closest(element, (node) => node === container)); }; /** * @hidden */ const focusableNode = (element) => element.nativeElement.querySelector('li[tabindex="0"]'); /** * @hidden */ const hasActiveNode = (target, node) => { const closestItem = node || closestNode(target); return closestItem && (closestItem === target || target.tabIndex < 0); }; /** * @hidden */ const nodeId = (node) => node ? node.getAttribute('data-treeindex') : ''; /** * @hidden */ const nodeIndex = (item) => (item || {}).index; /** * @hidden */ const dataItemsEqual = (first, second) => { if (!isPresent(first) && !isPresent(second)) { return true; } return isPresent(first) && isPresent(second) && first.item.dataItem === second.item.dataItem; }; /** * @hidden */ const getDataItem = (lookup) => { if (!isPresent(lookup)) { return lookup; } return lookup.item.dataItem; }; /** * @hidden */ const isArrayWithAtLeastOneItem = v => v && Array.isArray(v) && v.length !== 0; /** * @hidden * A recursive tree-filtering algorithm that returns: * - all child nodes of matching nodes * - a chain parent nodes from the match to the root node */ const filterTree = (items, term, { operator, ignoreCase, mode }, textField, depth = 0) => { const field = typeof textField === "string" ? textField : textField[depth]; items.forEach((wrapper) => { const matcher = typeof operator === "string" ? matchByFieldAndCase(field, operator, ignoreCase) : operator; const isMatch = matcher(wrapper.dataItem, term); wrapper.isMatch = isMatch; wrapper.visible = isMatch; wrapper.containsMatches = false; if (isMatch) { setParentChain(wrapper.parent); } if (wrapper.children && wrapper.children.length > 0) { if (mode === "strict" || !isMatch) { filterTree(wrapper.children, term, { operator, ignoreCase, mode }, textField, depth + 1); } else { makeAllVisible(wrapper.children); } } }); }; const setParentChain = (node) => { if (!isPresent(node)) { return; } node.containsMatches = true; node.visible = true; if (isPresent(node.parent) && !node.parent.containsMatches) { setParentChain(node.parent); } }; const makeAllVisible = (nodes) => { nodes.forEach(node => { node.visible = true; if (node.children) { makeAllVisible(node.children); } }); }; const operators = { contains: (a, b) => a.indexOf(b) >= 0, doesnotcontain: (a, b) => a.indexOf(b) === -1, startswith: (a, b) => a.lastIndexOf(b, 0) === 0, doesnotstartwith: (a, b) => a.lastIndexOf(b, 0) === -1, endswith: (a, b) => a.indexOf(b, a.length - b.length) >= 0, doesnotendwith: (a, b) => a.indexOf(b, a.length - b.length) < 0 }; const matchByCase = (matcher, ignoreCase) => (a, b) => { if (ignoreCase) { return matcher(a.toLowerCase(), b.toLowerCase()); } return matcher(a, b); }; const matchByFieldAndCase = (field, operator, ignoreCase) => (dataItem, term) => matchByCase(operators[operator], ignoreCase)(getter(field)(dataItem), term); /** * @hidden */ const buildTreeIndex = (parentIndex, itemIndex) => { return [parentIndex, itemIndex].filter(part => isPresent(part)).join('_'); }; /** * @hidden */ const buildTreeItem = (dataItem, currentLevelIndex, parentIndex) => { if (!isPresent(dataItem)) { return null; } return { dataItem, index: buildTreeIndex(parentIndex, currentLevelIndex) }; }; /** * @hidden * * Retrieves all descendant nodes' lookups which are currently registered in the provided lookup item as a flat array. */ const fetchLoadedDescendants = (lookup, filterExpression) => { if (!isPresent(lookup) || lookup.children.length === 0) { return []; } let descendants = lookup.children; if (isPresent(filterExpression)) { descendants = descendants.filter(filterExpression); } descendants.forEach(child => descendants = descendants.concat(fetchLoadedDescendants(child, filterExpression))); return descendants; }; /** * @hidden * * Compares two Seets to determine whether all unique elements in one, are present in the other. * Important: * - it disregards the element order */ const sameValues = (as, bs) => { if (as.size !== bs.size) { return false; } return Array.from(as).every(v => bs.has(v)); }; /** * @hidden * Returns the size class based on the component and size input. */ const getSizeClass = (component, size) => { const SIZE_CLASSES = { 'small': `k-${component}-sm`, 'medium': `k-${component}-md`, 'large': `k-${component}-lg` }; return SIZE_CLASSES[size]; }; const safe = node => (node || {}); const safeChildren = node => (safe(node).children || []); const lastVisibleNode = (nodes) => { if (!Array.isArray(nodes) || nodes.length === 0) { return null; } const nodesCount = nodes.length; const lastIndex = nodesCount - 1; for (let index = lastIndex; index >= 0; index -= 1) { const node = nodes[index]; if (node.visible) { return node; } } return null; }; /** * @hidden */ class NavigationModel { ib = new IndexBuilderService(); nodes = []; firstVisibleNode() { return (this.nodes || []).find(node => node.visible); } lastVisibleNode() { let node = lastVisibleNode(this.nodes); while (isPresent(node) && safeChildren(node).length > 0) { const children = safeChildren(node); const lastVisibleChild = lastVisibleNode(children); if (!isPresent(lastVisibleChild)) { return node; } node = lastVisibleChild; } return node; } closestNode(index) { const { prev } = safe(this.findNode(index)); const sibling = prev || this.firstVisibleNode(); return safe(sibling).index === index ? this.visibleSibling(sibling, 1) : sibling; } firstFocusableNode() { return this.nodes.find((node) => { return !node.disabled && node.visible; }); } findNode(index) { return this.find(index, this.nodes); } findParent(index) { const parentLevel = this.ib.level(index) - 1; return this.findNode(this.ib.indexForLevel(index, parentLevel)); } findVisibleChild(index) { const node = this.findNode(index); const children = safeChildren(node); return children.find((child) => child.visible); } findVisiblePrev(item) { const index = item.index; const parent = this.findParent(index); const levelIndex = this.ib.lastLevelIndex(index); const prevNodes = this.container(parent).slice(0, levelIndex); const prevNodesHidden = prevNodes.every(node => !node.visible); if (levelIndex === 0 || prevNodesHidden) { return parent; } const currentNode = this.findNode(index); let prev = this.visibleSibling(currentNode, -1); if (prev) { let children = this.container(prev); while (children.length > 0 && children.some(node => node.visible)) { prev = lastVisibleNode(children); children = this.container(prev); } } return prev; } findVisibleNext(item) { const children = this.container(item); const hasVisibleChildren = children.some(child => child.visible); if (children.length === 0 || !hasVisibleChildren) { return this.visibleSibling(item, 1); } return children.find(child => child.visible); } registerItem(id, index, disabled, loadMoreButton = false, visible = true) { const children = []; const level = this.ib.level(index); const parent = this.findParent(index); if (parent || level === 1) { const node = { id, children, index, parent, disabled, loadMoreButton, visible }; this.insert(node, parent); } } unregisterItem(id, index) { const node = this.find(index, this.nodes); if (!node || node.id !== id) { return; } const children = this.container(node.parent); children.splice(children.indexOf(node), 1); } childLevel(nodes) { const children = nodes.filter(node => isPresent(node)); if (!children || !children.length) { return 1; } return this.ib.level(children[0].index); } container(node) { return node ? node.children : this.nodes; } find(index, nodes) { const childLevel = this.childLevel(nodes); const indexToMatch = this.ib.indexForLevel(index, childLevel); const isLeaf = childLevel === this.ib.level(index); const node = nodes.find(n => n && n.index === indexToMatch); if (!node) { return null; } return isLeaf ? node : this.find(index, node.children); } insert(node, parent) { const nodes = this.container(parent); nodes.splice(this.ib.lastLevelIndex(node.index), 0, node); } visibleSibling(node, offset) { if (!node) { return null; } const parent = this.findParent(node.index); const container = this.container(parent); let nextItemIndex = container.indexOf(node) + offset; let nextItem = container[nextItemIndex]; while (isPresent(nextItem)) { if (nextItem.visible) { return nextItem; } nextItemIndex += offset; nextItem = container[nextItemIndex]; } return this.visibleSibling(parent, offset); } } /** * @hidden */ class NavigationService { localization; expands = new Subject(); moves = new Subject(); checks = new Subject(); selects = new Subject(); deselectAllButCurrentItem = new Subject(); loadMore = new Subject(); navigable = true; selection = 'single'; isTreeViewActive = false; get model() { return this._model; } set model(model) { this._model = model; } actions = { [Keys.ArrowUp]: () => this.activate(this.model.findVisiblePrev(this.focusableItem), true), [Keys.ArrowDown]: () => this.activate(this.model.findVisibleNext(this.focusableItem), true), [Keys.ArrowLeft]: () => !this.isLoadMoreButton && (this.expand({ expand: this.localization.rtl, intercept: this.localization.rtl ? this.moveToFirstVisibleChild : this.moveToParent })), [Keys.ArrowRight]: () => !this.isLoadMoreButton && (this.expand({ expand: !this.localization.rtl, intercept: this.localization.rtl ? this.moveToParent : this.moveToFirstVisibleChild })), [Keys.Home]: () => this.activate(this.model.firstVisibleNode(), true), [Keys.End]: () => this.activate(this.model.lastVisibleNode(), true), [Keys.Enter]: (e) => this.handleEnter(e), [Keys.NumpadEnter]: (e) => this.handleEnter(e), [Keys.Space]: () => this.handleSpace() }; activeItem; isFocused = false; shouldScroll = false; _model = new NavigationModel(); get activeIndex() { return nodeIndex(this.activeItem) || null; } get isActiveExpanded() { return this.activeItem && this.activeItem.children.length > 0; } get isLoadMoreButton() { return this.activeItem && this.activeItem.loadMoreButton; } get focusableItem() { return this.activeItem || this.model.firstFocusableNode(); } constructor(localization) { this.localization = localization; this.moveToFirstVisibleChild = this.moveToFirstVisibleChild.bind(this); this.moveToParent = this.moveToParent.bind(this); } activate(item, shouldScroll = false) { if (!this.navigable || !item || this.isActive(nodeIndex(item))) { return; } this.isFocused = true; this.activeItem = item || this.activeItem; this.shouldScroll = shouldScroll; this.notifyMove(); } activateParent(index) { this.activate(this.model.findParent(index)); } activateIndex(index) { if (!index) { return; } this.activate(this.model.findNode(index)); } activateClosest(index) { if (!index || nodeIndex(this.focusableItem) !== index) { return; } this.activeItem = this.model.closestNode(index); this.notifyMove(); } activateFocusable() { if (this.activeItem) { return; } this.activeItem = this.model.firstVisibleNode(); this.notifyMove(); } deactivate() { if (!this.navigable || !this.isFocused) { return; } this.isFocused = false; this.notifyMove(); } checkIndex(index) { if (!this.isDisabled(index)) { this.checks.next(index); } } selectIndex(index) { if (!this.isDisabled(index)) { this.selects.next(index); } } notifyLoadMore(index) { if (!isPresent(index)) { return; } this.loadMore.next(index); } isActive(index) { if (!index) { return false; } return this.isFocused && this.activeIndex === index; } isFocusable(index) { return nodeIndex(this.focusableItem) === index; } isDisabled(index) { if (!index) { return false; } return this.model.findNode(index).disabled; } registerItem(id, index, disabled, loadMoreButton = false, visible = true) { const itemAtIndex = this.model.findNode(index); if (isPresent(itemAtIndex)) { this.model.unregisterItem(itemAtIndex.id, itemAtIndex.index); if (this.isActive(index)) { this.deactivate(); } } this.model.registerItem(id, index, disabled, loadMoreButton, visible); } updateItem(index, disabled, visible = true) { const itemAtIndex = this.model.findNode(index); if (isPresent(itemAtIndex)) { if (this.isActive(index)) { this.deactivate(); } } itemAtIndex.disabled = disabled; itemAtIndex.visible = visible; } unregisterItem(id, index) { if (this.isActive(index)) { this.activateParent(index); } this.model.unregisterItem(id, index); } move(e) { if (!this.navigable) { return; } // on some keyboards arrow keys, PageUp/Down, and Home/End are mapped to Numpad keys const code = normalizeKeys(e); const moveAction = this.actions[code]; if (!moveAction) { return; } moveAction(e); e.preventDefault(); } expand({ expand, intercept }) { const index = nodeIndex(this.activeItem); if (!index || intercept(index)) { return; } this.notifyExpand(expand); } moveToParent() { if (this.isActiveExpanded) { return false; } this.activate(this.model.findParent(nodeIndex(this.activeItem))); return true; } moveToFirstVisibleChild() { if (!this.isActiveExpanded) { return false; } this.activate(this.model.findVisibleChild(nodeIndex(this.activeItem))); return true; } notifyExpand(expand) { this.expands.next(this.navigationState(expand)); } notifyMove() { this.moves.next(this.navigationState()); } navigationState(expand = false) { return ({ expand, index: this.activeIndex, isFocused: this.isFocused, shouldScroll: this.shouldScroll }); } handleEnter(event) { if (!this.navigable) { return; } if (this.isLoadMoreButton) { this.notifyLoadMore(this.activeIndex); } else { const isCtrlPressed = event.ctrlKey || event.metaKey; if (isCtrlPressed) { this.selectIndex(this.activeIndex); } else { if (this.selection === 'multiple') { this.deselectAllButCurrentItem.next({ dataItem: this.activeItem, index: this.activeIndex }); } else { this.selectIndex(this.activeIndex); } } } } handleSpace() { if (!this.navigable) { return; } if (this.isLoadMoreButton) { this.notifyLoadMore(this.activeIndex); } else { this.checkIndex(this.activeIndex); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NavigationService, deps: [{ token: i1.LocalizationService }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NavigationService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NavigationService, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: i1.LocalizationService }] }); /** * @hidden */ class NodeChildrenService { changes = new Subject(); childrenLoaded(item, children) { this.changes.next({ item, children }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NodeChildrenService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NodeChildrenService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NodeChildrenService, decorators: [{ type: Injectable }] }); /** * Represents the template for TreeView nodes ([more information and example](slug:nodetemplate_treeview)). * * Use this template to customize the content of the nodes. To define the node template, nest an `<ng-template>` * tag with the `kendoTreeViewNodeTemplate` directive inside a `<kendo-treeview>` tag. * * The template context provides the node data item and its hierarchical index as variables: * * - `let-dataItem`&mdash;The data item for the current node. * - `let-index="index"`&mdash;The hierarchical index of the current node. * * @example * ```html * <kendo-treeview> * <ng-template kendoTreeViewNodeTemplate let-dataItem let-index="index"> * <span [style.fontWeight]="dataItem.items ? 'bolder': 'normal' "> * {{ index }}: {{ dataItem.text }} * </span> * </ng-template> * </kendo-treeview> * ``` */ class NodeTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NodeTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: NodeTemplateDirective, isStandalone: true, selector: "[kendoTreeViewNodeTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: NodeTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoTreeViewNodeTemplate]', standalone: true }] }], ctorParameters: () => [{ type: i0.TemplateRef, decorators: [{ type: Optional }] }] }); /** * Represents a directive for customizing the load more button in the TreeView. * * To define the template, nest an `<ng-template>` tag with the `kendoTreeViewLoadMoreButtonTemplate` directive inside a `<kendo-treeview>` tag * ([see example](slug:loadmorebutton_treeview#button-template)). * * The template context provides the following variable: * - `let-index="index"`&mdash;The hierarchical index of the load more button node. * * @example * ```html * <kendo-treeview> * <ng-template kendoTreeViewLoadMoreButtonTemplate let-index="index"> * Load more at {{ index }} * </ng-template> * </kendo-treeview> * ``` */ class LoadMoreButtonTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoadMoreButtonTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: LoadMoreButtonTemplateDirective, isStandalone: true, selector: "[kendoTreeViewLoadMoreButtonTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoadMoreButtonTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoTreeViewLoadMoreButtonTemplate]', standalone: true }] }], ctorParameters: () => [{ type: i0.TemplateRef, decorators: [{ type: Optional }] }] }); /** * @hidden * * An injection token used by the data binding directives to interface with * the TreeView or the DropDownTree components. */ class DataBoundComponent { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DataBoundComponent, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DataBoundComponent }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: DataBoundComponent, decorators: [{ type: Injectable }] }); /** * @hidden * * An injection token used by the expand-directive to interface with * the TreeView or the DropDownTree components. */ class ExpandableComponent { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ExpandableComponent, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ExpandableComponent }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ExpandableComponent, decorators: [{ type: Injectable }] }); /** * @hidden */ class SelectionService { changes = new Subject(); firstIndex; isFirstSelected(index) { return this.firstIndex === index; } setFirstSelected(index, selected) { if (this.firstIndex === index && selected === false) { this.firstIndex = null; } else if (!this.firstIndex && selected) { this.firstIndex = index; } } select(index, dataItem) { this.changes.next({ dataItem, index }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SelectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SelectionService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SelectionService, decorators: [{ type: Injectable }] }); const INDEX_REGEX = /\d+$/; /** * @hidden */ class TreeViewLookupService { map = new Map(); reset() { this.map.clear(); } registerItem(item, parent) { const currentLookup = { children: [], item, parent: this.item(nodeIndex(parent)) }; this.map.set(item.index, currentLookup); } registerChildren(index, children) { const item = this.item(index); if (!item) { return; } item.children = children; } unregisterItem(index, dataItem) { const current = this.item(index); if (current && current.item.dataItem === dataItem) { this.map.delete(index); if (current.parent && current.parent.children) { current.parent.children = current.parent.children.filter(item => item.dataItem !== dataItem); } } } replaceItem(index, item, parent) { if (!item) { return; } this.unregisterItem(index, item.dataItem); this.registerItem(item, parent); this.addToParent(item, parent); } itemLookup(index) { const item = this.item(index); if (!item) { return null; } return { children: this.mapChildren(item.children), item: item.item, parent: item.parent }; } hasItem(index) { return this.map.has(index); } item(index) { return this.map.get(index) || null; } addToParent(item, parent) { if (parent) { const parentItem = this.item(parent.index); const index = parseInt(INDEX_REGEX.exec(item.index)[0], 10); parentItem.children = parentItem.children || []; parentItem.children.splice(index, 0, item); } } mapChildren(children = []) { return children.map(c => { const { item, parent, children } = this.item(c.index); return { children: this.mapChildren(children), item, parent }; }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TreeViewLookupService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TreeViewLookupService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TreeViewLookupService, decorators: [{ type: Injectable }] }); /** * @hidden * * A directive which manages the expanded state of the TreeView. */ class TreeViewItemContentDirective { element; navigationService; selectionService; renderer; dataItem; index; initialSelection = false; isSelected = isSelected; subscriptions = new Subscription(); constructor(element, navigationService, selectionService, renderer) { this.element = element; this.navigationService = navigationService; this.selectionService = selectionService; this.renderer = renderer; this.subscriptions.add(this.navigationService.moves .subscribe(this.updateFocusClass.bind(this))); this.subscriptions.add(this.navigationService.selects .pipe(filter((index) => index === this.index)) .subscribe((index) => this.selectionService.select(index, this.dataItem))); this.subscriptions.add(this.selectionService.changes .subscribe(() => { this.updateSelectionClass(this.isSelected(this.dataItem, this.index)); })); } ngOnChanges(changes) { if (changes['initialSelection']) { this.updateSelectionClass(this.initialSelection); } if (changes['index']) { this.updateFocusClass(); } } ngOnDestroy() { this.subscriptions.unsubscribe(); } updateFocusClass() { this.render(this.navigationService.isActive(this.index), 'k-focus'); } updateSelectionClass(selected) { this.render(selected, 'k-selected'); } render(addClass, className) { const action = addClass ? 'addClass' : 'removeClass'; this.renderer[action](this.element.nativeElement, className); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TreeViewItemContentDirective, deps: [{ token: i0.ElementRef }, { token: NavigationService }, { token: SelectionService }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: TreeViewItemContentDirective, isStandalone: true, selector: "[kendoTreeViewItemContent]", inputs: { dataItem: "dataItem", index: "index", initialSelection: "initialSelection", isSelected: "isSelected" }, usesOnChanges: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TreeViewItemContentDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoTreeViewItemContent]', standalone: true }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: NavigationService }, { type: SelectionService }, { type: i0.Renderer2 }], propDecorators: { dataItem: [{ type: Input }], index: [{ type: Input }], initialSelection: [{ type: Input }], isSelected: [{ type: Input }] } }); /** * @hidden */ class LoadingIndicatorDirective { expandService; loadingService; cd; get loading() { return this._loading; } set loading(value) { this._loading = value; this.cd.markForCheck(); } index; _loading = false; subscription; constructor(expandService, loadingService, cd) { this.expandService = expandService; this.loadingService = loadingService; this.cd = cd; } ngOnInit() { const loadingNotifications = this.loadingService .changes .pipe(filter(index => index === this.index)); this.subscription = this.expandService .changes .pipe(filter(({ index }) => index === this.index), tap(({ expand }) => { if (!expand && this.loading) { this.loading = false; } }), filter(({ expand }) => expand), switchMap(x => of(x).pipe(delay(100), takeUntil(loadingNotifications)))) .subscribe(() => this.loading = true); this.subscription.add(loadingNotifications.subscribe(() => this.loading = false)); } ngOnDestroy() { if (this.subscription) { this.subscription.unsubscribe(); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoadingIndicatorDirective, deps: [{ token: ExpandStateService }, { token: LoadingNotificationService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: LoadingIndicatorDirective, isStandalone: true, selector: "[kendoTreeViewLoading]", inputs: { index: ["kendoTreeViewLoading", "index"] }, host: { properties: { "class.k-i-loading": "this.loading" } }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LoadingIndicatorDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoTreeViewLoading]', standalone: true }] }], ctorParameters: () => [{ type: ExpandStateService }, { type: LoadingNotificationService }, { type: i0.ChangeDetectorRef }], propDecorators: { loading: [{ type: HostBinding, args: ["class.k-i-loading"] }], index: [{ type: Input, args: ["kendoTreeViewLoading"] }] } }); const buildItem = (index, dataItem) => ({ dataItem, index }); let id = 0; const TREE_ITEM_ROLE = 'treeitem'; const BUTTON_ROLE = 'button'; /** * @hidden * * A directive which manages the expanded state of the TreeView. */ class TreeViewItemDirective { element; expandService; navigationService; selectionService; lookupService; renderer; ib; dataItem; index; parentDataItem; parentIndex; role = TREE_ITEM_ROLE; loadOnDemand = true; checkable; selectable; expandable; set isChecked(checked) { if (checked === 'checked') { this.ariaChecked = 'true'; } else if (checked === 'indeterminate') { this.ariaChecked = 'mixed'; } else { this.ariaChecked = 'false'; } } isDisabled = false; isVisible = true; get isExpanded() { return this._isExpanded || false; } set isExpanded(isExpanded) { this._isExpanded = isExpanded; } get isSelected() { return this._isSelected || false; } set isSelected(isSelected) { this._isSelected = isSelected; } get isButton() { return this.role === BUTTON_ROLE; } get treeItem() { return buildItem(this.index, this.dataItem); } get parentTreeItem() { return this.parentDataItem ? buildItem(this.parentIndex, this.parentDataItem) : null; } ariaChecked = 'false'; id = id++; _isExpanded; _isSelected; isInitialized = false; subscriptions = []; constructor(element, expandService, navigationService, selectionService, lookupService, renderer, ib) { this.element = element; this.expandService = expandService; this.navigationService = navigationService; this.selectionService = selectionService; this.lookupService = lookupService; this.renderer = renderer; this.ib = ib; this.subscribe(); } ngOnInit() { if (this.loadOnDemand && !this.isButton) { this.lookupService.registerItem(this.treeItem, this.parentTreeItem); } this.registerNavigationItem(); this.isInitialized = true; this.setAttribute('role', this.role); this.setAriaAttributes(); this.updateTabIndex(); } ngOnChanges(changes) { const { index } = changes; if (anyChanged(['index', 'checkable', 'isChecked', 'expandable', 'isExpanded', 'selectable', 'isSelected'], changes)) { this.setAriaAttributes(); } if (this.loadOnDemand && !this.isButton) { this.moveLookupItem(changes); } this.moveNavigationItem(index); if (anyChanged(['isDisabled', 'isVisible'], changes)) { this.updateNodeAvailability(); } } ngOnDestroy() { this.navigationService.unregisterItem(this.id, this.index); if (this.loadOnDemand && !this.isButton) { this.lookupService.unregisterItem(this.index, this.dataItem); } this.subscriptions = this.subscriptions.reduce((list, callback) => (callback.unsubscribe(), list), []); } subscribe() { this.subscriptions = [ this.navigationService.moves .subscribe((navState) => { this.updateTabIndex(); this.focusItem(navState.shouldScroll); }), this.navigationService.expands .pipe(filter(({ index }) => index === this.index && !this.isDisabled)) .subscribe(({ expand }) => this.expand(expand)) ]; } registerNavigationItem() { this.navigationService.registerItem(this.id, this.index, this.isDisabled, this.isButton, this.isVisible); this.activateItem(); } activateItem() { if (this.isDisabled) { return; } const navigationService = this.navigationService; const selectionService = this.selectionService; const index = this.index; selectionService.setFirstSelected(index, this.isSelected); if (!navigationService.isActive(index) && selectionService.isFirstSelected(index)) { navigationService.activateIndex(index); } } expand(shouldExpand) { this.expandService[shouldExpand ? 'expand' : 'collapse'](this.index, this.dataItem); } isFocusable() { return !this.isDisabled && this.navigationService.isFocusable(this.index); } focusItem(scrollIntoView = false) { if (this.isInitialized && this.navigationService.isActive(this.index)) { this.element.nativeElement.focus({ preventScroll: !scrollIntoView }); } } moveLookupItem(changes = {}) { const { dataItem, index, parentDataItem, parentIndex } = changes; if ((index && index.firstChange) || //skip first change (!dataItem && !index && !parentDataItem && !parentIndex)) { return; } const oldIndex = (index || {}).previousValue || this.index; this.lookupService.replaceItem(oldIndex, this.treeItem, this.parentTreeItem); } moveNavigationItem(indexChange = {}) { const { currentValue, firstChange, previousValue } = indexChange; if (!firstChange && isPresent(currentValue) && isPresent(previousValue)) { this.navigationService.unregisterItem(this.id, previousValue); this.navigationService.registerItem(this.id, currentValue, this.isDisabled, this.isButton); } } updateNodeAvailability() { const service = this.navigationService; if (this.isDisabled || !this.isVisible && this.navigationService.isTreeViewActive) { service.activateClosest(this.index); // activate before updating the item } else { service.activateFocusable(); } service.updateItem(this.index, this.isDisabled, this.isVisible); } setAriaAttributes() { this.setAttribute('aria-level', this.ib.level(this.index).toString()); // don't render attributes when the component configuration doesn't allow the specified state this.setAttribute('aria-expanded', this.expandable ? this.isExpanded.toString() : null); this.setAttribute('aria-selected', this.selectable ? this.isSelected.toString() : null); this.setAttribute('aria-checked', this.checkable ? this.ariaChecked : null); } updateTabIndex() { this.setAttribute('tabIndex', this.isFocusable() ? '0' : '-1'); } setAttribute(attr, value) { if (!isPresent(value)) { this.renderer.removeAttribute(this.element.nativeElement, attr); return; } this.renderer.setAttribute(this.element.nativeElement, attr, value); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TreeViewItemDirective, deps: [{ token: i0.ElementRef }, { token: ExpandStateService }, { token: NavigationService }, { token: SelectionService }, { token: TreeViewLookupService }, { token: i0.Renderer2 }, { token: IndexBuilderService }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.14", type: TreeViewItemDirective, isStandalone: true, selector: "[kendoTreeViewItem]", inputs: { dataItem: "dataItem", index: "index", parentDataItem: "parentDataItem", parentIndex: "parentIndex", role: "role", loadOnDemand: "loadOnDemand", checkable: "checkable", selectable: "selectable", expandable: "expandable", isChecked: "isChecked", isDisabled: "isDisabled", isVisible: "isVisible", isExpanded: "isExpanded", isSelected: "isSelected" }, usesOnChanges: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TreeViewItemDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoTreeViewItem]', standalone: true }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: ExpandStateService }, { type: NavigationService }, { type: SelectionService }, { type: TreeViewLookupService }, { type: i0.Renderer2 }, { type: IndexBuilderService }], propDecorators: { dataItem: [{ type: Input }], index: [{ type: Input }], parentDataItem: [{ type: Input }], parentIndex: [{ type: Input }], role: [{ type: Input }], loadOnDemand: [{ type: Input }], checkable: [{ type: Input }], selectable: [{ type: Input }], expandable: [{ type: Input }], isChecked: [{ type: Input }], isDisabled: [{ type: Input }], isVisible: [{ type: Input }], isExpanded: [{ type: Input }], isSelected: [{