diff --git a/components/components.less b/components/components.less
index 558c2fb19df..dfce349789d 100644
--- a/components/components.less
+++ b/components/components.less
@@ -44,4 +44,5 @@
@import "./transfer/style/index.less";
@import "./upload/style/index.less";
@import "./auto-complete/style/index.less";
-@import "./cascader/style/index.less";
\ No newline at end of file
+@import "./cascader/style/index.less";
+@import "./tree/style/index.less";
\ No newline at end of file
diff --git a/components/ng-zorro-antd.module.ts b/components/ng-zorro-antd.module.ts
index 3c46719cc39..ed48e4c5257 100644
--- a/components/ng-zorro-antd.module.ts
+++ b/components/ng-zorro-antd.module.ts
@@ -47,6 +47,7 @@ import { NzTagModule } from './tag/nz-tag.module';
import { NzTimelineModule } from './timeline/nz-timeline.module';
import { NzToolTipModule } from './tooltip/nz-tooltip.module';
import { NzTransferModule } from './transfer/nz-transfer.module';
+import { NzTreeModule } from './tree/nz-tree.module';
import { NzUploadModule } from './upload/nz-upload.module';
export * from './affix';
@@ -95,6 +96,7 @@ export * from './notification';
export * from './popconfirm';
export * from './modal';
export * from './cascader';
+export * from './tree';
@NgModule({
exports: [
@@ -143,7 +145,8 @@ export * from './cascader';
NzPopconfirmModule,
NzModalModule,
NzBackTopModule,
- NzCascaderModule
+ NzCascaderModule,
+ NzTreeModule
]
})
export class NgZorroAntdModule {
diff --git a/components/tree/demo/basic.md b/components/tree/demo/basic.md
new file mode 100644
index 00000000000..e78d61e15c1
--- /dev/null
+++ b/components/tree/demo/basic.md
@@ -0,0 +1,19 @@
+---
+order: 0
+title:
+ zh-CN: 基本
+ en-US: basic
+---
+
+## zh-CN
+
+最简单的用法,展示可勾选,可选中,禁用,默认展开等功能。
+
+## en-US
+
+The most basic usage, tell you how to use checkable, selectable, disabled, defaultExpandKeys, and etc.
+
+
+
diff --git a/components/tree/demo/basic.ts b/components/tree/demo/basic.ts
new file mode 100644
index 00000000000..2ea296a310a
--- /dev/null
+++ b/components/tree/demo/basic.ts
@@ -0,0 +1,91 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'nz-demo-tree-basic',
+ template: `
+
+ `
+})
+export class NzDemoTreeBasicComponent implements OnInit {
+ expandKeys = ['1001', '10001'];
+ checkedKeys = ['10001', '100012'];
+ selectedKeys = ['10001', '100011'];
+ expandDefault = false;
+ nodes = [
+ {
+ title: 'root1',
+ key: '1001',
+ children: [
+ {
+ title: 'child1',
+ key: '10001',
+ children: [
+ {
+ title: 'child1.1',
+ key: '100011',
+ children: []
+ },
+ {
+ title: 'child1.2',
+ key: '100012',
+ children: [
+ {
+ title: 'grandchild1.2.1',
+ key: '1000121',
+ isLeaf: true,
+ disabled: true
+ },
+ {
+ title: 'grandchild1.2.2',
+ key: '1000122',
+ isLeaf: true,
+ }
+ ]
+ }
+ ]
+ },
+ {
+ title: 'child2',
+ key: '10002'
+ }
+ ]
+ },
+ {
+ title: 'root2',
+ key: '1002',
+ children: [
+ {
+ title: 'child2.1',
+ key: '10021',
+ children: [],
+ disableCheckbox: true,
+ },
+ {
+ title: 'child2.2',
+ key: '10022',
+ children: [
+ {
+ title: 'grandchild2.2.1',
+ key: '100221'
+ }
+ ]
+ }
+ ]
+ },
+ {title: 'root3', key: '1003'}
+ ];
+
+ mouseAction(name: string, event: any): void {
+ console.log(name, event);
+ }
+
+ ngOnInit(): void {
+ }
+}
diff --git a/components/tree/demo/customized-icon.md b/components/tree/demo/customized-icon.md
new file mode 100644
index 00000000000..96d29a43b48
--- /dev/null
+++ b/components/tree/demo/customized-icon.md
@@ -0,0 +1,15 @@
+---
+order: 4
+debug: true
+title:
+ zh-CN: 自定义图标
+ en-US: customize
+---
+
+## zh-CN
+
+可以针对不同节点采用样式覆盖的方式定制图标,双击展开。
+
+## en-US
+
+You can customize icons for different nodes by styles override and expand the node using dblclick.
diff --git a/components/tree/demo/customized-icon.ts b/components/tree/demo/customized-icon.ts
new file mode 100644
index 00000000000..544a312870c
--- /dev/null
+++ b/components/tree/demo/customized-icon.ts
@@ -0,0 +1,103 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'nz-demo-tree-customized-icon',
+ template: `
+
+
+
+
+ {{node.title}}
+
+ `,
+ styles: [`
+ .active {
+ background-color: #bae7ff;
+ }
+ `]
+})
+export class NzDemoTreeCustomizedIconComponent implements OnInit {
+ nodes = [
+ {
+ title: 'root1',
+ key: '1001',
+ children: [
+ {
+ title: 'child1',
+ key: '10001',
+ children: [
+ {
+ title: 'child1.1',
+ key: '100011',
+ selected: true,
+ children: []
+ },
+ {
+ title: 'child1.2',
+ key: '100012',
+ children: [
+ {
+ title: 'grandchild1.2.1',
+ key: '1000121',
+ isLeaf: true,
+ checked: true,
+ disabled: true
+ },
+ {
+ title: 'grandchild1.2.2',
+ key: '1000122',
+ isLeaf: true,
+ }
+ ]
+ }
+ ]
+ },
+ {
+ title: 'child2',
+ key: '10002'
+ }
+ ]
+ },
+ {
+ title: 'root2',
+ key: '1002',
+ children: [
+ {
+ title: 'child2.1',
+ key: '10021',
+ children: []
+ },
+ {
+ title: 'child2.2',
+ key: '10022',
+ children: [
+ {
+ title: 'grandchild2.2.1',
+ key: '100221',
+ }
+ ]
+ }
+ ]
+ },
+ {title: 'root3', key: '1003'}
+ ];
+
+ mouseAction(name: string, e: any): void {
+ console.log(name, e);
+ if (name === 'dblclick') {
+ e.node.isExpanded = !e.node.isExpanded;
+ }
+ }
+
+ ngOnInit(): void {
+
+ }
+}
diff --git a/components/tree/demo/draggable.md b/components/tree/demo/draggable.md
new file mode 100644
index 00000000000..a7d0b8d6d36
--- /dev/null
+++ b/components/tree/demo/draggable.md
@@ -0,0 +1,14 @@
+---
+order: 1
+title:
+ zh-CN: 拖动示例
+ en-US: draggable
+---
+
+## zh-CN
+
+将节点拖拽到其他节点内部或前后。
+
+## en-US
+
+Drag treeNode to insert after the other treeNode or insert into the other parent TreeNode.
diff --git a/components/tree/demo/draggable.ts b/components/tree/demo/draggable.ts
new file mode 100644
index 00000000000..6e9861b4cb8
--- /dev/null
+++ b/components/tree/demo/draggable.ts
@@ -0,0 +1,86 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'nz-demo-tree-draggable',
+ template: `
+
+ `
+})
+export class NzDemoTreeDraggableComponent {
+ nodes = [
+ {
+ title: 'root1',
+ key: '1001',
+ children: [
+ {
+ title: 'child1',
+ key: '10001',
+ children: [
+ {
+ title: 'child1.1',
+ key: '100011',
+ children: []
+ },
+ {
+ title: 'child1.2',
+ key: '100012',
+ children: [
+ {
+ title: 'grandchild1.2.1',
+ key: '1000121',
+ isLeaf: true,
+ checked: true,
+ disabled: true
+ },
+ {
+ title: 'grandchild1.2.2',
+ key: '1000122',
+ isLeaf: true,
+ }
+ ]
+ }
+ ]
+ },
+ {
+ title: 'child2',
+ key: '10002'
+ }
+ ]
+ },
+ {
+ title: 'root2',
+ key: '1002',
+ children: [
+ {
+ title: 'child2.1',
+ key: '10021',
+ children: []
+ },
+ {
+ title: 'child2.2',
+ key: '10022',
+ children: [
+ {
+ title: 'grandchild2.2.1',
+ key: '100221',
+ }
+ ]
+ }
+ ]
+ },
+ {title: 'root3', key: '1003'}
+ ];
+
+ mouseAction(name: string, e: any): void {
+ if (name !== 'over') {
+ console.log(name, e);
+ }
+ }
+}
diff --git a/components/tree/demo/dynamic.md b/components/tree/demo/dynamic.md
new file mode 100644
index 00000000000..436a2d24374
--- /dev/null
+++ b/components/tree/demo/dynamic.md
@@ -0,0 +1,14 @@
+---
+order: 5
+title:
+ zh-CN: 异步数据加载
+ en-US: load data asynchronously
+---
+
+## zh-CN
+
+点击展开节点,动态加载数据,直到执行 addChildren() 方法取消加载状态。
+
+## en-US
+
+To load data asynchronously when click to expand a treeNode, loading state keeps until excute addChildren().
diff --git a/components/tree/demo/dynamic.ts b/components/tree/demo/dynamic.ts
new file mode 100644
index 00000000000..6545c08967d
--- /dev/null
+++ b/components/tree/demo/dynamic.ts
@@ -0,0 +1,46 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'nz-demo-tree-dynamic',
+ template: `
+
+ `
+})
+export class NzDemoTreeDynamicComponent {
+ nodes = [
+ {
+ title: 'root1',
+ key: '1001',
+ children: []
+ },
+ {
+ title: 'root2',
+ key: '1002',
+ children: []
+ },
+ {
+ title: 'root3',
+ key: '1003'
+ }
+ ];
+
+ mouseAction(name: string, e: any): void {
+ if (name === 'expand') {
+ setTimeout(_ => {
+ if (e.node.getChildren().length === 0 && e.node.isExpanded) {
+ e.node.addChildren([
+ {
+ title: 'childAdd-1',
+ key: '10031-' + (new Date()).getTime()
+ },
+ {
+ title: 'childAdd-2',
+ key: '10032-' + (new Date()).getTime(),
+ isLeaf: true
+ }]);
+ }
+ }, 1000);
+ }
+ }
+}
diff --git a/components/tree/demo/line.md b/components/tree/demo/line.md
new file mode 100644
index 00000000000..85ac24d8670
--- /dev/null
+++ b/components/tree/demo/line.md
@@ -0,0 +1,14 @@
+---
+order: 2
+title:
+ zh-CN: 连接线
+ en-US: tree with line
+---
+
+## zh-CN
+
+带连接线的树。
+
+## en-US
+
+Tree With Line
diff --git a/components/tree/demo/line.ts b/components/tree/demo/line.ts
new file mode 100644
index 00000000000..6ce8fd35f2f
--- /dev/null
+++ b/components/tree/demo/line.ts
@@ -0,0 +1,82 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'nz-demo-tree-line',
+ template: `
+
+ `
+})
+export class NzDemoTreeLineComponent {
+ nodes = [
+ {
+ title: 'root1',
+ key: '1001',
+ children: [
+ {
+ title: 'child1',
+ key: '10001',
+ children: [
+ {
+ title: 'child1.1',
+ key: '100011',
+ children: []
+ },
+ {
+ title: 'child1.2',
+ key: '100012',
+ children: [
+ {
+ title: 'grandchild1.2.1',
+ key: '1000121',
+ isLeaf: true,
+ checked: true,
+ disabled: true
+ },
+ {
+ title: 'grandchild1.2.2',
+ key: '1000122',
+ isLeaf: true,
+ }
+ ]
+ }
+ ]
+ },
+ {
+ title: 'child2',
+ key: '10002'
+ }
+ ]
+ },
+ {
+ title: 'root2',
+ key: '1002',
+ children: [
+ {
+ title: 'child2.1',
+ key: '10021',
+ children: []
+ },
+ {
+ title: 'child2.2',
+ key: '10022',
+ children: [
+ {
+ title: 'grandchild2.2.1',
+ key: '100221',
+ }
+ ]
+ }
+ ]
+ },
+ {title: 'root3', key: '1003'}
+ ];
+
+ mouseAction(name: string, e: any): void {
+ console.log(name, e);
+ }
+}
diff --git a/components/tree/demo/search.md b/components/tree/demo/search.md
new file mode 100644
index 00000000000..ff648ea01b0
--- /dev/null
+++ b/components/tree/demo/search.md
@@ -0,0 +1,14 @@
+---
+order: 4
+title:
+ zh-CN: 可搜索
+ en-US: searchable
+---
+
+## zh-CN
+
+可搜索的树。
+
+## en-US
+
+Searchable Tree.
diff --git a/components/tree/demo/search.ts b/components/tree/demo/search.ts
new file mode 100644
index 00000000000..151ec841451
--- /dev/null
+++ b/components/tree/demo/search.ts
@@ -0,0 +1,64 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'nz-demo-tree-search',
+ template: `
+
+
+
+
+
+
+
+ `
+})
+export class NzDemoTreeSearchComponent {
+ searchValue;
+ nodes = [
+ {
+ title: 'root1',
+ key: '1001',
+ children: [
+ {
+ title: 'child1',
+ key: '10001',
+ children: [
+ {title: 'child1.1', children: []},
+ {
+ title: 'child1.3',
+ checked: true,
+ children: [
+ {title: 'grandchild1.2.1', key: '110101', isLeaf: true}
+ ]
+ }
+ ]
+ },
+ {title: 'child2', key: '10002', isLeaf: true}
+ ]
+ },
+ {
+ title: 'root2',
+ key: '1002',
+ children: [
+ {title: 'child2.1', children: []},
+ {
+ title: 'child1.2',
+ selectable: false,
+ children: [
+ {title: 'grandchild2.2.1'}
+ ]
+ }
+ ]
+ },
+ {title: 'root3', key: '1003'}
+ ];
+
+ mouseAction(name: string, e: any): void {
+ console.log(name, e);
+ }
+}
diff --git a/components/tree/doc/index.en-US.md b/components/tree/doc/index.en-US.md
new file mode 100644
index 00000000000..fc6685f48a0
--- /dev/null
+++ b/components/tree/doc/index.en-US.md
@@ -0,0 +1,96 @@
+---
+category: Components
+type: Data Display
+title: Tree
+---
+
+## When To Use
+
+Almost anything can be represented in a tree structure. Examples include directories, organization hierarchies, biological classifications, countries, etc. The `Tree` component is a way of representing the hierarchical relationship between these things. You can also expand, collapse, and select a treeNode within a `Tree`.
+
+## API
+
+### Tree props
+
+| Property | Description | Type | Default |
+| -------- | ----------- | ---- | ------- |
+| nzTreeData | Tree data (Reference TreeNodeOption) | array | \[] |
+| nzCheckable | Adds a Checkbox before the treeNodes| boolean | false |
+| nzShowExpand | Show a Expand Icon before the treeNodes | boolean | true |
+| nzShowLine | Shows a connecting line | boolean | false |
+| nzAsyncData | Load data asynchronously (should be used with NzTreeNode.addChildren(...)) | boolean | false |
+| nzDraggable | Specifies whether this Tree is draggable (IE > 8) | boolean | false |
+| nzMultiple | Allows selecting multiple treeNodes | boolean | false |
+| nzDefaultExpandAll | Whether to expand all treeNodes by default | boolean | false |
+| nzDefaultExpandedKeys | Specify the keys of the default expanded treeNodes | string\[] | \[] |
+| nzDefaultCheckedKeys | Specifies the keys of the default checked treeNodes | string\[] | \[] |
+| nzDefaultSelectedKeys | Specifies the keys of the default selected treeNodes(set nzMultiple to be true) | string\[] | \[] |
+| nzSearchValue | filter (highlight) treeNodes (see demo `Searchable`) | string | null |
+| nzClick | Callback function for when the user clicks a treeNode | EventEmitter | - |
+| nzDblClick | Callback function for when the user double clicks a treeNode | EventEmitter | - |
+| nzContextMenu | Callback function for when the user right clicks a treeNode | EventEmitter | - |
+| nzCheckBoxChange | Callback function for when user clicks the Checkbox | EventEmitter | - |
+| nzExpandChange | Callback function for when a treeNode is expanded or collapsed |EventEmitter | - |
+| nzOnDragStart | Callback function for when the onDragStart event occurs | EventEmitter | - |
+| nzOnDragEnter | Callback function for when the onDragEnter event occurs | EventEmitter | - |
+| nzOnDragOver | Callback function for when the onDragOver event occurs | EventEmitter | - |
+| nzOnDragLeave | Callback function for when the onDragLeave event occurs | EventEmitter | - |
+| nzOnDrop | Callback function for when the onDrop event occurs | EventEmitter | - |
+| nzOnDragEnd | Callback function for when the onDragEnd event occurs | EventEmitter | - |
+
+### NzTreeNodeOptions props
+
+| Property | Description | Type | Default |
+| --- | --- | --- | --- |
+| title | Title | string | '---' |
+| key | Used with nzDefaultExpandedKeys / nzDefaultCheckedKeys / nzDefaultSelectedKeys. P.S.: It must be unique in all of treeNodes of the tree!| string | null |
+| children | treeNode's children | array | \[] |
+| isLeaf | Determines if this is a leaf node(can not be dropped to) | boolean | false |
+| checked | Set the treeNode be checked | boolean | false |
+| selected | Set the treeNode be selected | boolean | false |
+| expanded | Set the treeNode be expanded () | boolean | false |
+| selectable | Set whether the treeNode can be selected | boolean | true |
+| disabled | Disables the treeNode | boolean | false |
+| disableCheckbox | Disables the checkbox of the treeNode | boolean | false |
+
+
+### NzFormatEmitEvent props
+
+| Property | Description | Type | Default |
+| --- | --- | --- | --- |
+| eventName | Event Name | enum: `click` `dblclick` `contextmenu` `check` `expand` & `dragstart` `dragenter` `dragover` `dragleave` `drop` `dragend` | '' |
+| node | The current operation node (such as the target node to drop while dragging) | NzTreeNode | null |
+| event | MouseEvent or DragEvent | enum: `MouseEvent` `DragEvent` | null |
+| dragNode? | Current drag node (existing when dragged) | NzTreeNode | null |
+| selectedKeys? | Selected node list (exist when clicked) | array | [] |
+| checkedKeys? | Checked node list (exist when click checkbox) | array | [] |
+
+
+### NzTreeNode props
+
+| Property | Description | Type | Default |
+| --- | --- | --- | --- |
+| title | Title | string | NzTreeNodeOptions.title |
+| key | Key | string | NzTreeNodeOptions.key |
+| level | TreeNode's level relative to the root node | number | number |
+| children | Children | array | array |
+| treeOptions | User's Tree Data(will not change) | NzTreeNodeOptions | NzTreeNodeOptions |
+| getParentNode | Get parentNode | function | `NzTreeNode` / `null` |
+| isLeaf | Whether treeNode is a Leaf Node | boolean | `true` / `false` |
+| isExpanded | Whether treeNode is expanded | boolean | `true` / `false` |
+| isDisabled | Whether treeNode is disabled | boolean | `true` / `false` |
+| isDisableCheckbox | Whether treeNode's checkbox can not be clicked | boolean | `true` / `false` |
+| isSelectable | Set whether the treeNode can be selected | boolean | `true` 或 `false` |
+| isChecked | Whether treeNode is checked | boolean | `true` / `false` |
+| isAllChecked | Whether all treeNode's children are checked | boolean | `true` / `false` |
+| isHalfChecked | Part of treeNode's children are checked | boolean | `true` / `false` |
+| isSelected | Whether treeNode is selected | boolean | `true` / `false` |
+| isLoading | Whether treeNode is loading(when nzAsyncData is true) | boolean | `true` / `false` |
+| isMatched | Whether treeNode's title contains nzSearchValue | boolean | `true` / `false` |
+| getChildren | Get all children | function | NzTreeNode[] |
+| addChildren | Add child nodes, receive NzTreeNode or NzTreeNodeOptions array, the second parameter is the inserted index position | (children: array, index?: number )=>{} | void |
+| clearChildren | clear the treeNode's children | function | void |
+
+## Note
+nzDefaultExpandedKeys, nzDefaultCheckedKeys will not associate child nodes when initialized!
+
diff --git a/components/tree/doc/index.zh-CN.md b/components/tree/doc/index.zh-CN.md
new file mode 100644
index 00000000000..e28cd875851
--- /dev/null
+++ b/components/tree/doc/index.zh-CN.md
@@ -0,0 +1,96 @@
+---
+category: Components
+type: Data Display
+title: Tree
+subtitle: 树形控件
+---
+
+## 何时使用
+
+文件夹、组织架构、生物分类、国家地区等等,世间万物的大多数结构都是树形结构。使用`树控件`可以完整展现其中的层级关系,并具有展开收起选择等交互功能。
+
+## API
+
+### Tree props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| nzTreeData | 元数据,单个节点结构参考NzTreeNode | array | \[] |
+| nzCheckable | 节点前添加 Checkbox 复选框 | boolean | false |
+| nzShowExpand | 节点前添加展开图标 | boolean | true |
+| nzShowLine | 是否展示连接线 | boolean | false |
+| nzAsyncData | 是否异步加载(显示加载状态) | boolean | false |
+| nzDraggable | 设置节点可拖拽(IE>8) | boolean | false |
+| nzMultiple | 支持点选多个节点(节点本身) | boolean | false |
+| nzDefaultExpandAll | 默认展开所有树节点 | boolean | false |
+| nzDefaultExpandedKeys | 默认展开指定的树节点 | string\[] | \[] |
+| nzDefaultCheckedKeys | 默认选中复选框的树节点 | string\[] | \[] |
+| nzDefaultSelectedKeys | 默认选中的树节点(nzMultiple为true) | string\[] | \[] |
+| nzSearchValue | 按需筛选树高亮节点(结合搜索控件) | string | null |
+| nzClick | 点击树节点触发 | EventEmitter | - |
+| nzDblClick | 双击树节点触发 | EventEmitter | - |
+| nzContextMenu | 右键树节点触发 | EventEmitter | - |
+| nzCheckBoxChange | 点击树节点 Checkbox 触发 | EventEmitter | - |
+| nzExpandChange | 点击展开树节点图标触发 |EventEmitter | - |
+| nzOnDragStart | 开始拖拽时调用 | EventEmitter | - |
+| nzOnDragEnter | dragenter 触发时调用 | EventEmitter | - |
+| nzOnDragOver | dragover 触发时调用 | EventEmitter | - |
+| nzOnDragLeave | dragleave 触发时调用 | EventEmitter | - |
+| nzOnDrop | drop 触发时调用 | EventEmitter | - |
+| nzOnDragEnd | dragend 触发时调用 | EventEmitter | - |
+
+### NzTreeNodeOptions props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| title | 标题 | string | '---' |
+| key | 整个树范围内的所有节点的 key 值不能重复且不为空! | string | null |
+| children | 子节点 | array | \[] |
+| isLeaf | 设置为叶子节点(叶子节点不可被拖拽模式放置) | boolean | false |
+| checked | 设置节点 Checkbox 是否选中 | boolean | false |
+| selected | 设置节点本身是否选中 | boolean | false |
+| expanded | 设置节点是否展开(叶子节点无效) | boolean | false |
+| selectable | 设置节点是否可被选中 | boolean | true |
+| disabled | 设置是否禁用节点(不可进行任何操作) | boolean | false |
+| disableCheckbox | 设置节点禁用 Checkbox | boolean | false |
+
+
+### NzFormatEmitEvent props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| eventName | 事件名 | enum: `click` `dblclick` `contextmenu` `check` `expand` & `dragstart` `dragenter` `dragover` `dragleave` `drop` `dragend` | '' |
+| node | 当前操作节点(拖拽时表示目标节点) | NzTreeNode | null |
+| event | 原生事件 | enum: `MouseEvent` `DragEvent` | null |
+| dragNode? | 当前拖拽节点(拖拽时存在) | NzTreeNode | null |
+| selectedKeys? | 已选中的节点(单击时存在) | array | [] |
+| checkedKeys? | checkBox 已选中的节点(点击 checkBox 存在) | array | [] |
+
+
+### NzTreeNode props
+
+| 方法 | 说明 | 类型 | 返回值类型 |
+| --- | --- | --- | --- |
+| title | 标题 | string | NzTreeNodeOptions.title |
+| key | key值 | string | NzTreeNodeOptions.key |
+| level | 层级(最顶层为0,子节点逐层加1) | number | number |
+| children | 子节点 | array | array |
+| treeOptions | 原始节点树结构 | NzTreeNodeOptions | NzTreeNodeOptions |
+| getParentNode | 获取父节点 | function | `NzTreeNode` 或 `null` |
+| isLeaf | 是否为叶子节点 | boolean | `true` 或 `false` |
+| isExpanded | 是否已展开 | boolean | `true` 或 `false` |
+| isDisabled | 是否禁用 | boolean | `true` 或 `false` |
+| isDisableCheckbox | 是否禁用 checkBox | boolean | `true` 或 `false` |
+| isSelectable | 是否可选中 | boolean | `true` 或 `false` |
+| isChecked | 是否选中 checkBox | boolean | `true` 或 `false` |
+| isAllChecked | 子节点是否全选 | boolean | `true` 或 `false` |
+| isHalfChecked | 子节点有选中但未全选 | boolean | `true` 或 `false` |
+| isSelected | 是否已选中 | boolean | `true` 或 `false` |
+| isLoading | 是否异步加载状态(影响展开图标展示) | boolean | `true` 或 `false` |
+| isMatched | title是否包含nzSearchValue(搜索使用) | boolean | `true` 或 `false` |
+| getChildren | 获取子节点,返回NzTreeNode数组 | function | NzTreeNode[] |
+| addChildren | 添加子节点,接收NzTreeNode或NzTreeNodeOptions数组,第二个参数为插入的索引位置 | (children: array, index?: number )=>{} | void |
+| clearChildren | 清除子节点 | function | void |
+
+## 注意
+nzDefaultExpandedKeys、nzDefaultCheckedKeys初始化时将不关联子节点!
diff --git a/components/tree/index.ts b/components/tree/index.ts
new file mode 100644
index 00000000000..7e1a213e3ea
--- /dev/null
+++ b/components/tree/index.ts
@@ -0,0 +1 @@
+export * from './public-api';
diff --git a/components/tree/interface.ts b/components/tree/interface.ts
new file mode 100644
index 00000000000..7b23c4d5df4
--- /dev/null
+++ b/components/tree/interface.ts
@@ -0,0 +1,32 @@
+import { NzTreeNode } from './nz-tree-node';
+
+export interface NzFormatEmitEvent {
+ eventName: string;
+ node: NzTreeNode;
+ event: MouseEvent | DragEvent;
+ dragNode?: NzTreeNode;
+ selectedKeys?: NzTreeNode[];
+ checkedKeys?: NzTreeNode[];
+}
+
+export interface NzFormatPosition {
+ top: number;
+ left: number;
+}
+export interface NzTreeNodeOptions {
+ title?: string;
+ key?: string;
+ children?: NzTreeNodeOptions[];
+ isLeaf?: boolean;
+ checked?: boolean;
+ selected?: boolean;
+ selectable?: boolean;
+ disabled?: boolean;
+ disableCheckbox?: boolean;
+ expanded?: boolean;
+}
+
+export interface NzFormatClickEvent {
+ event: MouseEvent;
+ node: NzTreeNode;
+}
diff --git a/components/tree/nz-tree-node.component.ts b/components/tree/nz-tree-node.component.ts
new file mode 100644
index 00000000000..b229c2f58e0
--- /dev/null
+++ b/components/tree/nz-tree-node.component.ts
@@ -0,0 +1,401 @@
+import {
+ AfterContentInit,
+ Component,
+ ElementRef,
+ EventEmitter,
+ Input,
+ NgZone,
+ OnDestroy,
+ OnInit,
+ Output,
+ Renderer2,
+ TemplateRef,
+ ViewChild
+} from '@angular/core';
+import { Subject } from 'rxjs/Subject';
+import { Subscription } from 'rxjs/Subscription';
+import { fromEvent } from 'rxjs/observable/fromEvent';
+import { debounceTime } from 'rxjs/operators/debounceTime';
+
+import { NzFormatClickEvent, NzFormatEmitEvent } from './interface';
+import { NzTreeNode } from './nz-tree-node';
+import { NzTreeService } from './nz-tree.service';
+
+@Component({
+ selector: 'nz-tree-node',
+ template: `
+
+
+
+
+
+
+
+
+
+
+ 0"
+ [class.ant-tree-node-content-wrapper-close]="!nzTreeNode.isExpanded && nzTreeNode.getChildren().length>0"
+ [class.ant-tree-node-content-wrapper-normal]="nzTreeNode.getChildren().length>0"
+ [attr.draggable]="nzDraggable"
+ [attr.aria-grabbed]="nzDraggable">
+
+
+
+ {{matchValue[0]}}{{nzSearchValue}}{{matchValue[1]}}
+
+
+
+ {{nzTreeNode.title}}
+
+
+
+
+
+
+
+ 0 && nzTreeNode.isExpanded">
+
+
+
+ `
+})
+
+export class NzTreeNodeComponent implements OnInit, AfterContentInit, OnDestroy {
+ dragPos = 2;
+ prefixCls = 'ant-tree';
+ _treeNode;
+ _expandAll = false;
+ _defaultCheckedKeys = [];
+ _defaultExpandedKeys = [];
+ _defaultSelectedKeys = [];
+ _searchValue = '';
+ matchValue = [];
+ // 拖动划过状态
+ dragPosClass: object = {
+ '0': 'drag-over',
+ '1': 'drag-over-gap-bottom',
+ '-1': 'drag-over-gap-top'
+ };
+ _clickNum = 0;
+ _emitSubject$ = new Subject();
+ _emitSubjection: Subscription;
+
+ @ViewChild('dragElement') dragElement: ElementRef;
+
+ @Output() clickNode: EventEmitter = new EventEmitter();
+ @Output() dblClick: EventEmitter = new EventEmitter();
+ @Output() contextMenu: EventEmitter = new EventEmitter();
+ @Output() clickCheckBox: EventEmitter = new EventEmitter();
+ @Output() clickExpand: EventEmitter = new EventEmitter();
+ @Output() nzDragStart: EventEmitter = new EventEmitter();
+ @Output() nzDragEnter: EventEmitter = new EventEmitter();
+ @Output() nzDragOver: EventEmitter = new EventEmitter();
+ @Output() nzDragLeave: EventEmitter = new EventEmitter();
+ @Output() nzDrop: EventEmitter = new EventEmitter();
+ @Output() nzDragEnd: EventEmitter = new EventEmitter();
+
+ @Input() nzShowLine: boolean;
+ @Input() nzShowExpand: boolean;
+ @Input() nzDraggable: boolean;
+ @Input() nzMultiple: boolean;
+ @Input() nzCheckable: boolean;
+ @Input() nzAsyncData;
+ @Input() nzTreeTemplate: TemplateRef;
+
+ @Input()
+ set nzTreeNode(node: NzTreeNode) {
+ if (this.nzDefaultExpandAll) {
+ node.isExpanded = this.nzDefaultExpandAll;
+ }
+ this._treeNode = node;
+ }
+
+ get nzTreeNode(): NzTreeNode {
+ return this._treeNode;
+ }
+
+ @Input()
+ set nzDefaultExpandAll(value: boolean) {
+ if (value && this.nzTreeNode) {
+ this.nzTreeNode.isExpanded = value;
+ }
+ this._expandAll = value;
+ }
+
+ get nzDefaultExpandAll(): boolean {
+ return this._expandAll;
+ }
+
+ @Input()
+ set nzDefaultCheckedKeys(value: string[]) {
+ this._defaultCheckedKeys = value;
+ if (value && !this.nzTreeNode.isDisabled && value.indexOf(this.nzTreeNode.key) > -1) {
+ this.nzTreeNode.isChecked = true;
+ this.nzTreeNode.isAllChecked = true;
+ this.nzTreeNode.isHalfChecked = false;
+ }
+ }
+
+ get nzDefaultCheckedKeys(): string[] {
+ return this._defaultCheckedKeys;
+ }
+
+ @Input()
+ set nzDefaultExpandedKeys(value: string[]) {
+ this._defaultExpandedKeys = value;
+ if (value && value.indexOf(this.nzTreeNode.key) > -1) {
+ this.nzTreeNode.isExpanded = true;
+ }
+ }
+
+ get nzDefaultExpandedKeys(): string[] {
+ return this._defaultExpandedKeys;
+ }
+
+ @Input()
+ set nzDefaultSelectedKeys(value: string[]) {
+ this._defaultSelectedKeys = value;
+ if (value && !this.nzTreeNode.isDisabled && this.nzMultiple && value.indexOf(this.nzTreeNode.key) > -1) {
+ this.nzTreeNode.isSelected = true;
+ }
+ }
+
+ get nzDefaultSelectedKeys(): string[] {
+ return this._defaultSelectedKeys;
+ }
+
+ @Input()
+ set nzSearchValue(value: string) {
+ if (value && this.nzTreeNode.title.includes(value)) {
+ this.nzTreeNode.isMatched = true;
+ this.matchValue = [];
+ // match the search value
+ const index = this.nzTreeNode.title.indexOf(value);
+ this.matchValue.push(this.nzTreeNode.title.slice(0, index));
+ this.matchValue.push(this.nzTreeNode.title.slice(index + value.length, this.nzTreeNode.title.length));
+ } else {
+ // close the node if title does't contain search value
+ this.nzTreeNode.isMatched = false;
+ this.matchValue = [];
+ }
+ this._searchValue = value;
+ }
+
+ get nzSearchValue(): string {
+ return this._searchValue;
+ }
+
+ constructor(private nzTreeService: NzTreeService, private el: ElementRef, private ngZone: NgZone, private _renderer: Renderer2) {
+ }
+
+ ngOnInit(): void {
+ // add select list
+ if (this.nzTreeNode.isSelected) {
+ this.nzTreeService.setSelectedNodeList(this.nzTreeNode, this.nzMultiple);
+ }
+ // add check list
+ if (this.nzTreeNode.isChecked) {
+ this.nzTreeService.setCheckedNodeList(this.nzTreeNode);
+ }
+ this._emitSubjection = this._emitSubject$.pipe(debounceTime(200)).subscribe((e: NzFormatClickEvent) => {
+ if (this._clickNum % 2 === 0) {
+ this.dblClick.emit(this.nzTreeService.formatEvent('dblclick', e.node, e.event));
+ } else {
+ if (this.nzTreeNode.isSelectable && !this.nzTreeNode.isDisabled) {
+ this.nzTreeService.initNodeActive(this.nzTreeNode, this.nzMultiple);
+ }
+ this.clickNode.emit(this.nzTreeService.formatEvent('click', e.node, e.event, null, true));
+ }
+ this._clickNum = 0;
+ });
+ }
+
+ handleDragStart(e: DragEvent): void {
+ e.stopPropagation();
+ this.nzTreeService.setSelectedNode(this.nzTreeNode);
+ this.nzTreeNode.isExpanded = false;
+ this.nzDragStart.emit(this.nzTreeService.formatEvent('dragstart', this.nzTreeNode, e, this.nzTreeService.getSelectedNode()));
+ }
+
+ handleDragEnter(e: DragEvent): void {
+ e.preventDefault();
+ e.stopPropagation();
+ this.ngZone.run(() => {
+ this.nzTreeService.targetNode = this.nzTreeNode;
+ if ((this.nzTreeNode !== this.nzTreeService.getSelectedNode()) && !this.nzTreeNode.isLeaf) {
+ this.nzTreeNode.isExpanded = true;
+ }
+ });
+ this.nzDragEnter.emit(this.nzTreeService.formatEvent('dragenter', this.nzTreeNode, e, this.nzTreeService.getSelectedNode()));
+ }
+
+ handleDragOver(e: DragEvent): void {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dragPos = this.nzTreeService.calcDropPosition(e);
+ this._renderer.addClass(this.dragElement.nativeElement, this.dragPosClass[this.dragPos]);
+ this.nzDragOver.emit(this.nzTreeService.formatEvent('dragover', this.nzTreeNode, e, this.nzTreeService.getSelectedNode()));
+ }
+
+ handleDragLeave(e: DragEvent): void {
+ e.stopPropagation();
+ this.ngZone.run(() => {
+ this._clearDragClass();
+ });
+ this.nzDragLeave.emit(this.nzTreeService.formatEvent('dragleave', this.nzTreeNode, e, this.nzTreeService.getSelectedNode()));
+ }
+
+ handleDragDrop(e: DragEvent): void {
+ e.preventDefault();
+ e.stopPropagation();
+ this.ngZone.run(() => {
+ // pass if node is leafNo
+ if (this.nzTreeNode !== this.nzTreeService.getSelectedNode() && !(this.dragPos === 0 && this.nzTreeNode.isLeaf)) {
+ this.nzTreeService.dropAndApply(this.nzTreeNode, this.dragPos);
+ }
+ this._clearDragClass();
+ });
+ this.nzDrop.emit(this.nzTreeService.formatEvent('drop', this.nzTreeNode, e, this.nzTreeService.getSelectedNode()));
+ }
+
+ handleDragEnd(e: DragEvent): void {
+ e.stopPropagation();
+ this.ngZone.run(() => {
+ this.nzTreeService.setSelectedNode(null);
+ });
+ this.nzDragEnd.emit(this.nzTreeService.formatEvent('dragend', this.nzTreeNode, e, this.nzTreeService.getSelectedNode()));
+ }
+
+ ngAfterContentInit(): void {
+ if (this.nzDraggable) {
+ this.ngZone.runOutsideAngular(() => {
+ fromEvent(this.dragElement.nativeElement, 'dragstart').subscribe((e: DragEvent) => this.handleDragStart(e));
+ fromEvent(this.dragElement.nativeElement, 'dragenter').subscribe((e: DragEvent) => this.handleDragEnter(e));
+ fromEvent(this.dragElement.nativeElement, 'dragover').subscribe((e: DragEvent) => this.handleDragOver(e));
+ fromEvent(this.dragElement.nativeElement, 'dragleave').subscribe((e: DragEvent) => this.handleDragLeave(e));
+ fromEvent(this.dragElement.nativeElement, 'drop').subscribe((e: DragEvent) => this.handleDragDrop(e));
+ fromEvent(this.dragElement.nativeElement, 'dragend').subscribe((e: DragEvent) => this.handleDragEnd(e));
+ });
+ }
+ }
+
+ _clearDragClass(): void {
+ const dragClass = ['drag-over-gap-top', 'drag-over-gap-bottom', 'drag-over'];
+ dragClass.forEach(e => {
+ this._renderer.removeClass(this.dragElement.nativeElement, e);
+ });
+ }
+
+ _clickNode($event: MouseEvent, node: NzTreeNode): void {
+ $event.preventDefault();
+ $event.stopPropagation();
+ this._clickNum++;
+ this._emitSubject$.next({
+ 'event': $event,
+ 'node': node
+ });
+ }
+
+ _dblClickNode($event: MouseEvent, node: NzTreeNode): void {
+ $event.preventDefault();
+ $event.stopPropagation();
+ this._emitSubject$.next({
+ 'event': $event,
+ 'node': node
+ });
+ }
+
+ _contextMenuNode($event: MouseEvent, node: NzTreeNode): void {
+ $event.preventDefault();
+ $event.stopPropagation();
+ this.contextMenu.emit(this.nzTreeService.formatEvent('contextmenu', node, $event));
+ }
+
+ _clickCheckBox($event: MouseEvent, node: NzTreeNode): void {
+ $event.preventDefault();
+ $event.stopPropagation();
+ // return if node is disabled
+ if (node.isDisableCheckbox || node.isDisabled) {
+ return;
+ }
+ this.nzTreeService.checkTreeNode(node);
+ this.clickCheckBox.emit(this.nzTreeService.formatEvent('check', node, $event, null, false, true));
+ }
+
+ _clickExpand($event: MouseEvent, node: NzTreeNode): void {
+ $event.preventDefault();
+ $event.stopPropagation();
+ if (!this.nzTreeNode.isLoading) {
+ if (!node.isLeaf) {
+ // set async state
+ if (this.nzAsyncData && this.nzTreeNode.getChildren().length === 0 && !this.nzTreeNode.isExpanded) {
+ this.nzTreeNode.isLoading = true;
+ }
+ node.isExpanded = !this.nzTreeNode.isExpanded;
+ }
+ if (!this.nzTreeNode.isLeaf) {
+ this.clickExpand.emit(this.nzTreeService.formatEvent('expand', node, $event));
+ }
+ }
+ }
+
+ ngOnDestroy(): void {
+ if (this._emitSubjection) {
+ this._emitSubjection.unsubscribe();
+ }
+ }
+}
diff --git a/components/tree/nz-tree-node.ts b/components/tree/nz-tree-node.ts
new file mode 100644
index 00000000000..d5110944b71
--- /dev/null
+++ b/components/tree/nz-tree-node.ts
@@ -0,0 +1,103 @@
+import { NzTreeNodeOptions } from './interface';
+
+export class NzTreeNode {
+ title?: string;
+ key?: string;
+ level: number = 0;
+ children: NzTreeNode[];
+ isLeaf: boolean;
+ // Parent Node
+ parentNode: NzTreeNode;
+ isChecked: boolean;
+ isSelectable: boolean;
+ isDisabled: boolean;
+ isDisableCheckbox: boolean;
+ isExpanded: boolean;
+ isHalfChecked: boolean;
+ isAllChecked: boolean;
+ isSelected: boolean;
+ isLoading: boolean;
+ isMatched: boolean;
+
+ constructor(option: NzTreeNodeOptions, parent: NzTreeNode = null) {
+ this.title = option.title || '---';
+ this.key = option.key || null;
+ this.isLeaf = option.isLeaf || false;
+
+ this.children = new Array();
+ this.parentNode = parent;
+
+ // option params
+ this.isChecked = option.checked || false;
+ this.isSelectable = option.disabled || (option.selectable === false ? false : true);
+ this.isDisabled = option.disabled || false;
+ this.isDisableCheckbox = option.disableCheckbox || false;
+ this.isExpanded = option.expanded || false;
+
+ this.isAllChecked = option.checked || false;
+ this.isHalfChecked = false;
+ this.isSelected = option.selected || false;
+ this.isLoading = false;
+ this.isMatched = false;
+
+ /**
+ * 初始化时父节点expanded/checked状态影响全部子节点
+ */
+ if (parent) {
+ this.level = parent.level + 1;
+ } else {
+ this.level = 0;
+ }
+ if (typeof(option.children) !== 'undefined' && option.children !== null) {
+ option.children.forEach(
+ (nodeOptions) => {
+ if (option.checked && !option.disabled) {
+ nodeOptions.checked = option.checked;
+ }
+ this.children.push(new NzTreeNode(nodeOptions, this));
+ }
+ );
+ }
+ }
+
+ public getParentNode(): NzTreeNode {
+ return this.parentNode;
+ }
+
+ public getChildren(): NzTreeNode[] {
+ return this.children;
+ }
+
+ /**
+ * 支持按索引位置插入,叶子节点不可添加
+ * @param {Array|any[]} children
+ * @param {number} childPos
+ */
+ // tslint:disable-next-line:no-any
+ public addChildren(children: any[], childPos: number = -1): void {
+ if (this.isLeaf) {
+ return;
+ }
+ children.forEach(
+ (node) => {
+ let tNode = node;
+ if (tNode instanceof NzTreeNode) {
+ tNode.parentNode = this;
+ } else {
+ tNode = new NzTreeNode(node, this);
+ }
+ tNode.level = this.level + 1;
+ try {
+ childPos === -1 ? this.children.push(tNode) : this.children.splice(childPos, 0, tNode);
+ } catch (e) {
+
+ }
+ });
+ // remove loading state
+ this.isLoading = false;
+ }
+
+ public clearChildren(): void {
+ this.children = [];
+ }
+}
diff --git a/components/tree/nz-tree.component.spec.ts b/components/tree/nz-tree.component.spec.ts
new file mode 100644
index 00000000000..abcbd07de38
--- /dev/null
+++ b/components/tree/nz-tree.component.spec.ts
@@ -0,0 +1,374 @@
+import { Component, DebugElement, OnInit } from '@angular/core';
+import { async, fakeAsync, tick, ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { NzTreeNode } from './nz-tree-node';
+import { NzTreeNodeComponent } from './nz-tree-node.component';
+import { NzTreeComponent } from './nz-tree.component';
+import { NzTreeModule } from './nz-tree.module';
+import { NzTreeService } from './nz-tree.service';
+
+describe('NzTreeBasicComponent', () => {
+ let fixture: ComponentFixture;
+ let component: TestNzTreeComponent;
+ let tree: DebugElement;
+ // node
+ let nodeFixture: ComponentFixture;
+ let nodeComponent: NzTreeNodeComponent;
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [NzTreeModule],
+ declarations: [TestNzTreeComponent],
+ providers: [
+ NzTreeService
+ ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestNzTreeComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges(); // trigger initial data binding
+ tree = fixture.debugElement.query(By.directive(NzTreeComponent));
+ fixture.detectChanges(); // trigger initial data binding
+
+ // node
+ nodeFixture = TestBed.createComponent(NzTreeNodeComponent);
+ nodeComponent = nodeFixture.componentInstance;
+ fixture.detectChanges(); // trigger initial data bindin
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+ it('should className correct', () => {
+ fixture.detectChanges();
+ expect(tree.nativeElement.firstElementChild.classList).toContain('ant-tree');
+ });
+ it('should correct init view', () => {
+ const nodeItems = fixture.debugElement.queryAll(By.css('li'));
+ expect(nodeItems.length).toEqual(3);
+ });
+ it('should expand node correctly', () => {
+ const targetNode = tree.query(By.css('.ant-tree-switcher'));
+ (targetNode.nativeElement as HTMLElement).click();
+ fixture.detectChanges();
+ let expandNode = (fixture.nativeElement as HTMLElement).querySelector('.ant-tree-switcher_open');
+ expect(expandNode.classList).toContain('ant-tree-switcher_open');
+ (targetNode.nativeElement as HTMLElement).click();
+ fixture.detectChanges();
+ expandNode = (fixture.nativeElement as HTMLElement).querySelector('.ant-tree-switcher_close');
+ expect(expandNode.classList).toContain('ant-tree-switcher_close');
+ });
+ it('should set correct state', () => {
+ expect(fixture.nativeElement.querySelectorAll('.ant-tree-checkbox').length).toEqual(0);
+ component.nzCheckable = true;
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelectorAll('.ant-tree-checkbox').length).toEqual(3);
+ // showExpand
+ component.nzShowExpand = false;
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelectorAll('.ant-tree-switcher').length).toEqual(0);
+ component.nzShowExpand = true;
+ fixture.detectChanges();
+ // show line
+ component.nzShowLine = true;
+ fixture.detectChanges();
+ expect(tree.nativeElement.firstElementChild.classList).toContain('ant-tree-show-line');
+ component.nzShowLine = false;
+ fixture.detectChanges();
+ // nzSearchValue
+ component.nzSearchValue = 'grandchild1';
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelectorAll('.font-red').length).toEqual(2);
+ });
+
+ it('should set default config correctly', () => {
+ expect(fixture.nativeElement.querySelectorAll('.ant-tree-switcher_open').length).toEqual(0);
+ // set nzDefaultExpandedKeys
+ component.nzDefaultExpandedKeys = ['1001', '10001'];
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelectorAll('.ant-tree-switcher_open').length).toEqual(2);
+ component.nzDefaultExpandedKeys = [];
+ fixture.detectChanges();
+ // set nzDefaultCheckedKeys
+ component.nzCheckable = true;
+ component.nzDefaultExpandAll = true;
+ component.nzDefaultCheckedKeys = ['100011', '1000122', '1002'];
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelectorAll('.ant-tree-checkbox-checked').length).toEqual(3);
+ // set nzDefaultSelectedKeys
+ component.nzDefaultSelectedKeys = ['100011', '1000121', '1000122'];
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelectorAll('.ant-tree-node-selected').length).toEqual(1);
+ component.nzMultiple = true;
+ component.nzDefaultSelectedKeys = ['100011', '1000121', '1000122'];
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelectorAll('.ant-tree-node-selected').length).toEqual(3);
+ });
+
+ it('should set correct state if click checkbox', () => {
+ component.nzDefaultExpandAll = true;
+ component.nzCheckable = true;
+ fixture.detectChanges();
+ (fixture.nativeElement.querySelectorAll('.ant-tree-checkbox')[1] as HTMLElement).click();
+ fixture.detectChanges();
+ let targetNode = fixture.nativeElement.querySelectorAll('.ant-tree-checkbox-indeterminate');
+ expect(targetNode.length).toEqual(1);
+ expect(targetNode[0].nextElementSibling.getAttribute('title')).toEqual('root1');
+ targetNode = fixture.nativeElement.querySelectorAll('.ant-tree-checkbox-checked');
+ expect(targetNode.length).toEqual(4);
+ // click disabled key
+ (fixture.nativeElement.querySelectorAll('.ant-tree-checkbox')[4] as HTMLElement).click();
+ fixture.detectChanges();
+ });
+
+ it('should set correct state if click node', fakeAsync(() => {
+ component.nzDefaultExpandAll = true;
+ fixture.detectChanges();
+ (fixture.nativeElement.querySelectorAll('li')[1] as HTMLElement).click();
+ tick(250);
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelectorAll('.ant-tree-node-selected').length).toEqual(0);
+ // test dblclick contextmenu
+ triggerEvent(fixture.nativeElement.querySelectorAll('li')[1] as HTMLElement, 'contextmenu', 'MouseEvent');
+ triggerEvent(fixture.nativeElement.querySelectorAll('li')[1] as HTMLElement, 'dblclick', 'MouseEvent');
+ fixture.detectChanges();
+ // multiple
+ component.nzMultiple = true;
+ fixture.detectChanges();
+ (fixture.nativeElement.querySelectorAll('li')[3] as HTMLElement).click();
+ (fixture.nativeElement.querySelectorAll('li')[5] as HTMLElement).click();
+ (fixture.nativeElement.querySelectorAll('li')[6] as HTMLElement).click(); // disable
+ tick(250);
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelectorAll('.ant-tree-node-selected').length).toEqual(3);
+ }));
+
+ it('add children async', () => {
+ component.nzAsyncData = true;
+ component.nodes = [
+ {
+ title: 'root1',
+ key: '1001',
+ children: []
+ },
+ {
+ title: 'root2',
+ key: '1002',
+ children: []
+ },
+ {title: 'root3', key: '1003'},
+ {title: 'root4', key: '1004', children: []},
+ {title: 'root5', key: '1005', children: []}
+ ];
+ fixture.detectChanges();
+ const targetNode = tree.query(By.css('.ant-tree-switcher'));
+ (targetNode.nativeElement as HTMLElement).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelectorAll('.ant-tree-icon_loading').length).toEqual(1);
+ });
+
+ it('test service functions', () => {
+ // drop func
+ component.nzTreeService.initTreeNodes(component.nodes);
+ component.nzTreeService.setSelectedNode(component.nzTreeService.rootNodes[2]);
+ component.nzTreeService.dropAndApply(component.nzTreeService.rootNodes[0], 0);
+ expect(component.nzTreeService.rootNodes.length).toEqual(2);
+ expect(component.nzTreeService.rootNodes[0].getChildren()[2].title).toEqual('root3');
+ component.nzTreeService.dropAndApply(component.nzTreeService.rootNodes[0], 0);
+ component.nzTreeService.setSelectedNode(component.nzTreeService.rootNodes[1]);
+ component.nzTreeService.dropAndApply(component.nzTreeService.rootNodes[0], 1);
+ component.nzTreeService.setSelectedNode(component.nzTreeService.rootNodes[1].getChildren()[0]);
+ component.nzTreeService.dropAndApply(component.nzTreeService.rootNodes[0], -1);
+ component.nzTreeService.dropAndApply(component.nzTreeService.rootNodes[0], 3); // error pos
+ // init node active
+ component.nzTreeService.initTreeNodes(component.nodes);
+ component.nzTreeService.setSelectedNode(component.nzTreeService.rootNodes[0].getChildren()[1]);
+ component.nzTreeService.initNodeActive(component.nzTreeService.rootNodes[0].getChildren()[1], false);
+ expect(component.nzTreeService.getSelectedNode().title).toEqual(component.nzTreeService.rootNodes[0].getChildren()[1].title);
+ expect(component.nzTreeService.getSelectedNodeList().length).toEqual(1);
+ component.nzTreeService.initNodeActive(component.nzTreeService.rootNodes[0].getChildren()[0], true);
+ expect(component.nzTreeService.getSelectedNodeList().length).toEqual(2);
+ // check(reset nodes)
+ component.nzTreeService.initTreeNodes(component.nodes);
+ component.nzTreeService.checkTreeNode(component.nzTreeService.rootNodes[0].getChildren()[1]);
+ expect(component.nzTreeService.rootNodes[0].isHalfChecked).toEqual(true);
+ expect(component.nzTreeService.rootNodes[0].getChildren()[1].isAllChecked).toEqual(true);
+ });
+
+ it('test drag', () => {
+ // drag, just test functions work fine. will rewrite
+ let dragEvent = new DragEvent('dragstart');
+ nodeComponent.nzTreeNode = new NzTreeNode({
+ title: 'child1',
+ key: '10001',
+ selected: true,
+ children: [
+ {
+ title: 'child1.1',
+ key: '100011',
+ children: []
+ },
+ {
+ title: 'child1.2',
+ key: '100012',
+ children: [
+ {
+ title: 'grandchild1.2.1',
+ key: '1000121',
+ isLeaf: true
+ },
+ {
+ title: 'grandchild1.2.2',
+ key: '1000122',
+ isLeaf: true
+ }
+ ]
+ }
+ ]
+ });
+ nodeComponent.nzDraggable = true;
+ nodeFixture.detectChanges();
+ nodeComponent.handleDragStart(dragEvent);
+ expect(nodeComponent.nzTreeNode.isExpanded).toEqual(false);
+
+ dragEvent = new DragEvent('dragenter');
+ nodeComponent.handleDragEnter(dragEvent);
+ expect(nodeComponent.nzTreeNode.isExpanded).toEqual(false);
+
+ dragEvent = new DragEvent('dragover');
+ nodeComponent.handleDragOver(dragEvent);
+ nodeFixture.detectChanges();
+ expect(nodeComponent.dragPos).toEqual(2); // no element
+
+ dragEvent = new DragEvent('dragleave');
+ nodeComponent.handleDragLeave(dragEvent);
+
+ dragEvent = new DragEvent('drop');
+ nodeComponent.dragPos = 0;
+ nodeComponent.handleDragDrop(dragEvent);
+ nodeComponent.dragPos = 1;
+ nodeComponent.handleDragDrop(dragEvent);
+ nodeComponent.dragPos = -1;
+ nodeComponent.handleDragDrop(dragEvent);
+
+ dragEvent = new DragEvent('dragend');
+ nodeComponent.handleDragEnd(dragEvent);
+
+ });
+});
+
+@Component({
+ template: `
+
+
+ `
+})
+
+class TestNzTreeComponent implements OnInit {
+ nodes = [
+ {
+ title: 'root1',
+ key: '1001',
+ children: [
+ {
+ title: 'child1',
+ key: '10001',
+ selected: true,
+ children: [
+ {
+ title: 'child1.1',
+ key: '100011',
+ children: []
+ },
+ {
+ title: 'child1.2',
+ key: '100012',
+ children: [
+ {
+ title: 'grandchild1.2.1',
+ key: '1000121',
+ isLeaf: true,
+ disabled: true
+ },
+ {
+ title: 'grandchild1.2.2',
+ key: '1000122',
+ isLeaf: true
+ }
+ ]
+ }
+ ]
+ },
+ {
+ title: 'child2',
+ key: '10002'
+ }
+ ]
+ },
+ {
+ title: 'root2',
+ key: '1002',
+ children: [
+ {
+ title: 'child2.1',
+ key: '10021',
+ children: []
+ },
+ {
+ title: 'child2.2',
+ key: '10022',
+ children: [
+ {
+ title: 'grandchild2.2.1',
+ key: '100221',
+ disableCheckbox: true
+ },
+ {
+ title: 'grandchild2.2.2',
+ key: '100222'
+ }
+ ]
+ }
+ ]
+ },
+ {title: 'root3', key: '1003'}
+ ];
+ nzCheckable = false;
+ nzDefaultExpandAll = false;
+ nzMultiple = false;
+ nzShowExpand = true;
+ nzShowLine = false;
+ nzDraggable = false;
+ nzAsyncData = false;
+ nzSearchValue = '';
+ nzDefaultExpandedKeys = [];
+ nzDefaultCheckedKeys = [];
+ nzDefaultSelectedKeys = [];
+
+ constructor(public nzTreeService: NzTreeService) {
+ }
+
+ ngOnInit(): void {
+ }
+}
+
+export function triggerEvent(elem: HTMLElement, eventName: string, eventType: string): void {
+ const event: Event = document.createEvent(eventType);
+ event.initEvent(eventName, true, true);
+ elem.dispatchEvent(event);
+}
diff --git a/components/tree/nz-tree.component.ts b/components/tree/nz-tree.component.ts
new file mode 100644
index 00000000000..1270e5a4da8
--- /dev/null
+++ b/components/tree/nz-tree.component.ts
@@ -0,0 +1,126 @@
+import { Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef } from '@angular/core';
+
+import { NzFormatEmitEvent } from './interface';
+import { NzTreeNode } from './nz-tree-node';
+import { NzTreeService } from './nz-tree.service';
+
+@Component({
+ selector: 'nz-tree',
+ template: `
+
+ `,
+ providers: [
+ NzTreeService
+ ]
+})
+export class NzTreeComponent implements OnInit {
+ _root: NzTreeNode[] = [];
+ _searchValue;
+ _showLine = false;
+ _prefixCls = 'ant-tree';
+ classMap = {
+ [ this._prefixCls ]: true,
+ [this._prefixCls + '-show-line']: false,
+ ['draggable-tree']: false
+ };
+
+ @ContentChild('nzTreeTemplate') nzTreeTemplate: TemplateRef<{}>;
+
+ @Input() nzCheckable;
+ @Input() nzShowExpand: boolean = true;
+ @Input() nzAsyncData: boolean = false;
+ @Input() nzDraggable;
+ @Input() nzMultiple;
+ @Input() nzDefaultExpandAll: boolean = false;
+ @Input() nzDefaultCheckedKeys: string[];
+ @Input() nzDefaultExpandedKeys: string[];
+ @Input() nzDefaultSelectedKeys: string[];
+
+ @Input()
+ set nzShowLine(value: boolean) {
+ this._showLine = value;
+ this.setClassMap();
+ }
+
+ get nzShowLine(): boolean {
+ return this._showLine;
+ }
+
+ @Input()
+ // tslint:disable-next-line:no-any
+ set nzTreeData(value: any[]) {
+ this._root = this.nzTreeService.initTreeNodes(value);
+ }
+
+ // tslint:disable-next-line:no-any
+ get nzTreeData(): any[] {
+ return this._root;
+ }
+
+ @Input()
+ set nzSearchValue(value: string) {
+ this._searchValue = value;
+ this.nzTreeService.searchExpand(value);
+ }
+
+ get nzSearchValue(): string {
+ return this._searchValue;
+ }
+
+ @Output() nzOnSearchNode: EventEmitter = new EventEmitter();
+ @Output() nzClick: EventEmitter = new EventEmitter();
+ @Output() nzDblClick: EventEmitter = new EventEmitter();
+ @Output() nzContextMenu: EventEmitter = new EventEmitter();
+ @Output() nzCheckBoxChange: EventEmitter = new EventEmitter();
+ @Output() nzExpandChange: EventEmitter = new EventEmitter();
+
+ @Output() nzOnDragStart: EventEmitter = new EventEmitter();
+ @Output() nzOnDragEnter: EventEmitter = new EventEmitter();
+ @Output() nzOnDragOver: EventEmitter = new EventEmitter();
+ @Output() nzOnDragLeave: EventEmitter = new EventEmitter();
+ @Output() nzOnDrop: EventEmitter = new EventEmitter();
+ @Output() nzOnDragEnd: EventEmitter = new EventEmitter();
+
+ setClassMap(): void {
+ this.classMap = {
+ [ this._prefixCls ]: true,
+ [this._prefixCls + '-show-line']: this.nzShowLine,
+ ['draggable-tree']: this.nzDraggable
+ };
+ }
+
+ constructor(private nzTreeService: NzTreeService) {
+ }
+
+ ngOnInit(): void {
+ }
+}
diff --git a/components/tree/nz-tree.module.ts b/components/tree/nz-tree.module.ts
new file mode 100644
index 00000000000..09520732421
--- /dev/null
+++ b/components/tree/nz-tree.module.ts
@@ -0,0 +1,12 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { NzTreeNodeComponent } from './nz-tree-node.component';
+import { NzTreeComponent } from './nz-tree.component';
+
+@NgModule({
+ declarations: [NzTreeComponent, NzTreeNodeComponent],
+ exports: [NzTreeComponent, NzTreeNodeComponent],
+ imports: [CommonModule]
+})
+export class NzTreeModule {
+}
diff --git a/components/tree/nz-tree.service.ts b/components/tree/nz-tree.service.ts
new file mode 100644
index 00000000000..c1f4c969456
--- /dev/null
+++ b/components/tree/nz-tree.service.ts
@@ -0,0 +1,366 @@
+import { Injectable } from '@angular/core';
+
+import { NzFormatEmitEvent, NzFormatPosition, NzTreeNodeOptions } from './interface';
+import { NzTreeNode } from './nz-tree-node';
+
+@Injectable()
+export class NzTreeService {
+ selectedNode: NzTreeNode;
+ targetNode: NzTreeNode;
+ selectedNodeList: NzTreeNode[] = [];
+ checkedNodeList: NzTreeNode[] = [];
+ rootNodes: NzTreeNode[] = [];
+
+ /**
+ * init data to NzTreeNode
+ * @param {any[]} root
+ */
+ initTreeNodes(root: NzTreeNodeOptions[]): NzTreeNode[] {
+ this.rootNodes = [];
+ if (root.length > 0) {
+ root.forEach((node) => {
+ const currentNode = new NzTreeNode(node);
+ this.initParentNode(currentNode);
+ this.rootNodes.push(currentNode);
+ });
+ }
+ return this.rootNodes;
+ }
+
+ /**
+ * init checkBox state
+ * @param {NzTreeNode} node
+ * @returns {NzTreeNode}
+ */
+ initParentNode(node: NzTreeNode): void {
+ if (node.children.length === 0) {
+ // until root
+ this.checkTreeNodeParents(node);
+ } else {
+ node.children.forEach((child) => {
+ this.initParentNode(child);
+ });
+ }
+ }
+
+ setSelectedNode(node: NzTreeNode | null): void {
+ this.selectedNode = node;
+ }
+
+ getSelectedNode(): NzTreeNode | null {
+ return this.selectedNode;
+ }
+
+ // add node to select list
+ setSelectedNodeList(node: NzTreeNode, isMultiple: boolean): void {
+ if (isMultiple) {
+ let sIndex = -1;
+ this.selectedNodeList.forEach((cNode, index) => {
+ if (node.key === cNode.key) {
+ sIndex = index;
+ }
+ });
+ if (node.isSelected && sIndex === -1) {
+ this.selectedNodeList.push(node);
+ } else if (sIndex > -1) {
+ this.selectedNodeList.splice(sIndex, 1);
+ }
+ } else {
+ if (node.isSelected) {
+ this.selectedNodeList = [node];
+ } else {
+ this.selectedNodeList = [];
+ }
+ }
+ }
+
+ getSelectedNodeList(): NzTreeNode[] {
+ return this.selectedNodeList;
+ }
+
+ // add node to checkbox list
+ setCheckedNodeList(node: NzTreeNode): void {
+ let isExist = false;
+ this.checkedNodeList.forEach((cNode) => {
+ if (node.key === cNode.key && node.title === cNode.title) {
+ isExist = true;
+ }
+ });
+ if (node.isChecked && !isExist) {
+ this.checkedNodeList.push(node);
+ }
+ const removeChild = (rNode) => {
+ let rIndex = -1;
+ this.checkedNodeList.forEach((cNode, index) => {
+ if (rNode.key === cNode.key && rNode.title === cNode.title) {
+ rIndex = index;
+ }
+ });
+ if (rIndex > -1) {
+ this.checkedNodeList.splice(rIndex, 1);
+ }
+ rNode.children.forEach(child => {
+ removeChild(child);
+ });
+ };
+ this.rootNodes.forEach((rNode) => {
+ const loopNode = (lNode) => {
+ let cIndex = -1;
+ this.checkedNodeList.forEach((cNode, index) => {
+ if (lNode.key === cNode.key) {
+ cIndex = index;
+ }
+ });
+ if (lNode.isChecked) {
+ if (cIndex === -1) {
+ this.checkedNodeList.push(lNode);
+ }
+ // reset child state
+ lNode.children.forEach((child) => {
+ removeChild(child);
+ });
+ } else {
+ if (cIndex > -1) {
+ this.checkedNodeList.splice(cIndex, 1);
+ }
+ lNode.children.forEach(child => {
+ loopNode(child);
+ });
+ }
+ };
+ loopNode(rNode);
+ });
+ }
+
+ getCheckedNodeList(): NzTreeNode[] {
+ return this.checkedNodeList;
+ }
+
+ /**
+ * keep selected state if isMultiple is true
+ * @param {NzTreeNode} node
+ * @param {boolean} isMultiple
+ */
+ initNodeActive(node: NzTreeNode, isMultiple: boolean = false): void {
+ if (node.isDisabled) {
+ return;
+ }
+ const isSelected = node.isSelected;
+ if (!isMultiple) {
+ this.rootNodes.forEach((child) => {
+ this.resetNodeActive(child);
+ });
+ }
+ node.isSelected = !isSelected;
+ this.setSelectedNodeList(node, isMultiple);
+ }
+
+ resetNodeActive(node: NzTreeNode): void {
+ node.isSelected = false;
+ node.children.forEach((child) => {
+ this.resetNodeActive(child);
+ });
+ }
+
+ /**
+ * click checkbox
+ * @param {NzTreeNode} checkedNode
+ */
+ checkTreeNode(node: NzTreeNode): void {
+ node.isChecked = !node.isChecked;
+ node.isAllChecked = node.isChecked;
+ const isChecked = node.isChecked;
+ this.checkTreeNodeChildren(node, isChecked);
+ this.checkTreeNodeParents(node);
+ this.setCheckedNodeList(node);
+ }
+
+ /**
+ * reset child check state
+ * @param {NzTreeNode} node
+ * @param {boolean} value
+ */
+ checkTreeNodeChildren(node: NzTreeNode, value: boolean): void {
+ if (!node.isDisabled && !node.isDisableCheckbox) {
+ node.isChecked = value;
+ node.isAllChecked = value;
+ if (value) {
+ node.isHalfChecked = false;
+ }
+ for (const n of node.children) {
+ this.checkTreeNodeChildren(n, value);
+ }
+ }
+ }
+
+ /**
+ * 1、children half checked
+ * 2、children all checked, parent checked
+ * 3、no children checked
+ * @param node
+ * @returns {boolean}
+ */
+ checkTreeNodeParents(node: NzTreeNode): void {
+ const parentNode = node.getParentNode();
+ if (parentNode) {
+ if (parentNode.children.every(child => child.isDisabled || child.isDisableCheckbox || (!child.isHalfChecked && child.isAllChecked))) {
+ parentNode.isChecked = true;
+ parentNode.isAllChecked = true;
+ parentNode.isHalfChecked = false;
+ } else if (parentNode.children.some(child => child.isHalfChecked || child.isAllChecked)) {
+ parentNode.isChecked = false;
+ parentNode.isAllChecked = false;
+ parentNode.isHalfChecked = true;
+ } else {
+ parentNode.isChecked = false;
+ parentNode.isAllChecked = false;
+ parentNode.isHalfChecked = false;
+ }
+ this.checkTreeNodeParents(parentNode);
+ }
+ }
+
+ /**
+ * search & expand node
+ */
+ searchExpand(value: string): void {
+ const loopParent = (node: NzTreeNode) => {
+ // expand parent node
+ if (node.getParentNode()) {
+ node.getParentNode().isExpanded = true;
+ loopParent(node.getParentNode());
+ }
+ };
+ const loopChild = (node: NzTreeNode) => {
+ if (value && node.title.includes(value)) {
+ // expand parentNode
+ loopParent(node);
+ } else {
+ node.isExpanded = false;
+ }
+ node.children.forEach(cNode => {
+ loopChild(cNode);
+ });
+ };
+ this.rootNodes.forEach(node => {
+ loopChild(node);
+ });
+ }
+
+ /**
+ * drop
+ * 0: inner -1: pre 1: next
+ */
+ dropAndApply(targetNode: NzTreeNode, dragPos: number = -1): void {
+ if (!targetNode || dragPos > 1) {
+ return;
+ }
+ const targetParent = targetNode.getParentNode();
+ const isSelectedRootNode = this.selectedNode.getParentNode();
+ // remove the dragNode
+ if (isSelectedRootNode) {
+ isSelectedRootNode.children.splice(isSelectedRootNode.children.indexOf(this.selectedNode), 1);
+ } else {
+ this.rootNodes.splice(this.rootNodes.indexOf(this.selectedNode), 1);
+ }
+ switch (dragPos) {
+ case 0:
+ targetNode.addChildren([this.selectedNode]);
+ this.resetNodeLevel(targetNode);
+ break;
+ case -1:
+ case 1:
+ const tIndex = dragPos === 1 ? 1 : 0;
+ if (targetParent) {
+ targetParent.addChildren([this.selectedNode], targetParent.children.indexOf(targetNode) + tIndex);
+ this.resetNodeLevel(this.selectedNode.getParentNode());
+ } else {
+ const targetIndex = this.rootNodes.indexOf(targetNode) + tIndex;
+ // 根节点插入
+ this.rootNodes.splice(targetIndex, 0, this.selectedNode);
+ this.rootNodes[targetIndex].parentNode = null;
+ this.rootNodes[targetIndex].level = 0;
+ }
+ break;
+ }
+ // flush all nodes
+ this.rootNodes.forEach((child) => {
+ this.initParentNode(child);
+ });
+ }
+
+ resetNodeLevel(node: NzTreeNode): void {
+ if (node.getParentNode()) {
+ node.level = node.getParentNode().level + 1;
+ } else {
+ node.level = 0;
+ }
+ for (const child of node.children) {
+ this.resetNodeLevel(child);
+ }
+ }
+
+ /**
+ * @param {DragEvent} e
+ * @returns {number}
+ */
+ calcDropPosition(e: DragEvent): number {
+ if (!e.srcElement) {
+ return 2;
+ }
+ const offsetTop = this.getOffset(e.srcElement as HTMLElement).top;
+ const offsetHeight = (e.srcElement as HTMLElement).offsetHeight;
+ const pageY = e.pageY;
+ const gapHeight = offsetHeight * 0.1; // TODO: remove hard code
+ if (pageY > offsetTop + offsetHeight * 0.9) {
+ return 1;
+ }
+ if (pageY < offsetTop + gapHeight) {
+ return -1;
+ }
+ return 0;
+ }
+
+ getOffset(ele: Element): NzFormatPosition {
+ if (!ele || !ele.getClientRects().length) {
+ return {top: 0, left: 0};
+ }
+ const rect = ele.getBoundingClientRect();
+ if (rect.width || rect.height) {
+ const doc = ele.ownerDocument;
+ const win = doc.defaultView;
+ const docElem = doc.documentElement;
+
+ return {
+ top: rect.top + win.pageYOffset - docElem.clientTop,
+ left: rect.left + win.pageXOffset - docElem.clientLeft
+ };
+ }
+ return rect;
+ }
+
+ /**
+ * emit Structure
+ * eventName
+ * node
+ * event: MouseEvent / DragEvent
+ * dragNode
+ */
+ formatEvent(eventName: string, node: NzTreeNode, event: MouseEvent | DragEvent, dragNode?: NzTreeNode | null, getSelected: boolean = false, getChecked: boolean = false): NzFormatEmitEvent {
+ const emitStructure = {
+ 'eventName': eventName,
+ 'node': node,
+ 'event': event
+ };
+ if (dragNode) {
+ Object.assign(emitStructure, {'dragNode': dragNode});
+ }
+ if (getSelected) {
+ Object.assign(emitStructure, {'selectedKeys': this.getSelectedNodeList()});
+ }
+ if (getChecked) {
+ Object.assign(emitStructure, {'checkedKeys': this.getCheckedNodeList()});
+ }
+ return emitStructure;
+ }
+}
diff --git a/components/tree/public-api.ts b/components/tree/public-api.ts
new file mode 100644
index 00000000000..43dbcf41008
--- /dev/null
+++ b/components/tree/public-api.ts
@@ -0,0 +1,5 @@
+export * from './nz-tree.component';
+export * from './nz-tree.module';
+export * from './nz-tree.service';
+export * from './nz-tree-node.component';
+export * from './nz-tree-node';
diff --git a/components/tree/style/index.less b/components/tree/style/index.less
new file mode 100644
index 00000000000..051201df5cb
--- /dev/null
+++ b/components/tree/style/index.less
@@ -0,0 +1,206 @@
+@import "../../style/themes/default";
+@import "../../style/mixins/index";
+@import "../../checkbox/style/mixin";
+@import "./mixin";
+
+@tree-prefix-cls: ~"@{ant-prefix}-tree";
+@tree-showline-icon-color: @text-color-secondary;
+
+.antCheckboxFn(@checkbox-prefix-cls: ~"@{ant-prefix}-tree-checkbox");
+
+.@{tree-prefix-cls} {
+ .reset-component;
+ margin: 0;
+ padding: 0;
+
+ ol, ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ li {
+ padding: 4px 0;
+ margin: 0;
+ list-style: none;
+ white-space: nowrap;
+ outline: 0;
+ span[draggable],
+ span[draggable="true"] {
+ user-select: none;
+ border-top: 2px transparent solid;
+ border-bottom: 2px transparent solid;
+ margin-top: -2px;
+ /* Required to make elements draggable in old WebKit */
+ -khtml-user-drag: element;
+ -webkit-user-drag: element;
+ }
+ &.drag-over {
+ > span[draggable] {
+ background-color: @primary-color;
+ color: white;
+ opacity: 0.8;
+ }
+ }
+ &.drag-over-gap-top {
+ > span[draggable] {
+ border-top-color: @primary-color;
+ }
+ }
+ &.drag-over-gap-bottom {
+ > span[draggable] {
+ border-bottom-color: @primary-color;
+ }
+ }
+ &.filter-node {
+ > span {
+ color: @highlight-color !important;
+ font-weight: 500 !important;
+ }
+ }
+ ul {
+ margin: 0;
+ padding: 0 0 0 18px;
+ }
+ .@{tree-prefix-cls}-node-content-wrapper {
+ display: inline-block;
+ padding: 0 5px;
+ border-radius: @border-radius-sm;
+ margin: 0;
+ cursor: pointer;
+ text-decoration: none;
+ vertical-align: top;
+ color: @text-color;
+ transition: all .3s;
+ position: relative;
+ height: 24px;
+ line-height: 24px;
+ &:hover {
+ background-color: @item-hover-bg;
+ }
+ &.@{tree-prefix-cls}-node-selected {
+ background-color: @primary-2;
+ }
+ .font-red {
+ color: #FF5500;
+ }
+ }
+ span {
+ cursor: pointer;
+ &.@{tree-prefix-cls}-checkbox {
+ margin: 4px 4px 0 2px;
+ }
+ &.@{tree-prefix-cls}-switcher,
+ &.@{tree-prefix-cls}-iconEle {
+ margin: 0;
+ width: 24px;
+ height: 24px;
+ line-height: 24px;
+ display: inline-block;
+ vertical-align: middle;
+ border: 0 none;
+ cursor: pointer;
+ outline: none;
+ text-align: center;
+ }
+ &.@{tree-prefix-cls}-icon_loading {
+ background: #fff;
+ &:after {
+ display: inline-block;
+ .iconfont-font("\E64D");
+ animation: loadingCircle 1s infinite linear;
+ color: @primary-color;
+ }
+ }
+ &.@{tree-prefix-cls}-switcher {
+ &.@{tree-prefix-cls}-switcher-noop {
+ cursor: default;
+ }
+ &.@{tree-prefix-cls}-switcher_open {
+ .antTreeSwitcherIcon();
+ }
+ &.@{tree-prefix-cls}-switcher_close {
+ .antTreeSwitcherIcon();
+ &:after {
+ transform: rotate(270deg) scale(0.59);
+ }
+ }
+ }
+ }
+ &:last-child > span {
+ &.@{tree-prefix-cls}-switcher,
+ &.@{tree-prefix-cls}-iconEle {
+ &:before {
+ display: none;
+ }
+ }
+ }
+ }
+ > li {
+ &:first-child {
+ padding-top: 7px;
+ }
+ &:last-child {
+ padding-bottom: 7px;
+ }
+ }
+ &-child-tree {
+ display: none;
+ &-open {
+ display: block;
+ }
+ }
+ li&-treenode-disabled {
+ > span,
+ > .@{tree-prefix-cls}-node-content-wrapper,
+ > .@{tree-prefix-cls}-node-content-wrapper span,
+ > span.@{tree-prefix-cls}-switcher {
+ color: @disabled-color;
+ cursor: not-allowed;
+ }
+ > .@{tree-prefix-cls}-node-content-wrapper:hover {
+ background: transparent;
+ }
+ }
+ &-icon__open {
+ margin-right: 2px;
+ vertical-align: top;
+ }
+ &-icon__close {
+ margin-right: 2px;
+ vertical-align: top;
+ }
+ // Tree with line
+ &&-show-line {
+ li {
+ position: relative;
+ span {
+ &.@{tree-prefix-cls}-switcher {
+ background: @component-background;
+ color: @tree-showline-icon-color;
+ &.@{tree-prefix-cls}-switcher-noop {
+ .antTreeShowLineIcon("tree-doc-icon");
+ }
+ &.@{tree-prefix-cls}-switcher_open {
+ .antTreeShowLineIcon("tree-showline-open-icon");
+ }
+ &.@{tree-prefix-cls}-switcher_close {
+ .antTreeShowLineIcon("tree-showline-close-icon");
+ }
+ }
+ }
+ }
+ li:before {
+ content: ' ';
+ width: 1px;
+ border-left: 1px solid @border-color-base;
+ height: 100%;
+ position: absolute;
+ left: 12px;
+ margin: 22px 0;
+ }
+ nz-tree-node:last-child li:before {
+ border-left: 0 solid @border-color-base !important;
+ }
+ }
+}
diff --git a/components/tree/style/mixin.less b/components/tree/style/mixin.less
new file mode 100644
index 00000000000..1a4a4cb87ae
--- /dev/null
+++ b/components/tree/style/mixin.less
@@ -0,0 +1,27 @@
+@import "../../style/mixins/index";
+
+@tree-default-open-icon: "\e606";
+@tree-showline-open-icon: "\e621";
+@tree-showline-close-icon: "\e645";
+@tree-doc-icon: "\e664";
+
+.antTreeSwitcherIcon(@type: "tree-default-open-icon") {
+ &:after {
+ .iconfont-size-under-12px(7px);
+ display: inline-block;
+ .iconfont-font(@@type);
+ font-weight: bold;
+ transition: transform .3s;
+ }
+}
+
+.antTreeShowLineIcon(@type) {
+ &:after {
+ .iconfont-size-under-12px(12px);
+ display: inline-block;
+ .iconfont-font(@@type);
+ vertical-align: baseline;
+ font-weight: normal;
+ transition: transform .3s;
+ }
+}