import { S as SelectionModel } from './selection-model-CeeHVIcP.mjs'; import { isObservable, Subject, BehaviorSubject, of, combineLatest, EMPTY, concat } from 'rxjs'; import { take, filter, takeUntil, startWith, tap, switchMap, map, reduce, concatMap, distinctUntilChanged } from 'rxjs/operators'; import * as i0 from '@angular/core'; import { InjectionToken, inject, ViewContainerRef, Directive, TemplateRef, IterableDiffers, ChangeDetectorRef, ElementRef, Component, ViewEncapsulation, ChangeDetectionStrategy, Input, ViewChild, ContentChildren, EventEmitter, booleanAttribute, Output, numberAttribute, NgModule } from '@angular/core'; import { T as TREE_KEY_MANAGER } from './tree-key-manager-KnCoIkIC.mjs'; import { D as Directionality } from './directionality-CBXD4hga.mjs'; import { i as isDataSource } from './data-source-D34wiQZj.mjs'; import { coerceObservable } from './coercion/private.mjs'; import './typeahead-9ZW4Dtsf.mjs'; import './keycodes-CpHkExLC.mjs'; import '@angular/common'; /** * Base tree control. It has basic toggle/expand/collapse operations on a single data node. * * @deprecated Use one of levelAccessor or childrenAccessor. To be removed in a future version. * @breaking-change 21.0.0 */ class BaseTreeControl { /** Saved data node for `expandAll` action. */ dataNodes; /** A selection model with multi-selection to track expansion status. */ expansionModel = new SelectionModel(true); /** * Returns the identifier by which a dataNode should be tracked, should its * reference change. * * Similar to trackBy for *ngFor */ trackBy; /** Get depth of a given data node, return the level number. This is for flat tree node. */ getLevel; /** * Whether the data node is expandable. Returns true if expandable. * This is for flat tree node. */ isExpandable; /** Gets a stream that emits whenever the given data node's children change. */ getChildren; /** Toggles one single data node's expanded/collapsed state. */ toggle(dataNode) { this.expansionModel.toggle(this._trackByValue(dataNode)); } /** Expands one single data node. */ expand(dataNode) { this.expansionModel.select(this._trackByValue(dataNode)); } /** Collapses one single data node. */ collapse(dataNode) { this.expansionModel.deselect(this._trackByValue(dataNode)); } /** Whether a given data node is expanded or not. Returns true if the data node is expanded. */ isExpanded(dataNode) { return this.expansionModel.isSelected(this._trackByValue(dataNode)); } /** Toggles a subtree rooted at `node` recursively. */ toggleDescendants(dataNode) { this.expansionModel.isSelected(this._trackByValue(dataNode)) ? this.collapseDescendants(dataNode) : this.expandDescendants(dataNode); } /** Collapse all dataNodes in the tree. */ collapseAll() { this.expansionModel.clear(); } /** Expands a subtree rooted at given data node recursively. */ expandDescendants(dataNode) { let toBeProcessed = [dataNode]; toBeProcessed.push(...this.getDescendants(dataNode)); this.expansionModel.select(...toBeProcessed.map(value => this._trackByValue(value))); } /** Collapses a subtree rooted at given data node recursively. */ collapseDescendants(dataNode) { let toBeProcessed = [dataNode]; toBeProcessed.push(...this.getDescendants(dataNode)); this.expansionModel.deselect(...toBeProcessed.map(value => this._trackByValue(value))); } _trackByValue(value) { return this.trackBy ? this.trackBy(value) : value; } } /** * Flat tree control. Able to expand/collapse a subtree recursively for flattened tree. * * @deprecated Use one of levelAccessor or childrenAccessor instead. To be removed in a future * version. * @breaking-change 21.0.0 */ class FlatTreeControl extends BaseTreeControl { getLevel; isExpandable; options; /** Construct with flat tree data node functions getLevel and isExpandable. */ constructor(getLevel, isExpandable, options) { super(); this.getLevel = getLevel; this.isExpandable = isExpandable; this.options = options; if (this.options) { this.trackBy = this.options.trackBy; } } /** * Gets a list of the data node's subtree of descendent data nodes. * * To make this working, the `dataNodes` of the TreeControl must be flattened tree nodes * with correct levels. */ getDescendants(dataNode) { const startIndex = this.dataNodes.indexOf(dataNode); const results = []; // Goes through flattened tree nodes in the `dataNodes` array, and get all descendants. // The level of descendants of a tree node must be greater than the level of the given // tree node. // If we reach a node whose level is equal to the level of the tree node, we hit a sibling. // If we reach a node whose level is greater than the level of the tree node, we hit a // sibling of an ancestor. for (let i = startIndex + 1; i < this.dataNodes.length && this.getLevel(dataNode) < this.getLevel(this.dataNodes[i]); i++) { results.push(this.dataNodes[i]); } return results; } /** * Expands all data nodes in the tree. * * To make this working, the `dataNodes` variable of the TreeControl must be set to all flattened * data nodes of the tree. */ expandAll() { this.expansionModel.select(...this.dataNodes.map(node => this._trackByValue(node))); } } /** * Nested tree control. Able to expand/collapse a subtree recursively for NestedNode type. * * @deprecated Use one of levelAccessor or childrenAccessor instead. To be removed in a future * version. * @breaking-change 21.0.0 */ class NestedTreeControl extends BaseTreeControl { getChildren; options; /** Construct with nested tree function getChildren. */ constructor(getChildren, options) { super(); this.getChildren = getChildren; this.options = options; if (this.options) { this.trackBy = this.options.trackBy; } if (this.options?.isExpandable) { this.isExpandable = this.options.isExpandable; } } /** * Expands all dataNodes in the tree. * * To make this working, the `dataNodes` variable of the TreeControl must be set to all root level * data nodes of the tree. */ expandAll() { this.expansionModel.clear(); const allNodes = this.dataNodes.reduce((accumulator, dataNode) => [...accumulator, ...this.getDescendants(dataNode), dataNode], []); this.expansionModel.select(...allNodes.map(node => this._trackByValue(node))); } /** Gets a list of descendant dataNodes of a subtree rooted at given data node recursively. */ getDescendants(dataNode) { const descendants = []; this._getDescendants(descendants, dataNode); // Remove the node itself return descendants.splice(1); } /** A helper function to get descendants recursively. */ _getDescendants(descendants, dataNode) { descendants.push(dataNode); const childrenNodes = this.getChildren(dataNode); if (Array.isArray(childrenNodes)) { childrenNodes.forEach((child) => this._getDescendants(descendants, child)); } else if (isObservable(childrenNodes)) { // TypeScript as of version 3.5 doesn't seem to treat `Boolean` like a function that // returns a `boolean` specifically in the context of `filter`, so we manually clarify that. childrenNodes.pipe(take(1), filter(Boolean)).subscribe(children => { for (const child of children) { this._getDescendants(descendants, child); } }); } } } /** * Injection token used to provide a `CdkTreeNode` to its outlet. * Used primarily to avoid circular imports. * @docs-private */ const CDK_TREE_NODE_OUTLET_NODE = new InjectionToken('CDK_TREE_NODE_OUTLET_NODE'); /** * Outlet for nested CdkNode. Put `[cdkTreeNodeOutlet]` on a tag to place children dataNodes * inside the outlet. */ class CdkTreeNodeOutlet { viewContainer = inject(ViewContainerRef); _node = inject(CDK_TREE_NODE_OUTLET_NODE, { optional: true }); constructor() { } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTreeNodeOutlet, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: CdkTreeNodeOutlet, isStandalone: true, selector: "[cdkTreeNodeOutlet]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTreeNodeOutlet, decorators: [{ type: Directive, args: [{ selector: '[cdkTreeNodeOutlet]', }] }], ctorParameters: () => [] }); /** Context provided to the tree node component. */ class CdkTreeNodeOutletContext { /** Data for the node. */ $implicit; /** Depth of the node. */ level; /** Index location of the node. */ index; /** Length of the number of total dataNodes. */ count; constructor(data) { this.$implicit = data; } } /** * Data node definition for the CdkTree. * Captures the node's template and a when predicate that describes when this node should be used. */ class CdkTreeNodeDef { /** @docs-private */ template = inject(TemplateRef); /** * Function that should return true if this node template should be used for the provided node * data and index. If left undefined, this node will be considered the default node template to * use when no other when functions return true for the data. * For every node, there must be at least one when function that passes or an undefined to * default. */ when; constructor() { } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTreeNodeDef, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: CdkTreeNodeDef, isStandalone: true, selector: "[cdkTreeNodeDef]", inputs: { when: ["cdkTreeNodeDefWhen", "when"] }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTreeNodeDef, decorators: [{ type: Directive, args: [{ selector: '[cdkTreeNodeDef]', inputs: [{ name: 'when', alias: 'cdkTreeNodeDefWhen' }], }] }], ctorParameters: () => [] }); /** * Returns an error to be thrown when there is no usable data. * @docs-private */ function getTreeNoValidDataSourceError() { return Error(`A valid data source must be provided.`); } /** * Returns an error to be thrown when there are multiple nodes that are missing a when function. * @docs-private */ function getTreeMultipleDefaultNodeDefsError() { return Error(`There can only be one default row without a when predicate function.`); } /** * Returns an error to be thrown when there are no matching node defs for a particular set of data. * @docs-private */ function getTreeMissingMatchingNodeDefError() { return Error(`Could not find a matching node definition for the provided node data.`); } /** * Returns an error to be thrown when there is no tree control. * @docs-private */ function getTreeControlMissingError() { return Error(`Could not find a tree control, levelAccessor, or childrenAccessor for the tree.`); } /** * Returns an error to be thrown when there are multiple ways of specifying children or level * provided to the tree. * @docs-private */ function getMultipleTreeControlsError() { return Error(`More than one of tree control, levelAccessor, or childrenAccessor were provided.`); } /** * 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. */ class CdkTree { _differs = inject(IterableDiffers); _changeDetectorRef = inject(ChangeDetectorRef); _elementRef = inject(ElementRef); _dir = inject(Directionality); /** Subject that emits when the component has been destroyed. */ _onDestroy = new Subject(); /** Differ used to find the changes in the data provided by the data source. */ _dataDiffer; /** Stores the node definition that does not have a when predicate. */ _defaultNodeDef; /** Data subscription */ _dataSubscription; /** Level of nodes */ _levels = new Map(); /** The immediate parents for a node. This is `null` if there is no parent. */ _parents = new Map(); /** * Nodes grouped into each set, which is a list of nodes displayed together in the DOM. * * Lookup key is the parent of a set. Root nodes have key of null. * * Values is a 'set' of tree nodes. Each tree node maps to a treeitem element. Sets are in the * order that it is rendered. Each set maps directly to aria-posinset and aria-setsize attributes. */ _ariaSets = new Map(); /** * 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. */ get dataSource() { return this._dataSource; } set dataSource(dataSource) { if (this._dataSource !== dataSource) { this._switchDataSource(dataSource); } } _dataSource; /** * The tree controller * * @deprecated Use one of `levelAccessor` or `childrenAccessor` instead. To be removed in a * future version. * @breaking-change 21.0.0 */ treeControl; /** * Given a data node, determines what tree level the node is at. * * One of levelAccessor or childrenAccessor must be specified, not both. * This is enforced at run-time. */ levelAccessor; /** * Given a data node, determines what the children of that node are. * * One of levelAccessor or childrenAccessor must be specified, not both. * This is enforced at run-time. */ childrenAccessor; /** * 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`. */ trackBy; /** * Given a data node, determines the key by which we determine whether or not this node is expanded. */ expansionKey; // Outlets within the tree's template where the dataNodes will be inserted. _nodeOutlet; /** The tree node template for the tree */ _nodeDefs; // TODO(tinayuangao): Setup a listener for scrolling, emit the calculated view to viewChange. // Remove the MAX_VALUE in viewChange /** * 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: 0, end: Number.MAX_VALUE, }); /** Keep track of which nodes are expanded. */ _expansionModel; /** * Maintain a synchronous cache of flattened data nodes. This will only be * populated after initial render, and in certain cases, will be delayed due to * relying on Observable `getChildren` calls. */ _flattenedNodes = new BehaviorSubject([]); /** The automatically determined node type for the tree. */ _nodeType = new BehaviorSubject(null); /** The mapping between data and the node that is rendered. */ _nodes = new BehaviorSubject(new Map()); /** * Synchronous cache of nodes for the `TreeKeyManager`. This is separate * from `_flattenedNodes` so they can be independently updated at different * times. */ _keyManagerNodes = new BehaviorSubject([]); _keyManagerFactory = inject(TREE_KEY_MANAGER); /** The key manager for this tree. Handles focus and activation based on user keyboard input. */ _keyManager; _viewInit = false; constructor() { } ngAfterContentInit() { this._initializeKeyManager(); } ngAfterContentChecked() { this._updateDefaultNodeDefinition(); this._subscribeToDataChanges(); } ngOnDestroy() { this._nodeOutlet.viewContainer.clear(); this.viewChange.complete(); this._onDestroy.next(); this._onDestroy.complete(); if (this._dataSource && typeof this._dataSource.disconnect === 'function') { this.dataSource.disconnect(this); } if (this._dataSubscription) { this._dataSubscription.unsubscribe(); this._dataSubscription = null; } // In certain tests, the tree might be destroyed before this is initialized // in `ngAfterContentInit`. this._keyManager?.destroy(); } ngOnInit() { this._checkTreeControlUsage(); this._initializeDataDiffer(); } ngAfterViewInit() { this._viewInit = true; } _updateDefaultNodeDefinition() { const defaultNodeDefs = this._nodeDefs.filter(def => !def.when); if (defaultNodeDefs.length > 1 && (typeof ngDevMode === 'undefined' || ngDevMode)) { throw getTreeMultipleDefaultNodeDefsError(); } this._defaultNodeDef = defaultNodeDefs[0]; } /** * Sets the node type for the tree, if it hasn't been set yet. * * This will be called by the first node that's rendered in order for the tree * to determine what data transformations are required. */ _setNodeTypeIfUnset(newType) { const currentType = this._nodeType.value; if (currentType === null) { this._nodeType.next(newType); } else if ((typeof ngDevMode === 'undefined' || ngDevMode) && currentType !== newType) { console.warn(`Tree is using conflicting node types which can cause unexpected behavior. ` + `Please use tree nodes of the same type (e.g. only flat or only nested). ` + `Current node type: "${currentType}", new node type "${newType}".`); } } /** * 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. */ _switchDataSource(dataSource) { if (this._dataSource && typeof this._dataSource.disconnect === 'function') { this.dataSource.disconnect(this); } if (this._dataSubscription) { this._dataSubscription.unsubscribe(); this._dataSubscription = null; } // Remove the all dataNodes if there is now no data source if (!dataSource) { this._nodeOutlet.viewContainer.clear(); } this._dataSource = dataSource; if (this._nodeDefs) { this._subscribeToDataChanges(); } } _getExpansionModel() { if (!this.treeControl) { this._expansionModel ??= new SelectionModel(true); return this._expansionModel; } return this.treeControl.expansionModel; } /** Set up a subscription for the data provided by the data source. */ _subscribeToDataChanges() { if (this._dataSubscription) { return; } let dataStream; if (isDataSource(this._dataSource)) { dataStream = this._dataSource.connect(this); } else if (isObservable(this._dataSource)) { dataStream = this._dataSource; } else if (Array.isArray(this._dataSource)) { dataStream = of(this._dataSource); } if (!dataStream) { if (typeof ngDevMode === 'undefined' || ngDevMode) { throw getTreeNoValidDataSourceError(); } return; } this._dataSubscription = this._getRenderData(dataStream) .pipe(takeUntil(this._onDestroy)) .subscribe(renderingData => { this._renderDataChanges(renderingData); }); } /** Given an Observable containing a stream of the raw data, returns an Observable containing the RenderingData */ _getRenderData(dataStream) { const expansionModel = this._getExpansionModel(); return combineLatest([ dataStream, this._nodeType, // We don't use the expansion data directly, however we add it here to essentially // trigger data rendering when expansion changes occur. expansionModel.changed.pipe(startWith(null), tap(expansionChanges => { this._emitExpansionChanges(expansionChanges); })), ]).pipe(switchMap(([data, nodeType]) => { if (nodeType === null) { return of({ renderNodes: data, flattenedNodes: null, nodeType }); } // If we're here, then we know what our node type is, and therefore can // perform our usual rendering pipeline, which necessitates converting the data return this._computeRenderingData(data, nodeType).pipe(map(convertedData => ({ ...convertedData, nodeType }))); })); } _renderDataChanges(data) { if (data.nodeType === null) { this.renderNodeChanges(data.renderNodes); return; } // If we're here, then we know what our node type is, and therefore can // perform our usual rendering pipeline. this._updateCachedData(data.flattenedNodes); this.renderNodeChanges(data.renderNodes); this._updateKeyManagerItems(data.flattenedNodes); } _emitExpansionChanges(expansionChanges) { if (!expansionChanges) { return; } const nodes = this._nodes.value; for (const added of expansionChanges.added) { const node = nodes.get(added); node?._emitExpansionState(true); } for (const removed of expansionChanges.removed) { const node = nodes.get(removed); node?._emitExpansionState(false); } } _initializeKeyManager() { const items = combineLatest([this._keyManagerNodes, this._nodes]).pipe(map(([keyManagerNodes, renderNodes]) => keyManagerNodes.reduce((items, data) => { const node = renderNodes.get(this._getExpansionKey(data)); if (node) { items.push(node); } return items; }, []))); const keyManagerOptions = { trackBy: node => this._getExpansionKey(node.data), skipPredicate: node => !!node.isDisabled, typeAheadDebounceInterval: true, horizontalOrientation: this._dir.value, }; this._keyManager = this._keyManagerFactory(items, keyManagerOptions); } _initializeDataDiffer() { // Provide a default trackBy based on `_getExpansionKey` if one isn't provided. const trackBy = this.trackBy ?? ((_index, item) => this._getExpansionKey(item)); this._dataDiffer = this._differs.find([]).create(trackBy); } _checkTreeControlUsage() { if (typeof ngDevMode === 'undefined' || ngDevMode) { // Verify that Tree follows API contract of using one of TreeControl, levelAccessor or // childrenAccessor. Throw an appropriate error if contract is not met. let numTreeControls = 0; if (this.treeControl) { numTreeControls++; } if (this.levelAccessor) { numTreeControls++; } if (this.childrenAccessor) { numTreeControls++; } if (!numTreeControls) { throw getTreeControlMissingError(); } else if (numTreeControls > 1) { throw getMultipleTreeControlsError(); } } } /** 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); // Some tree consumers expect change detection to propagate to nodes // even when the array itself hasn't changed; we explicitly detect changes // anyways in order for nodes to update their data. // // However, if change detection is called while the component's view is // still initing, then the order of child views initing will be incorrect; // to prevent this, we only exit early if the view hasn't initialized yet. if (!changes && !this._viewInit) { return; } changes?.forEachOperation((item, adjustedPreviousIndex, currentIndex) => { if (item.previousIndex == null) { this.insertNode(data[currentIndex], currentIndex, viewContainer, parentData); } else if (currentIndex == null) { viewContainer.remove(adjustedPreviousIndex); } else { const view = viewContainer.get(adjustedPreviousIndex); viewContainer.move(view, currentIndex); } }); // If the data itself changes, but keeps the same trackBy, we need to update the templates' // context to reflect the new object. changes?.forEachIdentityChange((record) => { const newData = record.item; if (record.currentIndex != undefined) { const view = viewContainer.get(record.currentIndex); view.context.$implicit = newData; } }); // Note: we only `detectChanges` from a top-level call, otherwise we risk overflowing // the call stack since this method is called recursively (see #29733.) // TODO: change to `this._changeDetectorRef.markForCheck()`, // or just switch this component to use signals. if (parentData) { this._changeDetectorRef.markForCheck(); } else { 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 && (typeof ngDevMode === 'undefined' || ngDevMode)) { 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 levelAccessor = this._getLevelAccessor(); const node = this._getNodeDef(nodeData, index); const key = this._getExpansionKey(nodeData); // Node context that will be provided to created embedded view const context = new CdkTreeNodeOutletContext(nodeData); parentData ??= this._parents.get(key) ?? undefined; // If the tree is flat tree, then use the `getLevel` function in flat tree control // Otherwise, use the level of parent node. if (levelAccessor) { context.level = levelAccessor(nodeData); } else if (parentData !== undefined && this._levels.has(this._getExpansionKey(parentData))) { context.level = this._levels.get(this._getExpansionKey(parentData)) + 1; } else { context.level = 0; } this._levels.set(key, context.level); // Use default tree nodeOutlet, or nested node's nodeOutlet const container = viewContainer ? viewContainer : this._nodeOutlet.viewContainer; container.createEmbeddedView(node.template, context, index); // Set the data to just created `CdkTreeNode`. // The `CdkTreeNode` created from `createEmbeddedView` will be saved in static variable // `mostRecentTreeNode`. We get it from static variable and pass the node data to it. if (CdkTreeNode.mostRecentTreeNode) { CdkTreeNode.mostRecentTreeNode.data = nodeData; } } /** Whether the data node is expanded or collapsed. Returns true if it's expanded. */ isExpanded(dataNode) { return !!(this.treeControl?.isExpanded(dataNode) || this._expansionModel?.isSelected(this._getExpansionKey(dataNode))); } /** If the data node is currently expanded, collapse it. Otherwise, expand it. */ toggle(dataNode) { if (this.treeControl) { this.treeControl.toggle(dataNode); } else if (this._expansionModel) { this._expansionModel.toggle(this._getExpansionKey(dataNode)); } } /** Expand the data node. If it is already expanded, does nothing. */ expand(dataNode) { if (this.treeControl) { this.treeControl.expand(dataNode); } else if (this._expansionModel) { this._expansionModel.select(this._getExpansionKey(dataNode)); } } /** Collapse the data node. If it is already collapsed, does nothing. */ collapse(dataNode) { if (this.treeControl) { this.treeControl.collapse(dataNode); } else if (this._expansionModel) { this._expansionModel.deselect(this._getExpansionKey(dataNode)); } } /** * If the data node is currently expanded, collapse it and all its descendants. * Otherwise, expand it and all its descendants. */ toggleDescendants(dataNode) { if (this.treeControl) { this.treeControl.toggleDescendants(dataNode); } else if (this._expansionModel) { if (this.isExpanded(dataNode)) { this.collapseDescendants(dataNode); } else { this.expandDescendants(dataNode); } } } /** * Expand the data node and all its descendants. If they are already expanded, does nothing. */ expandDescendants(dataNode) { if (this.treeControl) { this.treeControl.expandDescendants(dataNode); } else if (this._expansionModel) { const expansionModel = this._expansionModel; expansionModel.select(this._getExpansionKey(dataNode)); this._getDescendants(dataNode) .pipe(take(1), takeUntil(this._onDestroy)) .subscribe(children => { expansionModel.select(...children.map(child => this._getExpansionKey(child))); }); } } /** Collapse the data node and all its descendants. If it is already collapsed, does nothing. */ collapseDescendants(dataNode) { if (this.treeControl) { this.treeControl.collapseDescendants(dataNode); } else if (this._expansionModel) { const expansionModel = this._expansionModel; expansionModel.deselect(this._getExpansionKey(dataNode)); this._getDescendants(dataNode) .pipe(take(1), takeUntil(this._onDestroy)) .subscribe(children => { expansionModel.deselect(...children.map(child => this._getExpansionKey(child))); }); } } /** Expands all data nodes in the tree. */ expandAll() { if (this.treeControl) { this.treeControl.expandAll(); } else if (this._expansionModel) { this._forEachExpansionKey(keys => this._expansionModel?.select(...keys)); } } /** Collapse all data nodes in the tree. */ collapseAll() { if (this.treeControl) { this.treeControl.collapseAll(); } else if (this._expansionModel) { this._forEachExpansionKey(keys => this._expansionModel?.deselect(...keys)); } } /** Level accessor, used for compatibility between the old Tree and new Tree */ _getLevelAccessor() { return this.treeControl?.getLevel?.bind(this.treeControl) ?? this.levelAccessor; } /** Children accessor, used for compatibility between the old Tree and new Tree */ _getChildrenAccessor() { return this.treeControl?.getChildren?.bind(this.treeControl) ?? this.childrenAccessor; } /** * Gets the direct children of a node; used for compatibility between the old tree and the * new tree. */ _getDirectChildren(dataNode) { const levelAccessor = this._getLevelAccessor(); const expansionModel = this._expansionModel ?? this.treeControl?.expansionModel; if (!expansionModel) { return of([]); } const key = this._getExpansionKey(dataNode); const isExpanded = expansionModel.changed.pipe(switchMap(changes => { if (changes.added.includes(key)) { return of(true); } else if (changes.removed.includes(key)) { return of(false); } return EMPTY; }), startWith(this.isExpanded(dataNode))); if (levelAccessor) { return combineLatest([isExpanded, this._flattenedNodes]).pipe(map(([expanded, flattenedNodes]) => { if (!expanded) { return []; } return this._findChildrenByLevel(levelAccessor, flattenedNodes, dataNode, 1); })); } const childrenAccessor = this._getChildrenAccessor(); if (childrenAccessor) { return coerceObservable(childrenAccessor(dataNode) ?? []); } throw getTreeControlMissingError(); } /** * Given the list of flattened nodes, the level accessor, and the level range within * which to consider children, finds the children for a given node. * * For example, for direct children, `levelDelta` would be 1. For all descendants, * `levelDelta` would be Infinity. */ _findChildrenByLevel(levelAccessor, flattenedNodes, dataNode, levelDelta) { const key = this._getExpansionKey(dataNode); const startIndex = flattenedNodes.findIndex(node => this._getExpansionKey(node) === key); const dataNodeLevel = levelAccessor(dataNode); const expectedLevel = dataNodeLevel + levelDelta; const results = []; // Goes through flattened tree nodes in the `flattenedNodes` array, and get all // descendants within a certain level range. // // If we reach a node whose level is equal to or less than the level of the tree node, // we hit a sibling or parent's sibling, and should stop. for (let i = startIndex + 1; i < flattenedNodes.length; i++) { const currentLevel = levelAccessor(flattenedNodes[i]); if (currentLevel <= dataNodeLevel) { break; } if (currentLevel <= expectedLevel) { results.push(flattenedNodes[i]); } } return results; } /** * Adds the specified node component to the tree's internal registry. * * This primarily facilitates keyboard navigation. */ _registerNode(node) { this._nodes.value.set(this._getExpansionKey(node.data), node); this._nodes.next(this._nodes.value); } /** Removes the specified node component from the tree's internal registry. */ _unregisterNode(node) { this._nodes.value.delete(this._getExpansionKey(node.data)); this._nodes.next(this._nodes.value); } /** * For the given node, determine the level where this node appears in the tree. * * This is intended to be used for `aria-level` but is 0-indexed. */ _getLevel(node) { return this._levels.get(this._getExpansionKey(node)); } /** * For the given node, determine the size of the parent's child set. * * This is intended to be used for `aria-setsize`. */ _getSetSize(dataNode) { const set = this._getAriaSet(dataNode); return set.length; } /** * For the given node, determine the index (starting from 1) of the node in its parent's child set. * * This is intended to be used for `aria-posinset`. */ _getPositionInSet(dataNode) { const set = this._getAriaSet(dataNode); const key = this._getExpansionKey(dataNode); return set.findIndex(node => this._getExpansionKey(node) === key) + 1; } /** Given a CdkTreeNode, gets the node that renders that node's parent's data. */ _getNodeParent(node) { const parent = this._parents.get(this._getExpansionKey(node.data)); return parent && this._nodes.value.get(this._getExpansionKey(parent)); } /** Given a CdkTreeNode, gets the nodes that renders that node's child data. */ _getNodeChildren(node) { return this._getDirectChildren(node.data).pipe(map(children => children.reduce((nodes, child) => { const value = this._nodes.value.get(this._getExpansionKey(child)); if (value) { nodes.push(value); } return nodes; }, []))); } /** `keydown` event handler; this just passes the event to the `TreeKeyManager`. */ _sendKeydownToKeyManager(event) { // Only handle events directly on the tree or directly on one of the nodes, otherwise // we risk interfering with events in the projected content (see #29828). if (event.target === this._elementRef.nativeElement) { this._keyManager.onKeydown(event); } else { const nodes = this._nodes.getValue(); for (const [, node] of nodes) { if (event.target === node._elementRef.nativeElement) { this._keyManager.onKeydown(event); break; } } } } /** Gets all nested descendants of a given node. */ _getDescendants(dataNode) { if (this.treeControl) { return of(this.treeControl.getDescendants(dataNode)); } if (this.levelAccessor) { const results = this._findChildrenByLevel(this.levelAccessor, this._flattenedNodes.value, dataNode, Infinity); return of(results); } if (this.childrenAccessor) { return this._getAllChildrenRecursively(dataNode).pipe(reduce((allChildren, nextChildren) => { allChildren.push(...nextChildren); return allChildren; }, [])); } throw getTreeControlMissingError(); } /** * Gets all children and sub-children of the provided node. * * This will emit multiple times, in the order that the children will appear * in the tree, and can be combined with a `reduce` operator. */ _getAllChildrenRecursively(dataNode) { if (!this.childrenAccessor) { return of([]); } return coerceObservable(this.childrenAccessor(dataNode)).pipe(take(1), switchMap(children => { // Here, we cache the parents of a particular child so that we can compute the levels. for (const child of children) { this._parents.set(this._getExpansionKey(child), dataNode); } return of(...children).pipe(concatMap(child => concat(of([child]), this._getAllChildrenRecursively(child)))); })); } _getExpansionKey(dataNode) { // In the case that a key accessor function was not provided by the // tree user, we'll default to using the node object itself as the key. // // This cast is safe since: // - if an expansionKey is provided, TS will infer the type of K to be // the return type. // - if it's not, then K will be defaulted to T. return this.expansionKey?.(dataNode) ?? dataNode; } _getAriaSet(node) { const key = this._getExpansionKey(node); const parent = this._parents.get(key); const parentKey = parent ? this._getExpansionKey(parent) : null; const set = this._ariaSets.get(parentKey); return set ?? [node]; } /** * Finds the parent for the given node. If this is a root node, this * returns null. If we're unable to determine the parent, for example, * if we don't have cached node data, this returns undefined. */ _findParentForNode(node, index, cachedNodes) { // In all cases, we have a mapping from node to level; all we need to do here is backtrack in // our flattened list of nodes to determine the first node that's of a level lower than the // provided node. if (!cachedNodes.length) { return null; } const currentLevel = this._levels.get(this._getExpansionKey(node)) ?? 0; for (let parentIndex = index - 1; parentIndex >= 0; parentIndex--) { const parentNode = cachedNodes[parentIndex]; const parentLevel = this._levels.get(this._getExpansionKey(parentNode)) ?? 0; if (parentLevel < currentLevel) { return parentNode; } } return null; } /** * Given a set of root nodes and the current node level, flattens any nested * nodes into a single array. * * If any nodes are not expanded, then their children will not be added into the array. * This will still traverse all nested children in order to build up our internal data * models, but will not include them in the returned array. */ _flattenNestedNodesWithExpansion(nodes, level = 0) { const childrenAccessor = this._getChildrenAccessor(); // If we're using a level accessor, we don't need to flatten anything. if (!childrenAccessor) { return of([...nodes]); } return of(...nodes).pipe(concatMap(node => { const parentKey = this._getExpansionKey(node); if (!this._parents.has(parentKey)) { this._parents.set(parentKey, null); } this._levels.set(parentKey, level); const children = coerceObservable(childrenAccessor(node)); return concat(of([node]), children.pipe(take(1), tap(childNodes => { this._ariaSets.set(parentKey, [...(childNodes ?? [])]); for (const child of childNodes ?? []) { const childKey = this._getExpansionKey(child); this._parents.set(childKey, node); this._levels.set(childKey, level + 1); } }), switchMap(childNodes => { if (!childNodes) { return of([]); } return this._flattenNestedNodesWithExpansion(childNodes, level + 1).pipe(map(nestedNodes => (this.isExpanded(node) ? nestedNodes : []))); }))); }), reduce((results, children) => { results.push(...children); return results; }, [])); } /** * Converts children for certain tree configurations. * * This also computes parent, level, and group data. */ _computeRenderingData(nodes, nodeType) { // The only situations where we have to convert children types is when // they're mismatched; i.e. if the tree is using a childrenAccessor and the // nodes are flat, or if the tree is using a levelAccessor and the nodes are // nested. if (this.childrenAccessor && nodeType === 'flat') { // clear previously generated data so we don't keep end up retaining data overtime causing // memory leaks. this._clearPreviousCache(); // This flattens children into a single array. this._ariaSets.set(null, [...nodes]); return this._flattenNestedNodesWithExpansion(nodes).pipe(map(flattenedNodes => ({ renderNodes: flattenedNodes, flattenedNodes, }))); } else if (this.levelAccessor && nodeType === 'nested') { // In the nested case, we only look for root nodes. The CdkNestedNode // itself will handle rendering each individual node's children. const levelAccessor = this.levelAccessor; return of(nodes.filter(node => levelAccessor(node) === 0)).pipe(map(rootNodes => ({ renderNodes: rootNodes, flattenedNodes: nodes, })), tap(({ flattenedNodes }) => { this._calculateParents(flattenedNodes); })); } else if (nodeType === 'flat') { // In the case of a TreeControl, we know that the node type matches up // with the TreeControl, and so no conversions are necessary. Otherwise, // we've already confirmed that the data model matches up with the // desired node type here. return of({ renderNodes: nodes, flattenedNodes: nodes }).pipe(tap(({ flattenedNodes }) => { this._calculateParents(flattenedNodes); })); } else { // clear previously generated data so we don't keep end up retaining data overtime causing // memory leaks. this._clearPreviousCache(); // For nested nodes, we still need to perform the node flattening in order // to maintain our caches for various tree operations. this._ariaSets.set(null, [...nodes]); return this._flattenNestedNodesWithExpansion(nodes).pipe(map(flattenedNodes => ({ renderNodes: nodes, flattenedNodes, }))); } } _updateCachedData(flattenedNodes) { this._flattenedNodes.next(flattenedNodes); } _updateKeyManagerItems(flattenedNodes) { this._keyManagerNodes.next(flattenedNodes); } /** Traverse the flattened node data and compute parents, levels, and group data. */ _calculateParents(flattenedNodes) { const levelAccessor = this._getLevelAccessor(); if (!levelAccessor) { return; } // clear previously generated data so we don't keep end up retaining data overtime causing // memory leaks. this._clearPreviousCache(); for (let index = 0; index < flattenedNodes.length; index++) { const dataNode = flattenedNodes[index]; const key = this._getExpansionKey(dataNode); this._levels.set(key, levelAccessor(dataNode)); const parent = this._findParentForNode(dataNode, index, flattenedNodes); this._parents.set(key, parent); const parentKey = parent ? this._getExpansionKey(parent) : null; const group = this._ariaSets.get(parentKey) ?? []; group.splice(index, 0, dataNode); this._ariaSets.set(parentKey, group); } } /** Invokes a callback with all node expansion keys. */ _forEachExpansionKey(callback) { const toToggle = []; const observables = []; this._nodes.value.forEach(node => { toToggle.push(this._getExpansionKey(node.data)); observables.push(this._getDescendants(node.data)); }); if (observables.length > 0) { combineLatest(observables) .pipe(take(1), takeUntil(this._onDestroy)) .subscribe(results => { results.forEach(inner => inner.forEach(r => toToggle.push(this._getExpansionKey(r)))); callback(toToggle); }); } else { callback(toToggle); } } /** Clears the maps we use to store parents, level & aria-sets in. */ _clearPreviousCache() { this._parents.clear(); this._levels.clear(); this._ariaSets.clear(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTree, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.6", type: CdkTree, isStandalone: true, selector: "cdk-tree", inputs: { dataSource: "dataSource", treeControl: "treeControl", levelAccessor: "levelAccessor", childrenAccessor: "childrenAccessor", trackBy: "trackBy", expansionKey: "expansionKey" }, host: { attributes: { "role": "tree" }, listeners: { "keydown": "_sendKeydownToKeyManager($event)" }, classAttribute: "cdk-tree" }, queries: [{ propertyName: "_nodeDefs", predicate: CdkTreeNodeDef, descendants: true }], viewQueries: [{ propertyName: "_nodeOutlet", first: true, predicate: CdkTreeNodeOutlet, descendants: true, static: true }], exportAs: ["cdkTree"], ngImport: i0, template: ``, isInline: true, dependencies: [{ kind: "directive", type: CdkTreeNodeOutlet, selector: "[cdkTreeNodeOutlet]" }], changeDetection: i0.ChangeDetectionStrategy.Default, encapsulation: i0.ViewEncapsulation.None }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTree, decorators: [{ type: Component, args: [{ selector: 'cdk-tree', exportAs: 'cdkTree', template: ``, host: { 'class': 'cdk-tree', 'role': 'tree', '(keydown)': '_sendKeydownToKeyManager($event)', }, encapsulation: ViewEncapsulation.None, // The "OnPush" status for the `CdkTree` component is effectively a noop, so we are removing it. // The view for `CdkTree` consists entirely of templates declared in other views. As they are // declared elsewhere, they are checked when their declaration points are checked. // tslint:disable-next-line:validate-decorators changeDetection: ChangeDetectionStrategy.Default, imports: [CdkTreeNodeOutlet], }] }], ctorParameters: () => [], propDecorators: { dataSource: [{ type: Input }], treeControl: [{ type: Input }], levelAccessor: [{ type: Input }], childrenAccessor: [{ type: Input }], trackBy: [{ type: Input }], expansionKey: [{ type: Input }], _nodeOutlet: [{ type: ViewChild, args: [CdkTreeNodeOutlet, { static: true }] }], _nodeDefs: [{ type: ContentChildren, args: [CdkTreeNodeDef, { // We need to use `descendants: true`, because Ivy will no longer match // indirect descendants if it's left as false. descendants: true, }] }] } }); /** * Tree node for CdkTree. It contains the data in the tree node. */ class CdkTreeNode { _elementRef = inject(ElementRef); _tree = inject(CdkTree); _tabindex = -1; _type = 'flat'; /** * The role of the tree node. * * @deprecated This will be ignored; the tree will automatically determine the appropriate role * for tree node. This input will be removed in a future version. * @breaking-change 21.0.0 */ get role() { return 'treeitem'; } set role(_role) { // ignore any role setting, we handle this internally. } /** * Whether or not this node is expandable. * * If not using `FlatTreeControl`, or if `isExpandable` is not provided to * `NestedTreeControl`, this should be provided for correct node a11y. */ get isExpandable() { return this._isExpandable(); } set isExpandable(isExpandable) { this._inputIsExpandable = isExpandable; if ((this.data && !this._isExpandable) || !this._inputIsExpandable) { return; } // If the node is being set to expandable, ensure that the status of the // node is propagated if (this._inputIsExpanded) { this.expand(); } else if (this._inputIsExpanded === false) { this.collapse(); } } get isExpanded() { return this._tree.isExpanded(this._data); } set isExpanded(isExpanded) { this._inputIsExpanded = isExpanded; if (isExpanded) { this.expand(); } else { this.collapse(); } } /** * Whether or not this node is disabled. If it's disabled, then the user won't be able to focus * or activate this node. */ isDisabled; /** * The text used to locate this item during typeahead. If not specified, the `textContent` will * will be used. */ typeaheadLabel; getLabel() { return this.typeaheadLabel || this._elementRef.nativeElement.textContent?.trim() || ''; } /** This emits when the node has been programatically activated or activated by keyboard. */ activation = new EventEmitter(); /** This emits when the node's expansion status has been changed. */ expandedChange = new EventEmitter(); /** * 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 = null; /** Subject that emits when the component has been destroyed. */ _destroyed = new Subject(); /** Emits when the node's data has changed. */ _dataChanges = new Subject(); _inputIsExpandable = false; _inputIsExpanded = undefined; /** * Flag used to determine whether or not we should be focusing the actual element based on * some user interaction (click or focus). On click, we don't forcibly focus the element * since the click could trigger some other component that wants to grab its own focus * (e.g. menu, dialog). */ _shouldFocus = true; _parentNodeAriaLevel; /** The tree node's data. */ get data() { return this._data; } set data(value) { if (value !== this._data) { this._data = value; this._dataChanges.next(); } } _data; /* If leaf node, return true to not assign aria-expanded attribute */ get isLeafNode() { // If flat tree node data returns false for expandable property, it's a leaf node if (this._tree.treeControl?.isExpandable !== undefined && !this._tree.treeControl.isExpandable(this._data)) { return true; // If nested tree node data returns 0 descendants, it's a leaf node } else if (this._tree.treeControl?.isExpandable === undefined && this._tree.treeControl?.getDescendants(this._data).length === 0) { return true; } return false; } get level() { // If the tree has a levelAccessor, use it to get the level. Otherwise read the // aria-level off the parent node and use it as the level for this node (note aria-level is // 1-indexed, while this property is 0-indexed, so we don't need to increment). return this._tree._getLevel(this._data) ?? this._parentNodeAriaLevel; } /** Determines if the tree node is expandable. */ _isExpandable() { if (this._tree.treeControl) { if (this.isLeafNode) { return false; } // For compatibility with trees created using TreeControl before we added // CdkTreeNode#isExpandable. return true; } return this._inputIsExpandable; } /** * Determines the value for `aria-expanded`. * * For non-expandable nodes, this is `null`. */ _getAriaExpanded() { if (!this._isExpandable()) { return null; } return String(this.isExpanded); } /** * Determines the size of this node's parent's child set. * * This is intended to be used for `aria-setsize`. */ _getSetSize() { return this._tree._getSetSize(this._data); } /** * Determines the index (starting from 1) of this node in its parent's child set. * * This is intended to be used for `aria-posinset`. */ _getPositionInSet() { return this._tree._getPositionInSet(this._data); } _changeDetectorRef = inject(ChangeDetectorRef); constructor() { CdkTreeNode.mostRecentTreeNode = this; } ngOnInit() { this._parentNodeAriaLevel = getParentNodeAriaLevel(this._elementRef.nativeElement); this._tree ._getExpansionModel() .changed.pipe(map(() => this.isExpanded), distinctUntilChanged()) .subscribe(() => this._changeDetectorRef.markForCheck()); this._tree._setNodeTypeIfUnset(this._type); this._tree._registerNode(this); } ngOnDestroy() { // If this is the last tree node being destroyed, // clear out the reference to avoid leaking memory. if (CdkTreeNode.mostRecentTreeNode === this) { CdkTreeNode.mostRecentTreeNode = null; } this._dataChanges.complete(); this._destroyed.next(); this._destroyed.complete(); } getParent() { return this._tree._getNodeParent(this) ?? null; } getChildren() { return this._tree._getNodeChildren(this); } /** Focuses this data node. Implemented for TreeKeyManagerItem. */ focus() { this._tabindex = 0; if (this._shouldFocus) { this._elementRef.nativeElement.focus(); } this._changeDetectorRef.markForCheck(); } /** Defocus this data node. */ unfocus() { this._tabindex = -1; this._changeDetectorRef.markForCheck(); } /** Emits an activation event. Implemented for TreeKeyManagerItem. */ activate() { if (this.isDisabled) { return; } this.activation.next(this._data); } /** Collapses this data node. Implemented for TreeKeyManagerItem. */ collapse() { if (this.isExpandable) { this._tree.collapse(this._data); } } /** Expands this data node. Implemented for TreeKeyManagerItem. */ expand() { if (this.isExpandable) { this._tree.expand(this._data); } } /** Makes the node focusable. Implemented for TreeKeyManagerItem. */ makeFocusable() { this._tabindex = 0; this._changeDetectorRef.markForCheck(); } _focusItem() { if (this.isDisabled) { return; } this._tree._keyManager.focusItem(this); } _setActiveItem() { if (this.isDisabled) { return; } this._shouldFocus = false; this._tree._keyManager.focusItem(this); this._shouldFocus = true; } _emitExpansionState(expanded) { this.expandedChange.emit(expanded); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTreeNode, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.6", type: CdkTreeNode, isStandalone: true, selector: "cdk-tree-node", inputs: { role: "role", isExpandable: ["isExpandable", "isExpandable", booleanAttribute], isExpanded: "isExpanded", isDisabled: ["isDisabled", "isDisabled", booleanAttribute], typeaheadLabel: ["cdkTreeNodeTypeaheadLabel", "typeaheadLabel"] }, outputs: { activation: "activation", expandedChange: "expandedChange" }, host: { attributes: { "role": "treeitem" }, listeners: { "click": "_setActiveItem()", "focus": "_focusItem()" }, properties: { "attr.aria-expanded": "_getAriaExpanded()", "attr.aria-level": "level + 1", "attr.aria-posinset": "_getPositionInSet()", "attr.aria-setsize": "_getSetSize()", "tabindex": "_tabindex" }, classAttribute: "cdk-tree-node" }, exportAs: ["cdkTreeNode"], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTreeNode, decorators: [{ type: Directive, args: [{ selector: 'cdk-tree-node', exportAs: 'cdkTreeNode', host: { 'class': 'cdk-tree-node', '[attr.aria-expanded]': '_getAriaExpanded()', '[attr.aria-level]': 'level + 1', '[attr.aria-posinset]': '_getPositionInSet()', '[attr.aria-setsize]': '_getSetSize()', '[tabindex]': '_tabindex', 'role': 'treeitem', '(click)': '_setActiveItem()', '(focus)': '_focusItem()', }, }] }], ctorParameters: () => [], propDecorators: { role: [{ type: Input }], isExpandable: [{ type: Input, args: [{ transform: booleanAttribute }] }], isExpanded: [{ type: Input }], isDisabled: [{ type: Input, args: [{ transform: booleanAttribute }] }], typeaheadLabel: [{ type: Input, args: ['cdkTreeNodeTypeaheadLabel'] }], activation: [{ type: Output }], expandedChange: [{ type: Output }] } }); function getParentNodeAriaLevel(nodeElement) { let parent = nodeElement.parentElement; while (parent && !isNodeElement(parent)) { parent = parent.parentElement; } if (!parent) { if (typeof ngDevMode === 'undefined' || ngDevMode) { throw Error('Incorrect tree structure containing detached node.'); } else { return -1; } } else if (parent.classList.contains('cdk-nested-tree-node')) { return numberAttribute(parent.getAttribute('aria-level')); } else { // The ancestor element is the cdk-tree itself return 0; } } function isNodeElement(element) { const classList = element.classList; return !!(classList?.contains('cdk-nested-tree-node') || classList?.contains('cdk-tree')); } /** * Nested node is a child of ``. It works with nested tree. * By using `cdk-nested-tree-node` component in tree node template, children of the parent node will * be added in the `cdkTreeNodeOutlet` in tree node template. * The children of node will be automatically added to `cdkTreeNodeOutlet`. */ class CdkNestedTreeNode extends CdkTreeNode { _type = 'nested'; _differs = inject(IterableDiffers); /** Differ used to find the changes in the data provided by the data source. */ _dataDiffer; /** The children data dataNodes of current node. They will be placed in `CdkTreeNodeOutlet`. */ _children; /** The children node placeholder. */ nodeOutlet; constructor() { super(); } ngAfterContentInit() { this._dataDiffer = this._differs.find([]).create(this._tree.trackBy); this._tree ._getDirectChildren(this.data) .pipe(takeUntil(this._destroyed)) .subscribe(result => this.updateChildrenNodes(result)); this.nodeOutlet.changes .pipe(takeUntil(this._destroyed)) .subscribe(() => this.updateChildrenNodes()); } ngOnDestroy() { this._clear(); super.ngOnDestroy(); } /** Add children dataNodes to the NodeOutlet */ updateChildrenNodes(children) { const outlet = this._getNodeOutlet(); if (children) { this._children = children; } if (outlet && this._children) { const viewContainer = outlet.viewContainer; this._tree.renderNodeChanges(this._children, this._dataDiffer, viewContainer, this._data); } else { // Reset the data differ if there's no children nodes displayed this._dataDiffer.diff([]); } } /** Clear the children dataNodes. */ _clear() { const outlet = this._getNodeOutlet(); if (outlet) { outlet.viewContainer.clear(); this._dataDiffer.diff([]); } } /** Gets the outlet for the current node. */ _getNodeOutlet() { const outlets = this.nodeOutlet; // Note that since we use `descendants: true` on the query, we have to ensure // that we don't pick up the outlet of a child node by accident. return outlets && outlets.find(outlet => !outlet._node || outlet._node === this); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkNestedTreeNode, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: CdkNestedTreeNode, isStandalone: true, selector: "cdk-nested-tree-node", host: { classAttribute: "cdk-nested-tree-node" }, providers: [ { provide: CdkTreeNode, useExisting: CdkNestedTreeNode }, { provide: CDK_TREE_NODE_OUTLET_NODE, useExisting: CdkNestedTreeNode }, ], queries: [{ propertyName: "nodeOutlet", predicate: CdkTreeNodeOutlet, descendants: true }], exportAs: ["cdkNestedTreeNode"], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkNestedTreeNode, decorators: [{ type: Directive, args: [{ selector: 'cdk-nested-tree-node', exportAs: 'cdkNestedTreeNode', providers: [ { provide: CdkTreeNode, useExisting: CdkNestedTreeNode }, { provide: CDK_TREE_NODE_OUTLET_NODE, useExisting: CdkNestedTreeNode }, ], host: { 'class': 'cdk-nested-tree-node', }, }] }], ctorParameters: () => [], propDecorators: { nodeOutlet: [{ type: ContentChildren, args: [CdkTreeNodeOutlet, { // We need to use `descendants: true`, because Ivy will no longer match // indirect descendants if it's left as false. descendants: true, }] }] } }); /** Regex used to split a string on its CSS units. */ const cssUnitPattern = /([A-Za-z%]+)$/; /** * Indent for the children tree dataNodes. * This directive will add left-padding to the node to show hierarchy. */ class CdkTreeNodePadding { _treeNode = inject(CdkTreeNode); _tree = inject(CdkTree); _element = inject(ElementRef); _dir = inject(Directionality, { optional: true }); /** Current padding value applied to the element. Used to avoid unnecessarily hitting the DOM. */ _currentPadding; /** Subject that emits when the component has been destroyed. */ _destroyed = new Subject(); /** CSS units used for the indentation value. */ indentUnits = 'px'; /** The level of depth of the tree node. The padding will be `level * indent` pixels. */ get level() { return this._level; } set level(value) { this._setLevelInput(value); } _level; /** * The indent for each level. Can be a number or a CSS string. * Default number 40px from material design menu sub-menu spec. */ get indent() { return this._indent; } set indent(indent) { this._setIndentInput(indent); } _indent = 40; constructor() { this._setPadding(); this._dir?.change.pipe(takeUntil(this._destroyed)).subscribe(() => this._setPadding(true)); // In Ivy the indentation binding might be set before the tree node's data has been added, // which means that we'll miss the first render. We have to subscribe to changes in the // data to ensure that everything is up to date. this._treeNode._dataChanges.subscribe(() => this._setPadding()); } ngOnDestroy() { this._destroyed.next(); this._destroyed.complete(); } /** The padding indent value for the tree node. Returns a string with px numbers if not null. */ _paddingIndent() { const nodeLevel = (this._treeNode.data && this._tree._getLevel(this._treeNode.data)) ?? null; const level = this._level == null ? nodeLevel : this._level; return typeof level === 'number' ? `${level * this._indent}${this.indentUnits}` : null; } _setPadding(forceChange = false) { const padding = this._paddingIndent(); if (padding !== this._currentPadding || forceChange) { const element = this._element.nativeElement; const paddingProp = this._dir && this._dir.value === 'rtl' ? 'paddingRight' : 'paddingLeft'; const resetProp = paddingProp === 'paddingLeft' ? 'paddingRight' : 'paddingLeft'; element.style[paddingProp] = padding || ''; element.style[resetProp] = ''; this._currentPadding = padding; } } /** * This has been extracted to a util because of TS 4 and VE. * View Engine doesn't support property rename inheritance. * TS 4.0 doesn't allow properties to override accessors or vice-versa. * @docs-private */ _setLevelInput(value) { // Set to null as the fallback value so that _setPadding can fall back to the node level if the // consumer set the directive as `cdkTreeNodePadding=""`. We still want to take this value if // they set 0 explicitly. this._level = isNaN(value) ? null : value; this._setPadding(); } /** * This has been extracted to a util because of TS 4 and VE. * View Engine doesn't support property rename inheritance. * TS 4.0 doesn't allow properties to override accessors or vice-versa. * @docs-private */ _setIndentInput(indent) { let value = indent; let units = 'px'; if (typeof indent === 'string') { const parts = indent.split(cssUnitPattern); value = parts[0]; units = parts[1] || units; } this.indentUnits = units; this._indent = numberAttribute(value); this._setPadding(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTreeNodePadding, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.6", type: CdkTreeNodePadding, isStandalone: true, selector: "[cdkTreeNodePadding]", inputs: { level: ["cdkTreeNodePadding", "level", numberAttribute], indent: ["cdkTreeNodePaddingIndent", "indent"] }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTreeNodePadding, decorators: [{ type: Directive, args: [{ selector: '[cdkTreeNodePadding]', }] }], ctorParameters: () => [], propDecorators: { level: [{ type: Input, args: [{ alias: 'cdkTreeNodePadding', transform: numberAttribute }] }], indent: [{ type: Input, args: ['cdkTreeNodePaddingIndent'] }] } }); /** * Node toggle to expand and collapse the node. */ class CdkTreeNodeToggle { _tree = inject(CdkTree); _treeNode = inject(CdkTreeNode); /** Whether expand/collapse the node recursively. */ recursive = false; constructor() { } // Toggle the expanded or collapsed state of this node. // // Focus this node with expanding or collapsing it. This ensures that the active node will always // be visible when expanding and collapsing. _toggle() { this.recursive ? this._tree.toggleDescendants(this._treeNode.data) : this._tree.toggle(this._treeNode.data); this._tree._keyManager.focusItem(this._treeNode); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTreeNodeToggle, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.6", type: CdkTreeNodeToggle, isStandalone: true, selector: "[cdkTreeNodeToggle]", inputs: { recursive: ["cdkTreeNodeToggleRecursive", "recursive", booleanAttribute] }, host: { attributes: { "tabindex": "-1" }, listeners: { "click": "_toggle(); $event.stopPropagation();", "keydown.Enter": "_toggle(); $event.preventDefault();", "keydown.Space": "_toggle(); $event.preventDefault();" } }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTreeNodeToggle, decorators: [{ type: Directive, args: [{ selector: '[cdkTreeNodeToggle]', host: { '(click)': '_toggle(); $event.stopPropagation();', '(keydown.Enter)': '_toggle(); $event.preventDefault();', '(keydown.Space)': '_toggle(); $event.preventDefault();', 'tabindex': '-1', }, }] }], ctorParameters: () => [], propDecorators: { recursive: [{ type: Input, args: [{ alias: 'cdkTreeNodeToggleRecursive', transform: booleanAttribute }] }] } }); const EXPORTED_DECLARATIONS = [ CdkNestedTreeNode, CdkTreeNodeDef, CdkTreeNodePadding, CdkTreeNodeToggle, CdkTree, CdkTreeNode, CdkTreeNodeOutlet, ]; class CdkTreeModule { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTreeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.6", ngImport: i0, type: CdkTreeModule, imports: [CdkNestedTreeNode, CdkTreeNodeDef, CdkTreeNodePadding, CdkTreeNodeToggle, CdkTree, CdkTreeNode, CdkTreeNodeOutlet], exports: [CdkNestedTreeNode, CdkTreeNodeDef, CdkTreeNodePadding, CdkTreeNodeToggle, CdkTree, CdkTreeNode, CdkTreeNodeOutlet] }); static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTreeModule }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: CdkTreeModule, decorators: [{ type: NgModule, args: [{ imports: EXPORTED_DECLARATIONS, exports: EXPORTED_DECLARATIONS, }] }] }); export { BaseTreeControl, CDK_TREE_NODE_OUTLET_NODE, CdkNestedTreeNode, CdkTree, CdkTreeModule, CdkTreeNode, CdkTreeNodeDef, CdkTreeNodeOutlet, CdkTreeNodeOutletContext, CdkTreeNodePadding, CdkTreeNodeToggle, FlatTreeControl, NestedTreeControl, getMultipleTreeControlsError, getTreeControlMissingError, getTreeMissingMatchingNodeDefError, getTreeMultipleDefaultNodeDefsError, getTreeNoValidDataSourceError }; //# sourceMappingURL=tree.mjs.map