UNPKG

@ali-hm/angular-tree-component

Version:

A simple yet powerful tree component for Angular 12+

1,283 lines (1,274 loc) 103 kB
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: