diff --git a/components/_util/BaseMixin.js b/components/_util/BaseMixin.js index cdbd3fd87..1897d3cfe 100644 --- a/components/_util/BaseMixin.js +++ b/components/_util/BaseMixin.js @@ -1,4 +1,3 @@ -import { getOptionProps } from './props-util' export default { directives: { @@ -17,9 +16,9 @@ export default { methods: { setState (state, callback) { const newState = typeof state === 'function' ? state(this.$data) : state - if (this.getDerivedStateFromProps) { - Object.assign(newState, this.getDerivedStateFromProps(getOptionProps(this), this.$data, true) || {}) - } + // if (this.getDerivedStateFromProps) { + // Object.assign(newState, this.getDerivedStateFromProps(getOptionProps(this), { ...this.$data, ...newState }, true) || {}) + // } Object.assign(this.$data, newState) this.$nextTick(() => { callback && callback() diff --git a/components/tree-select/__tests__/__snapshots__/demo.test.js.snap b/components/tree-select/__tests__/__snapshots__/demo.test.js.snap index 1d75453ce..42922c124 100644 --- a/components/tree-select/__tests__/__snapshots__/demo.test.js.snap +++ b/components/tree-select/__tests__/__snapshots__/demo.test.js.snap @@ -1,25 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders ./components/tree-select/demo/basic.md correctly 1`] = ` -<span class="ant-select ant-select-enabled ant-select-allow-clear" style="width: 300px;"><span role="combobox" aria-autocomplete="list" aria-haspopup="true" tabindex="0" class="ant-select-selection - ant-select-selection--single"><span class="ant-select-selection__rendered"><span class="ant-select-selection__placeholder">Please select</span></span><span class="ant-select-arrow" style="outline: none;"><i class="ant-select-arrow-icon anticon anticon-down"><svg viewBox="64 64 896 896" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" class=""><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg></i></span></span></span> -`; +exports[`renders ./components/tree-select/demo/basic.md correctly 1`] = `<span role="combobox" aria-haspopup="listbox" tabindex="0" class="ant-select ant-select-enabled ant-select-allow-clear" style="width: 300px;"><span class="ant-select-selection ant-select-selection--single"><span class="ant-select-selection__rendered"><span class="ant-select-selection__placeholder">Please select</span></span><span class="ant-select-arrow" style="outline: none;"><i class="ant-select-arrow-icon anticon anticon-down"><svg viewBox="64 64 896 896" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" class=""><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg></i></span></span></span>`; exports[`renders ./components/tree-select/demo/checkable.md correctly 1`] = ` -<span class="ant-select ant-select-enabled" style="width: 300px;"><span role="combobox" aria-autocomplete="list" aria-haspopup="true" class="ant-select-selection - ant-select-selection--multiple"><div class="ant-select-selection__rendered"><li title="Node1" unselectable="unselectable" class="ant-select-selection__choice"><span class="ant-select-selection__choice__remove"><i class="ant-select-remove-icon anticon anticon-close"><svg viewBox="64 64 896 896" data-icon="close" width="1em" height="1em" fill="currentColor" aria-hidden="true" class=""><path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"></path></svg></i></span><span class="ant-select-selection__choice__content">Node1</span></li> -<li class="ant-select-search ant-select-search--inline"><span class="ant-select-search__field__wrap"><input role="textbox" class="ant-select-search__field"><span class="ant-select-search__field__mirror"> </span></span></li> -</div><span class="ant-select-search__field__placeholder" style="display: none;">Please select</span></span> -</span> +<span role="combobox" aria-haspopup="listbox" tabindex="-1" class="ant-select ant-select-enabled" style="width: 300px;"><span class="ant-select-selection ant-select-selection--multiple"><div class="ant-select-selection__rendered"><li unselectable="unselectable" role="menuitem" title="Node1" class="ant-select-selection__choice"><span class="ant-select-selection__choice__remove"><i class="ant-select-remove-icon anticon anticon-close"><svg viewBox="64 64 896 896" data-icon="close" width="1em" height="1em" fill="currentColor" aria-hidden="true" class=""><path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"></path></svg></i></span><span class="ant-select-selection__choice__content">Node1</span></li> +<li class="ant-select-search ant-select-search--inline"><span class="ant-select-search__field__wrap"><input type="text" aria-label="filter select" aria-autocomplete="list" aria-multiline="false" class="ant-select-search__field" style="width: 0px;"><span class="ant-select-search__field__mirror"> </span></span></li> +</div> +</span></span> `; exports[`renders ./components/tree-select/demo/multiple.md correctly 1`] = ` -<span class="ant-select ant-select-enabled ant-select-allow-clear" style="width: 300px;"><span role="combobox" aria-autocomplete="list" aria-haspopup="true" class="ant-select-selection - ant-select-selection--multiple"><div class="ant-select-selection__rendered"><li class="ant-select-search ant-select-search--inline"><span class="ant-select-search__field__wrap"><input role="textbox" class="ant-select-search__field"><span class="ant-select-search__field__mirror"> </span></span></li> +<span role="combobox" aria-haspopup="listbox" tabindex="-1" class="ant-select ant-select-enabled ant-select-allow-clear" style="width: 300px;"><span class="ant-select-selection ant-select-selection--multiple"><div class="ant-select-selection__rendered"><li class="ant-select-search ant-select-search--inline"><span class="ant-select-search__field__wrap"><input type="text" aria-label="filter select" aria-autocomplete="list" aria-multiline="false" class="ant-select-search__field" style="width: 0px;"><span class="ant-select-search__field__mirror"> </span></span></li> </div><span class="ant-select-search__field__placeholder" style="display: block;">Please select</span></span></span> `; -exports[`renders ./components/tree-select/demo/treeData.md correctly 1`] = ` -<span class="ant-select ant-select-enabled" style="width: 300px;"><span role="combobox" aria-autocomplete="list" aria-haspopup="true" tabindex="0" class="ant-select-selection - ant-select-selection--single"><span class="ant-select-selection__rendered"><span class="ant-select-selection__placeholder">Please select</span></span><span class="ant-select-arrow" style="outline: none;"><i class="ant-select-arrow-icon anticon anticon-down"><svg viewBox="64 64 896 896" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" class=""><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg></i></span></span></span> -`; +exports[`renders ./components/tree-select/demo/suffix.md correctly 1`] = `<span role="combobox" aria-haspopup="listbox" tabindex="0" class="ant-select ant-select-enabled ant-select-allow-clear" style="width: 300px;"><span class="ant-select-selection ant-select-selection--single"><span class="ant-select-selection__rendered"><span class="ant-select-selection__placeholder">Please select</span></span><span class="ant-select-arrow" style="outline: none;"><i class="anticon anticon-smile"><svg viewBox="64 64 896 896" data-icon="smile" width="1em" height="1em" fill="currentColor" aria-hidden="true" class=""><path d="M288 421a48 48 0 1 0 96 0 48 48 0 1 0-96 0zm352 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2A370.4 370.4 0 0 1 248.9 775c-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8A370.4 370.4 0 0 1 249 248.9c34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2A370.4 370.4 0 0 1 775.1 249c34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8A368.89 368.89 0 0 1 775 775zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-.3-4.2-3.9-7.4-8.1-7.4H360a8 8 0 0 0-8 8.4c4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6a8 8 0 0 0-8-8.4z"></path></svg></i></span></span></span>`; + +exports[`renders ./components/tree-select/demo/treeData.md correctly 1`] = `<span role="combobox" aria-haspopup="listbox" tabindex="0" class="ant-select ant-select-enabled" style="width: 300px;"><span class="ant-select-selection ant-select-selection--single"><span class="ant-select-selection__rendered"><span class="ant-select-selection__placeholder">Please select</span></span><span class="ant-select-arrow" style="outline: none;"><i class="ant-select-arrow-icon anticon anticon-down"><svg viewBox="64 64 896 896" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" class=""><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg></i></span></span></span>`; diff --git a/components/tree-select/demo/basic.md b/components/tree-select/demo/basic.md index 735280343..39cdbfece 100644 --- a/components/tree-select/demo/basic.md +++ b/components/tree-select/demo/basic.md @@ -22,7 +22,7 @@ The most basic usage. > <a-tree-select-node value='parent 1' title='parent 1' key='0-1'> <a-tree-select-node value='parent 1-0' title='parent 1-0' key='0-1-1'> - <a-tree-select-node value='leaf1' title='my leaf' key='random' /> + <a-tree-select-node :selectable="false" value='leaf1' title='my leaf' key='random' /> <a-tree-select-node value='leaf2' title='your leaf' key='random1' /> </a-tree-select-node> <a-tree-select-node value='parent 1-1' title='parent 1-1' key='random2'> @@ -39,12 +39,13 @@ The most basic usage. export default { data () { return { + treeExpandedKeys: [], value: undefined, } }, methods: { onChange (value) { - console.log(arguments) + console.log(value) this.value = value }, }, diff --git a/components/tree-select/demo/checkable.md b/components/tree-select/demo/checkable.md index 9abca3780..49be73220 100644 --- a/components/tree-select/demo/checkable.md +++ b/components/tree-select/demo/checkable.md @@ -18,7 +18,6 @@ Multiple and checkable. treeCheckable :showCheckedStrategy="SHOW_PARENT" searchPlaceholder='Please select' - treeNodeFilterProp='label' /> </template> @@ -27,29 +26,29 @@ import { TreeSelect } from 'ant-design-vue' const SHOW_PARENT = TreeSelect.SHOW_PARENT const treeData = [{ - label: 'Node1', + title: 'Node1', value: '0-0', key: '0-0', children: [{ - label: 'Child Node1', + title: 'Child Node1', value: '0-0-0', key: '0-0-0', }], }, { - label: 'Node2', + title: 'Node2', value: '0-1', key: '0-1', children: [{ - label: 'Child Node3', + title: 'Child Node3', value: '0-1-0', key: '0-1-0', disabled: true, }, { - label: 'Child Node4', + title: 'Child Node4', value: '0-1-1', key: '0-1-1', }, { - label: 'Child Node5', + title: 'Child Node5', value: '0-1-2', key: '0-1-2', }], @@ -64,7 +63,7 @@ export default { }, methods: { onChange (value) { - console.log('onChange ', value, arguments) + console.log('onChange ', value) this.value = value }, }, diff --git a/components/tree-select/demo/index.vue b/components/tree-select/demo/index.vue index a0e0f736d..7a74cb360 100644 --- a/components/tree-select/demo/index.vue +++ b/components/tree-select/demo/index.vue @@ -3,6 +3,7 @@ import Basic from './basic' import Checkable from './checkable' import Multiple from './multiple' import TreeData from './treeData' +import Suffix from './suffix' import CN from '../index.zh-CN.md' import US from '../index.en-US.md' @@ -35,6 +36,7 @@ export default { <Checkable/> <Multiple/> <TreeData/> + <Suffix /> <api> <template slot='cn'> <CN/> diff --git a/components/tree-select/demo/multiple.md b/components/tree-select/demo/multiple.md index 3fcf57a50..789d9bf89 100644 --- a/components/tree-select/demo/multiple.md +++ b/components/tree-select/demo/multiple.md @@ -47,14 +47,14 @@ export default { }, methods: { onChange (value) { - console.log(arguments) + console.log(value) this.value = value }, onSearch () { - console.log(arguments) + console.log(...arguments) }, onSelect () { - console.log(arguments) + console.log(...arguments) }, }, } diff --git a/components/tree-select/demo/suffix.md b/components/tree-select/demo/suffix.md new file mode 100644 index 000000000..1ea335936 --- /dev/null +++ b/components/tree-select/demo/suffix.md @@ -0,0 +1,54 @@ +<cn> +#### 后缀图标 +最简单的用法。 +</cn> + +<us> +#### Suffix +The most basic usage. +</us> + +```html +<template> + <a-tree-select + showSearch + style="width: 300px" + :value="value" + :dropdownStyle="{ maxHeight: '400px', overflow: 'auto' }" + placeholder='Please select' + allowClear + treeDefaultExpandAll + @change="onChange" + > + <a-icon slot="suffixIcon" type="smile" /> + <a-tree-select-node value='parent 1' title='parent 1' key='0-1'> + <a-tree-select-node value='parent 1-0' title='parent 1-0' key='0-1-1'> + <a-tree-select-node value='leaf1' title='my leaf' key='random' /> + <a-tree-select-node value='leaf2' title='your leaf' key='random1' /> + </a-tree-select-node> + <a-tree-select-node value='parent 1-1' title='parent 1-1' key='random2'> + <a-tree-select-node value='sss' key='random3'> + <b style="color: #08c" slot="title">sss</b> + </a-tree-select-node> + </a-tree-select-node> + </a-tree-select-node> + </a-tree-select> +</template> + +<script> + +export default { + data () { + return { + value: undefined, + } + }, + methods: { + onChange (value) { + console.log(value) + this.value = value + }, + }, +} +</script> +``` diff --git a/components/tree-select/demo/treeData.md b/components/tree-select/demo/treeData.md index e207ce136..ad386ce80 100644 --- a/components/tree-select/demo/treeData.md +++ b/components/tree-select/demo/treeData.md @@ -16,10 +16,9 @@ The tree structure can be populated using `treeData` property. This is a quick a :treeData="treeData" placeholder='Please select' treeDefaultExpandAll - labelInValue v-model="value" > - <span style="color: #08c" slot="label" slot-scope="{key, value}" v-if="key='0-0-1'"> + <span style="color: #08c" slot="title" slot-scope="{key, value}" v-if="key='0-0-1'"> <a-icon type="home"/>Child Node1 {{value}} </span> </a-tree-select> @@ -27,22 +26,22 @@ The tree structure can be populated using `treeData` property. This is a quick a <script> const treeData = [{ - label: 'Node1', + title: 'Node1', value: '0-0', key: '0-0', children: [{ value: '0-0-1', key: '0-0-1', - scopedSlots: { // custom label - label: 'label', + scopedSlots: { // custom title + title: 'title', }, }, { - label: 'Child Node2', + title: 'Child Node2', value: '0-0-2', key: '0-0-2', }], }, { - label: 'Node2', + title: 'Node2', value: '0-1', key: '0-1', }] diff --git a/components/tree-select/index.en-US.md b/components/tree-select/index.en-US.md index eac021c0d..7332d25ad 100644 --- a/components/tree-select/index.en-US.md +++ b/components/tree-select/index.en-US.md @@ -21,16 +21,17 @@ | showCheckedStrategy | The way show selected item in box. **Default:** just show child nodes. **`TreeSelect.SHOW_ALL`:** show all checked treeNodes (include parent treeNode). **`TreeSelect.SHOW_PARENT`:** show checked treeNodes (just show parent treeNode). | enum { TreeSelect.SHOW_ALL, TreeSelect.SHOW_PARENT, TreeSelect.SHOW_CHILD } | TreeSelect.SHOW_CHILD | | showSearch | Whether to display a search input in the dropdown menu(valid only in the single mode) | boolean | false | | size | To set the size of the select input, options: `large` `small` | string | 'default' | +| suffixIcon | The custom suffix icon | VNode \| slot | - | | treeCheckable | Whether to show checkbox on the treeNodes | boolean | false | | treeCheckStrictly | Whether to check nodes precisely (in the `checkable` mode), means parent and child nodes are not associated, and it will make `labelInValue` be true | boolean | false | | treeData | Data of the treeNodes, manual construction work is no longer needed if this property has been set(ensure the Uniqueness of each value) | array<{ value, label, children, [disabled, disableCheckbox, selectable] }> | \[] | | treeDataSimpleMode | Enable simple mode of treeData.(treeData should like this: [{id:1, pId:0, value:'1', label:"test1",...},...], pId is parent node's id) | false\|Array<{ id: string, pId: string, rootPId: null }> | false | | treeDefaultExpandAll | Whether to expand all treeNodes by default | boolean | false | | treeDefaultExpandedKeys | Default expanded treeNodes | string\[] | - | +| treeExpandedKeys | Set expanded keys | string\[] | - | | treeNodeFilterProp | Will be used for filtering if `filterTreeNode` returns true | string | 'value' | | treeNodeLabelProp | Will render as content of select | string | 'title' | | value(v-model) | To set the current selected treeNode(s). | string\|string\[] | - | -| suffixIcon | The custom suffix icon | VNode \| slot | - | ### Events | Events Name | Description | Arguments | @@ -38,6 +39,7 @@ | change | A callback function, can be executed when selected treeNodes or input value change | function(value, label, extra) | | search | A callback function, can be executed when the search input changes. | function(value: string) | | select | A callback function, can be executed when you select a treeNode. | function(value, node, extra) | +| treeExpand | A callback function, can be executed when treeNode expanded | function(expandedKeys) | ### Tree Methods @@ -52,6 +54,7 @@ | Property | Description | Type | Default | | -------- | ----------- | ---- | ------- | +| selectable | can be selected | boolean | true | | disableCheckbox | Disables the checkbox of the treeNode | boolean | false | | disabled | Disabled or not | boolean | false | | isLeaf | Leaf node or not | boolean | false | diff --git a/components/tree-select/index.jsx b/components/tree-select/index.jsx index e5e345239..ac44386a7 100644 --- a/components/tree-select/index.jsx +++ b/components/tree-select/index.jsx @@ -136,10 +136,12 @@ const TreeSelect = { dropdownStyle: { maxHeight: '100vh', overflow: 'auto', ...dropdownStyle }, treeCheckable: checkable, notFoundContent: notFoundContent || locale.notFoundContent, + __propsSymbol__: Symbol(), }, class: cls, on: { ...this.$listeners, change: this.onChange }, ref: 'vcTreeSelect', + scopedSlots: this.$scopedSlots, } return ( <VcTreeSelect {...VcTreeSelectProps}>{filterEmpty(this.$slots.default)}</VcTreeSelect> diff --git a/components/tree-select/index.zh-CN.md b/components/tree-select/index.zh-CN.md index ebb95f66e..bb252c21c 100644 --- a/components/tree-select/index.zh-CN.md +++ b/components/tree-select/index.zh-CN.md @@ -21,24 +21,26 @@ | showCheckedStrategy | 定义选中项回填的方式。`TreeSelect.SHOW_ALL`: 显示所有选中节点(包括父节点). `TreeSelect.SHOW_PARENT`: 只显示父节点(当父节点下所有子节点都选中时). 默认只显示子节点. | enum{TreeSelect.SHOW_ALL, TreeSelect.SHOW_PARENT, TreeSelect.SHOW_CHILD } | TreeSelect.SHOW_CHILD | | showSearch | 在下拉中显示搜索框(仅在单选模式下生效) | boolean | false | | size | 选择框大小,可选 `large` `small` | string | 'default' | +| suffixIcon | 自定义的选择框后缀图标 | VNode \| slot | - | | treeCheckable | 显示 checkbox | boolean | false | | treeCheckStrictly | checkable 状态下节点选择完全受控(父子节点选中状态不再关联),会使得 `labelInValue` 强制为 true | boolean | false | | treeData | treeNodes 数据,如果设置则不需要手动构造 TreeNode 节点(value 在整个树范围内唯一) | array<{value, label, children, [disabled, disableCheckbox, selectable]}> | \[] | | treeDataSimpleMode | 使用简单格式的 treeData,具体设置参考可设置的类型 (此时 treeData 应变为这样的数据结构: [{id:1, pId:0, value:'1', label:"test1",...},...], `pId` 是父节点的 id) | false\|Array<{ id: string, pId: string, rootPId: null }> | false | | treeDefaultExpandAll | 默认展开所有树节点 | boolean | false | | treeDefaultExpandedKeys | 默认展开的树节点 | string\[] | - | +| treeExpandedKeys | 设置展开的树节点 | string\[] | - | | treeNodeFilterProp | 输入项过滤对应的 treeNode 属性 | string | 'value' | | treeNodeLabelProp | 作为显示的 prop 设置 | string | 'title' | | value(v-model) | 指定当前选中的条目 | string/string\[] | - | -| suffixIcon | 自定义的选择框后缀图标 | VNode \| slot | - | ### 事件 | 事件名称 | 说明 | 回调参数 | | --- | --- | --- | -| change | 选中树节点时调用此函数 | function(value, label, extra) | - | -| search | 文本框值变化时回调 | function(value: string) | - | -| select | 被选中时调用 | function(value, node, extra) | - | +| change | 选中树节点时调用此函数 | function(value, label, extra) | +| search | 文本框值变化时回调 | function(value: string) | +| select | 被选中时调用 | function(value, node, extra) | +| treeExpand | 展开节点时调用 | function(expandedKeys) | ### Tree 方法 @@ -53,6 +55,7 @@ | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | +| selectable | 是否可选 | boolean | true | | disableCheckbox | 禁掉 checkbox | boolean | false | | disabled | 是否禁用 | boolean | false | | isLeaf | 是否是叶子节点 | boolean | false | diff --git a/components/tree-select/interface.jsx b/components/tree-select/interface.jsx index 22069cf87..93bfc6870 100644 --- a/components/tree-select/interface.jsx +++ b/components/tree-select/interface.jsx @@ -11,6 +11,14 @@ export const TreeData = PropTypes.shape({ export const TreeSelectProps = () => ({ ...AbstractSelectProps(), + autoFocus: PropTypes.bool, + dropdownStyle: PropTypes.object, + filterTreeNode: PropTypes.oneOfType([Function, Boolean]), + getPopupContainer: PropTypes.func, + labelInValue: PropTypes.bool, + loadData: PropTypes.func, + maxTagCount: PropTypes.number, + maxTagPlaceholder: PropTypes.any, value: PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.array]), defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), multiple: PropTypes.bool, @@ -18,21 +26,18 @@ export const TreeSelectProps = () => ({ // onChange: (value: any, label: any) => void, // onSearch: (value: any) => void, searchPlaceholder: PropTypes.string, - dropdownClassName: PropTypes.string, - dropdownStyle: PropTypes.object, - dropdownMatchSelectWidth: PropTypes.bool, - treeDefaultExpandAll: PropTypes.bool, + showCheckedStrategy: PropTypes.oneOf(['SHOW_ALL', 'SHOW_PARENT', 'SHOW_CHILD']), + suffixIcon: PropTypes.any, treeCheckable: PropTypes.bool, - treeDefaultExpandedKeys: PropTypes.arrayOf(String), - filterTreeNode: PropTypes.func, - treeNodeFilterProp: PropTypes.string, - treeNodeLabelProp: PropTypes.string, + treeCheckStrictly: PropTypes.bool, treeData: PropTypes.arrayOf(Object), treeDataSimpleMode: PropTypes.oneOfType([Boolean, Object]), - loadData: PropTypes.func, - showCheckedStrategy: PropTypes.oneOf(['SHOW_ALL', 'SHOW_PARENT', 'SHOW_CHILD']), - labelInValue: PropTypes.bool, - treeCheckStrictly: PropTypes.bool, - getPopupContainer: PropTypes.func, - suffixIcon: PropTypes.any, + + dropdownClassName: PropTypes.string, + dropdownMatchSelectWidth: PropTypes.bool, + treeDefaultExpandAll: PropTypes.bool, + treeExpandedKeys: PropTypes.arrayOf(String), + treeDefaultExpandedKeys: PropTypes.arrayOf(String), + treeNodeFilterProp: PropTypes.string, + treeNodeLabelProp: PropTypes.string, }) diff --git a/components/tree/Tree.jsx b/components/tree/Tree.jsx index 41ad942d4..28bc9bde4 100644 --- a/components/tree/Tree.jsx +++ b/components/tree/Tree.jsx @@ -2,7 +2,7 @@ import warning from 'warning' import { Tree as VcTree, TreeNode } from '../vc-tree' import animation from '../_util/openAnimation' import PropTypes from '../_util/vue-types' -import { initDefaultProps, getOptionProps } from '../_util/props-util' +import { initDefaultProps, getOptionProps, filterEmpty } from '../_util/props-util' import Icon from '../icon' function TreeProps () { @@ -179,7 +179,7 @@ export default { props: { ...props, checkable: checkable ? <span class={`${prefixCls}-checkbox-inner`} /> : checkable, - children: this.$slots.default || [], + children: filterEmpty(this.$slots.default || []), __propsSymbol__: Symbol(), switcherIcon: this.renderSwitcherIcon, }, diff --git a/components/vc-select/Select.jsx b/components/vc-select/Select.jsx index 721220ae7..33b1aeabb 100644 --- a/components/vc-select/Select.jsx +++ b/components/vc-select/Select.jsx @@ -107,7 +107,7 @@ const Select = { this.__propsSymbol__, 'Replace slots.default with props.children and pass props.__propsSymbol__' ) - return { + const state = { _value: this.getValueFromProps(props, true), // true: use default value _inputValue: props.combobox ? this.getInputValueForCombobox( props, @@ -119,10 +119,10 @@ const Select = { // a flag for aviod redundant getOptionsInfoFromProps call _skipBuildOptionsInfo: true, } - }, - beforeMount () { - const state = this.getDerivedStateFromProps(getOptionProps(this), this.$data) - Object.assign(this.$data, state) + return { + ...state, + ...this.getDerivedStateFromProps(props, state), + } }, mounted () { diff --git a/components/vc-tree-select/assets/select.less b/components/vc-tree-select/assets/select.less index 7efc6c2e4..908ddb88b 100644 --- a/components/vc-tree-select/assets/select.less +++ b/components/vc-tree-select/assets/select.less @@ -37,7 +37,8 @@ top: 1px; right: 1px; width: 20px; - b { + &:after { + content: ''; border-color: #999999 transparent transparent transparent; border-style: solid; border-width: 5px 4px 0 4px; @@ -66,22 +67,30 @@ &__clear { font-weight: bold; position: absolute; + } + } - &:after { - content: '×' + &-enabled { + .@{selectPrefixCls}-selection { + &:hover { + border-color: #23c0fa; + box-shadow: 0 0 2px fadeout(#2db7f5, 20%); + } + &:active { + border-color: #2db7f5; + } + } + + &.@{selectPrefixCls}-focused { + .@{selectPrefixCls}-selection { + //border-color: #23c0fa; + border-color: #7700fa; + box-shadow: 0 0 2px fadeout(#2db7f5, 20%); } } } - &-enabled &-selection { - &:hover { - border-color: #23c0fa; - box-shadow: 0 0 2px fadeout(#2db7f5, 20%); - } - &:active { - border-color: #2db7f5; - } - } + &-selection--single { height: 28px; @@ -105,6 +114,9 @@ .@{selectPrefixCls}-selection__clear { top: 5px; right: 20px; + &:after { + content: '×'; + } } } @@ -117,7 +129,7 @@ cursor: not-allowed; color: #ccc; - &:hover { + &:hover{ cursor: not-allowed; color: #ccc; } @@ -130,6 +142,7 @@ } &-search__field__placeholder { + display: block; position: absolute; top: 0; left: 3px; @@ -288,6 +301,7 @@ top: 0; right: 2px; transition: opacity .3s, transform .3s; + &:before { content: '×' } @@ -487,7 +501,7 @@ } &-open { - .@{selectPrefixCls}-arrow b { + .@{selectPrefixCls}-arrow:after { border-color: transparent transparent #888 transparent; border-width: 0 4px 5px 4px; } @@ -498,3 +512,25 @@ padding: 8px; } } + +.custom-icon-demo { + .@{selectPrefixCls} { + &-selection__choice__remove { + &:before { + content: ''; + } + } + + &-arrow { + &:after { + display: none; + } + } + + &-selection__clear { + &:after { + content: ''; + } + } + } +} \ No newline at end of file diff --git a/components/vc-tree-select/assets/tree.less b/components/vc-tree-select/assets/tree.less index 50744851a..7a74b2138 100644 --- a/components/vc-tree-select/assets/tree.less +++ b/components/vc-tree-select/assets/tree.less @@ -38,7 +38,7 @@ } } &.filter-node { - > a { + > .@{treePrefixCls}-node-content-wrapper { color: #a60000!important; font-weight: bold!important; } diff --git a/components/vc-tree-select/demo/basic.jsx b/components/vc-tree-select/demo/basic.jsx index 2eaf66b6d..7881c1bf9 100644 --- a/components/vc-tree-select/demo/basic.jsx +++ b/components/vc-tree-select/demo/basic.jsx @@ -1,12 +1,11 @@ /* eslint react/no-multi-comp:0, no-console:0, no-alert: 0 */ - +import BaseMixin from '../../_util/BaseMixin' import '../assets/index.less' -import './demo.less' - import '../../vc-dialog/assets/index.less' import Dialog from '../../vc-dialog' -import TreeSelect, { TreeNode, SHOW_PARENT } from '../index' +import TreeSelect, { TreeNode, SHOW_PARENT } from '../src/index' import { gData } from './util' +import './demo.less' function isLeaf (value) { if (!value) { @@ -51,89 +50,90 @@ function findPath (value, data) { } export default { - data () { - return { - tsOpen: false, - visible: false, - inputValue: '0-0-0-label', - value: '0-0-0-value1', - // value: ['0-0-0-0-value', '0-0-0-1-value', '0-0-0-2-value'], - lv: { value: '0-0-0-value', label: 'spe label' }, - multipleValue: [], - simpleTreeData: [ - { key: 1, pId: 0, label: 'test1', value: 'test1' }, - { key: 121, pId: 0, label: 'test1', value: 'test121' }, - { key: 11, pId: 1, label: 'test11', value: 'test11' }, - { key: 12, pId: 1, label: 'test12', value: 'test12' }, - { key: 111, pId: 11, label: 'test111', value: 'test111' }, - ], - treeDataSimpleMode: { - id: 'key', - rootPId: 0, - }, - } - }, - - mounted () { - // console.log(this.refs.mul.getInputDOMNode()); - // this.refs.mul.getInputDOMNode().setAttribute('disabled', true); - }, + mixins: [BaseMixin], + data: () => ({ + tsOpen: false, + visible: false, + searchValue: '0-0-0-label', + value: '0-0-0-value1', + // value: ['0-0-0-0-value', '0-0-0-1-value', '0-0-0-2-value'], + lv: { value: '0-0-0-value', label: 'spe label' }, + multipleValue: [], + simpleSearchValue: 'test111', + simpleTreeData: [ + { key: 1, pId: 0, label: 'test1', value: 'test1' }, + { key: 121, pId: 0, label: 'test2', value: 'test2' }, + { key: 11, pId: 1, label: 'test11', value: 'test11' }, + { key: 12, pId: 1, label: 'test12', value: 'test12' }, + { key: 111, pId: 11, label: 'test111', value: 'test111' }, + ], + treeDataSimpleMode: { + id: 'key', + rootPId: 0, + }, + }), methods: { - onClick () { - this.visible = true + onClick () { + this.setState({ + visible: true, + }) }, - onClose () { - this.visible = false + onClose () { + this.setState({ + visible: false, + }) }, onSearch (value) { - console.log(value, arguments) + console.log('Do Search:', value, arguments) + this.setState({ searchValue: value }) }, - onChange (value) { - console.log('onChange', arguments) - this.value = value + onChange (value, ...rest) { + console.log('onChange', value, ...rest) + this.setState({ value }) }, - onChangeChildren (value) { - console.log('onChangeChildren', arguments) + onChangeChildren (...args) { + console.log('onChangeChildren', ...args) + const value = args[0] const pre = value ? this.value : undefined - this.value = isLeaf(value) ? value : pre + this.setState({ value: isLeaf(value) ? value : pre }) }, - onChangeLV (value) { + onChangeLV (value) { console.log('labelInValue', arguments) if (!value) { - this.lv = undefined + this.setState({ lv: undefined }) return } const path = findPath(value.value, gData).map(i => i.label).reverse().join(' > ') - this.lv = { value: value.value, label: path } + this.setState({ lv: { value: value.value, label: path }}) }, onMultipleChange (value) { console.log('onMultipleChange', arguments) - this.multipleValue = value + this.setState({ multipleValue: value }) }, onSelect () { // use onChange instead - console.log(...arguments) + console.log(arguments) }, - onDropdownVisibleChange (visible, info) { + onDropdownVisibleChange (visible, info) { console.log(visible, this.value, info) if (Array.isArray(this.value) && this.value.length > 1 && this.value.length < 3) { - alert('please select more than two item or less than one item.') + window.alert('please select more than two item or less than one item.') return false } return true }, - filterTreeNode (input, child) { - return String(child.title).indexOf(input) === 0 + filterTreeNode (input, child) { + return String(child.data.props.title).indexOf(input) === 0 }, }, @@ -167,10 +167,10 @@ export default { onSearch={this.onSearch} onChange={this.onChange} onSelect={this.onSelect} + __propsSymbol__={Symbol()} /> </div> </Dialog> : null} - <h2>single select</h2> <TreeSelect style={{ width: '300px' }} @@ -180,31 +180,35 @@ export default { placeholder={<i>请下拉选择</i>} searchPlaceholder='please search' showSearch allowClear treeLine - inputValue={this.inputValue} + searchValue={this.searchValue} value={this.value} treeData={gData} treeNodeFilterProp='label' filterTreeNode={false} onSearch={this.onSearch} open={this.tsOpen} - onChange={(value) => { - console.log('onChange', value, arguments) + onChange={(value, ...args) => { + console.log('onChange', value, ...args) if (value === '0-0-0-0-value') { - this.tsOpen = true + this.setState({ tsOpen: true }) } else { - this.tsOpen = false + this.setState({ tsOpen: false }) } - this.value = value + this.setState({ value }) } } dropdownVisibleChange={(v, info) => { - console.log('single dropdownVisibleChange', v, info) + console.log('single onDropdownVisibleChange', v, info) // document clicked if (info.documentClickClose && this.value === '0-0-0-0-value') { return false } + this.setState({ + tsOpen: v, + }) return true } } onSelect={this.onSelect} + __propsSymbol__={Symbol()} /> <h2>single select (just select children)</h2> @@ -221,10 +225,11 @@ export default { treeNodeFilterProp='label' filterTreeNode={false} onChange={this.onChangeChildren} + __propsSymbol__={Symbol()} /> <h2>multiple select</h2> - <TreeSelect ref='mul' + <TreeSelect style={{ width: '300px' }} transitionName='rc-tree-select-dropdown-slide-up' choiceTransitionName='rc-tree-select-selection__choice-zoom' @@ -238,6 +243,7 @@ export default { onChange={this.onMultipleChange} onSelect={this.onSelect} allowClear + __propsSymbol__={Symbol()} /> <h2>check select</h2> @@ -247,17 +253,23 @@ export default { choiceTransitionName='rc-tree-select-selection__choice-zoom' dropdownStyle={{ height: '200px', overflow: 'auto' }} dropdownPopupAlign={{ overflow: { adjustY: 0, adjustX: 0 }, offset: [0, 2] }} - onDropdownVisibleChange={this.onDropdownVisibleChange} + dropdownVisibleChange={this.onDropdownVisibleChange} placeholder={<i>请下拉选择</i>} searchPlaceholder='please search' treeLine maxTagTextLength={10} value={this.value} - inputValue={null} + autoClearSearchValue treeData={gData} treeNodeFilterProp='title' treeCheckable showCheckedStrategy={SHOW_PARENT} onChange={this.onChange} onSelect={this.onSelect} + maxTagCount={2} + maxTagPlaceholder={(valueList) => { + console.log('Max Tag Rest Value:', valueList) + return `${valueList.length} rest...` + }} + __propsSymbol__={Symbol()} /> <h2>labelInValue & show path</h2> @@ -274,6 +286,7 @@ export default { treeNodeFilterProp='label' filterTreeNode={false} onChange={this.onChangeLV} + __propsSymbol__={Symbol()} /> <h2>use treeDataSimpleMode</h2> @@ -283,7 +296,10 @@ export default { placeholder={<i>请下拉选择</i>} searchPlaceholder='please search' treeLine maxTagTextLength={10} - inputValue={'test111'} + searchValue={this.simpleSearchValue} + onSearch={(simpleSearchValue) => { + this.setState({ simpleSearchValue }) + }} value={this.value} treeData={this.simpleTreeData} treeNodeFilterProp='title' @@ -291,13 +307,14 @@ export default { treeCheckable showCheckedStrategy={SHOW_PARENT} onChange={this.onChange} onSelect={this.onSelect} + __propsSymbol__={Symbol()} /> <h2>Testing in extreme conditions (Boundary conditions test) </h2> <TreeSelect style={{ width: '200px' }} dropdownStyle={{ maxHeight: '200px', overflow: 'auto' }} - defaultValue={'leaf1'} multiple treeCheckable showCheckedStrategy={SHOW_PARENT} + defaultValue='leaf1' multiple treeCheckable showCheckedStrategy={SHOW_PARENT} treeDefaultExpandAll treeData={[ { key: '', value: '', label: 'empty value', children: [] }, @@ -308,18 +325,20 @@ export default { ], }, ]} - onChange={(val) => console.log(val, arguments)} + onChange={(val, ...args) => console.log(val, ...args)} + __propsSymbol__={Symbol()} /> <h2>use TreeNode Component (not recommend)</h2> <TreeSelect style={{ width: '200px' }} dropdownStyle={{ maxHeight: '200px', overflow: 'auto' }} - defaultValue={'leaf1'} + defaultValue='leaf1' treeDefaultExpandAll treeNodeFilterProp='title' filterTreeNode={this.filterTreeNode} - onChange={(val) => console.log(val, arguments)} + onChange={(val, ...args) => console.log(val, ...args)} + __propsSymbol__={Symbol()} > <TreeNode value='' title='parent 1' key=''> <TreeNode value='parent 1-0' title='parent 1-0' key='0-1-0'> @@ -331,7 +350,7 @@ export default { title={<span style={{ color: 'red' }}>sss</span>} key='random3' /> <TreeNode value='same value1' title='same txtle' key='0-1-1-1'> - <TreeNode value='same value10' title='same titlexd' key='0-1-1-1-0' /> + <TreeNode value='same value10' title='same titlexd' key='0-1-1-1-0' style={{ color: 'red', background: 'green' }} /> </TreeNode> </TreeNode> </TreeNode> diff --git a/components/vc-tree-select/demo/big-data-generator.jsx b/components/vc-tree-select/demo/big-data-generator.js similarity index 100% rename from components/vc-tree-select/demo/big-data-generator.jsx rename to components/vc-tree-select/demo/big-data-generator.js diff --git a/components/vc-tree-select/demo/big-data.jsx b/components/vc-tree-select/demo/big-data.js similarity index 96% rename from components/vc-tree-select/demo/big-data.jsx rename to components/vc-tree-select/demo/big-data.js index 9ca93ec24..d9b9c9215 100644 --- a/components/vc-tree-select/demo/big-data.jsx +++ b/components/vc-tree-select/demo/big-data.js @@ -56,6 +56,7 @@ export default { treeCheckable showCheckedStrategy={SHOW_PARENT} onChange={this.onChange} + __propsSymbol__={Symbol()} /> </div> <div> @@ -70,6 +71,7 @@ export default { treeCheckStrictly showCheckedStrategy={SHOW_PARENT} onChange={this.onChangeStrictly} + __propsSymbol__={Symbol()} /> </div> </div> diff --git a/components/vc-tree-select/demo/controlled.jsx b/components/vc-tree-select/demo/controlled.jsx new file mode 100644 index 000000000..5a56572b5 --- /dev/null +++ b/components/vc-tree-select/demo/controlled.jsx @@ -0,0 +1,68 @@ +/* eslint react/no-multi-comp:0, no-console:0, no-alert: 0 */ + +import BaseMixin from '../../_util/BaseMixin' +import '../assets/index.less' +import '../../vc-dialog/assets/index.less' +import TreeSelect, { TreeNode } from '../src/index' +import './demo.less' + +export default { + mixins: [BaseMixin], + data: () => ({ + treeExpandedKeys: [], + }), + methods: { + onTreeExpand (treeExpandedKeys) { + this.setState({ + treeExpandedKeys, + }) + }, + + setTreeExpandedKeys () { + this.setState({ + treeExpandedKeys: ['000', '0-1-0'], + }) + }, + + }, + + render () { + const { treeExpandedKeys } = this + + return ( + <div> + <h2>Conrolled treeExpandedKeys</h2> + <TreeSelect + style={{ width: '200px' }} + dropdownStyle={{ maxHeight: '200px', overflow: 'auto' }} + treeExpandedKeys={treeExpandedKeys} + onTreeExpand={this.onTreeExpand} + __propsSymbol__={Symbol()} + > + <TreeNode value='' title='parent 1' key='000'> + <TreeNode value='parent 1-0' title='parent 1-0' key='0-1-0'> + <TreeNode value='leaf1' title='my leaf' key='random' /> + <TreeNode value='leaf2' title='your leaf' key='random1' disabled /> + </TreeNode> + <TreeNode value='parent 1-1' title='parent 1-1' key='0-1-1'> + <TreeNode value='sss' + title={<span style={{ color: 'red' }}>sss</span>} key='random3' + /> + <TreeNode value='same value1' title='same txtle' key='0-1-1-1'> + <TreeNode value='same value10' title='same titlexd' key='0-1-1-1-0' style={{ color: 'red', background: 'green' }} /> + </TreeNode> + </TreeNode> + </TreeNode> + <TreeNode value='same value2' title='same title' key='0-2'> + <TreeNode value='2same value' title='2same title' key='0-2-0' /> + </TreeNode> + <TreeNode value='same value3' title='same title' key='0-3' /> + </TreeSelect> + <button onClick={this.setTreeExpandedKeys}> + Set treeExpandedKeys + </button> + </div> + ) + }, +} + diff --git a/components/vc-tree-select/demo/custom-icons.jsx b/components/vc-tree-select/demo/custom-icons.jsx new file mode 100644 index 000000000..301fe44e3 --- /dev/null +++ b/components/vc-tree-select/demo/custom-icons.jsx @@ -0,0 +1,113 @@ +/* eslint react/no-multi-comp:0, no-console:0, no-alert: 0 */ + +import '../assets/index.less' +import '../../vc-dialog/assets/index.less' +import TreeSelect from '../src/index' +import { gData } from './util' +import './demo.less' + +const bubblePath = 'M632 888H392c-4.4 0-8 3.6-8 8v32c0 ' + + '17.7 14.3 32 32 32h192c17.7 0 32-14.3 32-32v-3' + + '2c0-4.4-3.6-8-8-8zM512 64c-181.1 0-328 146.9-3' + + '28 328 0 121.4 66 227.4 164 284.1V792c0 17.7 1' + + '4.3 32 32 32h264c17.7 0 32-14.3 32-32V676.1c98' + + '-56.7 164-162.7 164-284.1 0-181.1-146.9-328-32' + + '8-328z m127.9 549.8L604 634.6V752H420V634.6l-3' + + '5.9-20.8C305.4 568.3 256 484.5 256 392c0-141.4' + + ' 114.6-256 256-256s256 114.6 256 256c0 92.5-49' + + '.4 176.3-128.1 221.8z' + +const clearPath = 'M793 242H366v-74c0-6.7-7.7-10.4-12.9' + + '-6.3l-142 112c-4.1 3.2-4.1 9.4 0 12.6l142 112c' + + '5.2 4.1 12.9 0.4 12.9-6.3v-74h415v470H175c-4.4' + + ' 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h618c35.3 0 64-' + + '28.7 64-64V306c0-35.3-28.7-64-64-64z' + +const arrowPath = 'M765.7 486.8L314.9 134.7c-5.3-4.1' + + '-12.9-0.4-12.9 6.3v77.3c0 4.9 2.3 9.6 6.1 12.6l36' + + '0 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6' + + '.7 7.7 10.4 12.9 6.3l450.8-352.1c16.4-12.8 16.4-3' + + '7.6 0-50.4z' + +const getSvg = (h, path, iStyle = {}, style = {}) => { + return ( + <i style={iStyle}> + <svg + viewBox='0 0 1024 1024' + width='1em' + height='1em' + fill='currentColor' + style={{ verticalAlign: '-.125em', ...style }} + > + <path d={path} /> + </svg> + </i> + ) +} + +export default { + data () { + const h = this.$createElement + const switcherIcon = (obj) => { + if (obj.isLeaf) { + return getSvg(h, arrowPath, + { cursor: 'pointer', backgroundColor: 'white' }, + { transform: 'rotate(270deg)' }) + } + return getSvg(h, arrowPath, + { cursor: 'pointer', backgroundColor: 'white' }, + { transform: `rotate(${obj.expanded ? 90 : 0}deg)` }) + } + + const inputIcon = getSvg(h, bubblePath) + const clearIcon = getSvg(h, clearPath) + const removeIcon = getSvg(h, clearPath) + return { + iconProps: { + inputIcon, + clearIcon, + removeIcon, + switcherIcon, + }, + + iconPropsFunction: { + inputIcon: () => inputIcon, + clearIcon: () => clearIcon, + removeIcon: () => removeIcon, + switcherIcon, + }, + } + }, + render () { + return ( + <div class='custom-icon-demo'> + <h2>Single</h2> + <TreeSelect + treeData={gData} + placeholder={<span>Please Select</span>} + transitionName='rc-tree-select-dropdown-slide-up' + style={{ width: '300px' }} + dropdownStyle={{ maxHeight: '200px', overflow: 'auto', zIndex: 1500 }} + showSearch allowClear + {...{ props: { ...this.iconProps }}} + __propsSymbol__={Symbol()} + /> + <br /> + <h2>Multiple</h2> + <TreeSelect + treeData={gData} + multiple + placeholder={<span>Please Select</span>} + transitionName='rc-tree-select-dropdown-slide-up' + style={{ width: '300px' }} + dropdownStyle={{ maxHeight: '200px', overflow: 'auto', zIndex: 1500 }} + showSearch allowClear + {...{ props: { ...this.iconPropsFunction }}} + __propsSymbol__={Symbol()} + /> + </div> + ) + }, + +} + diff --git a/components/vc-tree-select/demo/demo.less b/components/vc-tree-select/demo/demo.less index e762c52e6..5d46fd9f8 100644 --- a/components/vc-tree-select/demo/demo.less +++ b/components/vc-tree-select/demo/demo.less @@ -1,4 +1,3 @@ - .rc-tree-select-selection--multiple { max-height: 50px; overflow-y: scroll; @@ -12,4 +11,4 @@ .rc-tree-select-selection--multiple { min-height: 50px; } -} +} \ No newline at end of file diff --git a/components/vc-tree-select/demo/disable.jsx b/components/vc-tree-select/demo/disable.js similarity index 80% rename from components/vc-tree-select/demo/disable.jsx rename to components/vc-tree-select/demo/disable.js index 2600215d8..b4604ed8d 100644 --- a/components/vc-tree-select/demo/disable.jsx +++ b/components/vc-tree-select/demo/disable.js @@ -1,6 +1,7 @@ /* eslint react/no-multi-comp:0, no-console:0 */ +import BaseMixin from '../../_util/BaseMixin' import '../assets/index.less' -import TreeSelect from '../index' +import TreeSelect from '../src/index' const SHOW_PARENT = TreeSelect.SHOW_PARENT @@ -33,20 +34,18 @@ const treeData = [{ }] export default { - data () { - return { - value: ['0-0-0'], - disabled: false, - } - }, - + mixins: [BaseMixin], + data: () => ({ + value: ['0-0-0'], + disabled: false, + }), methods: { - onChange (value) { + onChange (value) { console.log('onChange ', value, arguments) - this.value = value + this.setState({ value }) }, switch (checked) { - this.disabled = checked + this.setState({ disabled: checked }) }, }, @@ -61,6 +60,7 @@ export default { treeCheckable: true, showCheckedStrategy: SHOW_PARENT, searchPlaceholder: 'Please select', + __propsSymbol__: Symbol(), }, on: { change: this.onChange, @@ -77,4 +77,3 @@ export default { ) }, } - diff --git a/components/vc-tree-select/demo/dynamic.jsx b/components/vc-tree-select/demo/dynamic.js similarity index 59% rename from components/vc-tree-select/demo/dynamic.jsx rename to components/vc-tree-select/demo/dynamic.js index 1e92fc062..7c5ff4e20 100644 --- a/components/vc-tree-select/demo/dynamic.jsx +++ b/components/vc-tree-select/demo/dynamic.js @@ -1,35 +1,36 @@ /* eslint react/no-multi-comp:0, no-console:0 */ +import BaseMixin from '../../_util/BaseMixin' import '../assets/index.less' -import TreeSelect from '../index' +import TreeSelect from '../src/index' import { getNewTreeData, generateTreeNodes } from './util' export default { - data () { - return { - treeData: [ - { label: 'pNode 01', value: '0-0', key: '0-0' }, - { label: 'pNode 02', value: '0-1', key: '0-1' }, - { label: 'pNode 03', value: '0-2', key: '0-2', isLeaf: true }, - ], - // value: '0-0', - value: { value: '0-0-0-value', label: '0-0-0-label' }, - } - }, - + mixins: [BaseMixin], + data: () => ({ + treeData: [ + { label: 'pNode 01', value: '0-0', key: '0-0' }, + { label: 'pNode 02', value: '0-1', key: '0-1' }, + { label: 'pNode 03', value: '0-2', key: '0-2', isLeaf: true }, + ], + // value: '0-0', + value: { value: '0-0-0-value', label: '0-0-0-label' }, + }), methods: { - onChange (value) { + onChange (value) { console.log(value) - this.value = value + this.setState({ + value, + }) }, - onLoadData (treeNode) { + onLoadData (treeNode) { console.log(treeNode) return new Promise((resolve) => { setTimeout(() => { const treeData = [...this.treeData] getNewTreeData(treeData, treeNode.eventKey, generateTreeNodes(treeNode), 2) - this.treeData = treeData + this.setState({ treeData }) resolve() }, 500) }) @@ -47,9 +48,9 @@ export default { value={this.value} onChange={this.onChange} loadData={this.onLoadData} + __propsSymbol__={Symbol()} /> </div> ) }, } - diff --git a/components/vc-tree-select/demo/filter.jsx b/components/vc-tree-select/demo/filter.js similarity index 97% rename from components/vc-tree-select/demo/filter.jsx rename to components/vc-tree-select/demo/filter.js index 36f805386..c86a819ae 100644 --- a/components/vc-tree-select/demo/filter.jsx +++ b/components/vc-tree-select/demo/filter.js @@ -71,6 +71,7 @@ export default { treeCheckable onChange={this.onChange} onSelect={this.onSelect} + __propsSymbol__={Symbol()} /> <h2>use treeDataSimpleMode</h2> @@ -89,6 +90,7 @@ export default { treeCheckable showCheckedStrategy={SHOW_PARENT} onChange={this.onChange} onSelect={this.onSelect} + __propsSymbol__={Symbol()} /> <button onClick={this.onDataChange}>change data</button> </div> diff --git a/components/vc-tree-select/demo/form.js b/components/vc-tree-select/demo/form.js index f66317371..dcae2f5f2 100644 --- a/components/vc-tree-select/demo/form.js +++ b/components/vc-tree-select/demo/form.js @@ -25,7 +25,7 @@ const TreeSelectInput = { render () { return ( - <TreeSelect {...{ props: this.$props }} onChange={this.onChange.bind(this)} /> + <TreeSelect {...{ props: this.$props }} onChange={this.onChange} /> ) }, } @@ -57,6 +57,7 @@ const Form = { multiple: true, treeData: gData, treeCheckable: true, + __propsSymbol__: Symbol(), // treeDefaultExpandAll: true, }, } diff --git a/components/vc-tree-select/demo/util.js b/components/vc-tree-select/demo/util.js index 27fe44d06..6fb95eeb0 100644 --- a/components/vc-tree-select/demo/util.js +++ b/components/vc-tree-select/demo/util.js @@ -27,13 +27,15 @@ export function generateData (x = 3, y = 2, z = 1, gData = []) { tns[index].children = [] return _loop(__level, key, tns[index].children) }) + + return null } _loop(z) return gData } export function calcTotal (x = 3, y = 2, z = 1) { - /* eslint no-param-reassign:0*/ - const rec = (n) => n >= 0 ? x * Math.pow(y, n--) + rec(n) : 0 + /* eslint no-param-reassign:0 */ + const rec = (n) => n >= 0 ? x * (y ** (n--)) + rec(n) : 0 return rec(z + 1) } console.log('总节点数(单个tree):', calcTotal()) diff --git a/components/vc-tree-select/index.js b/components/vc-tree-select/index.js index f35201e54..8547ef4f9 100644 --- a/components/vc-tree-select/index.js +++ b/components/vc-tree-select/index.js @@ -1,7 +1,10 @@ -// rc-tree-select 1.12.13 tag // export this package's api +// base 2.4.4 +import Vue from 'vue' import TreeSelect from './src' +import ref from 'vue-ref' +Vue.use(ref, { name: 'ant-ref' }) export default TreeSelect export { TreeNode, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './src' diff --git a/components/vc-tree-select/src/Base/BasePopup.jsx b/components/vc-tree-select/src/Base/BasePopup.jsx new file mode 100644 index 000000000..376cab6cb --- /dev/null +++ b/components/vc-tree-select/src/Base/BasePopup.jsx @@ -0,0 +1,284 @@ +import warning from 'warning' +import PropTypes from '../../../_util/vue-types' +import { Tree } from '../../../vc-tree' +import BaseMixin from '../../../_util/BaseMixin' + +// export const popupContextTypes = { +// onPopupKeyDown: PropTypes.func.isRequired, +// onTreeNodeSelect: PropTypes.func.isRequired, +// onTreeNodeCheck: PropTypes.func.isRequired, +// } +function getDerivedStateFromProps (nextProps, prevState) { + const { _prevProps: prevProps = {}, + _loadedKeys: loadedKeys, + _expandedKeyList: expandedKeyList, + _cachedExpandedKeyList: cachedExpandedKeyList, + } = prevState || {} + const { + valueList, valueEntities, keyEntities, + treeExpandedKeys, filteredTreeNodes, searchValue, + } = nextProps + + const newState = { + _prevProps: { ...nextProps }, + } + + // Check value update + if (valueList !== prevProps.valueList) { + newState._keyList = valueList + .map(({ value }) => valueEntities[value]) + .filter(entity => entity) + .map(({ key }) => key) + } + + // Show all when tree is in filter mode + if ( + !treeExpandedKeys && + filteredTreeNodes && + filteredTreeNodes.length && + filteredTreeNodes !== prevProps.filteredTreeNodes + ) { + newState._expandedKeyList = Object.keys(keyEntities) + } + + // Cache `expandedKeyList` when filter set + if (searchValue && !prevProps.searchValue) { + newState._cachedExpandedKeyList = expandedKeyList + } else if (!searchValue && prevProps.searchValue && !treeExpandedKeys) { + newState._expandedKeyList = cachedExpandedKeyList || [] + newState._cachedExpandedKeyList = [] + } + + // Use expandedKeys if provided + if (prevProps.treeExpandedKeys !== treeExpandedKeys) { + newState._expandedKeyList = treeExpandedKeys + } + + // Clean loadedKeys if key not exist in keyEntities anymore + if (nextProps.loadData) { + newState._loadedKeys = loadedKeys.filter(key => key in keyEntities) + } + + return newState +} +const BasePopup = { + mixins: [BaseMixin], + name: 'BasePopup', + props: { + prefixCls: PropTypes.string, + upperSearchValue: PropTypes.string, + valueList: PropTypes.array, + searchHalfCheckedKeys: PropTypes.array, + valueEntities: PropTypes.object, + keyEntities: PropTypes.object, + treeIcon: PropTypes.bool, + treeLine: PropTypes.bool, + treeNodeFilterProp: PropTypes.string, + treeCheckable: PropTypes.any, + treeCheckStrictly: PropTypes.bool, + treeDefaultExpandAll: PropTypes.bool, + treeDefaultExpandedKeys: PropTypes.array, + treeExpandedKeys: PropTypes.array, + loadData: PropTypes.func, + multiple: PropTypes.bool, + // onTreeExpand: PropTypes.func, + searchValue: PropTypes.string, + treeNodes: PropTypes.any, + filteredTreeNodes: PropTypes.any, + notFoundContent: PropTypes.string, + + ariaId: PropTypes.string, + switcherIcon: PropTypes.any, + // HOC + renderSearch: PropTypes.func, + // onTreeExpanded: PropTypes.func, + + __propsSymbol__: PropTypes.any, + }, + inject: { + vcTreeSelect: { default: {}}, + }, + watch: { + __propsSymbol__ () { + const state = getDerivedStateFromProps(this.$props, this.$data) + this.setState(state) + }, + }, + data () { + warning(this.$props.__propsSymbol__, 'must pass __propsSymbol__') + const { + treeDefaultExpandAll, treeDefaultExpandedKeys, + keyEntities, + } = this.$props + + // TODO: make `expandedKeyList` control + let expandedKeyList = treeDefaultExpandedKeys + if (treeDefaultExpandAll) { + expandedKeyList = Object.keys(keyEntities) + } + + const state = { + _keyList: [], + _expandedKeyList: expandedKeyList, + // Cache `expandedKeyList` when tree is in filter. This is used in `getDerivedStateFromProps` + _cachedExpandedKeyList: [], // eslint-disable-line react/no-unused-state + _loadedKeys: [], + _prevProps: {}, + } + return { + ...state, + ...getDerivedStateFromProps(this.$props, state), + } + }, + methods: { + onTreeExpand (expandedKeyList) { + const { treeExpandedKeys } = this.$props + + // Set uncontrolled state + if (!treeExpandedKeys) { + this.setState({ _expandedKeyList: expandedKeyList }, () => { + this.__emit('treeExpanded') + }) + } + this.__emit('treeExpand', expandedKeyList) + }, + + onLoad (loadedKeys) { + this.setState({ _loadedKeys: loadedKeys }) + }, + + /** + * Not pass `loadData` when searching. To avoid loop ajax call makes browser crash. + */ + getLoadData () { + const { loadData, searchValue } = this.$props + if (searchValue) return null + return loadData + }, + + /** + * This method pass to Tree component which is used for add filtered class + * in TreeNode > li + */ + filterTreeNode (treeNode) { + const { upperSearchValue, treeNodeFilterProp } = this.$props + + const filterVal = treeNode[treeNodeFilterProp] + if (typeof filterVal === 'string') { + return upperSearchValue && (filterVal).toUpperCase().indexOf(upperSearchValue) !== -1 + } + + return false + }, + + renderNotFound () { + const { prefixCls, notFoundContent } = this.$props + + return ( + <span class={`${prefixCls}-not-found`}> + {notFoundContent} + </span> + ) + }, + }, + + render () { + const { _keyList: keyList, _expandedKeyList: expandedKeyList, _loadedKeys: loadedKeys } = this.$data + const { + prefixCls, + treeNodes, filteredTreeNodes, + treeIcon, treeLine, treeCheckable, treeCheckStrictly, multiple, + ariaId, + renderSearch, + switcherIcon, + searchHalfCheckedKeys, + } = this.$props + const { vcTreeSelect: { + onPopupKeyDown, + onTreeNodeSelect, + onTreeNodeCheck, + }} = this + + const loadData = this.getLoadData() + + const treeProps = {} + + if (treeCheckable) { + treeProps.checkedKeys = keyList + } else { + treeProps.selectedKeys = keyList + } + let $notFound + let $treeNodes + if (filteredTreeNodes) { + if (filteredTreeNodes.length) { + treeProps.checkStrictly = true + $treeNodes = filteredTreeNodes + + // Fill halfCheckedKeys + if (treeCheckable && !treeCheckStrictly) { + treeProps.checkedKeys = { + checked: keyList, + halfChecked: searchHalfCheckedKeys, + } + } + } else { + $notFound = this.renderNotFound() + } + } else if (!treeNodes.length) { + $notFound = this.renderNotFound() + } else { + $treeNodes = treeNodes + } + + let $tree + if ($notFound) { + $tree = $notFound + } else { + const treeAllProps = { + props: { + prefixCls: `${prefixCls}-tree`, + showIcon: treeIcon, + showLine: treeLine, + selectable: !treeCheckable, + checkable: treeCheckable, + checkStrictly: treeCheckStrictly, + multiple: multiple, + loadData: loadData, + loadedKeys: loadedKeys, + expandedKeys: expandedKeyList, + filterTreeNode: this.filterTreeNode, + switcherIcon: switcherIcon, + ...treeProps, + __propsSymbol__: Symbol(), + children: $treeNodes, + }, + on: { + select: onTreeNodeSelect, + check: onTreeNodeCheck, + expand: this.onTreeExpand, + load: this.onLoad, + }, + } + $tree = ( + <Tree + {...treeAllProps} + /> + ) + } + + return ( + <div + role='listbox' + id={ariaId} + onKeydown={onPopupKeyDown} + tabIndex={-1} + > + {renderSearch ? renderSearch() : null} + {$tree} + </div> + ) + }, +} + +export default BasePopup diff --git a/components/vc-tree-select/src/Base/BaseSelector.jsx b/components/vc-tree-select/src/Base/BaseSelector.jsx new file mode 100644 index 000000000..4580b4570 --- /dev/null +++ b/components/vc-tree-select/src/Base/BaseSelector.jsx @@ -0,0 +1,187 @@ +/** + * Input Box is in different position for different mode. + * This not the same design as `Select` cause it's followed by antd 0.x `Select`. + * We will not follow the new design immediately since antd 3.x is already released. + * + * So this file named as Selector to avoid confuse. + */ +import { createRef } from '../util' +import PropTypes from '../../../_util/vue-types' +import classNames from 'classnames' +import { initDefaultProps, getComponentFromProp } from '../../../_util/props-util' +import BaseMixin from '../../../_util/BaseMixin' +export const selectorPropTypes = () => ({ + prefixCls: PropTypes.string, + className: PropTypes.string, + open: PropTypes.bool, + valueList: PropTypes.array, // Name as valueList to diff the single value + allowClear: PropTypes.bool, + showArrow: PropTypes.bool, + // onClick: PropTypes.func, + // onBlur: PropTypes.func, + // onFocus: PropTypes.func, + removeSelected: PropTypes.func, + choiceTransitionName: PropTypes.string, + // Pass by component + ariaId: PropTypes.string, + inputIcon: PropTypes.any, + clearIcon: PropTypes.any, + removeIcon: PropTypes.any, + selectorValueList: PropTypes.array, + placeholder: PropTypes.any, + disabled: PropTypes.bool, + focused: PropTypes.bool, +}) + +function noop () {} +export default function (modeName) { + const BaseSelector = { + name: 'BaseSelector', + mixins: [BaseMixin], + props: initDefaultProps({ + ...selectorPropTypes(), + + // Pass by HOC + renderSelection: PropTypes.func.isRequired, + renderPlaceholder: PropTypes.func, + tabIndex: PropTypes.number, + }, { + tabIndex: 0, + }), + inject: { + vcTreeSelect: { default: {}}, + }, + created () { + this.domRef = createRef() + }, + methods: { + onFocus (e) { + const { focused } = this.$props + const { vcTreeSelect: { onSelectorFocus }} = this + + if (!focused) { + onSelectorFocus() + } + this.__emit('focus', e) + }, + + onBlur (e) { + const { vcTreeSelect: { onSelectorBlur }} = this + + // TODO: Not trigger when is inner component get focused + onSelectorBlur() + this.__emit('blur', e) + }, + + focus () { + this.domRef.current.focus() + }, + + blur () { + this.domRef.current.blur() + }, + + renderClear () { + const { prefixCls, allowClear, valueList } = this.$props + const { vcTreeSelect: { onSelectorClear }} = this + + if (!allowClear || !valueList.length || !valueList[0].value) { + return null + } + const clearIcon = getComponentFromProp(this, 'clearIcon') + return ( + <span + key='clear' + class={`${prefixCls}-selection__clear`} + onClick={onSelectorClear} + > + {clearIcon} + </span> + ) + }, + + renderArrow () { + const { prefixCls, showArrow } = this.$props + if (!showArrow) { + return null + } + const inputIcon = getComponentFromProp(this, 'inputIcon') + return ( + <span + key='arrow' + class={`${prefixCls}-arrow`} + style={{ outline: 'none' }} + > + {inputIcon} + </span> + ) + }, + }, + + render () { + const { + prefixCls, className, style, + open, focused, disabled, allowClear, + ariaId, + renderSelection, renderPlaceholder, + tabIndex, + } = this.$props + const { vcTreeSelect: { onSelectorKeyDown }, $listeners } = this + + let myTabIndex = tabIndex + if (disabled) { + myTabIndex = null + } + + return ( + <span + style={style} + onClick={$listeners.click || noop} + class={classNames( + className, + prefixCls, + { + [`${prefixCls}-open`]: open, + [`${prefixCls}-focused`]: open || focused, + [`${prefixCls}-disabled`]: disabled, + [`${prefixCls}-enabled`]: !disabled, + [`${prefixCls}-allow-clear`]: allowClear, + } + )} + {...{ + directives: [{ + name: 'ant-ref', + value: this.domRef, + }], + }} + role='combobox' + aria-expanded={open} + aria-owns={open ? ariaId : undefined} + aria-controls={open ? ariaId : undefined} + aria-haspopup='listbox' + aria-disabled={disabled} + tabIndex={myTabIndex} + onFocus={this.onFocus} + onBlur={this.onBlur} + onKeydown={onSelectorKeyDown} + > + <span + key='selection' + class={classNames( + `${prefixCls}-selection`, + `${prefixCls}-selection--${modeName}` + )} + > + {renderSelection()} + {this.renderClear()} + {this.renderArrow()} + + {renderPlaceholder && renderPlaceholder()} + </span> + </span> + ) + }, + } + + return BaseSelector +} diff --git a/components/vc-tree-select/src/Popup/MultiplePopup.jsx b/components/vc-tree-select/src/Popup/MultiplePopup.jsx new file mode 100644 index 000000000..8f68c4e22 --- /dev/null +++ b/components/vc-tree-select/src/Popup/MultiplePopup.jsx @@ -0,0 +1,3 @@ +import BasePopup from '../Base/BasePopup' + +export default BasePopup diff --git a/components/vc-tree-select/src/Popup/SinglePopup.jsx b/components/vc-tree-select/src/Popup/SinglePopup.jsx new file mode 100644 index 000000000..e72c7b896 --- /dev/null +++ b/components/vc-tree-select/src/Popup/SinglePopup.jsx @@ -0,0 +1,80 @@ +import PropTypes from '../../../_util/vue-types' +import BasePopup from '../Base/BasePopup' +import SearchInput from '../SearchInput' +import { createRef } from '../util' + +const SinglePopup = { + name: 'SinglePopup', + props: { + ...BasePopup.props, + ...SearchInput.props, + searchValue: PropTypes.string, + showSearch: PropTypes.bool, + dropdownPrefixCls: PropTypes.string, + disabled: PropTypes.bool, + searchPlaceholder: PropTypes.string, + }, + created () { + this.inputRef = createRef() + }, + methods: { + onPlaceholderClick () { + this.inputRef.current.focus() + }, + + _renderPlaceholder () { + const { searchPlaceholder, searchValue, prefixCls } = this.$props + + if (!searchPlaceholder) { + return null + } + + return ( + <span + style={{ + display: searchValue ? 'none' : 'block', + }} + onClick={this.onPlaceholderClick} + class={`${prefixCls}-search__field__placeholder`} + > + {searchPlaceholder} + </span> + ) + }, + + _renderSearch () { + const { showSearch, dropdownPrefixCls } = this.$props + + if (!showSearch) { + return null + } + + return ( + <span class={`${dropdownPrefixCls}-search`}> + <SearchInput + {...{ + props: { ...this.$props, renderPlaceholder: this._renderPlaceholder }, + on: this.$listeners, + directives: [{ + name: 'ant-ref', + value: this.inputRef, + }], + }} + /> + </span> + ) + }, + }, + render () { + return ( + <BasePopup + {...{ + props: { ...this.$props, renderSearch: this._renderSearch, __propsSymbol__: Symbol() }, + on: this.$listeners, + }} + /> + ) + }, +} + +export default SinglePopup diff --git a/components/vc-tree-select/src/PropTypes.js b/components/vc-tree-select/src/PropTypes.js index e6495bb03..2afe88e41 100644 --- a/components/vc-tree-select/src/PropTypes.js +++ b/components/vc-tree-select/src/PropTypes.js @@ -1,62 +1,46 @@ import PropTypes from '../../_util/vue-types' -import { SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './strategies' +import { isLabelInValue } from './util' -export const SelectPropTypes = { - // className: PropTypes.string, - prefixCls: PropTypes.string, - multiple: PropTypes.bool, - filterTreeNode: PropTypes.any, - showSearch: PropTypes.bool, - disabled: PropTypes.bool, - showArrow: PropTypes.bool, - allowClear: PropTypes.bool, - defaultOpen: PropTypes.bool, - open: PropTypes.bool, - transitionName: PropTypes.string, - animation: PropTypes.string, - choiceTransitionName: PropTypes.string, - // onClick: PropTypes.func, - // onChange: PropTypes.func, - // onSelect: PropTypes.func, - // onDeselect: PropTypes.func, - // onSearch: PropTypes.func, - searchPlaceholder: PropTypes.string, - placeholder: PropTypes.any, - inputValue: PropTypes.any, - value: PropTypes.any, - defaultValue: PropTypes.any, - label: PropTypes.any, // vnode - defaultLabel: PropTypes.any, - labelInValue: PropTypes.bool, - dropdownClassName: PropTypes.string, - dropdownStyle: PropTypes.object, - dropdownPopupAlign: PropTypes.object, - dropdownVisibleChange: PropTypes.func, - maxTagTextLength: PropTypes.number, - showCheckedStrategy: PropTypes.oneOf([ - SHOW_ALL, SHOW_PARENT, SHOW_CHILD, - ]), - treeCheckStrictly: PropTypes.bool, - treeIcon: PropTypes.bool, - treeLine: PropTypes.bool, - treeDefaultExpandAll: PropTypes.bool, - treeDefaultExpandedKeys: PropTypes.arrayOf(String), - treeCheckable: PropTypes.any, // bool vnode - treeNodeLabelProp: PropTypes.string, - treeNodeFilterProp: PropTypes.string, - treeData: PropTypes.array, - treeDataSimpleMode: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.object, - ]), - loadData: PropTypes.func, - dropdownMatchSelectWidth: PropTypes.bool, - notFoundContent: PropTypes.any, - children: PropTypes.any, - autoFocus: PropTypes.bool, - getPopupContainer: PropTypes.func, - switcherIcon: PropTypes.func, - inputIcon: PropTypes.any, - removeIcon: PropTypes.any, - clearIcon: PropTypes.any, +const internalValProp = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, +]) + +export function genArrProps (propType) { + return PropTypes.oneOfType([ + propType, + PropTypes.arrayOf(propType), + ]) +} + +/** + * Origin code check `multiple` is true when `treeCheckStrictly` & `labelInValue`. + * But in process logic is already cover to array. + * Check array is not necessary. Let's simplify this check logic. + */ +export function valueProp (...args) { + const [props, propName, Component] = args + + if (isLabelInValue(props)) { + const err = genArrProps(PropTypes.shape({ + label: PropTypes.node, + value: internalValProp, + }).loose)(...args) + if (err) { + return new Error( + `Invalid prop \`${propName}\` supplied to \`${Component}\`. ` + + `You should use { label: string, value: string | number } or [{ label: string, value: string | number }] instead.` + ) + } + return null + } + + const err = genArrProps(internalValProp)(...args) + if (err) { + return new Error( + `Invalid prop \`${propName}\` supplied to \`${Component}\`. ` + + `You should use string or [string] instead.` + ) + } + return null } diff --git a/components/vc-tree-select/src/SearchInput.jsx b/components/vc-tree-select/src/SearchInput.jsx new file mode 100644 index 000000000..c682e5b8e --- /dev/null +++ b/components/vc-tree-select/src/SearchInput.jsx @@ -0,0 +1,127 @@ +/** + * Since search box is in different position with different mode. + * - Single: in the popup box + * - multiple: in the selector + * Move the code as a SearchInput for easy management. + */ + +import PropTypes from '../../_util/vue-types' +import { createRef } from './util' + +const SearchInput = { + name: 'SearchInput', + props: { + open: PropTypes.bool, + searchValue: PropTypes.string, + prefixCls: PropTypes.string, + disabled: PropTypes.bool, + renderPlaceholder: PropTypes.func, + needAlign: PropTypes.bool, + ariaId: PropTypes.string, + }, + inject: { + vcTreeSelect: { default: {}}, + }, + + created () { + this.inputRef = createRef() + this.mirrorInputRef = createRef() + this.prevProps = { ...this.$props } + }, + mounted () { + this.$nextTick(() => { + const { open, needAlign } = this.$props + if (needAlign) { + this.alignInputWidth() + } + + if (open) { + this.focus(true) + } + }) + }, + + updated () { + const { open, searchValue, needAlign } = this.$props + const { prevProps } = this + this.$nextTick(() => { + if (open && prevProps.open !== open) { + this.focus() + } + if (needAlign && searchValue !== prevProps.searchValue) { + this.alignInputWidth() + } + this.prevProps = { ...this.$props } + }) + }, + methods: { + /** + * `scrollWidth` is not correct in IE, do the workaround. + * ref: https://github.com/react-component/tree-select/issues/65 + * clientWidth 0 when mounted in vue. why? + */ + alignInputWidth () { + this.inputRef.current.style.width = + `${this.mirrorInputRef.current.clientWidth || this.mirrorInputRef.current.offsetWidth}px` + }, + + /** + * Need additional timeout for focus cause parent dom is not ready when didMount trigger + */ + focus (isDidMount) { + if (this.inputRef.current) { + this.inputRef.current.focus() + if (isDidMount) { + setTimeout(() => { + this.inputRef.current.focus() + }, 0) + } + } + }, + + blur () { + if (this.inputRef.current) { + this.inputRef.current.blur() + } + }, + }, + + render () { + const { searchValue, prefixCls, disabled, renderPlaceholder, open, ariaId } = this.$props + const { vcTreeSelect: { + onSearchInputChange, onSearchInputKeyDown, + }} = this + return ( + <span class={`${prefixCls}-search__field__wrap`}> + <input + type='text' + {...{ directives: [{ + name: 'ant-ref', + value: this.inputRef, + }] }} + onInput={onSearchInputChange} + onKeydown={onSearchInputKeyDown} + value={searchValue} + disabled={disabled} + class={`${prefixCls}-search__field`} + aria-label='filter select' + aria-autocomplete='list' + aria-controls={open ? ariaId : undefined} + aria-multiline='false' + /> + <span + {...{ directives: [{ + name: 'ant-ref', + value: this.mirrorInputRef, + }] }} + class={`${prefixCls}-search__field__mirror`} + > + {searchValue} + </span> + {renderPlaceholder ? renderPlaceholder() : null} + </span> + ) + }, +} + +export default SearchInput diff --git a/components/vc-tree-select/src/Select.jsx b/components/vc-tree-select/src/Select.jsx index b450941de..935d1d9de 100644 --- a/components/vc-tree-select/src/Select.jsx +++ b/components/vc-tree-select/src/Select.jsx @@ -1,398 +1,800 @@ +/** + * ARIA: https://www.w3.org/TR/wai-aria/#combobox + * Sample 1: https://www.w3.org/TR/2017/NOTE-wai-aria-practices-1.1-20171214/examples/combobox/aria1.1pattern/listbox-combo.html + * Sample 2: https://www.w3.org/blog/wai-components-gallery/widget/combobox-with-aria-autocompleteinline/ + * + * Tab logic: + * Popup is close + * 1. Focus input (mark component as focused) + * 2. Press enter to show the popup + * 3. If popup has input, focus it + * + * Popup is open + * 1. press tab to close the popup + * 2. Focus back to the selection input box + * 3. Let the native tab going on + * + * TreeSelect use 2 design type. + * In single mode, we should focus on the `span` + * In multiple mode, we should focus on the `input` + */ + +import shallowEqual from 'shallowequal' +import raf from 'raf' +import warning from 'warning' import PropTypes from '../../_util/vue-types' import KeyCode from '../../_util/KeyCode' -import classnames from 'classnames' -import pick from 'lodash/pick' -import omit from 'omit.js' -import { - getPropValue, getValuePropValue, - isMultiple, toArray, - UNSELECTABLE_ATTRIBUTE, UNSELECTABLE_STYLE, - preventDefaultEvent, - getTreeNodesStates, flatToHierarchy, filterParentPosition, - isPositionPrefix, labelCompatible, loopAllChildren, filterAllCheckedData, - processSimpleTreeData, toTitle, -} from './util' + import SelectTrigger from './SelectTrigger' -import _TreeNode from './TreeNode' +import SingleSelector from './Selector/SingleSelector' +import MultipleSelector from './Selector/MultipleSelector' +import SinglePopup from './Popup/SinglePopup' +import MultiplePopup from './Popup/MultiplePopup' + import { SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './strategies' -import { SelectPropTypes } from './PropTypes' -import { initDefaultProps, getOptionProps, hasProp, getAllProps, getComponentFromProp } from '../../_util/props-util' import BaseMixin from '../../_util/BaseMixin' -import getTransitionProps from '../../_util/getTransitionProps' - -function filterFn (input, child) { - return String(getPropValue(child, labelCompatible(this.$props.treeNodeFilterProp))) - .indexOf(input) > -1 -} - -const defaultProps = { - prefixCls: 'rc-tree-select', - // filterTreeNode: filterFn, // [Legacy] TODO: Set false and filter not hide? - showSearch: true, - allowClear: false, - // placeholder: '', - // searchPlaceholder: '', - labelInValue: false, - // onClick: noop, - // onChange: noop, - // onSelect: noop, - // onDeselect: noop, - // onSearch: noop, - showArrow: true, - dropdownMatchSelectWidth: true, - dropdownStyle: {}, - dropdownVisibleChange: () => { return true }, - notFoundContent: 'Not Found', - showCheckedStrategy: SHOW_CHILD, - // skipHandleInitValue: false, // Deprecated (use treeCheckStrictly) - treeCheckStrictly: false, - treeIcon: false, - treeLine: false, - treeDataSimpleMode: false, - treeDefaultExpandAll: false, - treeCheckable: false, - treeNodeFilterProp: 'value', - treeNodeLabelProp: 'title', -} - -const Select = { - mixins: [BaseMixin], - name: 'VCTreeSelect', - props: initDefaultProps({ ...SelectPropTypes, __propsSymbol__: PropTypes.any }, defaultProps), - data () { - let value = [] - const props = getOptionProps(this) - this.preProps = { ...props } - if ('value' in props) { - value = toArray(props.value) - } else { - value = toArray(props.defaultValue) +import { + createRef, generateAriaId, + formatInternalValue, formatSelectorValue, + parseSimpleTreeData, + convertDataToTree, convertTreeToEntities, conductCheck, + getHalfCheckedKeys, + flatToHierarchy, + isPosRelated, isLabelInValue, getFilterTree, + cleanEntity, +} from './util' +import SelectNode from './SelectNode' +import { initDefaultProps, getOptionProps, mergeProps, getPropsData } from '../../_util/props-util' +function getWatch (keys = []) { + const watch = {} + keys.forEach(k => { + watch[k] = function () { + this.needSyncKeys[k] = true } - // save parsed treeData, for performance (treeData may be very big) - this.renderedTreeData = this.renderTreeData() - value = this.addLabelToValue(props, value) - value = this.getValue(props, value, props.inputValue ? '__strict' : true) - const inputValue = props.inputValue || '' - // if (props.combobox) { - // inputValue = value.length ? String(value[0].value) : ''; - // } + }) + return watch +} +const Select = { + name: 'Select', + mixins: [BaseMixin], + props: initDefaultProps({ + prefixCls: PropTypes.string, + prefixAria: PropTypes.string, + multiple: PropTypes.bool, + showArrow: PropTypes.bool, + open: PropTypes.bool, + value: PropTypes.any, + + autoFocus: PropTypes.bool, + + defaultOpen: PropTypes.bool, + defaultValue: PropTypes.any, + + showSearch: PropTypes.bool, + placeholder: PropTypes.any, + inputValue: PropTypes.string, // [Legacy] Deprecated. Use `searchValue` instead. + searchValue: PropTypes.string, + autoClearSearchValue: PropTypes.bool, + searchPlaceholder: PropTypes.any, // [Legacy] Confuse with placeholder + disabled: PropTypes.bool, + children: PropTypes.any, + labelInValue: PropTypes.bool, + maxTagCount: PropTypes.number, + maxTagPlaceholder: PropTypes.any, + maxTagTextLength: PropTypes.number, + showCheckedStrategy: PropTypes.oneOf([ + SHOW_ALL, SHOW_PARENT, SHOW_CHILD, + ]), + dropdownClassName: PropTypes.string, + dropdownStyle: PropTypes.object, + dropdownVisibleChange: PropTypes.func, + dropdownMatchSelectWidth: PropTypes.bool, + treeData: PropTypes.array, + treeDataSimpleMode: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), + treeNodeFilterProp: PropTypes.string, + treeNodeLabelProp: PropTypes.string, + treeCheckable: PropTypes.any, + treeCheckStrictly: PropTypes.bool, + treeIcon: PropTypes.bool, + treeLine: PropTypes.bool, + treeDefaultExpandAll: PropTypes.bool, + treeDefaultExpandedKeys: PropTypes.array, + treeExpandedKeys: PropTypes.array, + loadData: PropTypes.func, + filterTreeNode: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]), + + notFoundContent: PropTypes.any, + getPopupContainer: PropTypes.func, + + // onSearch: PropTypes.func, + // onSelect: PropTypes.func, + // onDeselect: PropTypes.func, + // onChange: PropTypes.func, + // onDropdownVisibleChange: PropTypes.func, + + // onTreeExpand: PropTypes.func, + allowClear: PropTypes.bool, + transitionName: PropTypes.string, + animation: PropTypes.string, + choiceTransitionName: PropTypes.string, + inputIcon: PropTypes.any, + clearIcon: PropTypes.any, + removeIcon: PropTypes.any, + switcherIcon: PropTypes.any, + __propsSymbol__: PropTypes.any, + }, { + prefixCls: 'rc-tree-select', + prefixAria: 'rc-tree-select', + showArrow: true, + showSearch: true, + autoClearSearchValue: true, + showCheckedStrategy: SHOW_CHILD, + + // dropdownMatchSelectWidth change the origin design, set to false now + // ref: https://github.com/react-component/select/blob/4cad95e098a341a09de239ad6981067188842020/src/Select.jsx#L344 + // ref: https://github.com/react-component/select/pull/71 + treeNodeFilterProp: 'value', + treeNodeLabelProp: 'title', + treeIcon: false, + notFoundContent: 'Not Found', + dropdownStyle: {}, + dropdownVisibleChange: () => { return true }, + }), + + data () { + warning(this.$props.__propsSymbol__, 'must pass __propsSymbol__') + const { + prefixAria, + defaultOpen, open, + } = this.$props + this.needSyncKeys = {} + this.selectorRef = createRef() + this.selectTriggerRef = createRef() + + // ARIA need `aria-controls` props mapping + // Since this need user input. Let's generate ourselves + this.ariaId = generateAriaId(`${prefixAria}-list`) + + const state = { + _open: open || defaultOpen, + _valueList: [], + _searchHalfCheckedKeys: [], + _missValueList: [], // Contains the value not in the tree + _selectorValueList: [], // Used for multiple selector + _valueEntities: {}, + _keyEntities: {}, + _searchValue: '', + _prevProps: {}, + _init: true, + _focused: undefined, + _treeNodes: undefined, + _filteredTreeNodes: undefined, + } + const newState = this.getDerivedStateFromProps(this.$props, state) return { - sValue: value, - sInputValue: inputValue, - sOpen: props.open || props.defaultOpen, - sFocused: false, + ...state, + ...newState, } }, + provide () { + return { + vcTreeSelect: { + onSelectorFocus: this.onSelectorFocus, + onSelectorBlur: this.onSelectorBlur, + onSelectorKeyDown: this.onComponentKeyDown, + onSelectorClear: this.onSelectorClear, + onMultipleSelectorRemove: this.onMultipleSelectorRemove, + + onTreeNodeSelect: this.onTreeNodeSelect, + onTreeNodeCheck: this.onTreeNodeCheck, + onPopupKeyDown: this.onComponentKeyDown, + + onSearchInputChange: this.onSearchInputChange, + onSearchInputKeyDown: this.onSearchInputKeyDown, + }, + } + }, + watch: { + ...getWatch(['treeData', 'defaultValue', 'value']), + __propsSymbol__ () { + const state = this.getDerivedStateFromProps(this.$props, this.$data) + this.setState(state) + this.needSyncKeys = {} + }, + '$data._valueList': function () { + this.$nextTick(() => { + this.forcePopupAlign() + }) + }, + }, mounted () { this.$nextTick(() => { - const { autoFocus, disabled } = this - if (isMultiple(this.$props)) { - const inputNode = this.getInputDOMNode() - if (inputNode.value) { - inputNode.style.width = '' - inputNode.style.width = `${this.$refs.inputMirrorInstance.clientWidth || this.$refs.inputMirrorInstance.offsetWidth}px` - } else { - inputNode.style.width = '' - } - } + const { autoFocus, disabled } = this.$props if (autoFocus && !disabled) { this.focus() } }) }, - watch: { - // for performance (use __propsSymbol__ avoid deep watch) - __propsSymbol__ () { - const nextProps = getOptionProps(this) - // save parsed treeData, for performance (treeData may be very big) - this.renderedTreeData = this.renderTreeData(nextProps) - // Detecting whether the object of `onChange`'s argument is old ref. - // Better to do a deep equal later. - this._cacheTreeNodesStates = this._cacheTreeNodesStates !== 'no' && - this._savedValue && - nextProps.value === this._savedValue - if (this.preProps.treeData !== nextProps.treeData || - this.preProps.children !== nextProps.children) { - // refresh this._treeNodesStates cache - this._treeNodesStates = getTreeNodesStates( - this.renderedTreeData || nextProps.children, - this.sValue.map(item => item.value) + + methods: { + getDerivedStateFromProps (nextProps, prevState) { + const h = this.$createElement + const { _prevProps: prevProps = {}} = prevState + const { + treeCheckable, treeCheckStrictly, + filterTreeNode, treeNodeFilterProp, + treeDataSimpleMode, + } = nextProps + const newState = { + _prevProps: { ...nextProps }, + _init: false, + } + const self = this + // Process the state when props updated + function processState (propName, updater) { + if (prevProps[propName] !== nextProps[propName] || self.needSyncKeys[propName]) { + updater(nextProps[propName], prevProps[propName]) + return true + } + return false + } + + let valueRefresh = false + + // Open + processState('open', (propValue) => { + newState._open = propValue + }) + + // Tree Nodes + let treeNodes + let treeDataChanged = false + let treeDataModeChanged = false + processState('treeData', (propValue) => { + treeNodes = convertDataToTree(h, propValue) + treeDataChanged = true + }) + + processState('treeDataSimpleMode', (propValue, prevValue) => { + if (!propValue) return + + const prev = !prevValue || prevValue === true ? {} : prevValue + + // Shallow equal to avoid dynamic prop object + if (!shallowEqual(propValue, prev)) { + treeDataModeChanged = true + } + }) + + // Parse by `treeDataSimpleMode` + if (treeDataSimpleMode && (treeDataChanged || treeDataModeChanged)) { + const simpleMapper = { + id: 'id', + pId: 'pId', + rootPId: null, + ...(treeDataSimpleMode !== true ? treeDataSimpleMode : {}), + } + treeNodes = convertDataToTree( + h, + parseSimpleTreeData(nextProps.treeData, simpleMapper) ) } - if ('value' in nextProps) { - let value = toArray(nextProps.value) - value = this.addLabelToValue(nextProps, value) - value = this.getValue(nextProps, value) - this.setState({ - sValue: value, - }, this.forcePopupAlign) - // if (nextProps.combobox) { - // this.setState({ - // inputValue: value.length ? String(value[0].key) : '', - // }); - // } + + // If `treeData` not provide, use children TreeNodes + if (!nextProps.treeData) { + // processState('children', (propValue) => { + // treeNodes = Array.isArray(propValue) ? propValue : [propValue] + // }) + treeNodes = this.$slots.default } - if (nextProps.inputValue !== this.preProps.inputValue) { - this.setState({ - sInputValue: nextProps.inputValue, + + // Convert `treeData` to entities + if (treeNodes) { + const entitiesMap = convertTreeToEntities(treeNodes) + newState._treeNodes = treeNodes + newState._posEntities = entitiesMap.posEntities + newState._valueEntities = entitiesMap.valueEntities + newState._keyEntities = entitiesMap.keyEntities + + valueRefresh = true + } + + // Value List + if (prevState._init) { + processState('defaultValue', (propValue) => { + newState._valueList = formatInternalValue(propValue, nextProps) + valueRefresh = true }) } - if ('open' in nextProps) { - this.setState({ - sOpen: nextProps.open, - }) - } - this.preProps = { ...nextProps } - }, - }, - beforeUpdate () { - if (this._savedValue && this.$props.value && - this.$props.value !== this._savedValue && - this.$props.value === this.preProps.value) { - this._cacheTreeNodesStates = false - this.getValue(this.$props, this.addLabelToValue(this.$props, toArray(this.$props.value))) - } - }, - - updated () { - const state = this.$data - const props = this.$props - if (state.sOpen && isMultiple(props)) { - this.$nextTick(() => { - const inputNode = this.getInputDOMNode() - if (inputNode.value) { - inputNode.style.width = '' - inputNode.style.width = `${this.$refs.inputMirrorInstance.clientWidth}px` - } else { - inputNode.style.width = '' - } + processState('value', (propValue) => { + newState._valueList = formatInternalValue(propValue, nextProps) + valueRefresh = true }) - } - }, - beforeDestroy () { - this.clearDelayTimer() - if (this.dropdownContainer) { - document.body.removeChild(this.dropdownContainer) - this.dropdownContainer = null - } - }, - methods: { - loopTreeData (data, level = 0, treeCheckable) { - return data.map((item, index) => { - const pos = `${level}-${index}` - const { - label, - value, - disabled, - key, - selectable, - children, - isLeaf, - ...otherProps - } = item - const tnProps = { - ...pick(item, ['on', 'class', 'style']), - props: { - value, - title: label, - disabled: disabled || false, - selectable: selectable === false ? selectable : !treeCheckable, - ...omit(otherProps, ['on', 'class', 'style']), - }, - key: key || value || pos, - } - let ret - if (children && children.length) { - ret = (<_TreeNode {...tnProps}>{this.loopTreeData(children, pos, treeCheckable)}</_TreeNode>) - } else { - ret = (<_TreeNode {...tnProps} isLeaf={isLeaf}/>) - } - return ret - }) - }, - onInputChange (event) { - const val = event.target.value - const { $props: props } = this - this.setState({ - sInputValue: val, - sOpen: true, - }, this.forcePopupAlign) - if (props.treeCheckable && !val) { - this.setState({ - sValue: this.getValue(props, [...this.sValue], false), - }) - } - this.__emit('search', val) - }, + // Selector Value List + if (valueRefresh) { + // Find out that value not exist in the tree + const missValueList = [] + const filteredValueList = [] + const keyList = [] - onDropdownVisibleChange (open) { - // selection inside combobox cause click - if (!open && document.activeElement === this.getInputDOMNode()) { - // return; - } - this.setOpenState(open, undefined, !open) - }, - - // combobox ignore - onKeyDown (event) { - const props = this.$props - if (props.disabled) { - return - } - const keyCode = event.keyCode - if (this.sOpen && !this.getInputDOMNode()) { - this.onInputKeyDown(event) - } else if (keyCode === KeyCode.ENTER || keyCode === KeyCode.DOWN) { - this.setOpenState(true) - event.preventDefault() - } - }, - - onInputKeyDown (event) { - const props = this.$props - if (props.disabled) { - return - } - const state = this.$data - const keyCode = event.keyCode - if (isMultiple(props) && !event.target.value && keyCode === KeyCode.BACKSPACE) { - const value = state.sValue.concat() - if (value.length) { - const popValue = value.pop() - this.removeSelected(this.isLabelInValue() ? popValue : popValue.value) + // Get latest value list + let latestValueList = newState._valueList + if (!latestValueList) { + // Also need add prev missValueList to avoid new treeNodes contains the value + latestValueList = [...prevState._valueList, ...prevState._missValueList] } - return - } - if (keyCode === KeyCode.DOWN) { - if (!state.sOpen) { - this.openIfHasChildren() - event.preventDefault() - event.stopPropagation() - return - } - } else if (keyCode === KeyCode.ESC) { - if (state.sOpen) { - this.setOpenState(false) - event.preventDefault() - event.stopPropagation() - } - return - } - }, - onSelect (selectedKeys, info) { - const item = info.node - let value = this.sValue - const props = this.$props - const selectedValue = getValuePropValue(item) - const selectedLabel = this.getLabelFromNode(item) - const checkableSelect = props.treeCheckable && info.event === 'select' - let event = selectedValue - if (this.isLabelInValue()) { - event = { - value: event, - label: selectedLabel, - } - } - if (info.selected === false) { - this.onDeselect(info) - if (!checkableSelect) return - } - this.__emit('select', event, item, info) + // Get key by value + latestValueList + .forEach((wrapperValue) => { + const { value } = wrapperValue + const entity = (newState._valueEntities || prevState._valueEntities)[value] - const checkEvt = info.event === 'check' - if (isMultiple(props)) { - this.$nextTick(() => { // clearSearchInput will change sInputValue - this.clearSearchInput() - }) - if (checkEvt) { - value = this.getCheckedNodes(info, props).map(n => { - return { - value: getValuePropValue(n), - label: this.getLabelFromNode(n), + if (entity) { + keyList.push(entity.key) + filteredValueList.push(wrapperValue) + return } + + // If not match, it may caused by ajax load. We need keep this + missValueList.push(wrapperValue) + }) + + // We need calculate the value when tree is checked tree + if (treeCheckable && !treeCheckStrictly) { + // Calculate the keys need to be checked + const { checkedKeys } = conductCheck( + keyList, + true, + newState._keyEntities || prevState._keyEntities, + ) + + // Format value list again for internal usage + newState._valueList = checkedKeys.map(key => ({ + value: (newState._keyEntities || prevState._keyEntities)[key].value, + })) + } else { + newState._valueList = filteredValueList + } + + // Fill the missValueList, we still need display in the selector + newState._missValueList = missValueList + + // Calculate the value list for `Selector` usage + newState._selectorValueList = formatSelectorValue( + newState._valueList, + nextProps, + newState._valueEntities || prevState._valueEntities, + ) + } + + // [Legacy] To align with `Select` component, + // We use `searchValue` instead of `inputValue` but still keep the api + // `inputValue` support `null` to work as `autoClearSearchValue` + processState('inputValue', (propValue) => { + if (propValue !== null) { + newState._searchValue = propValue + } + }) + + // Search value + processState('searchValue', (propValue) => { + newState._searchValue = propValue + }) + + // Do the search logic + if ( + newState._searchValue !== undefined || + (prevState._searchValue && treeNodes) + ) { + const searchValue = newState._searchValue !== undefined ? newState._searchValue : prevState._searchValue + const upperSearchValue = String(searchValue).toUpperCase() + + let filterTreeNodeFn = filterTreeNode + if (filterTreeNode === false) { + // Don't filter if is false + filterTreeNodeFn = () => true + } else if (typeof filterTreeNodeFn !== 'function') { + // When is not function (true or undefined), use inner filter + filterTreeNodeFn = (_, node) => { + const nodeValue = String(getPropsData(node)[treeNodeFilterProp]).toUpperCase() + return nodeValue.indexOf(upperSearchValue) !== -1 + } + } + + newState._filteredTreeNodes = getFilterTree( + this.$createElement, + newState._treeNodes || prevState._treeNodes, + searchValue, + filterTreeNodeFn, + newState._valueEntities || prevState._valueEntities, + ) + } + + // We should re-calculate the halfCheckedKeys when in search mode + if ( + valueRefresh && treeCheckable && !treeCheckStrictly && + (newState._searchValue || prevState._searchValue) + ) { + newState._searchHalfCheckedKeys = getHalfCheckedKeys( + newState._valueList, + newState._valueEntities || prevState._valueEntities, + ) + } + + // Checked Strategy + processState('showCheckedStrategy', () => { + newState._selectorValueList = newState._selectorValueList || formatSelectorValue( + newState._valueList || prevState._valueList, + nextProps, + newState._valueEntities || prevState._valueEntities, + ) + }) + + return newState + }, + // ==================== Selector ==================== + onSelectorFocus () { + this.setState({ _focused: true }) + }, + + onSelectorBlur () { + this.setState({ _focused: false }) + + // TODO: Close when Popup is also not focused + // this.setState({ open: false }); + }, + + // Handle key board event in both Selector and Popup + onComponentKeyDown (event) { + const { _open: open } = this.$data + const { keyCode } = event + + if (!open) { + if ([KeyCode.ENTER, KeyCode.DOWN].indexOf(keyCode) !== -1) { + this.setOpenState(true) + } + } else if (KeyCode.ESC === keyCode) { + this.setOpenState(false) + } else if ([KeyCode.UP, KeyCode.DOWN, KeyCode.LEFT, KeyCode.RIGHT].indexOf(keyCode) !== -1) { + // TODO: Handle `open` state + event.stopPropagation() + } + }, + + onDeselect (wrappedValue, node, nodeEventInfo) { + this.__emit('deselect', wrappedValue, node, nodeEventInfo) + }, + + onSelectorClear (event) { + const { disabled } = this.$props + if (disabled) return + + this.triggerChange([], []) + + if (!this.isSearchValueControlled()) { + this.setUncontrolledState({ + _searchValue: '', + _filteredTreeNodes: null, + }) + } + + event.stopPropagation() + }, + + onMultipleSelectorRemove (event, removeValue) { + event.stopPropagation() + + const { _valueList: valueList, _missValueList: missValueList, _valueEntities: valueEntities } = this.$data + + const { treeCheckable, treeCheckStrictly, treeNodeLabelProp, disabled } = this.$props + if (disabled) return + + // Find trigger entity + const triggerEntity = valueEntities[removeValue] + + // Clean up value + let newValueList = valueList + if (triggerEntity) { + // If value is in tree + if (treeCheckable && !treeCheckStrictly) { + newValueList = valueList.filter(({ value }) => { + const entity = valueEntities[value] + return !isPosRelated(entity.pos, triggerEntity.pos) }) } else { - if (value.some(i => i.value === selectedValue)) { - return - } - value = value.concat([{ - value: selectedValue, - label: selectedLabel, - }]) + newValueList = valueList.filter(({ value }) => value !== removeValue) } - } else { - if (value.length && value[0].value === selectedValue) { - this.setOpenState(false) - return - } - value = [{ - value: selectedValue, - label: selectedLabel, - }] - this.setOpenState(false) } + const triggerNode = triggerEntity ? triggerEntity.node : null + const extraInfo = { - triggerValue: selectedValue, - triggerNode: item, + triggerValue: removeValue, + triggerNode, } - if (checkEvt) { - extraInfo.checked = info.checked - // if inputValue existing, tree is checkStrictly - extraInfo.allCheckedNodes = props.treeCheckStrictly || this.sInputValue - ? info.checkedNodes : flatToHierarchy(info.checkedNodesPositions) - this._checkedNodes = info.checkedNodesPositions - const _tree = this.getPopupComponentRefs() - this._treeNodesStates = _tree.checkKeys - } else { - extraInfo.selected = info.selected + const deselectInfo = { + node: triggerNode, } - this.fireChange(value, extraInfo) - if (props.inputValue === null) { + // [Legacy] Little hack on this to make same action as `onCheck` event. + if (treeCheckable) { + const filteredEntityList = newValueList.map(({ value }) => valueEntities[value]) + + deselectInfo.event = 'check' + deselectInfo.checked = false + deselectInfo.checkedNodes = filteredEntityList.map(({ node }) => node) + deselectInfo.checkedNodesPositions = filteredEntityList + .map(({ node, pos }) => ({ node, pos })) + + if (treeCheckStrictly) { + extraInfo.allCheckedNodes = deselectInfo.checkedNodes + } else { + // TODO: It's too expansive to get `halfCheckedKeys` in onDeselect. Not pass this. + extraInfo.allCheckedNodes = flatToHierarchy(filteredEntityList) + .map(({ node }) => node) + } + } else { + deselectInfo.event = 'select' + deselectInfo.selected = false + deselectInfo.selectedNodes = newValueList.map(({ value }) => (valueEntities[value] || {}).node) + } + + // Some value user pass prop is not in the tree, we also need clean it + const newMissValueList = missValueList.filter(({ value }) => value !== removeValue) + let wrappedValue + if (this.isLabelInValue()) { + wrappedValue = { + label: triggerNode ? getPropsData(triggerNode)[treeNodeLabelProp] : null, + value: removeValue, + } + } else { + wrappedValue = removeValue + } + + this.onDeselect(wrappedValue, triggerNode, deselectInfo) + + this.triggerChange(newMissValueList, newValueList, extraInfo) + }, + + // ===================== Popup ====================== + onValueTrigger (isAdd, nodeList, nodeEventInfo, nodeExtraInfo) { + const { node } = nodeEventInfo + const { value } = node.$props + const { _missValueList: missValueList, _valueEntities: valueEntities, _keyEntities: keyEntities, _searchValue: searchValue } = this.$data + const { + disabled, inputValue, + treeNodeLabelProp, + treeCheckable, treeCheckStrictly, autoClearSearchValue, + } = this.$props + const label = node.$props[treeNodeLabelProp] + + if (disabled) return + + // Wrap the return value for user + let wrappedValue + if (this.isLabelInValue()) { + wrappedValue = { + value, + label, + } + } else { + wrappedValue = value + } + + // [Legacy] Origin code not trigger `onDeselect` every time. Let's align the behaviour. + if (isAdd) { + this.__emit('select', wrappedValue, node, nodeEventInfo) + } else { + this.__emit('deselect', wrappedValue, node, nodeEventInfo) + } + + // Get wrapped value list. + // This is a bit hack cause we use key to match the value. + let newValueList = nodeList.map(node => { + const props = getPropsData(node) + return { + value: props.value, + label: props[treeNodeLabelProp], + } + }) + + // When is `treeCheckable` and with `searchValue`, `valueList` is not full filled. + // We need calculate the missing nodes. + if (treeCheckable && !treeCheckStrictly) { + let keyList = newValueList.map(({ value: val }) => valueEntities[val].key) + if (isAdd) { + keyList = conductCheck( + keyList, + true, + keyEntities, + ).checkedKeys + } else { + keyList = conductCheck( + [valueEntities[value].key], + false, + keyEntities, + { checkedKeys: keyList }, + ).checkedKeys + } + newValueList = keyList.map(key => { + const props = getPropsData(keyEntities[key].node) + return { + value: props.value, + label: props[treeNodeLabelProp], + } + }) + } + + // Clean up `searchValue` when this prop is set + if (autoClearSearchValue || inputValue === null) { + // Clean state `searchValue` if uncontrolled + if (!this.isSearchValueControlled()) { + this.setUncontrolledState({ + _searchValue: '', + _filteredTreeNodes: null, + }) + } + + // Trigger onSearch if `searchValue` to be empty. + // We should also trigger onSearch with empty string here + // since if user use `treeExpandedKeys`, it need user have the ability to reset it. + if (searchValue && searchValue.length) { + this.__emit('search', '') + } + } + + // [Legacy] Provide extra info + const extraInfo = { + ...nodeExtraInfo, + triggerValue: value, + triggerNode: node, + } + + this.triggerChange(missValueList, newValueList, extraInfo) + }, + + onTreeNodeSelect (_, nodeEventInfo) { + const { _valueList: valueList, _valueEntities: valueEntities } = this.$data + const { treeCheckable, multiple } = this.$props + if (treeCheckable) return + + if (!multiple) { + this.setOpenState(false) + } + + const isAdd = nodeEventInfo.selected + const { $props: { value: selectedValue }} = nodeEventInfo.node + + let newValueList + + if (!multiple) { + newValueList = [{ value: selectedValue }] + } else { + newValueList = valueList.filter(({ value }) => value !== selectedValue) + if (isAdd) { + newValueList.push({ value: selectedValue }) + } + } + + const selectedNodes = newValueList + .map(({ value }) => valueEntities[value]) + .filter(entity => entity) + .map(({ node }) => node) + + this.onValueTrigger(isAdd, selectedNodes, nodeEventInfo, { selected: isAdd }) + }, + + onTreeNodeCheck (_, nodeEventInfo) { + const { _searchValue: searchValue, _keyEntities: keyEntities, _valueEntities: valueEntities, _valueList: valueList } = this.$data + const { treeCheckStrictly } = this.$props + + const { checkedNodes, checkedNodesPositions } = nodeEventInfo + const isAdd = nodeEventInfo.checked + + const extraInfo = { + checked: isAdd, + } + + let checkedNodeList = checkedNodes + + // [Legacy] Check event provide `allCheckedNodes`. + // When `treeCheckStrictly` or internal `searchValue` is set, TreeNode will be unrelated: + // - Related: Show the top checked nodes and has children prop. + // - Unrelated: Show all the checked nodes. + if (searchValue) { + const oriKeyList = valueList + .map(({ value }) => valueEntities[value]) + .filter(entity => entity) + .map(({ key }) => key) + + let keyList + if (isAdd) { + keyList = Array.from( + new Set([ + ...oriKeyList, + ...checkedNodeList.map(node => { + const { value } = getPropsData(node) + return valueEntities[value].key + }), + ]), + ) + } else { + keyList = conductCheck( + [getPropsData(nodeEventInfo.node).eventKey], + false, + keyEntities, + { checkedKeys: oriKeyList }, + ).checkedKeys + } + + checkedNodeList = keyList.map(key => keyEntities[key].node) + + // Let's follow as not `treeCheckStrictly` format + extraInfo.allCheckedNodes = keyList.map(key => cleanEntity(keyEntities[key])) + } else if (treeCheckStrictly) { + extraInfo.allCheckedNodes = nodeEventInfo.checkedNodes + } else { + extraInfo.allCheckedNodes = flatToHierarchy(checkedNodesPositions) + } + + this.onValueTrigger(isAdd, checkedNodeList, nodeEventInfo, extraInfo) + }, + + // ==================== Trigger ===================== + + onDropdownVisibleChange (open) { + this.setOpenState(open, true) + }, + + onSearchInputChange ({ target: { value }}) { + const { _treeNodes: treeNodes, _valueEntities: valueEntities } = this.$data + const { filterTreeNode, treeNodeFilterProp } = this.$props + this.__emit('search', value) + + let isSet = false + + if (!this.isSearchValueControlled()) { + isSet = this.setUncontrolledState({ + _searchValue: value, + }) + this.setOpenState(true) + } + + if (isSet) { + // Do the search logic + const upperSearchValue = String(value).toUpperCase() + + let filterTreeNodeFn = filterTreeNode + if (!filterTreeNodeFn) { + filterTreeNodeFn = (_, node) => { + const nodeValue = String(getPropsData(node)[treeNodeFilterProp]).toUpperCase() + return nodeValue.indexOf(upperSearchValue) !== -1 + } + } + this.setState({ - sInputValue: '', + _filteredTreeNodes: getFilterTree(this.$createElement, treeNodes, value, filterTreeNodeFn, valueEntities), }) } }, - onDeselect (info) { - this.removeSelected(getValuePropValue(info.node)) - if (!isMultiple(this.$props)) { - this.setOpenState(false) - } else { - this.clearSearchInput() - } - }, + onSearchInputKeyDown (event) { + const { _searchValue: searchValue, _valueList: valueList } = this.$data - onPlaceholderClick () { - this.getInputDOMNode().focus() - }, + const { keyCode } = event - onClearSelection (event) { - const props = this.$props - const state = this.$data - if (props.disabled) { - return - } - event.stopPropagation() - this._cacheTreeNodesStates = 'no' - this._checkedNodes = [] - if (state.sInputValue || state.sValue.length) { - this.setOpenState(false) - if (typeof props.inputValue === 'undefined') { - this.setState({ - sInputValue: '', - }, () => { - this.fireChange([]) - }) - } else { - this.fireChange([]) - } + if ( + KeyCode.BACKSPACE === keyCode && + this.isMultiple() && + !searchValue && + valueList.length + ) { + const lastValue = valueList[valueList.length - 1].value + this.onMultipleSelectorRemove(event, lastValue) } }, @@ -400,642 +802,250 @@ const Select = { this.forcePopupAlign() }, - getLabelFromNode (child) { - return getPropValue(child, this.$props.treeNodeLabelProp) - }, - - getLabelFromProps (props, value) { - if (value === undefined) { - return null - } - let label = null - loopAllChildren(this.renderedTreeData || props.children, item => { - if (getValuePropValue(item) === value) { - label = this.getLabelFromNode(item) - } - }) - if (label === null) { - return value - } - return label - }, - - getDropdownContainer () { - if (!this.dropdownContainer) { - this.dropdownContainer = document.createElement('div') - document.body.appendChild(this.dropdownContainer) - } - return this.dropdownContainer - }, - - getSearchPlaceholderElement (hidden) { - const props = this.$props - let placeholder - if (isMultiple(props)) { - placeholder = getComponentFromProp(this, 'placeholder') || getComponentFromProp(this, 'searchPlaceholder') - } else { - placeholder = getComponentFromProp(this, 'searchPlaceholder') - } - if (placeholder) { - return ( - <span - style={{ display: hidden ? 'none' : 'block' }} - onClick={this.onPlaceholderClick} - class={`${props.prefixCls}-search__field__placeholder`} - > - {placeholder} - </span> - ) - } - return null - }, - - getInputElement () { - const { sInputValue } = this.$data - const { prefixCls, disabled } = this.$props - const multiple = isMultiple(this.$props) - const inputListeners = { - input: this.onInputChange, - keydown: this.onInputKeyDown, - } - if (multiple) { - inputListeners.blur = this.onBlur - inputListeners.focus = this.onFocus - } - return ( - <span class={`${prefixCls}-search__field__wrap`}> - <input - ref='inputInstance' - {...{ on: inputListeners }} - value={sInputValue} - disabled={disabled} - class={`${prefixCls}-search__field`} - role='textbox' - /> - <span - ref='inputMirrorInstance' - class={`${prefixCls}-search__field__mirror`} - > - {sInputValue} - </span> - {isMultiple(this.$props) ? null : this.getSearchPlaceholderElement(!!sInputValue)} - </span> - ) - }, - - getInputDOMNode () { - return this.$refs.inputInstance - }, - - getPopupDOMNode () { - return this.$refs.trigger.getPopupDOMNode() - }, - - getPopupComponentRefs () { - return this.$refs.trigger.getPopupEleRefs() - }, - - getValue (_props, val, init = true) { - let value = val - // if inputValue existing, tree is checkStrictly - const _strict = init === '__strict' || - init && (this.sInputValue || - this.inputValue !== _props.inputValue) - if (_props.treeCheckable && - (_props.treeCheckStrictly || _strict)) { - this.halfCheckedValues = [] - value = [] - val.forEach(i => { - if (!i.halfChecked) { - value.push(i) - } else { - this.halfCheckedValues.push(i) - } - }) - } - // if (!(_props.treeCheckable && !_props.treeCheckStrictly)) { - if (!_props.treeCheckable || _props.treeCheckable && - (_props.treeCheckStrictly || _strict)) { - return value - } - let checkedTreeNodes - if (this._cachetreeData && this._cacheTreeNodesStates && this._checkedNodes && - !this.sInputValue) { - this.checkedTreeNodes = checkedTreeNodes = this._checkedNodes - } else { - /** - * Note: `this._treeNodesStates`'s treeNodesStates must correspond to nodes of the - * final tree (`processTreeNode` function from SelectTrigger.jsx produce the final tree). - * - * And, `this._treeNodesStates` from `onSelect` is previous value, - * so it perhaps only have a few nodes, but the newly filtered tree can have many nodes, - * thus, you cannot use previous _treeNodesStates. - */ - // getTreeNodesStates is not effective. - this._treeNodesStates = getTreeNodesStates( - this.renderedTreeData || _props.children, - value.map(item => item.value) - ) - this.checkedTreeNodes = checkedTreeNodes = this._treeNodesStates.checkedNodes - } - const mapLabVal = arr => arr.map(itemObj => { - return { - value: getValuePropValue(itemObj.node), - label: getPropValue(itemObj.node, _props.treeNodeLabelProp), - } - }) - const props = this.$props - let checkedValues = [] - if (props.showCheckedStrategy === SHOW_ALL) { - checkedValues = mapLabVal(checkedTreeNodes) - } else if (props.showCheckedStrategy === SHOW_PARENT) { - const posArr = filterParentPosition(checkedTreeNodes.map(itemObj => itemObj.pos)) - checkedValues = mapLabVal(checkedTreeNodes.filter( - itemObj => posArr.indexOf(itemObj.pos) !== -1 - )) - } else { - checkedValues = mapLabVal(checkedTreeNodes.filter(itemObj => { - return !itemObj.node.componentOptions.children - })) - } - return checkedValues - }, - - getCheckedNodes (info, props) { - // TODO treeCheckable does not support tags/dynamic - let { checkedNodes } = info - // if inputValue existing, tree is checkStrictly - if (props.treeCheckStrictly || this.sInputValue) { - return checkedNodes - } - const checkedNodesPositions = info.checkedNodesPositions - if (props.showCheckedStrategy === SHOW_ALL) { - // checkedNodes = checkedNodes - } else if (props.showCheckedStrategy === SHOW_PARENT) { - const posArr = filterParentPosition(checkedNodesPositions.map(itemObj => itemObj.pos)) - checkedNodes = checkedNodesPositions.filter(itemObj => posArr.indexOf(itemObj.pos) !== -1) - .map(itemObj => itemObj.node) - } else { - checkedNodes = checkedNodes.filter(n => { - return !n.componentOptions.children - }) - } - return checkedNodes - }, - - getDeselectedValue (selectedValue) { - const checkedTreeNodes = this.checkedTreeNodes - let unCheckPos - checkedTreeNodes.forEach(itemObj => { - const nodeProps = getAllProps(itemObj.node) - if (nodeProps.value === selectedValue) { - unCheckPos = itemObj.pos - } - }) - const newVals = [] - const newCkTns = [] - checkedTreeNodes.forEach(itemObj => { - if (isPositionPrefix(itemObj.pos, unCheckPos) || isPositionPrefix(unCheckPos, itemObj.pos)) { - // Filter ancestral and children nodes when uncheck a node. - return - } - const nodeProps = getAllProps(itemObj.node) - newCkTns.push(itemObj) - newVals.push(nodeProps.value) - }) - this.checkedTreeNodes = this._checkedNodes = newCkTns - const nv = this.sValue.filter(val => newVals.indexOf(val.value) !== -1) - this.fireChange(nv, { triggerValue: selectedValue, clear: true }) - }, - - setOpenState (open, needFocus, documentClickClose = false) { - this.clearDelayTimer() - const { $props: props } = this - // can not optimize, if children is empty - // if (this.sOpen === open) { - // return; - // } - if (!this.$props.dropdownVisibleChange(open, { documentClickClose })) { - return - } - this.setState({ - sOpen: open, - }, () => { - if (needFocus || open) { - // Input dom init after first time component render - // Add delay for this to get focus - setTimeout(() => { - if (open || isMultiple(props)) { - const input = this.getInputDOMNode() - if (input && document.activeElement !== input) { - input.focus() - } - } else if (this.$refs.selection) { - this.$refs.selection.focus() - } - }, 0) - } - }) - }, - - clearSearchInput () { - this.getInputDOMNode().focus() - if (!hasProp(this, 'inputValue')) { - this.setState({ sInputValue: '' }) - } - }, - - addLabelToValue (props, value_) { - let value = value_ - if (this.isLabelInValue()) { - value.forEach((v, i) => { - if (Object.prototype.toString.call(value[i]) !== '[object Object]') { - value[i] = { - value: '', - label: '', - } - return - } - v.label = v.label || this.getLabelFromProps(props, v.value) - }) - } else { - value = value.map(v => { - return { - value: v, - label: this.getLabelFromProps(props, v), - } - }) - } - return value - }, - - clearDelayTimer () { - if (this.delayTimer) { - clearTimeout(this.delayTimer) - this.delayTimer = null - } - }, - - removeSelected (selectedVal, e) { - const props = this.$props - if (props.disabled) { - return - } - - // Do not trigger Trigger popup - if (e && e.stopPropagation) { - e.stopPropagation() - } - - this._cacheTreeNodesStates = 'no' - if (props.treeCheckable && - (props.showCheckedStrategy === SHOW_ALL || props.showCheckedStrategy === SHOW_PARENT) && - !(props.treeCheckStrictly || this.sInputValue)) { - this.getDeselectedValue(selectedVal) - return - } - // click the node's `x`(in select box), likely trigger the TreeNode's `unCheck` event, - // cautiously, they are completely different, think about it, the tree may not render at first, - // but the nodes in select box are ready. - let label - const value = this.sValue.filter((singleValue) => { - if (singleValue.value === selectedVal) { - label = singleValue.label - } - return (singleValue.value !== selectedVal) - }) - const canMultiple = isMultiple(props) - - if (canMultiple) { - let event = selectedVal - if (this.isLabelInValue()) { - event = { - value: selectedVal, - label, - } - } - this.__emit('deselect', event) - } - if (props.treeCheckable) { - if (this.checkedTreeNodes && this.checkedTreeNodes.length) { - this.checkedTreeNodes = this._checkedNodes = this.checkedTreeNodes.filter(item => { - const nodeProps = getAllProps(item.node) - return value.some(i => i.value === nodeProps.value) - }) - } - } - - this.fireChange(value, { triggerValue: selectedVal, clear: true }) - }, - - openIfHasChildren () { - const props = this.$props - if (props.children.length || (props.treeData && props.treeData.length) || !isMultiple(props)) { - this.setOpenState(true) - } - }, - - fireChange (value, extraInfo = {}) { + /** + * Only update the value which is not in props + */ + setUncontrolledState (state) { + let needSync = false + const newState = {} const props = getOptionProps(this) - const vals = value.map(i => i.value) - const sv = this.sValue.map(i => i.value) - if (vals.length !== sv.length || !vals.every((val, index) => sv[index] === val)) { - const ex = { - preValue: [...this.sValue], - ...extraInfo, - } - let labs = null - let vls = value - if (!this.isLabelInValue()) { - labs = value.map(i => i.label) - vls = vls.map(v => v.value) - } else if (this.halfCheckedValues && this.halfCheckedValues.length) { - this.halfCheckedValues.forEach(i => { - if (!vls.some(v => v.value === i.value)) { - vls.push(i) - } - }) - } - if (props.treeCheckable && ex.clear) { - const treeData = this.renderedTreeData || props.children - ex.allCheckedNodes = flatToHierarchy(filterAllCheckedData(vals, treeData)) - } - if (props.treeCheckable && this.sInputValue) { - const _vls = [...this.sValue] - if (ex.checked) { - value.forEach(i => { - if (_vls.every(ii => ii.value !== i.value)) { - _vls.push({ ...i }) - } - }) - } else { - let index - const includeVal = _vls.some((i, ind) => { - if (i.value === ex.triggerValue) { - index = ind - return true - } - }) - if (includeVal) { - _vls.splice(index, 1) - } - } - vls = _vls - if (!this.isLabelInValue()) { - labs = _vls.map(v => v.label) - vls = _vls.map(v => v.value) - } - } - this._savedValue = isMultiple(props) ? vls : vls[0] - this.__emit('change', this._savedValue, labs, ex) - if (!('value' in props)) { - this._cacheTreeNodesStates = false - this.setState({ - sValue: this.getValue(props, toArray(this._savedValue).map((v, i) => { - return this.isLabelInValue() ? v : { - value: v, - label: labs && labs[i], - } - })), - }, this.forcePopupAlign) - } + Object.keys(state).forEach(name => { + if (name.slice(1) in props) return + + needSync = true + newState[name] = state[name] + }) + + if (needSync) { + this.setState(newState) } + + return needSync }, - isLabelInValue () { - const { treeCheckable, treeCheckStrictly, labelInValue } = this.$props - if (treeCheckable && treeCheckStrictly) { - return true + // [Legacy] Origin provide `documentClickClose` which triggered by `Trigger` + // Currently `TreeSelect` align the hide popup logic as `Select` which blur to hide. + // `documentClickClose` is not accurate anymore. Let's just keep the key word. + setOpenState (open, byTrigger = false) { + const { dropdownVisibleChange } = this.$props + + if ( + dropdownVisibleChange && + dropdownVisibleChange(open, { documentClickClose: !open && byTrigger }) === false + ) { + return } - return labelInValue || false - }, - onFocus (e) { - this.__emit('focus', e) - }, - onBlur (e) { - this.__emit('blur', e) + + this.setUncontrolledState({ _open: open }) }, - focus () { - if (!isMultiple(this.$props)) { - this.$refs.selection.focus() - } else { - this.getInputDOMNode().focus() - } + // Tree checkable is also a multiple case + isMultiple () { + const { multiple, treeCheckable } = this.$props + return !!(multiple || treeCheckable) }, - blur () { - if (!isMultiple(this.$props)) { - this.$refs.selection.blur() - } else { - this.getInputDOMNode().blur() - } + isLabelInValue () { + return isLabelInValue(this.$props) + }, + + // [Legacy] To align with `Select` component, + // We use `searchValue` instead of `inputValue` + // but currently still need support that. + // Add this method the check if is controlled + isSearchValueControlled () { + const props = getOptionProps(this) + const { inputValue } = props + if ('searchValue' in props) return true + return ('inputValue' in props) && inputValue !== null }, forcePopupAlign () { - this.$refs.trigger.$refs.trigger.forcePopupAlign() + const $trigger = this.selectTriggerRef.current + if ($trigger) { + $trigger.forcePopupAlign() + } }, - renderTopControlNode () { - const { sValue: value } = this.$data - const props = this.$props - const { choiceTransitionName, prefixCls, maxTagTextLength, removeIcon } = props - const multiple = isMultiple(props) - - // single and not combobox, input is inside dropdown - if (!multiple) { - let innerNode = (<span - key='placeholder' - class={`${prefixCls}-selection__placeholder`} - > - {getComponentFromProp(this, 'placeholder') || ''} - </span>) - if (value.length) { - innerNode = (<span - key='value' - title={toTitle(value[0].label)} - class={`${prefixCls}-selection-selected-value`} - > - {value[0].label} - </span>) - } - return (<span class={`${prefixCls}-selection__rendered`}> - {innerNode} - </span>) - } - - const selectedValueNodes = value.map((singleValue) => { - let content = singleValue.label - const title = content - if (maxTagTextLength && typeof content === 'string' && content.length > maxTagTextLength) { - content = `${content.slice(0, maxTagTextLength)}...` - } - return ( - <li - style={UNSELECTABLE_STYLE} - onMousedown={preventDefaultEvent} - class={`${prefixCls}-selection__choice`} - key={singleValue.value} - title={toTitle(title)} - {...{ attrs: UNSELECTABLE_ATTRIBUTE }} - > - <span - class={`${prefixCls}-selection__choice__remove`} - onClick={(event) => { - this.removeSelected(singleValue.value, event) - }} - >{removeIcon}</span> - <span class={`${prefixCls}-selection__choice__content`}>{content}</span> - </li> - ) + delayForcePopupAlign () { + // Wait 2 frame to avoid dom update & dom algin in the same time + // https://github.com/ant-design/ant-design/issues/12031 + raf(() => { + raf(this.forcePopupAlign) }) - - selectedValueNodes.push(<li - class={`${prefixCls}-search ${prefixCls}-search--inline`} - key='__input' - > - {this.getInputElement()} - </li>) - const className = `${prefixCls}-selection__rendered` - if (choiceTransitionName) { - const transitionProps = getTransitionProps(choiceTransitionName, { - tag: 'ul', - afterLeave: this.onChoiceAnimationLeave, - }) - return (<transition-group - class={className} - {...transitionProps} - > - {selectedValueNodes} - </transition-group>) - } - return (<ul class={className}>{selectedValueNodes}</ul>) }, - renderTreeData (props) { - const validProps = props || this.$props - if (validProps.treeData) { - if (props && props.treeData === this.preProps.treeData && this.renderedTreeData) { - // cache and use pre data. - this._cachetreeData = true - return this.renderedTreeData - } - this._cachetreeData = false - let treeData = [...validProps.treeData] - // process treeDataSimpleMode - if (validProps.treeDataSimpleMode) { - let simpleFormat = { - id: 'id', - pId: 'pId', - rootPId: null, - } - if (Object.prototype.toString.call(validProps.treeDataSimpleMode) === '[object Object]') { - simpleFormat = { ...simpleFormat, ...validProps.treeDataSimpleMode } - } - treeData = processSimpleTreeData(treeData, simpleFormat) - } - return this.loopTreeData(treeData, undefined, this.preProps.treeCheckable) + /** + * 1. Update state valueList. + * 2. Fire `onChange` event to user. + */ + triggerChange (missValueList, valueList, extraInfo = {}) { + const { _valueEntities: valueEntities, _searchValue: searchValue } = this.$data + const props = getOptionProps(this) + const { disabled, treeCheckable, treeCheckStrictly } = props + if (disabled) return + + // Trigger + const extra = { + // [Legacy] Always return as array contains label & value + preValue: this.$data._selectorValueList.map(({ label, value }) => ({ label, value })), + ...extraInfo, } + + // Format value by `treeCheckStrictly` + const selectorValueList = formatSelectorValue(valueList, props, valueEntities) + + if (!('value' in props)) { + const newState = { + _missValueList: missValueList, + _valueList: valueList, + _selectorValueList: selectorValueList, + } + + if (searchValue && treeCheckable && !treeCheckStrictly) { + newState._searchHalfCheckedKeys = getHalfCheckedKeys( + valueList, + valueEntities, + ) + } + + this.setState(newState) + } + + // Only do the logic when `onChange` function provided + if (this.$listeners.change) { + let connectValueList + + // Get value by mode + if (this.isMultiple()) { + connectValueList = [...missValueList, ...selectorValueList] + } else { + connectValueList = selectorValueList.slice(0, 1) + } + + let labelList = null + let returnValue + + if (this.isLabelInValue()) { + returnValue = connectValueList.map(({ label, value }) => ({ label, value })) + } else { + labelList = [] + returnValue = connectValueList.map(({ label, value }) => { + labelList.push(label) + return value + }) + } + + if (!this.isMultiple()) { + returnValue = returnValue[0] + } + this.__emit('change', returnValue, labelList, extra) + } + }, + + focus () { + this.selectorRef.current.focus() + }, + + blur () { + this.selectorRef.current.blur() }, }, + // ===================== Render ===================== + render () { - const props = this.$props - const multiple = isMultiple(props) - const state = this.$data - const { disabled, allowClear, prefixCls, inputIcon, clearIcon } = props - const ctrlNode = this.renderTopControlNode() - let extraSelectionProps = {} - if (!multiple) { - extraSelectionProps = { - on: { - keydown: this.onKeyDown, - blur: this.onBlur, - focus: this.onFocus, - }, - attrs: { - tabIndex: 0, - }, - } - } - const rootCls = { - [prefixCls]: 1, - [`${prefixCls}-open`]: state.sOpen, - [`${prefixCls}-focused`]: state.sOpen || state.sFocused, - // [`${prefixCls}-combobox`]: isCombobox(props), - [`${prefixCls}-disabled`]: disabled, - [`${prefixCls}-enabled`]: !disabled, - [`${prefixCls}-allow-clear`]: !!props.allowClear, - } - const clear = (<span - key='clear' - class={`${prefixCls}-selection__clear`} - onClick={this.onClearSelection} - >{clearIcon}</span>) - const selectTriggerProps = { + const { + _valueList: valueList, _missValueList: missValueList, _selectorValueList: selectorValueList, + _searchHalfCheckedKeys: searchHalfCheckedKeys, + _valueEntities: valueEntities, _keyEntities: keyEntities, + _searchValue: searchValue, + _open: open, _focused: focused, + _treeNodes: treeNodes, _filteredTreeNodes: filteredTreeNodes, + } = this.$data + const props = getOptionProps(this) + const { prefixCls, treeExpandedKeys } = props + const isMultiple = this.isMultiple() + + const passProps = { props: { ...props, - treeNodes: props.children, - treeData: this.renderedTreeData, - _cachetreeData: this._cachetreeData, - _treeNodesStates: this._treeNodesStates, - halfCheckedValues: this.halfCheckedValues, - multiple: multiple, - disabled: disabled, - visible: state.sOpen, - inputValue: state.sInputValue, - inputElement: this.getInputElement(), - value: state.sValue, - dropdownVisibleChange: this.onDropdownVisibleChange, - getPopupContainer: props.getPopupContainer, - filterTreeNode: this.filterTreeNode === undefined ? filterFn : this.filterTreeNode, + isMultiple, + valueList, + searchHalfCheckedKeys, + selectorValueList: [...missValueList, ...selectorValueList], + valueEntities, + keyEntities, + searchValue, + upperSearchValue: (searchValue || '').toUpperCase(), // Perf save + open, + focused, + dropdownPrefixCls: `${prefixCls}-dropdown`, + ariaId: this.ariaId, }, on: { ...this.$listeners, - select: this.onSelect, + choiceAnimationLeave: this.onChoiceAnimationLeave, }, - ref: 'trigger', + scopedSlots: this.$scopedSlots, } + const popupProps = mergeProps(passProps, { + props: { + treeNodes, + filteredTreeNodes, + // Tree expanded control + treeExpandedKeys, + __propsSymbol__: Symbol(), + }, + on: { + treeExpanded: this.delayForcePopupAlign, + }, + }) + + const Popup = isMultiple ? MultiplePopup : SinglePopup + const $popup = ( + <Popup + {...popupProps} + /> + ) + + const Selector = isMultiple ? MultipleSelector : SingleSelector + const $selector = ( + <Selector + {...passProps} + {...{ + directives: [{ + name: 'ant-ref', + value: this.selectorRef, + }] }} + /> + ) + const selectTriggerProps = mergeProps(passProps, { + props: { + popupElement: $popup, + dropdownVisibleChange: this.onDropdownVisibleChange, + }, + directives: [{ + name: 'ant-ref', + value: this.selectTriggerRef, + }], + }) return ( - <SelectTrigger {...selectTriggerProps}> - <span - onClick={props.onClick} - class={classnames(rootCls)} - > - <span - ref='selection' - key='selection' - class={`${prefixCls}-selection - ${prefixCls}-selection--${multiple ? 'multiple' : 'single'}`} - role='combobox' - aria-autocomplete='list' - aria-haspopup='true' - aria-expanded={state.sOpen} - {...extraSelectionProps} - > - {ctrlNode} - {allowClear && state.sValue.length && - state.sValue[0].value ? clear : null} - {multiple || !props.showArrow ? null - : (<span - key='arrow' - class={`${prefixCls}-arrow`} - style={{ outline: 'none' }} - > - {inputIcon} - </span>)} - {multiple - ? this.getSearchPlaceholderElement(!!state.sInputValue || state.sValue.length) - : null} - </span> - </span> + <SelectTrigger + {...selectTriggerProps} + > + {$selector} </SelectTrigger> ) }, } +Select.TreeNode = SelectNode Select.SHOW_ALL = SHOW_ALL Select.SHOW_PARENT = SHOW_PARENT Select.SHOW_CHILD = SHOW_CHILD +// Let warning show correct component name +Select.name = 'TreeSelect' + export default Select diff --git a/components/vc-tree-select/src/SelectNode.jsx b/components/vc-tree-select/src/SelectNode.jsx new file mode 100644 index 000000000..7bfbdd52c --- /dev/null +++ b/components/vc-tree-select/src/SelectNode.jsx @@ -0,0 +1,28 @@ +import { TreeNode } from '../../vc-tree' +/** + * SelectNode wrapped the tree node. + * Let's use SelectNode instead of TreeNode + * since TreeNode is so confuse here. + */ +export default { + functional: true, + name: 'SelectNode', + isTreeNode: true, + props: TreeNode.props, + render (h, context) { + const { props, slots, listeners, data } = context + const $slots = slots() + const children = $slots.default + delete $slots.default + const treeNodeProps = { + ...data, on: { ...listeners, ...data.nativeOn }, props, + } + const slotsKey = Object.keys($slots) + return <TreeNode {...treeNodeProps}> + {children} + {slotsKey.length ? slotsKey.map(name => { + return <template slot={name}>{$slots[name]}</template> + }) : null} + </TreeNode> + }, +} diff --git a/components/vc-tree-select/src/SelectTrigger.jsx b/components/vc-tree-select/src/SelectTrigger.jsx index b3fdef475..8b8fe023d 100644 --- a/components/vc-tree-select/src/SelectTrigger.jsx +++ b/components/vc-tree-select/src/SelectTrigger.jsx @@ -1,18 +1,8 @@ import PropTypes from '../../_util/vue-types' -import classnames from 'classnames' -import Trigger from '../../vc-trigger' -import Tree, { TreeNode } from '../../vc-tree' -import { SelectPropTypes } from './PropTypes' -import BaseMixin from '../../_util/BaseMixin' -import { - loopAllChildren, - flatToHierarchy, - getValuePropValue, - labelCompatible, -} from './util' -import { cloneElement } from '../../_util/vnode' -import { getSlotOptions, getKey, getAllProps, getComponentFromProp } from '../../_util/props-util' +import Trigger from '../../vc-trigger' +import { createRef } from './util' +import classNames from 'classnames' const BUILT_IN_PLACEMENTS = { bottomLeft: { @@ -22,6 +12,7 @@ const BUILT_IN_PLACEMENTS = { adjustX: 0, adjustY: 1, }, + ignoreShake: true, }, topLeft: { points: ['bl', 'tl'], @@ -30,335 +21,94 @@ const BUILT_IN_PLACEMENTS = { adjustX: 0, adjustY: 1, }, + ignoreShake: true, }, } const SelectTrigger = { - mixins: [BaseMixin], name: 'SelectTrigger', props: { - ...SelectPropTypes, - dropdownMatchSelectWidth: PropTypes.bool, - dropdownPopupAlign: PropTypes.object, - visible: PropTypes.bool, - filterTreeNode: PropTypes.any, - treeNodes: PropTypes.any, - inputValue: PropTypes.string, + // Pass by outside user props + disabled: PropTypes.bool, + showSearch: PropTypes.bool, prefixCls: PropTypes.string, - popupClassName: PropTypes.string, - _cachetreeData: PropTypes.any, - _treeNodesStates: PropTypes.any, - halfCheckedValues: PropTypes.any, - inputElement: PropTypes.any, - }, - data () { - return { - sExpandedKeys: [], - fireOnExpand: false, - dropdownWidth: null, - } - }, + dropdownPopupAlign: PropTypes.object, + dropdownClassName: PropTypes.string, + dropdownStyle: PropTypes.object, + transitionName: PropTypes.string, + animation: PropTypes.string, + getPopupContainer: PropTypes.func, - mounted () { - this.$nextTick(() => { - this.setDropdownWidth() - }) - }, - watch: { - inputValue (val) { - // set autoExpandParent to true - this.setState({ - sExpandedKeys: [], - fireOnExpand: false, - }) - }, - }, + dropdownMatchSelectWidth: PropTypes.bool, - updated () { - this.$nextTick(() => { - this.setDropdownWidth() - }) + // Pass by Select + isMultiple: PropTypes.bool, + dropdownPrefixCls: PropTypes.string, + dropdownVisibleChange: PropTypes.func, + popupElement: PropTypes.node, + open: PropTypes.bool, + }, + created () { + this.triggerRef = createRef() }, methods: { - onExpand (expandedKeys) { - // rerender - this.setState({ - sExpandedKeys: expandedKeys, - fireOnExpand: true, - }, () => { - // Fix https://github.com/ant-design/ant-design/issues/5689 - if (this.$refs.trigger && this.$refs.trigger.forcePopupAlign) { - this.$refs.trigger.forcePopupAlign() - } - }) - }, - - setDropdownWidth () { - const width = this.$el.offsetWidth - if (width !== this.dropdownWidth) { - this.setState({ dropdownWidth: width }) - } - }, - - getPopupEleRefs () { - return this.$refs.popupEle - }, - - getPopupDOMNode () { - return this.$refs.trigger.getPopupDomNode() - }, - getDropdownTransitionName () { - const props = this.$props - let transitionName = props.transitionName - if (!transitionName && props.animation) { - transitionName = `${this.getDropdownPrefixCls()}-${props.animation}` + const { transitionName, animation, dropdownPrefixCls } = this.$props + if (!transitionName && animation) { + return `${dropdownPrefixCls}-${animation}` } return transitionName }, - getDropdownPrefixCls () { - return `${this.prefixCls}-dropdown` - }, - - highlightTreeNode (treeNode) { - const props = this.$props - const filterVal = treeNode.$props[labelCompatible(props.treeNodeFilterProp)] - if (typeof filterVal === 'string') { - return props.inputValue && filterVal.indexOf(props.inputValue) > -1 + forcePopupAlign () { + const $trigger = this.triggerRef.current + if ($trigger) { + $trigger.forcePopupAlign() } - return false - }, - - filterTreeNode_ (input, child) { - if (!input) { - return true - } - const filterTreeNode = this.filterTreeNode - if (!filterTreeNode) { - return true - } - const props = getAllProps(child) - if (props && props.disabled) { - return false - } - return filterTreeNode.call(this, input, child) - }, - - processTreeNode (treeNodes) { - const filterPoss = [] - this._expandedKeys = [] - loopAllChildren(treeNodes, (child, index, pos) => { - if (this.filterTreeNode_(this.inputValue, child)) { - filterPoss.push(pos) - this._expandedKeys.push(String(getKey(child))) - } - }) - - // Include the filtered nodes's ancestral nodes. - const processedPoss = [] - filterPoss.forEach(pos => { - const arr = pos.split('-') - arr.reduce((pre, cur) => { - const res = `${pre}-${cur}` - if (processedPoss.indexOf(res) < 0) { - processedPoss.push(res) - } - return res - }) - }) - const filterNodesPositions = [] - loopAllChildren(treeNodes, (child, index, pos) => { - if (processedPoss.indexOf(pos) > -1) { - filterNodesPositions.push({ node: child, pos }) - } - }) - - const hierarchyNodes = flatToHierarchy(filterNodesPositions) - - const recursive = children => { - return children.map(child => { - if (child.children) { - return cloneElement(child.node, { - children: recursive(child.children), - }) - } - return child.node - }) - } - return recursive(hierarchyNodes) - }, - onSelect () { - this.__emit('select', ...arguments) - }, - - renderTree (keys, halfCheckedKeys, newTreeNodes, multiple) { - const props = this.$props - - const trProps = { - multiple, - prefixCls: `${props.prefixCls}-tree`, - showIcon: props.treeIcon, - showLine: props.treeLine, - defaultExpandAll: props.treeDefaultExpandAll, - defaultExpandedKeys: props.treeDefaultExpandedKeys, - filterTreeNode: this.highlightTreeNode, - } - const trListeners = {} - - if (props.treeCheckable) { - trProps.selectable = false - trProps.checkable = props.treeCheckable - trListeners.check = this.onSelect - trProps.checkStrictly = props.treeCheckStrictly - if (props.inputValue) { - // enable checkStrictly when search tree. - trProps.checkStrictly = true - } else { - trProps._treeNodesStates = props._treeNodesStates - } - if (trProps.treeCheckStrictly && halfCheckedKeys.length) { - trProps.checkedKeys = { checked: keys, halfChecked: halfCheckedKeys } - } else { - trProps.checkedKeys = keys - } - } else { - trProps.selectedKeys = keys - trListeners.select = this.onSelect - } - - // expand keys - if (!trProps.defaultExpandAll && !trProps.defaultExpandedKeys && !props.loadData) { - trProps.expandedKeys = keys - } - trProps.autoExpandParent = true - trListeners.expand = this.onExpand - if (this._expandedKeys && this._expandedKeys.length) { - trProps.expandedKeys = this._expandedKeys - } - if (this.fireOnExpand) { - trProps.expandedKeys = this.sExpandedKeys - trProps.autoExpandParent = false - } - - // async loadData - if (props.loadData) { - trProps.loadData = props.loadData - } - return ( - <Tree ref='popupEle' {...{ props: trProps, on: trListeners }}> - {newTreeNodes} - </Tree> - ) }, }, render () { - const props = this.$props - const multiple = props.multiple - const dropdownPrefixCls = this.getDropdownPrefixCls() - const popupClassName = { - [props.dropdownClassName]: !!props.dropdownClassName, - [`${dropdownPrefixCls}--${multiple ? 'multiple' : 'single'}`]: 1, - } - let visible = props.visible - const search = multiple || !props.showSearch ? null : ( - <span class={`${dropdownPrefixCls}-search`}>{props.inputElement}</span> - ) - const recursive = children => { - return children.map(function handler(child) { // eslint-disable-line - // if (isEmptyElement(child) || (child.data && child.data.slot)) { - // return null - // } - if (!getSlotOptions(child).__ANT_TREE_SELECT_NODE) { - return null - } - const treeNodeProps = { - ...child.data, - props: { - ...getAllProps(child), - switcherIcon: props.switcherIcon, - title: getComponentFromProp(child, 'title') || getComponentFromProp(child, 'label'), - }, - key: String(child.key), - } - if (child && child.componentOptions.children) { - // null or String has no Prop - return ( - <TreeNode {...treeNodeProps}> - {recursive(child.componentOptions.children) } - </TreeNode> - ) - } - return <TreeNode {...treeNodeProps} /> - }) - } - // const s = Date.now(); - let treeNodes - if (props._cachetreeData && this.cacheTreeNodes) { - treeNodes = this.cacheTreeNodes - } else { - treeNodes = recursive(props.treeData || props.treeNodes) - this.cacheTreeNodes = treeNodes - } - // console.log(Date.now()-s); + const { + disabled, isMultiple, + dropdownPopupAlign, dropdownMatchSelectWidth, dropdownClassName, + dropdownStyle, dropdownVisibleChange, getPopupContainer, + dropdownPrefixCls, popupElement, open, + } = this.$props - if (props.inputValue) { - treeNodes = this.processTreeNode(treeNodes) + // TODO: [Legacy] Use new action when trigger fixed: https://github.com/react-component/trigger/pull/86 + + // When false do nothing with the width + // ref: https://github.com/ant-design/ant-design/issues/10927 + let stretch + if (dropdownMatchSelectWidth !== false) { + stretch = dropdownMatchSelectWidth ? 'width' : 'minWidth' } - - const keys = [] - const halfCheckedKeys = [] - loopAllChildren(treeNodes, (child) => { - if (props.value.some(item => item.value === getValuePropValue(child))) { - keys.push(String(getKey(child))) - } - if (props.halfCheckedValues && - props.halfCheckedValues.some(item => item.value === getValuePropValue(child))) { - halfCheckedKeys.push(String(getKey(child))) - } - }) - - let notFoundContent - if (!treeNodes.length) { - if (props.notFoundContent) { - notFoundContent = ( - <span class={`${props.prefixCls}-not-found`}> - {props.notFoundContent} - </span> - ) - } else if (!search) { - visible = false - } - } - const popupElement = ( - <div> - {search} - {notFoundContent || this.renderTree(keys, halfCheckedKeys, treeNodes, multiple)} - </div> - ) - - const popupStyle = { ...props.dropdownStyle } - const widthProp = props.dropdownMatchSelectWidth ? 'width' : 'minWidth' - if (this.dropdownWidth) { - popupStyle[widthProp] = `${this.dropdownWidth}px` - } - return ( <Trigger - action={props.disabled ? [] : ['click']} - ref='trigger' + {...{ directives: [{ + name: 'ant-ref', + value: this.triggerRef, + }] }} + action={disabled ? [] : ['click']} popupPlacement='bottomLeft' builtinPlacements={BUILT_IN_PLACEMENTS} - popupAlign={props.dropdownPopupAlign} + popupAlign={dropdownPopupAlign} prefixCls={dropdownPrefixCls} popupTransitionName={this.getDropdownTransitionName()} - onPopupVisibleChange={props.dropdownVisibleChange} + onPopupVisibleChange={dropdownVisibleChange} popup={popupElement} - popupVisible={visible} - getPopupContainer={props.getPopupContainer} - popupClassName={classnames(popupClassName)} - popupStyle={popupStyle} + popupVisible={open} + getPopupContainer={getPopupContainer} + stretch={stretch} + popupClassName={classNames( + dropdownClassName, + { + [`${dropdownPrefixCls}--multiple`]: isMultiple, + [`${dropdownPrefixCls}--single`]: !isMultiple, + }, + )} + popupStyle={dropdownStyle} > {this.$slots.default} </Trigger> diff --git a/components/vc-tree-select/src/Selector/MultipleSelector/Selection.jsx b/components/vc-tree-select/src/Selector/MultipleSelector/Selection.jsx new file mode 100644 index 000000000..43f7faa77 --- /dev/null +++ b/components/vc-tree-select/src/Selector/MultipleSelector/Selection.jsx @@ -0,0 +1,64 @@ + +import PropTypes from '../../../../_util/vue-types' +import { + toTitle, + UNSELECTABLE_ATTRIBUTE, UNSELECTABLE_STYLE, +} from '../../util' +import { getComponentFromProp } from '../../../../_util/props-util' +import BaseMixin from '../../../../_util/BaseMixin' + +const Selection = { + mixins: [BaseMixin], + props: { + prefixCls: PropTypes.string, + maxTagTextLength: PropTypes.number, + // onRemove: PropTypes.func, + + label: PropTypes.any, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + removeIcon: PropTypes.any, + }, + methods: { + onRemove (event) { + const { value } = this.$props + this.__emit('remove', event, value) + event.stopPropagation() + }, + }, + + render () { + const { + prefixCls, maxTagTextLength, + label, value, + } = this.$props + const { $listeners } = this + let content = label || value + if (maxTagTextLength && typeof content === 'string' && content.length > maxTagTextLength) { + content = `${content.slice(0, maxTagTextLength)}...` + } + + return ( + <li + style={UNSELECTABLE_STYLE} + {...{ attrs: UNSELECTABLE_ATTRIBUTE }} + role='menuitem' + class={`${prefixCls}-selection__choice`} + title={toTitle(label)} + > + {$listeners.remove && + <span + class={`${prefixCls}-selection__choice__remove`} + onClick={this.onRemove} + > + {getComponentFromProp(this, 'removeIcon')} + </span> + } + <span class={`${prefixCls}-selection__choice__content`}> + {content} + </span> + </li> + ) + }, +} + +export default Selection diff --git a/components/vc-tree-select/src/Selector/MultipleSelector/index.jsx b/components/vc-tree-select/src/Selector/MultipleSelector/index.jsx new file mode 100644 index 000000000..3088b1de1 --- /dev/null +++ b/components/vc-tree-select/src/Selector/MultipleSelector/index.jsx @@ -0,0 +1,192 @@ +import PropTypes from '../../../../_util/vue-types' +import { createRef } from '../../util' +import generateSelector, { selectorPropTypes } from '../../Base/BaseSelector' +import SearchInput from '../../SearchInput' +import Selection from './Selection' +import { getComponentFromProp } from '../../../../_util/props-util' +import getTransitionProps from '../../../../_util/getTransitionProps' +import BaseMixin from '../../../../_util/BaseMixin' +const TREE_SELECT_EMPTY_VALUE_KEY = 'RC_TREE_SELECT_EMPTY_VALUE_KEY' + +const Selector = generateSelector('multiple') + +// export const multipleSelectorContextTypes = { +// onMultipleSelectorRemove: PropTypes.func.isRequired, +// } + +const MultipleSelector = { + mixins: [BaseMixin], + props: { + ...selectorPropTypes(), + ...SearchInput.props, + selectorValueList: PropTypes.array, + disabled: PropTypes.bool, + searchValue: PropTypes.string, + labelInValue: PropTypes.bool, + maxTagCount: PropTypes.number, + maxTagPlaceholder: PropTypes.any, + + // onChoiceAnimationLeave: PropTypes.func, + }, + inject: { + vcTreeSelect: { default: {}}, + }, + created () { + this.inputRef = createRef() + }, + methods: { + onPlaceholderClick () { + this.inputRef.current.focus() + }, + + focus () { + this.inputRef.current.focus() + }, + blur () { + this.inputRef.current.blur() + }, + + _renderPlaceholder () { + const { + prefixCls, + placeholder, searchPlaceholder, + searchValue, selectorValueList, + } = this.$props + + const currentPlaceholder = placeholder || searchPlaceholder + + if (!currentPlaceholder) return null + + const hidden = searchValue || selectorValueList.length + + // [Legacy] Not remove the placeholder + return ( + <span + style={{ + display: hidden ? 'none' : 'block', + }} + onClick={this.onPlaceholderClick} + class={`${prefixCls}-search__field__placeholder`} + > + {currentPlaceholder} + </span> + ) + }, + onChoiceAnimationLeave (...args) { + this.__emit('choiceAnimationLeave', ...args) + }, + renderSelection () { + const { + selectorValueList, choiceTransitionName, prefixCls, + labelInValue, maxTagCount, + } = this.$props + const { vcTreeSelect: { onMultipleSelectorRemove }, $listeners, $slots } = this + + // Check if `maxTagCount` is set + let myValueList = selectorValueList + if (maxTagCount >= 0) { + myValueList = selectorValueList.slice(0, maxTagCount) + } + // Selector node list + const selectedValueNodes = myValueList.map(({ label, value }) => ( + <Selection + {...{ + props: { + ...this.$props, + label, + value, + }, + on: { ...$listeners, remove: onMultipleSelectorRemove }, + }} + key={value || TREE_SELECT_EMPTY_VALUE_KEY} + >{$slots.default}</Selection> + )) + + // Rest node count + if (maxTagCount >= 0 && maxTagCount < selectorValueList.length) { + let content = `+ ${selectorValueList.length - maxTagCount} ...` + const maxTagPlaceholder = getComponentFromProp(this, 'maxTagPlaceholder', {}, false) + if (typeof maxTagPlaceholder === 'string') { + content = maxTagPlaceholder + } else if (typeof maxTagPlaceholder === 'function') { + const restValueList = selectorValueList.slice(maxTagCount) + content = maxTagPlaceholder( + labelInValue ? restValueList : restValueList.map(({ value }) => value) + ) + } + + const restNodeSelect = ( + <Selection + {...{ + props: { + ...this.$props, + label: content, + value: null, + }, + on: $listeners, + }} + key='rc-tree-select-internal-max-tag-counter' + >{$slots.default}</Selection> + ) + + selectedValueNodes.push(restNodeSelect) + } + + selectedValueNodes.push(<li + class={`${prefixCls}-search ${prefixCls}-search--inline`} + key='__input' + > + <SearchInput {...{ + props: { + ...this.$props, + needAlign: true, + }, + on: $listeners, + directives: [{ + name: 'ant-ref', + value: this.inputRef, + }], + }}>{$slots.default}</SearchInput> + </li>) + const className = `${prefixCls}-selection__rendered` + if (choiceTransitionName) { + const transitionProps = getTransitionProps(choiceTransitionName, { + tag: 'ul', + afterLeave: this.onChoiceAnimationLeave, + }) + return (<transition-group + class={className} + {...transitionProps} + > + {selectedValueNodes} + </transition-group>) + } + return ( + <ul class={className} role='menubar'> + {selectedValueNodes} + </ul> + ) + }, + }, + + render () { + const { $listeners, $slots } = this + return ( + <Selector + {...{ + props: { + ...this.$props, + tabIndex: -1, + showArrow: false, + renderSelection: this.renderSelection, + renderPlaceholder: this._renderPlaceholder, + }, + on: $listeners, + }} + + >{$slots.default}</Selector> + ) + }, +} + +export default MultipleSelector diff --git a/components/vc-tree-select/src/Selector/SingleSelector.jsx b/components/vc-tree-select/src/Selector/SingleSelector.jsx new file mode 100644 index 000000000..30ff09f4e --- /dev/null +++ b/components/vc-tree-select/src/Selector/SingleSelector.jsx @@ -0,0 +1,75 @@ +import generateSelector, { selectorPropTypes } from '../Base/BaseSelector' +import { toTitle } from '../util' +import { getOptionProps } from '../../../_util/props-util' +import { createRef } from '../util' +const Selector = generateSelector('single') + +const SingleSelector = { + name: 'SingleSelector', + props: selectorPropTypes(), + created () { + this.selectorRef = createRef() + }, + methods: { + focus () { + this.selectorRef.current.focus() + }, + blur () { + this.selectorRef.current.blur() + }, + renderSelection () { + const { selectorValueList, placeholder, prefixCls } = this.$props + + let innerNode + + if (selectorValueList.length) { + const { label, value } = selectorValueList[0] + innerNode = ( + <span + key='value' + title={toTitle(label)} + class={`${prefixCls}-selection-selected-value`} + > + {label || value} + </span> + ) + } else { + innerNode = ( + <span + key='placeholder' + class={`${prefixCls}-selection__placeholder`} + > + {placeholder} + </span> + ) + } + + return ( + <span class={`${prefixCls}-selection__rendered`}> + {innerNode} + </span> + ) + }, + }, + + render () { + const props = { + props: { + ...getOptionProps(this), + renderSelection: this.renderSelection, + }, + on: this.$listeners, + directives: [{ + name: 'ant-ref', + value: this.selectorRef, + }], + } + return ( + <Selector + {...props} + /> + ) + }, +} + +export default SingleSelector diff --git a/components/vc-tree-select/src/TreeNode.jsx b/components/vc-tree-select/src/TreeNode.jsx deleted file mode 100644 index 9a9d2d195..000000000 --- a/components/vc-tree-select/src/TreeNode.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import { TreeNode } from '../../vc-tree' -export default { - name: 'TreeNode', - __ANT_TREE_SELECT_NODE: true, - props: { - ...TreeNode.props, - value: String, - }, - render () { - return this - }, -} diff --git a/components/vc-tree-select/src/index.js b/components/vc-tree-select/src/index.js index 27d435e50..9c673b623 100644 --- a/components/vc-tree-select/src/index.js +++ b/components/vc-tree-select/src/index.js @@ -1,26 +1,7 @@ -// export this package's api -import TreeSelect from './Select' -import TreeNode from './TreeNode' -import omit from 'omit.js' -import { SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './strategies' -TreeSelect.TreeNode = TreeNode +import Select from './Select' +import SelectNode from './SelectNode' -export default { - functional: true, - render (h, context) { - const { props, listeners, children = [], data } = context - const treeSelectProps = { - ...omit(data, ['attrs']), - props: { - ...props, - children, - __propsSymbol__: Symbol(), - }, - on: listeners, - } - return <TreeSelect {...treeSelectProps}/> - }, - TreeNode, - SHOW_ALL, SHOW_PARENT, SHOW_CHILD, -} -export { TreeNode, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } +export { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './strategies' +export const TreeNode = SelectNode + +export default Select diff --git a/components/vc-tree-select/src/propTypes.js b/components/vc-tree-select/src/propTypes.js new file mode 100644 index 000000000..2afe88e41 --- /dev/null +++ b/components/vc-tree-select/src/propTypes.js @@ -0,0 +1,46 @@ +import PropTypes from '../../_util/vue-types' +import { isLabelInValue } from './util' + +const internalValProp = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, +]) + +export function genArrProps (propType) { + return PropTypes.oneOfType([ + propType, + PropTypes.arrayOf(propType), + ]) +} + +/** + * Origin code check `multiple` is true when `treeCheckStrictly` & `labelInValue`. + * But in process logic is already cover to array. + * Check array is not necessary. Let's simplify this check logic. + */ +export function valueProp (...args) { + const [props, propName, Component] = args + + if (isLabelInValue(props)) { + const err = genArrProps(PropTypes.shape({ + label: PropTypes.node, + value: internalValProp, + }).loose)(...args) + if (err) { + return new Error( + `Invalid prop \`${propName}\` supplied to \`${Component}\`. ` + + `You should use { label: string, value: string | number } or [{ label: string, value: string | number }] instead.` + ) + } + return null + } + + const err = genArrProps(internalValProp)(...args) + if (err) { + return new Error( + `Invalid prop \`${propName}\` supplied to \`${Component}\`. ` + + `You should use string or [string] instead.` + ) + } + return null +} diff --git a/components/vc-tree-select/src/util.js b/components/vc-tree-select/src/util.js index 4b7ebca91..4ee1ded02 100644 --- a/components/vc-tree-select/src/util.js +++ b/components/vc-tree-select/src/util.js @@ -1,5 +1,17 @@ -import { getPropsData, getAllProps, getKey, getAttrs, getSlotOptions, getSlots } from '../../_util/props-util' -import { cloneVNodes, cloneElement } from '../../_util/vnode' +import warning from 'warning' +import omit from 'omit.js' +import { + convertDataToTree as vcConvertDataToTree, + convertTreeToEntities as vcConvertTreeToEntities, + conductCheck as rcConductCheck, +} from '../../vc-tree/src/util' +import SelectNode from './SelectNode' +import { SHOW_CHILD, SHOW_PARENT } from './strategies' +import { getSlots, getPropsData } from '../../_util/props-util' + +let warnDeprecatedLabel = false + +// =================== MISC ==================== export function toTitle (title) { if (typeof title === 'string') { return title @@ -7,58 +19,20 @@ export function toTitle (title) { return null } -export function getValuePropValue (child) { - const props = getAllProps(child) - if ('value' in props) { - return props.value - } - if (getKey(child) !== undefined) { - return getKey(child) - } - throw new Error(`no key or value for ${child}`) +export function toArray (data) { + if (!data) return [] + + return Array.isArray(data) ? data : [data] } -export function getPropValue (child, prop) { - if (prop === 'value') { - return getValuePropValue(child) - } - const slots = getSlots(child) - if (prop === 'children') { - const newChild = child.$slots ? cloneVNodes(child.$slots.default, true) : cloneVNodes(child.componentOptions.children, true) - if (newChild.length === 1 && !newChild[0].tag) { - return newChild[0].text - } - return newChild - } - if (slots[prop]) { - return cloneVNodes(slots[prop], true) - } - const data = getPropsData(child) - if (prop in data) { - return data[prop] - } else { - return getAttrs(child)[prop] +export function createRef () { + const func = function setRef (node) { + func.current = node } + return func } -export function isMultiple (props) { - return !!(props.multiple || props.treeCheckable) -} - -export function toArray (value) { - let ret = value - if (value === undefined) { - ret = [] - } else if (!Array.isArray(value)) { - ret = [value] - } - return ret -} - -export function preventDefaultEvent (e) { - e.preventDefault() -} - +// =============== Legacy =============== export const UNSELECTABLE_STYLE = { userSelect: 'none', WebkitUserSelect: 'none', @@ -68,502 +42,389 @@ export const UNSELECTABLE_ATTRIBUTE = { unselectable: 'unselectable', } -export function labelCompatible (prop) { - let newProp = prop - if (newProp === 'label') { - newProp = 'title' +/** + * Convert position list to hierarchy structure. + * This is little hack since use '-' to split the position. + */ +export function flatToHierarchy (positionList) { + if (!positionList.length) { + return [] } - return newProp -} -export function isInclude (smallArray, bigArray) { - // attention: [0,0,1] [0,0,10] - return smallArray.every((ii, i) => { - return ii === bigArray[i] - }) -} + const entrances = {} -export function isPositionPrefix (smallPos, bigPos) { - if (!bigPos || !smallPos) { - // console.log(smallPos, bigPos); - return false - } - if (bigPos.length < smallPos.length) { - return false - } - // attention: "0-0-1" "0-0-10" - if ((bigPos.length > smallPos.length) && (bigPos.charAt(smallPos.length) !== '-')) { - return false - } - return bigPos.substr(0, smallPos.length) === smallPos -} - -/* -export function getCheckedKeys(node, checkedKeys, allCheckedNodesKeys) { - const nodeKey = node.props.eventKey; - let newCks = [...checkedKeys]; - let nodePos; - const unCheck = allCheckedNodesKeys.some(item => { - if (item.key === nodeKey) { - nodePos = item.pos; - return true; + // Prepare the position map + const posMap = {} + const parsedList = positionList.slice().map(entity => { + const clone = { + ...entity, + fields: entity.pos.split('-'), } - }); - if (unCheck) { - newCks = []; - allCheckedNodesKeys.forEach(item => { - if (isPositionPrefix(item.pos, nodePos) || isPositionPrefix(nodePos, item.pos)) { - return; - } - newCks.push(item.key); - }); - } else { - newCks.push(nodeKey); - } - return newCks; -} -*/ + delete clone.children + return clone + }) -function getChildrenlength (children) { - let len = 1 - if (Array.isArray(children)) { - len = children.length - } - return len + parsedList.forEach((entity) => { + posMap[entity.pos] = entity + }) + + parsedList.sort((a, b) => { + return a.fields.length - b.fields.length + }) + + // Create the hierarchy + parsedList.forEach((entity) => { + const parentPos = entity.fields.slice(0, -1).join('-') + const parentEntity = posMap[parentPos] + + if (!parentEntity) { + entrances[entity.pos] = entity + } else { + parentEntity.children = parentEntity.children || [] + parentEntity.children.push(entity) + } + + // Some time position list provide `key`, we don't need it + delete entity.key + delete entity.fields + }) + + return Object.keys(entrances).map(key => entrances[key]) } -function getSiblingPosition (index, len, siblingPosition) { - if (len === 1) { - siblingPosition.first = true - siblingPosition.last = true - } else { - siblingPosition.first = index === 0 - siblingPosition.last = index === len - 1 - } - return siblingPosition +// =============== Accessibility =============== +let ariaId = 0 + +export function resetAriaId () { + ariaId = 0 } -function filterChild (childs) { - const newChilds = [] - childs.forEach(child => { - const options = getSlotOptions(child) - if (options.__ANT_TREE_NODE || options.__ANT_TREE_SELECT_NODE) { - newChilds.push(child) +export function generateAriaId (prefix) { + ariaId += 1 + return `${prefix}_${ariaId}` +} + +export function isLabelInValue (props) { + const { treeCheckable, treeCheckStrictly, labelInValue } = props + if (treeCheckable && treeCheckStrictly) { + return true + } + return labelInValue || false +} + +// =================== Tree ==================== +export function parseSimpleTreeData (treeData, { id, pId, rootPId }) { + const keyNodes = {} + const rootNodeList = [] + + // Fill in the map + const nodeList = treeData.map((node) => { + const clone = { ...node } + const key = clone[id] + keyNodes[key] = clone + clone.key = clone.key || key + return clone + }) + + // Connect tree + nodeList.forEach((node) => { + const parentKey = node[pId] + const parent = keyNodes[parentKey] + + // Fill parent + if (parent) { + parent.children = parent.children || [] + parent.children.push(node) + } + + // Fill root tree node + if (parentKey === rootPId || (!parent && rootPId === null)) { + rootNodeList.push(node) } }) - return newChilds + + return rootNodeList } -export function loopAllChildren (childs, callback, parent) { - const loop = (children, level, _parent) => { - const len = getChildrenlength(children) - children.forEach(function handler(item, index) { // eslint-disable-line - const pos = `${level}-${index}` - if (item && item.componentOptions && item.componentOptions.children) { - loop(filterChild(item.componentOptions.children), pos, { node: item, pos }) - } - if (item) { - callback(item, index, pos, item.key || pos, getSiblingPosition(index, len, {}), _parent) +/** + * Detect if position has relation. + * e.g. 1-2 related with 1-2-3 + * e.g. 1-3-2 related with 1 + * e.g. 1-2 not related with 1-21 + */ +export function isPosRelated (pos1, pos2) { + const fields1 = pos1.split('-') + const fields2 = pos2.split('-') + + const minLen = Math.min(fields1.length, fields2.length) + for (let i = 0; i < minLen; i += 1) { + if (fields1[i] !== fields2[i]) { + return false + } + } + return true +} + +/** + * This function is only used on treeNode check (none treeCheckStrictly but has searchInput). + * We convert entity to { node, pos, children } format. + * This is legacy bug but we still need to do with it. + * @param entity + */ +export function cleanEntity ({ node, pos, children }) { + const instance = { + node, + pos, + } + + if (children) { + instance.children = children.map(cleanEntity) + } + + return instance +} + +/** + * Get a filtered TreeNode list by provided treeNodes. + * [Legacy] Since `Tree` use `key` as map but `key` will changed by React, + * we have to convert `treeNodes > data > treeNodes` to keep the key. + * Such performance hungry! + */ +export function getFilterTree (h, treeNodes, searchValue, filterFunc, valueEntities) { + if (!searchValue) { + return null + } + + function mapFilteredNodeToData (node) { + if (!node) return null + + let match = false + if (filterFunc(searchValue, node)) { + match = true + } + const $slots = getSlots(node) + const children = ($slots.default || []).map(mapFilteredNodeToData).filter(n => n) + delete $slots.default + const slotsKey = Object.keys($slots) + if (children.length || match) { + return ( + <SelectNode + {...node.data} + key={valueEntities[getPropsData(node).value].key} + > + {children} + {slotsKey.length ? slotsKey.map(name => { + return <template slot={name}>{$slots[name][0].tag === 'template' ? $slots[name][0].children : $slots[name]}</template> + }) : null} + </SelectNode> + ) + } + + return null + } + return treeNodes.map(mapFilteredNodeToData).filter(node => node) +} + +// =================== Value =================== +/** + * Convert value to array format to make logic simplify. + */ +export function formatInternalValue (value, props) { + const valueList = toArray(value) + + // Parse label in value + if (isLabelInValue(props)) { + return valueList.map((val) => { + if (typeof val !== 'object' || !val) { + return { + value: '', + label: '', + } } + + return val }) } - loop(filterChild(childs), 0, parent) + + return valueList.map(val => ({ + value: val, + })) } -// export function loopAllChildren(childs, callback) { -// const loop = (children, level) => { -// React.Children.forEach(children, (item, index) => { -// const pos = `${level}-${index}`; -// if (item && item.props.children) { -// loop(item.props.children, pos); -// } -// if (item) { -// callback(item, index, pos, getValuePropValue(item)); -// } -// }); -// }; -// loop(childs, 0); -// } - -// TODO: Here has the side effect. Update node children data affect. -export function flatToHierarchy (arr) { - if (!arr.length) { - return arr +export function getLabel (wrappedValue, entity, treeNodeLabelProp) { + if (wrappedValue.label) { + return wrappedValue.label } - const hierarchyNodes = [] - const levelObj = {} - arr.forEach((item) => { - if (!item.pos) { - return + + if (entity) { + const props = getPropsData(entity.node) + if (Object.keys(props).length) { + return props[treeNodeLabelProp] } - const posLen = item.pos.split('-').length - if (!levelObj[posLen]) { - levelObj[posLen] = [] - } - levelObj[posLen].push(item) - }) - const levelArr = Object.keys(levelObj).sort((a, b) => b - a) - // const s = Date.now(); - // todo: there are performance issues! - levelArr.reduce((pre, cur) => { - if (cur && cur !== pre) { - levelObj[pre].forEach((item) => { - let haveParent = false - levelObj[cur].forEach((ii) => { - if (isPositionPrefix(ii.pos, item.pos)) { - haveParent = true - if (!ii.children) { - ii.children = [] - } - ii.children.push(item) - } + } + + // Since value without entity will be in missValueList. + // This code will never reached, but we still need this in case. + return wrappedValue.value +} + +/** + * Convert internal state `valueList` to user needed value list. + * This will return an array list. You need check if is not multiple when return. + * + * `allCheckedNodes` is used for `treeCheckStrictly` + */ +export function formatSelectorValue (valueList, props, valueEntities) { + const { + treeNodeLabelProp, + treeCheckable, treeCheckStrictly, showCheckedStrategy, + } = props + + // Will hide some value if `showCheckedStrategy` is set + if (treeCheckable && !treeCheckStrictly) { + const values = {} + valueList.forEach((wrappedValue) => { + values[wrappedValue.value] = wrappedValue + }) + const hierarchyList = flatToHierarchy(valueList.map(({ value }) => valueEntities[value])) + + if (showCheckedStrategy === SHOW_PARENT) { + // Only get the parent checked value + return hierarchyList.map(({ node }) => { + const value = getPropsData(node).value + return { + label: getLabel(values[value], valueEntities[value], treeNodeLabelProp), + value, + } + }) + } else if (showCheckedStrategy === SHOW_CHILD) { + // Only get the children checked value + const targetValueList = [] + + // Find the leaf children + const traverse = ({ node, children }) => { + const value = getPropsData(node).value + if (!children || children.length === 0) { + targetValueList.push({ + label: getLabel(values[value], valueEntities[value], treeNodeLabelProp), + value, + }) + return + } + + children.forEach((entity) => { + traverse(entity) }) - if (!haveParent) { - hierarchyNodes.push(item) - } - }) - } - return cur - }) - // console.log(Date.now() - s); - return levelObj[levelArr[levelArr.length - 1]].concat(hierarchyNodes) -} + } -// arr.length === 628, use time: ~20ms -export function filterParentPosition (arr) { - const levelObj = {} - arr.forEach((item) => { - const posLen = item.split('-').length - if (!levelObj[posLen]) { - levelObj[posLen] = [] - } - levelObj[posLen].push(item) - }) - const levelArr = Object.keys(levelObj).sort() - for (let i = 0; i < levelArr.length; i++) { - if (levelArr[i + 1]) { - levelObj[levelArr[i]].forEach(ii => { - for (let j = i + 1; j < levelArr.length; j++) { - levelObj[levelArr[j]].forEach((_i, index) => { - if (isPositionPrefix(ii, _i)) { - levelObj[levelArr[j]][index] = null - } - }) - levelObj[levelArr[j]] = levelObj[levelArr[j]].filter(p => p) - } + hierarchyList.forEach((entity) => { + traverse(entity) }) + + return targetValueList } } - let nArr = [] - levelArr.forEach(i => { - nArr = nArr.concat(levelObj[i]) - }) - return nArr -} -// console.log(filterParentPosition( -// ['0-2', '0-3-3', '0-10', '0-10-0', '0-0-1', '0-0', '0-1-1', '0-1'] -// )); -function stripTail (str) { - const arr = str.match(/(.+)(-[^-]+)$/) - let st = '' - if (arr && arr.length === 3) { - st = arr[1] + return valueList.map(wrappedValue => ({ + label: getLabel(wrappedValue, valueEntities[wrappedValue.value], treeNodeLabelProp), + value: wrappedValue.value, + })) +} + +/** + * Use `rc-tree` convertDataToTree to convert treeData to TreeNodes. + * This will change the label to title value + */ +function processProps (props) { + const { title, label, key, value, class: cls, style, on = {}} = props + const p = { + props: omit(props, ['on', 'key', 'class', 'className', 'style']), + on, + class: cls || props.className, + style: style, + key: typeof key === 'number' ? String(key) : (key || value), } - return st -} -function splitPosition (pos) { - return pos.split('-') -} - -// todo: do optimization. -export function handleCheckState (obj, checkedPositionArr, checkIt) { - // console.log(stripTail('0-101-000')); - // let s = Date.now(); - let objKeys = Object.keys(obj) - - objKeys.forEach((i, index) => { - const iArr = splitPosition(i) - let saved = false - checkedPositionArr.forEach((_pos) => { - const _posArr = splitPosition(_pos) - if (iArr.length > _posArr.length && isInclude(_posArr, iArr)) { - obj[i].halfChecked = false - obj[i].checked = checkIt - objKeys[index] = null - } - if (iArr[0] === _posArr[0] && iArr[1] === _posArr[1]) { - saved = true - } - }) - if (!saved) { - objKeys[index] = null + // Warning user not to use deprecated label prop. + if (label && !title) { + if (!warnDeprecatedLabel) { + warning( + false, + '\'label\' in treeData is deprecated. Please use \'title\' instead.' + ) + warnDeprecatedLabel = true } - }) - objKeys = objKeys.filter(i => i) // filter non null; - for (let pIndex = 0; pIndex < checkedPositionArr.length; pIndex++) { - // loop to set ancestral nodes's `checked` or `halfChecked` - const loop = (__pos) => { - const _posLen = splitPosition(__pos).length - if (_posLen <= 2) { // e.g. '0-0', '0-1' - return - } - let sibling = 0 - let siblingChecked = 0 - const parentPosition = stripTail(__pos) - objKeys.forEach((i /* , index*/) => { - const iArr = splitPosition(i) - if (iArr.length === _posLen && isInclude(splitPosition(parentPosition), iArr)) { - sibling++ - if (obj[i].checked) { - siblingChecked++ - const _i = checkedPositionArr.indexOf(i) - if (_i > -1) { - checkedPositionArr.splice(_i, 1) - if (_i <= pIndex) { - pIndex-- - } - } - } else if (obj[i].halfChecked) { - siblingChecked += 0.5 - } - // objKeys[index] = null; - } - }) - // objKeys = objKeys.filter(i => i); // filter non null; - const parent = obj[parentPosition] - // not check, checked, halfChecked - if (siblingChecked === 0) { - parent.checked = false - parent.halfChecked = false - } else if (siblingChecked === sibling) { - parent.checked = true - parent.halfChecked = false - } else { - parent.halfChecked = true - parent.checked = false - } - loop(parentPosition) - } - loop(checkedPositionArr[pIndex], pIndex) + p.props.title = label } - // console.log(Date.now()-s, objKeys.length, checkIt); + + return p } -function getCheck (treeNodesStates, checkedPositions) { - const halfCheckedKeys = [] - const checkedKeys = [] - const checkedNodes = [] - Object.keys(treeNodesStates).forEach((item) => { - const itemObj = treeNodesStates[item] - if (itemObj.checked) { - checkedKeys.push(itemObj.key) - // checkedNodes.push(getValuePropValue(itemObj.node)); - checkedNodes.push({ ...itemObj, pos: item }) - } else if (itemObj.halfChecked) { - halfCheckedKeys.push(itemObj.key) - } - }) +export function convertDataToTree (h, treeData) { + return vcConvertDataToTree(h, treeData, { processProps }) +} + +/** + * Use `rc-tree` convertTreeToEntities for entities calculation. + * We have additional entities of `valueEntities` + */ +function initWrapper (wrapper) { return { - halfCheckedKeys, checkedKeys, checkedNodes, treeNodesStates, checkedPositions, + ...wrapper, + valueEntities: {}, } } -export function getTreeNodesStates (children, values) { - const checkedPositions = [] - const treeNodesStates = {} - loopAllChildren(children, (item, index, pos, keyOrPos, siblingPosition) => { - treeNodesStates[pos] = { - node: item, - key: keyOrPos, - checked: false, - halfChecked: false, - siblingPosition, - } - if (values.indexOf(getValuePropValue(item)) !== -1) { - treeNodesStates[pos].checked = true - checkedPositions.push(pos) - } - }) +function processEntity (entity, wrapper) { + const value = getPropsData(entity.node).value + entity.value = value - handleCheckState(treeNodesStates, filterParentPosition(checkedPositions.sort()), true) - - return getCheck(treeNodesStates, checkedPositions) + // This should be empty, or will get error message. + const currentEntity = wrapper.valueEntities[value] + if (currentEntity) { + warning( + false, + `Conflict! value of node '${entity.key}' (${value}) has already used by node '${currentEntity.key}'.` + ) + } + wrapper.valueEntities[value] = entity } -// can add extra prop to every node. -export function recursiveCloneChildren (children, cb = ch => ch) { - // return React.Children.map(children, child => { - return Array.from(children).map(child => { - const newChild = cb(child) - if (newChild && newChild.props && newChild.props.children) { - return cloneElement(newChild, { - children: recursiveCloneChildren(newChild.props.children, cb), - }) - } - return newChild - }) -} -// const newChildren = recursiveCloneChildren(children, child => { -// const extraProps = {}; -// if (child && child.type && child.type.xxx) { -// extraProps._prop = true; -// return React.cloneElement(child, extraProps); -// } -// return child; -// }); - -function recursiveGen (children, level = 0) { - return children.map((child, index) => { - const pos = `${level}-${index}` - const props = getAllProps(child) - const { title, label, value, ...rest } = props - const { children: subChildren } = child.componentOptions - const o = { - ...rest, - title, - label: label || title, - value, - key: child.key, - _pos: pos, - } - if (subChildren) { - o.children = recursiveGen(subChildren, pos) - } - return o +export function convertTreeToEntities (treeNodes) { + return vcConvertTreeToEntities(treeNodes, { + initWrapper, + processEntity, }) } -function recursive (children, cb) { - children.forEach(item => { - cb(item) - if (item.children) { - recursive(item.children, cb) +/** + * https://github.com/ant-design/ant-design/issues/13328 + * We need calculate the half check key when searchValue is set. + */ +// TODO: This logic may better move to rc-tree +export function getHalfCheckedKeys (valueList, valueEntities) { + const values = {} + + // Fill checked keys + valueList.forEach(({ value }) => { + values[value] = false + }) + + // Fill half checked keys + valueList.forEach(({ value }) => { + let current = valueEntities[value] + + while (current && current.parent) { + const parentValue = current.parent.value + if (parentValue in values) break + values[parentValue] = true + + current = current.parent } }) + + // Get half keys + return Object.keys(values).filter(value => values[value]).map(value => valueEntities[value].key) } -// Get the tree's checkedNodes (todo: can merge to the `handleCheckState` function) -// If one node checked, it's all children nodes checked. -// If sibling nodes all checked, the parent checked. -export function filterAllCheckedData (vs, treeNodes) { - const vals = [...vs] - if (!vals.length) { - return vals - } - - const data = recursiveGen(treeNodes) - const checkedNodesPositions = [] - - function checkChildren (children) { - children.forEach(item => { - if (item.__checked) { - return - } - const ci = vals.indexOf(item.value) - const childs = item.children - if (ci > -1) { - item.__checked = true - checkedNodesPositions.push({ node: item, pos: item._pos }) - vals.splice(ci, 1) - if (childs) { - recursive(childs, child => { - child.__checked = true - checkedNodesPositions.push({ node: child, pos: child._pos }) - }) - } - } else { - if (childs) { - checkChildren(childs) - } - } - }) - } - - function checkParent (children, parent = { root: true }) { - let siblingChecked = 0 - children.forEach(item => { - const childs = item.children - if (childs && !item.__checked && !item.__halfChecked) { - const p = checkParent(childs, item) - if (p.__checked) { - siblingChecked++ - } else if (p.__halfChecked) { - siblingChecked += 0.5 - } - } else if (item.__checked) { - siblingChecked++ - } else if (item.__halfChecked) { - siblingChecked += 0.5 - } - }) - const len = children.length - if (siblingChecked === len) { - parent.__checked = true - checkedNodesPositions.push({ node: parent, pos: parent._pos }) - } else if (siblingChecked < len && siblingChecked > 0) { - parent.__halfChecked = true - } - if (parent.root) { - return children - } - return parent - } - checkChildren(data) - checkParent(data) - - checkedNodesPositions.forEach((i, index) => { - // clear private metadata - delete checkedNodesPositions[index].node.__checked - delete checkedNodesPositions[index].node._pos - // create the same structure of `onCheck`'s return. - checkedNodesPositions[index].node.props = { - title: checkedNodesPositions[index].node.title, - label: checkedNodesPositions[index].node.label || checkedNodesPositions[index].node.title, - value: checkedNodesPositions[index].node.value, - } - if (checkedNodesPositions[index].node.children) { - checkedNodesPositions[index].node.props.children = checkedNodesPositions[index].node.children - } - delete checkedNodesPositions[index].node.title - delete checkedNodesPositions[index].node.label - delete checkedNodesPositions[index].node.value - delete checkedNodesPositions[index].node.children - }) - return checkedNodesPositions -} - -export function processSimpleTreeData (treeData, format) { - function unflatten2 (array, parent = { [format.id]: format.rootPId }) { - const children = [] - for (let i = 0; i < array.length; i++) { - array[i] = { ...array[i] } // copy, can not corrupts original data - if (array[i][format.pId] === parent[format.id]) { - array[i].key = array[i][format.id] - children.push(array[i]) - array.splice(i--, 1) - } - } - if (children.length) { - parent.children = children - children.forEach(child => unflatten2(array, child)) - } - if (parent[format.id] === format.rootPId) { - return children - } - } - return unflatten2(treeData) -} - -export function saveRef (instance, name) { - if (!instance.saveRefs) { - instance.saveRefs = {} - } - if (!instance.saveRefs[name]) { - instance.saveRefs[name] = (node) => { - instance[name] = node - } - } - return instance.saveRefs[name] -} +export const conductCheck = rcConductCheck diff --git a/components/vc-tree/src/Tree.jsx b/components/vc-tree/src/Tree.jsx index 60f9edbc3..43328ef7c 100644 --- a/components/vc-tree/src/Tree.jsx +++ b/components/vc-tree/src/Tree.jsx @@ -101,6 +101,8 @@ const Tree = { }), data () { + warning(this.$props.__propsSymbol__, 'must pass __propsSymbol__') + warning(this.$props.children, 'please children prop replace slots.default') this.needSyncKeys = {} const state = { _posEntities: {}, @@ -119,12 +121,6 @@ const Tree = { } return { ...state, - // ...this.getSyncProps(props), - // dragOverNodeKey: '', - // dropPosition: null, - // dragNodesKeys: [], - // sLoadedKeys: [], - // sLoadingKeys: [], ...this.getDerivedStateFromProps(getOptionProps(this), state), } }, @@ -478,7 +474,7 @@ const Tree = { event: 'load', node: treeNode, } - this.__emit('load', eventObj) + this.__emit('load', newLoadedKeys, eventObj) this.setUncontrolledState({ _loadedKeys: newLoadedKeys, }) @@ -593,7 +589,6 @@ const Tree = { return cloneElement(child, { props: { - key, eventKey: key, expanded: expandedKeys.indexOf(key) !== -1, selected: selectedKeys.indexOf(key) !== -1, @@ -608,6 +603,7 @@ const Tree = { dragOverGapTop: dragOverNodeKey === key && dropPosition === -1, dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1, }, + key, }) }, }, diff --git a/components/vc-tree/src/TreeNode.jsx b/components/vc-tree/src/TreeNode.jsx index bf10e6c4f..b16fa1a3d 100644 --- a/components/vc-tree/src/TreeNode.jsx +++ b/components/vc-tree/src/TreeNode.jsx @@ -46,6 +46,9 @@ const TreeNode = { icon: PropTypes.any, dataRef: PropTypes.object, switcherIcon: PropTypes.any, + + label: PropTypes.any, + value: PropTypes.any, }, {}), data () { diff --git a/components/vc-trigger/Popup.jsx b/components/vc-trigger/Popup.jsx index 454cdbad7..a7b1d7ff8 100644 --- a/components/vc-trigger/Popup.jsx +++ b/components/vc-trigger/Popup.jsx @@ -4,8 +4,10 @@ import Align from '../vc-align' import PopupInner from './PopupInner' import LazyRenderBox from './LazyRenderBox' import animate from '../_util/css-animation' +import BaseMixin from '../_util/BaseMixin' export default { + mixins: [BaseMixin], props: { visible: PropTypes.bool, getClassNameFromAlign: PropTypes.func, @@ -165,7 +167,7 @@ export default { // Delay force align to makes ui smooth if (!stretchChecked) { - sizeStyle.visibility = 'hidden' + // sizeStyle.visibility = 'hidden' setTimeout(() => { if (this.$refs.alignInstance) { this.$refs.alignInstance.forceAlign()