@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
JavaScript
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 =