mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
feat(components): add virtual tree (#3398)
* feat(components): add virtual tree * perf: optimize compute performance * perf: optimize update checked states performance * feat(components): [tree-v2] supports filter method * feat(components): [el-tree-v2] exposes check api * feat(components): [el-tree-v2] exposes current api * feat(components): [el-tree-v2] support contextmenu event * style(components): [el-tree-v2] optimized code style * refactor(components): [el-tree-v2] using svg icon * refactor(components): [el-tree-v2] replace all PropType with buildProp replace all PropType with buildProp, support perfMode, expose setData * refactor: improve prop * docs: [el-tree-v2] improve documention * refactor(components): [el-tree-v2] optimized code Co-authored-by: Kevin <sxzz@sxzz.moe>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"MD033": false
|
||||
"MD033": false,
|
||||
"MD013": false
|
||||
}
|
||||
|
||||
@@ -205,6 +205,10 @@
|
||||
{
|
||||
"link": "/tree",
|
||||
"text": "Tree"
|
||||
},
|
||||
{
|
||||
"link": "/tree-v2",
|
||||
"text": "Virtualized Tree"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
126
docs/en-US/component/tree-v2.md
Normal file
126
docs/en-US/component/tree-v2.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# <ElBadge value="beta">Tree V2 virtualized tree</ElBadge>
|
||||
|
||||
Tree view with blazing fast scrolling performance for any amount of data
|
||||
|
||||
## Basic usage
|
||||
|
||||
Basic tree structure.
|
||||
|
||||
:::demo
|
||||
|
||||
tree-v2/basic
|
||||
|
||||
:::
|
||||
|
||||
## Selectable
|
||||
|
||||
Used for node selection.
|
||||
|
||||
:::demo
|
||||
|
||||
tree-v2/selectable
|
||||
|
||||
:::
|
||||
|
||||
## Disabled checkbox
|
||||
|
||||
The checkbox of a node can be set as disabled.
|
||||
|
||||
:::demo In the example, `disabled` property is declared in defaultProps, and some nodes are set as `disabled: true`. The corresponding checkboxes are disabled and can't be clicked.
|
||||
|
||||
tree-v2/disabled
|
||||
|
||||
:::
|
||||
|
||||
## Default expanded and default checked
|
||||
|
||||
Tree nodes can be initially expanded or checked
|
||||
|
||||
:::demo Use `default-expanded-keys` and `default-checked-keys` to set initially expanded and initially checked nodes respectively.
|
||||
|
||||
tree-v2/default-state
|
||||
|
||||
:::
|
||||
|
||||
## Custom node content
|
||||
|
||||
The content of tree nodes can be customized, so you can add icons or buttons as you will
|
||||
|
||||
:::demo
|
||||
|
||||
tree-v2/custom-node
|
||||
|
||||
:::
|
||||
|
||||
## Tree node filtering
|
||||
|
||||
Tree nodes can be filtered
|
||||
|
||||
:::demo Invoke the `filter` method of the Tree instance to filter tree nodes. Its parameter is the filtering keyword. Note that for it to work, `filter-node-method` is required, and its value is the filtering method.
|
||||
|
||||
tree-v2/filter
|
||||
|
||||
:::
|
||||
|
||||
## Attributes
|
||||
|
||||
| Attribute | Description | Type | Default |
|
||||
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ------- |
|
||||
| data | tree data | array | — |
|
||||
| empty-text | text displayed when data is void | string | — |
|
||||
| props | configuration options, see the following table | object | — |
|
||||
| highlight-current | whether current node is highlighted | boolean | false |
|
||||
| expand-on-click-node | whether to expand or collapse node when clicking on the node, if false, then expand or collapse node only when clicking on the arrow icon. | boolean | true |
|
||||
| check-on-click-node | whether to check or uncheck node when clicking on the node, if false, the node can only be checked or unchecked by clicking on the checkbox. | boolean | false |
|
||||
| default-expanded-keys | array of keys of initially expanded nodes | array | — |
|
||||
| show-checkbox | whether node is selectable | boolean | false |
|
||||
| check-strictly | whether checked state of a node not affects its father and child nodes when `show-checkbox` is `true` | boolean | false |
|
||||
| default-checked-keys | array of keys of initially checked nodes | array | — |
|
||||
| current-node-key | key of initially selected node | string, number | — |
|
||||
| filter-method | this function will be executed on each node when use filter method. if return `false`, tree node will be hidden. | Function(value, data) | — |
|
||||
| indent | horizontal indentation of nodes in adjacent levels in pixels | number | 16 |
|
||||
| icon | custome tree node icon | string | - |
|
||||
|
||||
## props
|
||||
|
||||
| Attribute | Description | Type | Default |
|
||||
| --------- | ------------------------------------------------------------------------------------ | -------------- | -------- |
|
||||
| id | unique identity key name for nodes, its value should be unique across the whole tree | string, number | id |
|
||||
| label | specify which key of node object is used as the node's label | string | label |
|
||||
| children | specify which node object is used as the node's subtree | string | children |
|
||||
| disabled | specify which key of node object represents if node's checkbox is disabled | boolean | disabled |
|
||||
|
||||
## Method
|
||||
|
||||
`Tree` has the following method, which returns the currently selected array of nodes.
|
||||
| Method | Description | Parameters |
|
||||
| --------------- | ---------------------------------------- | ---------------------------------------- |
|
||||
| filter | filter all tree nodes, filtered nodes will be hidden | (query: string) |
|
||||
| getCheckedNodes | If the node can be selected (`show-checkbox` is `true`), it returns the currently selected array of nodes | (leafOnly: boolean) |
|
||||
| getCheckedKeys | If the node can be selected (`show-checkbox` is `true`), it returns the currently selected array of node's keys | (leafOnly: boolean) |
|
||||
| setCheckedKeys | set certain nodes to be checked | (keys: TreeKey[]) |
|
||||
| setChecked | set node to be checked or not | (key: TreeKey, checked: boolean) |
|
||||
| getHalfCheckedNodes | If the node can be selected (`show-checkbox` is `true`), it returns the currently half selected array of nodes | - |
|
||||
| getHalfCheckedKeys | If the node can be selected (`show-checkbox` is `true`), it returns the currently half selected array of node's keys | - |
|
||||
| getCurrentKey | return the highlight node's key (undefined if no node is highlighted) | — |
|
||||
| getCurrentNode | return the highlight node's data (undefined if no node is highlighted) | — |
|
||||
| setCurrentKey | set highlighted node by key | (key: TreeKey) |
|
||||
| setData | When the data is very large, using reactive data will cause the poor performance, so we provide a way to avoid this situation | (data: TreeData) |
|
||||
|
||||
## Events
|
||||
|
||||
| Event Name | Description | Parameters |
|
||||
| ---------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| node-click | triggers when a node is clicked | (data: TreeNodeData, node: TreeNode) |
|
||||
| node-contextmenu | triggers when a node is clicked by right button | (e: Event, data: TreeNodeData, node: TreeNode) |
|
||||
| check-change | triggers when the selected state of the node changes | (data: TreeNodeData, checked: boolean) |
|
||||
| check | triggers after clicking the checkbox of a node | (data: TreeNodeData, info: { checkedKeys: TreeKey[],checkedNodes: TreeData, halfCheckedKeys: TreeKey[], halfCheckedNodes: TreeData,}) |
|
||||
| current-change | triggers when current node changes | (data: TreeNodeData, node: TreeNode) |
|
||||
| node-expand | triggers when current node open | (data: TreeNodeData, node: TreeNode) |
|
||||
| node-collapse | triggers when current node close | (data: TreeNodeData, node: TreeNode) |
|
||||
|
||||
## Slots
|
||||
|
||||
| Name | Description |
|
||||
| ---- | -------------------------------------------------------------------------------------------- |
|
||||
| — | Custom content for tree nodes. The scope parameter is { node: TreeNode, data: TreeNodeData } |
|
||||
44
docs/examples/tree-v2/basic.vue
Normal file
44
docs/examples/tree-v2/basic.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<el-tree-v2 :data="data" :props="props" :height="208"></el-tree-v2>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
const getKey = (prefix, id) => {
|
||||
return `${prefix}-${id}`
|
||||
}
|
||||
|
||||
const createData = (
|
||||
maxDeep,
|
||||
maxChildren,
|
||||
minNodesNumber,
|
||||
deep = 1,
|
||||
key = 'node'
|
||||
) => {
|
||||
let id = 0
|
||||
return new Array(minNodesNumber).fill(deep).map(() => {
|
||||
const childrenNumber =
|
||||
deep === maxDeep ? 0 : Math.round(Math.random() * maxChildren)
|
||||
const nodeKey = getKey(key, ++id)
|
||||
return {
|
||||
id: nodeKey,
|
||||
label: nodeKey,
|
||||
children: childrenNumber
|
||||
? createData(maxDeep, maxChildren, childrenNumber, deep + 1, nodeKey)
|
||||
: undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
data: createData(4, 30, 40),
|
||||
props: ref({
|
||||
id: 'id',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
61
docs/examples/tree-v2/custom-node.vue
Normal file
61
docs/examples/tree-v2/custom-node.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<el-tree-v2 :data="data" :props="props" :height="208">
|
||||
<template #default="{ node }">
|
||||
<span class="prefix" :class="{ 'is-leaf': node.isLeaf }"
|
||||
>[ElementPlus]</span
|
||||
>
|
||||
<span>{{ node.label }}</span>
|
||||
</template>
|
||||
</el-tree-v2>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
const getKey = (prefix, id) => {
|
||||
return `${prefix}-${id}`
|
||||
}
|
||||
|
||||
const createData = (
|
||||
maxDeep,
|
||||
maxChildren,
|
||||
minNodesNumber,
|
||||
deep = 1,
|
||||
key = 'node'
|
||||
) => {
|
||||
let id = 0
|
||||
return new Array(minNodesNumber).fill(deep).map(() => {
|
||||
const childrenNumber =
|
||||
deep === maxDeep ? 0 : Math.round(Math.random() * maxChildren)
|
||||
const nodeKey = getKey(key, ++id)
|
||||
return {
|
||||
id: nodeKey,
|
||||
label: nodeKey,
|
||||
children: childrenNumber
|
||||
? createData(maxDeep, maxChildren, childrenNumber, deep + 1, nodeKey)
|
||||
: undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
data: createData(4, 30, 40),
|
||||
props: ref({
|
||||
id: 'id',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.prefix {
|
||||
color: var(--el-color-primary);
|
||||
margin-right: 10px;
|
||||
&.is-leaf {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
64
docs/examples/tree-v2/default-state.vue
Normal file
64
docs/examples/tree-v2/default-state.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<el-tree-v2
|
||||
:data="data"
|
||||
:height="208"
|
||||
:props="props"
|
||||
show-checkbox
|
||||
:default-checked-keys="defaultCheckedKeys"
|
||||
:default-expanded-keys="defaultExpandedKeys"
|
||||
></el-tree-v2>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
const getKey = (prefix, id) => {
|
||||
return `${prefix}-${id}`
|
||||
}
|
||||
|
||||
const createData = (
|
||||
maxDeep,
|
||||
maxChildren,
|
||||
minNodesNumber,
|
||||
deep = 1,
|
||||
key = 'node'
|
||||
) => {
|
||||
let id = 0
|
||||
return new Array(minNodesNumber).fill(deep).map(() => {
|
||||
const childrenNumber =
|
||||
deep === maxDeep ? 0 : Math.round(Math.random() * maxChildren)
|
||||
const nodeKey = getKey(key, ++id)
|
||||
return {
|
||||
id: nodeKey,
|
||||
label: nodeKey,
|
||||
children: childrenNumber
|
||||
? createData(maxDeep, maxChildren, childrenNumber, deep + 1, nodeKey)
|
||||
: undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const data = createData(4, 30, 40)
|
||||
const checkedKeys: any[] = []
|
||||
const expanedKeys: any[] = []
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
const children = data[i].children
|
||||
if (children) {
|
||||
expanedKeys.push(data[i].id)
|
||||
checkedKeys.push(children[0].id)
|
||||
break
|
||||
}
|
||||
}
|
||||
return {
|
||||
data,
|
||||
props: ref({
|
||||
id: 'id',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
}),
|
||||
defaultCheckedKeys: checkedKeys,
|
||||
defaultExpandedKeys: expanedKeys,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
51
docs/examples/tree-v2/disabled.vue
Normal file
51
docs/examples/tree-v2/disabled.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<el-tree-v2
|
||||
:data="data"
|
||||
:props="props"
|
||||
show-checkbox
|
||||
:height="208"
|
||||
></el-tree-v2>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
const getKey = (prefix, id) => {
|
||||
return `${prefix}-${id}`
|
||||
}
|
||||
|
||||
const createData = (
|
||||
maxDeep,
|
||||
maxChildren,
|
||||
minNodesNumber,
|
||||
deep = 1,
|
||||
key = 'node'
|
||||
) => {
|
||||
let id = 0
|
||||
return new Array(minNodesNumber).fill(deep).map(() => {
|
||||
const childrenNumber =
|
||||
deep === maxDeep ? 0 : Math.round(Math.random() * maxChildren)
|
||||
const nodeKey = getKey(key, ++id)
|
||||
return {
|
||||
id: nodeKey,
|
||||
label: nodeKey,
|
||||
disabled: Math.random() > 0.6,
|
||||
children: childrenNumber
|
||||
? createData(maxDeep, maxChildren, childrenNumber, deep + 1, nodeKey)
|
||||
: undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
data: createData(4, 30, 40),
|
||||
props: ref({
|
||||
id: 'id',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
disabled: 'disabled',
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
65
docs/examples/tree-v2/filter.vue
Normal file
65
docs/examples/tree-v2/filter.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<el-input
|
||||
v-model="query"
|
||||
placeholder="Please enter keyword"
|
||||
@input="onQueryChanged"
|
||||
></el-input>
|
||||
<el-tree-v2
|
||||
ref="treeRef"
|
||||
:data="data"
|
||||
:props="props"
|
||||
:filter-method="filterMethod"
|
||||
:height="208"
|
||||
></el-tree-v2>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
const getKey = (prefix, id) => {
|
||||
return `${prefix}-${id}`
|
||||
}
|
||||
|
||||
const createData = (
|
||||
maxDeep,
|
||||
maxChildren,
|
||||
minNodesNumber,
|
||||
deep = 1,
|
||||
key = 'node'
|
||||
) => {
|
||||
let id = 0
|
||||
return new Array(minNodesNumber).fill(deep).map(() => {
|
||||
const childrenNumber =
|
||||
deep === maxDeep ? 0 : Math.round(Math.random() * maxChildren)
|
||||
const nodeKey = getKey(key, ++id)
|
||||
return {
|
||||
id: nodeKey,
|
||||
label: nodeKey,
|
||||
children: childrenNumber
|
||||
? createData(maxDeep, maxChildren, childrenNumber, deep + 1, nodeKey)
|
||||
: undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const treeRef = ref(null)
|
||||
return {
|
||||
treeRef,
|
||||
data: createData(4, 30, 5),
|
||||
query: ref(''),
|
||||
props: ref({
|
||||
id: 'id',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
}),
|
||||
onQueryChanged(query) {
|
||||
const tree = treeRef.value as any
|
||||
tree?.filter(query)
|
||||
},
|
||||
filterMethod(query, node) {
|
||||
return node.label.indexOf(query) !== -1
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
49
docs/examples/tree-v2/selectable.vue
Normal file
49
docs/examples/tree-v2/selectable.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<el-tree-v2
|
||||
:data="data"
|
||||
:props="props"
|
||||
show-checkbox
|
||||
:height="208"
|
||||
></el-tree-v2>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
const getKey = (prefix, id) => {
|
||||
return `${prefix}-${id}`
|
||||
}
|
||||
|
||||
const createData = (
|
||||
maxDeep,
|
||||
maxChildren,
|
||||
minNodesNumber,
|
||||
deep = 1,
|
||||
key = 'node'
|
||||
) => {
|
||||
let id = 0
|
||||
return new Array(minNodesNumber).fill(deep).map(() => {
|
||||
const childrenNumber =
|
||||
deep === maxDeep ? 0 : Math.round(Math.random() * maxChildren)
|
||||
const nodeKey = getKey(key, ++id)
|
||||
return {
|
||||
id: nodeKey,
|
||||
label: nodeKey,
|
||||
children: childrenNumber
|
||||
? createData(maxDeep, maxChildren, childrenNumber, deep + 1, nodeKey)
|
||||
: undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
data: createData(4, 30, 40),
|
||||
props: ref({
|
||||
id: 'id',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -61,6 +61,7 @@ export * from './timeline'
|
||||
export * from './tooltip'
|
||||
export * from './transfer'
|
||||
export * from './tree'
|
||||
export * from './tree-v2'
|
||||
export * from './upload'
|
||||
export * from './virtual-list'
|
||||
// plugins
|
||||
|
||||
1159
packages/components/tree-v2/__tests__/tree.spec.ts
Normal file
1159
packages/components/tree-v2/__tests__/tree.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
5
packages/components/tree-v2/index.ts
Normal file
5
packages/components/tree-v2/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { withInstall } from '@element-plus/utils/with-install'
|
||||
import TreeV2 from './src/tree.vue'
|
||||
|
||||
export const ElTreeV2 = withInstall(TreeV2)
|
||||
export default ElTreeV2
|
||||
221
packages/components/tree-v2/src/composables/useCheck.ts
Normal file
221
packages/components/tree-v2/src/composables/useCheck.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import { nextTick, ref, watch, getCurrentInstance } from 'vue'
|
||||
import {
|
||||
NODE_CHECK_CHANGE,
|
||||
NODE_CHECK,
|
||||
SetOperationEnum,
|
||||
} from '../virtual-tree'
|
||||
import type { Ref } from 'vue'
|
||||
import type { TreeProps, TreeKey, TreeNode, Tree, TreeNodeData } from '../types'
|
||||
|
||||
export function useCheck(props: TreeProps, tree: Ref<Tree | undefined>) {
|
||||
const checkedKeys = ref<Set<TreeKey>>(new Set())
|
||||
const indeterminateKeys = ref<Set<TreeKey>>(new Set())
|
||||
const { emit } = getCurrentInstance()!
|
||||
|
||||
watch(
|
||||
() => tree.value,
|
||||
() => {
|
||||
return nextTick(() => {
|
||||
_setCheckedKeys(props.defaultCheckedKeys)
|
||||
})
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
|
||||
const updateCheckedKeys = () => {
|
||||
if (!tree.value || !props.showCheckbox || props.checkStrictly) {
|
||||
return
|
||||
}
|
||||
const { levelTreeNodeMap, maxLevel } = tree.value
|
||||
const checkedKeySet = checkedKeys.value
|
||||
const indeterminateKeySet = new Set<TreeKey>()
|
||||
// It is easier to determine the indeterminate state by
|
||||
// traversing from bottom to top
|
||||
// leaf nodes not have indeterminate status and can be skipped
|
||||
for (let level = maxLevel - 1; level >= 1; --level) {
|
||||
const nodes = levelTreeNodeMap.get(level)
|
||||
if (!nodes) continue
|
||||
nodes.forEach((node) => {
|
||||
const children = node.children
|
||||
if (children) {
|
||||
// Whether all child nodes are selected
|
||||
let allChecked = true
|
||||
// Whether a child node is selected
|
||||
let hasChecked = false
|
||||
for (let i = 0; i < children.length; ++i) {
|
||||
const childNode = children[i]
|
||||
const key = childNode.key
|
||||
if (checkedKeySet.has(key)) {
|
||||
hasChecked = true
|
||||
} else if (indeterminateKeySet.has(key)) {
|
||||
allChecked = false
|
||||
hasChecked = true
|
||||
break
|
||||
} else {
|
||||
allChecked = false
|
||||
}
|
||||
}
|
||||
if (allChecked) {
|
||||
checkedKeySet.add(node.key)
|
||||
} else if (hasChecked) {
|
||||
indeterminateKeySet.add(node.key)
|
||||
checkedKeySet.delete(node.key)
|
||||
} else {
|
||||
checkedKeySet.delete(node.key)
|
||||
indeterminateKeySet.delete(node.key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
indeterminateKeys.value = indeterminateKeySet
|
||||
}
|
||||
|
||||
const isChecked = (node: TreeNode) => checkedKeys.value.has(node.key)
|
||||
|
||||
const isIndeterminate = (node: TreeNode) =>
|
||||
indeterminateKeys.value.has(node.key)
|
||||
|
||||
const toggleCheckbox = (
|
||||
node: TreeNode,
|
||||
isChecked: boolean,
|
||||
nodeClick = true
|
||||
) => {
|
||||
const checkedKeySet = checkedKeys.value
|
||||
const toggle = (node: TreeNode, checked: boolean) => {
|
||||
checkedKeySet[checked ? SetOperationEnum.ADD : SetOperationEnum.DELETE](
|
||||
node.key
|
||||
)
|
||||
const children = node.children
|
||||
if (!props.checkStrictly && children) {
|
||||
children.forEach((childNode) => {
|
||||
if (!childNode.disabled) {
|
||||
toggle(childNode, checked)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
toggle(node, isChecked)
|
||||
updateCheckedKeys()
|
||||
if (nodeClick) {
|
||||
afterNodeCheck(node, isChecked)
|
||||
}
|
||||
}
|
||||
|
||||
const afterNodeCheck = (node: TreeNode, checked: boolean) => {
|
||||
const { checkedNodes, checkedKeys } = getChecked()
|
||||
const { halfCheckedNodes, halfCheckedKeys } = getHalfChecked()
|
||||
emit(NODE_CHECK, node.data, {
|
||||
checkedKeys,
|
||||
checkedNodes,
|
||||
halfCheckedKeys,
|
||||
halfCheckedNodes,
|
||||
})
|
||||
emit(NODE_CHECK_CHANGE, node.data, checked)
|
||||
}
|
||||
|
||||
// expose
|
||||
function getCheckedKeys(leafOnly = false): TreeKey[] {
|
||||
return getChecked(leafOnly).checkedKeys
|
||||
}
|
||||
|
||||
function getCheckedNodes(leafOnly = false): TreeNodeData[] {
|
||||
return getChecked(leafOnly).checkedNodes
|
||||
}
|
||||
|
||||
function getHalfCheckedKeys(): TreeKey[] {
|
||||
return getHalfChecked().halfCheckedKeys
|
||||
}
|
||||
|
||||
function getHalfCheckedNodes(): TreeNodeData[] {
|
||||
return getHalfChecked().halfCheckedNodes
|
||||
}
|
||||
|
||||
function getChecked(leafOnly = false): {
|
||||
checkedKeys: TreeKey[]
|
||||
checkedNodes: TreeNodeData[]
|
||||
} {
|
||||
const checkedNodes: TreeNodeData[] = []
|
||||
const keys: TreeKey[] = []
|
||||
if (tree?.value && props.showCheckbox) {
|
||||
const { treeNodeMap } = tree.value
|
||||
checkedKeys.value.forEach((key) => {
|
||||
const node = treeNodeMap.get(key)
|
||||
if (node && (!leafOnly || (leafOnly && node.isLeaf))) {
|
||||
keys.push(key)
|
||||
checkedNodes.push(node.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
return {
|
||||
checkedKeys: keys,
|
||||
checkedNodes,
|
||||
}
|
||||
}
|
||||
|
||||
function getHalfChecked(): {
|
||||
halfCheckedKeys: TreeKey[]
|
||||
halfCheckedNodes: TreeNodeData[]
|
||||
} {
|
||||
const halfCheckedNodes: TreeNodeData[] = []
|
||||
const halfCheckedKeys: TreeKey[] = []
|
||||
if (tree?.value && props.showCheckbox) {
|
||||
const { treeNodeMap } = tree.value
|
||||
indeterminateKeys.value.forEach((key) => {
|
||||
const node = treeNodeMap.get(key)
|
||||
if (node) {
|
||||
halfCheckedKeys.push(key)
|
||||
halfCheckedNodes.push(node.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
return {
|
||||
halfCheckedNodes,
|
||||
halfCheckedKeys,
|
||||
}
|
||||
}
|
||||
|
||||
function setCheckedKeys(keys: TreeKey[]) {
|
||||
checkedKeys.value.clear()
|
||||
_setCheckedKeys(keys)
|
||||
}
|
||||
|
||||
function setChecked(key: TreeKey, isChecked: boolean) {
|
||||
if (tree?.value && props.showCheckbox) {
|
||||
const node = tree.value.treeNodeMap.get(key)
|
||||
if (node) {
|
||||
toggleCheckbox(node, isChecked, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _setCheckedKeys(keys: TreeKey[]) {
|
||||
if (tree?.value) {
|
||||
const { treeNodeMap } = tree.value
|
||||
if (props.showCheckbox && treeNodeMap && keys) {
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
const key = keys[i]
|
||||
const node = treeNodeMap.get(key)
|
||||
if (node && !isChecked(node)) {
|
||||
toggleCheckbox(node, true, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
updateCheckedKeys,
|
||||
toggleCheckbox,
|
||||
isChecked,
|
||||
isIndeterminate,
|
||||
// expose
|
||||
getCheckedKeys,
|
||||
getCheckedNodes,
|
||||
getHalfCheckedKeys,
|
||||
getHalfCheckedNodes,
|
||||
setChecked,
|
||||
setCheckedKeys,
|
||||
}
|
||||
}
|
||||
79
packages/components/tree-v2/src/composables/useFilter.ts
Normal file
79
packages/components/tree-v2/src/composables/useFilter.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { isFunction } from '@vue/shared'
|
||||
import type { Ref } from 'vue'
|
||||
import type { TreeProps, TreeKey, TreeNode, Tree } from '../types'
|
||||
|
||||
// When the data volume is very large using filter will cause lag
|
||||
// I haven't found a better way to optimize it for now
|
||||
// Maybe this problem should be left to the server side
|
||||
export function useFilter(props: TreeProps, tree: Ref<Tree | undefined>) {
|
||||
const hiddenNodeKeySet = ref<Set<TreeKey>>(new Set([]))
|
||||
const hiddenExpandIconKeySet = ref<Set<TreeKey>>(new Set([]))
|
||||
|
||||
const filterable = computed(() => {
|
||||
return isFunction(props.filterMethod)
|
||||
})
|
||||
|
||||
function doFilter(query: string) {
|
||||
if (!filterable.value) {
|
||||
return
|
||||
}
|
||||
const expandKeySet = new Set<TreeKey>()
|
||||
const hiddenExpandIconKeys = hiddenExpandIconKeySet.value
|
||||
const hiddenKeys = hiddenNodeKeySet.value
|
||||
const family: TreeNode[] = []
|
||||
const nodes = tree.value?.treeNodes || []
|
||||
const filter = props.filterMethod
|
||||
hiddenKeys.clear()
|
||||
function traverse(nodes: TreeNode[]) {
|
||||
nodes.forEach((node) => {
|
||||
family.push(node)
|
||||
if (filter?.(query, node.data)) {
|
||||
family.forEach((member) => {
|
||||
expandKeySet.add(member.key)
|
||||
})
|
||||
} else if (node.isLeaf) {
|
||||
hiddenKeys.add(node.key)
|
||||
}
|
||||
const children = node.children
|
||||
if (children) {
|
||||
traverse(children)
|
||||
}
|
||||
if (!node.isLeaf) {
|
||||
if (!expandKeySet.has(node.key)) {
|
||||
hiddenKeys.add(node.key)
|
||||
} else if (children) {
|
||||
// If all child nodes are hidden, then the expand icon will be hidden
|
||||
let allHidden = true
|
||||
for (let i = 0; i < children.length; ++i) {
|
||||
const childNode = children[i]
|
||||
if (!hiddenKeys.has(childNode.key)) {
|
||||
allHidden = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (allHidden) {
|
||||
hiddenExpandIconKeys.add(node.key)
|
||||
} else {
|
||||
hiddenExpandIconKeys.delete(node.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
family.pop()
|
||||
})
|
||||
}
|
||||
traverse(nodes)
|
||||
return expandKeySet
|
||||
}
|
||||
|
||||
function isForceHiddenExpandIcon(node: TreeNode): boolean {
|
||||
return hiddenExpandIconKeySet.value.has(node.key)
|
||||
}
|
||||
|
||||
return {
|
||||
hiddenExpandIconKeySet,
|
||||
hiddenNodeKeySet,
|
||||
doFilter,
|
||||
isForceHiddenExpandIcon,
|
||||
}
|
||||
}
|
||||
295
packages/components/tree-v2/src/composables/useTree.ts
Normal file
295
packages/components/tree-v2/src/composables/useTree.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
import { computed, nextTick, ref, shallowRef, watch } from 'vue'
|
||||
import {
|
||||
NODE_CLICK,
|
||||
NODE_COLLAPSE,
|
||||
NODE_EXPAND,
|
||||
CURRENT_CHANGE,
|
||||
TreeOptionsEnum,
|
||||
} from '../virtual-tree'
|
||||
import { useCheck } from './useCheck'
|
||||
import { useFilter } from './useFilter'
|
||||
import type {
|
||||
TreeProps,
|
||||
TreeNodeData,
|
||||
TreeKey,
|
||||
TreeNode,
|
||||
TreeData,
|
||||
Tree,
|
||||
} from '../types'
|
||||
|
||||
export function useTree(props: TreeProps, emit) {
|
||||
const expandedKeySet = ref<Set<TreeKey>>(new Set(props.defaultExpandedKeys))
|
||||
const currentKey = ref<TreeKey | undefined>()
|
||||
const tree = shallowRef<Tree | undefined>()
|
||||
|
||||
watch(
|
||||
() => props.currentNodeKey,
|
||||
(key) => {
|
||||
currentKey.value = key
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(data: TreeData) => {
|
||||
setData(data)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
|
||||
const {
|
||||
isIndeterminate,
|
||||
isChecked,
|
||||
toggleCheckbox,
|
||||
getCheckedKeys,
|
||||
getCheckedNodes,
|
||||
getHalfCheckedKeys,
|
||||
getHalfCheckedNodes,
|
||||
setChecked,
|
||||
setCheckedKeys,
|
||||
} = useCheck(props, tree)
|
||||
|
||||
const { doFilter, hiddenNodeKeySet, isForceHiddenExpandIcon } = useFilter(
|
||||
props,
|
||||
tree
|
||||
)
|
||||
|
||||
const valueKey = computed(() => {
|
||||
return props.props?.value || TreeOptionsEnum.KEY
|
||||
})
|
||||
const childrenKey = computed(() => {
|
||||
return props.props?.children || TreeOptionsEnum.CHILDREN
|
||||
})
|
||||
const disabledKey = computed(() => {
|
||||
return props.props?.disabled || TreeOptionsEnum.DISABLED
|
||||
})
|
||||
const labelKey = computed(() => {
|
||||
return props.props?.label || TreeOptionsEnum.LABEL
|
||||
})
|
||||
|
||||
const flattenTree = computed(() => {
|
||||
const expandedKeys = expandedKeySet.value
|
||||
const hiddenKeys = hiddenNodeKeySet.value
|
||||
const flattenNodes: TreeNode[] = []
|
||||
const nodes = (tree.value && tree.value.treeNodes) || []
|
||||
function traverse() {
|
||||
const stack: TreeNode[] = []
|
||||
for (let i = nodes.length - 1; i >= 0; --i) {
|
||||
stack.push(nodes[i])
|
||||
}
|
||||
while (stack.length) {
|
||||
const node = stack.pop()
|
||||
if (!node) continue
|
||||
if (!hiddenKeys.has(node.key)) {
|
||||
flattenNodes.push(node)
|
||||
}
|
||||
// Only "visible" nodes will be rendered
|
||||
if (expandedKeys.has(node.key)) {
|
||||
const children = node.children
|
||||
if (children) {
|
||||
const length = children.length
|
||||
for (let i = length - 1; i >= 0; --i) {
|
||||
stack.push(children[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
traverse()
|
||||
return flattenNodes
|
||||
})
|
||||
|
||||
const isNotEmpty = computed(() => {
|
||||
return flattenTree.value.length > 0
|
||||
})
|
||||
|
||||
function createTree(data: TreeData): Tree {
|
||||
const treeNodeMap: Map<TreeKey, TreeNode> = new Map()
|
||||
const levelTreeNodeMap: Map<number, TreeNode[]> = new Map()
|
||||
let maxLevel = 1
|
||||
function traverse(
|
||||
nodes: TreeData,
|
||||
level = 1,
|
||||
parent: TreeNode | undefined = undefined
|
||||
) {
|
||||
const siblings: TreeNode[] = []
|
||||
for (let index = 0; index < nodes.length; ++index) {
|
||||
const rawNode = nodes[index]
|
||||
const value = getKey(rawNode)
|
||||
const node: TreeNode = {
|
||||
level,
|
||||
key: value,
|
||||
data: rawNode,
|
||||
}
|
||||
node.label = getLabel(rawNode)
|
||||
node.parent = parent
|
||||
const children = getChildren(rawNode)
|
||||
node.disabled = getDisabled(rawNode)
|
||||
node.isLeaf = !children || children.length === 0
|
||||
if (children && children.length) {
|
||||
node.children = traverse(children, level + 1, node)
|
||||
}
|
||||
siblings.push(node)
|
||||
treeNodeMap.set(value, node)
|
||||
if (!levelTreeNodeMap.has(level)) {
|
||||
levelTreeNodeMap.set(level, [])
|
||||
}
|
||||
levelTreeNodeMap.get(level)?.push(node)
|
||||
}
|
||||
if (level > maxLevel) {
|
||||
maxLevel = level
|
||||
}
|
||||
return siblings
|
||||
}
|
||||
const treeNodes: TreeNode[] = traverse(data)
|
||||
return {
|
||||
treeNodeMap,
|
||||
levelTreeNodeMap,
|
||||
maxLevel,
|
||||
treeNodes,
|
||||
}
|
||||
}
|
||||
|
||||
function filter(query: string) {
|
||||
const keys = doFilter(query)
|
||||
if (keys) {
|
||||
expandedKeySet.value = keys
|
||||
}
|
||||
}
|
||||
|
||||
function getChildren(node: TreeNodeData): TreeNodeData[] {
|
||||
return node[childrenKey.value]
|
||||
}
|
||||
|
||||
function getKey(node: TreeNodeData): TreeKey {
|
||||
if (!node) {
|
||||
return ''
|
||||
}
|
||||
return node[valueKey.value]
|
||||
}
|
||||
|
||||
function getDisabled(node: TreeNodeData): boolean {
|
||||
return node[disabledKey.value]
|
||||
}
|
||||
|
||||
function getLabel(node: TreeNodeData): string {
|
||||
return node[labelKey.value]
|
||||
}
|
||||
|
||||
function toggleExpand(node: TreeNode) {
|
||||
const expandedKeys = expandedKeySet.value
|
||||
if (expandedKeys.has(node.key)) {
|
||||
collapse(node)
|
||||
} else {
|
||||
expand(node)
|
||||
}
|
||||
}
|
||||
|
||||
function handleNodeClick(node: TreeNode) {
|
||||
emit(NODE_CLICK, node.data, node)
|
||||
handleCurrentChange(node)
|
||||
if (props.expandOnClickNode) {
|
||||
toggleExpand(node)
|
||||
}
|
||||
if (props.showCheckbox && props.checkOnClickNode && !node.disabled) {
|
||||
toggleCheckbox(node, !isChecked(node), true)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCurrentChange(node: TreeNode) {
|
||||
if (!isCurrent(node)) {
|
||||
currentKey.value = node.key
|
||||
emit(CURRENT_CHANGE, node.data, node)
|
||||
}
|
||||
}
|
||||
|
||||
function handleNodeCheck(node: TreeNode, checked: boolean) {
|
||||
toggleCheckbox(node, checked)
|
||||
}
|
||||
|
||||
function expand(node: TreeNode) {
|
||||
const keySet = expandedKeySet.value
|
||||
if (tree?.value && props.accordion) {
|
||||
// whether only one node among the same level can be expanded at one time
|
||||
const { treeNodeMap } = tree.value
|
||||
keySet.forEach((key) => {
|
||||
const node = treeNodeMap.get(key)
|
||||
if (node && node.level === node.level) {
|
||||
keySet.delete(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
keySet.add(node.key)
|
||||
emit(NODE_EXPAND, node.data, node)
|
||||
}
|
||||
|
||||
function collapse(node: TreeNode) {
|
||||
expandedKeySet.value.delete(node.key)
|
||||
emit(NODE_COLLAPSE, node.data, node)
|
||||
}
|
||||
|
||||
function isExpanded(node: TreeNode): boolean {
|
||||
return expandedKeySet.value.has(node.key)
|
||||
}
|
||||
|
||||
function isDisabled(node: TreeNode): boolean {
|
||||
return !!node.disabled
|
||||
}
|
||||
|
||||
function isCurrent(node: TreeNode): boolean {
|
||||
const current = currentKey.value
|
||||
return !!current && current === node.key
|
||||
}
|
||||
|
||||
function getCurrentNode(): TreeNodeData | undefined {
|
||||
if (!currentKey.value) return undefined
|
||||
return tree?.value?.treeNodeMap.get(currentKey.value)?.data
|
||||
}
|
||||
|
||||
function getCurrentKey(): TreeKey | undefined {
|
||||
return currentKey.value
|
||||
}
|
||||
|
||||
function setCurrentKey(key: TreeKey): void {
|
||||
currentKey.value = key
|
||||
}
|
||||
|
||||
function setData(data: TreeData) {
|
||||
nextTick(() => (tree.value = createTree(data)))
|
||||
}
|
||||
|
||||
return {
|
||||
tree,
|
||||
flattenTree,
|
||||
isNotEmpty,
|
||||
getKey,
|
||||
getChildren,
|
||||
toggleExpand,
|
||||
toggleCheckbox,
|
||||
isExpanded,
|
||||
isChecked,
|
||||
isIndeterminate,
|
||||
isDisabled,
|
||||
isCurrent,
|
||||
isForceHiddenExpandIcon,
|
||||
handleNodeClick,
|
||||
handleNodeCheck,
|
||||
// expose
|
||||
getCurrentNode,
|
||||
getCurrentKey,
|
||||
setCurrentKey,
|
||||
getCheckedKeys,
|
||||
getCheckedNodes,
|
||||
getHalfCheckedKeys,
|
||||
getHalfCheckedNodes,
|
||||
setChecked,
|
||||
setCheckedKeys,
|
||||
filter,
|
||||
setData,
|
||||
}
|
||||
}
|
||||
17
packages/components/tree-v2/src/tree-node-content.ts
Normal file
17
packages/components/tree-v2/src/tree-node-content.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { h, defineComponent, inject } from 'vue'
|
||||
import { ROOT_TREE_INJECTION_KEY, treeNodeContentProps } from './virtual-tree'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElTreeNodeContent',
|
||||
props: treeNodeContentProps,
|
||||
setup(props) {
|
||||
const tree = inject(ROOT_TREE_INJECTION_KEY)
|
||||
return () => {
|
||||
const node = props.node
|
||||
const { data } = node!
|
||||
return tree?.ctx.slots.default
|
||||
? tree.ctx.slots.default({ node, data })
|
||||
: h('span', { class: 'el-tree-node__label' }, [node?.label])
|
||||
}
|
||||
},
|
||||
})
|
||||
113
packages/components/tree-v2/src/tree-node.vue
Normal file
113
packages/components/tree-v2/src/tree-node.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div
|
||||
ref="node$"
|
||||
class="el-tree-node"
|
||||
:class="{
|
||||
'is-expanded': expanded,
|
||||
'is-current': current,
|
||||
'is-focusable': !disabled,
|
||||
'is-checked': !disabled && checked,
|
||||
}"
|
||||
role="treeitem"
|
||||
tabindex="-1"
|
||||
:aria-expanded="expanded"
|
||||
:aria-disabled="disabled"
|
||||
:aria-checked="checked"
|
||||
:data-key="node?.key"
|
||||
@click.stop="handleClick"
|
||||
@contextmenu="handleContextMenu"
|
||||
>
|
||||
<div
|
||||
class="el-tree-node__content"
|
||||
:style="{ paddingLeft: `${(node.level - 1) * indent}px` }"
|
||||
>
|
||||
<el-icon
|
||||
:class="[
|
||||
{
|
||||
'is-leaf': node?.isLeaf,
|
||||
'is-hidden': hiddenExpandIcon,
|
||||
expanded: !node?.isLeaf && expanded,
|
||||
},
|
||||
'el-tree-node__expand-icon',
|
||||
]"
|
||||
@click.stop="handleExpandIconClick"
|
||||
>
|
||||
<component :is="icon" />
|
||||
</el-icon>
|
||||
<el-checkbox
|
||||
v-if="showCheckbox"
|
||||
:model-value="checked"
|
||||
:indeterminate="indeterminate"
|
||||
:disabled="disabled"
|
||||
@change="handleCheckChange"
|
||||
@click.stop
|
||||
/>
|
||||
<el-node-content :node="node" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, inject } from 'vue'
|
||||
import { CaretRight } from '@element-plus/icons'
|
||||
import ElIcon from '@element-plus/components/icon'
|
||||
import ElCheckbox from '@element-plus/components/checkbox'
|
||||
import ElNodeContent from './tree-node-content'
|
||||
import {
|
||||
ROOT_TREE_INJECTION_KEY,
|
||||
NODE_CONTEXTMENU,
|
||||
treeNodeEmits,
|
||||
treeNodeProps,
|
||||
} from './virtual-tree'
|
||||
|
||||
const DEFAULT_ICON = 'caret-right'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElTreeNode',
|
||||
components: {
|
||||
ElIcon,
|
||||
CaretRight,
|
||||
ElCheckbox,
|
||||
ElNodeContent,
|
||||
},
|
||||
props: treeNodeProps,
|
||||
emits: treeNodeEmits,
|
||||
setup(props, { emit }) {
|
||||
const tree = inject(ROOT_TREE_INJECTION_KEY)
|
||||
|
||||
const indent = computed(() => {
|
||||
return tree?.props.indent || 16
|
||||
})
|
||||
|
||||
const icon = computed(() => {
|
||||
return tree?.props.icon ? tree.props.icon : DEFAULT_ICON
|
||||
})
|
||||
|
||||
const handleClick = () => {
|
||||
emit('click', props.node)
|
||||
}
|
||||
const handleExpandIconClick = () => {
|
||||
emit('toggle', props.node)
|
||||
}
|
||||
const handleCheckChange = (value: boolean) => {
|
||||
emit('check', props.node, value)
|
||||
}
|
||||
const handleContextMenu = (event: Event) => {
|
||||
if (tree?.instance?.vnode?.props?.['onNodeContextmenu']) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
tree?.ctx.emit(NODE_CONTEXTMENU, event, props.node?.data, props.node)
|
||||
}
|
||||
|
||||
return {
|
||||
indent,
|
||||
icon,
|
||||
handleClick,
|
||||
handleExpandIconClick,
|
||||
handleCheckChange,
|
||||
handleContextMenu,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
127
packages/components/tree-v2/src/tree.vue
Normal file
127
packages/components/tree-v2/src/tree.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div
|
||||
class="el-tree"
|
||||
:class="{
|
||||
'el-tree--highlight-current': highlightCurrent,
|
||||
}"
|
||||
role="tree"
|
||||
>
|
||||
<fixed-size-list
|
||||
v-if="isNotEmpty"
|
||||
class-name="el-tree-virtual-list"
|
||||
:data="flattenTree"
|
||||
:total="flattenTree.length"
|
||||
:height="height"
|
||||
:item-size="itemSize"
|
||||
:perf-mode="perfMode"
|
||||
>
|
||||
<template #default="{ data, index, style }">
|
||||
<el-tree-node
|
||||
:key="data[index].key"
|
||||
:style="style"
|
||||
:node="data[index]"
|
||||
:expanded="isExpanded(data[index])"
|
||||
:show-checkbox="showCheckbox"
|
||||
:checked="isChecked(data[index])"
|
||||
:indeterminate="isIndeterminate(data[index])"
|
||||
:disabled="isDisabled(data[index])"
|
||||
:current="isCurrent(data[index])"
|
||||
:hidden-expand-icon="isForceHiddenExpandIcon(data[index])"
|
||||
@click="handleNodeClick"
|
||||
@toggle="toggleExpand"
|
||||
@check="handleNodeCheck"
|
||||
></el-tree-node>
|
||||
</template>
|
||||
</fixed-size-list>
|
||||
<div v-else class="el-tree__empty-block">
|
||||
<span class="el-tree__empty-text">{{
|
||||
emptyText || t('el.tree.emptyText')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, getCurrentInstance, provide } from 'vue'
|
||||
import { useLocaleInject } from '@element-plus/hooks'
|
||||
import { FixedSizeList } from '@element-plus/components/virtual-list'
|
||||
import { useTree } from './composables/useTree'
|
||||
import ElTreeNode from './tree-node.vue'
|
||||
import { ROOT_TREE_INJECTION_KEY, treeEmits, treeProps } from './virtual-tree'
|
||||
import type { TreeProps } from './types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElTreeV2',
|
||||
components: {
|
||||
ElTreeNode,
|
||||
FixedSizeList,
|
||||
},
|
||||
props: treeProps,
|
||||
emits: treeEmits,
|
||||
setup(props: TreeProps, ctx) {
|
||||
provide(ROOT_TREE_INJECTION_KEY, {
|
||||
ctx,
|
||||
props,
|
||||
instance: getCurrentInstance(),
|
||||
})
|
||||
const { t } = useLocaleInject()
|
||||
const {
|
||||
flattenTree,
|
||||
isNotEmpty,
|
||||
toggleExpand,
|
||||
isExpanded,
|
||||
isIndeterminate,
|
||||
isChecked,
|
||||
isDisabled,
|
||||
isCurrent,
|
||||
isForceHiddenExpandIcon,
|
||||
toggleCheckbox,
|
||||
handleNodeClick,
|
||||
handleNodeCheck,
|
||||
// expose
|
||||
getCurrentNode,
|
||||
getCurrentKey,
|
||||
setCurrentKey,
|
||||
getCheckedKeys,
|
||||
getCheckedNodes,
|
||||
getHalfCheckedKeys,
|
||||
getHalfCheckedNodes,
|
||||
setChecked,
|
||||
setCheckedKeys,
|
||||
filter,
|
||||
setData,
|
||||
} = useTree(props, ctx.emit)
|
||||
|
||||
ctx.expose({
|
||||
getCurrentNode,
|
||||
getCurrentKey,
|
||||
setCurrentKey,
|
||||
getCheckedKeys,
|
||||
getCheckedNodes,
|
||||
getHalfCheckedKeys,
|
||||
getHalfCheckedNodes,
|
||||
setChecked,
|
||||
setCheckedKeys,
|
||||
filter,
|
||||
setData,
|
||||
})
|
||||
|
||||
return {
|
||||
t,
|
||||
flattenTree,
|
||||
itemSize: 26,
|
||||
isNotEmpty,
|
||||
toggleExpand,
|
||||
toggleCheckbox,
|
||||
isExpanded,
|
||||
isIndeterminate,
|
||||
isChecked,
|
||||
isDisabled,
|
||||
isCurrent,
|
||||
isForceHiddenExpandIcon,
|
||||
handleNodeClick,
|
||||
handleNodeCheck,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
54
packages/components/tree-v2/src/types.ts
Normal file
54
packages/components/tree-v2/src/types.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type {
|
||||
ComponentInternalInstance,
|
||||
SetupContext,
|
||||
ExtractPropTypes,
|
||||
} from 'vue'
|
||||
import type { treeProps, treeEmits } from './virtual-tree'
|
||||
|
||||
export type TreeNodeData = Record<string, any>
|
||||
|
||||
export type TreeData = TreeNodeData[]
|
||||
|
||||
export type TreeKey = string | number
|
||||
|
||||
export interface TreeOptionProps {
|
||||
children?: string
|
||||
label?: string
|
||||
value?: string
|
||||
disabled?: string
|
||||
}
|
||||
|
||||
export type TreeProps = ExtractPropTypes<typeof treeProps>
|
||||
|
||||
export interface TreeNode {
|
||||
key: TreeKey
|
||||
level: number
|
||||
parent?: TreeNode
|
||||
children?: TreeNode[]
|
||||
data: TreeNodeData
|
||||
disabled?: boolean
|
||||
label?: string
|
||||
isLeaf?: boolean
|
||||
}
|
||||
|
||||
export interface TreeContext {
|
||||
ctx: SetupContext<typeof treeEmits>
|
||||
instance: ComponentInternalInstance
|
||||
props: TreeProps
|
||||
}
|
||||
|
||||
export interface Tree {
|
||||
treeNodeMap: Map<TreeKey, TreeNode>
|
||||
levelTreeNodeMap: Map<number, TreeNode[]>
|
||||
treeNodes: TreeNode[]
|
||||
maxLevel: number
|
||||
}
|
||||
|
||||
export type FilterMethod = (query: string, node: TreeNodeData) => boolean
|
||||
|
||||
export interface CheckedInfo {
|
||||
checkedKeys: TreeKey[]
|
||||
checkedNodes: TreeData
|
||||
halfCheckedKeys: TreeKey[]
|
||||
halfCheckedNodes: TreeData
|
||||
}
|
||||
182
packages/components/tree-v2/src/virtual-tree.ts
Normal file
182
packages/components/tree-v2/src/virtual-tree.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { buildProp, definePropType, mutable } from '@element-plus/utils/props'
|
||||
import type { InjectionKey } from 'vue'
|
||||
import type { TreeNodeData } from '../../tree/src/tree.type'
|
||||
import type {
|
||||
TreeNode,
|
||||
TreeKey,
|
||||
TreeData,
|
||||
TreeOptionProps,
|
||||
FilterMethod,
|
||||
CheckedInfo,
|
||||
TreeContext,
|
||||
} from './types'
|
||||
|
||||
// constants
|
||||
export const ROOT_TREE_INJECTION_KEY: InjectionKey<TreeContext> = Symbol()
|
||||
const EMPTY_NODE = {
|
||||
key: -1,
|
||||
level: -1,
|
||||
data: {},
|
||||
} as const
|
||||
|
||||
// enums
|
||||
export enum TreeOptionsEnum {
|
||||
KEY = 'id',
|
||||
LABEL = 'label',
|
||||
CHILDREN = 'children',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export const enum SetOperationEnum {
|
||||
ADD = 'add',
|
||||
DELETE = 'delete',
|
||||
}
|
||||
|
||||
// props
|
||||
export const treeProps = {
|
||||
data: buildProp({
|
||||
type: definePropType<TreeData>(Array),
|
||||
default: () => mutable([] as const),
|
||||
} as const),
|
||||
emptyText: buildProp({
|
||||
type: String,
|
||||
}),
|
||||
height: buildProp({
|
||||
type: Number,
|
||||
default: 200,
|
||||
}),
|
||||
props: buildProp({
|
||||
type: definePropType<TreeOptionProps>(Object),
|
||||
default: () =>
|
||||
mutable({
|
||||
children: TreeOptionsEnum.CHILDREN,
|
||||
label: TreeOptionsEnum.LABEL,
|
||||
disabled: TreeOptionsEnum.DISABLED,
|
||||
value: TreeOptionsEnum.KEY,
|
||||
} as const),
|
||||
} as const),
|
||||
highlightCurrent: buildProp({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}),
|
||||
showCheckbox: buildProp({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}),
|
||||
defaultCheckedKeys: buildProp({
|
||||
type: definePropType<TreeKey[]>(Array),
|
||||
default: () => mutable([] as const),
|
||||
} as const),
|
||||
// Whether checked state of a node not affects its father and
|
||||
// child nodes when show-checkbox is true
|
||||
checkStrictly: buildProp({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}),
|
||||
defaultExpandedKeys: buildProp({
|
||||
type: definePropType<TreeKey[]>(Array),
|
||||
default: () => mutable([] as const),
|
||||
} as const),
|
||||
indent: buildProp({
|
||||
type: Number,
|
||||
default: 16,
|
||||
}),
|
||||
icon: buildProp({
|
||||
type: String,
|
||||
}),
|
||||
expandOnClickNode: buildProp({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
}),
|
||||
checkOnClickNode: buildProp({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}),
|
||||
currentNodeKey: buildProp({
|
||||
type: definePropType<TreeKey>([String, Number]),
|
||||
} as const),
|
||||
// TODO need to optimization
|
||||
accordion: buildProp({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}),
|
||||
filterMethod: buildProp({
|
||||
type: definePropType<FilterMethod>(Function),
|
||||
} as const),
|
||||
// Performance mode will increase memory usage, but scrolling will be smoother
|
||||
perfMode: buildProp({
|
||||
type: Boolean,
|
||||
default: true,
|
||||
}),
|
||||
} as const
|
||||
|
||||
export const treeNodeProps = {
|
||||
node: buildProp({
|
||||
type: definePropType<TreeNode>(Object),
|
||||
default: () => mutable(EMPTY_NODE),
|
||||
} as const),
|
||||
expanded: buildProp({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}),
|
||||
checked: buildProp({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}),
|
||||
indeterminate: buildProp({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}),
|
||||
showCheckbox: buildProp({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}),
|
||||
disabled: buildProp({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}),
|
||||
current: buildProp({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}),
|
||||
hiddenExpandIcon: buildProp({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}),
|
||||
} as const
|
||||
|
||||
export const treeNodeContentProps = {
|
||||
node: buildProp({
|
||||
type: definePropType<TreeNode>(Object),
|
||||
required: true,
|
||||
} as const),
|
||||
} as const
|
||||
|
||||
// emits
|
||||
export const NODE_CLICK = 'node-click'
|
||||
export const NODE_EXPAND = 'node-expand'
|
||||
export const NODE_COLLAPSE = 'node-collapse'
|
||||
export const CURRENT_CHANGE = 'current-change'
|
||||
export const NODE_CHECK = 'check'
|
||||
export const NODE_CHECK_CHANGE = 'check-change'
|
||||
export const NODE_CONTEXTMENU = 'node-contextmenu'
|
||||
|
||||
export const treeEmits = {
|
||||
[NODE_CLICK]: (data: TreeNodeData, node: TreeNode) => data && node,
|
||||
[NODE_EXPAND]: (data: TreeNodeData, node: TreeNode) => data && node,
|
||||
[NODE_COLLAPSE]: (data: TreeNodeData, node: TreeNode) => data && node,
|
||||
[CURRENT_CHANGE]: (data: TreeNodeData, node: TreeNode) => data && node,
|
||||
[NODE_CHECK]: (data: TreeNodeData, checkedInfo: CheckedInfo) =>
|
||||
data && checkedInfo,
|
||||
[NODE_CHECK_CHANGE]: (data: TreeNodeData, checked: boolean) =>
|
||||
data && typeof checked === 'boolean',
|
||||
[NODE_CONTEXTMENU]: (event: Event, data: TreeNodeData, node: TreeNode) =>
|
||||
event && data && node,
|
||||
}
|
||||
|
||||
export const treeNodeEmits = {
|
||||
click: (node: TreeNode) => !!node,
|
||||
toggle: (node: TreeNode) => !!node,
|
||||
check: (node: TreeNode, checked: boolean) =>
|
||||
node && typeof checked === 'boolean',
|
||||
}
|
||||
3
packages/components/tree-v2/style/css.ts
Normal file
3
packages/components/tree-v2/style/css.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import '@element-plus/components/base/style/css'
|
||||
import '@element-plus/theme-chalk/el-tree.css'
|
||||
import '@element-plus/components/checkbox/style/css'
|
||||
3
packages/components/tree-v2/style/index.ts
Normal file
3
packages/components/tree-v2/style/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import '@element-plus/components/base/style'
|
||||
import '@element-plus/theme-chalk/src/tree.scss'
|
||||
import '@element-plus/components/checkbox/style/index'
|
||||
@@ -93,6 +93,7 @@ import { ElTimeline, ElTimelineItem } from '@element-plus/components/timeline'
|
||||
import { ElTooltip } from '@element-plus/components/tooltip'
|
||||
import { ElTransfer } from '@element-plus/components/transfer'
|
||||
import { ElTree } from '@element-plus/components/tree'
|
||||
import { ElTreeV2 } from '@element-plus/components/tree-v2'
|
||||
import { ElUpload } from '@element-plus/components/upload'
|
||||
|
||||
export default [
|
||||
@@ -184,5 +185,6 @@ export default [
|
||||
ElTooltip,
|
||||
ElTransfer,
|
||||
ElTree,
|
||||
ElTreeV2,
|
||||
ElUpload,
|
||||
]
|
||||
|
||||
1
packages/element-plus/global.d.ts
vendored
1
packages/element-plus/global.d.ts
vendored
@@ -81,6 +81,7 @@ declare module 'vue' {
|
||||
ElTooltip: typeof import('element-plus')['ElTooltip']
|
||||
ElTransfer: typeof import('element-plus')['ElTransfer']
|
||||
ElTree: typeof import('element-plus')['ElTree']
|
||||
ElTreeV2: typeof import('element-plus')['ElTreeV2']
|
||||
ElUpload: typeof import('element-plus')['ElUpload']
|
||||
ElSpace: typeof import('element-plus')['ElSpace']
|
||||
ElSkeleton: typeof import('element-plus')['ElSkeleton']
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
cursor: pointer;
|
||||
|
||||
& > .#{$namespace}-tree-node__expand-icon {
|
||||
padding: 6px;
|
||||
margin: 6px;
|
||||
}
|
||||
& > label.#{$namespace}-checkbox {
|
||||
margin-right: 8px;
|
||||
@@ -101,6 +101,9 @@
|
||||
color: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
&.is-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@include e(label) {
|
||||
|
||||
Reference in New Issue
Block a user