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: ` +
  • + + + + + + + + + + + + + + {{matchValue[0]}}{{nzSearchValue}}{{matchValue[1]}} + + + + {{nzTreeNode.title}} + + + + + + + + +
      + +
    +
    +
  • + ` +}) + +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; + } +}