diff --git a/components/components.less b/components/components.less index edc88e8b005..95dc2fdd302 100644 --- a/components/components.less +++ b/components/components.less @@ -55,6 +55,7 @@ @import "./auto-complete/style/entry.less"; @import "./cascader/style/entry.less"; @import "./tree/style/entry.less"; +@import "./tree-view/style/entry.less"; @import "./tree-select/style/entry.less"; @import "./calendar/style/entry.less"; @import "./result/style/entry.less"; diff --git a/components/core/animation/collapse.ts b/components/core/animation/collapse.ts index c09420bfed8..a3f9eb5fc7d 100644 --- a/components/core/animation/collapse.ts +++ b/components/core/animation/collapse.ts @@ -19,17 +19,22 @@ export const collapseMotion: AnimationTriggerMetadata = trigger('collapseMotion' export const treeCollapseMotion: AnimationTriggerMetadata = trigger('treeCollapseMotion', [ transition('* => *', [ query( - 'nz-tree-node:leave', - [style({ overflow: 'hidden' }), stagger(0, [animate(`150ms ${AnimationCurves.EASE_IN_OUT}`, style({ height: 0 }))])], + 'nz-tree-node:leave,nz-tree-builtin-node:leave', + [ + style({ overflow: 'hidden' }), + stagger(0, [animate(`150ms ${AnimationCurves.EASE_IN_OUT}`, style({ height: 0, opacity: 0, 'padding-bottom': 0 }))]) + ], { optional: true } ), query( - 'nz-tree-node:enter', + 'nz-tree-node:enter,nz-tree-builtin-node:enter', [ - style({ overflow: 'hidden', height: 0 }), - stagger(0, [animate(`150ms ${AnimationCurves.EASE_IN_OUT}`, style({ overflow: 'hidden', height: '*' }))]) + style({ overflow: 'hidden', height: 0, opacity: 0, 'padding-bottom': 0 }), + stagger(0, [ + animate(`150ms ${AnimationCurves.EASE_IN_OUT}`, style({ overflow: 'hidden', height: '*', opacity: '*', 'padding-bottom': '*' })) + ]) ], { optional: true diff --git a/components/style/patch.less b/components/style/patch.less index b679d658849..f77f20f34a9 100644 --- a/components/style/patch.less +++ b/components/style/patch.less @@ -11,6 +11,20 @@ z-index: 1000; } +.cdk-visually-hidden { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + outline: 0; + -webkit-appearance: none; + -moz-appearance: none; +} + .cdk-overlay-backdrop { top: 0; bottom: 0; diff --git a/components/tree-select/style/patch.less b/components/tree-select/style/patch.less index 54a73c7e857..d42e8a8058f 100644 --- a/components/tree-select/style/patch.less +++ b/components/tree-select/style/patch.less @@ -1,4 +1,4 @@ -.ant-tree.ant-select-tree.ant-tree-show-line nz-tree-node:not(:last-child) > li::before { +.ant-tree.ant-select-tree.ant-tree-show-line nz-tree-builtin-node:not(:last-child) > li::before { content: ' '; width: 1px; border-left: 1px solid #d9d9d9; diff --git a/components/tree-select/tree-select.spec.ts b/components/tree-select/tree-select.spec.ts index 0cb8b8439f0..aa80102454a 100644 --- a/components/tree-select/tree-select.spec.ts +++ b/components/tree-select/tree-select.spec.ts @@ -325,7 +325,7 @@ describe('tree-select component', () => { treeSelect.nativeElement.click(); fixture.detectChanges(); expect(treeSelectComponent.nzOpen).toBe(true); - node = overlayContainerElement.querySelector('nz-tree-node')!; + node = overlayContainerElement.querySelector('nz-tree-builtin-node')!; dispatchMouseEvent(node, 'click'); fixture.detectChanges(); flush(); @@ -447,7 +447,7 @@ describe('tree-select component', () => { fixture.detectChanges(); expect(treeSelectComponent.nzOpen).toBe(true); fixture.detectChanges(); - const targetNode = overlayContainerElement.querySelectorAll('nz-tree-node')[2]; + const targetNode = overlayContainerElement.querySelectorAll('nz-tree-builtin-node')[2]; dispatchMouseEvent(targetNode, 'click'); fixture.detectChanges(); flush(); diff --git a/components/tree-view/checkbox.ts b/components/tree-view/checkbox.ts new file mode 100644 index 00000000000..7594429951d --- /dev/null +++ b/components/tree-view/checkbox.ts @@ -0,0 +1,39 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; + +import { BooleanInput } from 'ng-zorro-antd/core/types'; +import { InputBoolean } from 'ng-zorro-antd/core/util'; + +@Component({ + selector: 'nz-tree-node-checkbox', + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + preserveWhitespaces: false, + host: { + class: 'ant-tree-checkbox', + '[class.ant-tree-checkbox-checked]': `nzChecked`, + '[class.ant-tree-checkbox-indeterminate]': `nzIndeterminate`, + '[class.ant-tree-checkbox-disabled]': `nzDisabled`, + '(click)': 'onClick($event)' + } +}) +export class NzTreeNodeCheckboxComponent { + static ngAcceptInputType_nzDisabled: BooleanInput; + + @Input() nzChecked?: boolean; + @Input() nzIndeterminate?: boolean; + @Input() @InputBoolean() nzDisabled?: boolean; + @Output() readonly nzClick = new EventEmitter(); + + onClick(e: MouseEvent): void { + if (!this.nzDisabled) { + this.nzClick.emit(e); + } + } +} diff --git a/components/tree-view/data-source.ts b/components/tree-view/data-source.ts new file mode 100644 index 00000000000..f7118c8a874 --- /dev/null +++ b/components/tree-view/data-source.ts @@ -0,0 +1,117 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/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'; + +export class NzTreeFlattener { + constructor( + public transformFunction: (node: T, level: number) => F, + public getLevel: (node: F) => number, + public isExpandable: (node: F) => boolean, + public getChildren: (node: T) => Observable | T[] | undefined | null + ) {} + + private 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; + } + + private flattenChildren(children: T[], level: number, resultNodes: F[], parentMap: boolean[]): void { + children.forEach((child, index) => { + const 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[] { + const 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[] { + const results: F[] = []; + const 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; + } +} + +export class NzTreeFlatDataSource extends DataSource { + _flattenedData = new BehaviorSubject([]); + + _expandedData = new BehaviorSubject([]); + + _data: BehaviorSubject; + + constructor(private _treeControl: FlatTreeControl, private _treeFlattener: NzTreeFlattener, initialData: T[] = []) { + super(); + this._data = new BehaviorSubject(initialData); + } + + setData(value: T[]): void { + this._data.next(value); + this._flattenedData.next(this._treeFlattener.flattenNodes(this.getData())); + this._treeControl.dataNodes = this._flattenedData.value; + } + + getData(): T[] { + return this._data.getValue(); + } + + connect(collectionViewer: CollectionViewer): Observable { + const changes = [collectionViewer.viewChange, this._treeControl.expansionModel.changed, this._flattenedData]; + return merge(...changes).pipe( + map(() => { + this._expandedData.next(this._treeFlattener.expandFlattenedNodes(this._flattenedData.value, this._treeControl)); + return this._expandedData.value; + }) + ); + } + + disconnect(): void { + // no op + } +} diff --git a/components/tree-view/demo/basic.md b/components/tree-view/demo/basic.md new file mode 100644 index 00000000000..6069b19d236 --- /dev/null +++ b/components/tree-view/demo/basic.md @@ -0,0 +1,14 @@ +--- +order: 0 +title: + zh-CN: 基本 + en-US: basic +--- + +## zh-CN + +最简单的用法,选中,禁用,展开等功能。 + +## en-US + +The most basic usage including select, disable and expand features. diff --git a/components/tree-view/demo/basic.ts b/components/tree-view/demo/basic.ts new file mode 100644 index 00000000000..b952a119e3f --- /dev/null +++ b/components/tree-view/demo/basic.ts @@ -0,0 +1,98 @@ +import { SelectionModel } from '@angular/cdk/collections'; +import { FlatTreeControl } from '@angular/cdk/tree'; +import { Component } from '@angular/core'; + +import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view'; + +interface TreeNode { + name: string; + disabled?: boolean; + children?: TreeNode[]; +} + +const TREE_DATA: TreeNode[] = [ + { + name: 'parent 1', + children: [ + { + name: 'parent 1-0', + disabled: true, + children: [{ name: 'leaf' }, { name: 'leaf' }] + }, + { + name: 'parent 1-1', + children: [{ name: 'leaf' }] + } + ] + } +]; + +interface FlatNode { + expandable: boolean; + name: string; + level: number; + disabled: boolean; +} + +@Component({ + selector: 'nz-demo-tree-view-basic', + template: ` + + + + + {{ node.name }} + + + + + + + + + {{ node.name }} + + + + ` +}) +export class NzDemoTreeViewBasicComponent { + private transformer = (node: TreeNode, level: number) => { + return { + expandable: !!node.children && node.children.length > 0, + name: node.name, + level: level, + disabled: !!node.disabled + }; + }; + selectListSelection = new SelectionModel(true); + + treeControl = new FlatTreeControl( + node => node.level, + node => node.expandable + ); + + treeFlattener = new NzTreeFlattener( + this.transformer, + node => node.level, + node => node.expandable, + node => node.children + ); + + dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener); + + constructor() { + this.dataSource.setData(TREE_DATA); + this.treeControl.expandAll(); + } + + hasChild = (_: number, node: FlatNode) => node.expandable; +} diff --git a/components/tree-view/demo/checkbox.md b/components/tree-view/demo/checkbox.md new file mode 100644 index 00000000000..c3d5b193dbe --- /dev/null +++ b/components/tree-view/demo/checkbox.md @@ -0,0 +1,14 @@ +--- +order: 1 +title: + zh-CN: 选择框 + en-US: checkbox +--- + +## zh-CN + +带选择框的树。 + +## en-US + +Tree with checkboxes. diff --git a/components/tree-view/demo/checkbox.ts b/components/tree-view/demo/checkbox.ts new file mode 100644 index 00000000000..813261ffa85 --- /dev/null +++ b/components/tree-view/demo/checkbox.ts @@ -0,0 +1,189 @@ +import { SelectionModel } from '@angular/cdk/collections'; +import { FlatTreeControl } from '@angular/cdk/tree'; +import { AfterViewInit, Component } from '@angular/core'; + +import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view'; + +interface TreeNode { + name: string; + disabled?: boolean; + children?: TreeNode[]; +} + +const TREE_DATA: TreeNode[] = [ + { + name: '0-0', + disabled: true, + children: [{ name: '0-0-0' }, { name: '0-0-1' }, { name: '0-0-2' }] + }, + { + name: '0-1', + children: [ + { + name: '0-1-0', + children: [{ name: '0-1-0-0' }, { name: '0-1-0-1' }] + }, + { + name: '0-1-1', + children: [{ name: '0-1-1-0' }, { name: '0-1-1-1' }] + } + ] + } +]; + +interface FlatNode { + expandable: boolean; + name: string; + level: number; + disabled: boolean; +} + +@Component({ + selector: 'nz-demo-tree-view-checkbox', + template: ` + + + + + + {{ node.name }} + + + + + + + + + + {{ node.name }} + + + + ` +}) +export class NzDemoTreeViewCheckboxComponent implements AfterViewInit { + private transformer = (node: TreeNode, level: number) => { + const existingNode = this.nestedNodeMap.get(node); + const flatNode = + existingNode && existingNode.name === node.name + ? existingNode + : { + expandable: !!node.children && node.children.length > 0, + name: node.name, + level: level, + disabled: !!node.disabled + }; + this.flatNodeMap.set(flatNode, node); + this.nestedNodeMap.set(node, flatNode); + return flatNode; + }; + flatNodeMap = new Map(); + nestedNodeMap = new Map(); + checklistSelection = new SelectionModel(true); + + treeControl = new FlatTreeControl( + node => node.level, + node => node.expandable + ); + + treeFlattener = new NzTreeFlattener( + this.transformer, + node => node.level, + node => node.expandable, + node => node.children + ); + + dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener); + + constructor() { + this.dataSource.setData(TREE_DATA); + } + + hasChild = (_: number, node: FlatNode) => node.expandable; + + ngAfterViewInit(): void {} + + descendantsAllSelected(node: FlatNode): boolean { + const descendants = this.treeControl.getDescendants(node); + return ( + descendants.length > 0 && + descendants.every(child => { + return this.checklistSelection.isSelected(child); + }) + ); + } + + descendantsPartiallySelected(node: FlatNode): boolean { + const descendants = this.treeControl.getDescendants(node); + const result = descendants.some(child => this.checklistSelection.isSelected(child)); + return result && !this.descendantsAllSelected(node); + } + + leafItemSelectionToggle(node: FlatNode): void { + this.checklistSelection.toggle(node); + this.checkAllParentsSelection(node); + } + + itemSelectionToggle(node: FlatNode): void { + this.checklistSelection.toggle(node); + const descendants = this.treeControl.getDescendants(node); + this.checklistSelection.isSelected(node) + ? this.checklistSelection.select(...descendants) + : this.checklistSelection.deselect(...descendants); + + descendants.forEach(child => this.checklistSelection.isSelected(child)); + this.checkAllParentsSelection(node); + } + + checkAllParentsSelection(node: FlatNode): void { + let parent: FlatNode | null = this.getParentNode(node); + while (parent) { + this.checkRootNodeSelection(parent); + parent = this.getParentNode(parent); + } + } + + checkRootNodeSelection(node: FlatNode): void { + const nodeSelected = this.checklistSelection.isSelected(node); + const descendants = this.treeControl.getDescendants(node); + const descAllSelected = + descendants.length > 0 && + descendants.every(child => { + return this.checklistSelection.isSelected(child); + }); + if (nodeSelected && !descAllSelected) { + this.checklistSelection.deselect(node); + } else if (!nodeSelected && descAllSelected) { + this.checklistSelection.select(node); + } + } + + getParentNode(node: FlatNode): FlatNode | null { + const currentLevel = node.level; + + if (currentLevel < 1) { + return null; + } + + const startIndex = this.treeControl.dataNodes.indexOf(node) - 1; + + for (let i = startIndex; i >= 0; i--) { + const currentNode = this.treeControl.dataNodes[i]; + + if (currentNode.level < currentLevel) { + return currentNode; + } + } + return null; + } +} diff --git a/components/tree-view/demo/directory.md b/components/tree-view/demo/directory.md new file mode 100644 index 00000000000..61f27c9e5c6 --- /dev/null +++ b/components/tree-view/demo/directory.md @@ -0,0 +1,14 @@ +--- +order: 2 +title: + zh-CN: 目录 + en-US: Directory +--- + +## zh-CN + +目录树 + +## en-US + +Directory tree. diff --git a/components/tree-view/demo/directory.ts b/components/tree-view/demo/directory.ts new file mode 100644 index 00000000000..ff71d3073b6 --- /dev/null +++ b/components/tree-view/demo/directory.ts @@ -0,0 +1,113 @@ +import { SelectionModel } from '@angular/cdk/collections'; +import { FlatTreeControl } from '@angular/cdk/tree'; +import { AfterViewInit, Component } from '@angular/core'; + +import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view'; + +interface FoodNode { + name: string; + disabled?: boolean; + children?: FoodNode[]; +} + +const TREE_DATA: FoodNode[] = [ + { + name: 'Fruit', + children: [{ name: 'Apple' }, { name: 'Banana', disabled: true }, { name: 'Fruit loops' }] + }, + { + name: 'Vegetables', + children: [ + { + name: 'Green', + children: [{ name: 'Broccoli' }, { name: 'Brussels sprouts' }] + }, + { + name: 'Orange', + children: [{ name: 'Pumpkins' }, { name: 'Carrots' }] + } + ] + } +]; + +/** Flat node with expandable and level information */ +interface ExampleFlatNode { + expandable: boolean; + name: string; + level: number; + disabled: boolean; +} + +@Component({ + selector: 'nz-demo-tree-view-directory', + template: ` + + + + + + {{ node.name }} + + + + + + + + + + {{ node.name }} + + + + ` +}) +export class NzDemoTreeViewDirectoryComponent implements AfterViewInit { + private transformer = (node: FoodNode, level: number) => { + return { + expandable: !!node.children && node.children.length > 0, + name: node.name, + level: level, + disabled: !!node.disabled + }; + }; + selectListSelection = new SelectionModel(); + + treeControl = new FlatTreeControl( + node => node.level, + node => node.expandable + ); + + treeFlattener = new NzTreeFlattener( + this.transformer, + node => node.level, + node => node.expandable, + node => node.children + ); + + dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener); + + constructor() { + this.dataSource.setData(TREE_DATA); + } + + hasChild = (_: number, node: ExampleFlatNode) => node.expandable; + + ngAfterViewInit(): void { + setTimeout(() => { + this.treeControl.expand(this.getNode('Vegetables')!); + }, 300); + } + + getNode(name: string): ExampleFlatNode | null { + return this.treeControl.dataNodes.find(n => n.name === name) || null; + } +} diff --git a/components/tree-view/demo/dynamic.md b/components/tree-view/demo/dynamic.md new file mode 100644 index 00000000000..c301381d46e --- /dev/null +++ b/components/tree-view/demo/dynamic.md @@ -0,0 +1,14 @@ +--- +order: 3 +title: + zh-CN: 异步加载数据 + en-US: Load data asynchronously +--- + +## zh-CN + +点击展开节点,动态加载数据。 + +## en-US + +To load data asynchronously when click to expand a treeNode. diff --git a/components/tree-view/demo/dynamic.ts b/components/tree-view/demo/dynamic.ts new file mode 100644 index 00000000000..74136fb9e81 --- /dev/null +++ b/components/tree-view/demo/dynamic.ts @@ -0,0 +1,158 @@ +import { CollectionViewer, DataSource, SelectionChange } from '@angular/cdk/collections'; +import { FlatTreeControl, TreeControl } from '@angular/cdk/tree'; +import { AfterViewInit, Component } from '@angular/core'; + +import { BehaviorSubject, merge, Observable, of } from 'rxjs'; +import { delay, map, tap } from 'rxjs/operators'; + +interface FlatNode { + expandable: boolean; + id: number; + label: string; + level: number; + loading?: boolean; +} + +const TREE_DATA: FlatNode[] = [ + { + id: 0, + label: 'Expand to load', + level: 0, + expandable: true + }, + { + id: 1, + label: 'Expand to load', + level: 0, + expandable: true + } +]; + +function getChildren(node: FlatNode): Observable { + return of([ + { + id: Date.now(), + label: `Child Node (level-${node.level + 1})`, + level: node.level + 1, + expandable: true + }, + { + id: Date.now(), + label: `Child Node (level-${node.level + 1})`, + level: node.level + 1, + expandable: true + }, + { + id: Date.now(), + label: `Leaf Node (level-${node.level + 1})`, + level: node.level + 1, + expandable: false + } + ]).pipe(delay(500)); +} + +class DynamicDatasource implements DataSource { + private flattenedData: BehaviorSubject; + private childrenLoadedSet = new Set(); + + constructor(private treeControl: TreeControl, initData: FlatNode[]) { + this.flattenedData = new BehaviorSubject(initData); + treeControl.dataNodes = initData; + } + + connect(collectionViewer: CollectionViewer): Observable { + const changes = [ + collectionViewer.viewChange, + this.treeControl.expansionModel.changed.pipe(tap(change => this.handleExpansionChange(change))), + this.flattenedData + ]; + return merge(...changes).pipe( + map(() => { + return this.expandFlattenedNodes(this.flattenedData.getValue()); + }) + ); + } + + expandFlattenedNodes(nodes: FlatNode[]): FlatNode[] { + const treeControl = this.treeControl; + const results: FlatNode[] = []; + const currentExpand: boolean[] = []; + currentExpand[0] = true; + + nodes.forEach(node => { + let expand = true; + for (let i = 0; i <= treeControl.getLevel(node); i++) { + expand = expand && currentExpand[i]; + } + if (expand) { + results.push(node); + } + if (treeControl.isExpandable(node)) { + currentExpand[treeControl.getLevel(node) + 1] = treeControl.isExpanded(node); + } + }); + return results; + } + + handleExpansionChange(change: SelectionChange): void { + if (change.added) { + change.added.forEach(node => this.loadChildren(node)); + } + } + + loadChildren(node: FlatNode): void { + if (this.childrenLoadedSet.has(node)) { + return; + } + node.loading = true; + getChildren(node).subscribe(children => { + node.loading = false; + const flattenedData = this.flattenedData.getValue(); + const index = flattenedData.indexOf(node); + if (index !== -1) { + flattenedData.splice(index + 1, 0, ...children); + this.childrenLoadedSet.add(node); + } + this.flattenedData.next(flattenedData); + }); + } + + disconnect(): void { + this.flattenedData.complete(); + } +} + +@Component({ + selector: 'nz-demo-tree-view-dynamic', + template: ` + + + {{ node.label }} + + + + + + + + + + {{ node.label }} + + + ` +}) +export class NzDemoTreeViewDynamicComponent implements AfterViewInit { + treeControl = new FlatTreeControl( + node => node.level, + node => node.expandable + ); + + dataSource = new DynamicDatasource(this.treeControl, TREE_DATA); + + constructor() {} + + hasChild = (_: number, node: FlatNode) => node.expandable; + + ngAfterViewInit(): void {} +} diff --git a/components/tree-view/demo/editable.md b/components/tree-view/demo/editable.md new file mode 100644 index 00000000000..3e381231cb9 --- /dev/null +++ b/components/tree-view/demo/editable.md @@ -0,0 +1,14 @@ +--- +order: 5 +title: + zh-CN: 可编辑 + en-US: editable +--- + +## zh-CN + +带添加和删除功能的树。 + +## en-US + +Tree with add and delete actions. diff --git a/components/tree-view/demo/editable.ts b/components/tree-view/demo/editable.ts new file mode 100644 index 00000000000..3a27febe840 --- /dev/null +++ b/components/tree-view/demo/editable.ts @@ -0,0 +1,145 @@ +import { SelectionModel } from '@angular/cdk/collections'; +import { FlatTreeControl } from '@angular/cdk/tree'; +import { Component } from '@angular/core'; + +import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view'; + +interface TreeNode { + name: string; + key: string; + children?: TreeNode[]; +} + +const TREE_DATA: TreeNode[] = [ + { + name: 'parent 1', + key: '1', + children: [ + { + name: 'parent 1-0', + key: '1-0', + children: [ + { name: 'leaf', key: '1-0-0' }, + { name: 'leaf', key: '1-0-1' } + ] + }, + { + name: 'parent 1-1', + key: '1-1', + children: [{ name: 'leaf', key: '1-1-0' }] + } + ] + }, + { + key: '2', + name: 'parent 2', + children: [{ name: 'leaf', key: '2-0' }] + } +]; + +interface FlatNode { + expandable: boolean; + name: string; + key: string; + level: number; +} + +@Component({ + selector: 'nz-demo-tree-view-editable', + template: ` + + + + {{ node.name }} + + + + + +   + + + + + + + + {{ node.name }} + + + + `, + styles: [``] +}) +export class NzDemoTreeViewEditableComponent { + private transformer = (node: TreeNode, level: number) => { + const existingNode = this.nestedNodeMap.get(node); + const flatNode = + existingNode && existingNode.key === node.key + ? existingNode + : { + expandable: !!node.children && node.children.length > 0, + name: node.name, + level: level, + key: node.key + }; + flatNode.name = node.name; + this.flatNodeMap.set(flatNode, node); + this.nestedNodeMap.set(node, flatNode); + return flatNode; + }; + + treeData = TREE_DATA; + flatNodeMap = new Map(); + nestedNodeMap = new Map(); + selectListSelection = new SelectionModel(true); + + treeControl = new FlatTreeControl( + node => node.level, + node => node.expandable + ); + treeFlattener = new NzTreeFlattener( + this.transformer, + node => node.level, + node => node.expandable, + node => node.children + ); + + dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener); + + constructor() { + this.dataSource.setData(this.treeData); + this.treeControl.expandAll(); + } + + hasChild = (_: number, node: FlatNode) => node.expandable; + hasNoContent = (_: number, node: FlatNode) => node.name === ''; + trackBy = (_: number, node: FlatNode) => `${node.key}-${node.name}`; + + addNewNode(node: FlatNode): void { + const parentNode = this.flatNodeMap.get(node); + if (parentNode) { + parentNode.children = parentNode.children || []; + parentNode.children.push({ + name: '', + key: `${parentNode.key}-${parentNode.children.length}` + }); + this.dataSource.setData(this.treeData); + this.treeControl.expand(node); + } + } + + saveNode(node: FlatNode, value: string): void { + const nestedNode = this.flatNodeMap.get(node); + if (nestedNode) { + nestedNode.name = value; + this.dataSource.setData(this.treeData); + } + } +} diff --git a/components/tree-view/demo/line.md b/components/tree-view/demo/line.md new file mode 100644 index 00000000000..59c5cbf9e77 --- /dev/null +++ b/components/tree-view/demo/line.md @@ -0,0 +1,14 @@ +--- +order: 4 +title: + zh-CN: 带连接线的树 + en-US: Tree with line +--- + +## zh-CN + +节点之间带连接线的树,常用于文件目录结构展示。 + +## en-US + +Tree with connected line between nodes. diff --git a/components/tree-view/demo/line.ts b/components/tree-view/demo/line.ts new file mode 100644 index 00000000000..5e1622d9cda --- /dev/null +++ b/components/tree-view/demo/line.ts @@ -0,0 +1,108 @@ +import { FlatTreeControl } from '@angular/cdk/tree'; +import { AfterViewInit, Component } from '@angular/core'; + +import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view'; + +interface TreeNode { + name: string; + children?: TreeNode[]; +} + +const TREE_DATA: TreeNode[] = [ + { + name: 'parent 1', + children: [ + { + name: 'parent 1-0', + children: [{ name: 'leaf' }, { name: 'leaf' }] + }, + { + name: 'parent 1-1', + children: [ + { name: 'leaf' }, + { + name: 'parent 1-1-0', + children: [{ name: 'leaf' }, { name: 'leaf' }] + }, + { name: 'leaf' } + ] + } + ] + }, + { + name: 'parent 2', + children: [{ name: 'leaf' }, { name: 'leaf' }] + } +]; + +interface FlatNode { + expandable: boolean; + name: string; + level: number; +} + +@Component({ + selector: 'nz-demo-tree-view-line', + template: ` + Show Leaf Icon: + + + + + + + + + {{ node.name }} + + + + + + + + + {{ node.name }} + + + + ` +}) +export class NzDemoTreeViewLineComponent implements AfterViewInit { + private transformer = (node: TreeNode, level: number) => { + return { + expandable: !!node.children && node.children.length > 0, + name: node.name, + level: level + }; + }; + + treeControl = new FlatTreeControl( + node => node.level, + node => node.expandable + ); + + treeFlattener = new NzTreeFlattener( + this.transformer, + node => node.level, + node => node.expandable, + node => node.children + ); + + dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener); + + showLeafIcon = false; + constructor() { + this.dataSource.setData(TREE_DATA); + } + + hasChild = (_: number, node: FlatNode) => node.expandable; + + ngAfterViewInit(): void { + this.treeControl.expandAll(); + } + + getNode(name: string): FlatNode | null { + return this.treeControl.dataNodes.find(n => n.name === name) || null; + } +} diff --git a/components/tree-view/demo/module b/components/tree-view/demo/module new file mode 100644 index 00000000000..527e7bae318 --- /dev/null +++ b/components/tree-view/demo/module @@ -0,0 +1,10 @@ +import { NzButtonModule } from 'ng-zorro-antd/button'; +import { NzCheckboxModule } from 'ng-zorro-antd/checkbox'; +import { NzHighlightModule } from 'ng-zorro-antd/core/highlight'; +import { NzNoAnimationModule } from 'ng-zorro-antd/core/no-animation'; +import { NzIconModule } from 'ng-zorro-antd/icon'; +import { NzInputModule } from 'ng-zorro-antd/input'; +import { NzSwitchModule } from 'ng-zorro-antd/switch'; +import { NzTreeViewModule } from 'ng-zorro-antd/tree-view'; + +export const moduleList = [ NzTreeViewModule, NzIconModule, NzCheckboxModule, NzInputModule, NzSwitchModule, NzButtonModule, NzNoAnimationModule, NzHighlightModule ]; \ No newline at end of file diff --git a/components/tree-view/demo/search.md b/components/tree-view/demo/search.md new file mode 100644 index 00000000000..b1b7180b1f8 --- /dev/null +++ b/components/tree-view/demo/search.md @@ -0,0 +1,14 @@ +--- +order: 6 +title: + zh-CN: 搜索 + en-US: search +--- + +## zh-CN + +可搜索的树。 + +## en-US + +Searchable Tree. diff --git a/components/tree-view/demo/search.ts b/components/tree-view/demo/search.ts new file mode 100644 index 00000000000..bfa32c9cf4c --- /dev/null +++ b/components/tree-view/demo/search.ts @@ -0,0 +1,173 @@ +import { FlatTreeControl } from '@angular/cdk/tree'; +import { Component } from '@angular/core'; + +import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view'; +import { BehaviorSubject, combineLatest } from 'rxjs'; +import { auditTime, map } from 'rxjs/operators'; + +interface TreeNode { + name: string; + children?: TreeNode[]; +} + +const TREE_DATA: TreeNode[] = [ + { + name: '0-0', + children: [{ name: '0-0-0' }, { name: '0-0-1' }, { name: '0-0-2' }] + }, + { + name: '0-1', + children: [ + { + name: '0-1-0', + children: [{ name: '0-1-0-0' }, { name: '0-1-0-1' }] + }, + { + name: '0-1-1', + children: [{ name: '0-1-1-0' }, { name: '0-1-1-1' }] + } + ] + } +]; + +interface FlatNode { + expandable: boolean; + name: string; + level: number; +} + +class FilteredTreeResult { + constructor(public treeData: TreeNode[], public needsToExpanded: TreeNode[] = []) {} +} + +/** + * From https://stackoverflow.com/a/45290208/6851836 + */ +function filterTreeData(data: TreeNode[], value: string): FilteredTreeResult { + const needsToExpanded = new Set(); + const _filter = (node: TreeNode, result: TreeNode[]) => { + if (node.name.search(value) !== -1) { + result.push(node); + return result; + } + if (Array.isArray(node.children)) { + const nodes = node.children.reduce((a, b) => _filter(b, a), [] as TreeNode[]); + if (nodes.length) { + const parentNode = { ...node, children: nodes }; + needsToExpanded.add(parentNode); + result.push(parentNode); + } + } + return result; + }; + const treeData = data.reduce((a, b) => _filter(b, a), [] as TreeNode[]); + return new FilteredTreeResult(treeData, [...needsToExpanded]); +} + +@Component({ + selector: 'nz-demo-tree-view-search', + template: ` + + + + + + + + + + + + + + + + + + + + + `, + styles: [ + ` + nz-input-group { + margin-bottom: 8px; + } + + ::ng-deep .highlight { + color: red; + } + ` + ] +}) +export class NzDemoTreeViewSearchComponent { + flatNodeMap = new Map(); + nestedNodeMap = new Map(); + expandedNodes: TreeNode[] = []; + searchValue = ''; + originData$ = new BehaviorSubject(TREE_DATA); + searchValue$ = new BehaviorSubject(''); + + transformer = (node: TreeNode, level: number) => { + const existingNode = this.nestedNodeMap.get(node); + const flatNode = + existingNode && existingNode.name === node.name + ? existingNode + : { + expandable: !!node.children && node.children.length > 0, + name: node.name, + level: level + }; + this.flatNodeMap.set(flatNode, node); + this.nestedNodeMap.set(node, flatNode); + return flatNode; + }; + + treeControl = new FlatTreeControl( + node => node.level, + node => node.expandable, + { + trackBy: flatNode => this.flatNodeMap.get(flatNode)! + } + ); + + treeFlattener = new NzTreeFlattener( + this.transformer, + node => node.level, + node => node.expandable, + node => node.children + ); + + dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener); + + filteredData$ = combineLatest([ + this.originData$, + this.searchValue$.pipe( + auditTime(300), + map(value => (this.searchValue = value)) + ) + ]).pipe(map(([data, value]) => (value ? filterTreeData(data, value) : new FilteredTreeResult(data)))); + + constructor() { + this.filteredData$.subscribe(result => { + this.dataSource.setData(result.treeData); + + const hasSearchValue = !!this.searchValue; + if (hasSearchValue) { + if (this.expandedNodes.length === 0) { + this.expandedNodes = this.treeControl.expansionModel.selected; + this.treeControl.expansionModel.clear(); + } + this.treeControl.expansionModel.select(...result.needsToExpanded); + } else { + if (this.expandedNodes.length) { + this.treeControl.expansionModel.clear(); + this.treeControl.expansionModel.select(...this.expandedNodes); + this.expandedNodes = []; + } + } + }); + } + + hasChild = (_: number, node: FlatNode) => node.expandable; +} diff --git a/components/tree-view/demo/virtual-scroll.md b/components/tree-view/demo/virtual-scroll.md new file mode 100644 index 00000000000..9b4af611654 --- /dev/null +++ b/components/tree-view/demo/virtual-scroll.md @@ -0,0 +1,14 @@ +--- +order: 7 +title: + zh-CN: 虚拟滚动 + en-US: Virtual Scroll +--- + +## zh-CN + +使用虚拟滚动。 + +## en-US + +Use virtual scroll. diff --git a/components/tree-view/demo/virtual-scroll.ts b/components/tree-view/demo/virtual-scroll.ts new file mode 100644 index 00000000000..fd37730ec4e --- /dev/null +++ b/components/tree-view/demo/virtual-scroll.ts @@ -0,0 +1,97 @@ +import { FlatTreeControl } from '@angular/cdk/tree'; +import { AfterViewInit, Component } from '@angular/core'; + +import { NzTreeFlatDataSource, NzTreeFlattener } from 'ng-zorro-antd/tree-view'; + +interface FoodNode { + name: string; + children?: FoodNode[]; +} + +function dig(path: string = '0', level: number = 3): FoodNode[] { + const list: FoodNode[] = []; + for (let i = 0; i < 10; i += 1) { + const name = `${path}-${i}`; + const treeNode: FoodNode = { + name + }; + + if (level > 0) { + treeNode.children = dig(name, level - 1); + } + + list.push(treeNode); + } + return list; +} + +const TREE_DATA: FoodNode[] = dig(); + +/** Flat node with expandable and level information */ +interface ExampleFlatNode { + expandable: boolean; + name: string; + level: number; +} + +@Component({ + selector: 'nz-demo-tree-view-virtual-scroll', + template: ` + + + + {{ node.name }} + + + + + + + {{ node.name }} + + + `, + styles: [ + ` + .virtual-scroll-tree { + height: 200px; + } + ` + ] +}) +export class NzDemoTreeViewVirtualScrollComponent implements AfterViewInit { + private transformer = (node: FoodNode, level: number) => { + return { + expandable: !!node.children && node.children.length > 0, + name: node.name, + level: level + }; + }; + + treeControl = new FlatTreeControl( + node => node.level, + node => node.expandable + ); + + treeFlattener = new NzTreeFlattener( + this.transformer, + node => node.level, + node => node.expandable, + node => node.children + ); + + dataSource = new NzTreeFlatDataSource(this.treeControl, this.treeFlattener); + + constructor() { + this.dataSource.setData(TREE_DATA); + this.treeControl.expandAll(); + } + + hasChild = (_: number, node: ExampleFlatNode) => node.expandable; + + ngAfterViewInit(): void {} + + getNode(name: string): ExampleFlatNode | null { + return this.treeControl.dataNodes.find(n => n.name === name) || null; + } +} diff --git a/components/tree-view/doc/index.en-US.md b/components/tree-view/doc/index.en-US.md new file mode 100644 index 00000000000..779cd5e00b7 --- /dev/null +++ b/components/tree-view/doc/index.en-US.md @@ -0,0 +1,108 @@ +--- +category: Components +type: Data Display +title: Tree View +cover: https://gw.alipayobjects.com/zos/alicdn/Xh-oWqg9k/Tree.svg +--- + +## When To Use + +More basic Tree component, allowing each of its parts to be defined in the template, and state to be managed manually. With better performance and customizability. + +```ts +import { NzTreeViewModule } from 'ng-zorro-antd/tree-view'; +``` + +## API + +### nz-tree-view + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| [nzTreeControl] | The tree controller | [TreeControl](https://material.angular.io/cdk/tree/api#TreeControl) | - | +| [nzDataSource] | The data array to render | [DataSource](https://material.angular.io/cdk/tree/overview#data-source)<T> \| Observable \| T[] | - | +| [nzDirectoryTree] | Whether nodes are displayed as directory style | `boolean` | `false` | +| [nzBlockNode] | Whether treeNode fill remaining horizontal space| `boolean` | `false` | + +### nz-tree-virtual-scroll-view + +The virtual scroll tree vie, can be accessed from the [CdkVirtualScrollViewport](https://material.angular.io/cdk/scrolling/api#CdkVirtualScrollViewport) instance through the `virtualScrollViewport` member on the component instance. + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| [nzTreeControl] | The tree controller | [TreeControl](https://material.angular.io/cdk/tree/api#TreeControl) | - | +| [nzDataSource] | The data array to render | [DataSource](https://material.angular.io/cdk/tree/overview#data-source)<T> \| Observable \| T[] | - | +| [nzDirectoryTree] | Whether nodes are displayed as directory style | `boolean` | `false` | +| [nzBlockNode] | Whether treeNode fill remaining horizontal space| `boolean` | `false` | +| [nzNodeWidth] | The width of the nodes in the tree (in pixels) | `number` | `28` | +| [nzMinBufferPx] | The minimum amount of buffer rendered beyond the viewport (in pixels) | `number` | `28 * 5` | +| [nzMaxBufferPx] | The number of pixels worth of buffer to render for when rendering new nodes | `number` | `28 * 10` | + +### [nzTreeNodeDef] + +Directive to define `nz-tree-node`. + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| [nzTreeNodeDefWhen] | Function that should return true if this node template should be used for the provided node data and index. If left undefined, this node will be considered the default node template to use when no other when functions return true for the data. For every node, there must be at least one when function that passes or an undefined to default. | `(index: number, nodeData: T) => boolean` | - | + + +### nz-tree-node + +The tree node container component, which needs to be defined by the `nzTreeNodeDef` directive. +### [nzTreeNodePadding] + +```html + +``` + +Show node indentation by adding `padding` **Best Performance**. + +### nzTreeNodeIndentLine + +```html + +``` + +Show node indentation by adding indent lines. + +### nz-tree-node-toggle + +Toggle part, used to expand / collapse the node. + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| [nzTreeNodeToggleRecursive] | Is it recursively expand / collapse | `boolean` | `false` | + +### nz-tree-node-toggle[nzTreeNodeNoopToggle] + +Does not do anything to the toggle section, available with a placeholder or display icons. + +### [nz-icon][nzTreeNodeToggleRotateIcon] + +Define icons in the toggle part, which will automatically rotate with the collapse/expand state. + +### [nz-icon][nzTreeNodeToggleActiveIcon] + +Defines the icons in the toggle section to have an active style, which can be used for loading icon. + +### nz-tree-node-option + +Defines the selectable part of a node. + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| [nzSelected] | Whether the option is selected | `boolean` | `false` | +| [nzDisabled] | Whether the option is disabled | `boolean` | `false` | +| (nzClick) | Event on click | `EventEmitter` | - | + +### nz-tree-node-checkbox + +Defines the checkable parts of a node. + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| [nzChecked] | Whether the checkbox is checked | `boolean` | `false` | +| [nzDisabled] | Whether the checkbox is disabled | `boolean` | `false` | +| [nzIndeterminate] | Whether the checkbox is indeterminate | `boolean` | `false` | +| (nzClick) | Event on click | `EventEmitter` | - | diff --git a/components/tree-view/doc/index.zh-CN.md b/components/tree-view/doc/index.zh-CN.md new file mode 100644 index 00000000000..ad8b9fa1360 --- /dev/null +++ b/components/tree-view/doc/index.zh-CN.md @@ -0,0 +1,110 @@ +--- +category: Components +type: 数据展示 +title: Tree View +subtitle: 树视图 +cover: https://gw.alipayobjects.com/zos/alicdn/Xh-oWqg9k/Tree.svg +--- + +## 何时使用 + +更基础的 Tree 组件,允许在模版中定义每个组成部分,并手动管理状态。相比封装好的 Tree 组件定制度更高,也更容易更具不同的需求获得更好的性能。 + +```ts +import { NzTreeViewModule } from 'ng-zorro-antd/tree-view'; +``` + +## API + +### nz-tree-view + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| [nzTreeControl] | 树控制器 | [TreeControl](https://material.angular.io/cdk/tree/api#TreeControl) | - | +| [nzDataSource] | 用于渲染树的数组数据 | [DataSource](https://material.angular.io/cdk/tree/overview#data-source)<T> \| Observable \| T[] | - | +| [nzDirectoryTree] | 节点是否以文件夹样式显示 | boolean | `false` | +| [nzBlockNode] | 节点是否占据整行| boolean | `false` | + +### nz-tree-virtual-scroll-view + +虚拟滚动的树视图,可以通过组件实例上的 `virtualScrollViewport` 成员访问 [CdkVirtualScrollViewport](https://material.angular.io/cdk/scrolling/api#CdkVirtualScrollViewport) 实例。 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| [nzTreeControl] | 树控制器 | [TreeControl](https://material.angular.io/cdk/tree/api#TreeControl) | - | +| [nzDataSource] | 用于渲染树的数组数据 | [DataSource](https://material.angular.io/cdk/tree/overview#data-source)<T> \| Observable \| T[] | - | +| [nzDirectoryTree] | 节点是否以文件夹样式显示 | `boolean` | `false` | +| [nzBlockNode] | 节点是否占据整行| `boolean` | `false` | +| [nzNodeWidth] | 节点的宽度(px) | `number` | `28` | +| [nzMinBufferPx] | 超出渲染区的最小缓存区大小(px) | `number` | `28 * 5` | +| [nzMaxBufferPx] | 需要渲染新节点时的缓冲区大小(px) | `number` | `28 * 10` | + +### [nzTreeNodeDef] + +用于定义 `nz-tree-node` 的指令。 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| [nzTreeNodeDefWhen] | 用于定义是否使用此节点的方法,优先匹配第一个返回 `true` 的节点,如果没有则匹配未定义此参数的节点 | `(index: number, nodeData: T) => boolean` | - | + + +### nz-tree-node + +树节点容器组件,需要通过 `nzTreeNodeDef` 指令定义。 + +### [nzTreeNodePadding] + +```html + +``` + +以添加 `padding` 的方式显示节点缩进 **性能最好**。 + +### nzTreeNodeIndentLine + +```html + +``` + +以添加缩进线的方式显示节点缩进。 + +### nz-tree-node-toggle + +切换部分,用于节点的展开/收起。 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| [nzTreeNodeToggleRecursive] | 是否为递归展开/收起 | `boolean` | `false` | + +### nz-tree-node-toggle[nzTreeNodeNoopToggle] + +不做任何操作的切换部分,可用与占位或者显示图标。 + +### [nz-icon][nzTreeNodeToggleRotateIcon] + +定义切换部分中的图标,会随着展开收起状态自动旋转。 + +### [nz-icon][nzTreeNodeToggleActiveIcon] + +定义切换部分中的图标,使其具有激活状态的样式,可用于 loading 图标。 + +### nz-tree-node-option + +定义节点中的可选择部分。 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| [nzSelected] | 是否选中| `boolean` | `false` | +| [nzDisabled] | 是否禁用| `boolean` | `false` | +| (nzClick) | 点击时的事件 | `EventEmitter` | - | + +### nz-tree-node-checkbox + +定义节点中的可勾选的部分。 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| [nzChecked] | 是否勾选 | `boolean` | `false` | +| [nzIndeterminate] | 是否为半选 | `boolean` | `false` | +| [nzDisabled] | 是否禁用| `boolean` | `false` | +| (nzClick) | 点击时的事件 | `EventEmitter` | - | \ No newline at end of file diff --git a/components/tree-view/indent.ts b/components/tree-view/indent.ts new file mode 100644 index 00000000000..04c2208d6e4 --- /dev/null +++ b/components/tree-view/indent.ts @@ -0,0 +1,123 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { ChangeDetectionStrategy, Component, Directive, Input, OnDestroy } from '@angular/core'; +import { animationFrameScheduler, asapScheduler, merge, Subscription } from 'rxjs'; +import { auditTime } from 'rxjs/operators'; +import { NzTreeNodeComponent } from './node'; +import { NzTreeView } from './tree'; + +import { getNextSibling, getParent } from './utils'; + +/** + * [true, false, false, true] => 1001 + */ +function booleanArrayToString(arr: boolean[]): string { + return arr.map(i => (i ? 1 : 0)).join(''); +} + +const BUILD_INDENTS_SCHEDULER = typeof requestAnimationFrame !== 'undefined' ? animationFrameScheduler : asapScheduler; + +@Component({ + selector: 'nz-tree-node-indents', + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'ant-tree-indent' + } +}) +export class NzTreeNodeIndentsComponent { + @Input() indents: boolean[] = []; +} + +@Directive({ + selector: 'nz-tree-node[nzTreeNodeIndentLine]', + host: { + class: 'ant-tree-show-line', + '[class.ant-tree-treenode-leaf-last]': 'isLast && isLeaf' + } +}) +export class NzTreeNodeIndentLineDirective implements OnDestroy { + isLast: boolean | 'unset' = 'unset'; + isLeaf = false; + private preNodeRef: T | null = null; + private nextNodeRef: T | null = null; + private currentIndents: string = ''; + private changeSubscription: Subscription; + + constructor(private treeNode: NzTreeNodeComponent, private tree: NzTreeView) { + this.buildIndents(); + this.checkLast(); + + /** + * The dependent data (TreeControl.dataNodes) can be set after node instantiation, + * and setting the indents can cause frame rate loss if it is set too often. + */ + this.changeSubscription = merge(this.treeNode._dataChanges, tree._dataSourceChanged) + .pipe(auditTime(0, BUILD_INDENTS_SCHEDULER)) + .subscribe(() => { + this.buildIndents(); + this.checkAdjacent(); + }); + } + + private getIndents(): boolean[] { + const indents = []; + const nodes = this.tree.treeControl.dataNodes; + const getLevel = this.tree.treeControl.getLevel; + let parent = getParent(nodes, this.treeNode.data, getLevel); + while (parent) { + const parentNextSibling = getNextSibling(nodes, parent, getLevel); + if (parentNextSibling) { + indents.unshift(true); + } else { + indents.unshift(false); + } + parent = getParent(nodes, parent, getLevel); + } + return indents; + } + + private buildIndents(): void { + if (this.treeNode.data) { + const indents = this.getIndents(); + const diffString = booleanArrayToString(indents); + if (diffString !== this.currentIndents) { + this.treeNode.setIndents(this.getIndents()); + this.currentIndents = diffString; + } + } + } + + /** + * We need to add an class name for the last child node, + * this result can also be affected when the adjacent nodes are changed. + */ + private checkAdjacent(): void { + const nodes = this.tree.treeControl.dataNodes; + const index = nodes.indexOf(this.treeNode.data); + const preNode = nodes[index - 1] || null; + const nextNode = nodes[index + 1] || null; + if (this.nextNodeRef !== nextNode || this.preNodeRef !== preNode) { + this.checkLast(index); + } + this.preNodeRef = preNode; + this.nextNodeRef = nextNode; + } + + private checkLast(index?: number): void { + const nodes = this.tree.treeControl.dataNodes; + this.isLeaf = this.treeNode.isLeaf; + this.isLast = !getNextSibling(nodes, this.treeNode.data, this.tree.treeControl.getLevel, index); + } + + ngOnDestroy(): void { + this.preNodeRef = null; + this.nextNodeRef = null; + this.changeSubscription.unsubscribe(); + } +} diff --git a/components/tree-view/index.ts b/components/tree-view/index.ts new file mode 100644 index 00000000000..97717c1c837 --- /dev/null +++ b/components/tree-view/index.ts @@ -0,0 +1,6 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +export * from './public-api'; diff --git a/components/tree-view/node.ts b/components/tree-view/node.ts new file mode 100644 index 00000000000..87f2a49d986 --- /dev/null +++ b/components/tree-view/node.ts @@ -0,0 +1,177 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { CdkTreeNode, CdkTreeNodeDef, CdkTreeNodeOutletContext } from '@angular/cdk/tree'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Directive, + ElementRef, + EmbeddedViewRef, + Input, + OnChanges, + OnDestroy, + OnInit, + Renderer2, + SimpleChange, + SimpleChanges, + ViewContainerRef +} from '@angular/core'; + +import { NzSafeAny } from 'ng-zorro-antd/core/types'; + +import { NzTreeView } from './tree'; + +export interface NzTreeVirtualNodeData { + data: T; + context: CdkTreeNodeOutletContext; + nodeDef: CdkTreeNodeDef; +} + +@Component({ + selector: 'nz-tree-node', + exportAs: 'nzTreeNode', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [{ provide: CdkTreeNode, useExisting: NzTreeNodeComponent }], + template: ` + + + + + + + + + `, + host: { + '[class.ant-tree-treenode-switcher-open]': 'isExpanded', + '[class.ant-tree-treenode-switcher-close]': '!isExpanded' + } +}) +export class NzTreeNodeComponent extends CdkTreeNode implements OnDestroy, OnInit { + indents: boolean[] = []; + disabled = false; + selected = false; + isLeaf = false; + + constructor( + protected elementRef: ElementRef, + protected tree: NzTreeView, + private renderer: Renderer2, + private cdr: ChangeDetectorRef + ) { + super(elementRef, tree); + this._elementRef.nativeElement.classList.add('ant-tree-treenode'); + } + + ngOnInit(): void { + this.isLeaf = !this.tree.treeControl.isExpandable(this.data); + } + + disable(): void { + this.disabled = true; + this.updateDisabledClass(); + } + + enable(): void { + this.disabled = false; + this.updateDisabledClass(); + } + + select(): void { + this.selected = true; + this.updateSelectedClass(); + } + + deselect(): void { + this.selected = false; + this.updateSelectedClass(); + } + + setIndents(indents: boolean[]): void { + this.indents = indents; + this.cdr.markForCheck(); + } + + private updateSelectedClass(): void { + if (this.selected) { + this.renderer.addClass(this.elementRef.nativeElement, 'ant-tree-treenode-selected'); + } else { + this.renderer.removeClass(this.elementRef.nativeElement, 'ant-tree-treenode-selected'); + } + } + + private updateDisabledClass(): void { + if (this.disabled) { + this.renderer.addClass(this.elementRef.nativeElement, 'ant-tree-treenode-disabled'); + } else { + this.renderer.removeClass(this.elementRef.nativeElement, 'ant-tree-treenode-disabled'); + } + } +} + +@Directive({ + selector: '[nzTreeNodeDef]', + providers: [{ provide: CdkTreeNodeDef, useExisting: NzTreeNodeDefDirective }] +}) +export class NzTreeNodeDefDirective extends CdkTreeNodeDef { + @Input('nzTreeNodeDefWhen') when!: (index: number, nodeData: T) => boolean; +} + +@Directive({ + selector: '[nzTreeVirtualScrollNodeOutlet]' +}) +export class NzTreeVirtualScrollNodeOutletDirective implements OnChanges { + private _viewRef: EmbeddedViewRef | null = null; + @Input() data!: NzTreeVirtualNodeData; + + constructor(private _viewContainerRef: ViewContainerRef) {} + + ngOnChanges(changes: SimpleChanges): void { + const recreateView = this.shouldRecreateView(changes); + if (recreateView) { + const viewContainerRef = this._viewContainerRef; + + if (this._viewRef) { + viewContainerRef.remove(viewContainerRef.indexOf(this._viewRef)); + } + + this._viewRef = this.data ? viewContainerRef.createEmbeddedView(this.data.nodeDef.template, this.data.context) : null; + + if (CdkTreeNode.mostRecentTreeNode && this._viewRef) { + CdkTreeNode.mostRecentTreeNode.data = this.data.data; + } + } else if (this._viewRef && this.data.context) { + this.updateExistingContext(this.data.context); + } + } + + private shouldRecreateView(changes: SimpleChanges): boolean { + const ctxChange = changes.data; + return !!changes.data || (ctxChange && this.hasContextShapeChanged(ctxChange)); + } + + private hasContextShapeChanged(ctxChange: SimpleChange): boolean { + const prevCtxKeys = Object.keys(ctxChange.previousValue || {}); + const currCtxKeys = Object.keys(ctxChange.currentValue || {}); + + if (prevCtxKeys.length === currCtxKeys.length) { + for (const propName of currCtxKeys) { + if (prevCtxKeys.indexOf(propName) === -1) { + return true; + } + } + return false; + } + return true; + } + + private updateExistingContext(ctx: NzSafeAny): void { + for (const propName of Object.keys(ctx)) { + this._viewRef!.context[propName] = (this.data.context as NzSafeAny)[propName]; + } + } +} diff --git a/components/tree-view/option.ts b/components/tree-view/option.ts new file mode 100644 index 00000000000..f0ac57296b8 --- /dev/null +++ b/components/tree-view/option.ts @@ -0,0 +1,63 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { BooleanInput } from 'ng-zorro-antd/core/types'; +import { InputBoolean } from 'ng-zorro-antd/core/util'; + +import { NzTreeNodeComponent } from './node'; + +@Component({ + selector: 'nz-tree-node-option', + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'ant-tree-node-content-wrapper', + '[class.ant-tree-node-content-wrapper-open]': 'isExpanded', + '[class.ant-tree-node-selected]': 'nzSelected', + '(click)': 'onClick($event)' + } +}) +export class NzTreeNodeOptionComponent implements OnChanges { + static ngAcceptInputType_nzSelected: BooleanInput; + static ngAcceptInputType_nzDisabled: BooleanInput; + + @Input() @InputBoolean() nzSelected = false; + @Input() @InputBoolean() nzDisabled = false; + @Output() readonly nzClick = new EventEmitter(); + + constructor(private treeNode: NzTreeNodeComponent) {} + + get isExpanded(): boolean { + return this.treeNode.isExpanded; + } + + onClick(e: MouseEvent): void { + if (!this.nzDisabled) { + this.nzClick.emit(e); + } + } + + ngOnChanges(changes: SimpleChanges): void { + const { nzDisabled, nzSelected } = changes; + if (nzDisabled) { + if (nzDisabled.currentValue) { + this.treeNode.disable(); + } else { + this.treeNode.enable(); + } + } + + if (nzSelected) { + if (nzSelected.currentValue) { + this.treeNode.select(); + } else { + this.treeNode.deselect(); + } + } + } +} diff --git a/components/tree-view/outlet.ts b/components/tree-view/outlet.ts new file mode 100644 index 00000000000..dec72f1b98f --- /dev/null +++ b/components/tree-view/outlet.ts @@ -0,0 +1,22 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { CdkTreeNodeOutlet, CDK_TREE_NODE_OUTLET_NODE } from '@angular/cdk/tree'; +import { Directive, Inject, Optional, ViewContainerRef } from '@angular/core'; + +import { NzSafeAny } from 'ng-zorro-antd/core/types'; + +@Directive({ + selector: '[nzTreeNodeOutlet]', + providers: [ + { + provide: CdkTreeNodeOutlet, + useExisting: NzTreeNodeOutletDirective + } + ] +}) +export class NzTreeNodeOutletDirective implements CdkTreeNodeOutlet { + constructor(public viewContainer: ViewContainerRef, @Inject(CDK_TREE_NODE_OUTLET_NODE) @Optional() public _node?: NzSafeAny) {} +} diff --git a/components/tree-view/package.json b/components/tree-view/package.json new file mode 100644 index 00000000000..ded1e7a9fdf --- /dev/null +++ b/components/tree-view/package.json @@ -0,0 +1,7 @@ +{ + "ngPackage": { + "lib": { + "entryFile": "public-api.ts" + } + } +} diff --git a/components/tree-view/padding.ts b/components/tree-view/padding.ts new file mode 100644 index 00000000000..c68d3afb2b8 --- /dev/null +++ b/components/tree-view/padding.ts @@ -0,0 +1,31 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { CdkTreeNodePadding } from '@angular/cdk/tree'; +import { Directive, Input } from '@angular/core'; + +@Directive({ + selector: '[nzTreeNodePadding]', + providers: [{ provide: CdkTreeNodePadding, useExisting: NzTreeNodePaddingDirective }] +}) +export class NzTreeNodePaddingDirective extends CdkTreeNodePadding { + _indent = 24; + + @Input('nzTreeNodePadding') + get level(): number { + return this._level; + } + set level(value: number) { + this._setLevelInput(value); + } + + @Input('nzTreeNodePaddingIndent') + get indent(): number | string { + return this._indent; + } + set indent(indent: number | string) { + this._setIndentInput(indent); + } +} diff --git a/components/tree-view/public-api.ts b/components/tree-view/public-api.ts new file mode 100644 index 00000000000..2c4eb14131d --- /dev/null +++ b/components/tree-view/public-api.ts @@ -0,0 +1,17 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +export * from './tree-view.module'; +export * from './checkbox'; +export * from './utils'; +export * from './data-source'; +export * from './indent'; +export * from './node'; +export * from './option'; +export * from './outlet'; +export * from './padding'; +export * from './toggle'; +export * from './tree-view'; +export * from './tree-virtual-scroll-view'; diff --git a/components/tree-view/style/entry.less b/components/tree-view/style/entry.less new file mode 100644 index 00000000000..7c3b14a9d83 --- /dev/null +++ b/components/tree-view/style/entry.less @@ -0,0 +1 @@ +@import './patch.less'; diff --git a/components/tree-view/style/patch.less b/components/tree-view/style/patch.less new file mode 100644 index 00000000000..1bebb559128 --- /dev/null +++ b/components/tree-view/style/patch.less @@ -0,0 +1,23 @@ +/* + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +nz-tree-virtual-scroll-view { + display: block; + position: relative; + overflow: auto; + contain: strict; + transform: translateZ(0); + will-change: scroll-position; + -webkit-overflow-scrolling: touch; + .ant-tree-list, .ant-tree-list-holder-inner { + height: 100%; + } +} + +nz-tree-virtual-scroll-view, nz-tree-view { + .ant-tree-switcher + .ant-tree-switcher.nz-tree-leaf-line-icon { + display: none; + } +} diff --git a/components/tree-view/toggle.ts b/components/tree-view/toggle.ts new file mode 100644 index 00000000000..ea05c74daf7 --- /dev/null +++ b/components/tree-view/toggle.ts @@ -0,0 +1,57 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { CdkTreeNodeToggle } from '@angular/cdk/tree'; +import { Directive, Input } from '@angular/core'; +import { BooleanInput } from 'ng-zorro-antd/core/types'; + +@Directive({ + selector: 'nz-tree-node-toggle[nzTreeNodeNoopToggle], [nzTreeNodeNoopToggle]', + host: { + class: 'ant-tree-switcher ant-tree-switcher-noop' + } +}) +export class NzTreeNodeNoopToggleDirective {} + +@Directive({ + selector: 'nz-tree-node-toggle:not([nzTreeNodeNoopToggle]), [nzTreeNodeToggle]', + providers: [{ provide: CdkTreeNodeToggle, useExisting: NzTreeNodeToggleDirective }], + host: { + class: 'ant-tree-switcher', + '[class.ant-tree-switcher_open]': 'isExpanded', + '[class.ant-tree-switcher_close]': '!isExpanded' + } +}) +export class NzTreeNodeToggleDirective extends CdkTreeNodeToggle { + static ngAcceptInputType_recursive: BooleanInput; + @Input('nzTreeNodeToggleRecursive') + get recursive(): boolean { + return this._recursive; + } + set recursive(value: boolean) { + this._recursive = coerceBooleanProperty(value); + } + + get isExpanded(): boolean { + return this._treeNode.isExpanded; + } +} + +@Directive({ + selector: '[nz-icon][nzTreeNodeToggleRotateIcon]', + host: { + class: 'ant-tree-switcher-icon' + } +}) +export class NzTreeNodeToggleRotateIconDirective {} + +@Directive({ + selector: '[nz-icon][nzTreeNodeToggleActiveIcon]', + host: { + class: 'ant-tree-switcher-loading-icon' + } +}) +export class NzTreeNodeToggleActiveIconDirective {} diff --git a/components/tree-view/tree-view.module.ts b/components/tree-view/tree-view.module.ts new file mode 100644 index 00000000000..f39d6344032 --- /dev/null +++ b/components/tree-view/tree-view.module.ts @@ -0,0 +1,52 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { NzNoAnimationModule } from 'ng-zorro-antd/core/no-animation'; + +import { NzTreeNodeCheckboxComponent } from './checkbox'; +import { NzTreeNodeIndentLineDirective, NzTreeNodeIndentsComponent } from './indent'; +import { NzTreeNodeComponent, NzTreeNodeDefDirective, NzTreeVirtualScrollNodeOutletDirective } from './node'; +import { NzTreeNodeOptionComponent } from './option'; +import { NzTreeNodeOutletDirective } from './outlet'; +import { NzTreeNodePaddingDirective } from './padding'; +import { + NzTreeNodeNoopToggleDirective, + NzTreeNodeToggleActiveIconDirective, + NzTreeNodeToggleDirective, + NzTreeNodeToggleRotateIconDirective +} from './toggle'; +import { NzTreeView } from './tree'; +import { NzTreeViewComponent } from './tree-view'; +import { NzTreeVirtualScrollViewComponent } from './tree-virtual-scroll-view'; + +const treeWithControlComponents = [ + NzTreeView, + NzTreeNodeOutletDirective, + NzTreeViewComponent, + NzTreeNodeDefDirective, + NzTreeNodeComponent, + NzTreeNodeToggleDirective, + NzTreeNodePaddingDirective, + NzTreeNodeToggleRotateIconDirective, + NzTreeNodeToggleActiveIconDirective, + NzTreeNodeOptionComponent, + NzTreeNodeNoopToggleDirective, + NzTreeNodeCheckboxComponent, + NzTreeNodeIndentsComponent, + NzTreeVirtualScrollViewComponent, + NzTreeVirtualScrollNodeOutletDirective, + NzTreeNodeIndentLineDirective +]; + +@NgModule({ + imports: [CommonModule, NzNoAnimationModule, ScrollingModule], + declarations: [treeWithControlComponents], + exports: [treeWithControlComponents] +}) +export class NzTreeViewModule {} diff --git a/components/tree-view/tree-view.ts b/components/tree-view/tree-view.ts new file mode 100644 index 00000000000..9ca18db2588 --- /dev/null +++ b/components/tree-view/tree-view.ts @@ -0,0 +1,51 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { CdkTree } from '@angular/cdk/tree'; +import { AfterViewInit, ChangeDetectionStrategy, Component, ViewChild, ViewEncapsulation } from '@angular/core'; + +import { treeCollapseMotion } from 'ng-zorro-antd/core/animation'; + +import { NzTreeNodeOutletDirective } from './outlet'; +import { NzTreeView } from './tree'; + +@Component({ + selector: 'nz-tree-view', + exportAs: 'nzTreeView', + template: ` +
+
+ +
+
+ `, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { provide: CdkTree, useExisting: NzTreeViewComponent }, + { provide: NzTreeView, useExisting: NzTreeViewComponent } + ], + host: { + class: 'ant-tree', + '[class.ant-tree-block-node]': 'nzDirectoryTree || nzBlockNode', + '[class.ant-tree-directory]': 'nzDirectoryTree' + }, + animations: [treeCollapseMotion] +}) +export class NzTreeViewComponent extends NzTreeView implements AfterViewInit { + @ViewChild(NzTreeNodeOutletDirective, { static: true }) nodeOutlet!: NzTreeNodeOutletDirective; + _afterViewInit = false; + ngAfterViewInit(): void { + Promise.resolve().then(() => { + this._afterViewInit = true; + this.changeDetectorRef.markForCheck(); + }); + } +} diff --git a/components/tree-view/tree-virtual-scroll-view.ts b/components/tree-view/tree-virtual-scroll-view.ts new file mode 100644 index 00000000000..0225136d6df --- /dev/null +++ b/components/tree-view/tree-virtual-scroll-view.ts @@ -0,0 +1,73 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; +import { CdkTree, CdkTreeNodeOutletContext } from '@angular/cdk/tree'; +import { ChangeDetectionStrategy, Component, Input, ViewChild, ViewEncapsulation } from '@angular/core'; + +import { NzTreeVirtualNodeData } from './node'; +import { NzTreeNodeOutletDirective } from './outlet'; + +import { NzTreeView } from './tree'; + +@Component({ + selector: 'nz-tree-virtual-scroll-view', + exportAs: 'nzTreeVirtualScrollView', + template: ` +
+ + + + + +
+ + `, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { provide: NzTreeView, useExisting: NzTreeVirtualScrollViewComponent }, + { provide: CdkTree, useExisting: NzTreeVirtualScrollViewComponent } + ], + host: { + class: 'ant-tree', + '[class.ant-tree-block-node]': 'nzDirectoryTree || nzBlockNode', + '[class.ant-tree-directory]': 'nzDirectoryTree' + } +}) +export class NzTreeVirtualScrollViewComponent extends NzTreeView { + @ViewChild(NzTreeNodeOutletDirective, { static: true }) nodeOutlet!: NzTreeNodeOutletDirective; + @ViewChild(CdkVirtualScrollViewport, { static: true }) virtualScrollViewport!: CdkVirtualScrollViewport; + + @Input() nzNodeWidth = 28; + @Input() nzMinBufferPx = 28 * 5; + @Input() nzMaxBufferPx = 28 * 10; + + nodes: Array> = []; + + renderNodeChanges(data: T[] | ReadonlyArray): void { + this.nodes = new Array(...data).map((n, i) => this.createNode(n, i)); + } + + private createNode(nodeData: T, index: number): NzTreeVirtualNodeData { + const node = this._getNodeDef(nodeData, index); + const context = new CdkTreeNodeOutletContext(nodeData); + if (this.treeControl.getLevel) { + context.level = this.treeControl.getLevel(nodeData); + } else { + context.level = 0; + } + return { + data: nodeData, + context, + nodeDef: node + }; + } +} diff --git a/components/tree-view/tree.ts b/components/tree-view/tree.ts new file mode 100644 index 00000000000..707f73ebef8 --- /dev/null +++ b/components/tree-view/tree.ts @@ -0,0 +1,45 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { DataSource } from '@angular/cdk/collections'; +import { CdkTree, TreeControl } from '@angular/cdk/tree'; +import { ChangeDetectorRef, Component, Host, Input, IterableDiffer, IterableDiffers, Optional, ViewContainerRef } from '@angular/core'; +import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation'; + +import { Observable, Subject } from 'rxjs'; + +import { BooleanInput, NzSafeAny } from 'ng-zorro-antd/core/types'; +import { InputBoolean } from 'ng-zorro-antd/core/util'; + +@Component({ template: '' }) +// tslint:disable-next-line: component-class-suffix +export class NzTreeView extends CdkTree { + static ngAcceptInputType_nzDirectoryTree: BooleanInput; + static ngAcceptInputType_nzBlockNode: BooleanInput; + + _dataSourceChanged = new Subject(); + @Input('nzTreeControl') treeControl!: TreeControl; + @Input('nzDataSource') + get dataSource(): DataSource | Observable | T[] { + return super.dataSource; + } + set dataSource(dataSource: DataSource | Observable | T[]) { + super.dataSource = dataSource; + } + @Input() @InputBoolean() nzDirectoryTree = false; + @Input() @InputBoolean() nzBlockNode = false; + + constructor( + protected differs: IterableDiffers, + protected changeDetectorRef: ChangeDetectorRef, + @Host() @Optional() public noAnimation?: NzNoAnimationDirective + ) { + super(differs, changeDetectorRef); + } + renderNodeChanges(data: T[] | ReadonlyArray, dataDiffer?: IterableDiffer, viewContainer?: ViewContainerRef, parentData?: T): void { + super.renderNodeChanges(data, dataDiffer, viewContainer, parentData); + this._dataSourceChanged.next(); + } +} diff --git a/components/tree-view/utils.ts b/components/tree-view/utils.ts new file mode 100644 index 00000000000..4d6faaa5094 --- /dev/null +++ b/components/tree-view/utils.ts @@ -0,0 +1,41 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +export function getParent(nodes: T[], node: T, getLevel: (dataNode: T) => number): T | null { + let index = nodes.indexOf(node); + if (index < 0) { + return null; + } + const level = getLevel(node); + for (index--; index >= 0; index--) { + const preLevel = getLevel(nodes[index]); + if (preLevel + 1 === level) { + return nodes[index]; + } + if (preLevel + 1 < level) { + return null; + } + } + return null; +} + +export function getNextSibling(nodes: T[], node: T, getLevel: (dataNode: T) => number, _index?: number): T | null { + let index = typeof _index !== 'undefined' ? _index : nodes.indexOf(node); + if (index < 0) { + return null; + } + const level = getLevel(node); + + for (index++; index < nodes.length; index++) { + const nextLevel = getLevel(nodes[index]); + if (nextLevel < level) { + return null; + } + if (nextLevel === level) { + return nodes[index]; + } + } + return null; +} diff --git a/components/tree/demo/basic.ts b/components/tree/demo/basic.ts index ab285c3dccb..042385c39c2 100644 --- a/components/tree/demo/basic.ts +++ b/components/tree/demo/basic.ts @@ -15,8 +15,7 @@ import { NzFormatEmitEvent, NzTreeComponent, NzTreeNodeOptions } from 'ng-zorro- (nzContextMenu)="nzClick($event)" (nzCheckBoxChange)="nzCheck($event)" (nzExpandChange)="nzCheck($event)" - > - + > ` }) export class NzDemoTreeBasicComponent implements AfterViewInit { diff --git a/components/tree/tree-node-checkbox.component.ts b/components/tree/tree-node-checkbox.component.ts index 31ed2bdc502..c1dc411fb15 100644 --- a/components/tree/tree-node-checkbox.component.ts +++ b/components/tree/tree-node-checkbox.component.ts @@ -6,8 +6,10 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; @Component({ - selector: 'nz-tree-node-checkbox', - template: ` `, + selector: 'nz-tree-node-builtin-checkbox', + template: ` + + `, changeDetection: ChangeDetectionStrategy.OnPush, preserveWhitespaces: false, host: { @@ -21,7 +23,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; '[class.ant-tree-checkbox-disabled]': `!nzSelectMode && (isDisabled || isDisableCheckbox)` } }) -export class NzTreeNodeCheckboxComponent { +export class NzTreeNodeBuiltinCheckboxComponent { @Input() nzSelectMode = false; @Input() isChecked?: boolean; @Input() isHalfChecked?: boolean; diff --git a/components/tree/tree-node.component.ts b/components/tree/tree-node.component.ts index ca391299dc5..31c3ba21366 100644 --- a/components/tree/tree-node.component.ts +++ b/components/tree/tree-node.component.ts @@ -30,8 +30,8 @@ import { fromEvent, Observable, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ - selector: 'nz-tree-node', - exportAs: 'nzTreeNode', + selector: 'nz-tree-builtin-node', + exportAs: 'nzTreeBuiltinNode', template: ` - + > - + > `, changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/components/tree/tree.module.ts b/components/tree/tree.module.ts index 04757842e6f..02c4bed0ed8 100644 --- a/components/tree/tree.module.ts +++ b/components/tree/tree.module.ts @@ -10,23 +10,24 @@ import { NzHighlightModule } from 'ng-zorro-antd/core/highlight'; import { NzNoAnimationModule } from 'ng-zorro-antd/core/no-animation'; import { NzOutletModule } from 'ng-zorro-antd/core/outlet'; import { NzIconModule } from 'ng-zorro-antd/icon'; + import { NzTreeIndentComponent } from './tree-indent.component'; -import { NzTreeNodeCheckboxComponent } from './tree-node-checkbox.component'; +import { NzTreeNodeBuiltinCheckboxComponent } from './tree-node-checkbox.component'; import { NzTreeNodeSwitcherComponent } from './tree-node-switcher.component'; import { NzTreeNodeTitleComponent } from './tree-node-title.component'; -import { NzTreeNodeComponent } from './tree-node.component'; +import { NzTreeNodeBuiltinComponent } from './tree-node.component'; import { NzTreeComponent } from './tree.component'; @NgModule({ imports: [CommonModule, NzOutletModule, NzIconModule, NzNoAnimationModule, NzHighlightModule, ScrollingModule], declarations: [ NzTreeComponent, - NzTreeNodeComponent, + NzTreeNodeBuiltinComponent, NzTreeIndentComponent, NzTreeNodeSwitcherComponent, - NzTreeNodeCheckboxComponent, + NzTreeNodeBuiltinCheckboxComponent, NzTreeNodeTitleComponent ], - exports: [NzTreeComponent, NzTreeNodeComponent, NzTreeIndentComponent] + exports: [NzTreeComponent, NzTreeNodeBuiltinComponent, NzTreeIndentComponent] }) export class NzTreeModule {} diff --git a/components/tree/tree.spec.ts b/components/tree/tree.spec.ts index ba7f5de5b47..5aaee13c2bd 100644 --- a/components/tree/tree.spec.ts +++ b/components/tree/tree.spec.ts @@ -10,7 +10,7 @@ import { NzSafeAny } from 'ng-zorro-antd/core/types'; import { NzIconTestModule } from 'ng-zorro-antd/icon/testing'; import { Observable, of } from 'rxjs'; -import { NzTreeNodeComponent } from './tree-node.component'; +import { NzTreeNodeBuiltinComponent } from './tree-node.component'; import { NzTreeComponent } from './tree.component'; import { NzTreeModule } from './tree.module'; @@ -18,7 +18,7 @@ import Spy = jasmine.Spy; const prepareTest = (componentInstance?: NzSafeAny): ComponentBed => { return createComponentBed(componentInstance, { - declarations: [NzTreeNodeComponent], + declarations: [NzTreeNodeBuiltinComponent], providers: [], imports: [NzTreeModule, NoopAnimationsModule, FormsModule, ReactiveFormsModule, NzIconTestModule] }); @@ -37,7 +37,7 @@ describe('tree', () => { describe('basic tree under default value', () => { it('basic initial data', () => { const { nativeElement } = testBed; - const shownNodes = nativeElement.querySelectorAll('nz-tree-node'); + const shownNodes = nativeElement.querySelectorAll('nz-tree-builtin-node'); const enableCheckbox = nativeElement.querySelectorAll('.ant-tree-checkbox'); expect(shownNodes.length).toEqual(3); expect(enableCheckbox.length).toEqual(3); @@ -45,7 +45,7 @@ describe('tree', () => { it('should initialize properly', () => { const { nativeElement } = testBed; - const shownNodes = nativeElement.querySelectorAll('nz-tree-node'); + const shownNodes = nativeElement.querySelectorAll('nz-tree-builtin-node'); const enableCheckbox = nativeElement.querySelectorAll('.ant-tree-checkbox'); expect(shownNodes.length).toEqual(3); expect(enableCheckbox.length).toEqual(3); @@ -55,7 +55,7 @@ describe('tree', () => { const { component, fixture, nativeElement } = testBed; component.defaultExpandedKeys = ['0-1']; fixture.detectChanges(); - const shownNodes = nativeElement.querySelectorAll('nz-tree-node'); + const shownNodes = nativeElement.querySelectorAll('nz-tree-builtin-node'); expect(shownNodes.length).toEqual(4); tick(300); fixture.detectChanges(); @@ -67,7 +67,7 @@ describe('tree', () => { const { component, fixture, nativeElement } = testBed; component.expandAll = true; fixture.detectChanges(); - const shownNodes = nativeElement.querySelectorAll('nz-tree-node'); + const shownNodes = nativeElement.querySelectorAll('nz-tree-builtin-node'); expect(shownNodes.length).toEqual(7); tick(300); fixture.detectChanges(); @@ -181,7 +181,7 @@ describe('tree', () => { tick(); fixture.detectChanges(); // 0-1 0-2 hidden, others are not shown because not expanded - const hiddenNodes = nativeElement.querySelectorAll('nz-tree-node[style*="display: none;"]'); + const hiddenNodes = nativeElement.querySelectorAll('nz-tree-builtin-node[style*="display: none;"]'); expect(hiddenNodes.length).toEqual(2); })); }); @@ -379,7 +379,7 @@ describe('tree', () => { dispatchMouseEvent(dragNode, 'dragstart'); fixture.detectChanges(); expect(dragStartSpy).toHaveBeenCalledTimes(1); - let shownNodes = nativeElement.querySelectorAll('nz-tree-node'); + let shownNodes = nativeElement.querySelectorAll('nz-tree-builtin-node'); expect(shownNodes.length).toEqual(3); // ============ dragenter ============== @@ -407,7 +407,7 @@ describe('tree', () => { fixture.detectChanges(); // dragenter expands 0-1/0-1 - shownNodes = nativeElement.querySelectorAll('nz-tree-node'); + shownNodes = nativeElement.querySelectorAll('nz-tree-builtin-node'); expect(shownNodes.length).toEqual(7); })); @@ -437,7 +437,7 @@ describe('tree', () => { // ============ dragstart ============== dispatchMouseEvent(dragNode, 'dragstart'); fixture.detectChanges(); - let shownNodes = nativeElement.querySelectorAll('nz-tree-node'); + let shownNodes = nativeElement.querySelectorAll('nz-tree-builtin-node'); expect(shownNodes.length).toEqual(3); // ============ dragenter ============== @@ -448,7 +448,7 @@ describe('tree', () => { // =========== dragover with different position =========== // drag-over-gap-top dispatchMouseEvent(passedNode, 'dragover', 300, 340); - elementNode = nativeElement.querySelector('nz-tree-node:nth-child(2)') as HTMLElement; + elementNode = nativeElement.querySelector('nz-tree-builtin-node:nth-child(2)') as HTMLElement; expect(elementNode.classList).toContain('drag-over-gap-top'); // drag-over @@ -458,14 +458,14 @@ describe('tree', () => { // drag-over-gap-bottom dispatchMouseEvent(passedNode, 'dragover', 300, 570); - elementNode = nativeElement.querySelector('nz-tree-node:nth-child(2)') as HTMLElement; + elementNode = nativeElement.querySelector('nz-tree-builtin-node:nth-child(2)') as HTMLElement; expect(elementNode.classList).toContain('drag-over-gap-bottom'); // ======= enter check, expand passing nodes ======== expect(dragEnterSpy).toHaveBeenCalledTimes(1); expect(dragOverSpy).toHaveBeenCalledTimes(3); fixture.detectChanges(); - shownNodes = nativeElement.querySelectorAll('nz-tree-node'); + shownNodes = nativeElement.querySelectorAll('nz-tree-builtin-node'); expect(shownNodes.length).toEqual(4); })); @@ -584,8 +584,7 @@ describe('tree', () => { (nzContextMenu)="nzEvent($event)" (nzExpandChange)="nzEvent($event)" (nzCheckBoxChange)="nzEvent($event)" - > - + > @@ -664,8 +663,7 @@ export class NzTestTreeBasicControlledComponent { (nzOnDragOver)="onDragOver()" (nzOnDrop)="onDrop()" (nzOnDragEnd)="onDragEnd()" - > - + > ` }) export class NzTestTreeDraggableComponent { @@ -729,8 +727,7 @@ export class NzTestTreeDraggableComponent { [nzExpandAll]="expandAll" [nzAsyncData]="asyncData" [nzHideUnMatched]="hideUnMatched" - > - + > ` }) export class NzTestTreeBasicSearchComponent {