diff --git a/components/_util/props-util.js b/components/_util/props-util.js index a16c3059b..9fddb4427 100644 --- a/components/_util/props-util.js +++ b/components/_util/props-util.js @@ -117,6 +117,16 @@ const getComponentFromProp = (instance, prop) => { } } +const getAllProps = (ele) => { + let data = ele.data || {} + let componentOptions = ele.componentOptions || {} + if (ele.$vnode) { + data = ele.$vnode.data || {} + componentOptions = ele.$vnode.componentOptions || {} + } + return { ...data.props, ...data.attrs, ...componentOptions.propsData } +} + const getPropsData = (ele) => { let componentOptions = ele.componentOptions if (ele.$vnode) { @@ -247,5 +257,6 @@ export { isValidElement, camelize, getSlots, + getAllProps, } export default hasProp diff --git a/components/auto-complete/index.jsx b/components/auto-complete/index.jsx index e7dcc99ef..f6de0de77 100644 --- a/components/auto-complete/index.jsx +++ b/components/auto-complete/index.jsx @@ -21,7 +21,7 @@ import { getComponentFromProp, getOptionProps, filterEmpty, isValidElement } fro // } const AutoCompleteProps = { - ...AbstractSelectProps, + ...AbstractSelectProps(), value: SelectValue, defaultValue: SelectValue, dataSource: PropTypes.array, diff --git a/components/index.js b/components/index.js index a9779713a..5e6fce8f8 100644 --- a/components/index.js +++ b/components/index.js @@ -102,7 +102,7 @@ import { default as Transfer } from './transfer' import { default as Tree } from './tree' -// import { default as TreeSelect } from './tree-select' +import { default as TreeSelect } from './tree-select' import { default as Tabs } from './tabs' @@ -196,6 +196,8 @@ const components = [ Transfer, Tree, Tree.TreeNode, + TreeSelect, + TreeSelect.TreeNode, Tabs, Tabs.TabPane, Tag, @@ -269,6 +271,7 @@ export { Table, Transfer, Tree, + TreeSelect, Tabs, Tag, TimePicker, diff --git a/components/select/index.jsx b/components/select/index.jsx index 26b779062..7b8563293 100644 --- a/components/select/index.jsx +++ b/components/select/index.jsx @@ -5,7 +5,7 @@ import LocaleReceiver from '../locale-provider/LocaleReceiver' import defaultLocale from '../locale-provider/default' import { getComponentFromProp, getOptionProps, filterEmpty } from '../_util/props-util' -const AbstractSelectProps = { +const AbstractSelectProps = () => ({ prefixCls: PropTypes.string, size: PropTypes.oneOf(['small', 'large', 'default']), notFoundContent: PropTypes.any, @@ -28,7 +28,7 @@ const AbstractSelectProps = { autoFocus: PropTypes.bool, backfill: PropTypes.bool, showArrow: PropTypes.bool, -} +}) const Value = PropTypes.shape({ key: PropTypes.string, }).loose @@ -45,7 +45,7 @@ const SelectValue = PropTypes.oneOfType([ ]) const SelectProps = { - ...AbstractSelectProps, + ...AbstractSelectProps(), value: SelectValue, defaultValue: SelectValue, mode: PropTypes.oneOf(['default', 'multiple', 'tags', 'combobox']), diff --git a/components/style.js b/components/style.js index 640ee4c18..416939770 100644 --- a/components/style.js +++ b/components/style.js @@ -46,3 +46,4 @@ import './layout/style' import './form/style' import './anchor/style' import './list/style' +import './tree-select/style' diff --git a/components/tree-select/__tests__/__snapshots__/demo.test.js.snap b/components/tree-select/__tests__/__snapshots__/demo.test.js.snap new file mode 100644 index 000000000..42dac6459 --- /dev/null +++ b/components/tree-select/__tests__/__snapshots__/demo.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders ./components/tree-select/demo/basic.md correctly 1`] = ` +Please select + +`; + +exports[`renders ./components/tree-select/demo/checkable.md correctly 1`] = ` +
  • Node1
  • + +
    +
    +`; + +exports[`renders ./components/tree-select/demo/multiple.md correctly 1`] = ` +
    +
    Please select
    +
    +`; + +exports[`renders ./components/tree-select/demo/treeData.md correctly 1`] = ` +Please select + +`; diff --git a/components/tree-select/__tests__/demo.test.js b/components/tree-select/__tests__/demo.test.js new file mode 100644 index 000000000..1fc1e0b7d --- /dev/null +++ b/components/tree-select/__tests__/demo.test.js @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest' + +demoTest('tree-select') diff --git a/components/tree-select/__tests__/index.test.js b/components/tree-select/__tests__/index.test.js new file mode 100644 index 000000000..b1c6d668b --- /dev/null +++ b/components/tree-select/__tests__/index.test.js @@ -0,0 +1,6 @@ +import TreeSelect from '..' +import focusTest from '../../../tests/shared/focusTest' + +describe('TreeSelect', () => { + focusTest(TreeSelect) +}) diff --git a/components/tree-select/demo/basic.md b/components/tree-select/demo/basic.md new file mode 100644 index 000000000..735280343 --- /dev/null +++ b/components/tree-select/demo/basic.md @@ -0,0 +1,53 @@ + +#### 基本用法 +最简单的用法。 + + + +#### Basic +The most basic usage. + + +```html + + + +``` diff --git a/components/tree-select/demo/checkable.md b/components/tree-select/demo/checkable.md new file mode 100644 index 000000000..f2ff511ce --- /dev/null +++ b/components/tree-select/demo/checkable.md @@ -0,0 +1,73 @@ + +#### 可勾选 +使用勾选框实现多选功能。 + + + +#### Checkable +Multiple and checkable. + + +```html + + + +``` diff --git a/components/tree-select/demo/index.vue b/components/tree-select/demo/index.vue new file mode 100644 index 000000000..a0e0f736d --- /dev/null +++ b/components/tree-select/demo/index.vue @@ -0,0 +1,48 @@ + diff --git a/components/tree-select/demo/multiple.md b/components/tree-select/demo/multiple.md new file mode 100644 index 000000000..3fcf57a50 --- /dev/null +++ b/components/tree-select/demo/multiple.md @@ -0,0 +1,62 @@ + +#### 多选 +多选的树选择。 + + + +#### Multiple Selection +Multiple selection usage. + + +```html + + + +``` diff --git a/components/tree-select/demo/treeData.md b/components/tree-select/demo/treeData.md new file mode 100644 index 000000000..e207ce136 --- /dev/null +++ b/components/tree-select/demo/treeData.md @@ -0,0 +1,63 @@ + +#### 从数据直接生成 +使用 `treeData` 把 JSON 数据直接生成树结构。 + + + +#### Generate form tree data +The tree structure can be populated using `treeData` property. This is a quick and easy way to provide the tree content. + + +```html + + + +``` diff --git a/components/tree-select/index.en-US.md b/components/tree-select/index.en-US.md new file mode 100644 index 000000000..0383beeac --- /dev/null +++ b/components/tree-select/index.en-US.md @@ -0,0 +1,61 @@ + +## API + +### Tree props + +| Property | Description | Type | Default | +| -------- | ----------- | ---- | ------- | +| allowClear | Whether allow clear | boolean | false | +| defaultValue | To set the initial selected treeNode(s). | string\|string\[] | - | +| disabled | Disabled or not | boolean | false | +| dropdownClassName | className of dropdown menu | string | - | +| dropdownMatchSelectWidth | Determine whether the dropdown menu and the select input are the same width | boolean | true | +| dropdownStyle | To set the style of the dropdown menu | object | - | +| filterTreeNode | Whether to filter treeNodes by input value. The value of `treeNodeFilterProp` is used for filtering by default. | boolean\|Function(inputValue: string, treeNode: TreeNode) (should return boolean) | Function | +| getPopupContainer | To set the container of the dropdown menu. The default is to create a `div` element in `body`, you can reset it to the scrolling area and make a relative reposition. [example](https://codepen.io/afc163/pen/zEjNOy?editors=0010) | Function(triggerNode) | () => document.body | +| labelInValue | whether to embed label in value, turn the format of value from `string` to `{value: string, label: VNode, halfChecked: string[]}` | boolean | false | +| loadData | Load data asynchronously. | function(node) | - | +| multiple | Support multiple or not, will be `true` when enable `treeCheckable`. | boolean | false | +| placeholder | Placeholder of the select input | string\|slot | - | +| searchPlaceholder | Placeholder of the search input | string\|slot | - | +| 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' | +| 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\[] | - | +| 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\[] | - | + + +### Events +| Events Name | Description | Arguments | +| --- | --- | --- | +| 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) | + +### Tree Methods + +| Name | Description | +| ---- | ----------- | +| blur() | remove focus | +| focus() | get focus | + +### TreeNode props + +> We recommend you to use `treeData` rather than `TreeNode`, to avoid the trouble of manual construction. + +| Property | Description | Type | Default | +| -------- | ----------- | ---- | ------- | +| disableCheckbox | Disables the checkbox of the treeNode | boolean | false | +| disabled | Disabled or not | boolean | false | +| isLeaf | Leaf node or not | boolean | false | +| key | Required property, should be unique in the tree | string | - | +| title | Content showed on the treeNodes | string\|slot | '---' | +| value | Will be treated as `treeNodeFilterProp` by default, should be unique in the tree | string | - | +| scopedSlots | When using treeNodes, you can use this property to configure the properties that support the slot, such as `scopedSlots: { title: 'XXX'}` | object | - | diff --git a/components/tree-select/index.jsx b/components/tree-select/index.jsx new file mode 100644 index 000000000..1a00fb6a0 --- /dev/null +++ b/components/tree-select/index.jsx @@ -0,0 +1,115 @@ + +import VcTreeSelect, { TreeNode, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from '../vc-tree-select' +import classNames from 'classnames' +import { TreeSelectProps } from './interface' +import LocaleReceiver from '../locale-provider/LocaleReceiver' +import warning from '../_util/warning' +import { initDefaultProps, getOptionProps, getComponentFromProp, filterEmpty } from '../_util/props-util' + +export { TreeData, TreeSelectProps } from './interface' + +export default { + TreeNode: { ...TreeNode, name: 'ATreeSelectNode' }, + SHOW_ALL, + SHOW_PARENT, + SHOW_CHILD, + name: 'ATreeSelect', + props: initDefaultProps(TreeSelectProps(), { + prefixCls: 'ant-select', + transitionName: 'slide-up', + choiceTransitionName: 'zoom', + showSearch: false, + }), + model: { + prop: 'value', + event: 'change', + }, + + created () { + warning( + this.multiple !== false || !this.treeCheckable, + '`multiple` will alway be `true` when `treeCheckable` is true', + ) + }, + methods: { + focus () { + this.$refs.vcTreeSelect.focus() + }, + + blur () { + this.$refs.vcTreeSelect.blur() + }, + onChange () { + this.$emit('change', ...arguments) + }, + updateTreeData (list = []) { + for (let i = 0, len = list.length; i < len; i++) { + const { label, title, scopedSlots = {}, children } = list[i] + const { $scopedSlots } = this + let newLabel = typeof label === 'function' ? label(this.$createElement) : label + let newTitle = typeof title === 'function' ? title(this.$createElement) : title + if (!newLabel && scopedSlots.label && $scopedSlots[scopedSlots.label]) { + newLabel = $scopedSlots.label(list[i]) + } + if (!newTitle && scopedSlots.title && $scopedSlots[scopedSlots.title]) { + newTitle = $scopedSlots.title(list[i]) + } + const item = { + // label: newLabel, + title: newTitle || newLabel, + } + this.updateTreeData(children) + Object.assign(list[i], item) + } + }, + renderTreeSelect (locale) { + const props = getOptionProps(this) + const { + prefixCls, + size, + notFoundContent, + dropdownStyle, + dropdownClassName, + ...restProps + } = props + this.updateTreeData(props.treeData) + const cls = { + [`${prefixCls}-lg`]: size === 'large', + [`${prefixCls}-sm`]: size === 'small', + } + + let checkable = getComponentFromProp(this, 'treeCheckable') + if (checkable) { + checkable = + } + const VcTreeSelectProps = { + props: { + ...restProps, + dropdownClassName: classNames(dropdownClassName, `${prefixCls}-tree-dropdown`), + prefixCls, + dropdownStyle: { maxHeight: '100vh', overflow: 'auto', ...dropdownStyle }, + treeCheckable: checkable, + notFoundContent: notFoundContent || locale.notFoundContent, + }, + class: cls, + on: { ...this.$listeners, change: this.onChange }, + ref: 'vcTreeSelect', + } + return ( + {filterEmpty(this.$slots.default)} + ) + }, + }, + + render () { + return ( + + ) + }, +} diff --git a/components/tree-select/index.zh-CN.md b/components/tree-select/index.zh-CN.md new file mode 100644 index 000000000..a2a911675 --- /dev/null +++ b/components/tree-select/index.zh-CN.md @@ -0,0 +1,61 @@ + +## API + +### Tree props + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| allowClear | 显示清除按钮 | boolean | false | +| defaultValue | 指定默认选中的条目 | string/string\[] | - | +| disabled | 是否禁用 | boolean | false | +| dropdownClassName | 下拉菜单的 className 属性 | string | - | +| dropdownMatchSelectWidth | 下拉菜单和选择器同宽 | boolean | true | +| dropdownStyle | 下拉菜单的样式 | object | - | +| filterTreeNode | 是否根据输入项进行筛选,默认用 treeNodeFilterProp 的值作为要筛选的 TreeNode 的属性值 | boolean\|Function(inputValue: string, treeNode: TreeNode) (函数需要返回bool值) | Function | +| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。 | Function(triggerNode) | () => document.body | +| labelInValue | 是否把每个选项的 label 包装到 value 中,会把 value 类型从 `string` 变为 `{value: string, label: VNode, halfChecked(treeCheckStrictly 时有效): string[] }` 的格式 | boolean | false | +| loadData | 异步加载数据 | function(node) | - | +| multiple | 支持多选(当设置 treeCheckable 时自动变为true) | boolean | false | +| placeholder | 选择框默认文字 | string\|slot | - | +| searchPlaceholder | 搜索框默认文字 | string\|slot | - | +| 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' | +| 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\[] | - | +| treeNodeFilterProp | 输入项过滤对应的 treeNode 属性 | string | 'value' | +| treeNodeLabelProp | 作为显示的 prop 设置 | string | 'title' | +| value(v-model) | 指定当前选中的条目 | string/string\[] | - | + +### 事件 + +| 事件名称 | 说明 | 回调参数 | +| --- | --- | --- | +| change | 选中树节点时调用此函数 | function(value, label, extra) | - | +| search | 文本框值变化时回调 | function(value: string) | - | +| select | 被选中时调用 | function(value, node, extra) | - | + +### Tree 方法 + +| 名称 | 描述 | +| --- | --- | +| blur() | 移除焦点 | +| focus() | 获取焦点 | + +### TreeNode props + +> 建议使用 treeData 来代替 TreeNode,免去手工构造麻烦 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| disableCheckbox | 禁掉 checkbox | boolean | false | +| disabled | 是否禁用 | boolean | false | +| isLeaf | 是否是叶子节点 | boolean | false | +| key | 此项必须设置(其值在整个树范围内唯一) | string | - | +| title | 树节点显示的内容 | string\|slot | '---' | +| value | 默认根据此属性值进行筛选(其值在整个树范围内唯一) | string | - | +| scopedSlots | 使用treeData时,可以通过该属性配置支持slot的属性,如 `scopedSlots: { title: 'XXX'}` | object | - | diff --git a/components/tree-select/interface.jsx b/components/tree-select/interface.jsx new file mode 100644 index 000000000..23564b3cc --- /dev/null +++ b/components/tree-select/interface.jsx @@ -0,0 +1,37 @@ +import PropTypes from '../_util/vue-types' +import { AbstractSelectProps } from '../select' + +export const TreeData = PropTypes.shape({ + key: PropTypes.string, + value: PropTypes.string, + label: PropTypes.any, + scopedSlots: PropTypes.object, + children: PropTypes.array, +}).loose + +export const TreeSelectProps = () => ({ + ...AbstractSelectProps(), + value: PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.array]), + defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + multiple: PropTypes.bool, + // onSelect: (value: any) => void, + // 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, + treeCheckable: PropTypes.bool, + treeDefaultExpandedKeys: PropTypes.arrayOf(String), + filterTreeNode: PropTypes.func, + treeNodeFilterProp: PropTypes.string, + treeNodeLabelProp: PropTypes.string, + 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, +}) diff --git a/components/tree-select/style/index.js b/components/tree-select/style/index.js new file mode 100644 index 000000000..daa33e949 --- /dev/null +++ b/components/tree-select/style/index.js @@ -0,0 +1,6 @@ +import '../../style/index.less' +import './index.less' + +// style dependencies +import '../../select/style' +import '../../checkbox/style' diff --git a/components/tree-select/style/index.less b/components/tree-select/style/index.less new file mode 100644 index 000000000..8048e8086 --- /dev/null +++ b/components/tree-select/style/index.less @@ -0,0 +1,145 @@ +@import "../../style/themes/default"; +@import "../../style/mixins/index"; +@import "../../tree/style/mixin"; +@import "../../checkbox/style/mixin"; + +@select-prefix-cls: ~"@{ant-prefix}-select"; +@select-tree-prefix-cls: ~"@{ant-prefix}-select-tree"; + +.antCheckboxFn(@checkbox-prefix-cls: ~"@{ant-prefix}-select-tree-checkbox"); + +.@{select-tree-prefix-cls} { + .reset-component; + margin: 0; + padding: 0 4px; + margin-top: -4px; + li { + padding: 0; + margin: 8px 0; + list-style: none; + white-space: nowrap; + outline: 0; + &.filter-node { + > span { + font-weight: 500; + } + } + ul { + margin: 0; + padding: 0 0 0 18px; + } + .@{select-tree-prefix-cls}-node-content-wrapper { + display: inline-block; + padding: 3px 5px; + border-radius: 2px; + margin: 0; + cursor: pointer; + text-decoration: none; + color: @text-color; + transition: all .3s; + width: ~"calc(100% - 24px)"; + &:hover { + background-color: @item-hover-bg; + } + &.@{select-tree-prefix-cls}-node-selected { + background-color: @primary-2; + } + } + span { + &.@{select-tree-prefix-cls}-checkbox { + margin: 0 4px 0 0; + + .@{select-tree-prefix-cls}-node-content-wrapper { + width: ~"calc(100% - 46px)"; + } + } + &.@{select-tree-prefix-cls}-switcher, + &.@{select-tree-prefix-cls}-iconEle { + margin: 0; + width: 24px; + height: 24px; + line-height: 22px; + display: inline-block; + vertical-align: middle; + border: 0 none; + cursor: pointer; + outline: none; + text-align: center; + } + &.@{select-tree-prefix-cls}-icon_loading { + &:after { + display: inline-block; + .iconfont-font("\e6ae"); + animation: loadingCircle 1s infinite linear; + color: @primary-color; + } + } + &.@{select-tree-prefix-cls}-switcher { + &.@{select-tree-prefix-cls}-switcher-noop { + cursor: auto; + } + &.@{select-tree-prefix-cls}-switcher_open { + .antTreeSwitcherIcon(); + } + &.@{select-tree-prefix-cls}-switcher_close { + .antTreeSwitcherIcon(); + &:after { + transform: rotate(270deg) scale(0.59); + } + } + } + } + } + &-child-tree { + display: none; + &-open { + display: block; + } + } + li&-treenode-disabled { + > span:not(.@{select-tree-prefix-cls}-switcher), + > .@{select-tree-prefix-cls}-node-content-wrapper, + > .@{select-tree-prefix-cls}-node-content-wrapper span { + color: @disabled-color; + cursor: not-allowed; + } + > .@{select-tree-prefix-cls}-node-content-wrapper:hover { + background: transparent; + } + } + &-icon__open { + margin-right: 2px; + vertical-align: top; + } + &-icon__close { + margin-right: 2px; + vertical-align: top; + } +} + +.@{select-prefix-cls}-tree-dropdown { + .reset-component; + .@{select-prefix-cls}-dropdown-search { + display: block; + padding: 4px; + .@{select-prefix-cls}-search__field__wrap { + width: 100%; + } + .@{select-prefix-cls}-search__field { + padding: 4px 7px; + width: 100%; + box-sizing: border-box; + border: @border-width-base @border-style-base @border-color-base; + border-radius: 4px; + outline: none; + } + &.@{select-prefix-cls}-search--hide { + display: none; + } + } + .@{select-prefix-cls}-not-found { + cursor: not-allowed; + color: @disabled-color; + padding: 7px 16px; + display: block; + } +} diff --git a/components/vc-tree-select/assets/icons.png b/components/vc-tree-select/assets/icons.png new file mode 100644 index 000000000..ffda01ef1 Binary files /dev/null and b/components/vc-tree-select/assets/icons.png differ diff --git a/components/vc-tree-select/assets/index.less b/components/vc-tree-select/assets/index.less new file mode 100644 index 000000000..e02a9c033 --- /dev/null +++ b/components/vc-tree-select/assets/index.less @@ -0,0 +1,2 @@ +@import "./select.less"; +@import "./tree.less"; diff --git a/components/vc-tree-select/assets/loading.gif b/components/vc-tree-select/assets/loading.gif new file mode 100644 index 000000000..e8c289293 Binary files /dev/null and b/components/vc-tree-select/assets/loading.gif differ diff --git a/components/vc-tree-select/assets/minus.gif b/components/vc-tree-select/assets/minus.gif new file mode 100644 index 000000000..d561d36a9 Binary files /dev/null and b/components/vc-tree-select/assets/minus.gif differ diff --git a/components/vc-tree-select/assets/select.less b/components/vc-tree-select/assets/select.less new file mode 100644 index 000000000..7efc6c2e4 --- /dev/null +++ b/components/vc-tree-select/assets/select.less @@ -0,0 +1,500 @@ +@selectPrefixCls: rc-tree-select; + +.effect() { + animation-duration: .3s; + animation-fill-mode: both; + transform-origin: 0 0; +} + +.@{selectPrefixCls} { + box-sizing: border-box; + display: inline-block; + position: relative; + vertical-align: middle; + color: #666; + + &-allow-clear { + .@{selectPrefixCls}-selection--single .@{selectPrefixCls}-selection__rendered { + padding-right: 40px; + } + } + + ul, li { + margin: 0; + padding: 0; + list-style: none; + } + + > ul > li > a { + padding: 0; + background-color: #fff; + } + + // arrow + &-arrow { + height: 26px; + position: absolute; + top: 1px; + right: 1px; + width: 20px; + b { + border-color: #999999 transparent transparent transparent; + border-style: solid; + border-width: 5px 4px 0 4px; + height: 0; + width: 0; + margin-left: -4px; + margin-top: -2px; + position: absolute; + top: 50%; + left: 50%; + } + } + + &-selection { + outline: none; + user-select: none; + -webkit-user-select: none; + + box-sizing: border-box; + display: block; + + background-color: #fff; + border-radius: 6px; + border: 1px solid #d9d9d9; + + &__clear { + font-weight: bold; + position: absolute; + + &:after { + content: '×' + } + } + } + + &-enabled &-selection { + &:hover { + border-color: #23c0fa; + box-shadow: 0 0 2px fadeout(#2db7f5, 20%); + } + &:active { + border-color: #2db7f5; + } + } + + &-selection--single { + height: 28px; + cursor: pointer; + position: relative; + + .@{selectPrefixCls}-selection__rendered { + display: block; + padding-left: 10px; + padding-right: 20px; + line-height: 28px; + } + + .@{selectPrefixCls}-selection-selected-value { + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .@{selectPrefixCls}-selection__clear { + top: 5px; + right: 20px; + } + } + + &-disabled { + color: #ccc; + cursor: not-allowed; + + .@{selectPrefixCls}-selection--single, + .@{selectPrefixCls}-selection__choice__remove { + cursor: not-allowed; + color: #ccc; + + &:hover { + cursor: not-allowed; + color: #ccc; + } + } + } + + &-search__field__wrap { + display: inline-block; + position: relative; + } + + &-search__field__placeholder { + position: absolute; + top: 0; + left: 3px; + color: #aaa; + } + + &-search__field__mirror { + position: absolute; + top: 0; + left: -9999px; + white-space: pre; + pointer-events: none; + } + + &-search--inline { + float: left; + width: 100%; + .@{selectPrefixCls}-search__field__wrap { + width: 100%; + } + .@{selectPrefixCls}-search__field { + border: none; + font-size: 100%; + //margin-top: 5px; + background: transparent; + outline: 0; + width: 100%; + } + > i { + float: right; + } + } + + &-enabled&-selection--multiple { + cursor: text; + } + + &-selection--multiple { + min-height: 28px; + + .@{selectPrefixCls}-search--inline { + width: auto; + .@{selectPrefixCls}-search__field { + width: 0.75em; + } + } + + .@{selectPrefixCls}-search__field__placeholder { + top: 5px; + left: 8px; + } + + .@{selectPrefixCls}-selection__rendered { + //display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + padding-left: 8px; + padding-bottom: 2px; + padding-right: 10px; + } + + > ul > li { + margin-top: 4px; + height: 20px; + line-height: 20px; + } + + .@{selectPrefixCls}-selection__clear { + top: 5px; + right: 8px; + } + } + + &-enabled { + .@{selectPrefixCls}-selection__choice { + cursor: default; + &:hover { + .@{selectPrefixCls}-selection__choice__remove { + opacity: 1; + transform: scale(1); + } + .@{selectPrefixCls}-selection__choice__remove + + .@{selectPrefixCls}-selection__choice__content { + margin-left: -8px; + margin-right: 8px; + } + } + } + } + + & &-selection__choice { + background-color: #f3f3f3; + border-radius: 4px; + float: left; + padding: 0 15px; + margin-right: 4px; + position: relative; + overflow: hidden; + transition: padding .3s cubic-bezier(0.6, -0.28, 0.735, 0.045), width .3s cubic-bezier(0.6, -0.28, 0.735, 0.045); + + &__content { + margin-left: 0; + margin-right: 0; + transition: margin .3s cubic-bezier(0.165, 0.84, 0.44, 1); + } + + &-zoom-enter, &-zoom-appear, &-zoom-leave { + .effect(); + opacity: 0; + animation-play-state: paused; + animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); + } + + &-zoom-leave { + opacity: 1; + animation-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045); + } + + &-zoom-enter.@{selectPrefixCls}-selection__choice-zoom-enter-active, + &-zoom-appear.@{selectPrefixCls}-selection__choice-zoom-appear-active { + animation-play-state: running; + animation-name: rcSelectChoiceZoomIn; + } + + &-zoom-leave.@{selectPrefixCls}-selection__choice-zoom-leave-active { + animation-play-state: running; + animation-name: rcSelectChoiceZoomOut; + } + + @keyframes rcSelectChoiceZoomIn { + 0% { + transform: scale(0.6); + opacity: 0; + } + 100% { + transform: scale(1); + opacity: 1; + } + } + + @keyframes rcSelectChoiceZoomOut { + to { + transform: scale(0); + opacity: 0; + } + } + + &__remove { + color: #919191; + cursor: pointer; + font-weight: bold; + padding: 0 0 0 8px; + position: absolute; + opacity: 0; + transform: scale(0); + top: 0; + right: 2px; + transition: opacity .3s, transform .3s; + &:before { + content: '×' + } + + &:hover { + color: #333; + } + } + } + + &-dropdown { + background-color: white; + border: 1px solid #d9d9d9; + box-shadow: 0 0px 4px #d9d9d9; + border-radius: 4px; + box-sizing: border-box; + z-index: 100; + left: -9999px; + top: -9999px; + //border-top: none; + //border-top-left-radius: 0; + //border-top-right-radius: 0; + position: absolute; + outline: none; + + &-hidden { + display: none; + } + + &-menu { + outline: none; + margin: 0; + padding: 0; + list-style: none; + z-index: 9999; + + > li { + margin: 0; + padding: 0; + } + + &-item-group-list { + margin: 0; + padding: 0; + + > li.@{selectPrefixCls}-menu-item { + padding-left: 20px; + } + } + + &-item-group-title { + color: #999; + line-height: 1.5; + padding: 8px 10px; + border-bottom: 1px solid #dedede; + } + + li&-item { + margin: 0; + position: relative; + display: block; + padding: 7px 10px; + font-weight: normal; + color: #666666; + white-space: nowrap; + + &-selected { + background-color: #ddd; + } + + &-active { + background-color: #5897fb; + color: white; + cursor: pointer; + } + + &-disabled { + color: #ccc; + cursor: not-allowed; + } + + &-divider { + height: 1px; + margin: 1px 0; + overflow: hidden; + background-color: #e5e5e5; + line-height: 0; + } + } + } + + &-slide-up-enter, &-slide-up-appear { + .effect(); + opacity: 0; + animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); + animation-play-state: paused; + } + + &-slide-up-leave { + .effect(); + opacity: 1; + animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34); + animation-play-state: paused; + } + + &-slide-up-enter&-slide-up-enter-active&-placement-bottomLeft, &-slide-up-appear&-slide-up-appear-active&-placement-bottomLeft { + animation-name: rcSelectDropdownSlideUpIn; + animation-play-state: running; + } + + &-slide-up-leave&-slide-up-leave-active&-placement-bottomLeft { + animation-name: rcSelectDropdownSlideUpOut; + animation-play-state: running; + } + + &-slide-up-enter&-slide-up-enter-active&-placement-topLeft, &-slide-up-appear&-slide-up-appear-active&-placement-topLeft { + animation-name: rcSelectDropdownSlideDownIn; + animation-play-state: running; + } + + &-slide-up-leave&-slide-up-leave-active&-placement-topLeft { + animation-name: rcSelectDropdownSlideDownOut; + animation-play-state: running; + } + + @keyframes rcSelectDropdownSlideUpIn { + 0% { + opacity: 0; + transform-origin: 0% 0%; + transform: scaleY(0); + } + 100% { + opacity: 1; + transform-origin: 0% 0%; + transform: scaleY(1); + } + } + @keyframes rcSelectDropdownSlideUpOut { + 0% { + opacity: 1; + transform-origin: 0% 0%; + transform: scaleY(1); + } + 100% { + opacity: 0; + transform-origin: 0% 0%; + transform: scaleY(0); + } + } + + @keyframes rcSelectDropdownSlideDownIn { + 0% { + opacity: 0; + transform-origin: 0% 100%; + transform: scaleY(0); + } + 100% { + opacity: 1; + transform-origin: 0% 100%; + transform: scaleY(1); + } + } + @keyframes rcSelectDropdownSlideDownOut { + 0% { + opacity: 1; + transform-origin: 0% 100%; + transform: scaleY(1); + } + 100% { + opacity: 0; + transform-origin: 0% 100%; + transform: scaleY(0); + } + } + } + + &-dropdown-search { + display: block; + padding: 4px; + .@{selectPrefixCls}-search__field__wrap { + width: 100%; + } + .@{selectPrefixCls}-search__field__placeholder { + top: 4px; + } + .@{selectPrefixCls}-search__field { + padding: 4px; + width: 100%; + box-sizing: border-box; + border: 1px solid #d9d9d9; + border-radius: 4px; + outline: none; + } + &.@{selectPrefixCls}-search--hide { + display: none; + } + } + + &-open { + .@{selectPrefixCls}-arrow b { + border-color: transparent transparent #888 transparent; + border-width: 0 4px 5px 4px; + } + } + + &-not-found { + display: inline-block; + padding: 8px; + } +} diff --git a/components/vc-tree-select/assets/tree.less b/components/vc-tree-select/assets/tree.less new file mode 100644 index 000000000..50744851a --- /dev/null +++ b/components/vc-tree-select/assets/tree.less @@ -0,0 +1,164 @@ +@treePrefixCls: rc-tree-select-tree; +.@{treePrefixCls} { + margin: 0; + padding: 5px; + li { + padding: 0; + margin: 0; + list-style: none; + white-space: nowrap; + outline: 0; + a[draggable], + a[draggable="true"] { + color: #333; + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; + user-select: none; + /* Required to make elements draggable in old WebKit */ + -khtml-user-drag: element; + -webkit-user-drag: element; + } + &.drag-over { + > a[draggable] { + background-color: #316ac5; + color: white; + border: 1px #316ac5 solid; + opacity: 0.8; + } + } + &.drag-over-gap-top { + > a[draggable] { + border-top: 2px blue solid; + } + } + &.drag-over-gap-bottom { + > a[draggable] { + border-bottom: 2px blue solid; + } + } + &.filter-node { + > a { + color: #a60000!important; + font-weight: bold!important; + } + } + ul { + margin: 0; + padding: 0 0 0 18px; + &.@{treePrefixCls}-line { + background: url("data:image/gif;base64,R0lGODlhCQACAIAAAMzMzP///yH5BAEAAAEALAAAAAAJAAIAAAIEjI9pUAA7") 0 0 repeat-y; + } + } + a { + display: inline-block; + padding: 1px 3px 0 0; + margin: 0; + cursor: pointer; + height: 17px; + text-decoration: none; + vertical-align: top; + } + span { + &.@{treePrefixCls}-switcher, + &.@{treePrefixCls}-checkbox, + &.@{treePrefixCls}-iconEle { + line-height: 16px; + margin-right: 2px; + width: 16px; + height: 16px; + display: inline-block; + vertical-align: middle; + border: 0 none; + cursor: pointer; + outline: none; + background-color: transparent; + background-repeat: no-repeat; + background-attachment: scroll; + background-image: url("data:image/png;base64,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"); + } + &.@{treePrefixCls}-icon_loading { + margin-right: 2px; + vertical-align: top; + background: url("data:image/gif;base64,R0lGODlhEAAQAKIGAMLY8YSx5HOm4Mjc88/g9Ofw+v///wAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgAGACwAAAAAEAAQAAADMGi6RbUwGjKIXCAA016PgRBElAVlG/RdLOO0X9nK61W39qvqiwz5Ls/rRqrggsdkAgAh+QQFCgAGACwCAAAABwAFAAADD2hqELAmiFBIYY4MAutdCQAh+QQFCgAGACwGAAAABwAFAAADD1hU1kaDOKMYCGAGEeYFCQAh+QQFCgAGACwKAAIABQAHAAADEFhUZjSkKdZqBQG0IELDQAIAIfkEBQoABgAsCgAGAAUABwAAAxBoVlRKgyjmlAIBqCDCzUoCACH5BAUKAAYALAYACgAHAAUAAAMPaGpFtYYMAgJgLogA610JACH5BAUKAAYALAIACgAHAAUAAAMPCAHWFiI4o1ghZZJB5i0JACH5BAUKAAYALAAABgAFAAcAAAMQCAFmIaEp1motpDQySMNFAgA7") no-repeat scroll 0 0 transparent; + } + &.@{treePrefixCls}-switcher { + &-noop { + cursor: auto; + background: none; + } + &_open { + background-position: -93px -56px; + } + &_close { + background-position: -75px -56px; + } + } + &.@{treePrefixCls}-checkbox { + width: 13px; + height: 13px; + margin: 0 3px; + background-position: 0 0; + &-checked { + background-position: -14px 0; + } + &-indeterminate { + background-position: -14px -28px; + } + &-disabled { + background-position: 0 -56px; + } + &.@{treePrefixCls}-checkbox-checked.@{treePrefixCls}-checkbox-disabled { + background-position: -14px -56px; + } + &.@{treePrefixCls}-checkbox-indeterminate.@{treePrefixCls}-checkbox-disabled { + position: relative; + background: #ccc; + border-radius: 3px; + &::after { + content: ' '; + -webkit-transform: scale(1); + transform: scale(1); + position: absolute; + left: 3px; + top: 5px; + width: 5px; + height: 0; + border: 2px solid #fff; + border-top: 0; + border-left: 0; + } + } + } + } + } + &-child-tree { + display: none; + &-open { + display: block; + } + } + &-treenode-disabled { + >span, + >a, + >a span { + color: #ccc; + cursor: not-allowed; + } + } + &-node-selected { + background-color: #ffe6b0; + border: 1px #ffb951 solid; + opacity: 0.8; + } + &-icon__open { + margin-right: 2px; + background-position: -110px -16px; + vertical-align: top; + } + &-icon__close { + margin-right: 2px; + background-position: -110px 0; + vertical-align: top; + } +} diff --git a/components/vc-tree-select/demo/basic.jsx b/components/vc-tree-select/demo/basic.jsx new file mode 100644 index 000000000..2eaf66b6d --- /dev/null +++ b/components/vc-tree-select/demo/basic.jsx @@ -0,0 +1,347 @@ +/* eslint react/no-multi-comp:0, no-console:0, no-alert: 0 */ + +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 { gData } from './util' + +function isLeaf (value) { + if (!value) { + return false + } + let queues = [...gData] + while (queues.length) { // BFS + const item = queues.shift() + if (item.value === value) { + if (!item.children) { + return true + } + return false + } + if (item.children) { + queues = queues.concat(item.children) + } + } + return false +} + +function findPath (value, data) { + const sel = [] + function loop (selected, children) { + for (let i = 0; i < children.length; i++) { + const item = children[i] + if (selected === item.value) { + sel.push(item) + return + } + if (item.children) { + loop(selected, item.children, item) + if (sel.length) { + sel.push(item) + return + } + } + } + } + loop(value, data) + return sel +} + +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); + }, + methods: { + onClick () { + this.visible = true + }, + + onClose () { + this.visible = false + }, + + onSearch (value) { + console.log(value, arguments) + }, + + onChange (value) { + console.log('onChange', arguments) + this.value = value + }, + + onChangeChildren (value) { + console.log('onChangeChildren', arguments) + const pre = value ? this.value : undefined + this.value = isLeaf(value) ? value : pre + }, + + onChangeLV (value) { + console.log('labelInValue', arguments) + if (!value) { + this.lv = undefined + return + } + const path = findPath(value.value, gData).map(i => i.label).reverse().join(' > ') + this.lv = { value: value.value, label: path } + }, + + onMultipleChange (value) { + console.log('onMultipleChange', arguments) + this.multipleValue = value + }, + + onSelect () { + // use onChange instead + console.log(...arguments) + }, + + 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.') + return false + } + return true + }, + + filterTreeNode (input, child) { + return String(child.title).indexOf(input) === 0 + }, + }, + + render () { + return ( +
    +

    tree-select in dialog

    + + {this.visible ? +
    + triggerNode.parentNode} + style={{ width: '300px' }} + transitionName='rc-tree-select-dropdown-slide-up' + choiceTransitionName='rc-tree-select-selection__choice-zoom' + dropdownStyle={{ maxHeight: '200px', overflow: 'auto', zIndex: 1500 }} + placeholder={请下拉选择} + searchPlaceholder='please search' + showSearch allowClear treeLine + value={this.value} + treeData={gData} + treeNodeFilterProp='label' + filterTreeNode={false} + onSearch={this.onSearch} + onChange={this.onChange} + onSelect={this.onSelect} + /> +
    +
    : null} + +

    single select

    + 请下拉选择} + searchPlaceholder='please search' + showSearch allowClear treeLine + inputValue={this.inputValue} + value={this.value} + treeData={gData} + treeNodeFilterProp='label' + filterTreeNode={false} + onSearch={this.onSearch} + open={this.tsOpen} + onChange={(value) => { + console.log('onChange', value, arguments) + if (value === '0-0-0-0-value') { + this.tsOpen = true + } else { + this.tsOpen = false + } + this.value = value + } } + dropdownVisibleChange={(v, info) => { + console.log('single dropdownVisibleChange', v, info) + // document clicked + if (info.documentClickClose && this.value === '0-0-0-0-value') { + return false + } + return true + } } + onSelect={this.onSelect} + /> + +

    single select (just select children)

    + 请下拉选择} + searchPlaceholder='please search' + showSearch allowClear treeLine + value={this.value} + treeData={gData} + treeNodeFilterProp='label' + filterTreeNode={false} + onChange={this.onChangeChildren} + /> + +

    multiple select

    + 请下拉选择} + searchPlaceholder='please search' + multiple + value={this.multipleValue} + treeData={gData} + treeNodeFilterProp='title' + onChange={this.onMultipleChange} + onSelect={this.onSelect} + allowClear + /> + +

    check select

    + 请下拉选择} + searchPlaceholder='please search' + treeLine maxTagTextLength={10} + value={this.value} + inputValue={null} + treeData={gData} + treeNodeFilterProp='title' + treeCheckable showCheckedStrategy={SHOW_PARENT} + onChange={this.onChange} + onSelect={this.onSelect} + /> + +

    labelInValue & show path

    + 请下拉选择} + searchPlaceholder='please search' + showSearch allowClear treeLine + value={this.lv} labelInValue + treeData={gData} + treeNodeFilterProp='label' + filterTreeNode={false} + onChange={this.onChangeLV} + /> + +

    use treeDataSimpleMode

    + 请下拉选择} + searchPlaceholder='please search' + treeLine maxTagTextLength={10} + inputValue={'test111'} + value={this.value} + treeData={this.simpleTreeData} + treeNodeFilterProp='title' + treeDataSimpleMode={this.treeDataSimpleMode} + treeCheckable showCheckedStrategy={SHOW_PARENT} + onChange={this.onChange} + onSelect={this.onSelect} + /> + +

    Testing in extreme conditions (Boundary conditions test)

    + console.log(val, arguments)} + /> + +

    use TreeNode Component (not recommend)

    + console.log(val, arguments)} + > + + + + + + + sss} key='random3' + /> + + + + + + + + + + +
    + ) + }, +} + diff --git a/components/vc-tree-select/demo/big-data-generator.jsx b/components/vc-tree-select/demo/big-data-generator.jsx new file mode 100644 index 000000000..7166c79aa --- /dev/null +++ b/components/vc-tree-select/demo/big-data-generator.jsx @@ -0,0 +1,63 @@ +import PropTypes from '../../_util/vue-types' +import { generateData, calcTotal } from './util' + +const Gen = { + props: { + x: PropTypes.number.def(20), + y: PropTypes.number.def(18), + z: PropTypes.number.def(1), + }, + data () { + return { + nums: '', + } + }, + + mounted () { + this.$refs.x.value = this.x + this.$refs.y.value = this.y + this.$refs.z.value = this.z + const vals = this.getVals() + this.$emit('gen', generateData(vals.x, vals.y, vals.z)) + }, + methods: { + onGen (e) { + e.preventDefault() + const vals = this.getVals() + this.$emit('gen', generateData(vals.x, vals.y, vals.z)) + this.nums = calcTotal(vals.x, vals.y, vals.z) + }, + getVals () { + return { + x: parseInt(this.$refs.x.value, 10), + y: parseInt(this.$refs.y.value, 10), + z: parseInt(this.$refs.z.value, 10), + } + }, + }, + + render () { + const { x, y, z } = this + return (
    +

    big data generator

    +
    + + x: + + + y: + + + z: + + +

    total nodes: {this.nums || calcTotal(x, y, z)}

    +
    +

    + x:每一级下的节点总数。y:每级节点里有y个节点、存在子节点。z:树的level层级数(0表示一级) +

    +
    ) + }, +} + +export default Gen diff --git a/components/vc-tree-select/demo/big-data.jsx b/components/vc-tree-select/demo/big-data.jsx new file mode 100644 index 000000000..9ca93ec24 --- /dev/null +++ b/components/vc-tree-select/demo/big-data.jsx @@ -0,0 +1,78 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import '../assets/index.less' +import './demo.less' +import TreeSelect, { SHOW_PARENT } from '../index' +import Gen from './big-data-generator' + +export default { + data () { + return { + gData: [], + gData1: [], + value: '', + value1: '', + } + }, + methods: { + onChange (value) { + console.log('onChange', arguments) + this.value = value + }, + + onChangeStrictly (value1) { + console.log('onChangeStrictly', arguments) + const ind = parseInt(Math.random() * 3, 10) + value1.push({ value: `0-0-0-${ind}-value`, label: `0-0-0-${ind}-label`, halfChecked: true }) + this.value1 = value1 + }, + + onGen (data) { + Object.assign(this.$data, { + gData: data, + gData1: [...data], + value: '0-0-0-value', + value1: [ + { value: '0-0-value', label: '0-0-label', halfChecked: true }, + { value: '0-0-0-value', label: '0-0-0-label' }, + ], + // value: ['0-0-0-0-value', '0-0-0-1-value', '0-0-0-2-value'], + }) + }, + }, + + render () { + return (
    + +
    +
    +

    normal check

    + 请下拉选择} + treeCheckable + showCheckedStrategy={SHOW_PARENT} + onChange={this.onChange} + /> +
    +
    +

    checkStrictly

    + 请下拉选择} + treeCheckable + treeCheckStrictly + showCheckedStrategy={SHOW_PARENT} + onChange={this.onChangeStrictly} + /> +
    +
    +
    ) + }, +} diff --git a/components/vc-tree-select/demo/demo.less b/components/vc-tree-select/demo/demo.less new file mode 100644 index 000000000..e762c52e6 --- /dev/null +++ b/components/vc-tree-select/demo/demo.less @@ -0,0 +1,15 @@ + +.rc-tree-select-selection--multiple { + max-height: 50px; + overflow-y: scroll; +} +.rc-tree-select-dropdown { + max-height: 350px; + overflow-y: scroll; +} +.check-select { + width: 300px; + .rc-tree-select-selection--multiple { + min-height: 50px; + } +} diff --git a/components/vc-tree-select/demo/disable.jsx b/components/vc-tree-select/demo/disable.jsx new file mode 100644 index 000000000..2600215d8 --- /dev/null +++ b/components/vc-tree-select/demo/disable.jsx @@ -0,0 +1,80 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ +import '../assets/index.less' +import TreeSelect from '../index' + +const SHOW_PARENT = TreeSelect.SHOW_PARENT + +const treeData = [{ + label: 'Node1', + value: '0-0', + key: '0-0', + children: [{ + label: 'Child Node1', + value: '0-0-0', + key: '0-0-0', + }], +}, { + label: 'Node2', + value: '0-1', + key: '0-1', + children: [{ + label: 'Child Node3', + value: '0-1-0', + key: '0-1-0', + }, { + label: 'Child Node4', + value: '0-1-1', + key: '0-1-1', + }, { + label: 'Child Node5', + value: '0-1-2', + key: '0-1-2', + }], +}] + +export default { + data () { + return { + value: ['0-0-0'], + disabled: false, + } + }, + + methods: { + onChange (value) { + console.log('onChange ', value, arguments) + this.value = value + }, + switch (checked) { + this.disabled = checked + }, + }, + + render () { + const tProps = { + props: { + treeData, + disabled: this.disabled, + value: this.value, + multiple: true, + allowClear: true, + treeCheckable: true, + showCheckedStrategy: SHOW_PARENT, + searchPlaceholder: 'Please select', + }, + on: { + change: this.onChange, + }, + style: { + width: '300px', + }, + } + return ( +
    + + this.switch(e.target.checked)}/> 禁用 +
    + ) + }, +} + diff --git a/components/vc-tree-select/demo/dynamic.jsx b/components/vc-tree-select/demo/dynamic.jsx new file mode 100644 index 000000000..1e92fc062 --- /dev/null +++ b/components/vc-tree-select/demo/dynamic.jsx @@ -0,0 +1,55 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import '../assets/index.less' +import TreeSelect from '../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' }, + } + }, + + methods: { + onChange (value) { + console.log(value) + this.value = value + }, + + onLoadData (treeNode) { + console.log(treeNode) + return new Promise((resolve) => { + setTimeout(() => { + const treeData = [...this.treeData] + getNewTreeData(treeData, treeNode.eventKey, generateTreeNodes(treeNode), 2) + this.treeData = treeData + resolve() + }, 500) + }) + }, + }, + + render () { + return ( +
    +

    dynamic render

    + +
    + ) + }, +} + diff --git a/components/vc-tree-select/demo/filter.jsx b/components/vc-tree-select/demo/filter.jsx new file mode 100644 index 000000000..36f805386 --- /dev/null +++ b/components/vc-tree-select/demo/filter.jsx @@ -0,0 +1,98 @@ +/* eslint react/no-multi-comp:0, no-console:0 */ + +import '../assets/index.less' +import TreeSelect, { SHOW_PARENT } from '../index' +import { gData } from './util' + +export default { + data () { + return { + value: '11', + // value: ['0-0-0-0-value', '0-0-0-1-value', '0-0-0-2-value'], + simpleTreeData: [ + { key: 1, pId: 0, label: 'a', value: 'a' }, + { key: 11, pId: 1, label: 'a12', value: 'a12', disabled: true }, + { key: 111, pId: 11, label: 'a00', value: 'a00', selectable: false }, + { key: 2, pId: 0, label: 'b', value: 'b' }, + { key: 20, pId: 2, label: 'b10', value: 'b10' }, + { key: 21, pId: 2, label: 'b1', value: 'b1' }, + { key: 22, pId: 2, label: 'b12', value: 'b12' }, + ], + treeDataSimpleMode: { + id: 'key', + rootPId: 0, + }, + } + }, + methods: { + onChange (value) { + if (value.length === 1) { + // return; + } + console.log('onChange', arguments, this.simpleTreeData) + this.value = value + }, + + onSelect () { + // use onChange instead + // console.log(arguments); + }, + + onDataChange () { + const data = [...this.simpleTreeData] + data.forEach(i => { + if (i.key === 11) { + delete i.disabled + } + if (i.key === 20) { + i.disabled = true + } + }) + this.simpleTreeData = data + }, + }, + + render () { + return ( +
    +

    check select

    + 请下拉选择} + searchPlaceholder='please search' + treeLine maxTagTextLength={10} + value={this.value} + treeData={gData} + treeNodeFilterProp='title' + treeCheckable + onChange={this.onChange} + onSelect={this.onSelect} + /> + +

    use treeDataSimpleMode

    + 请下拉选择} + searchPlaceholder='please search' + treeLine maxTagTextLength={10} + inputValue={null} + value={this.value} + treeData={this.simpleTreeData} + treeDefaultExpandAll + treeNodeFilterProp='title' + treeDataSimpleMode={this.treeDataSimpleMode} + treeCheckable showCheckedStrategy={SHOW_PARENT} + onChange={this.onChange} + onSelect={this.onSelect} + /> + +
    + ) + }, +} + diff --git a/components/vc-tree-select/demo/form.js b/components/vc-tree-select/demo/form.js new file mode 100644 index 000000000..f66317371 --- /dev/null +++ b/components/vc-tree-select/demo/form.js @@ -0,0 +1,138 @@ +import '../assets/index.less' +import TreeSelect from '../index' +import Select from '../../vc-select' +import { createForm } from '../../vc-form' +import { regionStyle, errorStyle } from './styles' +import { gData } from './util' +import '../../vc-select/assets/index.less' +import './demo.less' + +const { Option } = Select + +const TreeSelectInput = { + props: [ + 'multiple', + 'treeData', + 'treeCheckable', + 'value', + ], + methods: { + onChange (value) { + console.log(value, arguments) + this.$emit('change', value) + }, + }, + + render () { + return ( + + ) + }, +} + +const Form = { + methods: { + onSubmit (e) { + console.log('submit') + e.preventDefault() + this.form.validateFields((error, values) => { + if (!error) { + console.log('ok', values) + } else { + console.log('error', error, values) + } + }) + }, + reset (e) { + e.preventDefault() + this.form.resetFields() + }, + }, + + render () { + const { form } = this + const { getFieldDecorator, getFieldError } = form + const tProps = { + props: { + multiple: true, + treeData: gData, + treeCheckable: true, + // treeDefaultExpandAll: true, + }, + } + return (
    +

    validity

    +
    +
    +
    +

    no onChange

    + {getFieldDecorator('tree-select', { + initialValue: ['0-0-0-value'], + rules: [ + { required: true, type: 'array', message: 'tree-select 需要必填' }, + ], + })( + + )} +
    +

    + {(getFieldError('tree-select')) ? getFieldError('tree-select').join(',') : null} +

    +
    + +
    +
    +

    custom onChange

    + {getFieldDecorator('tree-select1', { + initialValue: ['0-0-0-value'], + rules: [ + { required: true, type: 'array', message: 'tree-select1 需要必填' }, + ], + })( + + )} +
    +

    + {(getFieldError('tree-select1')) ? getFieldError('tree-select1').join(',') : null} +

    +
    + +
    + {getFieldDecorator('select', { + initialValue: ['jack'], + rules: [ + { required: true, type: 'array', message: 'select 需要必填' }, + ], + })( + + )} +

    + {(getFieldError('select')) ? getFieldError('select').join(',') : null} +

    +
    + +
    + +   + +
    +
    +
    ) + }, +} + +export default createForm()(Form) diff --git a/components/vc-tree-select/demo/styles.js b/components/vc-tree-select/demo/styles.js new file mode 100644 index 000000000..e0a5fe5ac --- /dev/null +++ b/components/vc-tree-select/demo/styles.js @@ -0,0 +1,11 @@ +export const regionStyle = { + border: '1px solid red', + marginTop: '10px', + padding: '10px', +} + +export const errorStyle = { + color: 'red', + marginTop: '10px', + padding: '10px', +} diff --git a/components/vc-tree-select/demo/util.js b/components/vc-tree-select/demo/util.js new file mode 100644 index 000000000..27fe44d06 --- /dev/null +++ b/components/vc-tree-select/demo/util.js @@ -0,0 +1,142 @@ +/* eslint no-loop-func: 0, no-console: 0 */ + +export function generateData (x = 3, y = 2, z = 1, gData = []) { + // x:每一级下的节点总数。y:每级节点里有y个节点、存在子节点。z:树的level层级数(0表示一级) + function _loop (_level, _preKey, _tns) { + const preKey = _preKey || '0' + const tns = _tns || gData + + const children = [] + for (let i = 0; i < x; i++) { + const key = `${preKey}-${i}` + tns.push({ + label: `${key}-label`, + value: `${key}-value`, + key, + disabled: key === '0-0-0-1' || false, + }) + if (i < y) { + children.push(key) + } + } + if (_level < 0) { + return tns + } + const __level = _level - 1 + children.forEach((key, index) => { + tns[index].children = [] + return _loop(__level, key, tns[index].children) + }) + } + _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 + return rec(z + 1) +} +console.log('总节点数(单个tree):', calcTotal()) +export const gData = generateData() + +export function generateTreeNodes (treeNode) { + const arr = [] + const key = treeNode.eventKey + for (let i = 0; i < 3; i++) { + arr.push({ label: `${key}-${i}-label`, value: `${key}-${i}-value`, key: `${key}-${i}` }) + } + return arr +} + +function setLeaf (treeData, curKey, level) { + const loopLeaf = (data, lev) => { + const l = lev - 1 + data.forEach((item) => { + if ((item.key.length > curKey.length) ? item.key.indexOf(curKey) !== 0 + : curKey.indexOf(item.key) !== 0) { + return + } + if (item.children) { + loopLeaf(item.children, l) + } else if (l < 1) { + item.isLeaf = true + } + }) + } + loopLeaf(treeData, level + 1) +} + +export function getNewTreeData (treeData, curKey, child, level) { + const loop = (data) => { + if (level < 1 || curKey.length - 3 > level * 2) return + data.forEach((item) => { + if (curKey.indexOf(item.key) === 0) { + if (item.children) { + loop(item.children) + } else { + item.children = child + } + } + }) + } + loop(treeData) + setLeaf(treeData, curKey, level) +} + +function loopData (data, callback) { + const loop = (d, level = 0) => { + d.forEach((item, index) => { + const pos = `${level}-${index}` + if (item.children) { + loop(item.children, pos) + } + callback(item, index, pos) + }) + } + loop(data) +} + +function isPositionPrefix (smallPos, bigPos) { + 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 +} +// console.log(isPositionPrefix("0-1", "0-10-1")); + +export function getFilterValue (val, sVal, delVal) { + const allPos = [] + const delPos = [] + loopData(gData, (item, index, pos) => { + if (sVal.indexOf(item.value) > -1) { + allPos.push(pos) + } + if (delVal.indexOf(item.value) > -1) { + delPos.push(pos) + } + }) + const newPos = [] + delPos.forEach((item) => { + allPos.forEach((i) => { + if (isPositionPrefix(item, i) || isPositionPrefix(i, item)) { + // 过滤掉 父级节点 和 所有子节点。 + // 因为 node节点 不选时,其 父级节点 和 所有子节点 都不选。 + return + } + newPos.push(i) + }) + }) + const newVal = [] + if (newPos.length) { + loopData(gData, (item, index, pos) => { + if (newPos.indexOf(pos) > -1) { + newVal.push(item.value) + } + }) + } + return newVal +} diff --git a/components/vc-tree-select/index.js b/components/vc-tree-select/index.js new file mode 100644 index 000000000..f35201e54 --- /dev/null +++ b/components/vc-tree-select/index.js @@ -0,0 +1,7 @@ +// rc-tree-select 1.12.13 tag +// export this package's api +import TreeSelect from './src' + +export default TreeSelect + +export { TreeNode, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './src' diff --git a/components/vc-tree-select/src/PropTypes.js b/components/vc-tree-select/src/PropTypes.js new file mode 100644 index 000000000..ac869461b --- /dev/null +++ b/components/vc-tree-select/src/PropTypes.js @@ -0,0 +1,108 @@ +import PropTypes from '../../_util/vue-types' +import { SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './strategies' + +function nonEmptyStringType (props, propsName) { + const value = props[propsName] + if (typeof value !== 'string' || !value) { + return new Error() // Just a flag, so don't need message. + } +} + +function valueType (props, propName, componentName) { + const labelInValueShape = PropTypes.shape({ + value: nonEmptyStringType, + label: PropTypes.node, + }) + if (props.labelInValue) { + const validate = PropTypes.oneOfType([ + PropTypes.arrayOf(labelInValueShape), + labelInValueShape, + ]) + const error = validate(...arguments) + if (error) { + return new Error( + `Invalid prop \`${propName}\` supplied to \`${componentName}\`, ` + + `when \`labelInValue\` is \`true\`, \`${propName}\` should in ` + + `shape of \`{ value: string, label?: string }\`.` + ) + } + } else if (props.treeCheckable && props.treeCheckStrictly) { + const validate = PropTypes.oneOfType([ + PropTypes.arrayOf(labelInValueShape), + labelInValueShape, + ]) + const error = validate(...arguments) + if (error) { + return new Error( + `Invalid prop \`${propName}\` supplied to \`${componentName}\`, ` + + `when \`treeCheckable\` and \`treeCheckStrictly\` are \`true\`, ` + + `\`${propName}\` should in shape of \`{ value: string, label?: string }\`.` + ) + } + } else if (props.multiple && props[propName] === '') { + return new Error( + `Invalid prop \`${propName}\` of type \`string\` supplied to \`${componentName}\`, ` + + `expected \`array\` when \`multiple\` is \`true\`.` + ) + } else { + const validate = PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.string), + PropTypes.string, + ]) + return validate(...arguments) + } +} + +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, + 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, +} diff --git a/components/vc-tree-select/src/Select.jsx b/components/vc-tree-select/src/Select.jsx new file mode 100644 index 000000000..8e1580c56 --- /dev/null +++ b/components/vc-tree-select/src/Select.jsx @@ -0,0 +1,1045 @@ +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 { 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 noop () { +} + +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) + } + // 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 { + sValue: value, + sInputValue: inputValue, + sOpen: props.open || props.defaultOpen, + sFocused: false, + } + }, + + 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 = '' + } + } + 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) + ) + } + 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 (nextProps.inputValue !== this.preProps.inputValue) { + this.setState({ + sInputValue: nextProps.inputValue, + }) + } + 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 = '' + } + }) + } + }, + + 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)}) + } 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) + }, + + 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) + } + 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) + + 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), + } + }) + } else { + if (value.some(i => i.value === selectedValue)) { + return + } + value = value.concat([{ + value: selectedValue, + label: selectedLabel, + }]) + } + } else { + if (value.length && value[0].value === selectedValue) { + this.setOpenState(false) + return + } + value = [{ + value: selectedValue, + label: selectedLabel, + }] + this.setOpenState(false) + } + + const extraInfo = { + triggerValue: selectedValue, + triggerNode: item, + } + 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 + } + + this.fireChange(value, extraInfo) + if (props.inputValue === null) { + this.setState({ + sInputValue: '', + }) + } + }, + + onDeselect (info) { + this.removeSelected(getValuePropValue(info.node)) + if (!isMultiple(this.$props)) { + this.setOpenState(false) + } else { + this.clearSearchInput() + } + }, + + onPlaceholderClick () { + this.getInputDOMNode().focus() + }, + + 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([]) + } + } + }, + + onChoiceAnimationLeave () { + 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, 'placeholder') + } + if (placeholder) { + return ( + + ) + } + 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 ( + + + + {sInputValue}  + + {isMultiple(this.$props) ? null : this.getSearchPlaceholderElement(!!sInputValue)} + + ) + }, + + 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 = {}) { + 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) + } + } + }, + + isLabelInValue () { + const { treeCheckable, treeCheckStrictly, labelInValue } = this.$props + if (treeCheckable && treeCheckStrictly) { + return true + } + return labelInValue || false + }, + onFocus (e) { + this.__emit('focus', e) + }, + onBlur (e) { + this.__emit('blur', e) + }, + + focus () { + if (!isMultiple(this.$props)) { + this.$refs.selection.focus() + } else { + this.getInputDOMNode().focus() + } + }, + + blur () { + if (!isMultiple(this.$props)) { + this.$refs.selection.blur() + } else { + this.getInputDOMNode().blur() + } + }, + + forcePopupAlign () { + this.$refs.trigger.$refs.trigger.forcePopupAlign() + }, + + renderTopControlNode () { + const { sValue: value } = this.$data + const props = this.$props + const { choiceTransitionName, prefixCls, maxTagTextLength } = props + const multiple = isMultiple(props) + + // single and not combobox, input is inside dropdown + if (!multiple) { + let innerNode = ( + {getComponentFromProp(this, 'placeholder') || ''} + ) + if (value.length) { + innerNode = ( + {value[0].label} + ) + } + return ( + {innerNode} + ) + } + + 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 ( +
  • + { + this.removeSelected(singleValue.value, event) + }} + /> + {content} +
  • + ) + }) + + selectedValueNodes.push() + const className = `${prefixCls}-selection__rendered` + if (choiceTransitionName) { + const transitionProps = getTransitionProps(choiceTransitionName, { + tag: 'ul', + afterLeave: this.onChoiceAnimationLeave, + }) + return ( + {selectedValueNodes} + ) + } + return (
      {selectedValueNodes}
    ) + }, + + 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) + } + }, + }, + + render () { + const props = this.$props + const multiple = isMultiple(props) + const state = this.$data + const { disabled, allowClear, prefixCls } = 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 = () + const selectTriggerProps = { + 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, + }, + on: { + ...this.$listeners, + select: this.onSelect, + }, + ref: 'trigger', + } + return ( + + + + {ctrlNode} + {allowClear && state.sValue.length && + state.sValue[0].value ? clear : null} + {multiple || !props.showArrow ? null + : ( + + )} + {multiple + ? this.getSearchPlaceholderElement(!!state.sInputValue || state.sValue.length) + : null} + + + + ) + }, +} + +Select.SHOW_ALL = SHOW_ALL +Select.SHOW_PARENT = SHOW_PARENT +Select.SHOW_CHILD = SHOW_CHILD + +export default Select diff --git a/components/vc-tree-select/src/SelectTrigger.jsx b/components/vc-tree-select/src/SelectTrigger.jsx new file mode 100644 index 000000000..8ad2cd7be --- /dev/null +++ b/components/vc-tree-select/src/SelectTrigger.jsx @@ -0,0 +1,371 @@ +import PropTypes from '../../_util/vue-types' +import classnames from 'classnames' +import omit from 'omit.js' +import Trigger from '../../trigger' +import Tree, { TreeNode } from '../../vc-tree' +import { SelectPropTypes } from './PropTypes' +import BaseMixin from '../../_util/BaseMixin' +import { + loopAllChildren, + flatToHierarchy, + getValuePropValue, + labelCompatible, + saveRef, +} from './util' + +import { cloneElement } from '../../_util/vnode' +import { isEmptyElement, getSlotOptions, getKey, getAllProps, getComponentFromProp } from '../../_util/props-util' +import { noop } from '../../_util/vue-types/utils' + +const BUILT_IN_PLACEMENTS = { + bottomLeft: { + points: ['tl', 'bl'], + offset: [0, 4], + overflow: { + adjustX: 0, + adjustY: 1, + }, + }, + topLeft: { + points: ['bl', 'tl'], + offset: [0, -4], + overflow: { + adjustX: 0, + adjustY: 1, + }, + }, +} + +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, + 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, + } + }, + + mounted () { + this.$nextTick(() => { + this.setDropdownWidth() + }) + }, + watch: { + inputValue (val) { + // set autoExpandParent to true + this.setState({ + sExpandedKeys: [], + fireOnExpand: false, + }) + }, + }, + + updated () { + this.$nextTick(() => { + this.setDropdownWidth() + }) + }, + 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}` + } + 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 + } + 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 ( + + {newTreeNodes} + + ) + }, + }, + + 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 : ( + {props.inputElement} + ) + 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), + title: getComponentFromProp(child, 'title') || getComponentFromProp(child, 'label'), + }, + key: String(child.key), + } + if (child && child.componentOptions.children) { + // null or String has no Prop + return ( + + {recursive(child.componentOptions.children) } + + ) + } + return + }) + } + // 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); + + if (props.inputValue) { + treeNodes = this.processTreeNode(treeNodes) + } + + 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 = ( + + {props.notFoundContent} + + ) + } else if (!search) { + visible = false + } + } + const popupElement = ( +
    + {search} + {notFoundContent || this.renderTree(keys, halfCheckedKeys, treeNodes, multiple)} +
    + ) + + const popupStyle = { ...props.dropdownStyle } + const widthProp = props.dropdownMatchSelectWidth ? 'width' : 'minWidth' + if (this.dropdownWidth) { + popupStyle[widthProp] = `${this.dropdownWidth}px` + } + + return ( + + {this.$slots.default} + + ) + }, +} + +export default SelectTrigger diff --git a/components/vc-tree-select/src/TreeNode.jsx b/components/vc-tree-select/src/TreeNode.jsx new file mode 100644 index 000000000..9a9d2d195 --- /dev/null +++ b/components/vc-tree-select/src/TreeNode.jsx @@ -0,0 +1,12 @@ +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 new file mode 100644 index 000000000..27d435e50 --- /dev/null +++ b/components/vc-tree-select/src/index.js @@ -0,0 +1,26 @@ +// 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 + +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 + }, + TreeNode, + SHOW_ALL, SHOW_PARENT, SHOW_CHILD, +} +export { TreeNode, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } diff --git a/components/vc-tree-select/src/strategies.js b/components/vc-tree-select/src/strategies.js new file mode 100644 index 000000000..1257b13e4 --- /dev/null +++ b/components/vc-tree-select/src/strategies.js @@ -0,0 +1,3 @@ +export const SHOW_ALL = 'SHOW_ALL' +export const SHOW_PARENT = 'SHOW_PARENT' +export const SHOW_CHILD = 'SHOW_CHILD' diff --git a/components/vc-tree-select/src/util.js b/components/vc-tree-select/src/util.js new file mode 100644 index 000000000..0aede0b40 --- /dev/null +++ b/components/vc-tree-select/src/util.js @@ -0,0 +1,569 @@ +import { getPropsData, getAllProps, getKey, getAttrs, getSlotOptions, filterEmpty, getSlots } from '../../_util/props-util' +import { cloneVNodes, cloneElement } from '../../_util/vnode' +export function toTitle (title) { + if (typeof title === 'string') { + return 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 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 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() +} + +export const UNSELECTABLE_STYLE = { + userSelect: 'none', + WebkitUserSelect: 'none', +} + +export const UNSELECTABLE_ATTRIBUTE = { + unselectable: 'unselectable', +} + +export function labelCompatible (prop) { + let newProp = prop + if (newProp === 'label') { + newProp = 'title' + } + return newProp +} + +export function isInclude (smallArray, bigArray) { + // attention: [0,0,1] [0,0,10] + return smallArray.every((ii, i) => { + return ii === bigArray[i] + }) +} + +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; + } + }); + 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; +} +*/ + +function getChildrenlength (children) { + let len = 1 + if (Array.isArray(children)) { + len = children.length + } + return len +} + +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 +} + +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) + } + }) + return newChilds +} + +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) + } + }) + } + loop(filterChild(childs), 0, parent) +} + +// 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 + } + const hierarchyNodes = [] + const levelObj = {} + arr.forEach((item) => { + if (!item.pos) { + return + } + 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) + } + }) + 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) + } + }) + } + } + 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 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 + } + }) + 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) + } + // console.log(Date.now()-s, objKeys.length, checkIt); +} + +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) + } + }) + return { + halfCheckedKeys, checkedKeys, checkedNodes, treeNodesStates, checkedPositions, + } +} + +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) + } + }) + + handleCheckState(treeNodesStates, filterParentPosition(checkedPositions.sort()), true) + + return getCheck(treeNodesStates, checkedPositions) +} + +// 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 + }) +} + +function recursive (children, cb) { + children.forEach(item => { + cb(item) + if (item.children) { + recursive(item.children, cb) + } + }) +} + +// 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] +} diff --git a/components/vc-tree/src/Tree.jsx b/components/vc-tree/src/Tree.jsx index 7c75378ea..25d0246e0 100644 --- a/components/vc-tree/src/Tree.jsx +++ b/components/vc-tree/src/Tree.jsx @@ -558,7 +558,7 @@ const Tree = { tabIndex={focusable ? '0' : null} onKeydown={focusable ? this.onKeydown : () => {}} > - {children.map(this.renderTreeNode)} + {children.map((child, index) => this.renderTreeNode(child, index))} ) }, diff --git a/components/vc-tree/src/TreeNode.jsx b/components/vc-tree/src/TreeNode.jsx index 58c4563b5..34820110f 100644 --- a/components/vc-tree/src/TreeNode.jsx +++ b/components/vc-tree/src/TreeNode.jsx @@ -21,6 +21,7 @@ let onlyTreeNodeWarned = false // Only accept TreeNode const TreeNode = { name: 'TreeNode', mixins: [BaseMixin], + __ANT_TREE_NODE: true, props: initDefaultProps({ eventKey: PropTypes.string, // Pass by parent `cloneElement` prefixCls: PropTypes.string, diff --git a/components/vc-tree/src/util.js b/components/vc-tree/src/util.js index 11fd3bd13..e00246cc7 100644 --- a/components/vc-tree/src/util.js +++ b/components/vc-tree/src/util.js @@ -332,6 +332,11 @@ export function calcCheckStateConduct (treeNodes, checkedKeys) { } } +// function keyListToString (keyList) { +// if (!keyList) return keyList +// return keyList.map(key => String(key)) +// } + /** * Calculate the value of checked and halfChecked keys. * This should be only run in init or props changed. @@ -361,6 +366,9 @@ export function calcCheckedKeys (keys, props, children = []) { return null } + // keyProps.checkedKeys = keyListToString(keyProps.checkedKeys) + // keyProps.halfCheckedKeys = keyListToString(keyProps.halfCheckedKeys) + // Do nothing if is checkStrictly mode if (checkStrictly) { return keyProps diff --git a/site/components.js b/site/components.js index 8b262e351..2bfd38350 100644 --- a/site/components.js +++ b/site/components.js @@ -45,7 +45,7 @@ import { Table, Transfer, Tree, - // TreeSelect, + TreeSelect, Tabs, Tag, TimePicker, @@ -133,7 +133,8 @@ Vue.component(Table.ColumnGroup.name, Table.ColumnGroup) Vue.component(Transfer.name, Transfer) Vue.component(Tree.name, Tree) Vue.component(Tree.TreeNode.name, Tree.TreeNode) -// Vue.component(TreeSelect.name, TreeSelect) +Vue.component(TreeSelect.name, TreeSelect) +Vue.component(TreeSelect.TreeNode.name, TreeSelect.TreeNode) Vue.component(Tabs.name, Tabs) Vue.component(Tabs.TabPane.name, Tabs.TabPane) Vue.component(Tag.name, Tag) diff --git a/site/demo.js b/site/demo.js index 3e028ce0f..8a713f481 100644 --- a/site/demo.js +++ b/site/demo.js @@ -43,6 +43,7 @@ export { default as inputNumber } from 'antd/input-number/demo/index.vue' export { default as transfer } from 'antd/transfer/demo/index.vue' export { default as upload } from 'antd/upload/demo/index.vue' export { default as tree } from 'antd/tree/demo/index.vue' +export { default as treeSelect } from 'antd/tree-select/demo/index.vue' export { default as layout } from 'antd/layout/demo/index.vue' export { default as form } from 'antd/form/demo/index.vue' export { default as anchor } from 'antd/anchor/demo/index.vue' diff --git a/tests/__snapshots__/index.test.js.snap b/tests/__snapshots__/index.test.js.snap index be8c0de07..4c6bdbab2 100644 --- a/tests/__snapshots__/index.test.js.snap +++ b/tests/__snapshots__/index.test.js.snap @@ -48,6 +48,7 @@ Array [ "Table", "Transfer", "Tree", + "TreeSelect", "Tabs", "Tag", "TimePicker",