From 7c6fd1eb4bdea3ab70dd3d97ebd638bdea288078 Mon Sep 17 00:00:00 2001 From: Hsuan Lee Date: Thu, 31 May 2018 18:49:06 +0800 Subject: [PATCH] feat:(module:tree-select): add tree-select component (#1477) close #1391 --- components/components.less | 3 +- .../animation/select-dropdown-animations.ts | 55 ++ .../core/animation/select-tag-animations.ts | 21 + components/ng-zorro-antd.module.ts | 3 + components/tree-select/demo/async.md | 15 + components/tree-select/demo/async.ts | 133 +++++ components/tree-select/demo/basic.md | 14 + components/tree-select/demo/basic.ts | 90 ++++ components/tree-select/demo/checkable.md | 15 + components/tree-select/demo/checkable.ts | 86 +++ components/tree-select/demo/multiple.md | 14 + components/tree-select/demo/multiple.ts | 88 +++ components/tree-select/doc/index.en-US.md | 35 ++ components/tree-select/doc/index.zh-CN.md | 35 ++ components/tree-select/index.ts | 1 + .../tree-select/nz-tree-select.component.ts | 510 ++++++++++++++++++ .../tree-select/nz-tree-select.module.ts | 14 + components/tree-select/nz-tree-select.spec.ts | 490 +++++++++++++++++ components/tree-select/public-api.ts | 2 + components/tree-select/style/index.less | 145 +++++ components/tree/nz-tree-node.component.ts | 3 +- components/tree/nz-tree-node.ts | 8 +- components/tree/nz-tree.component.ts | 10 +- components/tree/nz-tree.service.ts | 2 +- 24 files changed, 1786 insertions(+), 6 deletions(-) create mode 100644 components/core/animation/select-dropdown-animations.ts create mode 100644 components/core/animation/select-tag-animations.ts create mode 100755 components/tree-select/demo/async.md create mode 100755 components/tree-select/demo/async.ts create mode 100755 components/tree-select/demo/basic.md create mode 100755 components/tree-select/demo/basic.ts create mode 100755 components/tree-select/demo/checkable.md create mode 100755 components/tree-select/demo/checkable.ts create mode 100755 components/tree-select/demo/multiple.md create mode 100755 components/tree-select/demo/multiple.ts create mode 100755 components/tree-select/doc/index.en-US.md create mode 100755 components/tree-select/doc/index.zh-CN.md create mode 100644 components/tree-select/index.ts create mode 100644 components/tree-select/nz-tree-select.component.ts create mode 100644 components/tree-select/nz-tree-select.module.ts create mode 100644 components/tree-select/nz-tree-select.spec.ts create mode 100644 components/tree-select/public-api.ts create mode 100755 components/tree-select/style/index.less diff --git a/components/components.less b/components/components.less index fba25aff119..0258dda8484 100644 --- a/components/components.less +++ b/components/components.less @@ -48,4 +48,5 @@ @import "./upload/style/index.less"; @import "./auto-complete/style/index.less"; @import "./cascader/style/index.less"; -@import "./tree/style/index.less"; \ No newline at end of file +@import "./tree/style/index.less"; +@import "./tree-select/style/index.less"; \ No newline at end of file diff --git a/components/core/animation/select-dropdown-animations.ts b/components/core/animation/select-dropdown-animations.ts new file mode 100644 index 00000000000..230fd0ef179 --- /dev/null +++ b/components/core/animation/select-dropdown-animations.ts @@ -0,0 +1,55 @@ +import { + animate, + state, + style, + transition, + trigger, + AnimationTriggerMetadata +} from '@angular/animations'; + +export const selectDropDownAnimation: AnimationTriggerMetadata = trigger('selectDropDownAnimation', [ + state('hidden', style({ + opacity: 0, + display: 'none' + })), + state('bottom', style({ + opacity : 1, + transform : 'scaleY(1)', + transformOrigin: '0% 0%' + })), + state('top', style({ + opacity : 1, + transform : 'scaleY(1)', + transformOrigin: '0% 100%' + })), + transition('hidden => bottom', [ + style({ + opacity : 0, + transform : 'scaleY(0.8)', + transformOrigin: '0% 0%' + }), + animate('100ms cubic-bezier(0.755, 0.05, 0.855, 0.06)') + ]), + transition('bottom => hidden', [ + animate('100ms cubic-bezier(0.755, 0.05, 0.855, 0.06)', style({ + opacity : 0, + transform : 'scaleY(0.8)', + transformOrigin: '0% 0%' + })) + ]), + transition('hidden => top', [ + style({ + opacity : 0, + transform : 'scaleY(0.8)', + transformOrigin: '0% 100%' + }), + animate('100ms cubic-bezier(0.755, 0.05, 0.855, 0.06)') + ]), + transition('top => hidden', [ + animate('100ms cubic-bezier(0.755, 0.05, 0.855, 0.06)', style({ + opacity : 0, + transform : 'scaleY(0.8)', + transformOrigin: '0% 100%' + })) + ]) +]); diff --git a/components/core/animation/select-tag-animations.ts b/components/core/animation/select-tag-animations.ts new file mode 100644 index 00000000000..cc5e90b9701 --- /dev/null +++ b/components/core/animation/select-tag-animations.ts @@ -0,0 +1,21 @@ +import { + animate, + state, + style, + transition, + trigger, + AnimationTriggerMetadata +} from '@angular/animations'; + +export const selectTagAnimation: AnimationTriggerMetadata = trigger('selectTagAnimation', [ + state('*', style({ opacity: 1, transform: 'scale(1)' })), + transition('void => *', [ + style({ opacity: 0, transform: 'scale(0)' }), + animate('150ms linear') + ]), + state('void', style({ opacity: 0, transform: 'scale(0)' })), + transition('* => void', [ + style({ opacity: 1, transform: 'scale(1)' }), + animate('150ms linear') + ]) +]); diff --git a/components/ng-zorro-antd.module.ts b/components/ng-zorro-antd.module.ts index 28f95ba7ecb..6e12519abd6 100644 --- a/components/ng-zorro-antd.module.ts +++ b/components/ng-zorro-antd.module.ts @@ -52,6 +52,7 @@ 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 { NzTreeSelectModule } from './tree-select/nz-tree-select.module'; import { NzUploadModule } from './upload/nz-upload.module'; export * from './affix'; @@ -104,6 +105,7 @@ export * from './popconfirm'; export * from './modal'; export * from './cascader'; export * from './tree'; +export * from './tree-select'; export * from './time-picker'; @NgModule({ @@ -158,6 +160,7 @@ export * from './time-picker'; NzBackTopModule, NzCascaderModule, NzTreeModule, + NzTreeSelectModule, NzTimePickerModule ] }) diff --git a/components/tree-select/demo/async.md b/components/tree-select/demo/async.md new file mode 100755 index 00000000000..ee5c9eedd0c --- /dev/null +++ b/components/tree-select/demo/async.md @@ -0,0 +1,15 @@ +--- +order: 2 +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-select/demo/async.ts b/components/tree-select/demo/async.ts new file mode 100755 index 00000000000..374132cb383 --- /dev/null +++ b/components/tree-select/demo/async.ts @@ -0,0 +1,133 @@ +import { Component, OnInit } from '@angular/core'; +import { NzFormatEmitEvent, NzTreeNode } from 'ng-zorro-antd'; + +@Component({ + selector: 'nz-demo-tree-select-async', + template: ` + + + ` +}) + +export class NzDemoTreeSelectAsyncComponent implements OnInit { + expandKeys = [ '1001', '10001' ]; + value: string; + nodes = [ + new NzTreeNode({ + 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 + } + ] + } + ] + } + ] + }), + new NzTreeNode({ + title: 'root2', + key: '1002', + children: [ + { + title: 'child2.1', + key: '10021', + children: [], + disabled: true + }, + { + title: 'child2.2', + key: '10022', + children: [ + { + title: 'grandchild2.2.1', + key: '100221', + isLeaf: true + } + ] + } + ] + }) + ]; + + onExpandChange(e: NzFormatEmitEvent): void { + if (e.node.getChildren().length === 0 && e.node.isExpanded) { + this.loadNode().then(data => { + e.node.addChildren(data); + }); + } + } + + loadNode(): Promise { + return new Promise(resolve => { + setTimeout(() => resolve([ + new NzTreeNode({ + title: 'root2', + key: '10030-' + (new Date()).getTime(), + children: [ + { + title: 'child2.1', + key: '10021', + children: [], + checked: true, + disabled: true + }, + { + title: 'child2.2', + key: '10022', + children: [ + { + title: 'grandchild2.2.1', + key: '100221', + isLeaf: true + } + ] + } + ] + }), + { + title: 'childAdd-1', + key: '10031-' + (new Date()).getTime() + }, + { + title: 'childAdd-2', + key: '10032-' + (new Date()).getTime(), + isLeaf: true + }]), + 1000); + }); + } + + ngOnInit(): void { + } +} diff --git a/components/tree-select/demo/basic.md b/components/tree-select/demo/basic.md new file mode 100755 index 00000000000..79268f932a3 --- /dev/null +++ b/components/tree-select/demo/basic.md @@ -0,0 +1,14 @@ +--- +order: 0 +title: + zh-CN: 基本 + en-US: Basic +--- + +## zh-CN + +最简单的用法。 + +## en-US + +The most basic usage. diff --git a/components/tree-select/demo/basic.ts b/components/tree-select/demo/basic.ts new file mode 100755 index 00000000000..251656cc882 --- /dev/null +++ b/components/tree-select/demo/basic.ts @@ -0,0 +1,90 @@ +import { Component, OnInit } from '@angular/core'; +import { NzFormatEmitEvent, NzTreeNode } from 'ng-zorro-antd'; + +@Component({ + selector: 'nz-demo-tree-select-basic', + template: ` + + ` +}) + +export class NzDemoTreeSelectBasicComponent implements OnInit { + expandKeys = [ '1001', '10001' ]; + value: string; + nodes = [ + new NzTreeNode({ + 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 + } + ] + } + ] + } + ] + }), + new NzTreeNode({ + 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', + isLeaf: true + } + ] + } + ] + }) + ]; + + onChange($event: NzTreeNode): void { + console.log($event); + } + + ngOnInit(): void { + // mock async + setTimeout(() => { + this.value = '10001'; + }, 1000); + } +} diff --git a/components/tree-select/demo/checkable.md b/components/tree-select/demo/checkable.md new file mode 100755 index 00000000000..15a3f0cc95f --- /dev/null +++ b/components/tree-select/demo/checkable.md @@ -0,0 +1,15 @@ +--- +order: 3 +title: + zh-CN: 可勾选 + en-US: Checkable +--- + +## zh-CN + +使用勾选框实现多选功能。 + +## en-US + +Multiple and checkable. + diff --git a/components/tree-select/demo/checkable.ts b/components/tree-select/demo/checkable.ts new file mode 100755 index 00000000000..65b10cfe85c --- /dev/null +++ b/components/tree-select/demo/checkable.ts @@ -0,0 +1,86 @@ +import {Component, OnInit} from '@angular/core'; +import {NzFormatEmitEvent, NzTreeNode} from 'ng-zorro-antd'; + +@Component({ + selector: 'nz-demo-tree-select-checkable', + template: ` + + ` +}) + +export class NzDemoTreeSelectCheckableComponent implements OnInit { + + value: string[] = ['10001', '10022']; + nodes = [ + new NzTreeNode({ + 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 + } + ] + } + ] + } + ] + }), + new NzTreeNode({ + 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', + isLeaf: true + } + ] + } + ] + }) + ]; + + onChange($event: NzTreeNode): void { + console.log($event); + } + + ngOnInit(): void { + } +} diff --git a/components/tree-select/demo/multiple.md b/components/tree-select/demo/multiple.md new file mode 100755 index 00000000000..20fa00a62cf --- /dev/null +++ b/components/tree-select/demo/multiple.md @@ -0,0 +1,14 @@ +--- +order: 1 +title: + zh-CN: 多选 + en-US: Multiple Selection +--- + +## zh-CN + +多选的树选择。 + +## en-US + +Multiple selection usage. diff --git a/components/tree-select/demo/multiple.ts b/components/tree-select/demo/multiple.ts new file mode 100755 index 00000000000..5ec4524bc8a --- /dev/null +++ b/components/tree-select/demo/multiple.ts @@ -0,0 +1,88 @@ +import {Component, OnInit} from '@angular/core'; +import {NzFormatEmitEvent, NzTreeNode} from 'ng-zorro-antd'; + +@Component({ + selector: 'nz-demo-tree-select-multiple', + template: ` + + + ` +}) + +export class NzDemoTreeSelectMultipleComponent implements OnInit { + + value: string[] = ['100011']; + nodes = [ + new NzTreeNode({ + 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 + } + ] + } + ] + } + ] + }), + new NzTreeNode({ + 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', + isLeaf: true + } + ] + } + ] + }) + ]; + + onChange($event: NzTreeNode): void { + console.log($event); + } + + ngOnInit(): void { + } +} diff --git a/components/tree-select/doc/index.en-US.md b/components/tree-select/doc/index.en-US.md new file mode 100755 index 00000000000..497185fab84 --- /dev/null +++ b/components/tree-select/doc/index.en-US.md @@ -0,0 +1,35 @@ +--- +category: Components +type: Data Entry +title: TreeSelect +--- + +Tree selection control. + +## When To Use + +`TreeSelect` is similar to `Select`, but the values are provided in a tree like structure. +Any data whose entries are defined in a hierarchical manner is fit to use this control. Examples of such case may include a corporate hierarchy, a directory structure, and so on. + +## API + +### nz-tree-select + +| Property | Description | Type | Default | +| -------- | ----------- | ---- | ------- | +| nzAllowClear | Whether allow clear | boolean | false | +| nzPlaceHolder | Placeholder of the select input | string | - | +| nzDisabled | Disabled or not | boolean | false | +| nzShowSearch | Whether to display a search input in the dropdown menu(valid only in the single mode) | boolean | false | +| nzDropdownMatchSelectWidth | Determine whether the dropdown menu and the select input are the same width | boolean | true | +| nzDropdownStyle | To set the style of the dropdown menu | object | - | +| nzMultiple | Support multiple or not, will be `true` when enable `nzCheckable`. | boolean | false | +| nzSize | To set the size of the select input, options: `large` `small` | string | 'default' | +| nzCheckable | Whether to show checkbox on 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 | +| nzNodes | Data of the treeNodes | NzTreeNode\[] | \[] | +| nzDefaultExpandAll | Whether to expand all treeNodes by default | boolean | false | +| nzDefaultExpandedKeys | Default expanded treeNodes | string\[] | - | +| nzExpandChange | Callback function for when a treeNode is expanded or collapsed |EventEmitter | - | diff --git a/components/tree-select/doc/index.zh-CN.md b/components/tree-select/doc/index.zh-CN.md new file mode 100755 index 00000000000..d14df9ffb25 --- /dev/null +++ b/components/tree-select/doc/index.zh-CN.md @@ -0,0 +1,35 @@ +--- +category: Components +subtitle: 树选择 +type: Data Entry +title: TreeSelect +--- + +树型选择控件。 + +## 何时使用 + +类似 Select 的选择控件,可选择的数据结构是一个树形结构时,可以使用 TreeSelect,例如公司层级、学科系统、分类目录等等。 + +## API + +### nz-tree-select + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| nzAllowClear | 显示清除按钮 | boolean | false | +| nzPlaceHolder | 选择框默认文字 | string | - | +| nzDisabled | 禁用选择器 | boolean | false | +| nzShowSearch | 显示搜索框 | boolean | false | +| nzDropdownMatchSelectWidth | 下拉菜单和选择器同宽 | boolean | true | +| nzDropdownStyle | 下拉菜单的样式 | { [key: string]: string; } | - | +| nzMultiple | 支持多选(当设置 nzCheckable 时自动变为true) | boolean | false | +| nzSize | 选择框大小,可选 `large` `small` | string | 'default' | +| nzCheckable | 节点前添加 Checkbox 复选框 | boolean | false | +| nzShowExpand | 节点前添加展开图标 | boolean | true | +| nzShowLine | 是否展示连接线 | boolean | false | +| nzAsyncData | 是否异步加载(显示加载状态) | boolean | false | +| nzNodes | treeNodes 数据 | NzTreeNode\[] | \[] | +| nzDefaultExpandAll | 默认展开所有树节点 | boolean | false | +| nzDefaultExpandedKeys | 默认展开指定的树节点 | string\[] | \[] | +| nzExpandChange | 点击展开树节点图标调用 | EventEmitter | - | diff --git a/components/tree-select/index.ts b/components/tree-select/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/components/tree-select/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/components/tree-select/nz-tree-select.component.ts b/components/tree-select/nz-tree-select.component.ts new file mode 100644 index 00000000000..a903d20449c --- /dev/null +++ b/components/tree-select/nz-tree-select.component.ts @@ -0,0 +1,510 @@ +import { BACKSPACE } from '@angular/cdk/keycodes'; +import { ConnectedPositionStrategy, Overlay, OverlayConfig, OverlayRef, PositionStrategy } from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { DOCUMENT } from '@angular/common'; +import { + forwardRef, + AfterViewInit, + Component, + ElementRef, + EventEmitter, + HostListener, + Inject, + Input, + OnDestroy, + OnInit, + Optional, + Output, + Renderer2, + TemplateRef, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +import { Subscription } from 'rxjs/Subscription'; +import { fromEvent } from 'rxjs/observable/fromEvent'; +import { merge } from 'rxjs/observable/merge'; +import { of as observableOf } from 'rxjs/observable/of'; +import { filter } from 'rxjs/operators/filter'; +import { tap } from 'rxjs/operators/tap'; + +import { selectDropDownAnimation } from '../core/animation/select-dropdown-animations'; +import { selectTagAnimation } from '../core/animation/select-tag-animations'; +import { NzFormatEmitEvent } from '../tree/interface'; +import { NzTreeNode } from '../tree/nz-tree-node'; +import { NzTreeComponent } from '../tree/nz-tree.component'; + +@Component({ + selector : 'nz-tree-select', + animations: [ selectDropDownAnimation, selectTagAnimation ], + template : ` + + + + + +
+ + +
+
+ +
+ +
+
+ {{ nzPlaceHolder }} +
+ +
+ {{ selectedNodes[0].title }} +
+ + + +
+
+ +
    +
    + {{ nzPlaceHolder }} +
    + +
  • + + {{ node.title }} +
  • +
    + +
+
+ + +
+ `, + providers : [ + { + provide : NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NzTreeSelectComponent), + multi : true + } + ], + host : { + '[class.ant-select]' : 'true', + '[class.ant-select-lg]' : 'nzSize==="large"', + '[class.ant-select-sm]' : 'nzSize==="small"', + '[class.ant-select-enabled]' : '!nzDisabled', + '[class.ant-select-disabled]' : 'nzDisabled', + '[class.ant-select-allow-clear]': 'nzAllowClear', + '[class.ant-select-open]' : 'nzOpen' + }, + styles : [ ` + .ant-select-dropdown { + top: 100%; + left: 0; + position: relative; + width: 100%; + margin-top: 4px; + margin-bottom: 4px; + overflow: auto; + } + ` ] +}) +export class NzTreeSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { + + isInit = false; + isComposing = false; + isDestroy = true; + inputValue = ''; + dropDownClassMap: { [ className: string ]: boolean }; + dropDownPosition: 'top' | 'center' | 'bottom' = 'bottom'; + overlayRef: OverlayRef | null; + portal: TemplatePortal<{}>; + positionStrategy: ConnectedPositionStrategy; + overlayBackdropClickSubscription: Subscription; + selectionChangeSubscription: Subscription; + + selectedNodes: NzTreeNode[] = []; + value: string[] = []; + + @Input() nzOpen = false; + @Input() nzAllowClear = true; + @Input() nzSize = 'default'; + @Input() nzDropdownMatchSelectWidth = false; + @Input() nzPlaceHolder = ''; + @Input() nzShowSearch = true; + @Input() nzDisabled = false; + @Input() nzDropdownStyle: { [ key: string ]: string; }; + + @Input() nzCheckable = false; + @Input() nzShowExpand = true; + @Input() nzShowLine = false; + @Input() nzAsyncData = false; + @Input() nzMultiple = false; + @Input() nzDefaultExpandAll = false; + @Input() nzDefaultExpandedKeys: string[] = []; + @Input() nzNodes: NzTreeNode[] = []; + + @Output() nzOpenChange = new EventEmitter(); + @Output() nzCleared = new EventEmitter(); + @Output() nzRemoved = new EventEmitter(); + + @Output() nzExpandChange = new EventEmitter(); + @Output() nzTreeClick = new EventEmitter(); + @Output() nzTreeCheckBoxChange = new EventEmitter(); + + @ViewChild('inputElement') inputElement: ElementRef; + @ViewChild('treeSelect') treeSelect: ElementRef; + @ViewChild('dropdownTemplate', { read: TemplateRef }) dropdownTemplate; + @ViewChild('treeRef') treeRef: NzTreeComponent; + + onChange: (value: string[] | string) => void; + onTouched: () => void = () => null; + + get placeHolderDisplay(): string { + return this.inputValue || this.isComposing || this.selectedNodes.length ? 'none' : 'block'; + } + + get searchDisplay(): string { + return this.nzOpen ? 'block' : 'none'; + } + + get isMultiple(): boolean { + return this.nzMultiple || this.nzCheckable; + } + + get selectedValueDisplay(): { [ key: string ]: string } { + let showSelectedValue = false; + let opacity = 1; + if (!this.nzShowSearch) { + showSelectedValue = true; + } else { + if (this.nzOpen) { + showSelectedValue = !(this.inputValue || this.isComposing); + if (showSelectedValue) { + opacity = 0.4; + } + } else { + showSelectedValue = true; + } + } + return { + display: showSelectedValue ? 'block' : 'none', + opacity: `${opacity}` + }; + } + + constructor( + @Optional() @Inject(DOCUMENT) private document: any, // tslint:disable-line:no-any + @Optional() private element: ElementRef, + private renderer: Renderer2, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef) { + } + + @HostListener('click') + trigger(): void { + if (this.nzDisabled || (!this.nzDisabled && this.nzOpen)) { + this.closeDropDown(); + } else { + this.openDropdown(); + if (this.nzShowSearch) { + this.focusOnInput(); + } + } + } + + openDropdown(): void { + if (!this.nzDisabled) { + this.nzOpen = true; + this.nzOpenChange.emit(this.nzOpen); + this.updateCdkConnectedOverlayStatus(); + } + } + + closeDropDown(): void { + this.onTouched(); + this.nzOpen = false; + this.nzOpenChange.emit(this.nzOpen); + this.updateCdkConnectedOverlayStatus(); + } + + onKeyDownInput(e: KeyboardEvent): void { + const keyCode = e.keyCode; + const eventTarget = e.target as HTMLInputElement; + if ( + this.isMultiple && + !eventTarget.value && + keyCode === BACKSPACE + ) { + e.preventDefault(); + if (this.selectedNodes.length) { + this.removeSelected(this.selectedNodes[ this.selectedNodes.length - 1 ]); + } + } + } + + setInputValue(value: string): void { + this.inputValue = value; + this.updateInputWidth(); + this.updatePosition(); + } + + detachOverlay(): void { + if (this.overlayRef && this.overlayRef.hasAttached()) { + this.overlayRef.detach(); + this.overlayBackdropClickSubscription.unsubscribe(); + this.onTouched(); + this.nzOpen = false; + this.nzOpenChange.emit(this.nzOpen); + } + } + + removeSelected(node: NzTreeNode, emit: boolean = true): void { + node.isSelected = false; + node.isChecked = false; + if (this.nzCheckable) { + this.treeRef.nzTreeService.checkTreeNode(node); + this.treeRef.nzTreeService.setCheckedNodeList(node); + } else { + this.treeRef.nzTreeService.setSelectedNodeList(node, this.nzMultiple); + } + if (emit) { + this.nzRemoved.emit(node); + } + } + + focusOnInput(): void { + setTimeout(() => { + if (this.inputElement) { + this.inputElement.nativeElement.focus(); + } + }); + } + + attachOverlay(): void { + this.portal = new TemplatePortal(this.dropdownTemplate, this.viewContainerRef); + this.overlayRef = this.overlay.create(this.getOverlayConfig()); + this.overlayRef.attach(this.portal); + this.overlayBackdropClickSubscription = this.subscribeOverlayBackdropClick(); + } + + getOverlayConfig(): OverlayConfig { + const overlayWidth = this.treeSelect.nativeElement.getBoundingClientRect().width; + return new OverlayConfig({ + positionStrategy : this.getOverlayPosition(), + scrollStrategy : this.overlay.scrollStrategies.reposition(), + [ this.nzDropdownMatchSelectWidth ? 'width' : 'minWidth' ]: overlayWidth, + hasBackdrop: true + }); + } + + getOverlayPosition(): PositionStrategy { + this.positionStrategy = this.overlay.position().connectedTo( + this.treeSelect, + { originX: 'start', originY: 'bottom' }, { overlayX: 'start', overlayY: 'top' }) + .withFallbackPosition( + { originX: 'start', originY: 'top' }, { overlayX: 'start', overlayY: 'bottom' } + ); + return this.positionStrategy; + } + + subscribeOverlayBackdropClick(): Subscription { + return this.overlayRef.backdropClick() + .subscribe(() => { + this.closeDropDown(); + }); + } + + subscribeSelectionChange(): Subscription { + return merge( + this.nzTreeClick.pipe( + tap((event: NzFormatEmitEvent) => { + const node = event.node; + if (this.nzCheckable && !node.isDisabled && !node.isDisableCheckbox) { + node.isChecked = !node.isChecked; + this.treeRef.nzTreeService.checkTreeNode(node); + this.treeRef.nzTreeService.setCheckedNodeList(node); + } + if (this.nzCheckable) { + node.isSelected = false; + } + }), + filter((event: NzFormatEmitEvent) => { + return this.nzCheckable ? (!event.node.isDisabled && !event.node.isDisableCheckbox) : !event.node.isDisabled; + }) + ), + this.nzCheckable ? this.nzTreeCheckBoxChange : observableOf(), + this.nzCleared, + this.nzRemoved + ).subscribe(() => { + this.updateSelectedNodes(); + const value = this.selectedNodes.map(node => node.key); + this.value = [...value]; + if (this.nzShowSearch) { + this.inputValue = ''; + } + if (this.isMultiple) { + this.onChange(value); + if (this.nzShowSearch) { + this.focusOnInput(); + } + } else { + this.closeDropDown(); + this.onChange(value.length ? value[ 0 ] : null); + } + + }); + } + + updateSelectedNodes(): void { + this.selectedNodes = [ ...(this.nzCheckable ? this.treeRef.getCheckedNodeList() : this.treeRef.getSelectedNodeList()) ]; + } + + updatePosition(): void { + this.overlayRef.updatePosition(); + } + + updateInputWidth(): void { + if (this.isMultiple && this.inputElement) { + if (this.inputValue || this.isComposing) { + this.renderer.setStyle(this.inputElement.nativeElement, 'width', `${this.inputElement.nativeElement.scrollWidth}px`); + } else { + this.renderer.removeStyle(this.inputElement.nativeElement, 'width'); + } + } + } + + onClearSelection(): void { + this.selectedNodes.forEach(node => { + this.removeSelected(node, false); + }); + this.nzCleared.emit(); + this.closeDropDown(); + } + + updateDropDownClassMap(): void { + if (this.treeRef && !this.treeRef.classMap[ 'ant-select-tree' ]) { + this.treeRef.classMap = { ...this.treeRef.classMap, [ 'ant-select-tree' ]: true }; + } + this.dropDownClassMap = { + [ 'ant-select-dropdown' ] : true, + [ 'ant-select-tree-dropdown' ] : true, + [ `ant-select-dropdown--single` ] : !this.nzMultiple, + [ `ant-select-dropdown--multiple` ] : this.nzMultiple, + [ `ant-select-dropdown-placement-bottomLeft` ]: this.dropDownPosition === 'bottom', + [ `ant-select-dropdown-placement-topLeft` ] : this.dropDownPosition === 'top' + }; + } + + updateCdkConnectedOverlayStatus(): void { + if (this.nzOpen) { + this.renderer.removeStyle(this.overlayRef.backdropElement, 'display'); + } else { + this.renderer.setStyle(this.overlayRef.backdropElement, 'display', 'none'); + } + } + + writeValue(value: string[] | string): void { + if (value) { + if (this.isMultiple && Array.isArray(value)) { + this.value = value; + } else { + this.value = [ (value as string) ]; + } + setTimeout(() => this.updateSelectedNodes(), 100); + } + } + + registerOnChange(fn: (_: string[] | string) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + } + + ngOnInit(): void { + this.isDestroy = false; + this.selectionChangeSubscription = this.subscribeSelectionChange(); + Promise.resolve().then(() => { + this.updateDropDownClassMap(); + this.updateCdkConnectedOverlayStatus(); + }); + } + + ngOnDestroy(): void { + this.isDestroy = true; + this.detachOverlay(); + this.selectionChangeSubscription.unsubscribe(); + this.overlayBackdropClickSubscription.unsubscribe(); + } + + ngAfterViewInit(): void { + this.attachOverlay(); + this.isInit = true; + } + + setDisabledState(isDisabled: boolean): void { + this.nzDisabled = isDisabled; + this.closeDropDown(); + } +} diff --git a/components/tree-select/nz-tree-select.module.ts b/components/tree-select/nz-tree-select.module.ts new file mode 100644 index 00000000000..86e696764ce --- /dev/null +++ b/components/tree-select/nz-tree-select.module.ts @@ -0,0 +1,14 @@ +import { OverlayModule } from '@angular/cdk/overlay'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NzTreeModule } from '../tree/nz-tree.module'; +import { NzTreeSelectComponent } from './nz-tree-select.component'; + +@NgModule({ + imports : [ CommonModule, OverlayModule, FormsModule, NzTreeModule ], + declarations: [ NzTreeSelectComponent ], + exports : [ NzTreeSelectComponent ] +}) +export class NzTreeSelectModule { +} diff --git a/components/tree-select/nz-tree-select.spec.ts b/components/tree-select/nz-tree-select.spec.ts new file mode 100644 index 00000000000..b816c42e682 --- /dev/null +++ b/components/tree-select/nz-tree-select.spec.ts @@ -0,0 +1,490 @@ +import { BACKSPACE } from '@angular/cdk/keycodes'; +import { OverlayContainer } from '@angular/cdk/overlay'; +import { Component, ViewChild } from '@angular/core'; +import { async, fakeAsync, flush, inject, tick, TestBed } from '@angular/core/testing'; +import { FormsModule, FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + createKeyboardEvent, + dispatchFakeEvent, + dispatchMouseEvent, + typeInElement +} from '../core/testing'; + +import { NzTreeNode } from '../tree/nz-tree-node'; +import { NzTreeSelectComponent } from './nz-tree-select.component'; +import { NzTreeSelectModule } from './nz-tree-select.module'; + +describe('tree-select component', () => { + let overlayContainer: OverlayContainer; + let overlayContainerElement: HTMLElement; + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports : [ NzTreeSelectModule, NoopAnimationsModule, FormsModule, ReactiveFormsModule ], + declarations: [ NzTestTreeSelectBasicComponent, NzTestTreeSelectCheckableComponent, NzTestTreeSelectFormComponent ] + }); + TestBed.compileComponents(); + inject([ OverlayContainer ], (oc: OverlayContainer) => { + overlayContainer = oc; + overlayContainerElement = oc.getContainerElement(); + })(); + })); + afterEach(inject([ OverlayContainer ], (currentOverlayContainer: OverlayContainer) => { + currentOverlayContainer.ngOnDestroy(); + overlayContainer.ngOnDestroy(); + })); + + describe('basic', () => { + let fixture; + let testComponent: NzTestTreeSelectBasicComponent; + let treeSelectComponent: NzTreeSelectComponent; + let treeSelect; + + beforeEach(fakeAsync(() => { + fixture = TestBed.createComponent(NzTestTreeSelectBasicComponent); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + treeSelect = fixture.debugElement.query(By.directive(NzTreeSelectComponent)); + treeSelectComponent = treeSelect.componentInstance; + fixture.detectChanges(); + flush(); + tick(200); + fixture.detectChanges(); + })); + + it('should size work', () => { + testComponent.size = 'small'; + fixture.detectChanges(); + expect(treeSelect.nativeElement.classList).toContain('ant-select-sm'); + testComponent.size = 'large'; + fixture.detectChanges(); + expect(treeSelect.nativeElement.classList).toContain('ant-select-lg'); + }); + it('should allowClear work', () => { + expect(treeSelect.nativeElement.classList).not.toContain('ant-select-allow-clear'); + testComponent.allowClear = true; + fixture.detectChanges(); + expect(treeSelect.nativeElement.classList).toContain('ant-select-allow-clear'); + }); + it('should click toggle open', () => { + treeSelect.nativeElement.click(); + fixture.detectChanges(); + expect(treeSelectComponent.nzOpen).toBe(true); + treeSelect.nativeElement.click(); + fixture.detectChanges(); + expect(treeSelectComponent.nzOpen).toBe(false); + }); + it('should close when the outside clicks', fakeAsync(() => { + treeSelect.nativeElement.click(); + fixture.detectChanges(); + expect(treeSelectComponent.nzOpen).toBe(true); + dispatchFakeEvent(overlayContainerElement.querySelector('.cdk-overlay-backdrop'), 'click'); + fixture.detectChanges(); + tick(); + expect(treeSelectComponent.nzOpen).toBe(false); + })); + it('should disabled work', fakeAsync(() => { + expect(treeSelect.nativeElement.classList).toContain('ant-select-enabled'); + testComponent.disabled = true; + fixture.detectChanges(); + expect(treeSelect.nativeElement.classList).not.toContain('ant-select-enabled'); + expect(treeSelect.nativeElement.classList).toContain('ant-select-disabled'); + expect(treeSelectComponent.nzOpen).toBe(false); + treeSelect.nativeElement.click(); + fixture.detectChanges(); + tick(); + expect(treeSelectComponent.nzOpen).toBe(false); + treeSelectComponent.openDropdown(); + treeSelect.nativeElement.click(); + fixture.detectChanges(); + tick(); + })); + it('should clear value work', fakeAsync(() => { + testComponent.allowClear = true; + fixture.detectChanges(); + expect(testComponent.value).toBe('10001'); + treeSelectComponent.updateSelectedNodes(); + fixture.detectChanges(); + treeSelect.nativeElement.querySelector('.ant-select-selection__clear').click(); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expect(testComponent.value).toBe(null); + })); + it('should dropdown style work', fakeAsync(() => { + treeSelect.nativeElement.click(); + fixture.detectChanges(); + expect(treeSelectComponent.nzOpen).toBe(true); + flush(); + const targetElement = overlayContainerElement.querySelector('.ant-select-dropdown') as HTMLElement; + expect(targetElement.style.height).toBe('120px'); + })); + it('should click option close dropdown', fakeAsync(() => { + treeSelect.nativeElement.click(); + fixture.detectChanges(); + expect(treeSelectComponent.nzOpen).toBe(true); + fixture.detectChanges(); + const targetNode = overlayContainerElement.querySelectorAll('li')[ 2 ]; + dispatchMouseEvent(targetNode, 'click'); + fixture.detectChanges(); + flush(); + expect(treeSelectComponent.nzOpen).toBe(false); + })); + it('should showSearch work', fakeAsync(() => { + treeSelectComponent.updateSelectedNodes(); + fixture.detectChanges(); + testComponent.showSearch = true; + fixture.detectChanges(); + treeSelect.nativeElement.click(); + fixture.detectChanges(); + expect(treeSelect.nativeElement.querySelector('.ant-select-search--inline')).not.toBeNull(); + testComponent.showSearch = false; + fixture.detectChanges(); + tick(); + expect(treeSelect.nativeElement.querySelector('.ant-select-search--inline')).toBeNull(); + })); + it('should selectedValueDisplay style correct', fakeAsync(() => { + testComponent.showSearch = true; + fixture.detectChanges(); + treeSelectComponent.updateSelectedNodes(); + fixture.detectChanges(); + const selectedValueEl = treeSelect.nativeElement.querySelector('.ant-select-selection-selected-value'); + const inputEl = treeSelect.nativeElement.querySelector('.ant-select-search__field'); + expect(selectedValueEl.style.display).toBe('block'); + expect(selectedValueEl.style.opacity).toBe('1'); + treeSelectComponent.nzOpen = true; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + expect(selectedValueEl.style.display).toBe('block'); + expect(selectedValueEl.style.opacity).toBe('0.4'); + inputEl.value = 'test'; + dispatchFakeEvent(inputEl, 'input'); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expect(selectedValueEl.style.display).toBe('none'); + expect(selectedValueEl.style.opacity).toBe('1'); + })); + }); + + describe('checkable', () => { + let fixture; + let testComponent: NzTestTreeSelectCheckableComponent; + let treeSelectComponent: NzTreeSelectComponent; + let treeSelect; + + beforeEach(fakeAsync(() => { + fixture = TestBed.createComponent(NzTestTreeSelectCheckableComponent); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + treeSelect = fixture.debugElement.query(By.directive(NzTreeSelectComponent)); + treeSelectComponent = treeSelect.componentInstance; + fixture.detectChanges(); + flush(); + tick(200); + fixture.detectChanges(); + })); + + it('should is multiple', fakeAsync(() => { + treeSelect.nativeElement.click(); + fixture.detectChanges(); + expect(treeSelectComponent.nzOpen).toBe(true); + expect(treeSelectComponent.isMultiple).toBe(true); + flush(); + })); + + it('should update input width', fakeAsync(() => { + treeSelect.nativeElement.click(); + fixture.detectChanges(); + testComponent.showSearch = true; + fixture.detectChanges(); + flush(); + const input = treeSelect.nativeElement.querySelector('input') as HTMLInputElement; + typeInElement('test', input); + fixture.detectChanges(); + flush(); + const beforeWidth = input.style.width; + typeInElement('test test test', input); + fixture.detectChanges(); + flush(); + expect(input.style.width !== beforeWidth).toBe(true); + treeSelectComponent.inputValue = ''; + fixture.detectChanges(); + flush(); + typeInElement('', input); + fixture.detectChanges(); + flush(); + expect(input.style.width === '').toBe(true); + })); + + it('should remove checked when press backs', fakeAsync(() => { + treeSelect.nativeElement.click(); + fixture.detectChanges(); + testComponent.showSearch = true; + fixture.detectChanges(); + flush(); + const input = treeSelect.nativeElement.querySelector('input') as HTMLInputElement; + const BACKSPACE_EVENT = createKeyboardEvent('keydown', BACKSPACE, input); + treeSelectComponent.updateSelectedNodes(); + fixture.detectChanges(); + expect(treeSelectComponent.selectedNodes.length === 1).toBe(true); + treeSelectComponent.onKeyDownInput(BACKSPACE_EVENT); + fixture.detectChanges(); + tick(200); + expect(treeSelectComponent.selectedNodes.length === 0).toBe(true); + treeSelectComponent.onKeyDownInput(BACKSPACE_EVENT); + fixture.detectChanges(); + tick(200); + expect(treeSelectComponent.selectedNodes.length === 0).toBe(true); + })); + + it('should click option not close dropdown', fakeAsync(() => { + treeSelect.nativeElement.click(); + fixture.detectChanges(); + expect(treeSelectComponent.nzOpen).toBe(true); + fixture.detectChanges(); + const targetNode = overlayContainerElement.querySelectorAll('li')[ 2 ]; + dispatchMouseEvent(targetNode, 'click'); + fixture.detectChanges(); + flush(); + expect(treeSelectComponent.nzOpen).toBe(true); + })); + }); + + describe('form', () => { + let fixture; + let testComponent; + let treeSelect; + beforeEach(() => { + fixture = TestBed.createComponent(NzTestTreeSelectFormComponent); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + treeSelect = fixture.debugElement.query(By.directive(NzTreeSelectComponent)); + }); + it('should set disabled work', fakeAsync(() => { + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expect(treeSelect.nativeElement.classList).not.toContain('ant-select-disabled'); + testComponent.disable(); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expect(treeSelect.nativeElement.classList).toContain('ant-select-disabled'); + })); + }); + +}); + +@Component({ + selector: 'nz-test-tree-select-basic', + template: ` + + + ` +}) +export class NzTestTreeSelectBasicComponent { + @ViewChild(NzTreeSelectComponent) nzSelectTreeComponent: NzTreeSelectComponent; + expandKeys = [ '1001', '10001' ]; + value = '10001'; + size = 'default'; + allowClear = false; + disabled = false; + showSearch = false; + dropdownMatchSelectWidth = true; + nodes = [ + new NzTreeNode({ + 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 + } + ] + } + ] + } + ] + }), + new NzTreeNode({ + 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', + isLeaf: true + } + ] + } + ] + }) + ]; +} + +@Component({ + selector: 'nz-test-tree-select-checkable', + template: ` + + + ` +}) +export class NzTestTreeSelectCheckableComponent { + @ViewChild(NzTreeSelectComponent) nzSelectTreeComponent: NzTreeSelectComponent; + expandKeys = [ '1001', '10001' ]; + value = [ '1000122' ]; + showSearch = false; + nodes = [ + new NzTreeNode({ + 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 + } + ] + } + ] + } + ] + }), + new NzTreeNode({ + 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', + isLeaf: true + } + ] + } + ] + }) + ]; +} + +@Component({ + selector: 'nz-test-tree-select-form', + template: ` +
+ + +
+ ` +}) +export class NzTestTreeSelectFormComponent { + formGroup: FormGroup; + nodes = [ + new NzTreeNode({ + title : 'root2', + key : '1002', + children: [ + { + title: 'child2.1', + key : '10021' + }, + { + title: 'child2.2', + key : '10022' + } + ] + }) + ]; + + constructor(private formBuilder: FormBuilder) { + this.formGroup = this.formBuilder.group({ + select: [ '10021' ] + }); + } + + disable(): void { + this.formGroup.disable(); + } +} diff --git a/components/tree-select/public-api.ts b/components/tree-select/public-api.ts new file mode 100644 index 00000000000..9f298c0c6ae --- /dev/null +++ b/components/tree-select/public-api.ts @@ -0,0 +1,2 @@ +export * from './nz-tree-select.component'; +export * from './nz-tree-select.module'; diff --git a/components/tree-select/style/index.less b/components/tree-select/style/index.less new file mode 100755 index 00000000000..8048e8086be --- /dev/null +++ b/components/tree-select/style/index.less @@ -0,0 +1,145 @@ +@import "../../style/themes/default"; +@import "../../style/mixins/index"; +@import "../../tree/style/mixin"; +@import "../../checkbox/style/mixin"; + +@select-prefix-cls: ~"@{ant-prefix}-select"; +@select-tree-prefix-cls: ~"@{ant-prefix}-select-tree"; + +.antCheckboxFn(@checkbox-prefix-cls: ~"@{ant-prefix}-select-tree-checkbox"); + +.@{select-tree-prefix-cls} { + .reset-component; + margin: 0; + padding: 0 4px; + margin-top: -4px; + li { + padding: 0; + margin: 8px 0; + list-style: none; + white-space: nowrap; + outline: 0; + &.filter-node { + > span { + font-weight: 500; + } + } + ul { + margin: 0; + padding: 0 0 0 18px; + } + .@{select-tree-prefix-cls}-node-content-wrapper { + display: inline-block; + padding: 3px 5px; + border-radius: 2px; + margin: 0; + cursor: pointer; + text-decoration: none; + color: @text-color; + transition: all .3s; + width: ~"calc(100% - 24px)"; + &:hover { + background-color: @item-hover-bg; + } + &.@{select-tree-prefix-cls}-node-selected { + background-color: @primary-2; + } + } + span { + &.@{select-tree-prefix-cls}-checkbox { + margin: 0 4px 0 0; + + .@{select-tree-prefix-cls}-node-content-wrapper { + width: ~"calc(100% - 46px)"; + } + } + &.@{select-tree-prefix-cls}-switcher, + &.@{select-tree-prefix-cls}-iconEle { + margin: 0; + width: 24px; + height: 24px; + line-height: 22px; + display: inline-block; + vertical-align: middle; + border: 0 none; + cursor: pointer; + outline: none; + text-align: center; + } + &.@{select-tree-prefix-cls}-icon_loading { + &:after { + display: inline-block; + .iconfont-font("\e6ae"); + animation: loadingCircle 1s infinite linear; + color: @primary-color; + } + } + &.@{select-tree-prefix-cls}-switcher { + &.@{select-tree-prefix-cls}-switcher-noop { + cursor: auto; + } + &.@{select-tree-prefix-cls}-switcher_open { + .antTreeSwitcherIcon(); + } + &.@{select-tree-prefix-cls}-switcher_close { + .antTreeSwitcherIcon(); + &:after { + transform: rotate(270deg) scale(0.59); + } + } + } + } + } + &-child-tree { + display: none; + &-open { + display: block; + } + } + li&-treenode-disabled { + > span:not(.@{select-tree-prefix-cls}-switcher), + > .@{select-tree-prefix-cls}-node-content-wrapper, + > .@{select-tree-prefix-cls}-node-content-wrapper span { + color: @disabled-color; + cursor: not-allowed; + } + > .@{select-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; + } +} + +.@{select-prefix-cls}-tree-dropdown { + .reset-component; + .@{select-prefix-cls}-dropdown-search { + display: block; + padding: 4px; + .@{select-prefix-cls}-search__field__wrap { + width: 100%; + } + .@{select-prefix-cls}-search__field { + padding: 4px 7px; + width: 100%; + box-sizing: border-box; + border: @border-width-base @border-style-base @border-color-base; + border-radius: 4px; + outline: none; + } + &.@{select-prefix-cls}-search--hide { + display: none; + } + } + .@{select-prefix-cls}-not-found { + cursor: not-allowed; + color: @disabled-color; + padding: 7px 16px; + display: block; + } +} diff --git a/components/tree/nz-tree-node.component.ts b/components/tree/nz-tree-node.component.ts index 44dc84fc651..d06bac09679 100644 --- a/components/tree/nz-tree-node.component.ts +++ b/components/tree/nz-tree-node.component.ts @@ -192,8 +192,9 @@ export class NzTreeNodeComponent implements OnInit, AfterViewInit { @Input() set nzDefaultSelectedKeys(value: string[]) { this._defaultSelectedKeys = value; - if (value && !this.nzTreeNode.isDisabled && this.nzMultiple && value.indexOf(this.nzTreeNode.key) > -1) { + if (value && !this.nzTreeNode.isDisabled && value.indexOf(this.nzTreeNode.key) > -1) { this.nzTreeNode.isSelected = true; + this.nzTreeService.setSelectedNodeList(this.nzTreeNode, this.nzMultiple); } } diff --git a/components/tree/nz-tree-node.ts b/components/tree/nz-tree-node.ts index 0532a4ae098..3e4e3b917fd 100644 --- a/components/tree/nz-tree-node.ts +++ b/components/tree/nz-tree-node.ts @@ -65,7 +65,7 @@ export class NzTreeNode { if (typeof(option.children) !== 'undefined' && option.children !== null) { option.children.forEach( (nodeOptions) => { - if (option.checked && !option.disabled) { + if (option.checked && !option.disabled && !nodeOptions.disabled && !nodeOptions.disableCheckbox) { nodeOptions.checked = option.checked; } this.children.push(new NzTreeNode(nodeOptions, this)); @@ -95,8 +95,12 @@ export class NzTreeNode { (node) => { let tNode = node; if (tNode instanceof NzTreeNode) { - tNode.parentNode = this; + tNode = new NzTreeNode({ + checked: !tNode.origin.disabled && !tNode.origin.disableCheckbox && this.isChecked, + ...(tNode.origin as NzTreeNodeOptions) + }, this); } else { + node.checked = !node.disabled && !node.disableCheckbox && this.isChecked; tNode = new NzTreeNode(node, this); } tNode.level = this.level + 1; diff --git a/components/tree/nz-tree.component.ts b/components/tree/nz-tree.component.ts index 2a10ef0d51c..f5b78949b01 100644 --- a/components/tree/nz-tree.component.ts +++ b/components/tree/nz-tree.component.ts @@ -60,6 +60,7 @@ export class NzTreeComponent implements OnInit { ['draggable-tree'] : false }; ngModelNodes: NzTreeNode[] = []; + defaultCheckedKeys: string[] = []; @ContentChild('nzTreeTemplate') nzTreeTemplate: TemplateRef<{}>; @Input() nzCheckStrictly: boolean = false; @@ -69,10 +70,17 @@ export class NzTreeComponent implements OnInit { @Input() nzDraggable; @Input() nzMultiple; @Input() nzDefaultExpandAll: boolean = false; - @Input() nzDefaultCheckedKeys: string[] = []; @Input() nzDefaultExpandedKeys: string[] = []; @Input() nzDefaultSelectedKeys: string[] = []; @Input() nzBeforeDrop: (confirm: NzFormatBeforeDropEvent) => Observable; + @Input() + set nzDefaultCheckedKeys(value: string[]) { + this.defaultCheckedKeys = value; + this.nzTreeService.initTreeNodes(this.ngModelNodes, this.nzDefaultCheckedKeys, this.nzCheckStrictly); + } + get nzDefaultCheckedKeys(): string[] { + return this.defaultCheckedKeys; + } @Input() set nzShowLine(value: boolean) { diff --git a/components/tree/nz-tree.service.ts b/components/tree/nz-tree.service.ts index d141824e558..b765347b2e0 100644 --- a/components/tree/nz-tree.service.ts +++ b/components/tree/nz-tree.service.ts @@ -106,7 +106,7 @@ export class NzTreeService { const sIndex = this.selectedNodeList.findIndex(cNode => node.key === cNode.key); if (node.isSelected && sIndex === -1) { this.selectedNodeList.push(node); - } else if (sIndex > -1) { + } else if (sIndex > -1 && !node.isSelected) { this.selectedNodeList.splice(sIndex, 1); } } else {