@ali-hm/angular-tree-component
Version:
A simple yet powerful tree component for Angular 12+
1,283 lines (1,274 loc) • 103 kB
JavaScript
import * as i0 from '@angular/core';
import { signal, effect, Injectable, inject, input, ViewEncapsulation, Component, ElementRef, Injector, Renderer2, NgZone, output, HostListener, Directive, computed, TemplateRef, ViewContainerRef, forwardRef, EventEmitter, Output, Input, ViewChild, ContentChild, NgModule } from '@angular/core';
import { NgTemplateOutlet, NgIf, CommonModule } from '@angular/common';
const KEYS = {
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
ENTER: 13,
SPACE: 32,
CONTEXT_MENU: 32
};
const TREE_ACTIONS = {
TOGGLE_ACTIVE: (tree, node, $event) => node && node.toggleActivated(),
TOGGLE_ACTIVE_MULTI: (tree, node, $event) => node && node.toggleActivated(true),
TOGGLE_SELECTED: (tree, node, $event) => node && node.toggleSelected(),
ACTIVATE: (tree, node, $event) => node.setIsActive(true),
DEACTIVATE: (tree, node, $event) => node.setIsActive(false),
SELECT: (tree, node, $event) => node.setIsSelected(true),
DESELECT: (tree, node, $event) => node.setIsSelected(false),
FOCUS: (tree, node, $event) => node.focus(),
TOGGLE_EXPANDED: (tree, node, $event) => node.hasChildren && node.toggleExpanded(),
EXPAND: (tree, node, $event) => node.expand(),
COLLAPSE: (tree, node, $event) => node.collapse(),
DRILL_DOWN: (tree, node, $event) => tree.focusDrillDown(),
DRILL_UP: (tree, node, $event) => tree.focusDrillUp(),
NEXT_NODE: (tree, node, $event) => tree.focusNextNode(),
PREVIOUS_NODE: (tree, node, $event) => tree.focusPreviousNode(),
MOVE_NODE: (tree, node, $event, { from, to }) => {
// default action assumes from = node, to = {parent, index}
if ($event.ctrlKey) {
tree.copyNode(from, to);
}
else {
tree.moveNode(from, to);
}
}
};
const defaultActionMapping = {
mouse: {
click: TREE_ACTIONS.TOGGLE_ACTIVE,
dblClick: null,
contextMenu: null,
expanderClick: TREE_ACTIONS.TOGGLE_EXPANDED,
checkboxClick: TREE_ACTIONS.TOGGLE_SELECTED,
drop: TREE_ACTIONS.MOVE_NODE
},
keys: {
[KEYS.RIGHT]: TREE_ACTIONS.DRILL_DOWN,
[KEYS.LEFT]: TREE_ACTIONS.DRILL_UP,
[KEYS.DOWN]: TREE_ACTIONS.NEXT_NODE,
[KEYS.UP]: TREE_ACTIONS.PREVIOUS_NODE,
[KEYS.SPACE]: TREE_ACTIONS.TOGGLE_ACTIVE,
[KEYS.ENTER]: TREE_ACTIONS.TOGGLE_ACTIVE
}
};
class TreeOptions {
get hasChildrenField() { return this.options.hasChildrenField || 'hasChildren'; }
get childrenField() { return this.options.childrenField || 'children'; }
get displayField() { return this.options.displayField || 'name'; }
get idField() { return this.options.idField || 'id'; }
get isExpandedField() { return this.options.isExpandedField || 'isExpanded'; }
get getChildren() { return this.options.getChildren; }
get levelPadding() { return this.options.levelPadding || 0; }
get useVirtualScroll() { return this.options.useVirtualScroll; }
get animateExpand() { return this.options.animateExpand; }
get animateSpeed() { return this.options.animateSpeed || 1; }
get animateAcceleration() { return this.options.animateAcceleration || 1.2; }
get scrollOnActivate() { return this.options.scrollOnActivate === undefined ? true : this.options.scrollOnActivate; }
get rtl() { return !!this.options.rtl; }
get rootId() { return this.options.rootId; }
get useCheckbox() { return this.options.useCheckbox; }
get useTriState() { return this.options.useTriState === undefined ? true : this.options.useTriState; }
get scrollContainer() { return this.options.scrollContainer; }
get allowDragoverStyling() { return this.options.allowDragoverStyling === undefined ? true : this.options.allowDragoverStyling; }
constructor(options = {}) {
this.options = options;
this.actionMapping = {
mouse: {
click: this.options?.actionMapping?.mouse?.click ?? defaultActionMapping.mouse.click,
dblClick: this.options?.actionMapping?.mouse?.dblClick ?? defaultActionMapping.mouse.dblClick,
contextMenu: this.options?.actionMapping?.mouse?.contextMenu ?? defaultActionMapping.mouse.contextMenu,
expanderClick: this.options?.actionMapping?.mouse?.expanderClick ?? defaultActionMapping.mouse.expanderClick,
checkboxClick: this.options?.actionMapping?.mouse?.checkboxClick ?? defaultActionMapping.mouse.checkboxClick,
drop: this.options?.actionMapping?.mouse?.drop ?? defaultActionMapping.mouse.drop,
dragStart: this.options?.actionMapping?.mouse?.dragStart ?? undefined,
drag: this.options?.actionMapping?.mouse?.drag ?? undefined,
dragEnd: this.options?.actionMapping?.mouse?.dragEnd ?? undefined,
dragOver: this.options?.actionMapping?.mouse?.dragOver ?? undefined,
dragLeave: this.options?.actionMapping?.mouse?.dragLeave ?? undefined,
dragEnter: this.options?.actionMapping?.mouse?.dragEnter ?? undefined,
mouseOver: this.options?.actionMapping?.mouse?.mouseOver ?? undefined,
mouseOut: this.options?.actionMapping?.mouse?.mouseOut ?? undefined,
},
keys: {
[KEYS.RIGHT]: TREE_ACTIONS.DRILL_DOWN,
[KEYS.LEFT]: TREE_ACTIONS.DRILL_UP,
[KEYS.DOWN]: TREE_ACTIONS.NEXT_NODE,
[KEYS.UP]: TREE_ACTIONS.PREVIOUS_NODE,
[KEYS.SPACE]: TREE_ACTIONS.TOGGLE_ACTIVE,
[KEYS.ENTER]: TREE_ACTIONS.TOGGLE_ACTIVE
}
};
if (this.options?.actionMapping?.keys) {
this.actionMapping.keys = {
...this.actionMapping.keys,
...this.options.actionMapping.keys
};
}
if (options.rtl) {
this.actionMapping.keys[KEYS.RIGHT] = options.actionMapping?.keys[KEYS.RIGHT] || TREE_ACTIONS.DRILL_UP;
this.actionMapping.keys[KEYS.LEFT] = options.actionMapping?.keys[KEYS.LEFT] || TREE_ACTIONS.DRILL_DOWN;
}
}
getNodeClone(node) {
if (this.options.getNodeClone) {
return this.options.getNodeClone(node);
}
// remove id from clone
// keeping ie11 compatibility
const nodeClone = Object.assign({}, node.data);
if (nodeClone.id) {
delete nodeClone.id;
}
return nodeClone;
}
allowDrop(element, to, $event) {
if (this.options.allowDrop instanceof Function) {
return this.options.allowDrop(element, to, $event);
}
else {
return this.options.allowDrop === undefined ? true : this.options.allowDrop;
}
}
allowDrag(node) {
if (this.options.allowDrag instanceof Function) {
return this.options.allowDrag(node);
}
else {
return this.options.allowDrag;
}
}
nodeClass(node) {
return this.options.nodeClass ? this.options.nodeClass(node) : '';
}
nodeHeight(node) {
if (node.data.virtual) {
return 0;
}
let nodeHeight = this.options.nodeHeight || 22;
if (typeof nodeHeight === 'function') {
nodeHeight = nodeHeight(node);
}
// account for drop slots:
return nodeHeight + (node.index === 0 ? 2 : 1) * this.dropSlotHeight;
}
get dropSlotHeight() {
return typeof this.options.dropSlotHeight === 'number' ? this.options.dropSlotHeight : 2;
}
}
const TREE_EVENTS = {
toggleExpanded: 'toggleExpanded',
activate: 'activate',
deactivate: 'deactivate',
nodeActivate: 'nodeActivate',
nodeDeactivate: 'nodeDeactivate',
select: 'select',
deselect: 'deselect',
focus: 'focus',
blur: 'blur',
initialized: 'initialized',
updateData: 'updateData',
moveNode: 'moveNode',
copyNode: 'copyNode',
event: 'event',
loadNodeChildren: 'loadNodeChildren',
changeFilter: 'changeFilter',
stateChange: 'stateChange'
};
class TreeNode {
// Public getters/setters for API compatibility
get children() { return this._children(); }
set children(value) { this._children.set(value); }
get index() { return this._index(); }
set index(value) { this._index.set(value); }
get position() { return this._position(); }
set position(value) { this._position.set(value); }
get height() { return this._height(); }
set height(value) { this._height.set(value); }
// Computed properties
get isHidden() { return this.treeModel.isHidden(this); }
get isExpanded() { return this.treeModel.isExpanded(this); }
get isActive() { return this.treeModel.isActive(this); }
get isFocused() { return this.treeModel.isNodeFocused(this); }
get isSelected() {
if (this.isSelectable()) {
return this.treeModel.isSelected(this);
}
else {
return this.children?.some((node) => node.isSelected);
}
}
get isAllSelected() {
if (this.isSelectable()) {
return this.treeModel.isSelected(this);
}
else {
return this.children?.every((node) => node.isAllSelected);
}
}
get isPartiallySelected() {
return this.isSelected && !this.isAllSelected;
}
get level() {
return this.parent ? this.parent.level + 1 : 0;
}
get path() {
return this.parent ? [...this.parent.path, this.id] : [];
}
get elementRef() {
throw `Element Ref is no longer supported since introducing virtual scroll\n
You may use a template to obtain a reference to the element`;
}
get originalNode() { return this._originalNode; }
;
constructor(data, parent, treeModel, index) {
this.data = data;
this.parent = parent;
this.treeModel = treeModel;
this._isLoadingChildren = false;
// Private signals
this._children = signal(undefined, ...(ngDevMode ? [{ debugName: "_children" }] : []));
this._index = signal(undefined, ...(ngDevMode ? [{ debugName: "_index" }] : []));
this._position = signal(0, ...(ngDevMode ? [{ debugName: "_position" }] : []));
this._height = signal(undefined, ...(ngDevMode ? [{ debugName: "_height" }] : []));
this.allowDrop = (element, $event) => {
return this.options.allowDrop(element, { parent: this, index: 0 }, $event);
};
this.allowDragoverStyling = () => {
return this.options.allowDragoverStyling;
};
if (this.id === undefined || this.id === null) {
this.id = uuid();
} // Make sure there's a unique id without overriding existing ids to work with immutable data structures
this.index = index;
if (this.getField('children')) {
this._initChildren();
}
this.autoLoadChildren();
}
// helper get functions:
get hasChildren() {
return !!(this.getField('hasChildren') || (this.children && this.children.length > 0));
}
get isCollapsed() { return !this.isExpanded; }
get isLeaf() { return !this.hasChildren; }
get isRoot() { return this.parent.data.virtual; }
get realParent() { return this.isRoot ? null : this.parent; }
// proxy functions:
get options() { return this.treeModel.options; }
fireEvent(event) { this.treeModel.fireEvent(event); }
// field accessors:
get displayField() {
return this.getField('display');
}
get id() {
return this.getField('id');
}
set id(value) {
this.setField('id', value);
}
getField(key) {
return this.data[this.options[`${key}Field`]];
}
setField(key, value) {
this.data[this.options[`${key}Field`]] = value;
}
// traversing:
_findAdjacentSibling(steps, skipHidden = false) {
const siblings = this._getParentsChildren(skipHidden);
const index = siblings.indexOf(this);
return siblings.length > index + steps ? siblings[index + steps] : null;
}
findNextSibling(skipHidden = false) {
return this._findAdjacentSibling(+1, skipHidden);
}
findPreviousSibling(skipHidden = false) {
return this._findAdjacentSibling(-1, skipHidden);
}
getVisibleChildren() {
return this.visibleChildren;
}
get visibleChildren() {
return (this.children || []).filter((node) => !node.isHidden);
}
getFirstChild(skipHidden = false) {
let children = skipHidden ? this.visibleChildren : this.children;
return children != null && children.length ? children[0] : null;
}
getLastChild(skipHidden = false) {
let children = skipHidden ? this.visibleChildren : this.children;
return children != null && children.length ? children[children.length - 1] : null;
}
findNextNode(goInside = true, skipHidden = false) {
return goInside && this.isExpanded && this.getFirstChild(skipHidden) ||
this.findNextSibling(skipHidden) ||
this.parent && this.parent.findNextNode(false, skipHidden);
}
findPreviousNode(skipHidden = false) {
let previousSibling = this.findPreviousSibling(skipHidden);
if (!previousSibling) {
return this.realParent;
}
return previousSibling._getLastOpenDescendant(skipHidden);
}
_getLastOpenDescendant(skipHidden = false) {
const lastChild = this.getLastChild(skipHidden);
return (this.isCollapsed || !lastChild)
? this
: lastChild._getLastOpenDescendant(skipHidden);
}
_getParentsChildren(skipHidden = false) {
const children = this.parent &&
(skipHidden ? this.parent.getVisibleChildren() : this.parent.children);
return children || [];
}
getIndexInParent(skipHidden = false) {
return this._getParentsChildren(skipHidden).indexOf(this);
}
isDescendantOf(node) {
if (this === node)
return true;
else
return this.parent && this.parent.isDescendantOf(node);
}
getNodePadding() {
return this.options.levelPadding * (this.level - 1) + 'px';
}
getClass() {
return [this.options.nodeClass(this), `tree-node-level-${this.level}`].join(' ');
}
onDrop($event) {
this.mouseAction('drop', $event.event, {
from: $event.element,
to: { parent: this, index: 0, dropOnNode: true }
});
}
allowDrag() {
return this.options.allowDrag(this);
}
// helper methods:
loadNodeChildren() {
if (!this.options.getChildren) {
return Promise.resolve(); // Not getChildren method - for using redux
}
return Promise.resolve(this.options.getChildren(this))
.then((children) => {
if (children) {
this.setField('children', children);
this._initChildren();
if (this.options.useTriState && this.treeModel.isSelected(this)) {
this.setIsSelected(true);
}
this.children.forEach((child) => {
if (child.getField('isExpanded') && child.hasChildren) {
child.expand();
}
});
}
}).then(() => {
this.fireEvent({
eventName: TREE_EVENTS.loadNodeChildren,
node: this
});
});
}
expand() {
if (!this.isExpanded) {
this.toggleExpanded();
}
return this;
}
collapse() {
if (this.isExpanded) {
this.toggleExpanded();
}
return this;
}
doForAll(fn) {
Promise.resolve(fn(this)).then(() => {
if (this.children) {
this.children.forEach((child) => child.doForAll(fn));
}
});
}
expandAll() {
this.doForAll((node) => node.expand());
}
collapseAll() {
this.doForAll((node) => node.collapse());
}
ensureVisible() {
if (this.realParent) {
this.realParent.expand();
this.realParent.ensureVisible();
}
return this;
}
toggleExpanded() {
this.setIsExpanded(!this.isExpanded);
return this;
}
setIsExpanded(value) {
if (this.hasChildren) {
this.treeModel.setExpandedNode(this, value);
// Load children when expanding if they haven't been loaded yet
if (value && !this.children && this.hasChildren && !this._isLoadingChildren) {
this._isLoadingChildren = true;
this.loadNodeChildren().finally(() => {
this._isLoadingChildren = false;
});
}
}
return this;
}
;
autoLoadChildren() {
// Instead of using effect, we'll load children when the node is expanded
// This is handled by the toggleExpanded and setIsExpanded methods
// Check immediately if we should load
if (this.isExpanded && !this.children && this.hasChildren) {
this.loadNodeChildren();
}
}
dispose() {
if (this.children) {
this.children.forEach((child) => child.dispose());
}
this.parent = null;
this.children = null;
}
setIsActive(value, multi = false) {
this.treeModel.setActiveNode(this, value, multi);
if (value) {
this.focus(this.options.scrollOnActivate);
}
return this;
}
isSelectable() {
return this.isLeaf || !this.children || !this.options.useTriState;
}
setIsSelected(value) {
if (this.isSelectable()) {
this.treeModel.setSelectedNode(this, value);
}
else {
this.visibleChildren.forEach((child) => child.setIsSelected(value));
}
return this;
}
toggleSelected() {
this.setIsSelected(!this.isSelected);
return this;
}
toggleActivated(multi = false) {
this.setIsActive(!this.isActive, multi);
return this;
}
setActiveAndVisible(multi = false) {
this.setIsActive(true, multi)
.ensureVisible();
setTimeout(this.scrollIntoView.bind(this));
return this;
}
scrollIntoView(force = false) {
this.treeModel.virtualScroll.scrollIntoView(this, force);
}
focus(scroll = true) {
let previousNode = this.treeModel.getFocusedNode();
this.treeModel.setFocusedNode(this);
if (scroll) {
this.scrollIntoView();
}
if (previousNode) {
this.fireEvent({ eventName: TREE_EVENTS.blur, node: previousNode });
}
this.fireEvent({ eventName: TREE_EVENTS.focus, node: this });
return this;
}
blur() {
let previousNode = this.treeModel.getFocusedNode();
this.treeModel.setFocusedNode(null);
if (previousNode) {
this.fireEvent({ eventName: TREE_EVENTS.blur, node: this });
}
return this;
}
setIsHidden(value) {
this.treeModel.setIsHidden(this, value);
}
hide() {
this.setIsHidden(true);
}
show() {
this.setIsHidden(false);
}
mouseAction(actionName, $event, data = null) {
this.treeModel.setFocus(true);
const actionMapping = this.options.actionMapping.mouse;
const mouseAction = actionMapping[actionName];
if (mouseAction) {
mouseAction(this.treeModel, this, $event, data);
}
}
getSelfHeight() {
return this.options.nodeHeight(this);
}
_initChildren() {
this.children = this.getField('children')
.map((c, index) => new TreeNode(c, this, this.treeModel, index));
}
}
function uuid() {
return Math.floor(Math.random() * 10000000000000);
}
class TreeModel {
constructor() {
this.options = new TreeOptions();
this.eventNames = Object.keys(TREE_EVENTS);
// Private signals
this._roots = signal(undefined, ...(ngDevMode ? [{ debugName: "_roots" }] : []));
this._expandedNodeIds = signal({}, ...(ngDevMode ? [{ debugName: "_expandedNodeIds" }] : []));
this._selectedLeafNodeIds = signal({}, ...(ngDevMode ? [{ debugName: "_selectedLeafNodeIds" }] : []));
this._activeNodeIds = signal({}, ...(ngDevMode ? [{ debugName: "_activeNodeIds" }] : []));
this._hiddenNodeIds = signal({}, ...(ngDevMode ? [{ debugName: "_hiddenNodeIds" }] : []));
this._focusedNodeId = signal(null, ...(ngDevMode ? [{ debugName: "_focusedNodeId" }] : []));
this._virtualRoot = signal(undefined, ...(ngDevMode ? [{ debugName: "_virtualRoot" }] : []));
this.firstUpdate = true;
this.subscriptions = [];
}
static { this.focusedTree = null; }
// Public getters/setters to maintain API compatibility
get roots() { return this._roots(); }
set roots(value) { this._roots.set(value); }
get virtualRoot() { return this._virtualRoot(); }
get focusedNode() {
const id = this._focusedNodeId();
return id ? this.getNodeById(id) : null;
}
get expandedNodes() {
const ids = this._expandedNodeIds();
const nodes = Object.keys(ids)
.filter((id) => ids[id])
.map((id) => this.getNodeById(id));
return nodes.filter(Boolean);
}
get activeNodes() {
const ids = this._activeNodeIds();
const nodes = Object.keys(ids)
.filter((id) => ids[id])
.map((id) => this.getNodeById(id));
return nodes.filter(Boolean);
}
get hiddenNodes() {
const ids = this._hiddenNodeIds();
const nodes = Object.keys(ids)
.filter((id) => ids[id])
.map((id) => this.getNodeById(id));
return nodes.filter(Boolean);
}
get selectedLeafNodes() {
const ids = this._selectedLeafNodeIds();
const nodes = Object.keys(ids)
.filter((id) => ids[id])
.map((id) => this.getNodeById(id));
return nodes.filter(Boolean);
}
// events
fireEvent(event) {
event.treeModel = this;
this.events[event.eventName].emit(event);
this.events.event.emit(event);
}
subscribe(eventName, fn) {
const subscription = this.events[eventName].subscribe(fn);
this.subscriptions.push(subscription);
}
// getters
getFocusedNode() {
return this.focusedNode;
}
getActiveNode() {
return this.activeNodes[0];
}
getActiveNodes() {
return this.activeNodes;
}
getVisibleRoots() {
return this._virtualRoot()?.visibleChildren;
}
getFirstRoot(skipHidden = false) {
const root = skipHidden ? this.getVisibleRoots() : this.roots;
return root != null && root.length ? root[0] : null;
}
getLastRoot(skipHidden = false) {
const root = skipHidden ? this.getVisibleRoots() : this.roots;
return root != null && root.length ? root[root.length - 1] : null;
}
get isFocused() {
return TreeModel.focusedTree === this;
}
isNodeFocused(node) {
return this.focusedNode === node;
}
isEmptyTree() {
const rootNodes = this.roots;
return rootNodes && rootNodes.length === 0;
}
// locating nodes
getNodeByPath(path, startNode = null) {
if (!path)
return null;
startNode = startNode || this._virtualRoot();
if (path.length === 0)
return startNode;
if (!startNode.children)
return null;
const childId = path.shift();
const childNode = startNode.children.find(c => c.id === childId);
if (!childNode)
return null;
return this.getNodeByPath(path, childNode);
}
getNodeById(id) {
const idStr = id.toString();
return this.getNodeBy((node) => node.id.toString() === idStr);
}
getNodeBy(predicate, startNode = null) {
startNode = startNode || this._virtualRoot();
if (!startNode.children)
return null;
const found = startNode.children.find(predicate);
if (found) { // found in children
return found;
}
else { // look in children's children
for (let child of startNode.children) {
const foundInChildren = this.getNodeBy(predicate, child);
if (foundInChildren)
return foundInChildren;
}
}
}
isExpanded(node) {
return this._expandedNodeIds()[node.id];
}
isHidden(node) {
return this._hiddenNodeIds()[node.id];
}
isActive(node) {
return this._activeNodeIds()[node.id];
}
isSelected(node) {
return this._selectedLeafNodeIds()[node.id];
}
ngOnDestroy() {
this.dispose();
this.unsubscribeAll();
}
dispose() {
// Dispose reactions of the replaced nodes
const vRoot = this._virtualRoot();
if (vRoot) {
vRoot.dispose();
}
}
unsubscribeAll() {
this.subscriptions.forEach(subscription => subscription.unsubscribe());
this.subscriptions = [];
}
// actions
setData({ nodes, options = null, events = null }) {
if (options) {
this.options = new TreeOptions(options);
}
if (events) {
this.events = events;
}
if (nodes) {
this.nodes = nodes;
}
this.update();
}
update() {
// Rebuild tree:
let virtualRootConfig = {
id: this.options.rootId,
virtual: true,
[this.options.childrenField]: this.nodes
};
this.dispose();
const newVirtualRoot = new TreeNode(virtualRootConfig, null, this, 0);
this._virtualRoot.set(newVirtualRoot);
this.roots = newVirtualRoot.children;
// Fire event:
const currentRoots = this.roots;
if (this.firstUpdate) {
if (currentRoots) {
this.firstUpdate = false;
this._calculateExpandedNodes();
}
}
else {
this.fireEvent({ eventName: TREE_EVENTS.updateData });
}
}
setFocusedNode(node) {
this._focusedNodeId.set(node ? node.id : null);
}
setFocus(value) {
TreeModel.focusedTree = value ? this : null;
}
doForAll(fn) {
this.roots.forEach((root) => root.doForAll(fn));
}
focusNextNode() {
let previousNode = this.getFocusedNode();
let nextNode = previousNode ? previousNode.findNextNode(true, true) : this.getFirstRoot(true);
if (nextNode)
nextNode.focus();
}
focusPreviousNode() {
let previousNode = this.getFocusedNode();
let nextNode = previousNode ? previousNode.findPreviousNode(true) : this.getLastRoot(true);
if (nextNode)
nextNode.focus();
}
focusDrillDown() {
let previousNode = this.getFocusedNode();
if (previousNode && previousNode.isCollapsed && previousNode.hasChildren) {
previousNode.toggleExpanded();
}
else {
let nextNode = previousNode ? previousNode.getFirstChild(true) : this.getFirstRoot(true);
if (nextNode)
nextNode.focus();
}
}
focusDrillUp() {
let previousNode = this.getFocusedNode();
if (!previousNode)
return;
if (previousNode.isExpanded) {
previousNode.toggleExpanded();
}
else {
let nextNode = previousNode.realParent;
if (nextNode)
nextNode.focus();
}
}
setActiveNode(node, value, multi = false) {
if (multi) {
this._setActiveNodeMulti(node, value);
}
else {
this._setActiveNodeSingle(node, value);
}
if (value) {
node.focus(this.options.scrollOnActivate);
this.fireEvent({ eventName: TREE_EVENTS.activate, node });
this.fireEvent({ eventName: TREE_EVENTS.nodeActivate, node }); // For IE11
}
else {
this.fireEvent({ eventName: TREE_EVENTS.deactivate, node });
this.fireEvent({ eventName: TREE_EVENTS.nodeDeactivate, node }); // For IE11
}
}
setSelectedNode(node, value) {
this._selectedLeafNodeIds.update(ids => ({ ...ids, [node.id]: value }));
if (value) {
node.focus();
this.fireEvent({ eventName: TREE_EVENTS.select, node });
}
else {
this.fireEvent({ eventName: TREE_EVENTS.deselect, node });
}
}
setExpandedNode(node, value) {
this._expandedNodeIds.update(ids => ({ ...ids, [node.id]: value }));
this.fireEvent({ eventName: TREE_EVENTS.toggleExpanded, node, isExpanded: value });
}
expandAll() {
this.roots.forEach((root) => root.expandAll());
}
collapseAll() {
this.roots.forEach((root) => root.collapseAll());
}
setIsHidden(node, value) {
this._hiddenNodeIds.update(ids => ({ ...ids, [node.id]: value }));
}
setHiddenNodeIds(nodeIds) {
const ids = nodeIds.reduce((hiddenNodeIds, id) => ({
...hiddenNodeIds,
[id]: true
}), {});
this._hiddenNodeIds.set(ids);
}
performKeyAction(node, $event) {
const keyAction = this.options.actionMapping.keys[$event.keyCode];
if (keyAction) {
$event.preventDefault();
keyAction(this, node, $event);
return true;
}
else {
return false;
}
}
filterNodes(filter, autoShow = true) {
let filterFn;
if (!filter) {
return this.clearFilter();
}
// support function and string filter
if (filter && typeof filter.valueOf() === 'string') {
filterFn = (node) => node.displayField.toLowerCase().indexOf(filter.toLowerCase()) !== -1;
}
else if (filter && typeof filter === 'function') {
filterFn = filter;
}
else {
console.error('Don\'t know what to do with filter', filter);
console.error('Should be either a string or function');
return;
}
const ids = {};
this.roots.forEach((node) => this._filterNode(ids, node, filterFn, autoShow));
this._hiddenNodeIds.set(ids);
this.fireEvent({ eventName: TREE_EVENTS.changeFilter });
}
clearFilter() {
this._hiddenNodeIds.set({});
this.fireEvent({ eventName: TREE_EVENTS.changeFilter });
}
moveNode(node, to) {
const fromIndex = node.getIndexInParent();
const fromParent = node.parent;
if (!this.canMoveNode(node, to, fromIndex))
return;
const fromChildren = fromParent.getField('children');
// If node doesn't have children - create children array
if (!to.parent.getField('children')) {
to.parent.setField('children', []);
}
const toChildren = to.parent.getField('children');
const originalNode = fromChildren.splice(fromIndex, 1)[0];
// Compensate for index if already removed from parent:
let toIndex = (fromParent === to.parent && to.index > fromIndex) ? to.index - 1 : to.index;
toChildren.splice(toIndex, 0, originalNode);
fromParent.treeModel.update();
if (to.parent.treeModel !== fromParent.treeModel) {
to.parent.treeModel.update();
}
this.fireEvent({
eventName: TREE_EVENTS.moveNode,
node: originalNode,
to: { parent: to.parent.data, index: toIndex },
from: { parent: fromParent.data, index: fromIndex }
});
}
copyNode(node, to) {
const fromIndex = node.getIndexInParent();
if (!this.canMoveNode(node, to, fromIndex))
return;
// If node doesn't have children - create children array
if (!to.parent.getField('children')) {
to.parent.setField('children', []);
}
const toChildren = to.parent.getField('children');
const nodeCopy = this.options.getNodeClone(node);
toChildren.splice(to.index, 0, nodeCopy);
node.treeModel.update();
if (to.parent.treeModel !== node.treeModel) {
to.parent.treeModel.update();
}
this.fireEvent({ eventName: TREE_EVENTS.copyNode, node: nodeCopy, to: { parent: to.parent.data, index: to.index } });
}
getState() {
return {
expandedNodeIds: this._expandedNodeIds(),
selectedLeafNodeIds: this._selectedLeafNodeIds(),
activeNodeIds: this._activeNodeIds(),
hiddenNodeIds: this._hiddenNodeIds(),
focusedNodeId: this._focusedNodeId()
};
}
setState(state) {
if (!state)
return;
this._expandedNodeIds.set(state.expandedNodeIds || {});
this._selectedLeafNodeIds.set(state.selectedLeafNodeIds || {});
this._activeNodeIds.set(state.activeNodeIds || {});
this._hiddenNodeIds.set(state.hiddenNodeIds || {});
this._focusedNodeId.set(state.focusedNodeId);
}
subscribeToState(fn) {
effect(() => fn(this.getState()));
}
canMoveNode(node, to, fromIndex = undefined) {
const fromNodeIndex = fromIndex || node.getIndexInParent();
// same node:
if (node.parent === to.parent && fromIndex === to.index) {
return false;
}
return !to.parent.isDescendantOf(node);
}
calculateExpandedNodes() {
this._calculateExpandedNodes();
}
// private methods
_filterNode(ids, node, filterFn, autoShow) {
// if node passes function then it's visible
let isVisible = filterFn(node);
if (node.children) {
// if one of node's children passes filter then this node is also visible
node.children.forEach((child) => {
if (this._filterNode(ids, child, filterFn, autoShow)) {
isVisible = true;
}
});
}
// mark node as hidden
if (!isVisible) {
ids[node.id] = true;
}
// auto expand parents to make sure the filtered nodes are visible
if (autoShow && isVisible) {
node.ensureVisible();
}
return isVisible;
}
_calculateExpandedNodes(startNode = null) {
startNode = startNode || this._virtualRoot();
if (startNode.data[this.options.isExpandedField]) {
this._expandedNodeIds.update(ids => ({ ...ids, [startNode.id]: true }));
}
if (startNode.children) {
startNode.children.forEach((child) => this._calculateExpandedNodes(child));
}
}
_setActiveNodeSingle(node, value) {
// Deactivate all other nodes:
this.activeNodes
.filter((activeNode) => activeNode !== node)
.forEach((activeNode) => {
this.fireEvent({ eventName: TREE_EVENTS.deactivate, node: activeNode });
this.fireEvent({ eventName: TREE_EVENTS.nodeDeactivate, node: activeNode }); // For IE11
});
if (value) {
this._activeNodeIds.set({ [node.id]: true });
}
else {
this._activeNodeIds.set({});
}
}
_setActiveNodeMulti(node, value) {
this._activeNodeIds.update(ids => ({ ...ids, [node.id]: value }));
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: TreeModel, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: TreeModel }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: TreeModel, decorators: [{
type: Injectable
}] });
class TreeDraggedElement {
constructor() {
this._draggedElement = null;
}
set(draggedElement) {
this._draggedElement = draggedElement;
}
get() {
return this._draggedElement;
}
isDragging() {
return !!this.get();
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: TreeDraggedElement, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: TreeDraggedElement, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: TreeDraggedElement, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
const Y_OFFSET = 500; // Extra pixels outside the viewport, in each direction, to render nodes in
const Y_EPSILON = 150; // Minimum pixel change required to recalculate the rendered nodes
class TreeVirtualScroll {
get yBlocks() { return this._yBlocks(); }
set yBlocks(value) { this._yBlocks.set(value); }
get x() { return this._x(); }
set x(value) { this._x.set(value); }
get viewportHeight() { return this._viewportHeight(); }
set viewportHeight(value) { this._viewportHeight.set(value); }
get y() {
return this.yBlocks * Y_EPSILON;
}
get totalHeight() {
const vRoot = this.treeModel['_virtualRoot']();
return vRoot ? vRoot.height : 0;
}
constructor() {
this.treeModel = inject(TreeModel);
this._dispose = [];
this._yBlocks = signal(0, ...(ngDevMode ? [{ debugName: "_yBlocks" }] : []));
this._x = signal(0, ...(ngDevMode ? [{ debugName: "_x" }] : []));
this._viewportHeight = signal(null, ...(ngDevMode ? [{ debugName: "_viewportHeight" }] : []));
this.viewport = null;
const treeModel = this.treeModel;
treeModel.virtualScroll = this;
}
fireEvent(event) {
this.treeModel.fireEvent(event);
}
init() {
const fn = this.recalcPositions.bind(this);
fn();
this.treeModel.subscribe(TREE_EVENTS.loadNodeChildren, fn);
}
setupWatchers(injector) {
const fn = this.recalcPositions.bind(this);
const fixScrollEffect = effect(() => {
const yBlocks = this._yBlocks();
const totalHeight = this.totalHeight;
const viewportHeight = this._viewportHeight();
this.fixScroll();
}, ...(ngDevMode ? [{ debugName: "fixScrollEffect", injector }] : [{ injector }]));
const rootsEffect = effect(() => {
const roots = this.treeModel.roots;
fn();
}, ...(ngDevMode ? [{ debugName: "rootsEffect", injector }] : [{ injector }]));
const expandedEffect = effect(() => {
const expandedNodes = this.treeModel.expandedNodes;
fn();
}, ...(ngDevMode ? [{ debugName: "expandedEffect", injector }] : [{ injector }]));
const hiddenEffect = effect(() => {
const hiddenNodes = this.treeModel.hiddenNodes;
fn();
}, ...(ngDevMode ? [{ debugName: "hiddenEffect", injector }] : [{ injector }]));
this._dispose = [
() => fixScrollEffect.destroy(),
() => rootsEffect.destroy(),
() => expandedEffect.destroy(),
() => hiddenEffect.destroy()
];
}
isEnabled() {
return this.treeModel.options.useVirtualScroll;
}
_setYBlocks(value) {
this.yBlocks = value;
}
recalcPositions() {
const vRoot = this.treeModel['_virtualRoot']();
if (vRoot) {
vRoot.height = this._getPositionAfter(this.treeModel.getVisibleRoots(), 0);
}
}
_getPositionAfter(nodes, startPos) {
let position = startPos;
nodes.forEach((node) => {
node.position = position;
position = this._getPositionAfterNode(node, position);
});
return position;
}
_getPositionAfterNode(node, startPos) {
let position = node.getSelfHeight() + startPos;
if (node.children && node.isExpanded) { // TBD: consider loading component as well
position = this._getPositionAfter(node.visibleChildren, position);
}
node.height = position - startPos;
return position;
}
clear() {
this._dispose.forEach((d) => d());
}
setViewport(viewport) {
Object.assign(this, {
viewport,
x: viewport.scrollLeft,
yBlocks: Math.round(viewport.scrollTop / Y_EPSILON),
viewportHeight: viewport.getBoundingClientRect ? viewport.getBoundingClientRect().height : 0
});
}
scrollIntoView(node, force, scrollToMiddle = true) {
if (node.options.scrollContainer) {
const scrollContainer = node.options.scrollContainer;
const scrollContainerHeight = scrollContainer.getBoundingClientRect().height;
const scrollContainerTop = scrollContainer.getBoundingClientRect().top;
const nodeTop = this.viewport.getBoundingClientRect().top + node.position - scrollContainerTop;
if (force || // force scroll to node
nodeTop < scrollContainer.scrollTop || // node is above scroll container
nodeTop + node.getSelfHeight() > scrollContainer.scrollTop + scrollContainerHeight) { // node is below container
scrollContainer.scrollTop = scrollToMiddle ?
nodeTop - scrollContainerHeight / 2 : // scroll to middle
nodeTop; // scroll to start
}
}
else {
if (force || // force scroll to node
node.position < this.y || // node is above viewport
node.position + node.getSelfHeight() > this.y + this.viewportHeight) { // node is below viewport
if (this.viewport) {
this.viewport.scrollTop = scrollToMiddle ?
node.position - this.viewportHeight / 2 : // scroll to middle
node.position; // scroll to start
this._setYBlocks(Math.floor(this.viewport.scrollTop / Y_EPSILON));
}
}
}
}
getViewportNodes(nodes) {
if (!nodes)
return [];
const visibleNodes = nodes.filter((node) => !node.isHidden);
if (!this.isEnabled())
return visibleNodes;
if (!this.viewportHeight || !visibleNodes.length)
return [];
// When loading children async this method is called before their height and position is calculated.
// In that case firstIndex === 0 and lastIndex === visibleNodes.length - 1 (e.g. 1000),
// which means that it loops through every visibleNodes item and push them into viewportNodes array.
// We can prevent nodes from being pushed to the array and wait for the appropriate calculations to take place
const lastVisibleNode = visibleNodes.slice(-1)[0];
if (!lastVisibleNode.height && lastVisibleNode.position === 0)
return [];
// Search for first node in the viewport using binary search
// Look for first node that starts after the beginning of the viewport (with buffer)
// Or that ends after the beginning of the viewport
const firstIndex = binarySearch(visibleNodes, (node) => {
return (node.position + Y_OFFSET > this.y) ||
(node.position + node.height > this.y);
});
// Search for last node in the viewport using binary search
// Look for first node that starts after the end of the viewport (with buffer)
const lastIndex = binarySearch(visibleNodes, (node) => {
return node.position - Y_OFFSET > this.y + this.viewportHeight;
}, firstIndex);
const viewportNodes = [];
for (let i = firstIndex; i <= lastIndex; i++) {
viewportNodes.push(visibleNodes[i]);
}
return viewportNodes;
}
fixScroll() {
const maxY = Math.max(0, this.totalHeight - this.viewportHeight);
if (this.y < 0)
this._setYBlocks(0);
if (this.y > maxY)
this._setYBlocks(maxY / Y_EPSILON);
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: TreeVirtualScroll, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: TreeVirtualScroll }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: TreeVirtualScroll, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
function binarySearch(nodes, condition, firstIndex = 0) {
let index = firstIndex;
let toIndex = nodes.length - 1;
while (index !== toIndex) {
let midIndex = Math.floor((index + toIndex) / 2);
if (condition(nodes[midIndex])) {
toIndex = midIndex;
}
else {
if (index === midIndex)
index = toIndex;
else
index = midIndex;
}
}
return index;
}
class LoadingComponent {
constructor() {
this.template = input(undefined, ...(ngDevMode ? [{ debugName: "template" }] : []));
this.node = input(undefined, ...(ngDevMode ? [{ debugName: "node" }] : []));
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: LoadingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
/** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.3", type: LoadingComponent, isStandalone: true, selector: "tree-loading-component", inputs: { template: { classPropertyName: "template", publicName: "template", isSignal: true, isRequired: false, transformFunction: null }, node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
@if (!template()) {
<span>loading...</span>
}
<ng-container
[ngTemplateOutlet]="template()"
[ngTemplateOutletContext]="{ $implicit: node() }"
>
</ng-container>
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: LoadingComponent, decorators: [{
type: Component,
args: [{
encapsulation: ViewEncapsulation.None,
selector: 'tree-loading-component',
template: `
@if (!template()) {
<span>loading...</span>
}
<ng-container
[ngTemplateOutlet]="template()"
[ngTemplateOutletContext]="{ $implicit: node() }"
>
</ng-container>
`,
imports: [NgTemplateOutlet]
}]
}] });
class TreeViewportComponent {
constructor() {
this.elementRef = inject(ElementRef);
this.virtualScroll = inject(TreeVirtualScroll);
this.injector = inject(Injector);
this.setViewport = this.throttle(() => {
this.virtualScroll.setViewport(this.elementRef.nativeElement);
}, 17);
this.scrollEventHandler = this.setViewport.bind(this);
}
ngOnInit() {
this.virtualScroll.init();
this.virtualScroll.setupWatchers(this.injector);
}
ngAfterViewInit() {
setTimeout(() => {
this.setViewport();
this.virtualScroll.fireEvent({ eventName: TREE_EVENTS.initialized });
});
let el = this.elementRef.nativeElement;
el.addEventListener('scroll', this.scrollEventHandler);
}
ngOnDestroy() {
this.virtualScroll.clear();
let el = this.elementRef.nativeElement;
el.removeEventListener('scroll', this.scrollEventHandler);
}
getTotalHeight() {
return ((this.virtualScroll.isEnabled() &&
this.virtualScroll.totalHeight + 'px') ||
'auto');
}
throttle(func, timeFrame) {
let lastTime = 0;
return function () {
let now = Date.now();
if (now - lastTime >= timeFrame) {
func();
lastTime = now;
}
};
}
/** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: TreeViewportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
/** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.3", type: TreeViewportComponent, isStandalone: true, selector: "tree-viewport", providers: [TreeVirtualScroll], ngImport: i0, template: `
<div [style.height]="getTotalHeight()">
<ng-content></ng-content>
</div>
`, isInline: true }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: TreeViewportComponent, decorators: [{
type: Component,
args: [{ selector: