@angular/cdk
Version:
Angular Material Component Development Kit
1 lines • 123 kB
Source Map (JSON)
{"version":3,"file":"tree.mjs","sources":["../../../../../k8-fastbuild-ST-46c76129e412/bin/src/cdk/tree/control/base-tree-control.ts","../../../../../k8-fastbuild-ST-46c76129e412/bin/src/cdk/tree/control/flat-tree-control.ts","../../../../../k8-fastbuild-ST-46c76129e412/bin/src/cdk/tree/control/nested-tree-control.ts","../../../../../k8-fastbuild-ST-46c76129e412/bin/src/cdk/tree/outlet.ts","../../../../../k8-fastbuild-ST-46c76129e412/bin/src/cdk/tree/node.ts","../../../../../k8-fastbuild-ST-46c76129e412/bin/src/cdk/tree/tree-errors.ts","../../../../../k8-fastbuild-ST-46c76129e412/bin/src/cdk/tree/tree.ts","../../../../../k8-fastbuild-ST-46c76129e412/bin/src/cdk/tree/nested-node.ts","../../../../../k8-fastbuild-ST-46c76129e412/bin/src/cdk/tree/padding.ts","../../../../../k8-fastbuild-ST-46c76129e412/bin/src/cdk/tree/toggle.ts","../../../../../k8-fastbuild-ST-46c76129e412/bin/src/cdk/tree/tree-module.ts"],"sourcesContent":["/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\nimport {SelectionModel} from '../../collections';\nimport {Observable} from 'rxjs';\nimport {TreeControl} from './tree-control';\n\n/**\n * Base tree control. It has basic toggle/expand/collapse operations on a single data node.\n *\n * @deprecated Use one of levelAccessor or childrenAccessor. To be removed in a future version.\n * @breaking-change 21.0.0\n */\nexport abstract class BaseTreeControl<T, K = T> implements TreeControl<T, K> {\n /** Gets a list of descendent data nodes of a subtree rooted at given data node recursively. */\n abstract getDescendants(dataNode: T): T[];\n\n /** Expands all data nodes in the tree. */\n abstract expandAll(): void;\n\n /** Saved data node for `expandAll` action. */\n dataNodes: T[];\n\n /** A selection model with multi-selection to track expansion status. */\n expansionModel: SelectionModel<K> = new SelectionModel<K>(true);\n\n /**\n * Returns the identifier by which a dataNode should be tracked, should its\n * reference change.\n *\n * Similar to trackBy for *ngFor\n */\n trackBy?: (dataNode: T) => K;\n\n /** Get depth of a given data node, return the level number. This is for flat tree node. */\n getLevel: (dataNode: T) => number;\n\n /**\n * Whether the data node is expandable. Returns true if expandable.\n * This is for flat tree node.\n */\n isExpandable: (dataNode: T) => boolean;\n\n /** Gets a stream that emits whenever the given data node's children change. */\n getChildren: (dataNode: T) => Observable<T[]> | T[] | undefined | null;\n\n /** Toggles one single data node's expanded/collapsed state. */\n toggle(dataNode: T): void {\n this.expansionModel.toggle(this._trackByValue(dataNode));\n }\n\n /** Expands one single data node. */\n expand(dataNode: T): void {\n this.expansionModel.select(this._trackByValue(dataNode));\n }\n\n /** Collapses one single data node. */\n collapse(dataNode: T): void {\n this.expansionModel.deselect(this._trackByValue(dataNode));\n }\n\n /** Whether a given data node is expanded or not. Returns true if the data node is expanded. */\n isExpanded(dataNode: T): boolean {\n return this.expansionModel.isSelected(this._trackByValue(dataNode));\n }\n\n /** Toggles a subtree rooted at `node` recursively. */\n toggleDescendants(dataNode: T): void {\n this.expansionModel.isSelected(this._trackByValue(dataNode))\n ? this.collapseDescendants(dataNode)\n : this.expandDescendants(dataNode);\n }\n\n /** Collapse all dataNodes in the tree. */\n collapseAll(): void {\n this.expansionModel.clear();\n }\n\n /** Expands a subtree rooted at given data node recursively. */\n expandDescendants(dataNode: T): void {\n let toBeProcessed = [dataNode];\n toBeProcessed.push(...this.getDescendants(dataNode));\n this.expansionModel.select(...toBeProcessed.map(value => this._trackByValue(value)));\n }\n\n /** Collapses a subtree rooted at given data node recursively. */\n collapseDescendants(dataNode: T): void {\n let toBeProcessed = [dataNode];\n toBeProcessed.push(...this.getDescendants(dataNode));\n this.expansionModel.deselect(...toBeProcessed.map(value => this._trackByValue(value)));\n }\n\n protected _trackByValue(value: T | K): K {\n return this.trackBy ? this.trackBy(value as T) : (value as K);\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\nimport {BaseTreeControl} from './base-tree-control';\n\n/** Optional set of configuration that can be provided to the FlatTreeControl. */\nexport interface FlatTreeControlOptions<T, K> {\n trackBy?: (dataNode: T) => K;\n}\n\n/**\n * Flat tree control. Able to expand/collapse a subtree recursively for flattened tree.\n *\n * @deprecated Use one of levelAccessor or childrenAccessor instead. To be removed in a future\n * version.\n * @breaking-change 21.0.0\n */\nexport class FlatTreeControl<T, K = T> extends BaseTreeControl<T, K> {\n /** Construct with flat tree data node functions getLevel and isExpandable. */\n constructor(\n public override getLevel: (dataNode: T) => number,\n public override isExpandable: (dataNode: T) => boolean,\n public options?: FlatTreeControlOptions<T, K>,\n ) {\n super();\n\n if (this.options) {\n this.trackBy = this.options.trackBy;\n }\n }\n\n /**\n * Gets a list of the data node's subtree of descendent data nodes.\n *\n * To make this working, the `dataNodes` of the TreeControl must be flattened tree nodes\n * with correct levels.\n */\n getDescendants(dataNode: T): T[] {\n const startIndex = this.dataNodes.indexOf(dataNode);\n const results: T[] = [];\n\n // Goes through flattened tree nodes in the `dataNodes` array, and get all descendants.\n // The level of descendants of a tree node must be greater than the level of the given\n // tree node.\n // If we reach a node whose level is equal to the level of the tree node, we hit a sibling.\n // If we reach a node whose level is greater than the level of the tree node, we hit a\n // sibling of an ancestor.\n for (\n let i = startIndex + 1;\n i < this.dataNodes.length && this.getLevel(dataNode) < this.getLevel(this.dataNodes[i]);\n i++\n ) {\n results.push(this.dataNodes[i]);\n }\n return results;\n }\n\n /**\n * Expands all data nodes in the tree.\n *\n * To make this working, the `dataNodes` variable of the TreeControl must be set to all flattened\n * data nodes of the tree.\n */\n expandAll(): void {\n this.expansionModel.select(...this.dataNodes.map(node => this._trackByValue(node)));\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\nimport {Observable, isObservable} from 'rxjs';\nimport {take, filter} from 'rxjs/operators';\nimport {BaseTreeControl} from './base-tree-control';\n\n/** Optional set of configuration that can be provided to the NestedTreeControl. */\nexport interface NestedTreeControlOptions<T, K> {\n /** Function to determine if the provided node is expandable. */\n isExpandable?: (dataNode: T) => boolean;\n trackBy?: (dataNode: T) => K;\n}\n\n/**\n * Nested tree control. Able to expand/collapse a subtree recursively for NestedNode type.\n *\n * @deprecated Use one of levelAccessor or childrenAccessor instead. To be removed in a future\n * version.\n * @breaking-change 21.0.0\n */\nexport class NestedTreeControl<T, K = T> extends BaseTreeControl<T, K> {\n /** Construct with nested tree function getChildren. */\n constructor(\n public override getChildren: (dataNode: T) => Observable<T[]> | T[] | undefined | null,\n public options?: NestedTreeControlOptions<T, K>,\n ) {\n super();\n\n if (this.options) {\n this.trackBy = this.options.trackBy;\n }\n\n if (this.options?.isExpandable) {\n this.isExpandable = this.options.isExpandable;\n }\n }\n\n /**\n * Expands all dataNodes in the tree.\n *\n * To make this working, the `dataNodes` variable of the TreeControl must be set to all root level\n * data nodes of the tree.\n */\n expandAll(): void {\n this.expansionModel.clear();\n const allNodes = this.dataNodes.reduce(\n (accumulator: T[], dataNode) => [...accumulator, ...this.getDescendants(dataNode), dataNode],\n [],\n );\n this.expansionModel.select(...allNodes.map(node => this._trackByValue(node)));\n }\n\n /** Gets a list of descendant dataNodes of a subtree rooted at given data node recursively. */\n getDescendants(dataNode: T): T[] {\n const descendants: T[] = [];\n\n this._getDescendants(descendants, dataNode);\n // Remove the node itself\n return descendants.splice(1);\n }\n\n /** A helper function to get descendants recursively. */\n protected _getDescendants(descendants: T[], dataNode: T): void {\n descendants.push(dataNode);\n const childrenNodes = this.getChildren(dataNode);\n if (Array.isArray(childrenNodes)) {\n childrenNodes.forEach((child: T) => this._getDescendants(descendants, child));\n } else if (isObservable(childrenNodes)) {\n // TypeScript as of version 3.5 doesn't seem to treat `Boolean` like a function that\n // returns a `boolean` specifically in the context of `filter`, so we manually clarify that.\n childrenNodes.pipe(take(1), filter(Boolean as () => boolean)).subscribe(children => {\n for (const child of children) {\n this._getDescendants(descendants, child);\n }\n });\n }\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\nimport {Directive, InjectionToken, ViewContainerRef, inject} from '@angular/core';\n\n/**\n * Injection token used to provide a `CdkTreeNode` to its outlet.\n * Used primarily to avoid circular imports.\n * @docs-private\n */\nexport const CDK_TREE_NODE_OUTLET_NODE = new InjectionToken<{}>('CDK_TREE_NODE_OUTLET_NODE');\n\n/**\n * Outlet for nested CdkNode. Put `[cdkTreeNodeOutlet]` on a tag to place children dataNodes\n * inside the outlet.\n */\n@Directive({\n selector: '[cdkTreeNodeOutlet]',\n})\nexport class CdkTreeNodeOutlet {\n viewContainer = inject(ViewContainerRef);\n _node? = inject(CDK_TREE_NODE_OUTLET_NODE, {optional: true});\n\n constructor(...args: unknown[]);\n constructor() {}\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\nimport {Directive, TemplateRef, inject} from '@angular/core';\n\n/** Context provided to the tree node component. */\nexport class CdkTreeNodeOutletContext<T> {\n /** Data for the node. */\n $implicit: T;\n\n /** Depth of the node. */\n level: number;\n\n /** Index location of the node. */\n index?: number;\n\n /** Length of the number of total dataNodes. */\n count?: number;\n\n constructor(data: T) {\n this.$implicit = data;\n }\n}\n\n/**\n * Data node definition for the CdkTree.\n * Captures the node's template and a when predicate that describes when this node should be used.\n */\n@Directive({\n selector: '[cdkTreeNodeDef]',\n inputs: [{name: 'when', alias: 'cdkTreeNodeDefWhen'}],\n})\nexport class CdkTreeNodeDef<T> {\n /** @docs-private */\n template = inject<TemplateRef<any>>(TemplateRef);\n\n /**\n * Function that should return true if this node template should be used for the provided node\n * data and index. If left undefined, this node will be considered the default node template to\n * use when no other when functions return true for the data.\n * For every node, there must be at least one when function that passes or an undefined to\n * default.\n */\n when: (index: number, nodeData: T) => boolean;\n\n constructor(...args: unknown[]);\n constructor() {}\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\n\n/**\n * Returns an error to be thrown when there is no usable data.\n * @docs-private\n */\nexport function getTreeNoValidDataSourceError() {\n return Error(`A valid data source must be provided.`);\n}\n\n/**\n * Returns an error to be thrown when there are multiple nodes that are missing a when function.\n * @docs-private\n */\nexport function getTreeMultipleDefaultNodeDefsError() {\n return Error(`There can only be one default row without a when predicate function.`);\n}\n\n/**\n * Returns an error to be thrown when there are no matching node defs for a particular set of data.\n * @docs-private\n */\nexport function getTreeMissingMatchingNodeDefError() {\n return Error(`Could not find a matching node definition for the provided node data.`);\n}\n\n/**\n * Returns an error to be thrown when there is no tree control.\n * @docs-private\n */\nexport function getTreeControlMissingError() {\n return Error(`Could not find a tree control, levelAccessor, or childrenAccessor for the tree.`);\n}\n\n/**\n * Returns an error to be thrown when there are multiple ways of specifying children or level\n * provided to the tree.\n * @docs-private\n */\nexport function getMultipleTreeControlsError() {\n return Error(`More than one of tree control, levelAccessor, or childrenAccessor were provided.`);\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.dev/license\n */\nimport {\n TREE_KEY_MANAGER,\n TreeKeyManagerFactory,\n TreeKeyManagerItem,\n TreeKeyManagerOptions,\n TreeKeyManagerStrategy,\n} from '../a11y';\nimport {Directionality} from '../bidi';\nimport {\n CollectionViewer,\n DataSource,\n isDataSource,\n SelectionChange,\n SelectionModel,\n} from '../collections';\nimport {\n AfterContentChecked,\n AfterContentInit,\n AfterViewInit,\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ContentChildren,\n Directive,\n ElementRef,\n EventEmitter,\n EmbeddedViewRef,\n Input,\n IterableChangeRecord,\n IterableDiffer,\n IterableDiffers,\n OnDestroy,\n OnInit,\n Output,\n QueryList,\n TrackByFunction,\n ViewChild,\n ViewContainerRef,\n ViewEncapsulation,\n numberAttribute,\n inject,\n booleanAttribute,\n} from '@angular/core';\nimport {coerceObservable} from '../coercion/private';\nimport {\n BehaviorSubject,\n combineLatest,\n concat,\n EMPTY,\n Observable,\n Subject,\n Subscription,\n isObservable,\n of as observableOf,\n} from 'rxjs';\nimport {\n distinctUntilChanged,\n concatMap,\n map,\n reduce,\n startWith,\n switchMap,\n take,\n takeUntil,\n tap,\n} from 'rxjs/operators';\nimport {TreeControl} from './control/tree-control';\nimport {CdkTreeNodeDef, CdkTreeNodeOutletContext} from './node';\nimport {CdkTreeNodeOutlet} from './outlet';\nimport {\n getMultipleTreeControlsError,\n getTreeControlMissingError,\n getTreeMissingMatchingNodeDefError,\n getTreeMultipleDefaultNodeDefsError,\n getTreeNoValidDataSourceError,\n} from './tree-errors';\n\ntype RenderingData<T> =\n | {\n flattenedNodes: null;\n nodeType: null;\n renderNodes: readonly T[];\n }\n | {\n flattenedNodes: readonly T[];\n nodeType: 'nested' | 'flat';\n renderNodes: readonly T[];\n };\n\n/**\n * CDK tree component that connects with a data source to retrieve data of type `T` and renders\n * dataNodes with hierarchy. Updates the dataNodes when new data is provided by the data source.\n */\n@Component({\n selector: 'cdk-tree',\n exportAs: 'cdkTree',\n template: `<ng-container cdkTreeNodeOutlet></ng-container>`,\n host: {\n 'class': 'cdk-tree',\n 'role': 'tree',\n '(keydown)': '_sendKeydownToKeyManager($event)',\n },\n encapsulation: ViewEncapsulation.None,\n // The \"OnPush\" status for the `CdkTree` component is effectively a noop, so we are removing it.\n // The view for `CdkTree` consists entirely of templates declared in other views. As they are\n // declared elsewhere, they are checked when their declaration points are checked.\n // tslint:disable-next-line:validate-decorators\n changeDetection: ChangeDetectionStrategy.Default,\n imports: [CdkTreeNodeOutlet],\n})\nexport class CdkTree<T, K = T>\n implements\n AfterContentChecked,\n AfterContentInit,\n AfterViewInit,\n CollectionViewer,\n OnDestroy,\n OnInit\n{\n private _differs = inject(IterableDiffers);\n private _changeDetectorRef = inject(ChangeDetectorRef);\n private _elementRef = inject(ElementRef);\n\n private _dir = inject(Directionality);\n\n /** Subject that emits when the component has been destroyed. */\n private readonly _onDestroy = new Subject<void>();\n\n /** Differ used to find the changes in the data provided by the data source. */\n private _dataDiffer: IterableDiffer<T>;\n\n /** Stores the node definition that does not have a when predicate. */\n private _defaultNodeDef: CdkTreeNodeDef<T> | null;\n\n /** Data subscription */\n private _dataSubscription: Subscription | null;\n\n /** Level of nodes */\n private _levels: Map<K, number> = new Map<K, number>();\n\n /** The immediate parents for a node. This is `null` if there is no parent. */\n private _parents: Map<K, T | null> = new Map<K, T | null>();\n\n /**\n * Nodes grouped into each set, which is a list of nodes displayed together in the DOM.\n *\n * Lookup key is the parent of a set. Root nodes have key of null.\n *\n * Values is a 'set' of tree nodes. Each tree node maps to a treeitem element. Sets are in the\n * order that it is rendered. Each set maps directly to aria-posinset and aria-setsize attributes.\n */\n private _ariaSets: Map<K | null, T[]> = new Map<K | null, T[]>();\n\n /**\n * Provides a stream containing the latest data array to render. Influenced by the tree's\n * stream of view window (what dataNodes are currently on screen).\n * Data source can be an observable of data array, or a data array to render.\n */\n @Input()\n get dataSource(): DataSource<T> | Observable<T[]> | T[] {\n return this._dataSource;\n }\n set dataSource(dataSource: DataSource<T> | Observable<T[]> | T[]) {\n if (this._dataSource !== dataSource) {\n this._switchDataSource(dataSource);\n }\n }\n private _dataSource: DataSource<T> | Observable<T[]> | T[];\n\n /**\n * The tree controller\n *\n * @deprecated Use one of `levelAccessor` or `childrenAccessor` instead. To be removed in a\n * future version.\n * @breaking-change 21.0.0\n */\n @Input() treeControl?: TreeControl<T, K>;\n\n /**\n * Given a data node, determines what tree level the node is at.\n *\n * One of levelAccessor or childrenAccessor must be specified, not both.\n * This is enforced at run-time.\n */\n @Input() levelAccessor?: (dataNode: T) => number;\n\n /**\n * Given a data node, determines what the children of that node are.\n *\n * One of levelAccessor or childrenAccessor must be specified, not both.\n * This is enforced at run-time.\n */\n @Input() childrenAccessor?: (dataNode: T) => T[] | Observable<T[]>;\n\n /**\n * Tracking function that will be used to check the differences in data changes. Used similarly\n * to `ngFor` `trackBy` function. Optimize node operations by identifying a node based on its data\n * relative to the function to know if a node should be added/removed/moved.\n * Accepts a function that takes two parameters, `index` and `item`.\n */\n @Input() trackBy: TrackByFunction<T>;\n\n /**\n * Given a data node, determines the key by which we determine whether or not this node is expanded.\n */\n @Input() expansionKey?: (dataNode: T) => K;\n\n // Outlets within the tree's template where the dataNodes will be inserted.\n @ViewChild(CdkTreeNodeOutlet, {static: true}) _nodeOutlet: CdkTreeNodeOutlet;\n\n /** The tree node template for the tree */\n @ContentChildren(CdkTreeNodeDef, {\n // We need to use `descendants: true`, because Ivy will no longer match\n // indirect descendants if it's left as false.\n descendants: true,\n })\n _nodeDefs: QueryList<CdkTreeNodeDef<T>>;\n\n // TODO(tinayuangao): Setup a listener for scrolling, emit the calculated view to viewChange.\n // Remove the MAX_VALUE in viewChange\n /**\n * Stream containing the latest information on what rows are being displayed on screen.\n * Can be used by the data source to as a heuristic of what data should be provided.\n */\n readonly viewChange = new BehaviorSubject<{start: number; end: number}>({\n start: 0,\n end: Number.MAX_VALUE,\n });\n\n /** Keep track of which nodes are expanded. */\n private _expansionModel?: SelectionModel<K>;\n\n /**\n * Maintain a synchronous cache of flattened data nodes. This will only be\n * populated after initial render, and in certain cases, will be delayed due to\n * relying on Observable `getChildren` calls.\n */\n private _flattenedNodes: BehaviorSubject<readonly T[]> = new BehaviorSubject<readonly T[]>([]);\n\n /** The automatically determined node type for the tree. */\n private _nodeType: BehaviorSubject<'flat' | 'nested' | null> = new BehaviorSubject<\n 'flat' | 'nested' | null\n >(null);\n\n /** The mapping between data and the node that is rendered. */\n private _nodes: BehaviorSubject<Map<K, CdkTreeNode<T, K>>> = new BehaviorSubject(\n new Map<K, CdkTreeNode<T, K>>(),\n );\n\n /**\n * Synchronous cache of nodes for the `TreeKeyManager`. This is separate\n * from `_flattenedNodes` so they can be independently updated at different\n * times.\n */\n private _keyManagerNodes: BehaviorSubject<readonly T[]> = new BehaviorSubject<readonly T[]>([]);\n\n private _keyManagerFactory = inject(TREE_KEY_MANAGER) as TreeKeyManagerFactory<CdkTreeNode<T, K>>;\n\n /** The key manager for this tree. Handles focus and activation based on user keyboard input. */\n _keyManager: TreeKeyManagerStrategy<CdkTreeNode<T, K>>;\n private _viewInit = false;\n\n constructor(...args: unknown[]);\n constructor() {}\n\n ngAfterContentInit() {\n this._initializeKeyManager();\n }\n\n ngAfterContentChecked() {\n this._updateDefaultNodeDefinition();\n this._subscribeToDataChanges();\n }\n\n ngOnDestroy() {\n this._nodeOutlet.viewContainer.clear();\n\n this.viewChange.complete();\n this._onDestroy.next();\n this._onDestroy.complete();\n\n if (this._dataSource && typeof (this._dataSource as DataSource<T>).disconnect === 'function') {\n (this.dataSource as DataSource<T>).disconnect(this);\n }\n\n if (this._dataSubscription) {\n this._dataSubscription.unsubscribe();\n this._dataSubscription = null;\n }\n\n // In certain tests, the tree might be destroyed before this is initialized\n // in `ngAfterContentInit`.\n this._keyManager?.destroy();\n }\n\n ngOnInit() {\n this._checkTreeControlUsage();\n this._initializeDataDiffer();\n }\n\n ngAfterViewInit() {\n this._viewInit = true;\n }\n\n private _updateDefaultNodeDefinition() {\n const defaultNodeDefs = this._nodeDefs.filter(def => !def.when);\n if (defaultNodeDefs.length > 1 && (typeof ngDevMode === 'undefined' || ngDevMode)) {\n throw getTreeMultipleDefaultNodeDefsError();\n }\n this._defaultNodeDef = defaultNodeDefs[0];\n }\n\n /**\n * Sets the node type for the tree, if it hasn't been set yet.\n *\n * This will be called by the first node that's rendered in order for the tree\n * to determine what data transformations are required.\n */\n _setNodeTypeIfUnset(newType: 'flat' | 'nested') {\n const currentType = this._nodeType.value;\n\n if (currentType === null) {\n this._nodeType.next(newType);\n } else if ((typeof ngDevMode === 'undefined' || ngDevMode) && currentType !== newType) {\n console.warn(\n `Tree is using conflicting node types which can cause unexpected behavior. ` +\n `Please use tree nodes of the same type (e.g. only flat or only nested). ` +\n `Current node type: \"${currentType}\", new node type \"${newType}\".`,\n );\n }\n }\n\n /**\n * Switch to the provided data source by resetting the data and unsubscribing from the current\n * render change subscription if one exists. If the data source is null, interpret this by\n * clearing the node outlet. Otherwise start listening for new data.\n */\n private _switchDataSource(dataSource: DataSource<T> | Observable<T[]> | T[]) {\n if (this._dataSource && typeof (this._dataSource as DataSource<T>).disconnect === 'function') {\n (this.dataSource as DataSource<T>).disconnect(this);\n }\n\n if (this._dataSubscription) {\n this._dataSubscription.unsubscribe();\n this._dataSubscription = null;\n }\n\n // Remove the all dataNodes if there is now no data source\n if (!dataSource) {\n this._nodeOutlet.viewContainer.clear();\n }\n\n this._dataSource = dataSource;\n if (this._nodeDefs) {\n this._subscribeToDataChanges();\n }\n }\n\n _getExpansionModel() {\n if (!this.treeControl) {\n this._expansionModel ??= new SelectionModel<K>(true);\n return this._expansionModel;\n }\n return this.treeControl.expansionModel;\n }\n\n /** Set up a subscription for the data provided by the data source. */\n private _subscribeToDataChanges() {\n if (this._dataSubscription) {\n return;\n }\n\n let dataStream: Observable<readonly T[]> | undefined;\n\n if (isDataSource(this._dataSource)) {\n dataStream = this._dataSource.connect(this);\n } else if (isObservable(this._dataSource)) {\n dataStream = this._dataSource;\n } else if (Array.isArray(this._dataSource)) {\n dataStream = observableOf(this._dataSource);\n }\n\n if (!dataStream) {\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\n throw getTreeNoValidDataSourceError();\n }\n return;\n }\n\n this._dataSubscription = this._getRenderData(dataStream)\n .pipe(takeUntil(this._onDestroy))\n .subscribe(renderingData => {\n this._renderDataChanges(renderingData);\n });\n }\n\n /** Given an Observable containing a stream of the raw data, returns an Observable containing the RenderingData */\n private _getRenderData(dataStream: Observable<readonly T[]>): Observable<RenderingData<T>> {\n const expansionModel = this._getExpansionModel();\n return combineLatest([\n dataStream,\n this._nodeType,\n // We don't use the expansion data directly, however we add it here to essentially\n // trigger data rendering when expansion changes occur.\n expansionModel.changed.pipe(\n startWith(null),\n tap(expansionChanges => {\n this._emitExpansionChanges(expansionChanges);\n }),\n ),\n ]).pipe(\n switchMap(([data, nodeType]) => {\n if (nodeType === null) {\n return observableOf({renderNodes: data, flattenedNodes: null, nodeType} as const);\n }\n\n // If we're here, then we know what our node type is, and therefore can\n // perform our usual rendering pipeline, which necessitates converting the data\n return this._computeRenderingData(data, nodeType).pipe(\n map(convertedData => ({...convertedData, nodeType}) as const),\n );\n }),\n );\n }\n\n private _renderDataChanges(data: RenderingData<T>) {\n if (data.nodeType === null) {\n this.renderNodeChanges(data.renderNodes);\n return;\n }\n\n // If we're here, then we know what our node type is, and therefore can\n // perform our usual rendering pipeline.\n this._updateCachedData(data.flattenedNodes);\n this.renderNodeChanges(data.renderNodes);\n this._updateKeyManagerItems(data.flattenedNodes);\n }\n\n private _emitExpansionChanges(expansionChanges: SelectionChange<K> | null) {\n if (!expansionChanges) {\n return;\n }\n\n const nodes = this._nodes.value;\n for (const added of expansionChanges.added) {\n const node = nodes.get(added);\n node?._emitExpansionState(true);\n }\n for (const removed of expansionChanges.removed) {\n const node = nodes.get(removed);\n node?._emitExpansionState(false);\n }\n }\n\n private _initializeKeyManager() {\n const items = combineLatest([this._keyManagerNodes, this._nodes]).pipe(\n map(([keyManagerNodes, renderNodes]) =>\n keyManagerNodes.reduce<CdkTreeNode<T, K>[]>((items, data) => {\n const node = renderNodes.get(this._getExpansionKey(data));\n if (node) {\n items.push(node);\n }\n return items;\n }, []),\n ),\n );\n\n const keyManagerOptions: TreeKeyManagerOptions<CdkTreeNode<T, K>> = {\n trackBy: node => this._getExpansionKey(node.data),\n skipPredicate: node => !!node.isDisabled,\n typeAheadDebounceInterval: true,\n horizontalOrientation: this._dir.value,\n };\n\n this._keyManager = this._keyManagerFactory(items, keyManagerOptions);\n }\n\n private _initializeDataDiffer() {\n // Provide a default trackBy based on `_getExpansionKey` if one isn't provided.\n const trackBy = this.trackBy ?? ((_index: number, item: T) => this._getExpansionKey(item));\n this._dataDiffer = this._differs.find([]).create(trackBy);\n }\n\n private _checkTreeControlUsage() {\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\n // Verify that Tree follows API contract of using one of TreeControl, levelAccessor or\n // childrenAccessor. Throw an appropriate error if contract is not met.\n let numTreeControls = 0;\n\n if (this.treeControl) {\n numTreeControls++;\n }\n if (this.levelAccessor) {\n numTreeControls++;\n }\n if (this.childrenAccessor) {\n numTreeControls++;\n }\n\n if (!numTreeControls) {\n throw getTreeControlMissingError();\n } else if (numTreeControls > 1) {\n throw getMultipleTreeControlsError();\n }\n }\n }\n\n /** Check for changes made in the data and render each change (node added/removed/moved). */\n renderNodeChanges(\n data: readonly T[],\n dataDiffer: IterableDiffer<T> = this._dataDiffer,\n viewContainer: ViewContainerRef = this._nodeOutlet.viewContainer,\n parentData?: T,\n ) {\n const changes = dataDiffer.diff(data);\n\n // Some tree consumers expect change detection to propagate to nodes\n // even when the array itself hasn't changed; we explicitly detect changes\n // anyways in order for nodes to update their data.\n //\n // However, if change detection is called while the component's view is\n // still initing, then the order of child views initing will be incorrect;\n // to prevent this, we only exit early if the view hasn't initialized yet.\n if (!changes && !this._viewInit) {\n return;\n }\n\n changes?.forEachOperation(\n (\n item: IterableChangeRecord<T>,\n adjustedPreviousIndex: number | null,\n currentIndex: number | null,\n ) => {\n if (item.previousIndex == null) {\n this.insertNode(data[currentIndex!], currentIndex!, viewContainer, parentData);\n } else if (currentIndex == null) {\n viewContainer.remove(adjustedPreviousIndex!);\n } else {\n const view = viewContainer.get(adjustedPreviousIndex!);\n viewContainer.move(view!, currentIndex);\n }\n },\n );\n\n // If the data itself changes, but keeps the same trackBy, we need to update the templates'\n // context to reflect the new object.\n changes?.forEachIdentityChange((record: IterableChangeRecord<T>) => {\n const newData = record.item;\n if (record.currentIndex != undefined) {\n const view = viewContainer.get(record.currentIndex);\n (view as EmbeddedViewRef<any>).context.$implicit = newData;\n }\n });\n\n // Note: we only `detectChanges` from a top-level call, otherwise we risk overflowing\n // the call stack since this method is called recursively (see #29733.)\n // TODO: change to `this._changeDetectorRef.markForCheck()`,\n // or just switch this component to use signals.\n if (parentData) {\n this._changeDetectorRef.markForCheck();\n } else {\n this._changeDetectorRef.detectChanges();\n }\n }\n\n /**\n * Finds the matching node definition that should be used for this node data. If there is only\n * one node definition, it is returned. Otherwise, find the node definition that has a when\n * predicate that returns true with the data. If none return true, return the default node\n * definition.\n */\n _getNodeDef(data: T, i: number): CdkTreeNodeDef<T> {\n if (this._nodeDefs.length === 1) {\n return this._nodeDefs.first!;\n }\n\n const nodeDef =\n this._nodeDefs.find(def => def.when && def.when(i, data)) || this._defaultNodeDef;\n\n if (!nodeDef && (typeof ngDevMode === 'undefined' || ngDevMode)) {\n throw getTreeMissingMatchingNodeDefError();\n }\n\n return nodeDef!;\n }\n\n /**\n * Create the embedded view for the data node template and place it in the correct index location\n * within the data node view container.\n */\n insertNode(nodeData: T, index: number, viewContainer?: ViewContainerRef, parentData?: T) {\n const levelAccessor = this._getLevelAccessor();\n\n const node = this._getNodeDef(nodeData, index);\n const key = this._getExpansionKey(nodeData);\n\n // Node context that will be provided to created embedded view\n const context = new CdkTreeNodeOutletContext<T>(nodeData);\n\n parentData ??= this._parents.get(key) ?? undefined;\n // If the tree is flat tree, then use the `getLevel` function in flat tree control\n // Otherwise, use the level of parent node.\n if (levelAccessor) {\n context.level = levelAccessor(nodeData);\n } else if (parentData !== undefined && this._levels.has(this._getExpansionKey(parentData))) {\n context.level = this._levels.get(this._getExpansionKey(parentData))! + 1;\n } else {\n context.level = 0;\n }\n this._levels.set(key, context.level);\n\n // Use default tree nodeOutlet, or nested node's nodeOutlet\n const container = viewContainer ? viewContainer : this._nodeOutlet.viewContainer;\n container.createEmbeddedView(node.template, context, index);\n\n // Set the data to just created `CdkTreeNode`.\n // The `CdkTreeNode` created from `createEmbeddedView` will be saved in static variable\n // `mostRecentTreeNode`. We get it from static variable and pass the node data to it.\n if (CdkTreeNode.mostRecentTreeNode) {\n CdkTreeNode.mostRecentTreeNode.data = nodeData;\n }\n }\n\n /** Whether the data node is expanded or collapsed. Returns true if it's expanded. */\n isExpanded(dataNode: T): boolean {\n return !!(\n this.treeControl?.isExpanded(dataNode) ||\n this._expansionModel?.isSelected(this._getExpansionKey(dataNode))\n );\n }\n\n /** If the data node is currently expanded, collapse it. Otherwise, expand it. */\n toggle(dataNode: T): void {\n if (this.treeControl) {\n this.treeControl.toggle(dataNode);\n } else if (this._expansionModel) {\n this._expansionModel.toggle(this._getExpansionKey(dataNode));\n }\n }\n\n /** Expand the data node. If it is already expanded, does nothing. */\n expand(dataNode: T): void {\n if (this.treeControl) {\n this.treeControl.expand(dataNode);\n } else if (this._expansionModel) {\n this._expansionModel.select(this._getExpansionKey(dataNode));\n }\n }\n\n /** Collapse the data node. If it is already collapsed, does nothing. */\n collapse(dataNode: T): void {\n if (this.treeControl) {\n this.treeControl.collapse(dataNode);\n } else if (this._expansionModel) {\n this._expansionModel.deselect(this._getExpansionKey(dataNode));\n }\n }\n\n /**\n * If the data node is currently expanded, collapse it and all its descendants.\n * Otherwise, expand it and all its descendants.\n */\n toggleDescendants(dataNode: T): void {\n if (this.treeControl) {\n this.treeControl.toggleDescendants(dataNode);\n } else if (this._expansionModel) {\n if (this.isExpanded(dataNode)) {\n this.collapseDescendants(dataNode);\n } else {\n this.expandDescendants(dataNode);\n }\n }\n }\n\n /**\n * Expand the data node and all its descendants. If they are already expanded, does nothing.\n */\n expandDescendants(dataNode: T): void {\n if (this.treeControl) {\n this.treeControl.expandDescendants(dataNode);\n } else if (this._expansionModel) {\n const expansionModel = this._expansionModel;\n expansionModel.select(this._getExpansionKey(dataNode));\n this._getDescendants(dataNode)\n .pipe(take(1), takeUntil(this._onDestroy))\n .subscribe(children => {\n expansionModel.select(...children.map(child => this._getExpansionKey(child)));\n });\n }\n }\n\n /** Collapse the data node and all its descendants. If it is already collapsed, does nothing. */\n collapseDescendants(dataNode: T): void {\n if (this.treeControl) {\n this.treeControl.collapseDescendants(dataNode);\n } else if (this._expansionModel) {\n const expansionModel = this._expansionModel;\n expansionModel.deselect(this._getExpansionKey(dataNode));\n this._getDescendants(dataNode)\n .pipe(take(1), takeUntil(this._onDestroy))\n .subscribe(children => {\n expansionModel.deselect(...children.map(child => this._getExpansionKey(child)));\n });\n }\n }\n\n /** Expands all data nodes in the tree. */\n expandAll(): void {\n if (this.treeControl) {\n this.treeControl.expandAll();\n } else if (this._expansionModel) {\n this._forEachExpansionKey(keys => this._expansionModel?.select(...keys));\n }\n }\n\n /** Collapse all data nodes in the tree. */\n collapseAll(): void {\n if (this.treeControl) {\n this.treeControl.collapseAll();\n } else if (this._expansionModel) {\n this._forEachExpansionKey(keys => this._expansionModel?.deselect(...keys));\n }\n }\n\n /** Level accessor, used for compatibility between the old Tree and new Tree */\n _getLevelAccessor() {\n return this.treeControl?.getLevel?.bind(this.treeControl) ?? this.levelAccessor;\n }\n\n /** Children accessor, used for compatibility between the old Tree and new Tree */\n _getChildrenAccessor() {\n return this.treeControl?.getChildren?.bind(this.treeControl) ?? this.childrenAccessor;\n }\n\n /**\n * Gets the direct children of a node; used for compatibility between the old tree and the\n * new tree.\n */\n _getDirectChildren(dataNode: T): Observable<T[]> {\n const levelAccessor = this._getLevelAccessor();\n const expansionModel = this._expansionModel ?? this.treeControl?.expansionModel;\n if (!expansionModel) {\n return observableOf([]);\n }\n\n const key = this._getExpansionKey(dataNode);\n\n const isExpanded = expansionModel.changed.pipe(\n switchMap(changes => {\n if (changes.added.includes(key)) {\n return observableOf(true);\n } else if (changes.removed.includes(key)) {\n return observableOf(false);\n }\n return EMPTY;\n }),\n startWith(this.isExpanded(dataNode)),\n );\n\n if (levelAccessor) {\n return combineLatest([isExpanded, this._flattenedNodes]).pipe(\n map(([expanded, flattenedNodes]) => {\n if (!expanded) {\n return [];\n }\n return this._findChildrenByLevel(levelAccessor, flattenedNodes, dataNode, 1);\n }),\n );\n }\n const childrenAccessor = this._getChildrenAccessor();\n if (childrenAccessor) {\n return coerceObservable(childrenAccessor(dataNode) ?? []);\n }\n throw getTreeControlMissingError();\n }\n\n /**\n * Given the list of flattened nodes, the level accessor, and the level range within\n * which to consider children, finds the children for a given node.\n *\n * For example, for direct children, `levelDelta` would be 1. For all descendants,\n * `levelDelta` would be Infinity.\n */\n private _findChildrenByLevel(\n levelAccessor: (node: T) => number,\n flattenedNodes: readonly T[],\n dataNode: T,\n levelDelta: number,\n ): T[] {\n const key = this._getExpansionKey(dataNode);\n const startIndex = flattenedNodes.findIndex(node => this._getExpansionKey(node) === key);\n const dataNodeLevel = levelAccessor(dataNode);\n const expectedLevel = dataNodeLevel + levelDelta;\n const results: T[] = [];\n\n // Goes through flattened tree nodes in the `flattenedNodes` array, and get all\n // descendants within a certain level range.\n //\n // If we reach a node whose level is equal to or less than the level of the tree node,\n // we hit a sibling or parent's sibling, and should stop.\n for (let i = startIndex + 1; i < flattenedNodes.length; i++) {\n const currentLevel = levelAccessor(flattenedNodes[i]);\n if (currentLevel <= dataNodeLevel) {\n break;\n }\n if (currentLevel <= expectedLevel) {\n results.push(flattenedNodes[i]);\n }\n }\n return results;\n }\n\n /**\n * Adds the specified node component to the tree's internal registry.\n *\n * This primarily facilitates keyboard navigation.\n */\n _registerNode(node: CdkTreeNode<T, K>) {\n this._nodes.value.set(this._getExpansionKey(node.data), node);\n this._nodes.next(this._nodes.value);\n }\n\n /** Removes the specified node component from the tree's internal registry. */\n _unregisterNode(node: CdkTreeNode<T, K>) {\n this._nodes.value.delete(this._getExpansionKey(node.data));\n this._nodes.next(this._nodes.value);\n }\n\n /**\n * For the given node, determine the level where this node appears in the tree.\n *\n * This is intended to be used for `aria-level` but is 0-indexed.\n */\n _getLevel(node: T) {\n return this._levels.get(this._getExpansionKey(node));\n }\n\n /**\n * For the given node, determine the size of the parent's child set.\n *\n * This is intended to be used for `aria-setsize`.\n */\n _getSetSize(dataNode: T) {\n const set = this._getAriaSet(dataNode);\n return set.length;\n }\n\n /**\n * For the given node, determine the index (starting from 1) of the node in its parent's child set.\n *\n * This is intended to be used for `aria-posinset`.\n */\n _getPositionInSet(dataNode: T) {\n const set = this._getAriaSet(dataNode);\n const key = this._getExpansionKey(dataNode);\n return set.findIndex(node => this._getExpansionKey(node) === key) + 1;\n }\n\n /** Given a CdkTreeNode, gets the node that renders that node's parent's data. */\n _getNodeParent(node: CdkTreeNode<T, K>) {\n const parent = this._parents.get(this._getExpansionKey(node.data));\n return parent && this._nodes.value.get(this._getExpansionKey(parent));\n }\n\n /** Given a CdkTreeNode, gets the nodes that renders that node's child data. */\n _getNodeChildren(node: CdkTreeNode<T, K>) {\n return this._getDirectChildren(node.data).pipe(\n map(children =>\n children.reduce<CdkTreeNode<T, K>[]>((nodes, child) => {\n const value = this._nodes.value.get(this._getExpansionKey(child));\n if (value) {\n nodes.push(value);\n }\n\n return nodes;\n }, []),\n ),\n );\n }\n\n /** `keydown` event handler; this just passes the event to the `TreeKeyManager`. */\n protected _sendKeydownToKeyManager(event: KeyboardEvent): void {\n // Only handle events directly on the tree or directly on one of the nodes, otherwise\n // we risk interfering with events in the projected content (see #29828).\n if (event.target === this._elementRef.nativeElement) {\n this._keyManager.onKeydown(event);\n } else {\n const nodes = this._nodes.getValue();\n for (const [, node] of nodes) {\n if (event.target === node._elementRef.nativeElement) {\n this._keyManager.onKeydown(event);\n break;\n }\n }\n }\n }\n\n /** Gets all nested descendants of a given node. */\n private _getDescendants(dataNode: T): Observable<T[]> {\n if (this.treeControl) {\n return observableOf(this.treeControl.getDescendants(dataNode));\n }\n if (this.levelAccessor) {\n const results = this._findChildrenByLevel(\n this.levelAccessor,\n this._flattenedNodes.value,\n dataNode,\n Infinity,\n );\n return observableOf(results);\n }\n if (this.childrenAccessor) {\n return this._getAllChildrenRecursively(dataNode).pipe(\n reduce((allChildren: T[], nextChildren) => {\n allChildren.push(...nextChildren);\n return allChildren;\n }, []),\n );\n }\n throw getTreeControlMissingError();\n }\n\n /**\n * Gets all children and sub-children of the provided node.\n *\n * This will emit multiple times, in the order that the children will appear\n * in the tree, and can be combined with a `reduce` operator.\n */\n private _getAllChildrenRecursively(dataNode: T): Observable<T[]> {\n if (!this.childrenAccessor) {\n return observableOf([]);\n }\n\n return coerceObservable(this.childrenAccessor(dataNode)).pipe(\n take(1),\n switchMap(children => {\n // Here, we cache the parents of a particular child so that we can compute the levels.\n for (const child of children) {\n this._parents.set(this._getExpansionKey(child), dataNode);\n }\n return observableOf(...children).pipe(\n concatMap(child => concat(observableOf([child]), this._getAllChildrenRecursively(child))),\n );\n }),\n );\n }\n\n private _getExpansionKey(dataNode: T): K {\n // In the case that a key accessor function was not provided by the\n // tree user, we'll default to using the node object itself as the key.\n //\n // This cast is safe since:\n // - if an expansionKey is provided, TS will infer the type of K to be\n // the return type.\n // - if it's not, then K will be defaulted to T.\n return this.expansionKey?.(dataNode) ?? (dataNode as unknown as K);\n }\n\n private _getAriaSet(node: T) {\n const key = this._getExpansionKey(node);\n const parent = this._parents.get(key);\n const parentKey = parent ? this._getExpansionKey(parent) : null;\n const set = this._ariaSets.get(parentKey);\n return set ?? [node];\n }\n\n /**\n * Finds the parent for the given node. If this is a root node, this\n * returns null. If we're unable to determine the parent, for example,\n * if we don't have cached node data, this returns undefined.\n */\n private _findParentForNode(node: T, index: number, cachedNodes: readonly T[]): T | null {\n // In all cases, we have a mapping from node to level; all we need to do here is backtrack in\n // our flattened list of nodes to determine the first node that's of a level lower than the\n // provided node.\n if (!cachedNodes.length) {\n return null;\n }\n const currentLevel = this._levels.get(this._getExpansionKey(node)) ?? 0;\n for (let parentIndex = index - 1; parentIndex >= 0; parentIndex--) {\n const parentNode = cachedNodes[parentIndex];\n const parentLevel = this._levels.get(this._getExpansionKey(parentNode)) ?? 0;\n\n if (parentLevel < currentLevel) {\n return parentNode;\n }\n }\n return null;\n }\n\n /**\n * Given a set of root nodes and the current node level, flattens any nested\n * nodes into a single array.\n *\n * If any nodes are not expanded, then their children will not be added into the array.\n * This will still traverse all nested children in order to build up our internal data\n * models, but will not include them in the returned array.\n */\n private _flattenNestedNodesWithExpansion(nodes: readonly T[], level = 0): Observable<T[]> {\n const childrenAccessor = this._getChildrenAccessor();\n // If we're using a level accessor, we don't need to flatten anything.\n if (!childrenAccessor) {\n return observableOf([...nodes]);\n }\n\n return observableOf(...nodes).pipe(\n concatMap(node => {\n const parentKey = this._getExpansionKey(node);\n if (!this._parents.has(parentKey)) {\n this._parents.set(parentKey, null);\n }\n this._levels.set(parentKey, level);\n\n const children = coerceObservable(childrenAccessor(node));\n return concat(\n observableOf([node]),\n children.pipe(\n take(1),\n tap(childNodes => {\n this._ariaSets.set(parentKey, [...(childNodes ?? [])]);\n for (const child of childNodes ?? []) {\n const childKey = this._getExpansionKey(child);\n this._parents.set(childKey, node);\n this._levels.set(childKey, level + 1);\n }\n }),\n switchMap(childNodes => {\n if (!childNodes) {\n return observableOf([]);\n }\n return this._fla