diff --git a/components/components.less b/components/components.less index edc88e8b005..02d62f62208 100644 --- a/components/components.less +++ b/components/components.less @@ -54,6 +54,7 @@ @import "./upload/style/entry.less"; @import "./auto-complete/style/entry.less"; @import "./cascader/style/entry.less"; +@import "./tree-view/style/entry.less"; @import "./tree/style/entry.less"; @import "./tree-select/style/entry.less"; @import "./calendar/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..76e84810963 --- /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:not([builtin])', + 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..2b60e6dc317 --- /dev/null +++ b/components/tree-view/data-source.ts @@ -0,0 +1,122 @@ +/** + * 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); + this.flatNodes(); + } + + setData(value: T[]): void { + this._data.next(value); + this.flatNodes(); + } + + 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 + } + + private flatNodes(): void { + this._flattenedData.next(this._treeFlattener.flattenNodes(this.getData())); + this._treeControl.dataNodes = this._flattenedData.value; + } +} 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..007769fba27 --- /dev/null +++ b/components/tree-view/doc/index.en-US.md @@ -0,0 +1,154 @@ +--- +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 tree nodes fill remaining horizontal space| `boolean` | `false` | + +### nz-tree-virtual-scroll-view + +The virtual scroll tree view, which can be accessed from +the [CdkVirtualScrollViewport](https://material.angular.io/cdk/scrolling/api#CdkVirtualScrollViewport) instance through +the `virtualScrollViewport` member of 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 tree nodes fill remaining horizontal space| `boolean` | `false` | +| [nzNodeWidth] | The width of nodes in the tree (in pixels) | `number` | `28` | +| [nzMinBufferPx] | The minimum amount of buffer rendered allowed outside the viewport (in pixels) | `number` | `28 * 5` | +| [nzMaxBufferPx] | The amount of buffer required for rendering new nodes (in pixels) | `number` | `28 * 10` | + +### [nzTreeNodeDef] + +Directive to define `nz-tree-node`. + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| [nzTreeNodeDefWhen] | A matching function which indicates whether inputted node should be used. It matches the very first node that makes this function return `true`. If no nodes that makes this function return `true`, the node which does not define this function would be matched instead. | `(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 + +A toggle which is used to expand / collapse the node. + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| [nzTreeNodeToggleRecursive] | Is it recursively expand / collapse | `boolean` | `false` | + +### nz-tree-node-toggle[nzTreeNodeNoopToggle] + +A toggle that does no actions. This can be used for placeholders or displays icons. + +### [nz-icon][nzTreeNodeToggleRotateIcon] + +Define an icon in the toggle, which it will automatically rotate depending on the collapse/expand state. + +### [nz-icon][nzTreeNodeToggleActiveIcon] + +Define an icon in the toggle for an active style, which it can be used for the loading state. + +### nz-tree-node-option + +Define the selectable feature 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 + +Define the checkbox feature 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` | - | + +## Classes + +### __NzTreeFlatDataSource extends DataSource__ + +### Construction Parameters + +| Name | Description | +| --- | --- | +| `treeControl: FlatTreeControl` | The tree controller. | +| `treeFlattener: NzTreeFlattener` | Flattener for convert nested nodes `T` into flattened nodes `F`. | +| `initialData: T[] = []` | Initialized data. | + +### Methods + +| Name | Description | +| --- | --- | +| `connect(collectionViewer: CollectionViewer): Observable` | Call from the TreeView component to listen for data updates. | +| `disconnect(): void` | Call when TreeView component is destroyed. | +| `setData(value: T[]): void` | Set the origin data | +| `getData(): T[]` | Get the origin data | + +### __NzTreeFlattener__ + +Convert nested data with child nodes into node data with level information. + +### Construction Parameters + +| Name | Description | +| --- | --- | +| `transformFunction: (node: T, level: number) => F` | Receive a nested node and return a flattened node | +| `getLevel: (node: F) => number` | Define the method to get the `level` property | +| `isExpandable: (node: F) => boolean` | Methods for defining whether a node is expandable | +| `getChildren: (node: T) => Observable \| T[] \| undefined \| null` | Define methods to get children nodes from nested node | + +### Methods + +| Name | Description | +| --- | --- | +| `flattenNodes(structuredData: T[]): F[]` | Receive nested data and return flattened data | +| `expandFlattenedNodes(nodes: F[], treeControl: TreeControl): F[]` | Get flattened node data based on expansion status | 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..b250b5f3368 --- /dev/null +++ b/components/tree-view/doc/index.zh-CN.md @@ -0,0 +1,147 @@ +--- +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` 的节点。如果没有返回 `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` | - | + +## Classes + +### __NzTreeFlatDataSource extends DataSource__ + +### 构造参数 +| 名称 | 说明 | +| --- | --- | +| `treeControl: FlatTreeControl` | Tree 控制器 | +| `treeFlattener: NzTreeFlattener` | 用于将嵌套节点 `T` 处理为扁平节点 `F` 的展平器 | +| `initialData: T[] = []` | 初始化数据 | + +### 方法 +| 名称 | 说明 | +| --- | --- | +| `connect(collectionViewer: CollectionViewer): Observable` | TreeView 组件中调用,用于获取数据的更新 | +| `disconnect(): void` | TreeView 组件销毁时调用 | +| `setData(value: T[]): void` | 设置原始数据 | +| `getData(): T[]` | 获取原始数据 | + +### __NzTreeFlattener__ + +将具有子节点的嵌套数据转换为具有级别(level)信息的转换器类。 + +### 构造参数 +| 名称 | 说明 | +| --- | --- | +| `transformFunction: (node: T, level: number) => F` | 接收一个嵌套节点,返回扁平节点 | +| `getLevel: (node: F) => number` | 定义获取 `level` 属性的方法 | +| `isExpandable: (node: F) => boolean` | 定义是否为可展开节点的方法 | +| `getChildren: (node: T) => Observable \| T[] \| undefined \| null` | 定义从嵌套数据中获取子节点的方法 | + +### 方法 +| 名称 | 说明 | +| --- | --- | +| `flattenNodes(structuredData: T[]): F[]` | 接收嵌套数据,返回扁平数据 | +| `expandFlattenedNodes(nodes: F[], treeControl: TreeControl): F[]` | 按 TreeControl 中的展开状态获取节点 | 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..90a0e0c9839 --- /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:not([builtin])', + 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..d2fc8c63656 --- /dev/null +++ b/components/tree-view/style/entry.less @@ -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 + */ + +@import 'index.less'; diff --git a/components/tree-view/style/index.less b/components/tree-view/style/index.less new file mode 100644 index 00000000000..1bebb559128 --- /dev/null +++ b/components/tree-view/style/index.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..a5ed7b51bb6 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-checkbox[builtin]', + 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..9af17e8771e 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-node[builtin]', + exportAs: 'nzTreeBuiltinNode', template: ` => { 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-node[builtin]'); 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-node[builtin]'); 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-node[builtin]'); 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-node[builtin]'); 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-node[builtin][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-node[builtin]'); 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-node[builtin]'); 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-node[builtin]'); 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-node[builtin]: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-node[builtin]: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-node[builtin]'); 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 {