From 7fe64232b6d1fb7354e3357f2771c3502338a272 Mon Sep 17 00:00:00 2001 From: lskramarov Date: Fri, 16 Aug 2019 17:31:53 +0300 Subject: [PATCH] feat(tree): filtration (#UIM-14) (#195) * added base functions * added filtration with saving parents * added highlight pipe * highlight pipe moved in core * node will expand (toggle state) if the filter is not empty * fixed class name (McHighlightPipe) * fixed build * fix after review * fixed tests for tree * added test on filtration * fixed error in test * forbade expand and collapse when filter not empty --- .../cdk/tree/control/base-tree-control.ts | 18 +- .../cdk/tree/control/flat-tree-control.ts | 36 +- .../cdk/tree/control/nested-tree-control.ts | 2 +- packages/cdk/tree/control/tree-control.ts | 9 +- packages/cdk/tree/tree._spec.ts | 34 +- packages/cdk/tree/tree.ts | 4 +- packages/mosaic-dev/all/module.ts | 23 +- packages/mosaic-dev/tree/module.ts | 187 ++-- packages/mosaic-dev/tree/template.html | 20 +- .../core/highlight/_highlight-theme.scss | 10 + .../mosaic/core/highlight/highlight.pipe.ts | 11 + packages/mosaic/core/highlight/index.ts | 15 + packages/mosaic/core/public-api.ts | 1 + packages/mosaic/core/theming/_all-theme.scss | 2 + packages/mosaic/icon/_icon-theme.scss | 9 +- packages/mosaic/icon/icon.component.ts | 2 +- .../tree/data-source/flat-data-source.ts | 87 +- .../tree/data-source/nested-data-source.ts | 2 +- .../tree/{node.ts => node.directive.ts} | 0 .../tree/{padding.ts => padding.directive.ts} | 0 packages/mosaic/tree/public-api.ts | 8 +- packages/mosaic/tree/toggle.ts | 53 +- ...ree-option.ts => tree-option.component.ts} | 0 ...lection.ts => tree-selection.component.ts} | 8 +- packages/mosaic/tree/tree-selection.spec.ts | 574 +++++++++++++ packages/mosaic/tree/tree._spec.ts | 807 ------------------ packages/mosaic/tree/tree.module.ts | 13 +- packages/mosaic/tree/tree.scss | 22 +- 28 files changed, 939 insertions(+), 1018 deletions(-) create mode 100644 packages/mosaic/core/highlight/_highlight-theme.scss create mode 100644 packages/mosaic/core/highlight/highlight.pipe.ts create mode 100644 packages/mosaic/core/highlight/index.ts rename packages/mosaic/tree/{node.ts => node.directive.ts} (100%) rename packages/mosaic/tree/{padding.ts => padding.directive.ts} (100%) rename packages/mosaic/tree/{tree-option.ts => tree-option.component.ts} (100%) rename packages/mosaic/tree/{tree-selection.ts => tree-selection.component.ts} (97%) create mode 100644 packages/mosaic/tree/tree-selection.spec.ts delete mode 100644 packages/mosaic/tree/tree._spec.ts diff --git a/packages/cdk/tree/control/base-tree-control.ts b/packages/cdk/tree/control/base-tree-control.ts index 4bae25bef..304ce6d83 100644 --- a/packages/cdk/tree/control/base-tree-control.ts +++ b/packages/cdk/tree/control/base-tree-control.ts @@ -1,20 +1,22 @@ import { SelectionModel } from '@angular/cdk/collections'; -import { Observable } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; -import { ITreeControl } from './tree-control'; +import { TreeControl } from './tree-control'; /** Base tree control. It has basic toggle/expand/collapse operations on a single data node. */ -// todo здесь явно ошибка проектирования, абстрактный класс реализует функционал /* tslint:disable-next-line:naming-convention */ -export abstract class BaseTreeControl implements ITreeControl { +export abstract class BaseTreeControl implements TreeControl { - /** Saved data node for `expandAll` action. */ dataNodes: T[]; /** A selection model with multi-selection to track expansion status. */ expansionModel: SelectionModel = new SelectionModel(true); + filterModel: SelectionModel = new SelectionModel(true); + + filterValue = new BehaviorSubject(''); + /** Get depth of a given data node, return the level number. This is for flat tree node. */ getLevel: (dataNode: T) => number; @@ -35,16 +37,22 @@ export abstract class BaseTreeControl implements ITreeControl { /** Toggles one single data node's expanded/collapsed state. */ toggle(dataNode: T): void { + if (this.filterValue.value) { return; } + this.expansionModel.toggle(dataNode); } /** Expands one single data node. */ expand(dataNode: T): void { + if (this.filterValue.value) { return; } + this.expansionModel.select(dataNode); } /** Collapses one single data node. */ collapse(dataNode: T): void { + if (this.filterValue.value) { return; } + this.expansionModel.deselect(dataNode); } diff --git a/packages/cdk/tree/control/flat-tree-control.ts b/packages/cdk/tree/control/flat-tree-control.ts index 45c6c8941..37411d1ba 100644 --- a/packages/cdk/tree/control/flat-tree-control.ts +++ b/packages/cdk/tree/control/flat-tree-control.ts @@ -12,7 +12,7 @@ export class FlatTreeControl extends BaseTreeControl { /** * Gets a list of the data node's subtree of descendent data nodes. * - * To make this working, the `dataNodes` of the ITreeControl must be flattened tree nodes + * To make this working, the `dataNodes` of the TreeControl must be flattened tree nodes * with correct levels. */ getDescendants(dataNode: T): T[] { @@ -38,10 +38,42 @@ export class FlatTreeControl extends BaseTreeControl { /** * Expands all data nodes in the tree. * - * To make this working, the `dataNodes` variable of the ITreeControl must be set to all flattened + * 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); } + + getParents(node: any, result: T[]): T[] { + if (node.parent) { + result.unshift(node.parent); + + return this.getParents(node.parent, result); + } else { + return result; + } + } + + compareFunction(name: string, value: string): boolean { + return RegExp(value, 'gi').test(name); + } + + filterNodes(value: string): void { + this.filterModel.clear(); + + // todo нет возможности управлять параметром имени 'node.name' + const filteredNodes = this.dataNodes.filter((node: any) => this.compareFunction(node.name, value)); + + const filteredNodesWithTheirParents = new Set(); + filteredNodes.forEach((filteredNode) => { + this.getParents(filteredNode, []).forEach((node) => filteredNodesWithTheirParents.add(node)); + + filteredNodesWithTheirParents.add(filteredNode); + }); + + this.filterModel.select(...Array.from(filteredNodesWithTheirParents)); + + this.filterValue.next(value); + } } diff --git a/packages/cdk/tree/control/nested-tree-control.ts b/packages/cdk/tree/control/nested-tree-control.ts index e84a04a11..bd491f5b6 100644 --- a/packages/cdk/tree/control/nested-tree-control.ts +++ b/packages/cdk/tree/control/nested-tree-control.ts @@ -15,7 +15,7 @@ export class NestedTreeControl extends BaseTreeControl { /** * Expands all dataNodes in the tree. * - * To make this working, the `dataNodes` variable of the ITreeControl must be set to all root level + * To make this working, the `dataNodes` variable of the TreeControl must be set to all root level * data nodes of the tree. */ expandAll(): void { diff --git a/packages/cdk/tree/control/tree-control.ts b/packages/cdk/tree/control/tree-control.ts index b2b109238..dbf32b838 100644 --- a/packages/cdk/tree/control/tree-control.ts +++ b/packages/cdk/tree/control/tree-control.ts @@ -3,17 +3,20 @@ import { Observable } from 'rxjs'; /** - * Tree control interface. User can implement ITreeControl to expand/collapse dataNodes in the tree. - * The CDKTree will use this ITreeControl to expand/collapse a node. + * Tree control interface. User can implement TreeControl to expand/collapse dataNodes in the tree. + * The CDKTree will use this TreeControl to expand/collapse a node. * User can also use it outside the `` to control the expansion status of the tree. */ -export interface ITreeControl { +// tslint:disable-next-line:naming-convention +export interface TreeControl { /** The saved tree nodes data for `expandAll` action. */ dataNodes: T[]; /** The expansion model */ expansionModel: SelectionModel; + filterModel: SelectionModel; + /** Get depth of a given data node, return the level number. This is for flat tree node. */ getLevel(dataNode: T): number; diff --git a/packages/cdk/tree/tree._spec.ts b/packages/cdk/tree/tree._spec.ts index 9dd38b857..3d7cad02a 100644 --- a/packages/cdk/tree/tree._spec.ts +++ b/packages/cdk/tree/tree._spec.ts @@ -7,7 +7,7 @@ import { map } from 'rxjs/operators'; import { BaseTreeControl } from './control/base-tree-control'; import { FlatTreeControl } from './control/flat-tree-control'; import { NestedTreeControl } from './control/nested-tree-control'; -import { ITreeControl } from './control/tree-control'; +import { TreeControl } from './control/tree-control'; import { CdkTreeModule } from './index'; import { CdkTree } from './tree'; import { getTreeControlFunctionsMissingError } from './tree-errors'; @@ -897,7 +897,7 @@ class FakeDataSource extends DataSource { this._dataChange.next(data); } - constructor(public treeControl: ITreeControl) { + constructor(public treeControl: TreeControl) { super(); for (let i = 0; i < 3; i++) { this.addData(); @@ -1044,7 +1044,7 @@ class SimpleCdkTreeApp { getLevel = (node: TestData) => node.level; isExpandable = (node: TestData) => node.children.length > 0; - treeControl: ITreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + treeControl: TreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); dataSource: FakeDataSource | null = new FakeDataSource(this.treeControl); @@ -1065,7 +1065,7 @@ class SimpleCdkTreeApp { class NestedCdkTreeApp { getChildren = (node: TestData) => node.observableChildren; - treeControl: ITreeControl = new NestedTreeControl(this.getChildren); + treeControl: TreeControl = new NestedTreeControl(this.getChildren); dataSource: FakeDataSource | null = new FakeDataSource(this.treeControl); @@ -1091,7 +1091,7 @@ class WhenNodeNestedCdkTreeApp { getChildren = (node: TestData) => node.observableChildren; - treeControl: ITreeControl = new NestedTreeControl(this.getChildren); + treeControl: TreeControl = new NestedTreeControl(this.getChildren); dataSource: FakeDataSource | null = new FakeDataSource(this.treeControl); @@ -1116,7 +1116,7 @@ class CdkTreeAppWithToggle { getLevel = (node: TestData) => node.level; isExpandable = (node: TestData) => node.children.length > 0; - treeControl: ITreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + treeControl: TreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); dataSource: FakeDataSource | null = new FakeDataSource(this.treeControl); @ViewChild(CdkTree, {static: false}) tree: CdkTree; @@ -1140,7 +1140,7 @@ class NestedCdkTreeAppWithToggle { getChildren = (node: TestData) => node.observableChildren; - treeControl: ITreeControl = new NestedTreeControl(this.getChildren); + treeControl: TreeControl = new NestedTreeControl(this.getChildren); dataSource: FakeDataSource | null = new FakeDataSource(this.treeControl); @ViewChild(CdkTree, {static: false}) tree: CdkTree; @@ -1167,7 +1167,7 @@ class WhenNodeCdkTreeApp { getLevel = (node: TestData) => node.level; isExpandable = (node: TestData) => node.children.length > 0; - treeControl: ITreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + treeControl: TreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); dataSource: FakeDataSource | null = new FakeDataSource(this.treeControl); @@ -1189,7 +1189,7 @@ class ArrayDataSourceCdkTreeApp { getLevel = (node: TestData) => node.level; isExpandable = (node: TestData) => node.children.length > 0; - treeControl: ITreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + treeControl: TreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); dataSource: FakeDataSource = new FakeDataSource(this.treeControl); @@ -1215,7 +1215,7 @@ class ObservableDataSourceCdkTreeApp { getLevel = (node: TestData) => node.level; isExpandable = (node: TestData) => node.children.length > 0; - treeControl: ITreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + treeControl: TreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); dataSource: FakeDataSource = new FakeDataSource(this.treeControl); @@ -1240,7 +1240,7 @@ class ArrayDataSourceNestedCdkTreeApp { getChildren = (node: TestData) => node.observableChildren; - treeControl: ITreeControl = new NestedTreeControl(this.getChildren); + treeControl: TreeControl = new NestedTreeControl(this.getChildren); dataSource: FakeDataSource = new FakeDataSource(this.treeControl); @@ -1265,7 +1265,7 @@ class ObservableDataSourceNestedCdkTreeApp { getChildren = (node: TestData) => node.observableChildren; - treeControl: ITreeControl = new NestedTreeControl(this.getChildren); + treeControl: TreeControl = new NestedTreeControl(this.getChildren); dataSource: FakeDataSource = new FakeDataSource(this.treeControl); @@ -1291,7 +1291,7 @@ class NestedCdkErrorTreeApp { isExpandable = (node: TestData) => node.children.length > 0; - treeControl: ITreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + treeControl: TreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); dataSource: FakeDataSource | null = new FakeDataSource(this.treeControl); @@ -1324,7 +1324,7 @@ class FlatCdkErrorTreeApp { isExpandable = (node: TestData) => node.children.length > 0; - treeControl: ITreeControl = new FakeTreeControl(); + treeControl: TreeControl = new FakeTreeControl(); dataSource: FakeDataSource | null = new FakeDataSource(this.treeControl); @@ -1347,7 +1347,7 @@ class DepthNestedCdkTreeApp { getChildren = (node: TestData) => node.observableChildren; - treeControl: ITreeControl = new NestedTreeControl(this.getChildren); + treeControl: TreeControl = new NestedTreeControl(this.getChildren); dataSource: FakeDataSource = new FakeDataSource(this.treeControl); @@ -1384,7 +1384,7 @@ class CdkTreeAppWithTrackBy { getLevel = (node: TestData) => node.level; isExpandable = (node: TestData) => node.children.length > 0; - treeControl: ITreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + treeControl: TreeControl = new FlatTreeControl(this.getLevel, this.isExpandable); dataSource: FakeDataSource = new FakeDataSource(this.treeControl); @ViewChild(CdkTree, {static: false}) tree: CdkTree; @@ -1416,7 +1416,7 @@ class NestedCdkTreeAppWithTrackBy { getChildren = (node: TestData) => node.observableChildren; - treeControl: ITreeControl = new NestedTreeControl(this.getChildren); + treeControl: TreeControl = new NestedTreeControl(this.getChildren); dataSource: FakeDataSource = new FakeDataSource(this.treeControl); diff --git a/packages/cdk/tree/tree.ts b/packages/cdk/tree/tree.ts index d73814873..a09db5243 100644 --- a/packages/cdk/tree/tree.ts +++ b/packages/cdk/tree/tree.ts @@ -23,7 +23,7 @@ import { IFocusableOption } from '@ptsecurity/cdk/a11y'; import { BehaviorSubject, Observable, of as observableOf, Subject, Subscription } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { ITreeControl } from './control/tree-control'; +import { TreeControl } from './control/tree-control'; import { CdkTreeNodeDef, CdkTreeNodeOutletContext } from './node'; import { CdkTreeNodeOutlet } from './outlet'; import { @@ -53,7 +53,7 @@ import { export class CdkTree implements AfterContentChecked, CollectionViewer, OnDestroy, OnInit { /** The tree controller */ - @Input() treeControl: ITreeControl; + @Input() treeControl: TreeControl; /** * Tracking function that will be used to check the differences in data changes. Used similarly diff --git a/packages/mosaic-dev/all/module.ts b/packages/mosaic-dev/all/module.ts index 06a7cddb9..2545544bd 100644 --- a/packages/mosaic-dev/all/module.ts +++ b/packages/mosaic-dev/all/module.ts @@ -32,7 +32,7 @@ import { McToolTipModule } from '@ptsecurity/mosaic/tooltip'; import { McTreeFlatDataSource, McTreeFlattener, McTreeModule } from '@ptsecurity/mosaic/tree'; import { Observable, of as observableOf } from 'rxjs'; -import { FileDatabase, FileFlatNode, FileNode } from '../tree/module'; +import { buildFileTree, FileFlatNode, FileNode, DATA_OBJECT } from '../tree/module'; // Depending on whether rollup is used, moment needs to be imported differently. // Since Moment.js doesn't have a default export, we normally need to import using the `* as` @@ -57,8 +57,7 @@ const MAX_PERCENT: number = 100; selector: 'app', template: require('./template.html'), styleUrls: ['./styles.scss'], - encapsulation: ViewEncapsulation.None, - providers: [FileDatabase] + encapsulation: ViewEncapsulation.None }) export class DemoComponent { checked: boolean[] = [true, true, false]; @@ -129,21 +128,19 @@ export class DemoComponent { dataSource: McTreeFlatDataSource; treeFlattener: McTreeFlattener; - constructor(private modalService: McModalService, database: FileDatabase) { + constructor(private modalService: McModalService) { setInterval(() => { this.percent = (this.percent + STEP) % (MAX_PERCENT + STEP); }, INTERVAL); this.treeFlattener = new McTreeFlattener( - this.transformer, this._getLevel, this._isExpandable, this._getChildren + this.transformer, this.getLevel, this.isExpandable, this.getChildren ); - this.treeControl = new FlatTreeControl(this._getLevel, this._isExpandable); + this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); this.dataSource = new McTreeFlatDataSource(this.treeControl, this.treeFlattener); - database.dataChange.subscribe((data) => { - this.dataSource.data = data; - }); + this.dataSource.data = buildFileTree(DATA_OBJECT, 0); } showConfirm() { @@ -170,7 +167,7 @@ export class DemoComponent { return flatNode; } - hasChild(_: number, _nodeData: FileFlatNode) { return _nodeData.expandable; } + hasChild(_: number, nodeData: FileFlatNode) { return nodeData.expandable; } hasNestedChild(_: number, nodeData: FileNode) { return !(nodeData.type); @@ -180,11 +177,11 @@ export class DemoComponent { clearInterval(this.intervalId); } - private _getLevel(node: FileFlatNode) { return node.level; } + private getLevel(node: FileFlatNode) { return node.level; } - private _isExpandable(node: FileFlatNode) { return node.expandable; } + private isExpandable(node: FileFlatNode) { return node.expandable; } - private _getChildren = (node: FileNode): Observable => { + private getChildren = (node: FileNode): Observable => { return observableOf(node.children); } diff --git a/packages/mosaic-dev/tree/module.ts b/packages/mosaic-dev/tree/module.ts index 0294f3766..0a0ad7047 100644 --- a/packages/mosaic-dev/tree/module.ts +++ b/packages/mosaic-dev/tree/module.ts @@ -1,18 +1,19 @@ /* tslint:disable:no-console no-reserved-keywords */ -import { Component, Injectable, NgModule, ViewEncapsulation } from '@angular/core'; +import { Component, NgModule, ViewEncapsulation } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { FlatTreeControl, NestedTreeControl } from '@ptsecurity/cdk/tree'; +import { FlatTreeControl } from '@ptsecurity/cdk/tree'; import { McButtonModule } from '@ptsecurity/mosaic/button'; +import { McHighlightModule } from '@ptsecurity/mosaic/core/highlight'; +import { McFormFieldModule } from '@ptsecurity/mosaic/form-field'; import { McIconModule } from '@ptsecurity/mosaic/icon'; +import { McInputModule } from '@ptsecurity/mosaic/input'; import { McTreeFlatDataSource, McTreeFlattener, - McTreeNestedDataSource, McTreeModule } from '@ptsecurity/mosaic/tree'; -import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; export class FileNode { @@ -27,116 +28,92 @@ export class FileFlatNode { type: any; level: number; expandable: boolean; + parent: any; } -export const TREE_DATA = ` - { - "rootNode_1": "app", - "Pictures": { - "Sun": "png", - "Woods": "jpg", - "Photo Booth Library": { - "Contents": "dir", - "Pictures": "dir" +/** + * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object. + * The return value is the list of `FileNode`. + */ +export function buildFileTree(value: any, level: number): FileNode[] { + const data: any[] = []; + + for (const k of Object.keys(value)) { + const v = value[k]; + const node = new FileNode(); + + node.name = `${k}`; + + if (v === null || v === undefined) { + // no action + } else if (typeof v === 'object') { + node.children = buildFileTree(v, level + 1); + } else { + node.type = v; } - }, - "Documents": { - "Pictures": "Pictures", - "angular": { - "src": { - "core": "ts", - "compiler": "ts" - } - }, - "material2": { - "src": { - "button": "ts", - "checkbox": "ts", - "input": "ts" - } - } - }, - "Downloads": { - "Tutorial": "html", - "November": "pdf", - "October": "pdf" - }, - "Applications": { - "Chrome": "app", - "Calendar": "app", - "Webstorm": "app" - } -}`; - -@Injectable() -export class FileDatabase { - dataChange: BehaviorSubject = new BehaviorSubject([]); - get data(): FileNode[] { return this.dataChange.value; } - - constructor() { - this.initialize(); + data.push(node); } - initialize() { - // Parse the string to json object. - const dataObject = JSON.parse(TREE_DATA); - - // Build the tree nodes from Json object. The result is a list of `FileNode` with nested - // file node as children. - const data = this.buildFileTree(dataObject, 0); - - // Notify the change. - this.dataChange.next(data); - } + return data; +} - /** - * Build the file structure tree. The `value` is the Json object, or a sub-tree of a Json object. - * The return value is the list of `FileNode`. - */ - buildFileTree(value: any, level: number): FileNode[] { - const data: any[] = []; - - for (const k of Object.keys(value)) { - const v = value[k]; - const node = new FileNode(); - - node.name = `${k}`; - - if (v === null || v === undefined) { - // no action - } else if (typeof v === 'object') { - node.children = this.buildFileTree(v, level + 1); - } else { - node.type = v; +export const DATA_OBJECT = { + rootNode_1: 'app', + Pictures: { + Sun: 'png', + Woods: 'jpg', + PhotoBoothLibrary: { + Contents: 'dir', + Pictures: 'dir' + } + }, + Documents: { + Pictures: 'Pictures', + angular: { + src: { + core: 'ts', + compiler: 'ts' + } + }, + material2: { + src: { + button: 'ts', + checkbox: 'ts', + input: 'ts' } - - data.push(node); } - - return data; + }, + Downloads: { + Tutorial: 'html', + November: 'pdf', + October: 'pdf' + }, + Applications: { + Chrome: 'app', + Calendar: 'app', + Webstorm: 'app' } -} +}; @Component({ selector: 'app', template: require('./template.html'), styleUrls: ['./styles.scss'], - encapsulation: ViewEncapsulation.None, - providers: [FileDatabase] + encapsulation: ViewEncapsulation.None }) export class DemoComponent { treeControl: FlatTreeControl; - dataSource: McTreeFlatDataSource; treeFlattener: McTreeFlattener; - nestedTreeControl: NestedTreeControl; - nestedDataSource: McTreeNestedDataSource; + dataSource: McTreeFlatDataSource; + + filterValue: string = ''; modelValue: any[] = ['rootNode_1', 'Documents', 'Calendar', 'Chrome']; - constructor(database: FileDatabase) { + constructor() { this.treeFlattener = new McTreeFlattener( this.transformer, this.getLevel, this.isExpandable, this.getChildren ); @@ -144,19 +121,18 @@ export class DemoComponent { this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); this.dataSource = new McTreeFlatDataSource(this.treeControl, this.treeFlattener); - this.nestedTreeControl = new NestedTreeControl(this.getChildren); - this.nestedDataSource = new McTreeNestedDataSource(); + this.dataSource.data = buildFileTree(DATA_OBJECT, 0); + } - database.dataChange.subscribe((data) => { - this.dataSource.data = data; - this.nestedDataSource.data = data; - }); + onFilterChange(value): void { + this.treeControl.filterNodes(value); } - transformer(node: FileNode, level: number) { + transformer(node: FileNode, level: number, parent: any) { const flatNode = new FileFlatNode(); flatNode.name = node.name; + flatNode.parent = parent; flatNode.type = node.type; flatNode.level = level; flatNode.expandable = !!node.children; @@ -178,12 +154,16 @@ export class DemoComponent { console.log('onSelectionChange'); } - private getLevel(node: FileFlatNode) { return node.level; } + private getLevel(node: FileFlatNode) { + return node.level; + } - private isExpandable(node: FileFlatNode) { return node.expandable; } + private isExpandable(node: FileFlatNode) { + return node.expandable; + } - private getChildren = (node: FileNode): Observable => { - return observableOf(node.children); + private getChildren = (node: FileNode): FileNode[] => { + return node.children; } } @@ -193,9 +173,12 @@ export class DemoComponent { imports: [ BrowserModule, FormsModule, + McFormFieldModule, + McInputModule, McButtonModule, McTreeModule, - McIconModule + McIconModule, + McHighlightModule ], bootstrap: [DemoComponent] }) diff --git a/packages/mosaic-dev/tree/template.html b/packages/mosaic-dev/tree/template.html index 01b7b81b9..758047b43 100644 --- a/packages/mosaic-dev/tree/template.html +++ b/packages/mosaic-dev/tree/template.html @@ -4,7 +4,6 @@ height: 328px; } -
multiple selection
@@ -14,6 +13,11 @@
multiple selection



+ + + + +


@@ -28,7 +32,7 @@
multiple selection
*mcTreeNodeDef="let node" [value]="node.name" mcTreeNodePadding> - {{ node.name }} + multiple selection [value]="node.name" mcTreeNodePadding [disabled]="node.name === 'Downloads'"> - - - {{ node.name }}: {{ node.type }} + + + + - -
-
multipleSelected: {{ multipleSelected }}
diff --git a/packages/mosaic/core/highlight/_highlight-theme.scss b/packages/mosaic/core/highlight/_highlight-theme.scss new file mode 100644 index 000000000..f7be3413c --- /dev/null +++ b/packages/mosaic/core/highlight/_highlight-theme.scss @@ -0,0 +1,10 @@ + + +@mixin mc-highlight-theme($theme) { + .mc-highlight { + color: inherit; + + font-weight: bold; + background-color: transparent; + } +} diff --git a/packages/mosaic/core/highlight/highlight.pipe.ts b/packages/mosaic/core/highlight/highlight.pipe.ts new file mode 100644 index 000000000..e6378ef2c --- /dev/null +++ b/packages/mosaic/core/highlight/highlight.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; + + +@Pipe({ name: 'mcHighlight' }) +export class McHighlightPipe implements PipeTransform { + transform(value: any, args: any): any { + if (!args) { return value; } + + return value.replace(new RegExp(`(${args})`, 'gi'), '$1'); + } +} diff --git a/packages/mosaic/core/highlight/index.ts b/packages/mosaic/core/highlight/index.ts new file mode 100644 index 000000000..5afb55588 --- /dev/null +++ b/packages/mosaic/core/highlight/index.ts @@ -0,0 +1,15 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { McHighlightPipe } from './highlight.pipe'; + + +@NgModule({ + imports: [CommonModule], + exports: [McHighlightPipe], + declarations: [McHighlightPipe] +}) +export class McHighlightModule {} + + +export * from './highlight.pipe'; diff --git a/packages/mosaic/core/public-api.ts b/packages/mosaic/core/public-api.ts index f142ca3bc..f7e995478 100644 --- a/packages/mosaic/core/public-api.ts +++ b/packages/mosaic/core/public-api.ts @@ -9,3 +9,4 @@ export * from './label/label-options'; export * from './animation/index'; export * from './overlay/overlay-position-map'; export * from './select/index'; +export * from './highlight/index'; diff --git a/packages/mosaic/core/theming/_all-theme.scss b/packages/mosaic/core/theming/_all-theme.scss index 0a61572d1..51748c9a4 100644 --- a/packages/mosaic/core/theming/_all-theme.scss +++ b/packages/mosaic/core/theming/_all-theme.scss @@ -33,6 +33,7 @@ @import '../../tree/tree-theme'; @import '../../vertical-navbar/vertical-navbar-theme'; @import '../option/option-theme'; +@import '../highlight/highlight-theme'; @import '../visual/panel-theme'; @@ -74,4 +75,5 @@ @include mc-tree-select-theme($theme); @include mc-tree-theme($theme); @include mc-vertical-navbar-theme($theme); + @include mc-highlight-theme($theme); } diff --git a/packages/mosaic/icon/_icon-theme.scss b/packages/mosaic/icon/_icon-theme.scss index da19b6801..7f10530b2 100644 --- a/packages/mosaic/icon/_icon-theme.scss +++ b/packages/mosaic/icon/_icon-theme.scss @@ -46,8 +46,13 @@ } } - .mc-icon:not(.mc-primary):not(.mc-second):not(.mc-error) { - color: map-get($foreground, text); + &:not(.mc-primary):not(.mc-second):not(.mc-error) { + &[disabled], + &.mc-disabled { + color: mc-color($second); + + cursor: default; + } } } diff --git a/packages/mosaic/icon/icon.component.ts b/packages/mosaic/icon/icon.component.ts index 0e4d8e053..f51ad3af9 100644 --- a/packages/mosaic/icon/icon.component.ts +++ b/packages/mosaic/icon/icon.component.ts @@ -39,7 +39,7 @@ export class McIcon extends _McIconMixinBase implements CanColor { elementRef.nativeElement.classList.add(iconName); } - _getHostElement() { + getHostElement() { return this._elementRef.nativeElement; } } diff --git a/packages/mosaic/tree/data-source/flat-data-source.ts b/packages/mosaic/tree/data-source/flat-data-source.ts index ee1f3f05d..d5c2e7c07 100644 --- a/packages/mosaic/tree/data-source/flat-data-source.ts +++ b/packages/mosaic/tree/data-source/flat-data-source.ts @@ -1,5 +1,5 @@ import { CollectionViewer, DataSource } from '@angular/cdk/collections'; -import { FlatTreeControl, ITreeControl } from '@ptsecurity/cdk/tree'; +import { FlatTreeControl, TreeControl } from '@ptsecurity/cdk/tree'; import { BehaviorSubject, merge, Observable } from 'rxjs'; import { map, take } from 'rxjs/operators'; @@ -40,32 +40,41 @@ import { map, take } from 'rxjs/operators'; */ export class McTreeFlattener { constructor( - public transformFunction: (node: T, level: number) => F, + public transformFunction: (node: T, level: number, parent: F | null) => F, public getLevel: (node: F) => number, public isExpandable: (node: F) => boolean, - public getChildren: (node: T) => Observable + public getChildren: (node: T) => Observable | T[] | undefined | null ) {} - flattenNode(node: T, level: number, resultNodes: F[], parentMap: boolean[]): F[] { - const flatNode = this.transformFunction(node, level); + flattenNode(node: T, level: number, resultNodes: F[], parent: F | null): F[] { + const flatNode = this.transformFunction(node, level, parent); resultNodes.push(flatNode); if (this.isExpandable(flatNode)) { - this.getChildren(node) - .pipe(take(1)) - .subscribe((children) => { - children.forEach((child, index) => { - const childParentMap: boolean[] = parentMap.slice(); - childParentMap.push(index !== children.length - 1); - - this.flattenNode(child, level + 1, resultNodes, childParentMap); - }); - }); + const childrenNodes = this.getChildren(node); + + if (childrenNodes) { + if (Array.isArray(childrenNodes)) { + this.flattenChildren(childrenNodes, level, resultNodes, flatNode); + } else { + childrenNodes + .pipe(take(1)) + .subscribe((children) => { + this.flattenChildren(children, level, resultNodes, flatNode); + }); + } + } } return resultNodes; } + flattenChildren(children: T[], level: number, resultNodes: F[], parent: F | null): void { + children.forEach((child) => { + this.flattenNode(child, level + 1, resultNodes, parent); + }); + } + /** * 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 @@ -73,7 +82,7 @@ export class McTreeFlattener { */ flattenNodes(structuredData: T[]): F[] { const resultNodes: F[] = []; - structuredData.forEach((node) => this.flattenNode(node, 0, resultNodes, [])); + structuredData.forEach((node) => this.flattenNode(node, 0, resultNodes, null)); return resultNodes; } @@ -82,7 +91,7 @@ export class McTreeFlattener { * Expand flattened node with current expansion status. * The returned list may have different length. */ - expandFlattenedNodes(nodes: F[], treeControl: ITreeControl): F[] { + expandFlattenedNodes(nodes: F[], treeControl: TreeControl): F[] { const results: F[] = []; const currentExpand: boolean[] = []; currentExpand[0] = true; @@ -104,6 +113,10 @@ export class McTreeFlattener { } } +enum McTreeDataSourceChangeTypes { + Expansion = 'expansion', + Filter = 'filter' +} /** * Data source for flat tree. @@ -117,6 +130,8 @@ export class McTreeFlatDataSource extends DataSource { expandedData = new BehaviorSubject([]); + filteredData = new BehaviorSubject([]); + get data() { return this._data.value; } @@ -141,20 +156,38 @@ export class McTreeFlatDataSource extends DataSource { } connect(collectionViewer: CollectionViewer): Observable { - const changes = [ + return merge( collectionViewer.viewChange, - this.treeControl.expansionModel.changed, + this.treeControl.expansionModel.changed + .pipe(map((value) => ({ type: McTreeDataSourceChangeTypes.Expansion, value }))), + this.treeControl.filterValue + .pipe(map((value) => ({ type: McTreeDataSourceChangeTypes.Filter, value }))), this.flattenedData - ]; + ) + .pipe(map((changeObj: any): any => { + if (changeObj.type === McTreeDataSourceChangeTypes.Filter) { + if (changeObj.value && changeObj.value.length > 0) { + return this.filterHandler(); + } else { + return this.expansionHandler(); + } + } + + return this.expansionHandler(); + })); + } + + filterHandler(): F[] { + this.filteredData.next(this.treeControl.filterModel.selected); + + return this.filteredData.value; + } - return merge(...changes) - .pipe(map(() => { - this.expandedData.next( - this.treeFlattener.expandFlattenedNodes(this.flattenedData.value, this.treeControl) - ); + expansionHandler(): F[] { + const expandedNodes = this.treeFlattener.expandFlattenedNodes(this.flattenedData.value, this.treeControl); + this.expandedData.next(expandedNodes); - return this.expandedData.value; - })); + return this.expandedData.value; } disconnect() { diff --git a/packages/mosaic/tree/data-source/nested-data-source.ts b/packages/mosaic/tree/data-source/nested-data-source.ts index c6836471b..b55e0c8dc 100644 --- a/packages/mosaic/tree/data-source/nested-data-source.ts +++ b/packages/mosaic/tree/data-source/nested-data-source.ts @@ -7,7 +7,7 @@ import { map } from 'rxjs/operators'; * Data source for nested tree. * * The data source for nested tree doesn't have to consider node flattener, or the way to expand - * or collapse. The expansion/collapsion will be handled by ITreeControl and each non-leaf node. + * or collapse. The expansion/collapsion will be handled by TreeControl and each non-leaf node. */ export class McTreeNestedDataSource extends DataSource { diff --git a/packages/mosaic/tree/node.ts b/packages/mosaic/tree/node.directive.ts similarity index 100% rename from packages/mosaic/tree/node.ts rename to packages/mosaic/tree/node.directive.ts diff --git a/packages/mosaic/tree/padding.ts b/packages/mosaic/tree/padding.directive.ts similarity index 100% rename from packages/mosaic/tree/padding.ts rename to packages/mosaic/tree/padding.directive.ts diff --git a/packages/mosaic/tree/public-api.ts b/packages/mosaic/tree/public-api.ts index b9870748a..285edeca2 100644 --- a/packages/mosaic/tree/public-api.ts +++ b/packages/mosaic/tree/public-api.ts @@ -1,8 +1,8 @@ export * from './tree.module'; -export * from './node'; -export * from './padding'; +export * from './node.directive'; +export * from './padding.directive'; export * from './toggle'; -export * from './tree-selection'; -export * from './tree-option'; +export * from './tree-selection.component'; +export * from './tree-option.component'; export * from './data-source/flat-data-source'; export * from './data-source/nested-data-source'; diff --git a/packages/mosaic/tree/toggle.ts b/packages/mosaic/tree/toggle.ts index 284b62ecb..2ae12f742 100644 --- a/packages/mosaic/tree/toggle.ts +++ b/packages/mosaic/tree/toggle.ts @@ -1,12 +1,55 @@ -import { Directive } from '@angular/core'; -import { CdkTreeNodeToggle } from '@ptsecurity/cdk/tree'; +import { Component, Directive, Input, ViewEncapsulation } from '@angular/core'; +import { BaseTreeControl, CdkTree, CdkTreeNode, CdkTreeNodeToggle } from '@ptsecurity/cdk/tree'; +@Component({ + selector: 'mc-tree-node-toggle', + template: ` + + `, + host: { + class: 'mc-tree-node-toggle', + '(click)': 'toggle($event)', + '[class.mc-disabled]': 'disabled', + '[class.mc-opened]': 'iconState' + }, + encapsulation: ViewEncapsulation.None, + providers: [{ provide: CdkTreeNodeToggle, useExisting: McTreeNodeToggleComponent }] +}) +export class McTreeNodeToggleComponent extends CdkTreeNodeToggle { + disabled: boolean = false; + + @Input() node: T; + + get iconState(): any { + return this.disabled || this.tree.treeControl.isExpanded(this.node); + } + + constructor(protected tree: CdkTree, protected treeNode: CdkTreeNode) { + super(tree, treeNode); + + // todo может пересмотреть, как то не очень + (this.tree.treeControl as BaseTreeControl).filterValue + .subscribe((value: string) => { this.disabled = value.length > 0; }); + } +} + @Directive({ selector: '[mcTreeNodeToggle]', host: { - '(click)': 'toggle($event)' + '(click)': 'toggle($event)', + '[class.mc-disabled]': 'disabled' }, - providers: [{ provide: CdkTreeNodeToggle, useExisting: McTreeNodeToggle }] + providers: [{ provide: CdkTreeNodeToggle, useExisting: McTreeNodeToggleDirective }] }) -export class McTreeNodeToggle extends CdkTreeNodeToggle {} +export class McTreeNodeToggleDirective extends CdkTreeNodeToggle { + disabled: boolean = false; + + constructor(protected tree: CdkTree, protected treeNode: CdkTreeNode) { + super(tree, treeNode); + + // todo может пересмотреть, как то не очень + (this.tree.treeControl as BaseTreeControl).filterValue + .subscribe((value: string) => { this.disabled = value.length > 0; }); + } +} diff --git a/packages/mosaic/tree/tree-option.ts b/packages/mosaic/tree/tree-option.component.ts similarity index 100% rename from packages/mosaic/tree/tree-option.ts rename to packages/mosaic/tree/tree-option.component.ts diff --git a/packages/mosaic/tree/tree-selection.ts b/packages/mosaic/tree/tree-selection.component.ts similarity index 97% rename from packages/mosaic/tree/tree-selection.ts rename to packages/mosaic/tree/tree-selection.component.ts index 7630f12c0..b385785b0 100644 --- a/packages/mosaic/tree/tree-selection.ts +++ b/packages/mosaic/tree/tree-selection.component.ts @@ -41,7 +41,7 @@ import { import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { MC_TREE_OPTION_PARENT_COMPONENT, McTreeOption } from './tree-option'; +import { MC_TREE_OPTION_PARENT_COMPONENT, McTreeOption } from './tree-option.component'; export class McTreeNavigationChange { @@ -321,7 +321,11 @@ export class McTreeSelection extends CdkTree arrayOfInstances.push(nodeData.instance as never); - setTimeout(() => nodeData.instance.changeDetectorRef.detectChanges()); + setTimeout(() => { + if (!nodeData.instance.changeDetectorRef.destroyed) { + nodeData.instance.changeDetectorRef.detectChanges(); + } + }); } }); }); diff --git a/packages/mosaic/tree/tree-selection.spec.ts b/packages/mosaic/tree/tree-selection.spec.ts new file mode 100644 index 000000000..2c079e978 --- /dev/null +++ b/packages/mosaic/tree/tree-selection.spec.ts @@ -0,0 +1,574 @@ +/* tslint:disable:no-magic-numbers max-func-body-length no-reserved-keywords */ +import { Component, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FlatTreeControl } from '@ptsecurity/cdk/tree'; + +import { + McTreeSelection, + McTreeFlatDataSource, + McTreeFlattener, + McTreeModule +} from './index'; + + +describe('McTreeSelection', () => { + let treeElement: HTMLElement; + + function configureMatTreeTestingModule(declarations) { + TestBed.configureTestingModule({ + imports: [McTreeModule], + declarations + }).compileComponents(); + } + + describe('flat tree', () => { + describe('should initialize', () => { + let fixture: ComponentFixture; + let component: SimpleMcTreeApp; + + beforeEach(() => { + configureMatTreeTestingModule([SimpleMcTreeApp]); + fixture = TestBed.createComponent(SimpleMcTreeApp); + + component = fixture.componentInstance; + treeElement = fixture.nativeElement.querySelector('.mc-tree-selection'); + + fixture.detectChanges(); + }); + + it('with rendered dataNodes', () => { + const nodes = getNodes(treeElement); + + expect(nodes).toBeDefined('Expect nodes to be defined'); + expect(nodes[0].classList).toContain('customNodeClass'); + }); + + it('with the right data', () => { + expect(component.treeData.length).toBe(5); + + expectFlatTreeToMatch( + treeElement, + 28, + [`rootNode_1`], [`Pictures`], [`Documents`], [`Downloads`], [`Applications`] + ); + }); + }); + + describe('with toggle', () => { + let fixture: ComponentFixture; + let component: McTreeAppWithToggle; + + beforeEach(() => { + configureMatTreeTestingModule([McTreeAppWithToggle]); + fixture = TestBed.createComponent(McTreeAppWithToggle); + + component = fixture.componentInstance; + treeElement = fixture.nativeElement.querySelector('mc-tree-selection'); + + fixture.detectChanges(); + }); + + it('should expand/collapse the node', () => { + expect(component.treeData.length).toBe(5); + + expect(component.treeControl.expansionModel.selected.length) + .toBe(0, `Expect no expanded node`); + + component.toggleRecursively = false; + + expectFlatTreeToMatch( + treeElement, + 40, + [`rootNode_1`], [`Pictures`], [`Documents`], [`Downloads`], [`Applications`] + ); + + (getNodes(treeElement)[1].querySelectorAll('mc-tree-node-toggle')[0] as HTMLElement).click(); + fixture.detectChanges(); + + expect(component.treeControl.expansionModel.selected.length) + .toBe(1, `Expect node expanded one level`); + expectFlatTreeToMatch( + treeElement, + 40, + [`rootNode_1`], + [`Pictures`], + [null, 'Sun'], + [null, 'Woods'], + [null, 'PhotoBoothLibrary'], + [`Documents`], + [`Downloads`], + [`Applications`] + ); + + (getNodes(treeElement)[5].querySelectorAll('mc-tree-node-toggle')[0] as HTMLElement).click(); + fixture.detectChanges(); + + expect(component.treeControl.expansionModel.selected.length).toBe(2, `Expect node expanded`); + expectFlatTreeToMatch( + treeElement, + 40, + [`rootNode_1`], + [`Pictures`], + [null, 'Sun'], + [null, 'Woods'], + [null, 'PhotoBoothLibrary'], + [`Documents`], + [null, `Pictures`], + [null, `angular`], + [null, `material2`], + [`Downloads`], + [`Applications`] + ); + + (getNodes(treeElement)[5].querySelectorAll('mc-tree-node-toggle')[0] as HTMLElement).click(); + fixture.detectChanges(); + + expectFlatTreeToMatch( + treeElement, + 40, + [`rootNode_1`], + [`Pictures`], + [null, 'Sun'], + [null, 'Woods'], + [null, 'PhotoBoothLibrary'], + [`Documents`], + [`Downloads`], + [`Applications`] + ); + }); + }); + + describe('with when node template', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + configureMatTreeTestingModule([WhenNodeMcTreeApp]); + fixture = TestBed.createComponent(WhenNodeMcTreeApp); + + treeElement = fixture.nativeElement.querySelector('mc-tree-selection'); + + fixture.detectChanges(); + }); + + it('with the right data', () => { + expectFlatTreeToMatch( + treeElement, + 40, + [`>>>rootNode_1`], [`Pictures`], [`Documents`], [`Downloads`], [`Applications`] + ); + }); + }); + + describe('should initialize', () => { + let fixture: ComponentFixture; + let component: FiltrationMcTreeApp; + + beforeEach(() => { + configureMatTreeTestingModule([FiltrationMcTreeApp]); + fixture = TestBed.createComponent(FiltrationMcTreeApp); + + component = fixture.componentInstance; + treeElement = fixture.nativeElement.querySelector('.mc-tree-selection'); + + fixture.detectChanges(); + }); + + it('should filter nodes by condition', () => { + let nodes = getNodes(treeElement); + expect(nodes.length).toBe(5); + + component.treeControl.filterNodes('Pictures'); + nodes = getNodes(treeElement); + expect(nodes.length).toBe(3); + + component.treeControl.filterNodes('Documents'); + fixture.detectChanges(); + nodes = getNodes(treeElement); + expect(nodes.length).toBe(1); + + component.treeControl.filterNodes('condition for filter all nodes'); + nodes = getNodes(treeElement); + expect(nodes.length).toBe(0); + }); + + it('should filter nodes and but not their parents', () => { + let nodes = getNodes(treeElement); + expect(nodes.length).toBe(5); + + component.treeControl.filterNodes('Sun'); + nodes = getNodes(treeElement); + + const parentOfFoundedNode = nodes[0].textContent!.trim(); + expect(parentOfFoundedNode).toBe('Pictures'); + + const foundedNode = nodes[1].textContent!.trim(); + expect(foundedNode).toBe('Sun'); + + expect(nodes.length).toBe(2); + }); + + it('should delete filtration with empty condition', () => { + let nodes = getNodes(treeElement); + expect(nodes.length).toBe(5); + + component.treeControl.filterNodes('Pictures'); + nodes = getNodes(treeElement); + expect(nodes.length).toBe(3); + + component.treeControl.filterNodes(''); + nodes = getNodes(treeElement); + expect(nodes.length).toBe(5); + }); + }); + }); +}); + + +export const DATA_OBJECT = { + rootNode_1: 'app', + Pictures: { + Sun: 'png', + Woods: 'jpg', + PhotoBoothLibrary: 'jpg' + }, + Documents: { + Pictures: 'Pictures', + angular: 'ts', + material2: 'ts' + }, + Downloads: { + Tutorial: 'html', + November: 'pdf', + October: 'pdf' + }, + Applications: { + Chrome: 'app', + Calendar: 'app', + Webstorm: 'app' + } +}; + +export class FileNode { + children: FileNode[]; + name: string; + type: any; + isSpecial: boolean; +} + +/** Flat node with expandable and level information */ +export class FileFlatNode { + name: string; + type: any; + level: number; + expandable: boolean; + parent: any; + isSpecial: boolean; +} + +export function buildFileTree(value: any, level: number): FileNode[] { + const data: any[] = []; + + for (const k of Object.keys(value)) { + const v = value[k]; + const node = new FileNode(); + + node.name = `${k}`; + + if (v === null || v === undefined) { + // no action + } else if (typeof v === 'object') { + node.children = buildFileTree(v, level + 1); + } else { + node.type = v; + } + + data.push(node); + } + + return data; +} + +function getNodes(treeElement: Element): Element[] { + // tslint:disable-next-line: no-unnecessary-type-assertion + return [].slice.call(treeElement.querySelectorAll('.mc-tree-option'))!; +} + +function expectFlatTreeToMatch(treeElement: Element, expectedPaddingIndent: number = 28, ...expectedTree: any[]) { + const missedExpectations: string[] = []; + + function checkNode(node: Element, expectedNode: any[]) { + const actualTextContent = node.textContent!.trim(); + const expectedTextContent = expectedNode[expectedNode.length - 1]; + + if (actualTextContent !== expectedTextContent) { + missedExpectations.push(`Expected node contents to be ${expectedTextContent} but was ${actualTextContent}`); + } + } + + function checkLevel(node: Element, expectedNode: any[]) { + const actualLevel = (node as HTMLElement).style.paddingLeft; + + if (expectedNode.length === 1) { + + if (actualLevel !== `12px`) { + missedExpectations.push(`Expected node level to be 0 but was ${actualLevel}`); + } + } else { + const expectedLevel = `${((expectedNode.length - 1) * expectedPaddingIndent) + 12}px`; + + if (actualLevel !== expectedLevel) { + missedExpectations.push(`Expected node level to be ${expectedLevel} but was ${actualLevel}`); + } + } + } + + getNodes(treeElement).forEach((node, index) => { + const expected = expectedTree ? expectedTree[index] : null; + + checkLevel(node, expected); + checkNode(node, expected); + }); + + if (missedExpectations.length) { + fail(missedExpectations.join('\n')); + } +} + +@Component({ + template: ` + + + + {{ node.name }} + + + ` +}) +class SimpleMcTreeApp { + treeControl: FlatTreeControl; + treeFlattener: McTreeFlattener; + + dataSource: McTreeFlatDataSource; + + treeData: FileNode[]; + + @ViewChild(McTreeSelection, { static: false }) tree: McTreeSelection; + + constructor() { + this.treeFlattener = new McTreeFlattener( + this.transformer, this.getLevel, this.isExpandable, this.getChildren + ); + + this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + this.dataSource = new McTreeFlatDataSource(this.treeControl, this.treeFlattener); + + this.dataSource.data = this.treeData = buildFileTree(DATA_OBJECT, 0); + } + + getLevel = (node: FileFlatNode) => node.level; + + isExpandable = (node: FileFlatNode) => node.expandable; + + getChildren = (node: FileNode) => node.children; + + transformer = (node: FileNode, level: number, parent: any) => { + const flatNode = new FileFlatNode(); + + flatNode.name = node.name; + flatNode.parent = parent; + flatNode.type = node.type; + flatNode.level = level; + flatNode.expandable = !!node.children; + + return flatNode; + } +} + +@Component({ + template: ` + + + + {{ node.name }} + + + + + + {{ node.name }} + + + ` +}) +class McTreeAppWithToggle { + toggleRecursively: boolean = true; + treeControl: FlatTreeControl; + treeFlattener: McTreeFlattener; + + dataSource: McTreeFlatDataSource; + + treeData: FileNode[]; + + @ViewChild(McTreeSelection, { static: false }) tree: McTreeSelection; + + constructor() { + this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + + this.treeFlattener = new McTreeFlattener( + this.transformer, this.getLevel, this.isExpandable, this.getChildren + ); + + this.dataSource = new McTreeFlatDataSource(this.treeControl, this.treeFlattener); + + this.dataSource.data = this.treeData = buildFileTree(DATA_OBJECT, 0); + } + + hasChild(_: number, nodeData: FileFlatNode) { + return nodeData.expandable; + } + + getLevel = (node: FileFlatNode) => node.level; + + isExpandable = (node: FileFlatNode) => node.expandable; + + getChildren = (node: FileNode) => node.children; + + transformer = (node: FileNode, level: number, parent: any) => { + const flatNode = new FileFlatNode(); + + flatNode.name = node.name; + flatNode.parent = parent; + flatNode.type = node.type; + flatNode.level = level; + flatNode.expandable = !!node.children; + + return flatNode; + } +} + +@Component({ + template: ` + + + {{ node.name }} + + + + >>>{{ node.name }} + + + ` +}) +class WhenNodeMcTreeApp { + treeControl: FlatTreeControl; + treeFlattener: McTreeFlattener; + + dataSource: McTreeFlatDataSource; + + treeData: FileNode[]; + + @ViewChild(McTreeSelection, { static: false }) tree: McTreeSelection; + + constructor() { + this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + this.treeFlattener = new McTreeFlattener( + this.transformer, this.getLevel, this.isExpandable, this.getChildren + ); + + this.dataSource = new McTreeFlatDataSource(this.treeControl, this.treeFlattener); + + this.dataSource.data = this.treeData = buildFileTree(DATA_OBJECT, 0); + } + + getLevel = (node: FileFlatNode) => node.level; + + isExpandable = (node: FileFlatNode) => node.expandable; + + getChildren = (node: FileNode) => node.children; + + transformer = (node: FileNode, level: number, parent: any) => { + const flatNode = new FileFlatNode(); + + flatNode.name = node.name; + flatNode.parent = parent; + flatNode.type = node.type; + flatNode.level = level; + flatNode.expandable = !!node.children; + flatNode.isSpecial = !node.children; + + return flatNode; + } + + isSpecial = (_: number, node: FileFlatNode) => node.isSpecial; +} + +@Component({ + template: ` + + + + {{ node.name }} + + + + {{ node.name }} + + + ` +}) +class FiltrationMcTreeApp { + treeControl: FlatTreeControl; + treeFlattener: McTreeFlattener; + + dataSource: McTreeFlatDataSource; + + treeData: FileNode[]; + + @ViewChild(McTreeSelection, { static: false }) tree: McTreeSelection; + + constructor() { + this.treeFlattener = new McTreeFlattener( + this.transformer, this.getLevel, this.isExpandable, this.getChildren + ); + + this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + this.dataSource = new McTreeFlatDataSource(this.treeControl, this.treeFlattener); + + this.dataSource.data = this.treeData = buildFileTree(DATA_OBJECT, 0); + } + + getLevel = (node: FileFlatNode) => node.level; + + isExpandable = (node: FileFlatNode) => node.expandable; + + getChildren = (node: FileNode) => node.children; + + transformer = (node: FileNode, level: number, parent: any) => { + const flatNode = new FileFlatNode(); + + flatNode.name = node.name; + flatNode.parent = parent; + flatNode.type = node.type; + flatNode.level = level; + flatNode.expandable = !!node.children; + + return flatNode; + } + + hasChild(_: number, nodeData: FileFlatNode) { + return nodeData.expandable; + } +} diff --git a/packages/mosaic/tree/tree._spec.ts b/packages/mosaic/tree/tree._spec.ts deleted file mode 100644 index 65ea089dc..000000000 --- a/packages/mosaic/tree/tree._spec.ts +++ /dev/null @@ -1,807 +0,0 @@ -/* tslint:disable:no-magic-numbers */ -import { Component, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FlatTreeControl, NestedTreeControl, ITreeControl } from '@ptsecurity/cdk/tree'; -import { BehaviorSubject, Observable } from 'rxjs'; - -import { - McTreeSelection, - McTreeFlatDataSource, - McTreeFlattener, - McTreeModule, - McTreeNestedDataSource -} from './index'; - - -xdescribe('McTree', () => { - /** Represents an indent for expectNestedTreeToMatch */ - const _ = {}; - - let treeElement: HTMLElement; - let underlyingDataSource: FakeDataSource; - - function configureMatTreeTestingModule(declarations) { - TestBed.configureTestingModule({ - imports: [McTreeModule], - declarations - }).compileComponents(); - } - - describe('flat tree', () => { - describe('should initialize', () => { - let fixture: ComponentFixture; - let component: SimpleMatTreeApp; - - - beforeEach(() => { - configureMatTreeTestingModule([SimpleMatTreeApp]); - fixture = TestBed.createComponent(SimpleMatTreeApp); - - component = fixture.componentInstance; - underlyingDataSource = component.underlyingDataSource; - treeElement = fixture.nativeElement.querySelector('mc-tree'); - - fixture.detectChanges(); - }); - - it('with rendered dataNodes', () => { - const nodes = getNodes(treeElement); - - expect(nodes).toBeDefined('Expect nodes to be defined'); - expect(nodes[0].classList).toContain('customNodeClass'); - }); - - it('with the right accessibility roles', () => { - expect(treeElement.getAttribute('role')).toBe('tree'); - - getNodes(treeElement).forEach((node) => { - expect(node.getAttribute('role')).toBe('treeitem'); - }); - }); - - it('with the right data', () => { - expect(underlyingDataSource.data.length).toBe(3); - - const data = underlyingDataSource.data; - expectFlatTreeToMatch(treeElement, 28, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`]); - - underlyingDataSource.addChild(data[2]); - fixture.detectChanges(); - - expectFlatTreeToMatch(treeElement, 28, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`], - [`_, topping_4 - cheese_4 + base_4`]); - }); - }); - - describe('with toggle', () => { - let fixture: ComponentFixture; - let component: MatTreeAppWithToggle; - - beforeEach(() => { - configureMatTreeTestingModule([MatTreeAppWithToggle]); - fixture = TestBed.createComponent(MatTreeAppWithToggle); - - component = fixture.componentInstance; - underlyingDataSource = component.underlyingDataSource; - treeElement = fixture.nativeElement.querySelector('mc-tree'); - - fixture.detectChanges(); - }); - - it('should expand/collapse the node', () => { - expect(underlyingDataSource.data.length).toBe(3); - - expect(component.treeControl.expansionModel.selected.length) - .toBe(0, `Expect no expanded node`); - - component.toggleRecursively = false; - const data = underlyingDataSource.data; - const child = underlyingDataSource.addChild(data[2]); - underlyingDataSource.addChild(child); - fixture.detectChanges(); - - expectFlatTreeToMatch(treeElement, 40, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`]); - - - (getNodes(treeElement)[2] as HTMLElement).click(); - fixture.detectChanges(); - - expect(component.treeControl.expansionModel.selected.length) - .toBe(1, `Expect node expanded one level`); - expectFlatTreeToMatch(treeElement, 40, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`], - [_, `topping_4 - cheese_4 + base_4`]); - - (getNodes(treeElement)[3] as HTMLElement).click(); - fixture.detectChanges(); - - expect(component.treeControl.expansionModel.selected.length) - .toBe(2, `Expect node expanded`); - expectFlatTreeToMatch(treeElement, 40, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`], - [_, `topping_4 - cheese_4 + base_4`], - [_, _, `topping_5 - cheese_5 + base_5`]); - - (getNodes(treeElement)[2] as HTMLElement).click(); - fixture.detectChanges(); - - expectFlatTreeToMatch(treeElement, 40, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`]); - }); - - it('should expand/collapse the node recursively', () => { - expect(underlyingDataSource.data.length).toBe(3); - - expect(component.treeControl.expansionModel.selected.length) - .toBe(0, `Expect no expanded node`); - - const data = underlyingDataSource.data; - const child = underlyingDataSource.addChild(data[2]); - underlyingDataSource.addChild(child); - fixture.detectChanges(); - - expectFlatTreeToMatch(treeElement, 40, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`]); - - (getNodes(treeElement)[2] as HTMLElement).click(); - fixture.detectChanges(); - - expect(component.treeControl.expansionModel.selected.length) - .toBe(3, `Expect nodes expanded`); - expectFlatTreeToMatch(treeElement, 40, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`], - [_, `topping_4 - cheese_4 + base_4`], - [_, _, `topping_5 - cheese_5 + base_5`]); - - - (getNodes(treeElement)[2] as HTMLElement).click(); - fixture.detectChanges(); - - expect(component.treeControl.expansionModel.selected.length) - .toBe(0, `Expect node collapsed`); - - expectFlatTreeToMatch(treeElement, 40, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`]); - }); - }); - - describe('with when node template', () => { - let fixture: ComponentFixture; - let component: WhenNodeMatTreeApp; - - beforeEach(() => { - configureMatTreeTestingModule([WhenNodeMatTreeApp]); - fixture = TestBed.createComponent(WhenNodeMatTreeApp); - - component = fixture.componentInstance; - underlyingDataSource = component.underlyingDataSource; - treeElement = fixture.nativeElement.querySelector('mc-tree'); - - fixture.detectChanges(); - }); - - it('with the right data', () => { - expectFlatTreeToMatch(treeElement, 28, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`], - [`>>> topping_4 - cheese_4 + base_4`]); - }); - }); - }); - - describe('nested tree', () => { - describe('should initialize', () => { - let fixture: ComponentFixture; - let component: NestedMatTreeApp; - - beforeEach(() => { - configureMatTreeTestingModule([NestedMatTreeApp]); - fixture = TestBed.createComponent(NestedMatTreeApp); - - component = fixture.componentInstance; - underlyingDataSource = component.underlyingDataSource; - treeElement = fixture.nativeElement.querySelector('mc-tree'); - - fixture.detectChanges(); - }); - - it('with rendered dataNodes', () => { - const nodes = getNodes(treeElement); - - expect(nodes).toBeDefined('Expect nodes to be defined'); - expect(nodes[0].classList).toContain('customNodeClass'); - }); - - it('with the right accessibility roles', () => { - expect(treeElement.getAttribute('role')).toBe('tree'); - - getNodes(treeElement).forEach((node) => { - expect(node.getAttribute('role')).toBe('treeitem'); - }); - }); - - it('with the right data', () => { - expect(underlyingDataSource.data.length).toBe(3); - - let data = underlyingDataSource.data; - expectNestedTreeToMatch(treeElement, - [`${data[0].pizzaTopping} - ${data[0].pizzaCheese} + ${data[0].pizzaBase}`], - [`${data[1].pizzaTopping} - ${data[1].pizzaCheese} + ${data[1].pizzaBase}`], - [`${data[2].pizzaTopping} - ${data[2].pizzaCheese} + ${data[2].pizzaBase}`]); - - underlyingDataSource.addChild(data[1]); - fixture.detectChanges(); - - treeElement = fixture.nativeElement.querySelector('mc-tree'); - data = underlyingDataSource.data; - expect(data.length).toBe(3); - expectNestedTreeToMatch(treeElement, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [_, `topping_4 - cheese_4 + base_4`], - [`topping_3 - cheese_3 + base_3`]); - }); - - it('with nested child data', () => { - expect(underlyingDataSource.data.length).toBe(3); - - const data = underlyingDataSource.data; - const child = underlyingDataSource.addChild(data[1]); - underlyingDataSource.addChild(child); - fixture.detectChanges(); - - expect(data.length).toBe(3); - expectNestedTreeToMatch(treeElement, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [_, `topping_4 - cheese_4 + base_4`], - [_, _, `topping_5 - cheese_5 + base_5`], - [`topping_3 - cheese_3 + base_3`]); - - underlyingDataSource.addChild(child); - fixture.detectChanges(); - - expect(data.length).toBe(3); - expectNestedTreeToMatch(treeElement, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [_, `topping_4 - cheese_4 + base_4`], - [_, _, `topping_5 - cheese_5 + base_5`], - [_, _, `topping_6 - cheese_6 + base_6`], - [`topping_3 - cheese_3 + base_3`]); - }); - }); - - describe('with when node', () => { - let fixture: ComponentFixture; - let component: WhenNodeNestedMatTreeApp; - - beforeEach(() => { - configureMatTreeTestingModule([WhenNodeNestedMatTreeApp]); - fixture = TestBed.createComponent(WhenNodeNestedMatTreeApp); - - component = fixture.componentInstance; - underlyingDataSource = component.underlyingDataSource; - treeElement = fixture.nativeElement.querySelector('mc-tree'); - - fixture.detectChanges(); - }); - - it('with the right data', () => { - expectNestedTreeToMatch(treeElement, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`], - [`>>> topping_4 - cheese_4 + base_4`]); - }); - }); - - describe('with toggle', () => { - let fixture: ComponentFixture; - let component: NestedMatTreeAppWithToggle; - - beforeEach(() => { - configureMatTreeTestingModule([NestedMatTreeAppWithToggle]); - fixture = TestBed.createComponent(NestedMatTreeAppWithToggle); - - component = fixture.componentInstance; - underlyingDataSource = component.underlyingDataSource; - treeElement = fixture.nativeElement.querySelector('mc-tree'); - - fixture.detectChanges(); - }); - - it('should expand/collapse the node', () => { - component.toggleRecursively = false; - const data = underlyingDataSource.data; - const child = underlyingDataSource.addChild(data[1]); - underlyingDataSource.addChild(child); - - fixture.detectChanges(); - - expectNestedTreeToMatch(treeElement, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`]); - - fixture.detectChanges(); - - (getNodes(treeElement)[1] as HTMLElement).click(); - fixture.detectChanges(); - - expect(component.treeControl.expansionModel.selected.length) - .toBe(1, `Expect node expanded`); - expectNestedTreeToMatch(treeElement, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [_, `topping_4 - cheese_4 + base_4`], - [`topping_3 - cheese_3 + base_3`]); - - (getNodes(treeElement)[1] as HTMLElement).click(); - fixture.detectChanges(); - - expectNestedTreeToMatch(treeElement, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`]); - expect(component.treeControl.expansionModel.selected.length) - .toBe(0, `Expect node collapsed`); - }); - - it('should expand/collapse the node recursively', () => { - const data = underlyingDataSource.data; - const child = underlyingDataSource.addChild(data[1]); - underlyingDataSource.addChild(child); - fixture.detectChanges(); - - expectNestedTreeToMatch(treeElement, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`]); - - (getNodes(treeElement)[1] as HTMLElement).click(); - fixture.detectChanges(); - - expect(component.treeControl.expansionModel.selected.length) - .toBe(3, `Expect node expanded`); - expectNestedTreeToMatch(treeElement, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [_, `topping_4 - cheese_4 + base_4`], - [_, _, `topping_5 - cheese_5 + base_5`], - [`topping_3 - cheese_3 + base_3`]); - - (getNodes(treeElement)[1] as HTMLElement).click(); - fixture.detectChanges(); - - expect(component.treeControl.expansionModel.selected.length) - .toBe(0, `Expect node collapsed`); - expectNestedTreeToMatch(treeElement, - [`topping_1 - cheese_1 + base_1`], - [`topping_2 - cheese_2 + base_2`], - [`topping_3 - cheese_3 + base_3`]); - }); - }); - }); -}); - -export class TestData { - pizzaTopping: string; - pizzaCheese: string; - pizzaBase: string; - level: number; - children: TestData[]; - observableChildren: BehaviorSubject; - isSpecial: boolean; - - constructor(pizzaTopping: string, pizzaCheese: string, pizzaBase: string, - children: TestData[] = [], isSpecial: boolean = false) { - this.pizzaTopping = pizzaTopping; - this.pizzaCheese = pizzaCheese; - this.pizzaBase = pizzaBase; - this.isSpecial = isSpecial; - this.children = children; - this.observableChildren = new BehaviorSubject(this.children); - } -} - -class FakeDataSource { - - get data() { - return this._dataChange.getValue(); - } - - set data(data: TestData[]) { - this._dataChange.next(data); - } - - dataIndex = 0; - _dataChange = new BehaviorSubject([]); - - constructor() { - for (let i = 0; i < 3; i++) { - this.addData(); - } - } - - connect(): Observable { - return this._dataChange; - } - - addChild(parent: TestData, isSpecial: boolean = false) { - const nextIndex = ++this.dataIndex; - const child = new TestData( - `topping_${nextIndex}`, - `cheese_${nextIndex}`, - `base_${nextIndex}` - ); - - const index = this.data.indexOf(parent); - - if (index > -1) { - parent = new TestData( - parent.pizzaTopping, parent.pizzaCheese, parent.pizzaBase, parent.children, isSpecial); - } - parent.children.push(child); - parent.observableChildren.next(parent.children); - - const copiedData = this.data.slice(); - if (index > -1) { - copiedData.splice(index, 1, parent); - } - this.data = copiedData; - - return child; - } - - addData(isSpecial: boolean = false) { - const nextIndex = ++this.dataIndex; - const copiedData = this.data.slice(); - copiedData.push(new TestData( - `topping_${nextIndex}`, `cheese_${nextIndex}`, `base_${nextIndex}`, [], isSpecial)); - - this.data = copiedData; - } -} - -function getNodes(treeElement: Element): Element[] { - return [].slice.call(treeElement.querySelectorAll('.mc-tree-node'))!; -} - -function expectFlatTreeToMatch(treeElement: Element, expectedPaddingIndent: number = 28, - ...expectedTree: any[]) { - const missedExpectations: string[] = []; - - function checkNode(node: Element, expectedNode: any[]) { - const actualTextContent = node.textContent!.trim(); - const expectedTextContent = expectedNode[expectedNode.length - 1]; - - if (actualTextContent !== expectedTextContent) { - missedExpectations.push( - `Expected node contents to be ${expectedTextContent} but was ${actualTextContent}`); - } - } - - function checkLevel(node: Element, expectedNode: any[]) { - - const actualLevel = (node as HTMLElement).style.paddingLeft; - - if (expectedNode.length === 1) { - if (actualLevel !== ``) { - missedExpectations.push( - `Expected node level to be 0 but was ${actualLevel}`); - } - } else { - const expectedLevel = `${(expectedNode.length - 1) * expectedPaddingIndent}px`; - if (actualLevel !== expectedLevel) { - missedExpectations.push( - `Expected node level to be ${expectedLevel} but was ${actualLevel}`); - } - } - } - - getNodes(treeElement).forEach((node, index) => { - const expected = expectedTree ? - expectedTree[index] : - null; - - checkLevel(node, expected); - checkNode(node, expected); - }); - - if (missedExpectations.length) { - fail(missedExpectations.join('\n')); - } -} - -function expectNestedTreeToMatch(treeElement: Element, ...expectedTree: any[]) { - const missedExpectations: string[] = []; - - function checkNodeContent(node: Element, expectedNode: any[]) { - const expectedTextContent = expectedNode[expectedNode.length - 1]; - const actualTextContent = node.childNodes.item(0).textContent!.trim(); - - if (actualTextContent !== expectedTextContent) { - missedExpectations.push( - `Expected node contents to be ${expectedTextContent} but was ${actualTextContent}`); - } - } - - function checkNodeDescendants(node: Element, expectedNode: any[], currentIndex: number) { - let expectedDescendant = 0; - - for (let i = currentIndex + 1; i < expectedTree.length; ++i) { - if (expectedTree[i].length > expectedNode.length) { - ++expectedDescendant; - } else if (expectedTree[i].length === expectedNode.length) { - break; - } - } - - const actualDescendant = getNodes(node).length; - - if (actualDescendant !== expectedDescendant) { - missedExpectations.push( - `Expected node descendant num to be ${expectedDescendant} but was ${actualDescendant}`); - } - } - - getNodes(treeElement).forEach((node, index) => { - - const expected = expectedTree ? - expectedTree[index] : - null; - - checkNodeDescendants(node, expected, index); - checkNodeContent(node, expected); - }); - - if (missedExpectations.length) { - fail(missedExpectations.join('\n')); - } -} - -@Component({ - template: ` - - - {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} - - - ` -}) -class SimpleMatTreeApp { - - getLevel = (node: TestData) => node.level; - isExpandable = (node: TestData) => node.children.length > 0; - getChildren = (node: TestData) => node.observableChildren; - transformer = (node: TestData, level: number) => { - node.level = level; - - return node; - }; - - treeFlattener = new McTreeFlattener( - this.transformer, this.getLevel, this.isExpandable, this.getChildren); - - treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); - - dataSource = new McTreeFlatDataSource(this.treeControl, this.treeFlattener); - - underlyingDataSource = new FakeDataSource(); - - @ViewChild(McTreeSelection, {static: false}) tree: McTreeSelection; - - constructor() { - this.underlyingDataSource.connect().subscribe((data) => { - this.dataSource.data = data; - }); - } -} - -@Component({ - template: ` - - - {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} - - - - ` -}) -class NestedMatTreeApp { - - getChildren = (node: TestData) => node.observableChildren; - - treeControl = new NestedTreeControl(this.getChildren); - - dataSource = new McTreeNestedDataSource(); - underlyingDataSource = new FakeDataSource(); - - @ViewChild(McTreeSelection, {static: false}) tree: McTreeSelection; - - constructor() { - this.underlyingDataSource.connect().subscribe((data) => { - this.dataSource.data = data; - }); - } -} - -@Component({ - template: ` - - - {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} - - - - >>> {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} -
- -
-
-
- ` -}) -class WhenNodeNestedMatTreeApp { - - getChildren = (node: TestData) => node.observableChildren; - - treeControl: ITreeControl = new NestedTreeControl(this.getChildren); - - dataSource = new McTreeNestedDataSource(); - underlyingDataSource = new FakeDataSource(); - - @ViewChild(McTreeSelection, {static: false}) tree: McTreeSelection; - - constructor() { - this.underlyingDataSource.connect().subscribe((data) => { - this.dataSource.data = data; - }); - } - - isSpecial = (_: number, node: TestData) => node.isSpecial; -} - - -@Component({ - template: ` - - - {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} - - - ` -}) -class MatTreeAppWithToggle { - toggleRecursively: boolean = true; - - getLevel = (node: TestData) => node.level; - isExpandable = (node: TestData) => node.children.length > 0; - getChildren = (node: TestData) => node.observableChildren; - transformer = (node: TestData, level: number) => { - node.level = level; - - return node; - } - - treeFlattener = new McTreeFlattener( - this.transformer, this.getLevel, this.isExpandable, this.getChildren); - - treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); - - dataSource = new McTreeFlatDataSource(this.treeControl, this.treeFlattener); - underlyingDataSource = new FakeDataSource(); - - @ViewChild(McTreeSelection, {static: false}) tree: McTreeSelection; - - constructor() { - this.underlyingDataSource.connect().subscribe((data) => { - this.dataSource.data = data; - }); - } -} - -@Component({ - template: ` - - - {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} -
- -
-
-
- ` -}) -class NestedMatTreeAppWithToggle { - toggleRecursively: boolean = true; - - getChildren = (node: TestData) => node.observableChildren; - - treeControl = new NestedTreeControl(this.getChildren); - dataSource = new McTreeNestedDataSource(); - underlyingDataSource = new FakeDataSource(); - - @ViewChild(McTreeSelection, {static: false}) tree: McTreeSelection; - - constructor() { - this.underlyingDataSource.connect().subscribe((data) => { - this.dataSource.data = data; - }); - } -} - -@Component({ - template: ` - - - {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} - - - >>> {{node.pizzaTopping}} - {{node.pizzaCheese}} + {{node.pizzaBase}} - - - ` -}) -class WhenNodeMatTreeApp { - - getLevel = (node: TestData) => node.level; - isExpandable = (node: TestData) => node.children.length > 0; - getChildren = (node: TestData) => node.observableChildren; - transformer = (node: TestData, level: number) => { - node.level = level; - - return node; - }; - - treeFlattener = new McTreeFlattener( - this.transformer, this.getLevel, this.isExpandable, this.getChildren); - - treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); - - dataSource = new McTreeFlatDataSource(this.treeControl, this.treeFlattener); - underlyingDataSource = new FakeDataSource(); - - @ViewChild(McTreeSelection, {static: false}) tree: McTreeSelection; - - constructor() { - this.underlyingDataSource.connect().subscribe((data) => { - this.dataSource.data = data; - }); - } - - isSpecial = (_: number, node: TestData) => node.isSpecial; -} diff --git a/packages/mosaic/tree/tree.module.ts b/packages/mosaic/tree/tree.module.ts index da7664251..bf36eaef1 100644 --- a/packages/mosaic/tree/tree.module.ts +++ b/packages/mosaic/tree/tree.module.ts @@ -3,11 +3,11 @@ import { NgModule } from '@angular/core'; import { CdkTreeModule } from '@ptsecurity/cdk/tree'; import { McPseudoCheckboxModule } from '@ptsecurity/mosaic/core'; -import { McTreeNodeDef } from './node'; -import { McTreeNodePadding } from './padding'; -import { McTreeNodeToggle } from './toggle'; -import { McTreeOption } from './tree-option'; -import { McTreeSelection } from './tree-selection'; +import { McTreeNodeDef } from './node.directive'; +import { McTreeNodePadding } from './padding.directive'; +import { McTreeNodeToggleDirective, McTreeNodeToggleComponent } from './toggle'; +import { McTreeOption } from './tree-option.component'; +import { McTreeSelection } from './tree-selection.component'; const MC_TREE_DIRECTIVES = [ @@ -15,7 +15,8 @@ const MC_TREE_DIRECTIVES = [ McTreeOption, McTreeNodeDef, McTreeNodePadding, - McTreeNodeToggle + McTreeNodeToggleComponent, + McTreeNodeToggleDirective ]; @NgModule({ diff --git a/packages/mosaic/tree/tree.scss b/packages/mosaic/tree/tree.scss index 857eb9e9c..75129fff9 100644 --- a/packages/mosaic/tree/tree.scss +++ b/packages/mosaic/tree/tree.scss @@ -38,14 +38,20 @@ $mc-tree-node-padding: 16px; } } -.mc-icon-rotate_90 { - transform: rotate(90deg); -} +.mc-tree-node-toggle { + margin-right: 4px; -.mc-icon-rotate_180 { - transform: rotate(180deg); -} + & .mc-icon { + transform: rotate(-90deg); + } -.mc-icon-rotate_270 { - transform: rotate(270deg); + &.mc-opened { + & .mc-icon { + transform: rotate(0); + } + } + + &.mc-disabled { + cursor: default; + } }