180 lines
5.3 KiB
TypeScript
180 lines
5.3 KiB
TypeScript
|
|
/**
|
||
|
|
* @license
|
||
|
|
* Copyright Google LLC All Rights Reserved.
|
||
|
|
*
|
||
|
|
* Use of this source code is governed by an MIT-style license that can be
|
||
|
|
* found in the LICENSE file at https://angular.dev/license
|
||
|
|
*/
|
||
|
|
|
||
|
|
import {CollectionViewer, DataSource} from '@angular/cdk/collections';
|
||
|
|
import {FlatTreeControl, TreeControl} from '@angular/cdk/tree';
|
||
|
|
import {BehaviorSubject, merge, Observable} from 'rxjs';
|
||
|
|
import {map, take} from 'rxjs/operators';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Tree flattener to convert a normal type of node to node with children & level information.
|
||
|
|
* Transform nested nodes of type `T` to flattened nodes of type `F`.
|
||
|
|
*
|
||
|
|
* For example, the input data of type `T` is nested, and contains its children data:
|
||
|
|
* SomeNode: {
|
||
|
|
* key: 'Fruits',
|
||
|
|
* children: [
|
||
|
|
* NodeOne: {
|
||
|
|
* key: 'Apple',
|
||
|
|
* },
|
||
|
|
* NodeTwo: {
|
||
|
|
* key: 'Pear',
|
||
|
|
* }
|
||
|
|
* ]
|
||
|
|
* }
|
||
|
|
* After flattener flatten the tree, the structure will become
|
||
|
|
* SomeNode: {
|
||
|
|
* key: 'Fruits',
|
||
|
|
* expandable: true,
|
||
|
|
* level: 1
|
||
|
|
* },
|
||
|
|
* NodeOne: {
|
||
|
|
* key: 'Apple',
|
||
|
|
* expandable: false,
|
||
|
|
* level: 2
|
||
|
|
* },
|
||
|
|
* NodeTwo: {
|
||
|
|
* key: 'Pear',
|
||
|
|
* expandable: false,
|
||
|
|
* level: 2
|
||
|
|
* }
|
||
|
|
* and the output flattened type is `F` with additional information.
|
||
|
|
*
|
||
|
|
* @deprecated Use MatTree#childrenAccessor and MatTreeNode#isExpandable
|
||
|
|
* instead. To be removed in a future version.
|
||
|
|
* @breaking-change 21.0.0
|
||
|
|
*/
|
||
|
|
export class MatTreeFlattener<T, F, K = F> {
|
||
|
|
constructor(
|
||
|
|
public transformFunction: (node: T, level: number) => F,
|
||
|
|
public getLevel: (node: F) => number,
|
||
|
|
public isExpandable: (node: F) => boolean,
|
||
|
|
public getChildren: (node: T) => Observable<T[]> | T[] | undefined | null,
|
||
|
|
) {}
|
||
|
|
|
||
|
|
_flattenNode(node: T, level: number, resultNodes: F[], parentMap: boolean[]): F[] {
|
||
|
|
const flatNode = this.transformFunction(node, level);
|
||
|
|
resultNodes.push(flatNode);
|
||
|
|
|
||
|
|
if (this.isExpandable(flatNode)) {
|
||
|
|
const childrenNodes = this.getChildren(node);
|
||
|
|
if (childrenNodes) {
|
||
|
|
if (Array.isArray(childrenNodes)) {
|
||
|
|
this._flattenChildren(childrenNodes, level, resultNodes, parentMap);
|
||
|
|
} else {
|
||
|
|
childrenNodes.pipe(take(1)).subscribe(children => {
|
||
|
|
this._flattenChildren(children, level, resultNodes, parentMap);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return resultNodes;
|
||
|
|
}
|
||
|
|
|
||
|
|
_flattenChildren(children: T[], level: number, resultNodes: F[], parentMap: boolean[]): void {
|
||
|
|
children.forEach((child, index) => {
|
||
|
|
let childParentMap: boolean[] = parentMap.slice();
|
||
|
|
childParentMap.push(index != children.length - 1);
|
||
|
|
this._flattenNode(child, level + 1, resultNodes, childParentMap);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Flatten a list of node type T to flattened version of node F.
|
||
|
|
* Please note that type T may be nested, and the length of `structuredData` may be different
|
||
|
|
* from that of returned list `F[]`.
|
||
|
|
*/
|
||
|
|
flattenNodes(structuredData: T[]): F[] {
|
||
|
|
let resultNodes: F[] = [];
|
||
|
|
structuredData.forEach(node => this._flattenNode(node, 0, resultNodes, []));
|
||
|
|
return resultNodes;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Expand flattened node with current expansion status.
|
||
|
|
* The returned list may have different length.
|
||
|
|
*/
|
||
|
|
expandFlattenedNodes(nodes: F[], treeControl: TreeControl<F, K>): F[] {
|
||
|
|
let results: F[] = [];
|
||
|
|
let currentExpand: boolean[] = [];
|
||
|
|
currentExpand[0] = true;
|
||
|
|
|
||
|
|
nodes.forEach(node => {
|
||
|
|
let expand = true;
|
||
|
|
for (let i = 0; i <= this.getLevel(node); i++) {
|
||
|
|
expand = expand && currentExpand[i];
|
||
|
|
}
|
||
|
|
if (expand) {
|
||
|
|
results.push(node);
|
||
|
|
}
|
||
|
|
if (this.isExpandable(node)) {
|
||
|
|
currentExpand[this.getLevel(node) + 1] = treeControl.isExpanded(node);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return results;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Data source for flat tree.
|
||
|
|
* The data source need to handle expansion/collapsion of the tree node and change the data feed
|
||
|
|
* to `MatTree`.
|
||
|
|
* The nested tree nodes of type `T` are flattened through `MatTreeFlattener`, and converted
|
||
|
|
* to type `F` for `MatTree` to consume.
|
||
|
|
*
|
||
|
|
* @deprecated Use one of levelAccessor or childrenAccessor instead. To be removed in a future
|
||
|
|
* version.
|
||
|
|
* @breaking-change 21.0.0
|
||
|
|
*/
|
||
|
|
export class MatTreeFlatDataSource<T, F, K = F> extends DataSource<F> {
|
||
|
|
private readonly _flattenedData = new BehaviorSubject<F[]>([]);
|
||
|
|
private readonly _expandedData = new BehaviorSubject<F[]>([]);
|
||
|
|
|
||
|
|
get data() {
|
||
|
|
return this._data.value;
|
||
|
|
}
|
||
|
|
set data(value: T[]) {
|
||
|
|
this._data.next(value);
|
||
|
|
this._flattenedData.next(this._treeFlattener.flattenNodes(this.data));
|
||
|
|
this._treeControl.dataNodes = this._flattenedData.value;
|
||
|
|
}
|
||
|
|
private readonly _data = new BehaviorSubject<T[]>([]);
|
||
|
|
|
||
|
|
constructor(
|
||
|
|
private _treeControl: FlatTreeControl<F, K>,
|
||
|
|
private _treeFlattener: MatTreeFlattener<T, F, K>,
|
||
|
|
initialData?: T[],
|
||
|
|
) {
|
||
|
|
super();
|
||
|
|
|
||
|
|
if (initialData) {
|
||
|
|
// Assign the data through the constructor to ensure that all of the logic is executed.
|
||
|
|
this.data = initialData;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
connect(collectionViewer: CollectionViewer): Observable<F[]> {
|
||
|
|
return merge(
|
||
|
|
collectionViewer.viewChange,
|
||
|
|
this._treeControl.expansionModel.changed,
|
||
|
|
this._flattenedData,
|
||
|
|
).pipe(
|
||
|
|
map(() => {
|
||
|
|
this._expandedData.next(
|
||
|
|
this._treeFlattener.expandFlattenedNodes(this._flattenedData.value, this._treeControl),
|
||
|
|
);
|
||
|
|
return this._expandedData.value;
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
disconnect() {
|
||
|
|
// no op
|
||
|
|
}
|
||
|
|
}
|