import { Observable } from 'rxjs/Observable'; import { omit, isFunction, isNull, size, isEmpty } from './fn.utils'; import { FoldingType } from './mat-tree.types'; export type ChildrenLoadingFunction = (callback: (children: MatTreeModel[]) => void) => void; export class TreeModelSettings { /* cssClasses - set custom css classes which will be used for a tree */ // public cssClasses?: CssClasses; /* Templates - set custom html templates to be used in a tree */ public static readonly NOT_CASCADING_SETTINGS = ['selectionAllowed']; public static merge(child: MatTreeModel, parent: MatTreeModel): TreeModelSettings { const parentCascadingSettings = omit(get(parent, 'settings'), TreeModelSettings.NOT_CASCADING_SETTINGS); return { ...get(child, 'settings'), ...parentCascadingSettings, ...{ static: false, leftMenu: false, rightMenu: true, isCollapsedOnInit: false, // checked: false, selectionAllowed: true } }; } public templates?: any; /** * "leftMenu" property when set to true makes left menu available. * @name TreeModelSettings#leftMenu * @type boolean * @default false */ public leftMenu?: boolean; /** * "rightMenu" property when set to true makes right menu available. * @name TreeModelSettings#rightMenu * @type boolean * @default true */ public rightMenu?: boolean; /** * "menu" property when set will be available as custom context menu. * @name TreeModelSettings#MenuItems * @type NodeMenuItem */ // public menuItems?: NodeMenuItem[]; /** * "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 */ public static?: boolean; public isCollapsedOnInit?: boolean; public checked?: boolean; public selectionAllowed?: boolean; } export interface MatTreeModel { value: string; id?: string | number; children?: MatTreeModel[]; loadChildren?: ChildrenLoadingFunction; settings?: TreeModelSettings; emitLoadNextLevel?: boolean; // _status?: TreeStatus; _foldingType?: FoldingType; [additionalData: string]: any; } export function get(value: any, path: string, defaultValue?: any) { let result = value; for (const prop of path.split('.')) { if (!result || !Reflect.has(result, prop)) { return defaultValue; } result = result[prop]; } return isNull(result) || result === value ? defaultValue : result; } enum ChildrenLoadingState { NotStarted, Loading, Completed } export class MatTree { node: MatTreeModel; parent: MatTree; private _children: MatTree[]; private _loadChildren: ChildrenLoadingFunction; private _childrenLoadingState: ChildrenLoadingState = ChildrenLoadingState.NotStarted; public constructor(node: MatTreeModel, parent: MatTree = null, isBranch: boolean = false) { this.buildTreeFromModel(node, parent, isBranch || Array.isArray(node.children)); } private buildTreeFromModel(model: MatTreeModel, parent: MatTree, isBranch: boolean): void { this.parent = parent; this.node = Object.assign( omit(model, 'children') as MatTreeModel, { settings: TreeModelSettings.merge(model, get(parent, 'node')) }, // { emitLoadNextLevel: model.emitLoadNextLevel === true } ) as MatTreeModel; if (isFunction(this.node.loadChildren)) { this._loadChildren = this.node.loadChildren; } else { get(model, 'children', []).forEach((child: MatTreeModel, index: number) => { this._addChild(new MatTree(child, this), index); }); } if (!Array.isArray(this._children)) { this._children = this.node.loadChildren || isBranch ? [] : null; } } private _addChild(child: MatTree, position: number = size(this._children) || 0): MatTree { child.parent = this; if (Array.isArray(this._children)) { this._children.splice(position, 0, child); } else { this._children = [child]; } return child; } get foldingType() { return this.node._foldingType; } public isNodeExpanded(): boolean { return this.foldingType === FoldingType.Expanded; } public get childrenAsync(): Observable { // if (this.canLoadChildren()) { // return this._childrenAsyncOnce(); // } return Observable.of(this.children); } public set checked(checked: boolean) { this.node.settings = {...this.node.settings, checked}; } public get checked(): boolean { return !!get(this.node.settings, 'checked'); } public get value(): any { return this.node.value; } public get children(): MatTree[] { return this._children; } public get id(): number | string { return get(this.node, 'id'); } public set id(id: number | string) { this.node.id = id; } public hasChildren(): boolean { return !isEmpty(this._children) || this.childrenShouldBeLoaded(); } /** * 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 this.node.emitLoadNextLevel === true || Array.isArray(this._children); } public switchFoldingType(): void { if (this.isLeaf() || !this.hasChildren()) { return; } this.disableCollapseOnInit(); this.node._foldingType = this.isNodeExpanded() ? FoldingType.Collapsed : FoldingType.Expanded; } hasLoadedChildern() { return !isEmpty(this.children); } public disableCollapseOnInit() { if (this.node.settings) { this.node.settings.isCollapsedOnInit = false; } } public get foldingCssClass(): string { return this.getCssClassesFromSettings() || this.foldingType.cssClass; } public get checkedChildren(): MatTree[] { return this.hasLoadedChildern() ? this.children.filter(child => child.checked) : []; } public childrenShouldBeLoaded(): boolean { return !this.childrenWereLoaded() && (!!this._loadChildren || this.node.emitLoadNextLevel === true); } public childrenWereLoaded(): boolean { return this._childrenLoadingState === ChildrenLoadingState.Completed; } public isCollapsedOnInit() { return !!get(this.node.settings, 'isCollapsedOnInit'); } checkedChildrenAmount() { return size(this.checkedChildren); } loadedChildrenAmount() { return size(this.children); } // tree是否是子节点 public hasChild(tree: MatTree): boolean { if (isNull(this._children)) return false; return this._children.includes(tree); } /** * 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; } /* Setting the children loading state to Loading since a request was dispatched to the client */ public loadingChildrenRequested(): void { this._childrenLoadingState = ChildrenLoadingState.Loading; } /** * 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. */ public isNodeCollapsed(): boolean { return this.foldingType === FoldingType.Collapsed; } /** * Get a html template to render before every node's name. * @returns {string} A string representing a html template. */ public get nodeTemplate(): string { return this.getTemplateFromSettings(); } /** * Set a current folding type: expanded, collapsed or leaf. */ private _setFoldingType(): void { 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; } } private getTemplateFromSettings(): string { // if (this.isLeaf()) { // return get(this.node.settings, 'templates.leaf', ''); // } else { // return get(this.node.settings, 'templates.node', ''); // } return get(this.node.settings, 'templates', ''); } private getCssClassesFromSettings(): string { 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); } }