@progress/kendo-angular-treeview
Version:
Kendo UI TreeView for Angular
302 lines (301 loc) • 8.74 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 { isDocumentAvailable } from '@progress/kendo-angular-common';
import { getter } from '@progress/kendo-common';
const focusableRegex = /^(?:a|input|select|option|textarea|button|object)$/i;
/**
* @hidden
*/
export const match = (element, selector) => {
const matcher = element.matches || element.msMatchesSelector || element.webkitMatchesSelector;
if (!matcher) {
return false;
}
return matcher.call(element, selector.toLowerCase());
};
/**
* @hidden
*/
export 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
*/
export const noop = () => { };
/**
* @hidden
*/
export const isPresent = (value) => value !== null && value !== undefined;
/**
* @hidden
*/
export const isBlank = (value) => value === null || value === undefined;
/**
* @hidden
*/
export const isArray = (value) => Array.isArray(value);
/**
* @hidden
*/
export const isNullOrEmptyString = (value) => isBlank(value) || value.trim().length === 0;
/**
* @hidden
*/
export const isBoolean = (value) => typeof value === 'boolean';
/**
* @hidden
*/
export 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
*/
export 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
*/
export 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.
*/
export 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
*/
export const isLoadMoreButton = (element) => {
return isPresent(closestWithMatch(element, '.k-treeview-leaf.k-treeview-load-more-button'));
};
/**
* @hidden
*/
export const closest = (node, predicate) => {
while (node && !predicate(node)) {
node = node.parentNode;
}
return node;
};
/**
* @hidden
*/
export const hasParent = (element, container) => {
return Boolean(closest(element, (node) => node === container));
};
/**
* @hidden
*/
export const focusableNode = (element) => element.nativeElement.querySelector('li[tabindex="0"]');
/**
* @hidden
*/
export const hasActiveNode = (target, node) => {
const closestItem = node || closestNode(target);
return closestItem && (closestItem === target || target.tabIndex < 0);
};
/**
* @hidden
*/
export const nodeId = (node) => node ? node.getAttribute('data-treeindex') : '';
/**
* @hidden
*/
export const nodeIndex = (item) => (item || {}).index;
/**
* @hidden
*/
export const dataItemsEqual = (first, second) => {
if (!isPresent(first) && !isPresent(second)) {
return true;
}
return isPresent(first) && isPresent(second) && first.item.dataItem === second.item.dataItem;
};
/**
* @hidden
*/
export const getDataItem = (lookup) => {
if (!isPresent(lookup)) {
return lookup;
}
return lookup.item.dataItem;
};
/**
* @hidden
*/
export 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
*/
export 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
*/
export const buildTreeIndex = (parentIndex, itemIndex) => {
return [parentIndex, itemIndex].filter(part => isPresent(part)).join('_');
};
/**
* @hidden
*/
export 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.
*/
export 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
*/
export 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.
*/
export const getSizeClass = (component, size) => {
const SIZE_CLASSES = {
'small': `k-${component}-sm`,
'medium': `k-${component}-md`,
'large': `k-${component}-lg`
};
return SIZE_CLASSES[size];
};