@progress/kendo-angular-treeview
Version:
Kendo UI TreeView for Angular
1,399 lines (1,379 loc) • 243 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 * 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, 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 { NgFor, NgClass, NgIf, NgSwitch, NgSwitchCase, NgTemplateOutlet, NgSwitchDefault } 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: 1751463060,
version: '19.2.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: "16.2.12", ngImport: i0, type: ExpandStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ExpandStateService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", 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: "16.2.12", ngImport: i0, type: IndexBuilderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IndexBuilderService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", 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: "16.2.12", ngImport: i0, type: LoadingNotificationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: LoadingNotificationService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", 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.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;
}
const moveAction = this.actions[e.keyCode];
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: "16.2.12", ngImport: i0, type: NavigationService, deps: [{ token: i1.LocalizationService }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavigationService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavigationService, decorators: [{
type: Injectable
}], ctorParameters: function () { return [{ 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: "16.2.12", ngImport: i0, type: NodeChildrenService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeChildrenService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", 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`—The data item for the current node.
* - `let-index="index"`—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: "16.2.12", ngImport: i0, type: NodeTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: NodeTemplateDirective, isStandalone: true, selector: "[kendoTreeViewNodeTemplate]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeTemplateDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoTreeViewNodeTemplate]',
standalone: true
}]
}], ctorParameters: function () { return [{ 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"`—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: "16.2.12", ngImport: i0, type: LoadMoreButtonTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: LoadMoreButtonTemplateDirective, isStandalone: true, selector: "[kendoTreeViewLoadMoreButtonTemplate]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: LoadMoreButtonTemplateDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoTreeViewLoadMoreButtonTemplate]',
standalone: true
}]
}], ctorParameters: function () { return [{ 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: "16.2.12", ngImport: i0, type: DataBoundComponent, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DataBoundComponent });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", 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: "16.2.12", ngImport: i0, type: ExpandableComponent, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ExpandableComponent });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", 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: "16.2.12", ngImport: i0, type: SelectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SelectionService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", 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: "16.2.12", ngImport: i0, type: TreeViewLookupService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TreeViewLookupService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", 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: "16.2.12", 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: "16.2.12", 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: "16.2.12", ngImport: i0, type: TreeViewItemContentDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoTreeViewItemContent]',
standalone: true
}]
}], ctorParameters: function () { return [{ 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: "16.2.12", 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: "16.2.12", 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: "16.2.12", ngImport: i0, type: LoadingIndicatorDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoTreeViewLoading]',
standalone: true
}]
}], ctorParameters: function () { return [{ 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: "16.2.12", 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: "16.2.12", 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: "16.2.12", ngImport: i0, type: TreeViewItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[kendoTreeViewItem]',
standalone: true
}]
}], ctorParameters: function () { return [{ 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: [{
type: Input