ng2-tree
Version:
angular2 component for visualizing data that can be naturally represented as a tree
1,359 lines (1,341 loc) • 81.3 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, Inject, ElementRef, Renderer2, Directive, Input, EventEmitter, Output, HostListener, Component, ViewChild, Pipe, TemplateRef, ContentChild, NgModule } from '@angular/core';
import { Subject, Observable, of, merge } from 'rxjs';
import { filter } from 'rxjs/operators';
import * as uuidv4 from 'uuid/v4';
import * as i3 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i1 from '@angular/platform-browser';
class NodeEvent {
node;
constructor(node) {
this.node = node;
}
}
class NodeSelectedEvent extends NodeEvent {
constructor(node) {
super(node);
}
}
class NodeUnselectedEvent extends NodeEvent {
constructor(node) {
super(node);
}
}
class NodeDestructiveEvent extends NodeEvent {
constructor(node) {
super(node);
}
}
class NodeMovedEvent extends NodeDestructiveEvent {
previousParent;
constructor(node, previousParent) {
super(node);
this.previousParent = previousParent;
}
}
class NodeRemovedEvent extends NodeDestructiveEvent {
lastIndex;
constructor(node, lastIndex) {
super(node);
this.lastIndex = lastIndex;
}
}
class NodeCreatedEvent extends NodeDestructiveEvent {
constructor(node) {
super(node);
}
}
class NodeRenamedEvent extends NodeDestructiveEvent {
oldValue;
newValue;
constructor(node, oldValue, newValue) {
super(node);
this.oldValue = oldValue;
this.newValue = newValue;
}
}
class NodeExpandedEvent extends NodeEvent {
constructor(node) {
super(node);
}
}
class NodeCollapsedEvent extends NodeEvent {
constructor(node) {
super(node);
}
}
class MenuItemSelectedEvent extends NodeEvent {
selectedItem;
constructor(node, selectedItem) {
super(node);
this.selectedItem = selectedItem;
}
}
class LoadNextLevelEvent extends NodeEvent {
constructor(node) {
super(node);
}
}
class NodeCheckedEvent extends NodeEvent {
constructor(node) {
super(node);
}
}
class NodeUncheckedEvent extends NodeEvent {
constructor(node) {
super(node);
}
}
class NodeIndeterminedEvent extends NodeEvent {
constructor(node) {
super(node);
}
}
class NodeDraggableEvent {
captured;
target;
constructor(captured, target) {
this.captured = captured;
this.target = target;
}
}
class NodeDraggableService {
draggableNodeEvents$ = new Subject();
capturedNode;
fireNodeDragged(captured, target) {
if (!captured.tree || captured.tree.isStatic()) {
return;
}
this.draggableNodeEvents$.next(new NodeDraggableEvent(captured, target));
}
captureNode(node) {
this.capturedNode = node;
}
getCapturedNode() {
return this.capturedNode;
}
releaseCapturedNode() {
this.capturedNode = null;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeDraggableService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeDraggableService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeDraggableService, decorators: [{
type: Injectable
}] });
function isEmpty(value) {
if (typeof value === 'string') {
return !/\S/.test(value);
}
if (Array.isArray(value)) {
return value.length === 0;
}
return isNil(value);
}
function trim(value) {
return isNil(value) ? '' : value.trim();
}
function has(value, prop) {
return value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, prop);
}
function isFunction(value) {
return typeof value === 'function';
}
function get(value, path, defaultValue) {
let result = value;
for (const prop of path.split('.')) {
if (!result || !Reflect.has(result, prop)) {
return defaultValue;
}
result = result[prop];
}
return isNil(result) || result === value ? defaultValue : result;
}
function omit(value, propsToSkip) {
if (!value) {
return value;
}
const normalizedPropsToSkip = typeof propsToSkip === 'string' ? [propsToSkip] : propsToSkip;
return Object.keys(value).reduce((result, prop) => {
if (includes(normalizedPropsToSkip, prop)) {
return result;
}
return Object.assign(result, { [prop]: value[prop] });
}, {});
}
function size(value) {
return isEmpty(value) ? 0 : value.length;
}
function once(fn) {
let result;
return (...args) => {
if (fn) {
result = fn(...args);
fn = null;
}
return result;
};
}
function defaultsDeep(target, ...sources) {
return [target].concat(sources).reduce((result, source) => {
if (!source) {
return result;
}
Object.keys(source).forEach(prop => {
if (isNil(result[prop])) {
result[prop] = source[prop];
return;
}
if (typeof result[prop] === 'object' && !Array.isArray(result[prop])) {
result[prop] = defaultsDeep(result[prop], source[prop]);
return;
}
});
return result;
}, {});
}
function includes(target, value) {
if (isNil(target)) {
return false;
}
const index = typeof target === 'string' ? target.indexOf(value) : target.indexOf(value);
return index > -1;
}
function isNil(value) {
return value === undefined || value === null;
}
class TreeService {
nodeDraggableService;
nodeMoved$ = new Subject();
nodeRemoved$ = new Subject();
nodeRenamed$ = new Subject();
nodeCreated$ = new Subject();
nodeSelected$ = new Subject();
nodeUnselected$ = new Subject();
nodeExpanded$ = new Subject();
nodeCollapsed$ = new Subject();
menuItemSelected$ = new Subject();
loadNextLevel$ = new Subject();
nodeChecked$ = new Subject();
nodeUnchecked$ = new Subject();
nodeIndetermined$ = new Subject();
controllers = new Map();
constructor(nodeDraggableService) {
this.nodeDraggableService = nodeDraggableService;
this.nodeRemoved$.subscribe((e) => e.node.removeItselfFromParent());
}
unselectStream(tree) {
return this.nodeSelected$.pipe(filter((e) => tree !== e.node));
}
fireNodeRemoved(tree) {
this.nodeRemoved$.next(new NodeRemovedEvent(tree, tree.positionInParent));
}
fireNodeCreated(tree) {
this.nodeCreated$.next(new NodeCreatedEvent(tree));
}
fireNodeSelected(tree) {
this.nodeSelected$.next(new NodeSelectedEvent(tree));
}
fireNodeUnselected(tree) {
this.nodeUnselected$.next(new NodeUnselectedEvent(tree));
}
fireNodeRenamed(oldValue, tree) {
this.nodeRenamed$.next(new NodeRenamedEvent(tree, oldValue, tree.value));
}
fireNodeMoved(tree, parent) {
this.nodeMoved$.next(new NodeMovedEvent(tree, parent));
}
fireMenuItemSelected(tree, selectedItem) {
this.menuItemSelected$.next(new MenuItemSelectedEvent(tree, selectedItem));
}
fireNodeSwitchFoldingType(tree) {
if (tree.isNodeExpanded()) {
this.fireNodeExpanded(tree);
if (this.shouldFireLoadNextLevel(tree)) {
this.fireLoadNextLevel(tree);
}
}
else if (tree.isNodeCollapsed()) {
this.fireNodeCollapsed(tree);
}
}
fireNodeExpanded(tree) {
this.nodeExpanded$.next(new NodeExpandedEvent(tree));
}
fireNodeCollapsed(tree) {
this.nodeCollapsed$.next(new NodeCollapsedEvent(tree));
}
fireLoadNextLevel(tree) {
this.loadNextLevel$.next(new LoadNextLevelEvent(tree));
}
fireNodeChecked(tree) {
this.nodeChecked$.next(new NodeCheckedEvent(tree));
}
fireNodeUnchecked(tree) {
this.nodeUnchecked$.next(new NodeUncheckedEvent(tree));
}
draggedStream(tree, element) {
return this.nodeDraggableService.draggableNodeEvents$.pipe(filter((e) => e.target === element), filter((e) => !e.captured.tree.hasChild(tree)));
}
setController(id, controller) {
this.controllers.set(id, controller);
}
deleteController(id) {
if (this.controllers.has(id)) {
this.controllers.delete(id);
}
}
getController(id) {
if (this.controllers.has(id)) {
return this.controllers.get(id);
}
return null;
}
hasController(id) {
return this.controllers.has(id);
}
shouldFireLoadNextLevel(tree) {
const shouldLoadNextLevel = tree.node.emitLoadNextLevel &&
!tree.node.loadChildren &&
!tree.childrenAreBeingLoaded() &&
isEmpty(tree.children);
if (shouldLoadNextLevel) {
tree.loadingChildrenRequested();
}
return shouldLoadNextLevel;
}
fireNodeIndetermined(tree) {
this.nodeIndetermined$.next(new NodeIndeterminedEvent(tree));
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TreeService, deps: [{ token: NodeDraggableService }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TreeService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TreeService, decorators: [{
type: Injectable
}], ctorParameters: function () { return [{ type: NodeDraggableService, decorators: [{
type: Inject,
args: [NodeDraggableService]
}] }]; } });
// This forces angular compiler to generate a "rxjs-imports.metadata.json"
// with a valid metadata instead of "[null]"
const noop = () => { };
class FoldingType {
_cssClass;
static Expanded = new FoldingType('node-expanded');
static Collapsed = new FoldingType('node-collapsed');
static Empty = new FoldingType('node-empty');
static Leaf = new FoldingType('node-leaf');
constructor(_cssClass) {
this._cssClass = _cssClass;
}
get cssClass() {
return this._cssClass;
}
}
class TreeModelSettings {
/* cssClasses - set custom css classes which will be used for a tree */
cssClasses;
/* Templates - set custom html templates to be used in a tree */
templates;
/**
* "leftMenu" property when set to true makes left menu available.
* @name TreeModelSettings#leftMenu
* @type boolean
* @default false
*/
leftMenu;
/**
* "rightMenu" property when set to true makes right menu available.
* @name TreeModelSettings#rightMenu
* @type boolean
* @default true
*/
rightMenu;
/**
* "menu" property when set will be available as custom context menu.
* @name TreeModelSettings#MenuItems
* @type NodeMenuItem
*/
menuItems;
/**
* "static" property when set to true makes it impossible to drag'n'drop tree or call a menu on it.
* @name TreeModelSettings#static
* @type boolean
* @default false
*/
static;
isCollapsedOnInit;
checked;
selectionAllowed;
keepNodesInDOM;
static NOT_CASCADING_SETTINGS = ['selectionAllowed'];
static merge(child, parent) {
const parentCascadingSettings = omit(get(parent, 'settings'), TreeModelSettings.NOT_CASCADING_SETTINGS);
return defaultsDeep({}, get(child, 'settings'), parentCascadingSettings, {
static: false,
leftMenu: false,
rightMenu: true,
isCollapsedOnInit: false,
checked: false,
keepNodesInDOM: false,
selectionAllowed: true
});
}
}
class Ng2TreeSettings {
/**
* Indicates root visibility in the tree. When true - root is invisible.
* @name Ng2TreeSettings#rootIsVisible
* @type boolean
*/
rootIsVisible = true;
showCheckboxes = false;
enableCheckboxes = true;
}
var TreeStatus;
(function (TreeStatus) {
TreeStatus[TreeStatus["New"] = 0] = "New";
TreeStatus[TreeStatus["Modified"] = 1] = "Modified";
TreeStatus[TreeStatus["IsBeingRenamed"] = 2] = "IsBeingRenamed";
})(TreeStatus || (TreeStatus = {}));
var ChildrenLoadingState;
(function (ChildrenLoadingState) {
ChildrenLoadingState[ChildrenLoadingState["NotStarted"] = 0] = "NotStarted";
ChildrenLoadingState[ChildrenLoadingState["Loading"] = 1] = "Loading";
ChildrenLoadingState[ChildrenLoadingState["Completed"] = 2] = "Completed";
})(ChildrenLoadingState || (ChildrenLoadingState = {}));
class Tree {
_children;
_loadChildren;
_childrenLoadingState = ChildrenLoadingState.NotStarted;
_childrenAsyncOnce = once(() => {
return new Observable((observer) => {
setTimeout(() => {
this._childrenLoadingState = ChildrenLoadingState.Loading;
this._loadChildren((children) => {
this._children = (children || []).map((child) => new Tree(child, this));
this._childrenLoadingState = ChildrenLoadingState.Completed;
observer.next(this.children);
observer.complete();
});
});
});
});
node;
parent;
// STATIC METHODS ----------------------------------------------------------------------------------------------------
/**
* Check that value passed is not empty (it doesn't consist of only whitespace symbols).
* @param {string} value - A value that should be checked.
* @returns {boolean} - A flag indicating that value is empty or not.
* @static
*/
static isValueEmpty(value) {
return isEmpty(trim(value));
}
/**
* Check whether a given value can be considered RenamableNode.
* @param {any} value - A value to check.
* @returns {boolean} - A flag indicating whether given value is Renamable node or not.
* @static
*/
static isRenamable(value) {
return (has(value, 'setName') &&
isFunction(value.setName) &&
(has(value, 'toString') && isFunction(value.toString) && value.toString !== Object.toString));
}
static cloneTreeShallow(origin) {
const tree = new Tree(Object.assign({}, origin.node));
tree._children = origin._children;
return tree;
}
static applyNewValueToRenamable(value, newValue) {
const renamableValue = Object.assign({}, value);
renamableValue.setName(newValue);
return renamableValue;
}
/**
* Build an instance of Tree from an object implementing TreeModel interface.
* @param {TreeModel} model - A model that is used to build a tree.
* @param {Tree} [parent] - An optional parent if you want to build a tree from the model that should be a child of an existing Tree instance.
* @param {boolean} [isBranch] - An option that makes a branch from created tree. Branch can have children.
*/
constructor(node, parent = null, isBranch = false) {
this.buildTreeFromModel(node, parent, isBranch || Array.isArray(node.children));
}
buildTreeFromModel(model, parent, isBranch) {
this.parent = parent;
this.node = Object.assign(omit(model, 'children'), { settings: TreeModelSettings.merge(model, get(parent, 'node')) }, { emitLoadNextLevel: model.emitLoadNextLevel === true });
if (isFunction(this.node.loadChildren)) {
this._loadChildren = this.node.loadChildren;
}
else {
get(model, 'children', []).forEach((child, index) => {
this._addChild(new Tree(child, this), index);
});
}
if (!Array.isArray(this._children)) {
this._children = this.node.loadChildren || isBranch ? [] : null;
}
}
hasDeferredChildren() {
return typeof this._loadChildren === 'function';
}
/* Setting the children loading state to Loading since a request was dispatched to the client */
loadingChildrenRequested() {
this._childrenLoadingState = ChildrenLoadingState.Loading;
}
/**
* Check whether children of the node are being loaded.
* Makes sense only for nodes that define `loadChildren` function.
* @returns {boolean} A flag indicating that children are being loaded.
*/
childrenAreBeingLoaded() {
return this._childrenLoadingState === ChildrenLoadingState.Loading;
}
/**
* Check whether children of the node were loaded.
* Makes sense only for nodes that define `loadChildren` function.
* @returns {boolean} A flag indicating that children were loaded.
*/
childrenWereLoaded() {
return this._childrenLoadingState === ChildrenLoadingState.Completed;
}
canLoadChildren() {
return (this._childrenLoadingState === ChildrenLoadingState.NotStarted &&
this.foldingType === FoldingType.Expanded &&
!!this._loadChildren);
}
/**
* Check whether children of the node should be loaded and not loaded yet.
* Makes sense only for nodes that define `loadChildren` function.
* @returns {boolean} A flag indicating that children should be loaded for the current node.
*/
childrenShouldBeLoaded() {
return !this.childrenWereLoaded() && (!!this._loadChildren || this.node.emitLoadNextLevel === true);
}
/**
* Get children of the current tree.
* @returns {Tree[]} The children of the current tree.
*/
get children() {
return this._children;
}
/**
* By getting value from this property you start process of loading node's children using `loadChildren` function.
* Once children are loaded `loadChildren` function won't be called anymore and loaded for the first time children are emitted in case of subsequent calls.
* @returns {Observable<Tree[]>} An observable which emits children once they are loaded.
*/
get childrenAsync() {
if (this.canLoadChildren()) {
return this._childrenAsyncOnce();
}
return of(this.children);
}
/**
* By calling this method you start process of loading node's children using `loadChildren` function.
*/
reloadChildren() {
if (this.childrenShouldBeLoaded()) {
this._childrenLoadingState = ChildrenLoadingState.Loading;
this._loadChildren((children) => {
this._children = children && children.map((child) => new Tree(child, this));
this._childrenLoadingState = ChildrenLoadingState.Completed;
});
}
}
/**
* By calling this method you will remove all current children of a treee and create new.
*/
setChildren(children) {
this._children = children && children.map((child) => new Tree(child, this));
if (this.childrenShouldBeLoaded()) {
this._childrenLoadingState = ChildrenLoadingState.Completed;
}
}
/**
* Create a new node in the current tree.
* @param {boolean} isBranch - A flag that indicates whether a new node should be a "Branch". "Leaf" node will be created by default
* @param {TreeModel} model - Tree model of the new node which will be inserted. Empty node will be created by default and it will fire edit mode of this node
* @returns {Tree} A newly created child node.
*/
createNode(isBranch, model = { value: '' }) {
const tree = new Tree(model, this, isBranch);
if (!model.id) {
tree.markAsNew();
}
tree.id = tree.id || uuidv4();
if (this.childrenShouldBeLoaded() && !(this.childrenAreBeingLoaded() || this.childrenWereLoaded())) {
return null;
}
if (this.isLeaf()) {
return this.addSibling(tree);
}
else {
return this.addChild(tree);
}
}
/**
* Get the value of the current node
* @returns {(string|RenamableNode)} The value of the node.
*/
get value() {
return this.node.value;
}
set checked(checked) {
this.node.settings = Object.assign({}, this.node.settings, { checked });
}
get checked() {
return !!get(this.node.settings, 'checked');
}
get checkedChildren() {
return this.hasLoadedChildern() ? this.children.filter(child => child.checked) : [];
}
set selectionAllowed(selectionAllowed) {
this.node.settings = Object.assign({}, this.node.settings, { selectionAllowed });
}
get selectionAllowed() {
const value = get(this.node.settings, 'selectionAllowed');
return isNil(value) ? true : !!value;
}
hasLoadedChildern() {
return !isEmpty(this.children);
}
loadedChildrenAmount() {
return size(this.children);
}
checkedChildrenAmount() {
return size(this.checkedChildren);
}
/**
* Set the value of the current node
* @param {(string|RenamableNode)} value - The new value of the node.
*/
set value(value) {
if (typeof value !== 'string' && !Tree.isRenamable(value)) {
return;
}
const stringifiedValue = '' + value;
if (Tree.isRenamable(this.value)) {
this.node.value = Tree.applyNewValueToRenamable(this.value, stringifiedValue);
}
else {
this.node.value = Tree.isValueEmpty(stringifiedValue) ? this.node.value : stringifiedValue;
}
}
/**
* Add a sibling node for the current node. This won't work if the current node is a root.
* @param {Tree} sibling - A node that should become a sibling.
* @param [number] position - Position in which sibling will be inserted. By default it will be inserted at the last position in a parent.
* @returns {Tree} A newly inserted sibling, or null if you are trying to make a sibling for the root.
*/
addSibling(sibling, position) {
if (Array.isArray(get(this.parent, 'children'))) {
return this.parent.addChild(sibling, position);
}
return null;
}
/**
* Add a child node for the current node.
* @param {Tree} child - A node that should become a child.
* @param [number] position - Position in which child will be inserted. By default it will be inserted at the last position in a parent.
* @returns {Tree} A newly inserted child.
*/
addChild(child, position) {
const newborn = this._addChild(Tree.cloneTreeShallow(child), position);
this._setFoldingType();
if (this.isNodeCollapsed()) {
this.switchFoldingType();
}
return newborn;
}
_addChild(child, position = size(this._children) || 0) {
child.parent = this;
if (Array.isArray(this._children)) {
this._children.splice(position, 0, child);
}
else {
this._children = [child];
}
return child;
}
/**
* Swap position of the current node with the given sibling. If node passed as a parameter is not a sibling - nothing happens.
* @param {Tree} sibling - A sibling with which current node shold be swapped.
*/
swapWithSibling(sibling) {
if (!this.hasSibling(sibling)) {
return;
}
const siblingIndex = sibling.positionInParent;
const thisTreeIndex = this.positionInParent;
this.parent._children[siblingIndex] = this;
this.parent._children[thisTreeIndex] = sibling;
}
/**
* Get a node's position in its parent.
* @returns {number} The position inside a parent.
*/
get positionInParent() {
if (this.isRoot()) {
return -1;
}
return this.parent.children ? this.parent.children.indexOf(this) : -1;
}
/**
* Check whether or not this tree is static.
* @returns {boolean} A flag indicating whether or not this tree is static.
*/
isStatic() {
return get(this.node.settings, 'static', false);
}
/**
* Check whether or not this tree has a left menu.
* @returns {boolean} A flag indicating whether or not this tree has a left menu.
*/
hasLeftMenu() {
return !get(this.node.settings, 'static', false) && get(this.node.settings, 'leftMenu', false);
}
/**
* Check whether or not this tree has a right menu.
* @returns {boolean} A flag indicating whether or not this tree has a right menu.
*/
hasRightMenu() {
return !get(this.node.settings, 'static', false) && get(this.node.settings, 'rightMenu', false);
}
/**
* Check whether this tree is "Leaf" or not.
* @returns {boolean} A flag indicating whether or not this tree is a "Leaf".
*/
isLeaf() {
return !this.isBranch();
}
/**
* Get menu items of the current tree.
* @returns {NodeMenuItem[]} The menu items of the current tree.
*/
get menuItems() {
return get(this.node.settings, 'menuItems');
}
/**
* Check whether or not this tree has a custom menu.
* @returns {boolean} A flag indicating whether or not this tree has a custom menu.
*/
hasCustomMenu() {
return !this.isStatic() && !!get(this.node.settings, 'menuItems', false);
}
/**
* Check whether this tree is "Branch" or not. "Branch" is a node that has children.
* @returns {boolean} A flag indicating whether or not this tree is a "Branch".
*/
isBranch() {
return this.node.emitLoadNextLevel === true || Array.isArray(this._children);
}
/**
* Check whether this tree has children.
* @returns {boolean} A flag indicating whether or not this tree has children.
*/
hasChildren() {
return !isEmpty(this._children) || this.childrenShouldBeLoaded();
}
/**
* Check whether this tree is a root or not. The root is the tree (node) that doesn't have parent (or technically its parent is null).
* @returns {boolean} A flag indicating whether or not this tree is the root.
*/
isRoot() {
return isNil(this.parent);
}
/**
* Check whether provided tree is a sibling of the current tree. Sibling trees (nodes) are the trees that have the same parent.
* @param {Tree} tree - A tree that should be tested on a siblingness.
* @returns {boolean} A flag indicating whether or not provided tree is the sibling of the current one.
*/
hasSibling(tree) {
return !this.isRoot() && includes(this.parent.children, tree);
}
/**
* Check whether provided tree is a child of the current tree.
* This method tests that provided tree is a <strong>direct</strong> child of the current tree.
* @param {Tree} tree - A tree that should be tested (child candidate).
* @returns {boolean} A flag indicating whether provided tree is a child or not.
*/
hasChild(tree) {
return includes(this._children, tree);
}
/**
* Remove given tree from the current tree.
* The given tree will be removed only in case it is a direct child of the current tree (@see {@link hasChild}).
* @param {Tree} tree - A tree that should be removed.
*/
removeChild(tree) {
if (!this.hasChildren()) {
return;
}
const childIndex = this._children.findIndex((child) => child === tree);
if (childIndex >= 0) {
this._children.splice(childIndex, 1);
}
this._setFoldingType();
}
/**
* Remove current tree from its parent.
*/
removeItselfFromParent() {
if (!this.parent) {
return;
}
this.parent.removeChild(this);
}
/**
* Switch folding type of the current tree. "Leaf" node cannot switch its folding type cause it doesn't have children, hence nothing to fold.
* If node is a "Branch" and it is expanded, then by invoking current method state of the tree should be switched to "collapsed" and vice versa.
*/
switchFoldingType() {
if (this.isLeaf() || !this.hasChildren()) {
return;
}
this.disableCollapseOnInit();
this.node._foldingType = this.isNodeExpanded() ? FoldingType.Collapsed : FoldingType.Expanded;
}
/**
* Check that tree is expanded.
* @returns {boolean} A flag indicating whether current tree is expanded. Always returns false for the "Leaf" tree and for an empty tree.
*/
isNodeExpanded() {
return this.foldingType === FoldingType.Expanded;
}
/**
* Check that tree is collapsed.
* @returns {boolean} A flag indicating whether current tree is collapsed. Always returns false for the "Leaf" tree and for an empty tree.
*/
isNodeCollapsed() {
return this.foldingType === FoldingType.Collapsed;
}
/**
* Set a current folding type: expanded, collapsed or leaf.
*/
_setFoldingType() {
if (this.childrenShouldBeLoaded()) {
this.node._foldingType = FoldingType.Collapsed;
}
else if (this._children && !isEmpty(this._children)) {
this.node._foldingType = this.isCollapsedOnInit() ? FoldingType.Collapsed : FoldingType.Expanded;
}
else if (Array.isArray(this._children)) {
this.node._foldingType = FoldingType.Empty;
}
else {
this.node._foldingType = FoldingType.Leaf;
}
}
/**
* Get a current folding type: expanded, collapsed or leaf.
* @returns {FoldingType} A folding type of the current tree.
*/
get foldingType() {
if (!this.node._foldingType) {
this._setFoldingType();
}
return this.node._foldingType;
}
/**
* Get a css class for element which displayes folding state - expanded, collapsed or leaf
* @returns {string} A string icontaining css class (classes)
*/
get foldingCssClass() {
return this.getCssClassesFromSettings() || this.foldingType.cssClass;
}
getCssClassesFromSettings() {
if (!this.node._foldingType) {
this._setFoldingType();
}
if (this.node._foldingType === FoldingType.Collapsed) {
return get(this.node.settings, 'cssClasses.collapsed', null);
}
else if (this.node._foldingType === FoldingType.Expanded) {
return get(this.node.settings, 'cssClasses.expanded', null);
}
else if (this.node._foldingType === FoldingType.Empty) {
return get(this.node.settings, 'cssClasses.empty', null);
}
return get(this.node.settings, 'cssClasses.leaf', null);
}
/**
* Get a html template to render before every node's name.
* @returns {string} A string representing a html template.
*/
get nodeTemplate() {
return this.getTemplateFromSettings();
}
getTemplateFromSettings() {
if (this.isLeaf()) {
return get(this.node.settings, 'templates.leaf', '');
}
else {
return get(this.node.settings, 'templates.node', '');
}
}
/**
* Get a html template to render for an element activatin left menu of a node.
* @returns {string} A string representing a html template.
*/
get leftMenuTemplate() {
if (this.hasLeftMenu()) {
return get(this.node.settings, 'templates.leftMenu', '<span></span>');
}
return '';
}
disableCollapseOnInit() {
if (this.node.settings) {
this.node.settings.isCollapsedOnInit = false;
}
}
isCollapsedOnInit() {
return !!get(this.node.settings, 'isCollapsedOnInit');
}
keepNodesInDOM() {
return get(this.node.settings, 'keepNodesInDOM');
}
/**
* Check that current tree is newly created (added by user via menu for example). Tree that was built from the TreeModel is not marked as new.
* @returns {boolean} A flag whether the tree is new.
*/
isNew() {
return this.node._status === TreeStatus.New;
}
get id() {
return get(this.node, 'id');
}
set id(id) {
this.node.id = id;
}
/**
* Mark current tree as new (@see {@link isNew}).
*/
markAsNew() {
this.node._status = TreeStatus.New;
}
/**
* Check that current tree is being renamed (it is in the process of its value renaming initiated by a user).
* @returns {boolean} A flag whether the tree is being renamed.
*/
isBeingRenamed() {
return this.node._status === TreeStatus.IsBeingRenamed;
}
/**
* Mark current tree as being renamed (@see {@link isBeingRenamed}).
*/
markAsBeingRenamed() {
this.node._status = TreeStatus.IsBeingRenamed;
}
/**
* Check that current tree is modified (for example it was renamed).
* @returns {boolean} A flag whether the tree is modified.
*/
isModified() {
return this.node._status === TreeStatus.Modified;
}
/**
* Mark current tree as modified (@see {@link isModified}).
*/
markAsModified() {
this.node._status = TreeStatus.Modified;
}
/**
* Makes a clone of an underlying TreeModel instance
* @returns {TreeModel} a clone of an underlying TreeModel instance
*/
toTreeModel() {
const model = defaultsDeep(this.isLeaf() ? {} : { children: [] }, this.node);
if (this.children) {
this.children.forEach(child => {
model.children.push(child.toTreeModel());
});
}
return model;
}
}
var NodeMenuItemAction;
(function (NodeMenuItemAction) {
NodeMenuItemAction[NodeMenuItemAction["NewFolder"] = 0] = "NewFolder";
NodeMenuItemAction[NodeMenuItemAction["NewTag"] = 1] = "NewTag";
NodeMenuItemAction[NodeMenuItemAction["Rename"] = 2] = "Rename";
NodeMenuItemAction[NodeMenuItemAction["Remove"] = 3] = "Remove";
NodeMenuItemAction[NodeMenuItemAction["Custom"] = 4] = "Custom";
})(NodeMenuItemAction || (NodeMenuItemAction = {}));
var NodeMenuAction;
(function (NodeMenuAction) {
NodeMenuAction[NodeMenuAction["Close"] = 0] = "Close";
})(NodeMenuAction || (NodeMenuAction = {}));
var Keys;
(function (Keys) {
Keys[Keys["Escape"] = 27] = "Escape";
})(Keys || (Keys = {}));
var MouseButtons;
(function (MouseButtons) {
MouseButtons[MouseButtons["Left"] = 0] = "Left";
MouseButtons[MouseButtons["Right"] = 2] = "Right";
})(MouseButtons || (MouseButtons = {}));
function isLeftButtonClicked(e) {
return e.button === MouseButtons.Left;
}
function isRightButtonClicked(e) {
return e.button === MouseButtons.Right;
}
function isEscapePressed(e) {
return e.keyCode === Keys.Escape;
}
class TreeController {
component;
tree;
treeService;
constructor(component) {
this.component = component;
this.tree = this.component.tree;
this.treeService = this.component.treeService;
}
select() {
if (!this.isSelected()) {
this.component.onNodeSelected({ button: MouseButtons.Left });
}
}
unselect() {
if (this.isSelected()) {
this.component.onNodeUnselected({ button: MouseButtons.Left });
}
}
isSelected() {
return this.component.isSelected;
}
expand() {
if (this.isCollapsed()) {
this.component.onSwitchFoldingType();
}
}
expandToParent(tree = this.tree) {
if (tree) {
const controller = this.treeService.getController(tree.id);
if (controller) {
requestAnimationFrame(() => {
controller.expand();
this.expandToParent(tree.parent);
});
}
}
}
isExpanded() {
return this.tree.isNodeExpanded();
}
collapse() {
if (this.isExpanded()) {
this.component.onSwitchFoldingType();
}
}
isCollapsed() {
return this.tree.isNodeCollapsed();
}
toTreeModel() {
return this.tree.toTreeModel();
}
rename(newValue) {
this.tree.markAsBeingRenamed();
this.component.applyNewValue({ type: 'keyup', value: newValue });
}
remove() {
this.component.onMenuItemSelected({ nodeMenuItemAction: NodeMenuItemAction.Remove });
}
addChild(newNode) {
if (this.tree.hasDeferredChildren() && !this.tree.childrenWereLoaded()) {
return;
}
const newTree = this.tree.createNode(Array.isArray(newNode.children), newNode);
this.treeService.fireNodeCreated(newTree);
}
addChildAsync(newNode) {
if (this.tree.hasDeferredChildren() && !this.tree.childrenWereLoaded()) {
return Promise.reject(new Error('This node loads its children asynchronously, hence child cannot be added this way'));
}
const newTree = this.tree.createNode(Array.isArray(newNode.children), newNode);
this.treeService.fireNodeCreated(newTree);
// This will give TreeInternalComponent to set up a controller for the node
return new Promise(resolve => {
setTimeout(() => {
resolve(newTree);
});
});
}
changeNodeId(id) {
if (!id) {
throw Error('You should supply an id!');
}
if (this.treeService.hasController(id)) {
throw Error(`Controller already exists for the given id: ${id}`);
}
this.treeService.deleteController(this.tree.id);
this.tree.id = id;
this.treeService.setController(this.tree.id, this);
}
reloadChildren() {
this.tree.reloadChildren();
}
setChildren(children) {
if (!this.tree.isLeaf()) {
this.tree.setChildren(children);
}
}
startRenaming() {
this.tree.markAsBeingRenamed();
}
check() {
this.component.onNodeChecked();
}
uncheck() {
this.component.onNodeUnchecked();
}
isChecked() {
return this.tree.checked;
}
isIndetermined() {
return get(this.component, 'checkboxElementRef.nativeElement.indeterminate');
}
allowSelection() {
this.tree.selectionAllowed = true;
}
forbidSelection() {
this.tree.selectionAllowed = false;
}
isSelectionAllowed() {
return this.tree.selectionAllowed;
}
}
var NodeEditableEventAction;
(function (NodeEditableEventAction) {
NodeEditableEventAction[NodeEditableEventAction["Cancel"] = 0] = "Cancel";
})(NodeEditableEventAction || (NodeEditableEventAction = {}));
class NodeMenuService {
nodeMenuEvents$ = new Subject();
fireMenuEvent(sender, action) {
const nodeMenuEvent = { sender, action };
this.nodeMenuEvents$.next(nodeMenuEvent);
}
hideMenuStream(treeElementRef) {
return this.nodeMenuEvents$.pipe(filter((e) => treeElementRef.nativeElement !== e.sender), filter((e) => e.action === NodeMenuAction.Close));
}
hideMenuForAllNodesExcept(treeElementRef) {
this.nodeMenuEvents$.next({
sender: treeElementRef.nativeElement,
action: NodeMenuAction.Close
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeMenuService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeMenuService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeMenuService, decorators: [{
type: Injectable
}] });
class CapturedNode {
anElement;
aTree;
constructor(anElement, aTree) {
this.anElement = anElement;
this.aTree = aTree;
}
canBeDroppedAt(element) {
return !this.sameAs(element) && !this.contains(element);
}
contains(other) {
return this.element.nativeElement.contains(other.nativeElement);
}
sameAs(other) {
return this.element === other;
}
get element() {
return this.anElement;
}
get tree() {
return this.aTree;
}
}
class NodeDraggableDirective {
element;
nodeDraggableService;
renderer;
static DATA_TRANSFER_STUB_DATA = 'some browsers enable drag-n-drop only when dataTransfer has data';
nodeDraggable;
tree;
nodeNativeElement;
disposersForDragListeners = [];
constructor(element, nodeDraggableService, renderer) {
this.element = element;
this.nodeDraggableService = nodeDraggableService;
this.renderer = renderer;
this.nodeNativeElement = element.nativeElement;
}
ngOnInit() {
if (!this.tree.isStatic()) {
this.renderer.setAttribute(this.nodeNativeElement, 'draggable', 'true');
this.disposersForDragListeners.push(this.renderer.listen(this.nodeNativeElement, 'dragenter', this.handleDragEnter.bind(this)));
this.disposersForDragListeners.push(this.renderer.listen(this.nodeNativeElement, 'dragover', this.handleDragOver.bind(this)));
this.disposersForDragListeners.push(this.renderer.listen(this.nodeNativeElement, 'dragstart', this.handleDragStart.bind(this)));
this.disposersForDragListeners.push(this.renderer.listen(this.nodeNativeElement, 'dragleave', this.handleDragLeave.bind(this)));
this.disposersForDragListeners.push(this.renderer.listen(this.nodeNativeElement, 'drop', this.handleDrop.bind(this)));
this.disposersForDragListeners.push(this.renderer.listen(this.nodeNativeElement, 'dragend', this.handleDragEnd.bind(this)));
}
}
ngOnDestroy() {
/* tslint:disable:typedef */
this.disposersForDragListeners.forEach(dispose => dispose());
/* tslint:enable:typedef */
}
handleDragStart(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
this.nodeDraggableService.captureNode(new CapturedNode(this.nodeDraggable, this.tree));
e.dataTransfer.setData('text', NodeDraggableDirective.DATA_TRANSFER_STUB_DATA);
e.dataTransfer.effectAllowed = 'move';
}
handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}
handleDragEnter(e) {
e.preventDefault();
if (this.containsElementAt(e)) {
this.addClass('over-drop-target');
}
}
handleDragLeave(e) {
if (!this.containsElementAt(e)) {
this.removeClass('over-drop-target');
}
}
handleDrop(e) {
e.preventDefault();
if (e.stopPropagation) {
e.stopPropagation();
}
this.removeClass('over-drop-target');
if (!this.isDropPossible(e)) {
return false;
}
if (this.nodeDraggableService.getCapturedNode()) {
return this.notifyThatNodeWasDropped();
}
}
isDropPossible(e) {
const capturedNode = this.nodeDraggableService.getCapturedNode();
return capturedNode && capturedNode.canBeDroppedAt(this.nodeDraggable) && this.containsElementAt(e);
}
handleDragEnd(e) {
this.removeClass('over-drop-target');
this.nodeDraggableService.releaseCapturedNode();
}
containsElementAt(e) {
const { x = e.clientX, y = e.clientY } = e;
return this.nodeNativeElement.contains(document.elementFromPoint(x, y));
}
addClass(className) {
const classList = this.nodeNativeElement.classList;
classList.add(className);
}
removeClass(className) {
const classList = this.nodeNativeElement.classList;
classList.remove(className);
}
notifyThatNodeWasDropped() {
this.nodeDraggableService.fireNodeDragged(this.nodeDraggableService.getCapturedNode(), this.nodeDraggable);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeDraggableDirective, deps: [{ token: ElementRef }, { token: NodeDraggableService }, { token: Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: NodeDraggableDirective, selector: "[nodeDraggable]", inputs: { nodeDraggable: "nodeDraggable", tree: "tree" }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeDraggableDirective, decorators: [{
type: Directive,
args: [{
selector: '[nodeDraggable]'
}]
}], ctorParameters: function () { return [{ type: i0.ElementRef, decorators: [{
type: Inject,
args: [ElementRef]
}] }, { type: NodeDraggableService, decorators: [{
type: Inject,
args: [NodeDraggableService]
}] }, { type: i0.Renderer2, decorators: [{
type: Inject,
args: [Renderer2]
}] }]; }, propDecorators: { nodeDraggable: [{
type: Input
}], tree: [{
type: Input
}] } });
class NodeEditableDirective {
renderer;
elementRef;
/* tslint:disable:no-input-rename */
nodeValue;
/* tslint:enable:no-input-rename */
valueChanged = new EventEmitter(false);
constructor(renderer, elementRef) {
this.renderer = renderer;
this.elementRef = elementRef;
}
ngOnInit() {
const nativeElement = this.elementRef.nativeElement;
if (nativeElement) {
nativeElement.focus();
}
this.renderer.setProperty(nativeElement, 'value', this.nodeValue);
}
applyNewValue(newNodeValue) {
this.valueChanged.emit({ type: 'keyup', value: newNodeValue });
}
applyNewValueByLoosingFocus(newNodeValue) {
this.valueChanged.emit({ type: 'blur', value: newNodeValue });
}
cancelEditing() {
this.valueChanged.emit({
type: 'keyup',
value: this.nodeValue,
action: NodeEditableEventAction.Cancel
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeEditableDirective, deps: [{ token: Renderer2 }, { token: ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: NodeEditableDirective, selector: "[nodeEditable]", inputs: { nodeValue: ["nodeEditable", "nodeValue"] }, outputs: { valueChanged: "valueChanged" }, host: { listeners: { "keyup.enter": "applyNewValue($event.target.value)", "blur": "applyNewValueByLoosingFocus($event.target.value)", "keyup.esc": "cancelEditing()" } }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NodeEditableDirective, decorators: [{
type: Directive,
args: [{
selector: '[nodeEditable]'
}]
}], ctorParameters: function () { return [{ type: i0.Renderer2, decorators: [{
type: Inject,
args: [Renderer2]
}] }, { type: i0.ElementRef, decorators: [{
type: Inject,
args: [ElementRef]
}] }]; }, propDecorators: { nodeValue: [{
type: Input,
args: ['nodeEditable']
}], valueChanged: [{
type: Output
}], applyNewValue: [{
type: HostListener,
args: ['keyup.enter', ['$event.target.value']]
}], applyNewValueByLoosingFocus: [{
type: HostListener,
args: ['blur', ['$event.target.value']]
}], cancelEditing: [{
type: HostListener,
args: ['keyup.esc']
}] } });
class NodeMenuComponent {
renderer;
nodeMenuService;
menuItemSelected = new EventEmitter();
menuItems;
menuContainer;
availableMenuItems = [
{
name: 'New tag',
action: NodeMenuItemAction.NewTag,