import { defaultsDeep , get , has , includes , isEmpty , isFunction , isNil , omit , once , size , trim } from './utils/fn.utils' ; import { Observable } from 'rxjs/Observable' ; import { Observer } from 'rxjs/Observer' ; import { ChildrenLoadingFunction , FoldingType , RenamableNode , TreeModel , TreeModelSettings , TreeStatus } from './tree.types' ; import { NodeMenuItem } from './menu/node-menu.component' ; import * as uuidv4 from 'uuid/v4' ; enum ChildrenLoadingState { NotStarted , Loading , Completed } export class Tree { private _children : Tree [ ] ; private _loadChildren : ChildrenLoadingFunction ; private _childrenLoadingState : ChildrenLoadingState = ChildrenLoadingState . NotStarted ; private _childrenAsyncOnce : ( ) => Observable < Tree [ ] > = 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 ( ) ; } ) ; } ) ; } ) ; } ) ; public node : TreeModel ; public parent : Tree ; param { string } value returns { boolean } static public static isValueEmpty ( value ) { return isEmpty ( trim ( value ) ) ; } param { any } value returns { boolean } static public static isRenamable ( value ) { return ( has ( value , 'setName' ) && isFunction ( value . setName ) && ( has ( value , 'toString' ) && isFunction ( value . toString ) && value . toString !== Object . toString ) ) ; } private static cloneTreeShallow ( origin ) { const tree = new Tree ( Object . assign ( { } , origin . node ) ) ; tree . _children = origin . _children ; return tree ; } private static applyNewValueToRenamable ( value , newValue ) { const renamableValue = Object . assign ( { } , value as RenamableNode ) ; renamableValue . setName ( newValue ) ; return renamableValue ; } param { TreeModel } model param { Tree } parent param { boolean } isBranch public constructor ( node , parent = null , isBranch = false ) { this . buildTreeFromModel ( node , parent , isBranch || Array . isArray ( node . children ) ) ; } private buildTreeFromModel ( model , parent , isBranch ) { this . parent = parent ; this . node = Object . assign ( omit ( model , 'children' ) as TreeModel , { settings : TreeModelSettings . merge ( model , get ( parent , 'node' ) ) } , { emitLoadNextLevel : model . emitLoadNextLevel === true } ) as TreeModel ; 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 ; } } public hasDeferredChildren ( ) { return typeof this . _loadChildren === 'function' ; } public loadingChildrenRequested ( ) { this . _childrenLoadingState = ChildrenLoadingState . Loading ; } returns { boolean } public childrenAreBeingLoaded ( ) { return this . _childrenLoadingState === ChildrenLoadingState . Loading ; } returns { boolean } public childrenWereLoaded ( ) { return this . _childrenLoadingState === ChildrenLoadingState . Completed ; } private canLoadChildren ( ) { return ( this . _childrenLoadingState === ChildrenLoadingState . NotStarted && this . foldingType === FoldingType . Expanded && ! ! this . _loadChildren ) ; } returns { boolean } public childrenShouldBeLoaded ( ) { return ! this . childrenWereLoaded ( ) && ( ! ! this . _loadChildren || this . node . emitLoadNextLevel === true ) ; } returns { Tree [ ] } public get children ( ) : Tree [ ] { return this . _children ; } returns { Observable < Tree [ ] > } public get childrenAsync ( ) : Observable < Tree [ ] > { if ( this . canLoadChildren ( ) ) { return this . _childrenAsyncOnce ( ) ; } return Observable . of ( this . children ) ; } /**
     * By calling this method you start process of loading node's children using `loadChildren` function.
     */ public 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.
     */ public setChildren ( children ) { this . _children = children && children . map ( ( child ) => new Tree ( child , this ) ) ; if ( this . childrenShouldBeLoaded ( ) ) { this . _childrenLoadingState = ChildrenLoadingState . Completed ; } } param { boolean } isBranch param { TreeModel } model returns { Tree } public 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 ) ; } } returns { ( string | RenamableNode ) } public get value ( ) : any { return this . node . value ; } public set checked ( checked ) { this . node . settings = Object . assign ( { } , this . node . settings , { checked } ) ; } public get checked ( ) : boolean { return ! ! get ( this . node . settings , 'checked' ) ; } public get checkedChildren ( ) : Tree [ ] { return this . hasLoadedChildern ( ) ? this . children . filter ( child => child . checked ) : [ ] ; } public set selectionAllowed ( selectionAllowed ) { this . node . settings = Object . assign ( { } , this . node . settings , { selectionAllowed } ) ; } public get selectionAllowed ( ) : boolean { 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 ) ; } param { ( string | RenamableNode ) } value public 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 as RenamableNode , stringifiedValue ) ; } else { this . node . value = Tree . isValueEmpty ( stringifiedValue ) ? this . node . value : stringifiedValue ; } } param { Tree } sibling param number returns { Tree } public addSibling ( sibling , position ? ) { if ( Array . isArray ( get ( this . parent , 'children' ) ) ) { return this . parent . addChild ( sibling , position ) ; } return null ; } param { Tree } child param number returns { Tree } public addChild ( child , position ? ) { const newborn = this . _addChild ( Tree . cloneTreeShallow ( child ) , position ) ; this . _setFoldingType ( ) ; if ( this . isNodeCollapsed ( ) ) { this . switchFoldingType ( ) ; } return newborn ; } private _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 ; } param { Tree } sibling public 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 ; } returns { number } public get positionInParent ( ) : number { if ( this . isRoot ( ) ) { return - 1 ; } return this . parent . children ? this . parent . children . indexOf ( this ) : - 1 ; } returns { boolean } public isStatic ( ) { return get ( this . node . settings , 'static' , false ) ; } returns { boolean } public hasLeftMenu ( ) { return ! get ( this . node . settings , 'static' , false ) && get ( this . node . settings , 'leftMenu' , false ) ; } returns { boolean } public hasRightMenu ( ) { return ! get ( this . node . settings , 'static' , false ) && get ( this . node . settings , 'rightMenu' , false ) ; } returns { boolean } public isLeaf ( ) { return ! this . isBranch ( ) ; } returns { NodeMenuItem [ ] } public get menuItems ( ) : NodeMenuItem [ ] { return get ( this . node . settings , 'menuItems' ) ; } returns { boolean } public hasCustomMenu ( ) { return ! this . isStatic ( ) && ! ! get ( this . node . settings , 'menuItems' , false ) ; } returns { boolean } public isBranch ( ) { return this . node . emitLoadNextLevel === true || Array . isArray ( this . _children ) ; } returns { boolean } public hasChildren ( ) { return ! isEmpty ( this . _children ) || this . childrenShouldBeLoaded ( ) ; } returns { boolean } public isRoot ( ) { return isNil ( this . parent ) ; } param { Tree } tree returns { boolean } public hasSibling ( tree ) { return ! this . isRoot ( ) && includes ( this . parent . children , tree ) ; } param { Tree } tree returns { boolean } public 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  hasChild ).
     *  param { Tree } tree public 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.
     */ public 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.
     */ public switchFoldingType ( ) { if ( this . isLeaf ( ) || ! this . hasChildren ( ) ) { return ; } this . disableCollapseOnInit ( ) ; this . node . _foldingType = this . isNodeExpanded ( ) ? FoldingType . Collapsed : FoldingType . Expanded ; } returns { boolean } public isNodeExpanded ( ) { return this . foldingType === FoldingType . Expanded ; } returns { boolean } public isNodeCollapsed ( ) { return this . foldingType === FoldingType . Collapsed ; } /**
     * Set a current folding type: expanded, collapsed or leaf.
     */ private _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 ; } } returns { FoldingType } public get foldingType ( ) : FoldingType { if ( ! this . node . _foldingType ) { this . _setFoldingType ( ) ; } return this . node . _foldingType ; } returns { string } public get foldingCssClass ( ) : string { return this . getCssClassesFromSettings ( ) || this . foldingType . cssClass ; } private 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 ) ; } returns { string } public get nodeTemplate ( ) : string { return this . getTemplateFromSettings ( ) ; } private getTemplateFromSettings ( ) { if ( this . isLeaf ( ) ) { return get ( this . node . settings , 'templates.leaf' , '' ) ; } else { return get ( this . node . settings , 'templates.node' , '' ) ; } } returns { string } public get leftMenuTemplate ( ) : string { if ( this . hasLeftMenu ( ) ) { return get ( this . node . settings , 'templates.leftMenu' , '<span></span>' ) ; } return '' ; } public disableCollapseOnInit ( ) { if ( this . node . settings ) { this . node . settings . isCollapsedOnInit = false ; } } public isCollapsedOnInit ( ) { return ! ! get ( this . node . settings , 'isCollapsedOnInit' ) ; } returns { boolean } public isNew ( ) { return this . node . _status === TreeStatus . New ; } public get id ( ) : number | string { return get ( this . node , 'id' ) ; } public set id ( id ) { this . node . id = id ; } /**
     * Mark current tree as new (@see  isNew ).
      public markAsNew ( ) { this . node . _status = TreeStatus . New ; } returns { boolean } public isBeingRenamed ( ) { return this . node . _status === TreeStatus . IsBeingRenamed ; } /**
     * Mark current tree as being renamed (@see  isBeingRenamed ).
      public markAsBeingRenamed ( ) { this . node . _status = TreeStatus . IsBeingRenamed ; } returns { boolean } public isModified ( ) { return this . node . _status === TreeStatus . Modified ; } /**
     * Mark current tree as modified (@see  isModified ).
      public markAsModified ( ) { this . node . _status = TreeStatus . Modified ; } returns { TreeModel } public toTreeModel ( ) { const model = defaultsDeep ( this . isLeaf ( ) ? { } : { children : [ ] } , this . node ) ; if ( this . children ) { this . children . forEach ( child => { model . children . push ( child . toTreeModel ( ) ) ; } ) ; } return model ; } }