UNPKG

ng2-tree-pms

Version:

angular2 component for visualizing data that can be naturally represented as a tree

390 lines (340 loc) 13.9 kB
import * as _ from 'lodash'; import { Observable, Observer } from 'rxjs'; import { TreeModel, RenamableNode, FoldingType, TreeStatus, TreeModelSettings, ChildrenLoadingFunction } from './tree.types'; enum ChildrenLoadingState { NotStarted, Loading, Completed } export class Tree { private _children: Tree[]; private _loadChildren: ChildrenLoadingFunction; private _childrenLoadingState: ChildrenLoadingState = ChildrenLoadingState.NotStarted; public node: TreeModel; public parent: Tree; /** * 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. */ public constructor(node: TreeModel, parent: Tree = null, isBranch: boolean = false) { this.buildTreeFromModel(node, parent, isBranch); } private buildTreeFromModel(model: TreeModel, parent: Tree, isBranch: boolean): void { this.parent = parent; this.node = _.extend(_.omit(model, 'children') as TreeModel, { settings: TreeModelSettings.merge(model, _.get(parent, 'node') as TreeModel) }) as TreeModel; if (_.isFunction(this.node.loadChildren)) { this._loadChildren = this.node.loadChildren; } else { _.forEach(_.get(model, 'children') as TreeModel[], (child: TreeModel, index: number) => { this._addChild(new Tree(child, this), index); }); } if (!Array.isArray(this._children)) { this._children = this.node.loadChildren || isBranch ? [] : null; } } /** * 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. */ public childrenAreBeingLoaded(): boolean { return (this._childrenLoadingState === ChildrenLoadingState.Loading); } private canLoadChildren(): boolean { 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. */ public childrenShouldBeLoaded(): boolean { return !!this._loadChildren; } /** * Get children of the current tree. * @returns {Tree[]} The children of the current tree. */ public get children(): Tree[] { 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. */ public get childrenAsync(): Observable<Tree[]> { if(this.canLoadChildren()) { setTimeout(() => this._childrenLoadingState = ChildrenLoadingState.Loading); return new Observable((observer: Observer<Tree[]>) => { this._loadChildren((children: TreeModel[]) => { this._children = _.map(children, (child: TreeModel) => new Tree(child, this)); this._childrenLoadingState = ChildrenLoadingState.Completed; observer.next(this.children); observer.complete(); }); }); } return Observable.of(this.children); } /** * 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 * @returns {Tree} A newly created child node. */ public createNode(isBranch: boolean): Tree { const tree = new Tree({ value: '',typeModel: '' }, null, isBranch); tree.markAsNew(); 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. */ public get value(): any { return this.node.value; } /** * Set the value of the current node * @param {(string|RenamableNode)} value - The new value of the node. */ public set value(value: any) { if (typeof value !== 'string' && !Tree.isRenamable(value)) { return; } if (Tree.isRenamable(this.value)) { const newValue = typeof value === 'string' ? value : _.toString(value); this.node.value = Tree.applyNewValueToRenamable(this.value as RenamableNode, newValue); } else { this.node.value = Tree.isValueEmpty(value as string) ? this.node.value : _.toString(value); } } /** * 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. */ public addSibling(sibling: Tree, position?: number): Tree { if (_.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. */ public addChild(child: Tree, position?: number): Tree { return this._addChild(Tree.cloneTreeShallow(child), position); } private _addChild(child: Tree, position: number = _.size(this._children) || 0): Tree { 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. */ public swapWithSibling(sibling: Tree): void { 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. */ public get positionInParent(): number { return _.indexOf(this.parent.children, this); } /** * Check whether or not this tree is static. * @returns {boolean} A flag indicating whether or not this tree is static. */ public isStatic(): boolean { return _.get(this.node.settings, 'static', false); } /** * Check whether this tree is "Leaf" or not. * @returns {boolean} A flag indicating whether or not this tree is a "Leaf". */ public isLeaf(): boolean { return !this.isBranch(); } /** * 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". */ public isBranch(): boolean { return Array.isArray(this._children); } /** * 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. */ public isRoot(): boolean { return this.parent === null; } /** * 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. */ public hasSibling(tree: Tree): boolean { 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. */ public hasChild(tree: Tree): boolean { 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. */ public removeChild(tree: Tree): void { const childIndex = _.findIndex(this._children, (child: Tree) => child === tree); if (childIndex >= 0) { this._children.splice(childIndex, 1); } } /** * Remove current tree from its parent. */ public removeItselfFromParent(): void { 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. */ public switchFoldingType(): void { if (this.isLeaf()) { return; } 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. */ public isNodeExpanded(): boolean { return this.foldingType === FoldingType.Expanded; } /** * Get a current folding type: expanded, collapsed or leaf. * @returns {FoldingType} A folding type of the current tree. */ public get foldingType(): FoldingType { if (!this.node._foldingType) { if (this.childrenShouldBeLoaded()) { this.node._foldingType = FoldingType.Collapsed; } else if (this._children) { this.node._foldingType = FoldingType.Expanded; } else { this.node._foldingType = FoldingType.Leaf; } } return this.node._foldingType; } /** * 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. */ public isNew(): boolean { return this.node._status === TreeStatus.New; } /** * Mark current tree as new (@see {@link isNew}). */ public markAsNew(): void { 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. */ public isBeingRenamed(): boolean { return this.node._status === TreeStatus.IsBeingRenamed; } /** * Mark current tree as being renamed (@see {@link isBeingRenamed}). */ public markAsBeingRenamed(): void { 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. */ public isModified(): boolean { return this.node._status === TreeStatus.Modified; } /** * Mark current tree as modified (@see {@link isModified}). */ public markAsModified(): void { this.node._status = TreeStatus.Modified; } // 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 */ public static isValueEmpty(value: string): boolean { return _.isEmpty(_.trim(value)); } /** * Check whether a given value can be considered RenamableNode. * @param {any} value - A value to check. * @returns {boolan} - A flag indicating whether given value is Renamable node or not. * @static */ public static isRenamable(value: any): value is RenamableNode { return (_.has(value, 'setName') && _.isFunction(value.setName)) && (_.has(value, 'toString') && _.isFunction(value.toString) && value.toString !== Object.toString); } private static cloneTreeShallow(origin: Tree): Tree { const tree = new Tree(_.clone(origin.node)); tree._children = origin._children; return tree; }; private static applyNewValueToRenamable(value: RenamableNode, newValue: string): RenamableNode { const renamableValue: RenamableNode = _.merge({}, value as RenamableNode); renamableValue.setName(newValue); return renamableValue; } }