1801 lines
76 KiB
JavaScript
Executable File
1801 lines
76 KiB
JavaScript
Executable File
import { SelectionModel } from './selection-model.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 { TREE_KEY_MANAGER } from './tree-key-manager.mjs';
|
|
import { Directionality } from './directionality.mjs';
|
|
import { isDataSource } from './data-source.mjs';
|
|
import { coerceObservable } from './coercion/private.mjs';
|
|
import './typeahead.mjs';
|
|
import './keycodes2.mjs';
|
|
|
|
/**
|
|
* 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: "20.2.0-next.2", ngImport: i0, type: CdkTreeNodeOutlet, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.2.0-next.2", type: CdkTreeNodeOutlet, isStandalone: true, selector: "[cdkTreeNodeOutlet]", ngImport: i0 });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", 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: "20.2.0-next.2", ngImport: i0, type: CdkTreeNodeDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.2.0-next.2", type: CdkTreeNodeDef, isStandalone: true, selector: "[cdkTreeNodeDef]", inputs: { when: ["cdkTreeNodeDefWhen", "when"] }, ngImport: i0 });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", 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._nodes.complete();
|
|
this._keyManagerNodes.complete();
|
|
this._nodeType.complete();
|
|
this._flattenedNodes.complete();
|
|
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);
|
|
context.index = index;
|
|
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: "20.2.0-next.2", ngImport: i0, type: CdkTree, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.2.0-next.2", 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: `<ng-container cdkTreeNodeOutlet></ng-container>`, isInline: true, dependencies: [{ kind: "directive", type: CdkTreeNodeOutlet, selector: "[cdkTreeNodeOutlet]" }], changeDetection: i0.ChangeDetectionStrategy.Default, encapsulation: i0.ViewEncapsulation.None });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", ngImport: i0, type: CdkTree, decorators: [{
|
|
type: Component,
|
|
args: [{
|
|
selector: 'cdk-tree',
|
|
exportAs: 'cdkTree',
|
|
template: `<ng-container cdkTreeNodeOutlet></ng-container>`,
|
|
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(), takeUntil(this._destroyed))
|
|
.pipe(takeUntil(this._destroyed))
|
|
.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: "20.2.0-next.2", ngImport: i0, type: CdkTreeNode, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "20.2.0-next.2", 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: "20.2.0-next.2", 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 `<cdk-tree>`. 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: "20.2.0-next.2", ngImport: i0, type: CdkNestedTreeNode, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.2.0-next.2", 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: "20.2.0-next.2", 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: "20.2.0-next.2", ngImport: i0, type: CdkTreeNodePadding, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "20.2.0-next.2", type: CdkTreeNodePadding, isStandalone: true, selector: "[cdkTreeNodePadding]", inputs: { level: ["cdkTreeNodePadding", "level", numberAttribute], indent: ["cdkTreeNodePaddingIndent", "indent"] }, ngImport: i0 });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", 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: "20.2.0-next.2", ngImport: i0, type: CdkTreeNodeToggle, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "20.2.0-next.2", 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: "20.2.0-next.2", 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: "20.2.0-next.2", ngImport: i0, type: CdkTreeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.2.0-next.2", 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: "20.2.0-next.2", ngImport: i0, type: CdkTreeModule });
|
|
}
|
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.0-next.2", 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
|