-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(tree): merge tree branch to master (#9796)
- Loading branch information
1 parent
4a36cf6
commit c975ca8
Showing
69 changed files
with
5,271 additions
and
1 deletion.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/** | ||
* @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.io/license | ||
*/ | ||
|
||
import {SelectionModel} from './selection'; | ||
|
||
|
||
/** | ||
* Interface for a class that can flatten hierarchical structured data and re-expand the flattened | ||
* data back into its original structure. Should be used in conjunction with the cdk-tree. | ||
*/ | ||
export interface TreeDataNodeFlattener<T> { | ||
/** Transforms a set of hierarchical structured data into a flattened data array. */ | ||
flattenNodes(structuredData: any[]): T[]; | ||
|
||
/** | ||
* Expands a flattened array of data into its hierarchical form using the provided expansion | ||
* model. | ||
*/ | ||
expandFlattenedNodes(nodes: T[], expansionModel: SelectionModel<T>): T[]; | ||
|
||
/** | ||
* Put node descendants of node in array. | ||
* If `onlyExpandable` is true, then only process expandable descendants. | ||
*/ | ||
nodeDescendents(node: T, nodes: T[], onlyExpandable: boolean); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/** | ||
* @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.io/license | ||
*/ | ||
import {SelectionModel} from '@angular/cdk/collections'; | ||
import {Observable} from 'rxjs/Observable'; | ||
import {TreeControl} from './tree-control'; | ||
|
||
/** Base tree control. It has basic toggle/expand/collapse operations on a single data node. */ | ||
export abstract class BaseTreeControl<T> implements TreeControl<T> { | ||
|
||
/** Gets a list of descendent data nodes of a subtree rooted at given data node recursively. */ | ||
abstract getDescendants(dataNode: T): T[]; | ||
|
||
/** Expands all data nodes in the tree. */ | ||
abstract expandAll(): void; | ||
|
||
/** Saved data node for `expandAll` action. */ | ||
dataNodes: T[]; | ||
|
||
/** A selection model with multi-selection to track expansion status. */ | ||
expansionModel: SelectionModel<T> = new SelectionModel<T>(true); | ||
|
||
/** Get depth of a given data node, return the level number. This is for flat tree node. */ | ||
getLevel: (dataNode: T) => number; | ||
|
||
/** | ||
* Whether the data node is expandable. Returns true if expandable. | ||
* This is for flat tree node. | ||
*/ | ||
isExpandable: (dataNode: T) => boolean; | ||
|
||
/** Gets a stream that emits whenever the given data node's children change. */ | ||
getChildren: (dataNode: T) => Observable<T[]>; | ||
|
||
/** Toggles one single data node's expanded/collapsed state. */ | ||
toggle(dataNode: T): void { | ||
this.expansionModel.toggle(dataNode); | ||
} | ||
|
||
/** Expands one single data node. */ | ||
expand(dataNode: T): void { | ||
this.expansionModel.select(dataNode); | ||
} | ||
|
||
/** Collapses one single data node. */ | ||
collapse(dataNode: T): void { | ||
this.expansionModel.deselect(dataNode); | ||
} | ||
|
||
/** Whether a given data node is expanded or not. Returns true if the data node is expanded. */ | ||
isExpanded(dataNode: T): boolean { | ||
return this.expansionModel.isSelected(dataNode); | ||
} | ||
|
||
/** Toggles a subtree rooted at `node` recursively. */ | ||
toggleDescendants(dataNode: T): void { | ||
this.expansionModel.isSelected(dataNode) | ||
? this.collapseDescendants(dataNode) | ||
: this.expandDescendants(dataNode); | ||
} | ||
|
||
/** Collapse all dataNodes in the tree. */ | ||
collapseAll(): void { | ||
this.expansionModel.clear(); | ||
} | ||
|
||
/** Expands a subtree rooted at given data node recursively. */ | ||
expandDescendants(dataNode: T): void { | ||
let toBeProcessed = [dataNode]; | ||
toBeProcessed.push(...this.getDescendants(dataNode)); | ||
this.expansionModel.select(...toBeProcessed); | ||
} | ||
|
||
/** Collapses a subtree rooted at given data node recursively. */ | ||
collapseDescendants(dataNode: T): void { | ||
let toBeProcessed = [dataNode]; | ||
toBeProcessed.push(...this.getDescendants(dataNode)); | ||
this.expansionModel.deselect(...toBeProcessed); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
import {FlatTreeControl} from './flat-tree-control'; | ||
|
||
describe('CdkFlatTreeControl', () => { | ||
let treeControl: FlatTreeControl<TestData>; | ||
let getLevel = (node: TestData) => node.level; | ||
let isExpandable = (node: TestData) => node.children && node.children.length > 0; | ||
|
||
beforeEach(() => { | ||
treeControl = new FlatTreeControl<TestData>(getLevel, isExpandable); | ||
}); | ||
|
||
describe('base tree control actions', () => { | ||
it('should be able to expand and collapse dataNodes', () => { | ||
const nodes = generateData(10, 4); | ||
const secondNode = nodes[1]; | ||
const sixthNode = nodes[5]; | ||
treeControl.dataNodes = nodes; | ||
|
||
treeControl.expand(secondNode); | ||
|
||
|
||
expect(treeControl.isExpanded(secondNode)) | ||
.toBeTruthy('Expect second node to be expanded'); | ||
expect(treeControl.expansionModel.selected) | ||
.toContain(secondNode, 'Expect second node in expansionModel'); | ||
expect(treeControl.expansionModel.selected.length) | ||
.toBe(1, 'Expect only second node in expansionModel'); | ||
|
||
treeControl.toggle(sixthNode); | ||
|
||
expect(treeControl.isExpanded(secondNode)) | ||
.toBeTruthy('Expect second node to stay expanded'); | ||
expect(treeControl.isExpanded(sixthNode)) | ||
.toBeTruthy('Expect sixth node to be expanded'); | ||
expect(treeControl.expansionModel.selected) | ||
.toContain(sixthNode, 'Expect sixth node in expansionModel'); | ||
expect(treeControl.expansionModel.selected) | ||
.toContain(secondNode, 'Expect second node in expansionModel'); | ||
expect(treeControl.expansionModel.selected.length) | ||
.toBe(2, 'Expect two dataNodes in expansionModel'); | ||
|
||
treeControl.collapse(secondNode); | ||
|
||
expect(treeControl.isExpanded(secondNode)) | ||
.toBeFalsy('Expect second node to be collapsed'); | ||
expect(treeControl.expansionModel.selected.length) | ||
.toBe(1, 'Expect one node in expansionModel'); | ||
expect(treeControl.isExpanded(sixthNode)).toBeTruthy('Expect sixth node to stay expanded'); | ||
expect(treeControl.expansionModel.selected) | ||
.toContain(sixthNode, 'Expect sixth node in expansionModel'); | ||
}); | ||
|
||
it('should return correct expandable values', () => { | ||
const nodes = generateData(10, 4); | ||
treeControl.dataNodes = nodes; | ||
|
||
for (let i = 0; i < 10; i++) { | ||
expect(treeControl.isExpandable(nodes[i])) | ||
.toBeTruthy(`Expect node[${i}] to be expandable`); | ||
|
||
for (let j = 0; j < 4; j++) { | ||
expect(treeControl.isExpandable(nodes[i].children[j])) | ||
.toBeFalsy(`Expect node[${i}]'s child[${j}] to be not expandable`); | ||
} | ||
} | ||
}); | ||
|
||
it('should return correct levels', () => { | ||
const numNodes = 10; | ||
const numChildren = 4; | ||
const numGrandChildren = 2; | ||
const nodes = generateData(numNodes, numChildren, numGrandChildren); | ||
treeControl.dataNodes = nodes; | ||
|
||
for (let i = 0; i < numNodes; i++) { | ||
expect(treeControl.getLevel(nodes[i])) | ||
.toBe(1, `Expec node[${i}]'s level to be 1`); | ||
|
||
for (let j = 0; j < numChildren; j++) { | ||
expect(treeControl.getLevel(nodes[i].children[j])) | ||
.toBe(2, `Expect node[${i}]'s child[${j}] to be not expandable`); | ||
|
||
for (let k = 0; k < numGrandChildren; k++) { | ||
expect(treeControl.getLevel(nodes[i].children[j].children[k])) | ||
.toBe(3, `Expect node[${i}]'s child[${j}] to be not expandable`); | ||
} | ||
} | ||
} | ||
}); | ||
|
||
it('should toggle descendants correctly', () => { | ||
const numNodes = 10; | ||
const numChildren = 4; | ||
const numGrandChildren = 2; | ||
const nodes = generateData(numNodes, numChildren, numGrandChildren); | ||
|
||
let data = []; | ||
flatten(nodes, data); | ||
treeControl.dataNodes = data; | ||
|
||
treeControl.expandDescendants(nodes[1]); | ||
|
||
const expandedNodesNum = 1 + numChildren + numChildren * numGrandChildren; | ||
expect(treeControl.expansionModel.selected.length) | ||
.toBe(expandedNodesNum, `Expect expanded ${expandedNodesNum} nodes`); | ||
|
||
expect(treeControl.isExpanded(nodes[1])).toBeTruthy('Expect second node to be expanded'); | ||
for (let i = 0; i < numChildren; i++) { | ||
|
||
expect(treeControl.isExpanded(nodes[1].children[i])) | ||
.toBeTruthy(`Expect second node's children to be expanded`); | ||
for (let j = 0; j < numGrandChildren; j++) { | ||
expect(treeControl.isExpanded(nodes[1].children[i].children[j])) | ||
.toBeTruthy(`Expect second node grand children to be not expanded`); | ||
} | ||
} | ||
|
||
}); | ||
|
||
it('should be able to expand/collapse all the dataNodes', () => { | ||
const numNodes = 10; | ||
const numChildren = 4; | ||
const numGrandChildren = 2; | ||
const nodes = generateData(numNodes, numChildren, numGrandChildren); | ||
let data = []; | ||
flatten(nodes, data); | ||
treeControl.dataNodes = data; | ||
|
||
treeControl.expandDescendants(nodes[1]); | ||
|
||
treeControl.collapseAll(); | ||
|
||
expect(treeControl.expansionModel.selected.length).toBe(0, `Expect no expanded nodes`); | ||
|
||
treeControl.expandAll(); | ||
|
||
const totalNumber = numNodes + numNodes * numChildren | ||
+ numNodes * numChildren * numGrandChildren; | ||
expect(treeControl.expansionModel.selected.length) | ||
.toBe(totalNumber, `Expect ${totalNumber} expanded nodes`); | ||
}); | ||
}); | ||
}); | ||
|
||
export class TestData { | ||
a: string; | ||
b: string; | ||
c: string; | ||
level: number; | ||
children: TestData[]; | ||
|
||
constructor(a: string, b: string, c: string, level: number = 1, children: TestData[] = []) { | ||
this.a = a; | ||
this.b = b; | ||
this.c = c; | ||
this.level = level; | ||
this.children = children; | ||
} | ||
} | ||
|
||
function generateData(dataLength: number, childLength: number, grandChildLength: number = 0) | ||
: TestData[] { | ||
let data = <any>[]; | ||
let nextIndex = 0; | ||
for (let i = 0; i < dataLength; i++) { | ||
let children = <any>[]; | ||
for (let j = 0; j < childLength; j++) { | ||
let grandChildren = <any>[]; | ||
for (let k = 0; k < grandChildLength; k++) { | ||
grandChildren.push(new TestData(`a_${nextIndex}`, `b_${nextIndex}`, `c_${nextIndex++}`, 3)); | ||
} | ||
children.push( | ||
new TestData(`a_${nextIndex}`, `b_${nextIndex}`, `c_${nextIndex++}`, 2, grandChildren)); | ||
} | ||
data.push(new TestData(`a_${nextIndex}`, `b_${nextIndex}`, `c_${nextIndex++}`, 1, children)); | ||
} | ||
return data; | ||
} | ||
|
||
function flatten(nodes: TestData[], data: TestData[]) { | ||
for (let node of nodes) { | ||
data.push(node); | ||
|
||
if (node.children && node.children.length > 0) { | ||
flatten(node.children, data); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
/** | ||
* @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.io/license | ||
*/ | ||
|
||
import {BaseTreeControl} from './base-tree-control'; | ||
|
||
/** Flat tree control. Able to expand/collapse a subtree recursively for flattened tree. */ | ||
export class FlatTreeControl<T> extends BaseTreeControl<T> { | ||
|
||
/** Construct with flat tree data node functions getLevel and isExpandable. */ | ||
constructor(public getLevel: (dataNode: T) => number, | ||
public isExpandable: (dataNode: T) => boolean) { | ||
super(); | ||
} | ||
|
||
/** | ||
* 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: T): T[] { | ||
const startIndex = this.dataNodes.indexOf(dataNode); | ||
const results: T[] = []; | ||
|
||
// 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(): void { | ||
this.expansionModel.select(...this.dataNodes); | ||
} | ||
} |
Oops, something went wrong.