@angular/cdk
Version:
Angular Material Component Development Kit
1,167 lines (1,160 loc) • 77.1 kB
JavaScript
import { S as SelectionModel } from './selection-model-ee9ac707.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-1212bcbe.mjs';
import { D as Directionality } from './directionality-9d44e426.mjs';
import { i as isDataSource } from './data-source-d79c6e09.mjs';
import { c as coerceObservable } from './observable-3cba8a1c.mjs';
import './typeahead-0113d27c.mjs';
import './keycodes-0e4398c6.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.0", ngImport: i0, type: CdkTreeNodeOutlet, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.0", type: CdkTreeNodeOutlet, isStandalone: true, selector: "[cdkTreeNodeOutlet]", ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", 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.0", ngImport: i0, type: CdkTreeNodeDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.0", type: CdkTreeNodeDef, isStandalone: true, selector: "[cdkTreeNodeDef]", inputs: { when: ["cdkTreeNodeDefWhen", "when"] }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", 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.0", ngImport: i0, type: CdkTree, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.0", 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: C