license import { FocusableOption } from '@angular/cdk/a11y' ; import { CollectionViewer , DataSource } from '@angular/cdk/collections' ; import { AfterContentChecked , ChangeDetectionStrategy , ChangeDetectorRef , Component , ContentChildren , Directive , ElementRef , Input , IterableChangeRecord , IterableDiffer , IterableDiffers , OnDestroy , OnInit , QueryList , ViewChild , ViewContainerRef , ViewEncapsulation , TrackByFunction } from '@angular/core' ; import { BehaviorSubject , Observable , of as observableOf , Subject , Subscription } from 'rxjs' ; import { takeUntil } from 'rxjs/operators' ; import { TreeControl } from './control/tree-control' ; import { CdkTreeNodeDef , CdkTreeNodeOutletContext } from './node' ; import { CdkTreeNodeOutlet } from './outlet' ; import { getTreeControlFunctionsMissingError , getTreeControlMissingError , getTreeMissingMatchingNodeDefError , getTreeMultipleDefaultNodeDefsError , getTreeNoValidDataSourceError } from './tree-errors' ; /**
 * CDK tree component that connects with a data source to retrieve data of type `T` and renders
 * dataNodes with hierarchy. Updates the dataNodes when new data is provided by the data source.
 */ @ Component ( { moduleId : module . id , selector : 'cdk-tree' , exportAs : 'cdkTree' , template : `<ng-container cdkTreeNodeOutlet></ng-container>` , host : { 'class' : 'cdk-tree' , 'role' : 'tree' , } , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . OnPush } ) export class CdkTree < T > implements AfterContentChecked , CollectionViewer , OnDestroy , OnInit { /** Subject that emits when the component has been destroyed. */ private _onDestroy = new Subject < void > ( ) ; /** Differ used to find the changes in the data provided by the data source. */ private _dataDiffer : IterableDiffer < T > ; /** Stores the node definition that does not have a when predicate. */ private _defaultNodeDef : CdkTreeNodeDef < T > | null ; /** Data subscription */ private _dataSubscription : Subscription | null ; /** Level of nodes */ private _levels : Map < T , number > = new Map < T , number > ( ) ; /**
     * Provides a stream containing the latest data array to render. Influenced by the tree's
     * stream of view window (what dataNodes are currently on screen).
     * Data source can be an observable of data array, or a data array to render.
     */ @ Input ( ) get dataSource ( ) : DataSource < T > | Observable < T [ ] > | T [ ] { return this . _dataSource ; } set dataSource ( dataSource ) { if ( this . _dataSource !== dataSource ) { this . _switchDataSource ( dataSource ) ; } } private _dataSource : DataSource < T > | Observable < T [ ] > | T [ ] ; /** The tree controller */ @ Input ( ) treeControl : TreeControl < T > ; /**
     * Tracking function that will be used to check the differences in data changes. Used similarly
     * to `ngFor` `trackBy` function. Optimize node operations by identifying a node based on its data
     * relative to the function to know if a node should be added/removed/moved.
     * Accepts a function that takes two parameters, `index` and `item`.
     */ @ Input ( ) trackBy : TrackByFunction < T > ; @ ViewChild ( CdkTreeNodeOutlet ) _nodeOutlet : CdkTreeNodeOutlet ; /** The tree node template for the tree */ @ ContentChildren ( CdkTreeNodeDef ) _nodeDefs : QueryList < CdkTreeNodeDef < T > > ; /**
     * Stream containing the latest information on what rows are being displayed on screen.
     * Can be used by the data source to as a heuristic of what data should be provided.
     */ viewChange = new BehaviorSubject < { start : number ; end : number ; } > ( { start : 0 , end : Number . MAX_VALUE } ) ; constructor ( private _differs , private _changeDetectorRef ) { } ngOnInit ( ) { this . _dataDiffer = this . _differs . find ( [ ] ) . create ( this . trackBy ) ; if ( ! this . treeControl ) { throw getTreeControlMissingError ( ) ; } } ngOnDestroy ( ) { this . _nodeOutlet . viewContainer . clear ( ) ; this . _onDestroy . next ( ) ; this . _onDestroy . complete ( ) ; if ( this . _dataSource && typeof ( this . _dataSource as DataSource < T > ) . disconnect === 'function' ) { ( this . dataSource as DataSource < T > ) . disconnect ( this ) ; } if ( this . _dataSubscription ) { this . _dataSubscription . unsubscribe ( ) ; this . _dataSubscription = null ; } } ngAfterContentChecked ( ) { const defaultNodeDefs = this . _nodeDefs . filter ( def => ! def . when ) ; if ( defaultNodeDefs . length > 1 ) { throw getTreeMultipleDefaultNodeDefsError ( ) ; } this . _defaultNodeDef = defaultNodeDefs [ 0 ] ; if ( this . dataSource && this . _nodeDefs && ! this . _dataSubscription ) { this . _observeRenderChanges ( ) ; } } /**
     * Switch to the provided data source by resetting the data and unsubscribing from the current
     * render change subscription if one exists. If the data source is null, interpret this by
     * clearing the node outlet. Otherwise start listening for new data.
     */ private _switchDataSource ( dataSource ) { if ( this . _dataSource && typeof ( this . _dataSource as DataSource < T > ) . disconnect === 'function' ) { ( this . dataSource as DataSource < T > ) . disconnect ( this ) ; } if ( this . _dataSubscription ) { this . _dataSubscription . unsubscribe ( ) ; this . _dataSubscription = null ; } if ( ! dataSource ) { this . _nodeOutlet . viewContainer . clear ( ) ; } this . _dataSource = dataSource ; if ( this . _nodeDefs ) { this . _observeRenderChanges ( ) ; } } /** Set up a subscription for the data provided by the data source. */ private _observeRenderChanges ( ) { let dataStream ; if ( typeof ( this . _dataSource as DataSource < T > ) . connect === 'function' ) { dataStream = ( this . _dataSource as DataSource < T > ) . connect ( this ) ; } else if ( this . _dataSource instanceof Observable ) { dataStream = this . _dataSource ; } else if ( Array . isArray ( this . _dataSource ) ) { dataStream = observableOf ( this . _dataSource ) ; } if ( dataStream ) { this . _dataSubscription = dataStream . pipe ( takeUntil ( this . _onDestroy ) ) . subscribe ( data => this . renderNodeChanges ( data ) ) ; } else { throw getTreeNoValidDataSourceError ( ) ; } } /** Check for changes made in the data and render each change (node added/removed/moved). */ renderNodeChanges ( data , dataDiffer = this . _dataDiffer , viewContainer = this . _nodeOutlet . viewContainer , parentData ? ) { const changes = dataDiffer . diff ( data ) ; if ( ! changes ) { return ; } changes . forEachOperation ( ( item , adjustedPreviousIndex , currentIndex ) => { if ( item . previousIndex == null ) { this . insertNode ( data [ currentIndex ! ] , currentIndex ! , viewContainer , parentData ) ; } else if ( currentIndex == null ) { viewContainer . remove ( adjustedPreviousIndex ! ) ; this . _levels . delete ( item . item ) ; } else { const view = viewContainer . get ( adjustedPreviousIndex ! ) ; viewContainer . move ( view ! , currentIndex ) ; } } ) ; this . _changeDetectorRef . detectChanges ( ) ; } /**
     * Finds the matching node definition that should be used for this node data. If there is only
     * one node definition, it is returned. Otherwise, find the node definition that has a when
     * predicate that returns true with the data. If none return true, return the default node
     * definition.
     */ _getNodeDef ( data , i ) { if ( this . _nodeDefs . length === 1 ) { return this . _nodeDefs . first ; } const nodeDef = this . _nodeDefs . find ( def => def . when && def . when ( i , data ) ) || this . _defaultNodeDef ; if ( ! nodeDef ) { throw getTreeMissingMatchingNodeDefError ( ) ; } return nodeDef ; } /**
     * Create the embedded view for the data node template and place it in the correct index location
     * within the data node view container.
     */ insertNode ( nodeData , index , viewContainer ? , parentData ? ) { const node = this . _getNodeDef ( nodeData , index ) ; const context = new CdkTreeNodeOutletContext < T > ( nodeData ) ; if ( this . treeControl . getLevel ) { context . level = this . treeControl . getLevel ( nodeData ) ; } else if ( typeof parentData !== 'undefined' && this . _levels . has ( parentData ) ) { context . level = this . _levels . get ( parentData ) ! + 1 ; } else { context . level = 0 ; } this . _levels . set ( nodeData , context . level ) ; const container = viewContainer ? viewContainer : this . _nodeOutlet . viewContainer ; container . createEmbeddedView ( node . template , context , index ) ; if ( CdkTreeNode . mostRecentTreeNode ) { CdkTreeNode . mostRecentTreeNode . data = nodeData ; } } } /**
 * Tree node for CdkTree. It contains the data in the tree node.
 */ @ Directive ( { selector : 'cdk-tree-node' , exportAs : 'cdkTreeNode' , host : { '[attr.aria-expanded]' : 'isExpanded' , '[attr.aria-level]' : 'role === "treeitem" ? level : null' , '[attr.role]' : 'role' , 'class' : 'cdk-tree-node' , } , } ) export class CdkTreeNode < T > implements FocusableOption , OnDestroy { /**
     * The most recently created `CdkTreeNode`. We save it in static variable so we can retrieve it
     * in `CdkTree` and set the data to it.
     */ static mostRecentTreeNode : CdkTreeNode < any > | null = null ; /** Subject that emits when the component has been destroyed. */ protected _destroyed = new Subject < void > ( ) ; /** The tree node's data. */ get data ( ) : T { return this . _data ; } set data ( value ) { this . _data = value ; this . _setRoleFromData ( ) ; } protected _data : T ; get isExpanded ( ) : boolean { return this . _tree . treeControl . isExpanded ( this . _data ) ; } get level ( ) : number { return this . _tree . treeControl . getLevel ? this . _tree . treeControl . getLevel ( this . _data ) : 0 ; } /**
     * The role of the node should be 'group' if it's an internal node,
     * and 'treeitem' if it's a leaf node.
     */ @ Input ( ) role : 'treeitem' | 'group' = 'treeitem' ; constructor ( protected _elementRef , protected _tree ) { CdkTreeNode . mostRecentTreeNode = this as CdkTreeNode < T > ; } ngOnDestroy ( ) { if ( CdkTreeNode . mostRecentTreeNode === this ) { CdkTreeNode . mostRecentTreeNode = null ; } this . _destroyed . next ( ) ; this . _destroyed . complete ( ) ; } /** Focuses the menu item. Implements for FocusableOption. */ focus ( ) { this . _elementRef . nativeElement . focus ( ) ; } protected _setRoleFromData ( ) { if ( this . _tree . treeControl . isExpandable ) { this . role = this . _tree . treeControl . isExpandable ( this . _data ) ? 'group' : 'treeitem' ; } else { if ( ! this . _tree . treeControl . getChildren ) { throw getTreeControlFunctionsMissingError ( ) ; } const childrenNodes = this . _tree . treeControl . getChildren ( this . _data ) ; if ( Array . isArray ( childrenNodes ) ) { this . _setRoleFromChildren ( childrenNodes as T [ ] ) ; } else if ( childrenNodes instanceof Observable ) { childrenNodes . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( children => this . _setRoleFromChildren ( children ) ) ; } } } protected _setRoleFromChildren ( children ) { this . role = children && children . length ? 'group' : 'treeitem' ; } }