@ephraim-haber/angular-tree-component
Version:
A simple yet powerful tree component for Angular 14+
1,398 lines (1,390 loc) • 107 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 i1 from '@angular/common';
import { CommonModule } from '@angular/common';
import { autorun, reaction, computed as computed$1, observable as observable$1, action as action$1 } from 'mobx';
class TreeMobxAutorunDirective {
constructor(templateRef, viewContainer) {
this.templateRef = templateRef;
this.viewContainer = viewContainer;
this.templateBindings = {};
}
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 { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TreeMobxAutorunDirective, deps: [{ token: i0.TemplateRef }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive }); }
/** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.1", type: TreeMobxAutorunDirective, isStandalone: false, selector: "[treeMobxAutorun]", inputs: { treeMobxAutorun: "treeMobxAutorun" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TreeMobxAutorunDirective, decorators: [{
type: Directive,
args: [{
selector: '[treeMobxAutorun]',
standalone: false,
}]
}], ctorParameters: () => [{ 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 {
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',
};
var __decorate$3 = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
class TreeNode {
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.position = 0;
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);
}
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));
}
}
__decorate$3([
computed$1
], TreeNode.prototype, "isHidden", null);
__decorate$3([
computed$1
], TreeNode.prototype, "isExpanded", null);
__decorate$3([
computed$1
], TreeNode.prototype, "isActive", null);
__decorate$3([
computed$1
], TreeNode.prototype, "isFocused", null);
__decorate$3([
computed$1
], TreeNode.prototype, "isSelected", null);
__decorate$3([
computed$1
], TreeNode.prototype, "isAllSelected", null);
__decorate$3([
computed$1
], TreeNode.prototype, "isPartiallySelected", null);
__decorate$3([
observable$1
], TreeNode.prototype, "children", void 0);
__decorate$3([
observable$1
], TreeNode.prototype, "index", void 0);
__decorate$3([
observable$1
], TreeNode.prototype, "position", void 0);
__decorate$3([
observable$1
], TreeNode.prototype, "height", void 0);
__decorate$3([
computed$1
], TreeNode.prototype, "level", null);
__decorate$3([
computed$1
], TreeNode.prototype, "path", null);
__decorate$3([
computed$1
], TreeNode.prototype, "visibleChildren", null);
__decorate$3([
action$1
], TreeNode.prototype, "setIsSelected", null);
__decorate$3([
action$1
], TreeNode.prototype, "_initChildren", null);
function uuid() {
return Math.floor(Math.random() * 10000000000000);
}
var __decorate$2 = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
class TreeModel {
constructor() {
this.options = new TreeOptions();
this.eventNames = Object.keys(TREE_EVENTS);
this.expandedNodeIds = {};
this.selectedLeafNodeIds = {};
this.activeNodeIds = {};
this.hiddenNodeIds = {};
this.focusedNodeId = null;
this.firstUpdate = true;
this.subscriptions = [];
}
static { this.focusedTree = null; }
// 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 { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TreeModel, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TreeModel }); }
}
__decorate$2([
observable$1
], TreeModel.prototype, "roots", void 0);
__decorate$2([
observable$1
], TreeModel.prototype, "expandedNodeIds", void 0);
__decorate$2([
observable$1
], TreeModel.prototype, "selectedLeafNodeIds", void 0);
__decorate$2([
observable$1
], TreeModel.prototype, "activeNodeIds", void 0);
__decorate$2([
observable$1
], TreeModel.prototype, "hiddenNodeIds", void 0);
__decorate$2([
observable$1
], TreeModel.prototype, "focusedNodeId", void 0);
__decorate$2([
observable$1
], TreeModel.prototype, "virtualRoot", void 0);
__decorate$2([
computed$1
], TreeModel.prototype, "focusedNode", null);
__decorate$2([
computed$1
], TreeModel.prototype, "expandedNodes", null);
__decorate$2([
computed$1
], TreeModel.prototype, "activeNodes", null);
__decorate$2([
computed$1
], TreeModel.prototype, "hiddenNodes", null);
__decorate$2([
computed$1
], TreeModel.prototype, "selectedLeafNodes", null);
__decorate$2([
action$1
], TreeModel.prototype, "setData", null);
__decorate$2([
action$1
], TreeModel.prototype, "update", null);
__decorate$2([
action$1
], TreeModel.prototype, "setFocusedNode", null);
__decorate$2([
action$1
], TreeModel.prototype, "setFocus", null);
__decorate$2([
action$1
], TreeModel.prototype, "doForAll", null);
__decorate$2([
action$1
], TreeModel.prototype, "focusNextNode", null);
__decorate$2([
action$1
], TreeModel.prototype, "focusPreviousNode", null);
__decorate$2([
action$1
], TreeModel.prototype, "focusDrillDown", null);
__decorate$2([
action$1
], TreeModel.prototype, "focusDrillUp", null);
__decorate$2([
action$1
], TreeModel.prototype, "setActiveNode", null);
__decorate$2([
action$1
], TreeModel.prototype, "setSelectedNode", null);
__decorate$2([
action$1
], TreeModel.prototype, "setExpandedNode", null);
__decorate$2([
action$1
], TreeModel.prototype, "expandAll", null);
__decorate$2([
action$1
], TreeModel.prototype, "collapseAll", null);
__decorate$2([
action$1
], TreeModel.prototype, "setIsHidden", null);
__decorate$2([
action$1
], TreeModel.prototype, "setHiddenNodeIds", null);
__decorate$2([
action$1
], TreeModel.prototype, "filterNodes", null);
__decorate$2([
action$1
], TreeModel.prototype, "clearFilter", null);
__decorate$2([
action$1
], TreeModel.prototype, "moveNode", null);
__decorate$2([
action$1
], TreeModel.prototype, "copyNode", null);
__decorate$2([
action$1
], TreeModel.prototype, "setState", null);
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TreeModel, decorators: [{
type: Injectable
}], propDecorators: { roots: [], expandedNodeIds: [], selectedLeafNodeIds: [], activeNodeIds: [], hiddenNodeIds: [], focusedNodeId: [], virtualRoot: [], focusedNode: [], expandedNodes: [], activeNodes: [], hiddenNodes: [], selectedLeafNodes: [], setData: [], update: [], setFocusedNode: [], setFocus: [], doForAll: [], focusNextNode: [], focusPreviousNode: [], focusDrillDown: [], focusDrillUp: [], setActiveNode: [], setSelectedNode: [], setExpandedNode: [], expandAll: [], collapseAll: [], setIsHidden: [], setHiddenNodeIds: [], filterNodes: [], clearFilter: [], moveNode: [], copyNode: [], setState: [] } });
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: "19.0.1", ngImport: i0, type: TreeDraggedElement, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TreeDraggedElement, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TreeDraggedElement, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
var __decorate$1 = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
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 y() {
return this.yBlocks * Y_EPSILON;
}
get totalHeight() {
return this.treeModel.virtualRoot ? this.treeModel.virtualRoot.height : 0;
}
constructor(treeModel) {
this.treeModel = treeModel;
this.yBlocks = 0;
this.x = 0;
this.viewportHeight = null;
this.viewport = null;
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;
}
_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 { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TreeVirtualScroll, deps: [{ token: TreeModel }], target: i0.ɵɵFactoryTarget.Injectable }); }
/** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: TreeVirtualScroll }); }
}
__decorate$1([
observable$1
], TreeVirtualScroll.prototype, "yBlocks", void 0);
__decorate$1([
observable$1
], TreeVirtualScrol