mirror of
https://github.com/element-plus/element-plus.git
synced 2025-02-17 11:49:41 +08:00
feat(components): add tree select component (#6843)
* feat(ElTreeSelect): add tree select base component * refactor(ElTreeSelect): use render function and move select/tree props to them self module * fix(ElTreeSelect): init value not checked * fix(ElTreeSelect): `toArray` ignores valid values * fix(ElTreeSelect): expose not working when defined on mounted * fix(ElTreeSelect): watch `modelValue` deep * test(ElTreeSelect): add base unit test * perf(ElTreeSelect): default slot should be a function * fix(ElTreeSelect): `onNodeClick` can not call, * test(ElTreeSelect): update unit test * fix(ElTreeSelect): `onNodeClick` can not call, * fix(ElTreeSelect): remove folder node when `checkStrictly` is false * feat(ElTreeSelect): export `ElTreeSelect` * fix(ElTreeSelect): `filterMethod` conflicts with `filterNodeMethod` * docs(ElTreeSelect): add component docs * fix(ElTreeSelect): fix lint * docs(ElTreeSelect): fix lazy loading requires non-leaf nodes, and change mock labels * docs(ElTreeSelect): the link address of the attributes is incorrect * docs(ElTreeSelect): `dropdown` doesn't need the `-` symbol * refactor(ElTreeSelect): use alias path and make sure vue is above to components * refactor(ElTreeSelect): use a unified namespace for styles * docs(ElTreeSelect): change option labels in default slots * refactor(ElTreeSelect): import `ElOption` using unified entry and change the way to override the select click event * style(ElTreeSelect): sort imports * docs(ElTreeSelect): update the documentation for special codes * refactor(ElTreeSelect): keep it consistent with the select style * refactor(ElTreeSelect): use `isFunction` from `@element-plus/utils` * refactor(ElTreeSelect): use single closing tag when no subset * docs(ElTreeSelect): set `TreeSelect` promotion as `2.1.8`
This commit is contained in:
parent
ada12878d1
commit
904aa0e21b
@ -206,6 +206,11 @@
|
|||||||
"link": "/tree",
|
"link": "/tree",
|
||||||
"text": "Tree"
|
"text": "Tree"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"link": "/tree-select",
|
||||||
|
"text": "TreeSelect",
|
||||||
|
"promotion": "2.1.8"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"link": "/tree-v2",
|
"link": "/tree-v2",
|
||||||
"text": "Virtualized Tree"
|
"text": "Virtualized Tree"
|
||||||
|
93
docs/en-US/component/tree-select.md
Normal file
93
docs/en-US/component/tree-select.md
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
title: TreeSelect
|
||||||
|
lang: en-US
|
||||||
|
---
|
||||||
|
|
||||||
|
# TreeSelect
|
||||||
|
|
||||||
|
The tree selector of the dropdown menu,
|
||||||
|
it combines the functions of components `el-tree` and `el-select`.
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
Selector for tree structures.
|
||||||
|
|
||||||
|
:::demo
|
||||||
|
|
||||||
|
tree-select/basic
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Select any level
|
||||||
|
|
||||||
|
When using the `check-strictly=true` attribute, any node can be checked,
|
||||||
|
otherwise only leaf nodes are supported.
|
||||||
|
|
||||||
|
:::demo
|
||||||
|
|
||||||
|
tree-select/check-strictly
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Multiple Selection
|
||||||
|
|
||||||
|
Multiple selection using clicks or checkbox.
|
||||||
|
|
||||||
|
:::demo
|
||||||
|
|
||||||
|
tree-select/multiple
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Disabled Selection
|
||||||
|
|
||||||
|
Disable options using the disabled field.
|
||||||
|
|
||||||
|
:::demo
|
||||||
|
|
||||||
|
tree-select/disabled
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Filterable
|
||||||
|
|
||||||
|
Use keyword filtering or custom filtering methods.
|
||||||
|
`filterMethod` can custom filter method for data,
|
||||||
|
`filterNodeMethod` can custom filter method for data node.
|
||||||
|
|
||||||
|
:::demo
|
||||||
|
|
||||||
|
tree-select/filterable
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Custom content
|
||||||
|
|
||||||
|
Contents of custom tree nodes.
|
||||||
|
|
||||||
|
:::demo
|
||||||
|
|
||||||
|
tree-select/slots
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## LazyLoad
|
||||||
|
|
||||||
|
Lazy loading of tree nodes, suitable for large data lists.
|
||||||
|
|
||||||
|
:::demo
|
||||||
|
|
||||||
|
tree-select/lazy
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
Since this component combines the functions of components `el-tree` and `el-select`,
|
||||||
|
the original properties have not been changed, so no repetition here,
|
||||||
|
and please go to the original component to view the documentation.
|
||||||
|
|
||||||
|
| Attributes | Methods | Events | Slots |
|
||||||
|
| --------------------------------------- | ----------------------------- | ----------------------------------- | ---------------------------------- |
|
||||||
|
| [tree](./tree.md#attributes) | [tree](./tree.md#method) | [tree](./tree.md#events) | [tree](./tree.md#slots) |
|
||||||
|
| [select](./select.md#select-attributes) | [select](./select.md#methods) | [select](./select.md#select-events) | [select](./select.md#select-slots) |
|
81
docs/examples/tree-select/basic.vue
Normal file
81
docs/examples/tree-select/basic.vue
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<el-tree-select v-model="value" :data="data" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const value = ref()
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
value: '1',
|
||||||
|
label: 'Level one 1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '1-1',
|
||||||
|
label: 'Level two 1-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '1-1-1',
|
||||||
|
label: 'Level three 1-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2',
|
||||||
|
label: 'Level one 2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-1',
|
||||||
|
label: 'Level two 2-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-1-1',
|
||||||
|
label: 'Level three 2-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2-2',
|
||||||
|
label: 'Level two 2-2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-2-1',
|
||||||
|
label: 'Level three 2-2-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '3',
|
||||||
|
label: 'Level one 3',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-1',
|
||||||
|
label: 'Level two 3-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-1-1',
|
||||||
|
label: 'Level three 3-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '3-2',
|
||||||
|
label: 'Level two 3-2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-2-1',
|
||||||
|
label: 'Level three 3-2-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
```
|
81
docs/examples/tree-select/check-strictly.vue
Normal file
81
docs/examples/tree-select/check-strictly.vue
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<el-tree-select v-model="value" :data="data" check-strictly />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const value = ref()
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
value: '1',
|
||||||
|
label: 'Level one 1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '1-1',
|
||||||
|
label: 'Level two 1-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '1-1-1',
|
||||||
|
label: 'Level three 1-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2',
|
||||||
|
label: 'Level one 2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-1',
|
||||||
|
label: 'Level two 2-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-1-1',
|
||||||
|
label: 'Level three 2-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2-2',
|
||||||
|
label: 'Level two 2-2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-2-1',
|
||||||
|
label: 'Level three 2-2-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '3',
|
||||||
|
label: 'Level one 3',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-1',
|
||||||
|
label: 'Level two 3-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-1-1',
|
||||||
|
label: 'Level three 3-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '3-2',
|
||||||
|
label: 'Level two 3-2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-2-1',
|
||||||
|
label: 'Level three 3-2-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
```
|
84
docs/examples/tree-select/disabled.vue
Normal file
84
docs/examples/tree-select/disabled.vue
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<el-tree-select v-model="value" :data="data" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const value = ref()
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
value: '1',
|
||||||
|
label: 'Level one 1',
|
||||||
|
disabled: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '1-1',
|
||||||
|
label: 'Level two 1-1',
|
||||||
|
disabled: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
disabled: true,
|
||||||
|
value: '1-1-1',
|
||||||
|
label: 'Level three 1-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2',
|
||||||
|
label: 'Level one 2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-1',
|
||||||
|
label: 'Level two 2-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-1-1',
|
||||||
|
label: 'Level three 2-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2-2',
|
||||||
|
label: 'Level two 2-2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-2-1',
|
||||||
|
label: 'Level three 2-2-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '3',
|
||||||
|
label: 'Level one 3',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-1',
|
||||||
|
label: 'Level two 3-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-1-1',
|
||||||
|
label: 'Level three 3-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '3-2',
|
||||||
|
label: 'Level two 3-2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-2-1',
|
||||||
|
label: 'Level three 3-2-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
```
|
104
docs/examples/tree-select/filterable.vue
Normal file
104
docs/examples/tree-select/filterable.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<el-tree-select v-model="value" :data="data" filterable />
|
||||||
|
<el-divider />
|
||||||
|
filter method:
|
||||||
|
<el-tree-select
|
||||||
|
v-model="value"
|
||||||
|
:data="data"
|
||||||
|
:filter-method="filterMethod"
|
||||||
|
filterable
|
||||||
|
/>
|
||||||
|
<el-divider />
|
||||||
|
filter node method:
|
||||||
|
<el-tree-select
|
||||||
|
v-model="value"
|
||||||
|
:data="data"
|
||||||
|
:filter-node-method="filterNodeMethod"
|
||||||
|
filterable
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const value = ref()
|
||||||
|
|
||||||
|
const sourceData = [
|
||||||
|
{
|
||||||
|
value: '1',
|
||||||
|
label: 'Level one 1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '1-1',
|
||||||
|
label: 'Level two 1-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '1-1-1',
|
||||||
|
label: 'Level three 1-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2',
|
||||||
|
label: 'Level one 2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-1',
|
||||||
|
label: 'Level two 2-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-1-1',
|
||||||
|
label: 'Level three 2-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2-2',
|
||||||
|
label: 'Level two 2-2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-2-1',
|
||||||
|
label: 'Level three 2-2-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '3',
|
||||||
|
label: 'Level one 3',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-1',
|
||||||
|
label: 'Level two 3-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-1-1',
|
||||||
|
label: 'Level three 3-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '3-2',
|
||||||
|
label: 'Level two 3-2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-2-1',
|
||||||
|
label: 'Level three 3-2-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const data = ref(sourceData)
|
||||||
|
|
||||||
|
const filterMethod = (value) => {
|
||||||
|
data.value = [...sourceData].filter((item) => item.label.includes(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterNodeMethod = (value, data) => data.label.includes(value)
|
||||||
|
</script>
|
||||||
|
```
|
36
docs/examples/tree-select/lazy.vue
Normal file
36
docs/examples/tree-select/lazy.vue
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<el-tree-select v-model="value" lazy :load="load" :props="props" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const value = ref()
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
label: 'label',
|
||||||
|
children: 'children',
|
||||||
|
isLeaf: 'isLeaf',
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = 0
|
||||||
|
|
||||||
|
const load = (node, resolve) => {
|
||||||
|
if (node.isLeaf) return resolve([])
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve([
|
||||||
|
{
|
||||||
|
value: ++id,
|
||||||
|
label: `lazy load node${id}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ++id,
|
||||||
|
label: `lazy load node${id}`,
|
||||||
|
isLeaf: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}, 400)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
84
docs/examples/tree-select/multiple.vue
Normal file
84
docs/examples/tree-select/multiple.vue
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<el-tree-select v-model="value" :data="data" multiple />
|
||||||
|
<el-divider />
|
||||||
|
show checkbox:
|
||||||
|
<el-tree-select v-model="value" :data="data" multiple show-checkbox />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const value = ref()
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
value: '1',
|
||||||
|
label: 'Level one 1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '1-1',
|
||||||
|
label: 'Level two 1-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '1-1-1',
|
||||||
|
label: 'Level three 1-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2',
|
||||||
|
label: 'Level one 2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-1',
|
||||||
|
label: 'Level two 2-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-1-1',
|
||||||
|
label: 'Level three 2-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2-2',
|
||||||
|
label: 'Level two 2-2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-2-1',
|
||||||
|
label: 'Level three 2-2-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '3',
|
||||||
|
label: 'Level one 3',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-1',
|
||||||
|
label: 'Level two 3-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-1-1',
|
||||||
|
label: 'Level three 3-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '3-2',
|
||||||
|
label: 'Level two 3-2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-2-1',
|
||||||
|
label: 'Level three 3-2-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
```
|
104
docs/examples/tree-select/slots.vue
Normal file
104
docs/examples/tree-select/slots.vue
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<el-tree-select v-model="value" :data="data">
|
||||||
|
<template #default="{ data: { label } }">
|
||||||
|
{{ label }}<span style="color: gray">(suffix)</span></template
|
||||||
|
>
|
||||||
|
</el-tree-select>
|
||||||
|
<el-divider />
|
||||||
|
use render content:
|
||||||
|
<el-tree-select
|
||||||
|
v-model="value"
|
||||||
|
:data="data"
|
||||||
|
:render-content="renderContent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const value = ref()
|
||||||
|
|
||||||
|
const renderContent = (h, { data }) => {
|
||||||
|
return h(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
color: 'orange',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data.label
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
value: '1',
|
||||||
|
label: 'Level one 1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '1-1',
|
||||||
|
label: 'Level two 1-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '1-1-1',
|
||||||
|
label: 'Level three 1-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2',
|
||||||
|
label: 'Level one 2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-1',
|
||||||
|
label: 'Level two 2-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-1-1',
|
||||||
|
label: 'Level three 2-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2-2',
|
||||||
|
label: 'Level two 2-2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2-2-1',
|
||||||
|
label: 'Level three 2-2-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '3',
|
||||||
|
label: 'Level one 3',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-1',
|
||||||
|
label: 'Level two 3-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-1-1',
|
||||||
|
label: 'Level three 3-1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '3-2',
|
||||||
|
label: 'Level two 3-2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '3-2-1',
|
||||||
|
label: 'Level three 3-2-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
```
|
@ -61,6 +61,7 @@ export * from './timeline'
|
|||||||
export * from './tooltip'
|
export * from './tooltip'
|
||||||
export * from './transfer'
|
export * from './transfer'
|
||||||
export * from './tree'
|
export * from './tree'
|
||||||
|
export * from './tree-select'
|
||||||
export * from './tree-v2'
|
export * from './tree-v2'
|
||||||
export * from './upload'
|
export * from './upload'
|
||||||
export * from './virtual-list'
|
export * from './virtual-list'
|
||||||
|
312
packages/components/tree-select/__tests__/tree-select.spec.ts
Normal file
312
packages/components/tree-select/__tests__/tree-select.spec.ts
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import { h, nextTick, ref } from 'vue'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import TreeSelect from '../src/tree-select.vue'
|
||||||
|
import type { RenderFunction } from 'vue'
|
||||||
|
import type { VueWrapper } from '@vue/test-utils'
|
||||||
|
import type ElSelect from '@element-plus/components/select'
|
||||||
|
import type ElTree from '@element-plus/components/tree'
|
||||||
|
|
||||||
|
const createComponent = ({
|
||||||
|
slots = {},
|
||||||
|
props = {},
|
||||||
|
}: {
|
||||||
|
slots?: Record<string, any>
|
||||||
|
props?: typeof TreeSelect['props']
|
||||||
|
} = {}) => {
|
||||||
|
// vm can not get component expose, use ref
|
||||||
|
const wrapperRef = ref()
|
||||||
|
const value = props.modelValue || ref('')
|
||||||
|
const wrapper = mount({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
modelValue: value,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: '一级 1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 11,
|
||||||
|
label: '二级 1-1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 111,
|
||||||
|
label: '三级 1-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'onUpdate:modelValue': (val: string) => (value.value = val),
|
||||||
|
...props,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h(
|
||||||
|
TreeSelect,
|
||||||
|
{
|
||||||
|
...this.$data,
|
||||||
|
ref: (val: object) => (wrapperRef.value = val),
|
||||||
|
},
|
||||||
|
slots
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrapper,
|
||||||
|
getWrapperRef: () =>
|
||||||
|
new Promise((resolve) =>
|
||||||
|
nextTick(() => resolve(wrapperRef.value))
|
||||||
|
) as Promise<InstanceType<typeof ElTree> & InstanceType<typeof ElSelect>>,
|
||||||
|
select: wrapper.findComponent({ name: 'ElSelect' }) as VueWrapper<
|
||||||
|
InstanceType<typeof ElSelect>
|
||||||
|
>,
|
||||||
|
tree: wrapper.findComponent({ name: 'ElTree' }) as VueWrapper<
|
||||||
|
InstanceType<typeof ElTree>
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('TreeSelect.vue', () => {
|
||||||
|
test('render test', async () => {
|
||||||
|
const { wrapper, tree } = createComponent({
|
||||||
|
props: {
|
||||||
|
defaultExpandAll: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.find('.el-tree')).toBeTruthy()
|
||||||
|
expect(wrapper.find('.el-select')).toBeTruthy()
|
||||||
|
|
||||||
|
expect(tree.findAll('.el-tree > .el-tree-node').length).toBe(1)
|
||||||
|
expect(tree.findAll('.el-tree .el-tree-node').length).toBe(3)
|
||||||
|
expect(tree.findAll('.el-tree .el-select-dropdown__item').length).toBe(3)
|
||||||
|
|
||||||
|
wrapper.vm.data[0].children = []
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(tree.findAll('.el-tree .el-tree-node').length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('modelValue', async () => {
|
||||||
|
const value = ref(1)
|
||||||
|
const { getWrapperRef, select, tree } = createComponent({
|
||||||
|
props: {
|
||||||
|
modelValue: value,
|
||||||
|
checkStrictly: true,
|
||||||
|
showCheckbox: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapperRef = await getWrapperRef()
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
expect(select.vm.modelValue).toBe(1)
|
||||||
|
expect(wrapperRef.getCheckedKeys()).toEqual([1])
|
||||||
|
|
||||||
|
value.value = 11
|
||||||
|
await nextTick(nextTick)
|
||||||
|
expect(select.vm.modelValue).toBe(11)
|
||||||
|
expect(wrapperRef.getCheckedKeys()).toEqual([11])
|
||||||
|
|
||||||
|
await tree
|
||||||
|
.findAll('.el-select-dropdown__item')
|
||||||
|
.slice(-1)[0]
|
||||||
|
.trigger('click')
|
||||||
|
await nextTick()
|
||||||
|
expect(select.vm.modelValue).toBe(111)
|
||||||
|
expect(wrapperRef.getCheckedKeys()).toEqual([111])
|
||||||
|
|
||||||
|
await tree.find('.el-tree-node__content').trigger('click')
|
||||||
|
await nextTick()
|
||||||
|
expect(select.vm.modelValue).toBe(1)
|
||||||
|
expect(wrapperRef.getCheckedKeys()).toEqual([1])
|
||||||
|
|
||||||
|
await tree.findAll('.el-checkbox')[1].trigger('click')
|
||||||
|
await nextTick()
|
||||||
|
expect(select.vm.modelValue).toBe(11)
|
||||||
|
expect(wrapperRef.getCheckedKeys()).toEqual([11])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('disabled', async () => {
|
||||||
|
const { wrapper, tree } = createComponent({
|
||||||
|
props: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: '1',
|
||||||
|
label: '1',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: '2',
|
||||||
|
label: '2',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showCheckbox: true,
|
||||||
|
checkStrictly: true,
|
||||||
|
defaultExpandAll: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
await tree.find('.el-tree-node').trigger('click')
|
||||||
|
await tree.find('.el-tree-node .el-checkbox.is-disabled').trigger('click')
|
||||||
|
await tree
|
||||||
|
.find('.el-tree-node .el-select-dropdown__item.is-disabled')
|
||||||
|
.trigger('click')
|
||||||
|
await nextTick()
|
||||||
|
expect(wrapper.vm.modelValue).toBe('1')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('multiple', async () => {
|
||||||
|
const value = ref([1])
|
||||||
|
const { getWrapperRef, select, tree } = createComponent({
|
||||||
|
props: {
|
||||||
|
modelValue: value,
|
||||||
|
checkStrictly: true,
|
||||||
|
showCheckbox: true,
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapperRef = await getWrapperRef()
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
expect(select.vm.modelValue).toEqual([1])
|
||||||
|
expect(wrapperRef.getCheckedKeys()).toEqual([1])
|
||||||
|
|
||||||
|
value.value = [11]
|
||||||
|
await nextTick(nextTick)
|
||||||
|
expect(select.vm.modelValue).toEqual([11])
|
||||||
|
expect(wrapperRef.getCheckedKeys()).toEqual([11])
|
||||||
|
|
||||||
|
await tree
|
||||||
|
.findAll('.el-select-dropdown__item')
|
||||||
|
.slice(-1)[0]
|
||||||
|
.trigger('click')
|
||||||
|
await nextTick()
|
||||||
|
expect(select.vm.modelValue).toEqual([11, 111])
|
||||||
|
expect(wrapperRef.getCheckedKeys()).toEqual([11, 111])
|
||||||
|
|
||||||
|
await tree.find('.el-tree-node__content').trigger('click')
|
||||||
|
await nextTick()
|
||||||
|
expect(select.vm.modelValue).toEqual([11, 111, 1])
|
||||||
|
expect(wrapperRef.getCheckedKeys()).toEqual([1, 11, 111])
|
||||||
|
|
||||||
|
await tree.findAll('.el-checkbox')[1].trigger('click')
|
||||||
|
await nextTick()
|
||||||
|
expect(select.vm.modelValue).toEqual([1, 111])
|
||||||
|
expect(wrapperRef.getCheckedKeys()).toEqual([1, 111])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('filter', async () => {
|
||||||
|
const { select, tree } = createComponent({
|
||||||
|
props: {
|
||||||
|
filterable: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
select.vm.query = '一级 1'
|
||||||
|
await nextTick()
|
||||||
|
expect(tree.findAll('.el-tree-node').length).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('props', async () => {
|
||||||
|
const { wrapper, select, tree } = createComponent({
|
||||||
|
props: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: '1',
|
||||||
|
childrens: [
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: '2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
label: 'name',
|
||||||
|
children: 'childrens',
|
||||||
|
},
|
||||||
|
valueKey: 'id',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
expect(tree.find('.el-tree-node').text()).toBe('1')
|
||||||
|
wrapper.vm.modelValue = '2'
|
||||||
|
await nextTick()
|
||||||
|
expect(select.vm.selectedLabel).toBe('2')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('slots', async () => {
|
||||||
|
const { select, tree } = createComponent({
|
||||||
|
slots: {
|
||||||
|
default: ({ data }: { data: { label: string } }) => `123${data.label}`,
|
||||||
|
prefix: () => 'prefix',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
expect(tree.find('.el-select-dropdown__item').text()).toBe('123一级 1')
|
||||||
|
expect(select.find('.el-input__prefix-inner').text()).toBe('prefix')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('renderContent', async () => {
|
||||||
|
const { tree } = createComponent({
|
||||||
|
props: {
|
||||||
|
renderContent: (
|
||||||
|
h: RenderFunction,
|
||||||
|
{ data }: { data: { label: string } }
|
||||||
|
) => {
|
||||||
|
return `123${data.label}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
expect(tree.find('.el-select-dropdown__item').text()).toBe('123一级 1')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('lazy', async () => {
|
||||||
|
const { tree } = createComponent({
|
||||||
|
props: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
lazy: true,
|
||||||
|
load: (node: object, resolve: (p: any) => any[]) => {
|
||||||
|
resolve([{ value: 2, label: 2, isLeaf: true }])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
await tree.find('.el-tree-node').trigger('click')
|
||||||
|
await nextTick()
|
||||||
|
expect(tree.find('.el-tree-node .el-tree-node').text()).toBe('2')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('events', async () => {
|
||||||
|
const onNodeClick = jest.fn()
|
||||||
|
const { tree } = createComponent({
|
||||||
|
props: {
|
||||||
|
onNodeClick,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await nextTick()
|
||||||
|
await tree.find('.el-tree-node').trigger('click')
|
||||||
|
await nextTick()
|
||||||
|
expect(onNodeClick).toBeCalled()
|
||||||
|
})
|
||||||
|
})
|
13
packages/components/tree-select/index.ts
Normal file
13
packages/components/tree-select/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import TreeSelect from './src/tree-select.vue'
|
||||||
|
|
||||||
|
import type { App } from 'vue'
|
||||||
|
import type { SFCWithInstall } from '@element-plus/utils'
|
||||||
|
|
||||||
|
TreeSelect.install = (app: App): void => {
|
||||||
|
app.component(TreeSelect.name, TreeSelect)
|
||||||
|
}
|
||||||
|
|
||||||
|
const _TreeSelect = TreeSelect as SFCWithInstall<typeof TreeSelect>
|
||||||
|
|
||||||
|
export default _TreeSelect
|
||||||
|
export const ElTreeSelect = _TreeSelect
|
50
packages/components/tree-select/src/select.ts
Normal file
50
packages/components/tree-select/src/select.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { computed, nextTick, toRefs } from 'vue'
|
||||||
|
import { pick } from 'lodash-unified'
|
||||||
|
import ElSelect from '@element-plus/components/select'
|
||||||
|
import { useNamespace } from '@element-plus/hooks'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type ElTree from '@element-plus/components/tree'
|
||||||
|
|
||||||
|
export const useSelect = (
|
||||||
|
props,
|
||||||
|
{ attrs },
|
||||||
|
{
|
||||||
|
tree,
|
||||||
|
key,
|
||||||
|
}: {
|
||||||
|
select: Ref<InstanceType<typeof ElSelect> | undefined>
|
||||||
|
tree: Ref<InstanceType<typeof ElTree> | undefined>
|
||||||
|
key: Ref<string>
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const ns = useNamespace('tree-select')
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
...pick(toRefs(props), Object.keys(ElSelect.props)),
|
||||||
|
...attrs,
|
||||||
|
valueKey: key,
|
||||||
|
popperClass: computed(() => {
|
||||||
|
const classes = [ns.e('popper')]
|
||||||
|
if (props.popperClass) classes.push(props.popperClass)
|
||||||
|
return classes.join(' ')
|
||||||
|
}),
|
||||||
|
filterMethod: (keyword = '') => {
|
||||||
|
if (props.filterMethod) props.filterMethod(keyword)
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
// let tree node expand only, same with tree filter
|
||||||
|
tree.value?.filter(keyword)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// clear filter text when visible change
|
||||||
|
onVisibleChange: (visible: boolean) => {
|
||||||
|
attrs.onVisibleChange?.(visible)
|
||||||
|
|
||||||
|
if (props.filterable && visible) {
|
||||||
|
result.filterMethod()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
22
packages/components/tree-select/src/tree-select-option.ts
Normal file
22
packages/components/tree-select/src/tree-select-option.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { ElOption } from '@element-plus/components/select'
|
||||||
|
|
||||||
|
const component = defineComponent({
|
||||||
|
extends: ElOption,
|
||||||
|
setup(props, ctx) {
|
||||||
|
const result = (ElOption.setup as NonNullable<any>)(props, ctx)
|
||||||
|
|
||||||
|
// use methods.selectOptionClick
|
||||||
|
delete result.selectOptionClick
|
||||||
|
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectOptionClick() {
|
||||||
|
// $el.parentElement => el-tree-node__content
|
||||||
|
this.$el.parentElement.click()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default component
|
83
packages/components/tree-select/src/tree-select.vue
Normal file
83
packages/components/tree-select/src/tree-select.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, h, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { pick } from 'lodash-unified'
|
||||||
|
import ElSelect from '@element-plus/components/select'
|
||||||
|
import ElTree from '@element-plus/components/tree'
|
||||||
|
import { useSelect } from './select'
|
||||||
|
import { useTree } from './tree'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ElTreeSelect',
|
||||||
|
props: {
|
||||||
|
...ElSelect.props,
|
||||||
|
...ElTree.props,
|
||||||
|
},
|
||||||
|
setup(props, context) {
|
||||||
|
const { slots, expose } = context
|
||||||
|
|
||||||
|
const select = ref<InstanceType<typeof ElSelect>>()
|
||||||
|
const tree = ref<InstanceType<typeof ElTree>>()
|
||||||
|
|
||||||
|
const key = computed(() => props.valueKey || props.nodeKey || 'value')
|
||||||
|
|
||||||
|
const selectProps = useSelect(props, context, { select, tree, key })
|
||||||
|
const treeProps = useTree(props, context, { select, tree, key })
|
||||||
|
|
||||||
|
// expose ElTree/ElSelect methods
|
||||||
|
const methods = reactive({})
|
||||||
|
expose(methods)
|
||||||
|
onMounted(() => {
|
||||||
|
Object.assign(methods, {
|
||||||
|
...pick(tree.value, [
|
||||||
|
'filter',
|
||||||
|
'updateKeyChildren',
|
||||||
|
'getCheckedNodes',
|
||||||
|
'setCheckedNodes',
|
||||||
|
'getCheckedKeys',
|
||||||
|
'setCheckedKeys',
|
||||||
|
'setChecked',
|
||||||
|
'getHalfCheckedNodes',
|
||||||
|
'getHalfCheckedKeys',
|
||||||
|
'getCurrentKey',
|
||||||
|
'getCurrentNode',
|
||||||
|
'setCurrentKey',
|
||||||
|
'setCurrentNode',
|
||||||
|
'getNode',
|
||||||
|
'remove',
|
||||||
|
'append',
|
||||||
|
'insertBefore',
|
||||||
|
'insertAfter',
|
||||||
|
]),
|
||||||
|
...pick(select.value, ['focus', 'blur']),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
h(
|
||||||
|
ElSelect,
|
||||||
|
/**
|
||||||
|
* 1. The `props` is processed into `Refs`, but `v-bind` and
|
||||||
|
* render function props cannot read `Refs`, so use `reactive`
|
||||||
|
* unwrap the `Refs` and keep reactive.
|
||||||
|
* 2. The keyword `ref` requires `Ref`, but `reactive` broke it,
|
||||||
|
* so use function.
|
||||||
|
*/
|
||||||
|
reactive({
|
||||||
|
...selectProps,
|
||||||
|
ref: (ref) => (select.value = ref),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
...slots,
|
||||||
|
default: () =>
|
||||||
|
h(
|
||||||
|
ElTree,
|
||||||
|
reactive({
|
||||||
|
...treeProps,
|
||||||
|
ref: (ref) => (tree.value = ref),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
136
packages/components/tree-select/src/tree.ts
Normal file
136
packages/components/tree-select/src/tree.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { computed, nextTick, toRefs, watch } from 'vue'
|
||||||
|
import { isEqual, pick } from 'lodash-unified'
|
||||||
|
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||||
|
import { isFunction } from '@element-plus/utils'
|
||||||
|
import ElTree from '@element-plus/components/tree'
|
||||||
|
import TreeSelectOption from './tree-select-option'
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
import type ElSelect from '@element-plus/components/select'
|
||||||
|
import type Node from '@element-plus/components/tree/src/model/node'
|
||||||
|
import type { TreeNodeData } from '@element-plus/components/tree/src/tree.type'
|
||||||
|
|
||||||
|
export const useTree = (
|
||||||
|
props,
|
||||||
|
{ attrs, slots, emit },
|
||||||
|
{
|
||||||
|
select,
|
||||||
|
tree,
|
||||||
|
key,
|
||||||
|
}: {
|
||||||
|
select: Ref<InstanceType<typeof ElSelect> | undefined>
|
||||||
|
tree: Ref<InstanceType<typeof ElTree> | undefined>
|
||||||
|
key: Ref<string>
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
() => {
|
||||||
|
if (props.showCheckbox) {
|
||||||
|
nextTick(() => {
|
||||||
|
const treeInstance = tree.value
|
||||||
|
if (
|
||||||
|
treeInstance &&
|
||||||
|
!isEqual(
|
||||||
|
treeInstance.getCheckedKeys(),
|
||||||
|
toValidArray(props.modelValue)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
treeInstance.setCheckedKeys(toValidArray(props.modelValue))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const propsMap = computed(() => ({
|
||||||
|
value: key.value,
|
||||||
|
...props.props,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const getNodeValByProp = (
|
||||||
|
prop: 'value' | 'label' | 'children' | 'disabled' | 'isLeaf',
|
||||||
|
data: TreeNodeData
|
||||||
|
) => {
|
||||||
|
const propVal = propsMap.value[prop]
|
||||||
|
if (isFunction(propVal)) {
|
||||||
|
return propVal(
|
||||||
|
data,
|
||||||
|
tree.value?.getNode(getNodeValByProp('value', data)) as Node
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return data[propVal as string]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pick(toRefs(props), Object.keys(ElTree.props)),
|
||||||
|
...attrs,
|
||||||
|
nodeKey: key,
|
||||||
|
defaultExpandedKeys: computed(() =>
|
||||||
|
props.defaultExpandedKeys
|
||||||
|
? props.defaultExpandedKeys.concat(props.modelValue)
|
||||||
|
: toValidArray(props.modelValue)
|
||||||
|
),
|
||||||
|
renderContent: (h, { node, data, store }) => {
|
||||||
|
return h(
|
||||||
|
TreeSelectOption,
|
||||||
|
{
|
||||||
|
value: getNodeValByProp('value', data),
|
||||||
|
label: getNodeValByProp('label', data),
|
||||||
|
disabled: getNodeValByProp('disabled', data),
|
||||||
|
},
|
||||||
|
props.renderContent
|
||||||
|
? () => props.renderContent(h, { node, data, store })
|
||||||
|
: slots.default
|
||||||
|
? () => slots.default({ node, data, store })
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
},
|
||||||
|
filterNodeMethod: (value, data, node) => {
|
||||||
|
if (props.filterNodeMethod)
|
||||||
|
return props.filterNodeMethod(value, data, node)
|
||||||
|
if (!value) return true
|
||||||
|
return getNodeValByProp('label', data)?.includes(value)
|
||||||
|
},
|
||||||
|
onNodeClick: (data, node, e) => {
|
||||||
|
attrs.onNodeClick?.(data, node, e)
|
||||||
|
|
||||||
|
if (props.checkStrictly || node.isLeaf) {
|
||||||
|
if (!getNodeValByProp('disabled', data)) {
|
||||||
|
const option = select.value?.options.get(
|
||||||
|
getNodeValByProp('value', data)
|
||||||
|
)
|
||||||
|
select.value?.handleOptionSelect(option, true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
e.ctx.handleExpandIconClick()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCheck: (data, params) => {
|
||||||
|
attrs.onCheck?.(data, params)
|
||||||
|
|
||||||
|
// remove folder node when `checkStrictly` is false
|
||||||
|
const checkedKeys = !props.checkStrictly
|
||||||
|
? tree.value?.getCheckedKeys(true)
|
||||||
|
: params.checkedKeys
|
||||||
|
|
||||||
|
const value = getNodeValByProp('value', data)
|
||||||
|
emit(
|
||||||
|
UPDATE_MODEL_EVENT,
|
||||||
|
props.multiple
|
||||||
|
? checkedKeys
|
||||||
|
: checkedKeys.includes(value)
|
||||||
|
? value
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toValidArray(val: any) {
|
||||||
|
return Array.isArray(val) ? val : val || val === 0 ? [val] : []
|
||||||
|
}
|
3
packages/components/tree-select/style/css.ts
Normal file
3
packages/components/tree-select/style/css.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import '@element-plus/components/select/style/css'
|
||||||
|
import '@element-plus/components/tree/style/css'
|
||||||
|
import '@element-plus/theme-chalk/src/tree-select/css'
|
3
packages/components/tree-select/style/index.ts
Normal file
3
packages/components/tree-select/style/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import '@element-plus/components/select/style'
|
||||||
|
import '@element-plus/components/tree/style'
|
||||||
|
import '@element-plus/theme-chalk/src/tree-select.scss'
|
@ -94,6 +94,7 @@ import { ElTooltip } from '@element-plus/components/tooltip'
|
|||||||
import { ElTooltipV2 } from '@element-plus/components/tooltip-v2'
|
import { ElTooltipV2 } from '@element-plus/components/tooltip-v2'
|
||||||
import { ElTransfer } from '@element-plus/components/transfer'
|
import { ElTransfer } from '@element-plus/components/transfer'
|
||||||
import { ElTree } from '@element-plus/components/tree'
|
import { ElTree } from '@element-plus/components/tree'
|
||||||
|
import { ElTreeSelect } from '@element-plus/components/tree-select'
|
||||||
import { ElTreeV2 } from '@element-plus/components/tree-v2'
|
import { ElTreeV2 } from '@element-plus/components/tree-v2'
|
||||||
import { ElUpload } from '@element-plus/components/upload'
|
import { ElUpload } from '@element-plus/components/upload'
|
||||||
import type { Plugin } from 'vue'
|
import type { Plugin } from 'vue'
|
||||||
@ -188,6 +189,7 @@ export default [
|
|||||||
ElTooltipV2,
|
ElTooltipV2,
|
||||||
ElTransfer,
|
ElTransfer,
|
||||||
ElTree,
|
ElTree,
|
||||||
|
ElTreeSelect,
|
||||||
ElTreeV2,
|
ElTreeV2,
|
||||||
ElUpload,
|
ElUpload,
|
||||||
] as Plugin[]
|
] as Plugin[]
|
||||||
|
@ -92,6 +92,7 @@
|
|||||||
@use './tooltip-v2.scss';
|
@use './tooltip-v2.scss';
|
||||||
@use './transfer.scss';
|
@use './transfer.scss';
|
||||||
@use './tree.scss';
|
@use './tree.scss';
|
||||||
|
@use './tree-select.scss';
|
||||||
@use './upload.scss';
|
@use './upload.scss';
|
||||||
@use './virtual-list.scss';
|
@use './virtual-list.scss';
|
||||||
@use './popper.scss';
|
@use './popper.scss';
|
||||||
|
36
packages/theme-chalk/src/tree-select.scss
Normal file
36
packages/theme-chalk/src/tree-select.scss
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
@use 'mixins/mixins' as *;
|
||||||
|
@use 'mixins/var' as *;
|
||||||
|
@use 'common/var' as *;
|
||||||
|
|
||||||
|
@include b(tree-select) {
|
||||||
|
@include set-component-css-var('tree', $tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include b(tree-select) {
|
||||||
|
@include e(popper) {
|
||||||
|
// padding-left same with select option
|
||||||
|
.#{$namespace}-tree-node__expand-icon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove icon when show checkbox
|
||||||
|
.#{$namespace}-tree-node.is-checked
|
||||||
|
> .#{$namespace}-tree-node__content
|
||||||
|
.#{$namespace}-select-dropdown__item.selected::after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$namespace}-select-dropdown__item {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent !important;
|
||||||
|
|
||||||
|
// padding-left move to `el-tree-node__expand-icon`
|
||||||
|
padding-left: 0;
|
||||||
|
|
||||||
|
// fix: select height > tree node height
|
||||||
|
// https://github.com/yujinpan/el-select-tree/pull/33
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user