UNPKG

@odymaui/angular-tree-component

Version:

A simple yet powerful tree component for Angular16. WARNING: This is an unsupported fork for use in a dependent project to upgrade it to Angular 16. Unit tests pass and the example-app works as expected.

1,313 lines (1,302 loc) 106 kB
import * as i0 from '@angular/core'; import { Directive, Input, Injectable, Component, ViewEncapsulation, EventEmitter, Output, HostListener, ContentChild, ViewChild, NgModule } from '@angular/core'; import * as i3 from '@angular/common'; import { CommonModule } from '@angular/common'; import { autorun, makeObservable, computed as computed$1, observable as observable$1, action as action$1, reaction } from 'mobx'; class TreeMobxAutorunDirective { templateRef; viewContainer; templateBindings = {}; dispose; view; treeMobxAutorun; constructor(templateRef, viewContainer) { this.templateRef = templateRef; this.viewContainer = viewContainer; } ngOnInit() { this.view = this.viewContainer.createEmbeddedView(this.templateRef); if (this.dispose) { this.dispose(); } if (this.shouldDetach()) { this.view.detach(); } this.autoDetect(this.view); } shouldDetach() { return this.treeMobxAutorun && this.treeMobxAutorun.detach; } autoDetect(view) { this.dispose = autorun(() => view.detectChanges()); } ngOnDestroy() { if (this.dispose) { this.dispose(); } } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.2", ngImport: i0, type: TreeMobxAutorunDirective, deps: [{ token: i0.TemplateRef }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive }); /** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.1.2", type: TreeMobxAutorunDirective, selector: "[treeMobxAutorun]", inputs: { treeMobxAutorun: "treeMobxAutorun" }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.2", ngImport: i0, type: TreeMobxAutorunDirective, decorators: [{ type: Directive, args: [{ selector: '[treeMobxAutorun]' }] }], ctorParameters: function () { return [{ type: i0.TemplateRef }, { type: i0.ViewContainerRef }]; }, propDecorators: { treeMobxAutorun: [{ type: Input }] } }); 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 { options; 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; } actionMapping; 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 { data; parent; treeModel; handler; 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; } children; index; position = 0; height; 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`; } _originalNode; get originalNode() { return this._originalNode; } ; constructor(data, parent, treeModel, index) { this.data = data; this.parent = parent; this.treeModel = treeModel; makeObservable(this, { isHidden: computed$1, isExpanded: computed$1, isActive: computed$1, isFocused: computed$1, isSelected: computed$1, isAllSelected: computed$1, isPartiallySelected: computed$1, children: observable$1, index: observable$1, position: observable$1, height: observable$1, level: computed$1, path: computed$1, visibleChildren: computed$1, setIsSelected: action$1, _initChildren: action$1, }); 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 } }); } allowDrop = (element, $event) => { return this.options.allowDrop(element, { parent: this, index: 0 }, $event); }; allowDragoverStyling = () => { return this.options.allowDragoverStyling; }; 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); } return this; } ; autoLoadChildren() { this.handler = reaction(() => this.isExpanded, (isExpanded) => { if (!this.children && this.hasChildren && isExpanded) { this.loadNodeChildren(); } }, { fireImmediately: true }); } dispose() { if (this.children) { this.children.forEach((child) => child.dispose()); } if (this.handler) { this.handler(); } 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 { static focusedTree = null; options = new TreeOptions(); nodes; eventNames = Object.keys(TREE_EVENTS); virtualScroll; roots; expandedNodeIds = {}; selectedLeafNodeIds = {}; activeNodeIds = {}; hiddenNodeIds = {}; focusedNodeId = null; virtualRoot; firstUpdate = true; events; subscriptions = []; constructor() { makeObservable(this, { roots: observable$1, expandedNodeIds: observable$1, selectedLeafNodeIds: observable$1, activeNodeIds: observable$1, hiddenNodeIds: observable$1, focusedNodeId: observable$1, virtualRoot: observable$1, focusedNode: computed$1, expandedNodes: computed$1, activeNodes: computed$1, hiddenNodes: computed$1, selectedLeafNodes: computed$1, setData: action$1, update: action$1, setFocusedNode: action$1, setFocus: action$1, doForAll: action$1, focusNextNode: action$1, focusPreviousNode: action$1, focusDrillDown: action$1, focusDrillUp: action$1, setActiveNode: action$1, setSelectedNode: action$1, setExpandedNode: action$1, expandAll: action$1, collapseAll: action$1, setIsHidden: action$1, setHiddenNodeIds: action$1, filterNodes: action$1, clearFilter: action$1, moveNode: action$1, copyNode: action$1, setState: action$1, }); } // 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() { return this.roots && this.roots.length === 0; } get focusedNode() { return this.focusedNodeId ? this.getNodeById(this.focusedNodeId) : null; } get expandedNodes() { const nodes = Object.keys(this.expandedNodeIds) .filter((id) => this.expandedNodeIds[id]) .map((id) => this.getNodeById(id)); return nodes.filter(Boolean); } get activeNodes() { const nodes = Object.keys(this.activeNodeIds) .filter((id) => this.activeNodeIds[id]) .map((id) => this.getNodeById(id)); return nodes.filter(Boolean); } get hiddenNodes() { const nodes = Object.keys(this.hiddenNodeIds) .filter((id) => this.hiddenNodeIds[id]) .map((id) => this.getNodeById(id)); return nodes.filter(Boolean); } get selectedLeafNodes() { const nodes = Object.keys(this.selectedLeafNodeIds) .filter((id) => this.selectedLeafNodeIds[id]) .map((id) => this.getNodeById(id)); return nodes.filter(Boolean); } // 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 if (this.virtualRoot) { this.virtualRoot.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(); this.virtualRoot = new TreeNode(virtualRootConfig, null, this, 0); this.roots = this.virtualRoot.children; // Fire event: if (this.firstUpdate) { if (this.roots) { this.firstUpdate = false; this._calculateExpandedNodes(); } } else { this.fireEvent({ eventName: TREE_EVENTS.updateData }); } } setFocusedNode(node) { this.focusedNodeId = 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 = Object.assign({}, this.selectedLeafNodeIds, { [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 = Object.assign({}, this.expandedNodeIds, { [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 = Object.assign({}, this.hiddenNodeIds, { [node.id]: value }); } setHiddenNodeIds(nodeIds) { this.hiddenNodeIds = nodeIds.reduce((hiddenNodeIds, id) => Object.assign(hiddenNodeIds, { [id]: true }), {}); } 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 = ids; this.fireEvent({ eventName: TREE_EVENTS.changeFilter }); } clearFilter() { this.hiddenNodeIds = {}; 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; Object.assign(this, { expandedNodeIds: state.expandedNodeIds || {}, selectedLeafNodeIds: state.selectedLeafNodeIds || {}, activeNodeIds: state.activeNodeIds || {}, hiddenNodeIds: state.hiddenNodeIds || {}, focusedNodeId: state.focusedNodeId }); } subscribeToState(fn) { autorun(() => 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 = Object.assign({}, this.expandedNodeIds, { [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 = { [node.id]: true }; } else { this.activeNodeIds = {}; } } _setActiveNodeMulti(node, value) { this.activeNodeIds = Object.assign({}, this.activeNodeIds, { [node.id]: value }); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.2", ngImport: i0, type: TreeModel, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.1.2", ngImport: i0, type: TreeModel }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.2", ngImport: i0, type: TreeModel, decorators: [{ type: Injectable }], ctorParameters: function () { return []; } }); class TreeDraggedElement { _draggedElement = null; set(draggedElement) { this._draggedElement = draggedElement; } get() { return this._draggedElement; } isDragging() { return !!this.get(); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.2", ngImport: i0, type: TreeDraggedElement, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.1.2", ngImport: i0, type: TreeDraggedElement, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.2", 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 { treeModel; _dispose; yBlocks = 0; x = 0; viewportHeight = null; viewport = null; get y() { return this.yBlocks * Y_EPSILON; } get totalHeight() { return this.treeModel.virtualRoot ? this.treeModel.virtualRoot.height : 0; } constructor(treeModel) { this.treeModel = treeModel; makeObservable(this, { yBlocks: observable$1, x: observable$1, viewportHeight: observable$1, y: computed$1, totalHeight: computed$1, _setYBlocks: action$1, recalcPositions: action$1, setViewport: action$1, scrollIntoView: action$1, }); treeModel.virtualScroll = this; this._dispose = [autorun(() => this.fixScroll())]; } fireEvent(event) { this.treeModel.fireEvent(event); } init() { const fn = this.recalcPositions.bind(this); fn(); this._dispose = [ ...this._dispose, reaction(() => this.treeModel.roots, fn), reaction(() => this.treeModel.expandedNodeIds, fn), reaction(() => this.treeModel.hiddenNodeIds, fn) ]; this.treeModel.subscribe(TREE_EVENTS.loadNodeChildren, fn); } isEnabled() { return this.treeModel.options.useVirtualScroll; } //was private, public for now so makeObservable to work and deal as a todo _setYBlocks(value) { this.yBlocks = value; } recalcPositions() { this.treeModel.virtualRoot.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 ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.2", ngImport: i0, type: TreeVirtualScroll, deps: [{ token: TreeModel }], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.1.2", ngImport: i0, type: TreeVirtualScroll }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.2", ngImport: i0, type: TreeVirtualScroll, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: TreeModel }]; } }); 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 { template; node; /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.1.2", ngImport: i0, type: LoadingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); /** @nocollapse */ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.1.2", type: LoadingComponent, selector: "tree-loading-component", inputs: { template: "template", node: "node" }, ngImport: i0, template: ` <span *ngIf="!template">loading...</span> <ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{ $implicit: node }"> </ng-container> `, isInline: true, dependencies: [{ kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], encapsulation: i0.ViewEncapsulation.None }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.1.2", ngImport: i0, type: LoadingComponent, decorators: [{ type: Component, args: [{ encapsulation: ViewEncapsulation.None, selector: 'tree-loading-component', template: ` <span *ngIf="!template">loading...</span> <ng-container [ngTemplateOutlet]="template" [ngTemplateOutletContext]="{ $implicit: node }"> </ng-container> `, }] }], propDecorators: { template: [{ type: Input }], node: [{ type: Input }] } }); // Re-export mobx operators to be able to use inside components with AOT: function actionInternal(...args) { return action$1(...args); } const action = Object.assign(actionInternal, action$1); function computedInternal(...args) { return computed$1(...args); } const computed = Object.assign(computedInternal, computed$1); function observableInternal(...args) { return observable$1(...args); } const observable = Object.assign(observableInternal, observable$1); const DRAG_OVER_CLASS$1 = 'is-dragging-over'; const DRAG_DISABLED_CLASS = 'is-dragging-over-disabled'; class TreeDropDirective { el; renderer; treeDraggedElement; ngZone; allowDragoverStyling = true; onDropCallback = new EventEmitter(); onDragOverCallback =