feat(tree): tree component

This commit is contained in:
bastarder 2020-08-21 16:16:08 +08:00 committed by Herrington Darkholme
parent 4b7f671774
commit 567da19429
33 changed files with 3737 additions and 111 deletions

View File

@ -3,6 +3,8 @@ import Checkbox from './src/checkbox.vue'
import CheckboxButton from './src/checkbox-button.vue'
import CheckboxGroup from './src/checkbox-group.vue'
export { Checkbox }
export default (app: App): void => {
app.component(Checkbox.name, Checkbox)
app.component(CheckboxButton.name, CheckboxButton)

View File

@ -37,6 +37,7 @@ import ElDialog from '@element-plus/dialog'
import ElCalendar from '@element-plus/calendar'
import ElInfiniteScroll from '@element-plus/infinite-scroll'
import ElDrawer from '@element-plus/drawer'
import ElTree from '@element-plus/tree'
export {
ElAlert,
@ -76,6 +77,7 @@ export {
ElCalendar,
ElInfiniteScroll,
ElDrawer,
ElTree,
}
const install = (app: App): void => {
@ -117,6 +119,7 @@ const install = (app: App): void => {
ElCalendar(app)
ElInfiniteScroll(app)
ElDrawer(app)
ElTree(app)
}
const elementUI = {

View File

@ -37,6 +37,7 @@
"@element-plus/notification": "^0.0.0",
"@element-plus/collapse": "^0.0.0",
"@element-plus/time-picker": "^0.0.0",
"@element-plus/tabs": "^0.0.0"
"@element-plus/tabs": "^0.0.0",
"@element-plus/tree": "^0.0.0"
}
}

View File

@ -0,0 +1,837 @@
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import Tree from '../src/tree.vue'
const delay = (t = 10) => new Promise(resolve => {
setTimeout(resolve, t)
})
const ALL_NODE_COUNT = 9
const getTreeVm = (props = '', options = {}) => {
const wrapper = mount(Object.assign({
components: {
'el-tree': Tree,
},
template: `
<el-tree ref="tree" :data="data" ${ props }></el-tree>
`,
data() {
return {
currentNode: null,
nodeExpended: false,
defaultExpandedKeys: [],
defaultCheckedKeys: [],
clickedNode: null,
count: 1,
data: [{
id: 1,
label: '一级 1',
children: [{
id: 11,
label: '二级 1-1',
children: [{
id: 111,
label: '三级 1-1',
}],
}],
}, {
id: 2,
label: '一级 2',
children: [{
id: 21,
label: '二级 2-1',
}, {
id: 22,
label: '二级 2-2',
}],
}, {
id: 3,
label: '一级 3',
children: [{
id: 31,
label: '二级 3-1',
}, {
id: 32,
label: '二级 3-2',
}],
}],
defaultProps: {
children: 'children',
label: 'label',
},
}
},
}, options))
return { wrapper, vm: wrapper.vm }
}
const getDisableTreeVm = (props = '', options = {}) => {
const wrapper = mount(Object.assign({
components: {
'el-tree': Tree,
},
template: `
<el-tree ref="tree" :data="data" ${ props }></el-tree>
`,
data() {
return {
defaultExpandedKeys: [],
defaultCheckedKeys: [],
clickedNode: null,
count: 1,
data: [{
id: 1,
label: '一级 1',
children: [{
id: 11,
label: '二级 1-1',
children: [{
id: 111,
label: '三级 1-1',
disabled: true,
}],
}],
}, {
id: 2,
label: '一级 2',
children: [{
id: 21,
label: '二级 2-1',
}, {
id: 22,
label: '二级 2-2',
}],
}, {
id: 3,
label: '一级 3',
children: [{
id: 31,
label: '二级 3-1',
}, {
id: 32,
label: '二级 3-2',
}],
}],
defaultProps: {
children: 'children',
label: 'label',
disabled: 'disabled',
},
}
},
}, options))
return { wrapper, vm: wrapper.vm }
}
describe('Tree.vue', () => {
test('create', async () => {
const { wrapper, vm } = getTreeVm(`:props="defaultProps" default-expand-all`)
expect(wrapper.find('.el-tree').exists()).toBeTruthy()
expect(wrapper.findAll('.el-tree > .el-tree-node').length).toEqual(3)
expect(wrapper.findAll('.el-tree .el-tree-node').length).toEqual(ALL_NODE_COUNT)
vm.data[1].children = [{ label: '二级 2-1' }] as any
await nextTick()
expect(wrapper.findAll('.el-tree .el-tree-node').length).toEqual(ALL_NODE_COUNT - 1)
})
test('click node', async () => {
const { wrapper, vm } = getTreeVm(`:props="defaultProps" @node-click="handleNodeClick"`, {
methods: {
handleNodeClick(data) {
this.clickedNode = data
},
},
})
const firstNodeContentWrapper = wrapper.find('.el-tree-node__content')
const firstNodeWrapper = wrapper.find('.el-tree-node')
await firstNodeContentWrapper.trigger('click')
await delay() // because node click method to expaned is async
expect(vm.clickedNode.label).toEqual('一级 1')
expect(firstNodeWrapper.classes('is-expanded')).toBe(true)
expect(firstNodeWrapper.classes('is-current')).toBe(true)
await firstNodeContentWrapper.trigger('click')
await delay()
expect(firstNodeWrapper.classes('is-expanded')).toBe(false)
expect(firstNodeWrapper.classes('is-current')).toBe(true)
})
test('emptyText', async () => {
const { wrapper, vm } = getTreeVm(`:props="defaultProps"`)
vm.data = []
await nextTick()
expect(wrapper.findAll('.el-tree__empty-block').length).toEqual(1)
})
test('expandOnNodeClick', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" :expand-on-click-node="false"`)
const firstNodeContentWrapper = wrapper.find('.el-tree-node__content')
const firstNodeWrapper = wrapper.find('.el-tree-node')
await firstNodeContentWrapper.trigger('click')
await delay() // because node click method to expaned is async
expect(firstNodeWrapper.classes('is-expanded')).toBe(false)
})
test('checkOnNodeClick', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" node-key="id" show-checkbox check-on-click-node`)
const treeWrapper = wrapper.findComponent(Tree)
const firstNodeContentWrapper = wrapper.find('.el-tree-node__content')
await firstNodeContentWrapper.trigger('click')
expect(treeWrapper.vm.getCheckedKeys()).toEqual([1, 11, 111])
})
test('current-node-key', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" default-expand-all highlight-current node-key="id" :current-node-key="11"`)
const currentNodeLabelWrapper = wrapper.find('.is-current .el-tree-node__label')
expect(currentNodeLabelWrapper.text()).toEqual('二级 1-1')
expect(wrapper.find('.el-tree--highlight-current').exists()).toBe(true)
})
test('defaultExpandAll', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" default-expand-all`)
const expanedNodeWrappers = wrapper.findAll('.el-tree-node.is-expanded')
expect(expanedNodeWrappers.length).toEqual(ALL_NODE_COUNT)
})
test('defaultExpandedKeys', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" :default-expanded-keys="defaultExpandedKeys" node-key="id"`, {
created() {
this.defaultExpandedKeys = [1, 3]
},
})
const expanedNodeWrappers = wrapper.findAll('.el-tree-node.is-expanded')
expect(expanedNodeWrappers.length).toEqual(2)
})
test('defaultExpandedKeys set', async () => {
const { wrapper, vm } = getTreeVm(`:props="defaultProps" :default-expanded-keys="defaultExpandedKeys" node-key="id"`, {
created() {
this.defaultExpandedKeys = [1, 3]
},
})
let expanedNodeWrappers = wrapper.findAll('.el-tree-node.is-expanded')
expect(expanedNodeWrappers.length).toEqual(2)
vm.defaultExpandedKeys = [2]
await nextTick()
vm.data = JSON.parse(JSON.stringify(vm.data))
await delay()
expanedNodeWrappers = wrapper.findAll('.el-tree-node.is-expanded')
expect(expanedNodeWrappers.length).toEqual(1)
})
test('filter-node-method', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" :filter-node-method="filterNode"`, {
methods: {
filterNode(value, data) {
if (!value) return true
return data.label.indexOf(value) !== -1
},
},
})
const treeWrapper = wrapper.findComponent(Tree)
treeWrapper.vm.filter('2-1')
await nextTick()
expect(treeWrapper.findAll('.el-tree-node.is-hidden').length).toEqual(3)
})
test('autoExpandParent = true', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" :default-expanded-keys="defaultExpandedKeys" node-key="id"`, {
created() {
this.defaultExpandedKeys = [111]
},
})
expect(wrapper.findAll('.el-tree-node.is-expanded').length).toEqual(3)
})
test('autoExpandParent = false', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" :default-expanded-keys="defaultExpandedKeys" node-key="id" :auto-expand-parent="false"`, {
created() {
this.defaultExpandedKeys = [11]
},
})
expect(wrapper.findAll('.el-tree-node.is-expanded').length).toEqual(0)
const firstNodeContentWrapper = wrapper.find('.el-tree-node__content')
await firstNodeContentWrapper.trigger('click')
await delay()
expect(wrapper.findAll('.el-tree-node.is-expanded').length).toEqual(2)
})
test('defaultCheckedKeys & check-strictly = false', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" default-expand-all show-checkbox :default-checked-keys="defaultCheckedKeys" node-key="id"`, {
created() {
this.defaultCheckedKeys = [1]
},
})
expect(wrapper.findAll('.el-checkbox .is-checked').length).toEqual(3)
})
test('defaultCheckedKeys & check-strictly', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" default-expand-all show-checkbox :default-checked-keys="defaultCheckedKeys" node-key="id" check-strictly`, {
created() {
this.defaultCheckedKeys = [1]
},
})
expect(wrapper.findAll('.el-checkbox .is-checked').length).toEqual(1)
})
test('show checkbox', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" show-checkbox`)
const treeWrapper = wrapper.findComponent(Tree)
const treeVm = treeWrapper.vm
const seconNodeContentWrapper = treeWrapper.findAll('.el-tree-node__content')[1]
const seconNodeCheckboxWrapper = seconNodeContentWrapper.find('.el-checkbox')
const seconNodeExpandIconWrapper = seconNodeContentWrapper.find('.el-tree-node__expand-icon')
expect(seconNodeCheckboxWrapper.exists()).toBe(true)
await seconNodeCheckboxWrapper.trigger('click')
expect(treeVm.getCheckedNodes().length).toEqual(3)
expect(treeVm.getCheckedNodes(true).length).toEqual(2)
await seconNodeExpandIconWrapper.trigger('click')
await delay()
const secondTreeNodeWrapper = treeWrapper.findAll('.el-tree-node')[1]
const secondNodefirstLeafCheckboxWrapper = secondTreeNodeWrapper.find('.el-tree-node__children .el-tree-node__content .el-checkbox')
await secondNodefirstLeafCheckboxWrapper.trigger('click')
expect(treeVm.getCheckedNodes().length).toEqual(1)
})
test('check', async () => {
const handleCheckMockFunction = jest.fn()
const { wrapper } = getTreeVm(`:props="defaultProps" show-checkbox @check="handleCheck"`, {
methods: {
handleCheck: handleCheckMockFunction,
},
})
const secondNodeContentWrapper = wrapper.findAll('.el-tree-node__content')[1]
const secondNodeCheckboxWrapper = secondNodeContentWrapper.find('.el-checkbox')
expect(secondNodeCheckboxWrapper.exists()).toBe(true)
await secondNodeCheckboxWrapper.trigger('click')
await delay()
expect(handleCheckMockFunction.mock.calls.length).toBe(1)
const [data, args] = handleCheckMockFunction.mock.calls[0]
expect(data.id).toEqual(2)
expect(args.checkedNodes.length).toEqual(3)
})
test('setCheckedNodes', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" show-checkbox node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const treeVm = treeWrapper.vm
const secondNodeContentWrapper = wrapper.findAll('.el-tree-node__content')[1]
const secondNodeCheckWrapper = secondNodeContentWrapper.find('.el-checkbox')
await secondNodeCheckWrapper.trigger('click')
expect(treeVm.getCheckedNodes().length).toEqual(3)
expect(treeVm.getCheckedNodes(true).length).toEqual(2)
treeVm.setCheckedNodes([])
expect(treeVm.getCheckedNodes().length).toEqual(0)
})
test('setCheckedKeys', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" show-checkbox node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
tree.setCheckedKeys([111])
expect(tree.getCheckedNodes().length).toEqual(3)
expect(tree.getCheckedKeys().length).toEqual(3)
tree.setCheckedKeys([1])
expect(tree.getCheckedNodes().length).toEqual(3)
expect(tree.getCheckedKeys().length).toEqual(3)
tree.setCheckedKeys([2])
expect(tree.getCheckedNodes().length).toEqual(3)
expect(tree.getCheckedKeys().length).toEqual(3)
tree.setCheckedKeys([21])
expect(tree.getCheckedNodes().length).toEqual(1)
expect(tree.getCheckedKeys().length).toEqual(1)
})
test('setCheckedKeys with checkStrictly', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" checkStrictly show-checkbox node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
tree.setCheckedKeys([111])
expect(tree.getCheckedNodes().length).toEqual(1)
expect(tree.getCheckedKeys().length).toEqual(1)
tree.setCheckedKeys([1])
expect(tree.getCheckedNodes().length).toEqual(1)
expect(tree.getCheckedKeys().length).toEqual(1)
tree.setCheckedKeys([2])
expect(tree.getCheckedNodes().length).toEqual(1)
expect(tree.getCheckedKeys().length).toEqual(1)
tree.setCheckedKeys([21, 22])
expect(tree.getCheckedNodes().length).toEqual(2)
expect(tree.getCheckedKeys().length).toEqual(2)
})
test('method setChecked', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" show-checkbox node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
tree.setChecked(111, true, true)
expect(tree.getCheckedNodes().length).toEqual(3)
expect(tree.getCheckedKeys().length).toEqual(3)
tree.setChecked(tree.data[0], false, true)
expect(tree.getCheckedNodes().length).toEqual(0)
expect(tree.getCheckedKeys().length).toEqual(0)
})
test('setCheckedKeys with leafOnly=false', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" show-checkbox node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
tree.setCheckedKeys([1, 11, 111, 2], false)
expect(tree.getCheckedNodes().length).toEqual(6)
expect(tree.getCheckedKeys().length).toEqual(6)
})
test('setCheckedKeys with leafOnly=true', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" show-checkbox node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
tree.setCheckedKeys([2], true)
expect(tree.getCheckedNodes().length).toEqual(2)
expect(tree.getCheckedKeys().length).toEqual(2)
})
test('setCurrentKey', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" show-checkbox node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
tree.setCurrentKey(111)
expect(tree.store.currentNode.data.id).toEqual(111)
tree.setCurrentKey(null)
expect(tree.store.currentNode).toEqual(null)
})
test('setCurrentNode', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" show-checkbox node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
tree.setCurrentNode({
id: 111,
label: '三级 1-1',
})
expect(tree.store.currentNode.data.id).toEqual(111)
tree.setCurrentKey(null)
expect(tree.store.currentNode).toEqual(null)
})
test('getCurrentKey', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" show-checkbox node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
tree.setCurrentKey(111)
expect(tree.getCurrentKey()).toEqual(111)
tree.setCurrentKey(null)
expect(tree.getCurrentKey()).toEqual(null)
})
test('getCurrentNode', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" show-checkbox node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
tree.setCurrentKey(111)
expect(tree.getCurrentNode().id).toEqual(111)
})
test('getNode', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
const node = tree.getNode(111)
expect(node.data.id).toEqual(111)
})
test('remove', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
tree.setCurrentKey(1)
expect(tree.getCurrentNode().id).toEqual(1)
tree.remove(1)
expect(tree.data[0].id).toEqual(2)
expect(tree.getNode(1)).toEqual(null)
expect(tree.getCurrentNode()).toEqual(null)
})
test('append', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
const nodeData = { id: 88, label: '88' }
tree.append(nodeData, tree.getNode(1))
expect(tree.data[0].children.length).toEqual(2)
expect(tree.getNode(88).data).toEqual(nodeData)
})
test('insertBefore', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
const nodeData = { id: 88, label: '88' }
tree.insertBefore(nodeData, tree.getNode(11))
expect(tree.data[0].children.length).toEqual(2)
expect(tree.data[0].children[0]).toEqual(nodeData)
expect(tree.getNode(88).data).toEqual(nodeData)
})
test('insertAfter', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" node-key="id"`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
const nodeData = { id: 88, label: '88' }
tree.insertAfter(nodeData, tree.getNode(11))
expect(tree.data[0].children.length).toEqual(2)
expect(tree.data[0].children[1]).toEqual(nodeData)
expect(tree.getNode(88).data).toEqual(nodeData)
})
test('set disabled checkbox', async () => {
const { wrapper } = getDisableTreeVm(`:props="defaultProps" show-checkbox node-key="id" default-expand-all`)
const nodeWrapper = wrapper.findAll('.el-tree-node__content')[2]
const checkboxWrapper = nodeWrapper.find('.el-checkbox input')
expect((checkboxWrapper.element as HTMLInputElement).disabled).toEqual(true)
})
test('check strictly', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" show-checkbox check-strictly default-expand-all`)
const treeWrapper = wrapper.findComponent(Tree)
const secondNodeContentWrapper = wrapper.findAll('.el-tree-node__content')[3]
const secondNodeCheckboxWrapper = secondNodeContentWrapper.find('.el-checkbox')
await secondNodeCheckboxWrapper.trigger('click')
expect(treeWrapper.vm.getCheckedNodes().length).toEqual(1)
expect(treeWrapper.vm.getCheckedNodes(true).length).toEqual(0)
const secondTreeNodeWrapper = treeWrapper.findAll('.el-tree-node')[3]
const secondNodefirstLeafCheckboxWrapper = secondTreeNodeWrapper.find('.el-tree-node__children .el-tree-node__content .el-checkbox')
await secondNodefirstLeafCheckboxWrapper.trigger('click')
expect(treeWrapper.vm.getCheckedNodes().length).toEqual(2)
})
test('render content', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" :render-content="renderContent"`, {
methods: {
renderContent(h, node) {
return h('div', { class: 'custom-content' }, [
h('button', { class: 'el-button' }, [node.node.label]),
])
},
},
})
const firstNodeWrapper = wrapper.find('.el-tree-node__content')
expect(firstNodeWrapper.find('.custom-content').exists()).toBe(true)
const buttonWrapper = firstNodeWrapper.find('.custom-content button')
expect(buttonWrapper.exists()).toBe(true)
expect(buttonWrapper.text()).toEqual('一级 1')
})
test('scoped slot', async () => {
const { wrapper } = getTreeVm('', {
template: `
<el-tree ref="tree" :data="data">
<template #default="scope">
<div class="custom-tree-template">
<span>{{ scope.node.label }}</span>
<button></button>
</div>
</template>
</el-tree>
`,
methods: {
renderContent(h, node) {
return h('div', { class: 'custom-content' }, [
h('button', { class: 'el-button' }, [node.node.label]),
])
},
},
})
const firstNodeWrapper = wrapper.find('.custom-tree-template')
expect(firstNodeWrapper.exists()).toBe(true)
const spanWrapper = firstNodeWrapper.find('span')
const buttonWrapper = firstNodeWrapper.find('button')
expect(spanWrapper.exists()).toBe(true)
expect(spanWrapper.text()).toEqual('一级 1')
expect(buttonWrapper.exists()).toBe(true)
})
test('load node', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" lazy :load="loadNode" show-checkbox`, {
methods: {
loadNode(node, resolve) {
if (node.level === 0) {
return resolve([{ label: 'region1' }, { label: 'region2' }])
}
if (node.level > 4) return resolve([])
setTimeout(() => {
resolve([{
label: 'zone' + this.count++,
}, {
label: 'zone' + this.count++,
}])
}, 50)
},
},
})
let nodeWrappers = wrapper.findAll('.el-tree-node__content')
expect(nodeWrappers.length).toEqual(2)
await nodeWrappers[0].trigger('click')
await delay(100) // wait load finish
nodeWrappers = wrapper.findAll('.el-tree-node__content')
expect(nodeWrappers.length).toEqual(4)
})
test('lazy defaultChecked', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" node-key="id" lazy :load="loadNode" show-checkbox`, {
methods: {
loadNode(node, resolve) {
if (node.level === 0) {
return resolve([{ label: 'region1', id: this.count++ }, { label: 'region2', id: this.count++ }])
}
if (node.level > 4) return resolve([])
setTimeout(() => {
resolve([{
label: 'zone' + this.count,
id: this.count++,
}, {
label: 'zone' + this.count,
id: this.count++,
}])
}, 50)
},
},
})
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
const firstNodeWrapper = treeWrapper.find('.el-tree-node__content')
expect(firstNodeWrapper.find('.is-indeterminate').exists()).toEqual(false)
tree.store.setCheckedKeys([3])
await firstNodeWrapper.find('.el-tree-node__expand-icon').trigger('click')
await delay(100)
expect(firstNodeWrapper.find('.is-indeterminate').exists()).toEqual(true)
const childWrapper = treeWrapper.findAll('.el-tree-node__content')[1]
expect(childWrapper.find('input').element.checked).toEqual(true)
})
test('lazy expandOnChecked', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" node-key="id" lazy :load="loadNode" show-checkbox check-descendants`, {
methods: {
loadNode(node, resolve) {
if (node.level === 0) {
return resolve([{ label: 'region1', id: this.count++ }, { label: 'region2', id: this.count++ }])
}
if (node.level > 2) return resolve([])
setTimeout(() => {
resolve([{
label: 'zone' + this.count,
id: this.count++,
}, {
label: 'zone' + this.count,
id: this.count++,
}])
}, 10)
},
},
})
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
tree.store.setCheckedKeys([1])
await delay(400)
expect(tree.getCheckedKeys().length).toEqual(7)
})
test('lazy without expandOnChecked', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" node-key="id" lazy :load="loadNode" show-checkbox`, {
methods: {
loadNode(node, resolve) {
if (node.level === 0) {
return resolve([{ label: 'region1', id: this.count++ }, { label: 'region2', id: this.count++ }])
}
if (node.level > 4) return resolve([])
setTimeout(() => {
resolve([{
label: 'zone' + this.count,
id: this.count++,
}, {
label: 'zone' + this.count,
id: this.count++,
}])
}, 50)
},
},
})
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
tree.store.setCheckedKeys([1])
await delay(300)
const nodeWrappers = treeWrapper.findAll('.el-tree-node__content')
expect(nodeWrappers[0].find('input').element.checked).toEqual(true)
expect(nodeWrappers.length).toEqual(2)
})
test('accordion', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" accordion`)
const firstNodeContentWrapper = wrapper.find('.el-tree-node__content')
const secondNodeContentWrapper = wrapper.find('.el-tree-node:nth-child(2) .el-tree-node__content')
await firstNodeContentWrapper.trigger('click')
await delay()
expect(wrapper.find('.el-tree-node').classes('is-expanded')).toBe(true)
await secondNodeContentWrapper.trigger('click')
await delay()
expect(wrapper.find('.el-tree-node').classes('is-expanded')).toBe(false)
})
test('handleNodeOpen & handleNodeClose', async () => {
const { wrapper, vm } = getTreeVm(`:props="defaultProps" lazy :load="loadNode" @node-expand="handleNodeOpen" @node-collapse="handleNodeClose"`, {
methods: {
loadNode(node, resolve) {
if (node.level === 0) {
return resolve([{ label: 'region1' }, { label: 'region2' }])
}
if (node.level > 4) return resolve([])
setTimeout(() => {
resolve([{
label: 'zone' + this.count++,
}, {
label: 'zone' + this.count++,
}])
}, 50)
},
handleNodeOpen(data) {
this.currentNode = data
this.nodeExpended = true
},
handleNodeClose(data) {
this.currentNode = data
this.nodeExpended = false
},
},
})
const firstNodeContentWrapper = wrapper.find('.el-tree-node__content')
const firstNodeWrapper = wrapper.find('.el-tree-node')
expect(firstNodeWrapper.find('.el-tree-node__children').exists()).toBe(false)
await firstNodeContentWrapper.trigger('click')
await delay(100)
expect(vm.nodeExpended).toEqual(true)
expect(vm.currentNode.label).toEqual('region1')
await firstNodeContentWrapper.trigger('click')
await delay(100)
expect(vm.nodeExpended).toEqual(false)
expect(vm.currentNode.label).toEqual('region1')
})
test('updateKeyChildren', async () => {
const { wrapper } = getTreeVm(`:props="defaultProps" show-checkbox node-key="id" default-expand-all`)
const treeWrapper = wrapper.findComponent(Tree)
const tree = treeWrapper.vm
tree.updateKeyChildren(1, [{
id: 111,
label: '三级 1-1',
}])
await nextTick()
const nodeContentWrapper = wrapper.findAll('.el-tree-node__content')[1]
const nodeLabelWrapper = nodeContentWrapper.find('.el-tree-node__label')
expect(tree.store.nodesMap['11']).toEqual(undefined)
expect(tree.store.nodesMap['1'].childNodes[0].data.id).toEqual(111)
expect(nodeLabelWrapper.text()).toEqual('三级 1-1')
})
test('update multi tree data', async () => {
const { wrapper, vm } = getTreeVm(``, {
template: `
<div>
<el-tree ref="tree1" :data="data" node-key="id" :props="defaultProps"></el-tree>
<el-tree ref="tree2" :data="data" node-key="id" :props="defaultProps"></el-tree>
</div>
`,
})
const nodeData = { label: '新增 1', id: 4, children: [] }
vm.data.push(nodeData)
vm.data = [...vm.data]
await nextTick()
const treeWrappers: any = wrapper.findAllComponents(Tree)
expect(treeWrappers[0].vm.getNode(4).data).toEqual(nodeData)
expect(treeWrappers[1].vm.getNode(4).data).toEqual(nodeData)
})
})

View File

@ -0,0 +1,94 @@
<template>
<div class="block">
<el-tree
ref="tree"
:data="data"
:props="defaultProps"
show-checkbox
node-key="id"
default-expand-all
/>
<button @click="test">test</button>
</div>
</template>
<script lang='ts'>
import { defineComponent } from 'vue'
export default defineComponent({
data() {
return {
count: 1,
defaultExpandedKeys: [1, 3],
data: [{
id: 1,
label: '一级 1',
children: [{
id: 11,
label: '二级 1-1',
children: [{
id: 111,
label: '三级 1-1',
}],
}],
}, {
id: 2,
label: '一级 2',
children: [{
id: 21,
label: '二级 2-1',
}, {
id: 22,
label: '二级 2-2',
}],
}, {
id: 3,
label: '一级 3',
children: [{
id: 31,
label: '二级 3-1',
}, {
id: 32,
label: '二级 3-2',
}],
}],
defaultProps: {
children: 'children',
label: 'label',
},
}
},
methods: {
loadNode(node, resolve) {
if (node.level === 0) {
return resolve([{ label: 'region1', id: this.count++ }, { label: 'region2', id: this.count++ }])
}
if (node.level > 2) return resolve([])
setTimeout(() => {
resolve([{
label: 'zone' + this.count,
id: this.count++,
}, {
label: 'zone' + this.count,
id: this.count++,
}])
}, 50)
},
handleCheck(...a) {
console.log(...a)
},
test() {
console.log(111)
// this.$refs.tree.updateKeyChildren(1, [{
// id: 111,
// label: ' 1-1'
// }]);
},
handleNodeClick(data) {
console.log(data)
},
},
})
</script>

View File

@ -0,0 +1,104 @@
<template>
<el-tree
:data="data"
node-key="id"
default-expand-all
draggable
:allow-drop="allowDrop"
:allow-drag="allowDrag"
@node-drag-start="handleDragStart"
@node-drag-enter="handleDragEnter"
@node-drag-leave="handleDragLeave"
@node-drag-over="handleDragOver"
@node-drag-end="handleDragEnd"
@node-drop="handleDrop"
/>
</template>
<script>
export default {
data() {
return {
data: [{
id: 1,
label: '一级 1',
children: [{
id: 4,
label: '二级 1-1',
children: [{
id: 9,
label: '三级 1-1-1',
}, {
id: 10,
label: '三级 1-1-2',
}],
}],
}, {
id: 2,
label: '一级 2',
children: [{
id: 5,
label: '二级 2-1',
}, {
id: 6,
label: '二级 2-2',
}],
}, {
id: 3,
label: '一级 3',
children: [{
id: 7,
label: '二级 3-1',
}, {
id: 8,
label: '二级 3-2',
children: [{
id: 11,
label: '三级 3-2-1',
}, {
id: 12,
label: '三级 3-2-2',
}, {
id: 13,
label: '三级 3-2-3',
}],
}],
}],
defaultProps: {
children: 'children',
label: 'label',
},
}
},
methods: {
handleDragStart(node) {
console.log('drag start', node)
},
handleDragEnter(draggingNode, dropNode) {
console.log('tree drag enter: ', dropNode.label)
},
handleDragLeave(draggingNode, dropNode) {
console.log('tree drag leave: ', dropNode.label)
},
handleDragOver(draggingNode, dropNode) {
console.log('tree drag over: ', dropNode.label)
},
handleDragEnd(draggingNode, dropNode, dropType) {
console.log('tree drag end: ', dropNode && dropNode.label, dropType)
},
handleDrop(draggingNode, dropNode, dropType) {
console.log('tree drop: ', dropNode.label, dropType)
},
allowDrop(draggingNode, dropNode, type) {
if (dropNode.data.label === '二级 3-1') {
return type !== 'inner'
} else {
return true
}
},
allowDrag(draggingNode) {
return draggingNode.data.label.indexOf('三级 3-2-2') === -1
},
},
}
</script>

View File

@ -0,0 +1,60 @@
<template>
<el-tree
:props="props"
:load="loadNode"
lazy
show-checkbox
@check-change="handleCheckChange"
/>
</template>
<script>
export default {
data() {
return {
props: {
label: 'name',
children: 'zones',
},
count: 1,
}
},
methods: {
handleCheckChange(data, checked, indeterminate) {
console.log(data, checked, indeterminate)
},
handleNodeClick(data) {
console.log(data)
},
loadNode(node, resolve) {
if (node.level === 0) {
return resolve([{ name: 'region1' }, { name: 'region2' }])
}
if (node.level > 3) return resolve([])
var hasChild
if (node.data.name === 'region1') {
hasChild = true
} else if (node.data.name === 'region2') {
hasChild = false
} else {
hasChild = Math.random() > 0.5
}
setTimeout(() => {
var data
if (hasChild) {
data = [{
name: 'zone' + this.count++,
}, {
name: 'zone' + this.count++,
}]
} else {
data = []
}
resolve(data)
}, 500)
},
},
}
</script>

View File

@ -0,0 +1,40 @@
<template>
<el-tree
:props="props"
:load="loadNode"
lazy
show-checkbox
/>
</template>
<script>
export default {
data() {
return {
props: {
label: 'name',
children: 'zones',
isLeaf: 'leaf',
},
}
},
methods: {
loadNode(node, resolve) {
if (node.level === 0) {
return resolve([{ name: 'region' }])
}
if (node.level > 1) return resolve([])
setTimeout(() => {
const data = [{
name: 'leaf',
leaf: true,
}, {
name: 'zone',
}]
resolve(data)
}, 500)
},
},
}
</script>

View File

@ -0,0 +1,59 @@
<template>
<el-tree
:data="data"
show-checkbox
node-key="id"
:default-expanded-keys="[2, 3]"
:default-checked-keys="[5]"
:props="defaultProps"
check-on-click-node
highlight-current
/>
</template>
<script>
export default {
data() {
return {
data: [{
id: 1,
label: '一级 1',
children: [{
id: 4,
label: '二级 1-1',
children: [{
id: 9,
label: '三级 1-1-1',
}, {
id: 10,
label: '三级 1-1-2',
}],
}],
}, {
id: 2,
label: '一级 2',
children: [{
id: 5,
label: '二级 2-1',
}, {
id: 6,
label: '二级 2-2',
}],
}, {
id: 3,
label: '一级 3',
children: [{
id: 7,
label: '二级 3-1',
}, {
id: 8,
label: '二级 3-2',
}],
}],
defaultProps: {
children: 'children',
label: 'label',
},
}
},
}
</script>

View File

@ -0,0 +1,49 @@
<template>
<el-tree
:data="data"
show-checkbox
node-key="id"
:default-expanded-keys="[2, 3]"
:default-checked-keys="[5]"
/>
</template>
<script>
export default {
data() {
return {
data: [{
id: 1,
label: '一级 2',
children: [{
id: 3,
label: '二级 2-1',
children: [{
id: 4,
label: '三级 3-1-1',
}, {
id: 5,
label: '三级 3-1-2',
disabled: true,
}],
}, {
id: 2,
label: '二级 2-2',
disabled: true,
children: [{
id: 6,
label: '三级 3-2-1',
}, {
id: 7,
label: '三级 3-2-2',
disabled: true,
}],
}],
}],
defaultProps: {
children: 'children',
label: 'label',
},
}
},
}
</script>

View File

@ -0,0 +1,92 @@
<template>
<el-tree
ref="tree"
:data="data"
show-checkbox
default-expand-all
node-key="id"
highlight-current
:props="defaultProps"
check-on-click-node
/>
<div class="buttons">
<el-button @click="getCheckedNodes">通过 node 获取</el-button>
<el-button @click="getCheckedKeys">通过 key 获取</el-button>
<el-button @click="setCheckedNodes">通过 node 设置</el-button>
<el-button @click="setCheckedKeys">通过 key 设置</el-button>
<el-button @click="resetChecked">清空</el-button>
</div>
</template>
<script>
export default {
data() {
return {
data: [{
id: 1,
label: '一级 1',
children: [{
id: 4,
label: '二级 1-1',
children: [{
id: 9,
label: '三级 1-1-1',
}, {
id: 10,
label: '三级 1-1-2',
}],
}],
}, {
id: 2,
label: '一级 2',
children: [{
id: 5,
label: '二级 2-1',
}, {
id: 6,
label: '二级 2-2',
}],
}, {
id: 3,
label: '一级 3',
children: [{
id: 7,
label: '二级 3-1',
}, {
id: 8,
label: '二级 3-2',
}],
}],
defaultProps: {
children: 'children',
label: 'label',
},
}
},
methods: {
getCheckedNodes() {
console.log(this.$refs.tree.getCheckedNodes())
},
getCheckedKeys() {
console.log(this.$refs.tree.getCheckedKeys())
},
setCheckedNodes() {
this.$refs.tree.setCheckedNodes([{
id: 5,
label: '二级 2-1',
}, {
id: 9,
label: '三级 1-1-1',
}])
},
setCheckedKeys() {
this.$refs.tree.setCheckedKeys([3])
},
resetChecked() {
this.$refs.tree.setCheckedKeys([])
},
},
}
</script>

View File

@ -0,0 +1,127 @@
<template>
<div class="custom-tree-container">
<div class="block">
<p>使用 render-content</p>
<el-tree
:data="data"
show-checkbox
node-key="id"
default-expand-all
:expand-on-click-node="false"
:render-content="renderContent"
/>
</div>
<div class="block">
<p>使用 scoped slot</p>
<el-tree
:data="data"
show-checkbox
node-key="id"
default-expand-all
:expand-on-click-node="false"
>
<template #default="{ node, data: innerData }">
<span class="custom-tree-node">
<span>{{ node.label }}</span>
<span>
<el-button
type="text"
size="mini"
@click="append(innerData)"
>
Append
</el-button>
<el-button
type="text"
size="mini"
@click="remove(node, innerData)"
>
Delete
</el-button>
</span>
</span>
</template>
</el-tree>
</div>
</div>
</template>
<script>
let id = 1000
export default {
data() {
const data = [{
id: 1,
label: '一级 1',
children: [{
id: 4,
label: '二级 1-1',
children: [{
id: 9,
label: '三级 1-1-1',
}, {
id: 10,
label: '三级 1-1-2',
}],
}],
}, {
id: 2,
label: '一级 2',
children: [{
id: 5,
label: '二级 2-1',
}, {
id: 6,
label: '二级 2-2',
}],
}, {
id: 3,
label: '一级 3',
children: [{
id: 7,
label: '二级 3-1',
}, {
id: 8,
label: '二级 3-2',
}],
}]
return {
data: JSON.parse(JSON.stringify(data)),
}
},
methods: {
append(data) {
const newChild = { id: id++, label: 'testtest', children: [] }
if (!data.children) {
data.children = []
}
data.children.push(newChild)
this.data = [...this.data]
},
remove(node, data) {
const parent = node.parent
const children = parent.data.children || parent.data
const index = children.findIndex(d => d.id === data.id)
children.splice(index, 1)
this.data = [...this.data]
},
renderContent(h) {
return h('div', { class: 'custom-tree-node' }, ['haha'])
},
},
}
</script>
<style>
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
</style>

View File

@ -0,0 +1,78 @@
<template>
<input
v-model="filterText"
placeholder="输入关键字进行过滤"
>
<el-tree
ref="tree"
class="filter-tree"
:data="data"
:props="defaultProps"
default-expand-all
:filter-node-method="filterNode"
/>
</template>
<script>
export default {
data() {
return {
filterText: '',
data: [{
id: 1,
label: '一级 1',
children: [{
id: 4,
label: '二级 1-1',
children: [{
id: 9,
label: '三级 1-1-1',
}, {
id: 10,
label: '三级 1-1-2',
}],
}],
}, {
id: 2,
label: '一级 2',
children: [{
id: 5,
label: '二级 2-1',
}, {
id: 6,
label: '二级 2-2',
}],
}, {
id: 3,
label: '一级 3',
children: [{
id: 7,
label: '二级 3-1',
}, {
id: 8,
label: '二级 3-2',
}],
}],
defaultProps: {
children: 'children',
label: 'label',
},
}
},
watch: {
filterText(val) {
this.$refs.tree.filter(val)
},
},
methods: {
filterNode(value, data) {
if (!value) return true
return data.label.indexOf(value) !== -1
},
},
}
</script>

View File

@ -0,0 +1,62 @@
<template>
<el-tree
:data="data"
:props="defaultProps"
accordion
@node-click="handleNodeClick"
/>
</template>
<script>
export default {
data() {
return {
data: [{
label: '一级 1',
children: [{
label: '二级 1-1',
children: [{
label: '三级 1-1-1',
}],
}],
}, {
label: '一级 2',
children: [{
label: '二级 2-1',
children: [{
label: '三级 2-1-1',
}],
}, {
label: '二级 2-2',
children: [{
label: '三级 2-2-1',
}],
}],
}, {
label: '一级 3',
children: [{
label: '二级 3-1',
children: [{
label: '三级 3-1-1',
}],
}, {
label: '二级 3-2',
children: [{
label: '三级 3-2-1',
}],
}],
}],
defaultProps: {
children: 'children',
label: 'label',
},
}
},
methods: {
handleNodeClick(data) {
console.log(data)
},
},
}
</script>

View File

@ -0,0 +1,6 @@
export { default as BasicUsage } from './index.vue'
export default {
title: 'Tree',
}

View File

@ -0,0 +1,77 @@
<template>
<div class="item">
<div class="title">1</div>
<Basic />
</div>
<div class="item">
<div class="title">2</div>
<Basic2 />
</div>
<div class="item">
<div class="title">3</div>
<Basic3 />
</div>
<div class="item">
<div class="title">4</div>
<Basic4 />
</div>
<div class="item">
<div class="title">5</div>
<Basic5 />
</div>
<div class="item">
<div class="title">6</div>
<Basic6 />
</div>
<div class="item">
<div class="title">7</div>
<Basic7 />
</div>
<div class="item">
<div class="title">8</div>
<Basic8 />
</div>
<div class="item">
<div class="title">9</div>
<Basic9 />
</div>
<div class="item">
<div class="title">10</div>
<Basic10 />
</div>
</template>
<script>
import Basic from './basic.vue'
import Basic2 from './basic2.vue'
import Basic3 from './basic3.vue'
import Basic4 from './basic4.vue'
import Basic5 from './basic5.vue'
import Basic6 from './basic6.vue'
import Basic7 from './basic7.vue'
import Basic8 from './basic8.vue'
import Basic9 from './basic9.vue'
import Basic10 from './basic10.vue'
export default {
components: {
Basic,
Basic2,
Basic3,
Basic4,
Basic5,
Basic6,
Basic7,
Basic8,
Basic9,
Basic10,
},
}
</script>
<style>
.item {
border: 2px solid #eee;
padding: 10px;
margin-bottom: 15px;
}
</style>

5
packages/tree/index.ts Normal file
View File

@ -0,0 +1,5 @@
import { App } from 'vue'
import Tree from './src/tree.vue'
export default (app: App): void => {
app.component(Tree.name, Tree)
}

View File

@ -0,0 +1,12 @@
{
"name": "@element-plus/tree",
"version": "0.0.0",
"main": "dist/index.js",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.0-rc.6"
},
"devDependencies": {
"@vue/test-utils": "^2.0.0-beta.0"
}
}

View File

@ -0,0 +1,514 @@
import { markNodeData, NODE_KEY } from './util'
import TreeStore from './tree-store'
import objectAssign from '@element-plus/utils/merge'
import {
TreeNodeOptions,
TreeNodeData,
TreeKey,
FakeNode,
TreeNodeLoadedDefaultProps,
TreeNodeChildState,
} from '../tree.d'
export const getChildState = (node: Node[]): TreeNodeChildState => {
let all = true
let none = true
let allWithoutDisable = true
for (let i = 0, j = node.length; i < j; i++) {
const n = node[i]
if (n.checked !== true || n.indeterminate) {
all = false
if (!n.disabled) {
allWithoutDisable = false
}
}
if (n.checked !== false || n.indeterminate) {
none = false
}
}
return { all, none, allWithoutDisable, half: !all && !none }
}
const reInitChecked = function(node: Node): void {
if (node.childNodes.length === 0) return
const { all, none, half } = getChildState(node.childNodes)
if (all) {
node.checked = true
node.indeterminate = false
} else if (half) {
node.checked = false
node.indeterminate = true
} else if (none) {
node.checked = false
node.indeterminate = false
}
const parent = node.parent
if (!parent || parent.level === 0) return
if (!node.store.checkStrictly) {
reInitChecked(parent)
}
}
const getPropertyFromData = function(node: Node, prop: string): any {
const props = node.store.props
const data = node.data || {}
const config = props[prop]
if (typeof config === 'function') {
return config(data, node)
} else if (typeof config === 'string') {
return data[config]
} else if (typeof config === 'undefined') {
const dataProp = data[prop]
return dataProp === undefined ? '' : dataProp
}
}
let nodeIdSeed = 0
export default class Node {
id: number;
text: string;
checked: boolean;
indeterminate: boolean;
data: TreeNodeData;
expanded: boolean;
parent: Node;
visible: boolean;
isCurrent: boolean;
store: TreeStore;
isLeafByUser: boolean;
isLeaf: boolean;
level: number;
loaded: boolean;
childNodes: Node[];
loading: boolean;
constructor(options: TreeNodeOptions) {
this.id = nodeIdSeed++
this.text = null
this.checked = false
this.indeterminate = false
this.data = null
this.expanded = false
this.parent = null
this.visible = true
this.isCurrent = false
for (const name in options) {
if (options.hasOwnProperty(name)) {
this[name] = options[name]
}
}
// internal
this.level = 0
this.loaded = false
this.childNodes = []
this.loading = false
if (this.parent) {
this.level = this.parent.level + 1
}
const store = this.store
if (!store) {
throw new Error('[Node]store is required!')
}
store.registerNode(this)
const props = store.props
if (props && typeof props.isLeaf !== 'undefined') {
const isLeaf = getPropertyFromData(this, 'isLeaf')
if (typeof isLeaf === 'boolean') {
this.isLeafByUser = isLeaf
}
}
if (store.lazy !== true && this.data) {
this.setData(this.data)
if (store.defaultExpandAll) {
this.expanded = true
}
} else if (this.level > 0 && store.lazy && store.defaultExpandAll) {
this.expand()
}
if (!Array.isArray(this.data)) {
markNodeData(this, this.data)
}
if (!this.data) return
const defaultExpandedKeys = store.defaultExpandedKeys
const key = store.key
if (key && defaultExpandedKeys && defaultExpandedKeys.indexOf(this.key) !== -1) {
this.expand(null, store.autoExpandParent)
}
if (key && store.currentNodeKey !== undefined && this.key === store.currentNodeKey) {
store.currentNode = this
store.currentNode.isCurrent = true
}
if (store.lazy) {
store._initDefaultCheckedNode(this)
}
this.updateLeafState()
}
setData(data: TreeNodeData): void {
if (!Array.isArray(data)) {
markNodeData(this, data)
}
this.data = data
this.childNodes = []
let children
if (this.level === 0 && this.data instanceof Array) {
children = this.data
} else {
children = getPropertyFromData(this, 'children') || []
}
for (let i = 0, j = children.length; i < j; i++) {
this.insertChild({ data: children[i] })
}
}
get label(): string {
return getPropertyFromData(this, 'label')
}
get key(): TreeKey {
const nodeKey = this.store.key
if (this.data) return this.data[nodeKey]
return null
}
get disabled(): boolean {
return getPropertyFromData(this, 'disabled')
}
get nextSibling(): Nullable<Node> {
const parent = this.parent
if (parent) {
const index = parent.childNodes.indexOf(this)
if (index > -1) {
return parent.childNodes[index + 1]
}
}
return null
}
get previousSibling(): Nullable<Node> {
const parent = this.parent
if (parent) {
const index = parent.childNodes.indexOf(this)
if (index > -1) {
return index > 0 ? parent.childNodes[index - 1] : null
}
}
return null
}
contains(target: Node, deep = true): boolean {
const walk = function(parent: Node): boolean {
const children = parent.childNodes || []
let result = false
for (let i = 0, j = children.length; i < j; i++) {
const child = children[i]
if (child === target || (deep && walk(child))) {
result = true
break
}
}
return result
}
return walk(this)
}
remove(): void {
const parent = this.parent
if (parent) {
parent.removeChild(this)
}
}
insertChild(child?: FakeNode | Node, index?: number, batch?: boolean): void {
if (!child) throw new Error('insertChild error: child is required.')
if (!(child instanceof Node)) {
if (!batch) {
const children = this.getChildren(true)
if (children.indexOf(child.data) === -1) {
if (typeof index === 'undefined' || index < 0) {
children.push(child.data)
} else {
children.splice(index, 0, child.data)
}
}
}
objectAssign(child, {
parent: this,
store: this.store,
})
child = new Node(child as TreeNodeOptions)
}
(child as Node).level = this.level + 1
if (typeof index === 'undefined' || index < 0) {
this.childNodes.push(child as Node)
} else {
this.childNodes.splice(index, 0, child as Node)
}
this.updateLeafState()
}
insertBefore(child: FakeNode | Node, ref: Node): void {
let index
if (ref) {
index = this.childNodes.indexOf(ref)
}
this.insertChild(child, index)
}
insertAfter(child: FakeNode | Node, ref: Node): void {
let index
if (ref) {
index = this.childNodes.indexOf(ref)
if (index !== -1) index += 1
}
this.insertChild(child, index)
}
removeChild(child: Node): void {
const children = this.getChildren() || []
const dataIndex = children.indexOf(child.data)
if (dataIndex > -1) {
children.splice(dataIndex, 1)
}
const index = this.childNodes.indexOf(child)
if (index > -1) {
this.store && this.store.deregisterNode(child)
child.parent = null
this.childNodes.splice(index, 1)
}
this.updateLeafState()
}
removeChildByData(data: TreeNodeData): void {
let targetNode: Node = null
for (let i = 0; i < this.childNodes.length; i++) {
if (this.childNodes[i].data === data) {
targetNode = this.childNodes[i]
break
}
}
if (targetNode) {
this.removeChild(targetNode)
}
}
expand(callback?: () => void, expandParent?: boolean): void {
const done = (): void => {
if (expandParent) {
let parent = this.parent
while (parent.level > 0) {
parent.expanded = true
parent = parent.parent
}
}
this.expanded = true
if (callback) callback()
}
if (this.shouldLoadData()) {
this.loadData(data => {
if (data instanceof Array) {
if (this.checked) {
this.setChecked(true, true)
} else if (!this.store.checkStrictly) {
reInitChecked(this)
}
done()
}
})
} else {
done()
}
}
doCreateChildren(array: TreeNodeData[], defaultProps: TreeNodeLoadedDefaultProps = {}): void {
array.forEach(item => {
this.insertChild(objectAssign({ data: item }, defaultProps), undefined, true)
})
}
collapse(): void {
this.expanded = false
}
shouldLoadData(): boolean {
return this.store.lazy === true && this.store.load && !this.loaded
}
updateLeafState(): void {
if (this.store.lazy === true && this.loaded !== true && typeof this.isLeafByUser !== 'undefined') {
this.isLeaf = this.isLeafByUser
return
}
const childNodes = this.childNodes
if (!this.store.lazy || (this.store.lazy === true && this.loaded === true)) {
this.isLeaf = !childNodes || childNodes.length === 0
return
}
this.isLeaf = false
}
setChecked(value?: boolean | string, deep?: boolean, recursion?: boolean, passValue?: boolean) {
this.indeterminate = value === 'half'
this.checked = value === true
if (this.store.checkStrictly) return
if (!(this.shouldLoadData() && !this.store.checkDescendants)) {
const { all, allWithoutDisable } = getChildState(this.childNodes)
if (!this.isLeaf && (!all && allWithoutDisable)) {
this.checked = false
value = false
}
const handleDescendants = (): void => {
if (deep) {
const childNodes = this.childNodes
for (let i = 0, j = childNodes.length; i < j; i++) {
const child = childNodes[i]
passValue = passValue || value !== false
const isCheck = child.disabled ? child.checked : passValue
child.setChecked(isCheck, deep, true, passValue)
}
const { half, all } = getChildState(childNodes)
if (!all) {
this.checked = all
this.indeterminate = half
}
}
}
if (this.shouldLoadData()) {
// Only work on lazy load data.
this.loadData(() => {
handleDescendants()
reInitChecked(this)
}, {
checked: value !== false,
})
return
} else {
handleDescendants()
}
}
const parent = this.parent
if (!parent || parent.level === 0) return
if (!recursion) {
reInitChecked(parent)
}
}
getChildren(forceInit = false): TreeNodeData | TreeNodeData[] { // this is data
if (this.level === 0) return this.data
const data = this.data
if (!data) return null
const props = this.store.props
let children = 'children'
if (props) {
children = props.children || 'children'
}
if (data[children] === undefined) {
data[children] = null
}
if (forceInit && !data[children]) {
data[children] = []
}
return data[children]
}
updateChildren(): void {
const newData = (this.getChildren() || []) as TreeNodeData[]
const oldData = this.childNodes.map(node => node.data)
const newDataMap = {}
const newNodes = []
newData.forEach((item, index) => {
const key = item[NODE_KEY]
const isNodeExists = !!key && oldData.findIndex(data => data[NODE_KEY] === key) >= 0
if (isNodeExists) {
newDataMap[key] = { index, data: item }
} else {
newNodes.push({ index, data: item })
}
})
if (!this.store.lazy) {
oldData.forEach(item => {
if (!newDataMap[item[NODE_KEY]]) this.removeChildByData(item)
})
}
newNodes.forEach(({ index, data }) => {
this.insertChild({ data }, index)
})
this.updateLeafState()
}
loadData(callback: (node: Node) => void, defaultProps: TreeNodeLoadedDefaultProps = {}) {
if (this.store.lazy === true && this.store.load && !this.loaded && (!this.loading || Object.keys(defaultProps).length)) {
this.loading = true
const resolve = children => {
this.loaded = true
this.loading = false
this.childNodes = []
this.doCreateChildren(children, defaultProps)
this.updateLeafState()
if (callback) {
callback.call(this, children)
}
}
this.store.load(this, resolve)
} else {
if (callback) {
callback.call(this)
}
}
}
}

View File

@ -0,0 +1,367 @@
import Node from './node'
import { getNodeKey } from './util'
import {
TreeKey,
TreeData,
TreeStoreNodesMap,
LoadFunction,
FilterNodeMethodFunction,
TreeOptionProps,
TreeStoreOptions,
FilterValue,
TreeNodeData,
} from '../tree.d'
export default class TreeStore {
currentNode: Node
currentNodeKey: TreeKey
nodesMap: TreeStoreNodesMap
root: Node
data: TreeData
lazy: boolean
load: LoadFunction
filterNodeMethod: FilterNodeMethodFunction
key: TreeKey
defaultCheckedKeys: TreeKey[];
checkStrictly: boolean;
defaultExpandedKeys: TreeKey[];
autoExpandParent: boolean;
defaultExpandAll: boolean;
checkDescendants: boolean;
props: TreeOptionProps;
constructor(options: TreeStoreOptions) {
this.currentNode = null
this.currentNodeKey = null
for (const option in options) {
if (options.hasOwnProperty(option)) {
this[option] = options[option]
}
}
this.nodesMap = {}
this.root = new Node({
data: this.data,
store: this,
})
if (this.lazy && this.load) {
const loadFn = this.load
loadFn(this.root, data => {
this.root.doCreateChildren(data)
this._initDefaultCheckedNodes()
})
} else {
this._initDefaultCheckedNodes()
}
}
filter(value: FilterValue): void {
const filterNodeMethod = this.filterNodeMethod
const lazy = this.lazy
const traverse = function(node: TreeStore | Node) {
const childNodes = (node as TreeStore).root ? (node as TreeStore).root.childNodes : (node as Node).childNodes
childNodes.forEach(child => {
child.visible = filterNodeMethod.call(child, value, child.data, child)
traverse(child)
})
if (!(node as Node).visible && childNodes.length) {
let allHidden = true
allHidden = !childNodes.some(child => child.visible)
if ((node as TreeStore).root) {
(node as TreeStore).root.visible = allHidden === false
} else {
(node as Node).visible = allHidden === false
}
}
if (!value) return
if ((node as Node).visible && !(node as Node).isLeaf && !lazy) (node as Node).expand()
}
traverse(this)
}
setData(newVal: TreeData): void {
const instanceChanged = newVal !== this.root.data
if (instanceChanged) {
this.root.setData(newVal)
this._initDefaultCheckedNodes()
} else {
this.root.updateChildren()
}
}
getNode(data: TreeKey | TreeNodeData ): Node {
if (data instanceof Node) return data
const key = typeof data !== 'object' ? data : getNodeKey(this.key, data)
return this.nodesMap[key] || null
}
insertBefore(data: TreeNodeData, refData: TreeKey | TreeNodeData): void {
const refNode = this.getNode(refData)
refNode.parent.insertBefore({ data }, refNode)
}
insertAfter(data: TreeNodeData, refData: TreeKey | TreeNodeData): void {
const refNode = this.getNode(refData)
refNode.parent.insertAfter({ data }, refNode)
}
remove(data: TreeNodeData | Node): void {
const node = this.getNode(data)
if (node && node.parent) {
if (node === this.currentNode) {
this.currentNode = null
}
node.parent.removeChild(node)
}
}
append(data: TreeNodeData, parentData: TreeNodeData| TreeKey | Node ): void {
const parentNode = parentData ? this.getNode(parentData) : this.root
if (parentNode) {
parentNode.insertChild({ data })
}
}
_initDefaultCheckedNodes(): void {
const defaultCheckedKeys = this.defaultCheckedKeys || []
const nodesMap = this.nodesMap
defaultCheckedKeys.forEach(checkedKey => {
const node = nodesMap[checkedKey]
if (node) {
node.setChecked(true, !this.checkStrictly)
}
})
}
_initDefaultCheckedNode(node: Node): void {
const defaultCheckedKeys = this.defaultCheckedKeys || []
if (defaultCheckedKeys.indexOf(node.key) !== -1) {
node.setChecked(true, !this.checkStrictly)
}
}
setDefaultCheckedKey(newVal: TreeKey[]): void {
if (newVal !== this.defaultCheckedKeys) {
this.defaultCheckedKeys = newVal
this._initDefaultCheckedNodes()
}
}
registerNode(node: Node): void {
const key = this.key
if (!key || !node || !node.data) return
const nodeKey = node.key
if (nodeKey !== undefined) this.nodesMap[node.key] = node
}
deregisterNode(node: Node): void {
const key = this.key
if (!key || !node || !node.data) return
node.childNodes.forEach(child => {
this.deregisterNode(child)
})
delete this.nodesMap[node.key]
}
getCheckedNodes(leafOnly = false, includeHalfChecked = false): TreeNodeData[] {
const checkedNodes: TreeNodeData[] = []
const traverse = function(node: TreeStore | Node) {
const childNodes = (node as TreeStore).root ? (node as TreeStore).root.childNodes : (node as Node).childNodes
childNodes.forEach(child => {
if ((child.checked || (includeHalfChecked && child.indeterminate)) && (!leafOnly || (leafOnly && child.isLeaf))) {
checkedNodes.push(child.data)
}
traverse(child)
})
}
traverse(this)
return checkedNodes
}
getCheckedKeys(leafOnly = false): TreeKey[] {
return this.getCheckedNodes(leafOnly).map(data => (data || {})[this.key])
}
getHalfCheckedNodes(): TreeNodeData[] {
const nodes: TreeNodeData[] = []
const traverse = function(node: TreeStore | Node) {
const childNodes = (node as TreeStore).root ? (node as TreeStore).root.childNodes : (node as Node).childNodes
childNodes.forEach(child => {
if (child.indeterminate) {
nodes.push(child.data)
}
traverse(child)
})
}
traverse(this)
return nodes
}
getHalfCheckedKeys(): TreeKey[] {
return this.getHalfCheckedNodes().map(data => (data || {})[this.key])
}
_getAllNodes(): Node[] {
const allNodes: Node[] = []
const nodesMap = this.nodesMap
for (const nodeKey in nodesMap) {
if (nodesMap.hasOwnProperty(nodeKey)) {
allNodes.push(nodesMap[nodeKey])
}
}
return allNodes
}
updateChildren(key: TreeKey, data: TreeData): void {
const node = this.nodesMap[key]
if (!node) return
const childNodes = node.childNodes
for (let i = childNodes.length - 1; i >= 0; i--) {
const child = childNodes[i]
this.remove(child.data)
}
for (let i = 0, j = data.length; i < j; i++) {
const child = data[i]
this.append(child, node.data)
}
}
_setCheckedKeys(key: TreeKey, leafOnly = false, checkedKeys: { [key: string]: boolean; }): void {
const allNodes = this._getAllNodes().sort((a, b) => b.level - a.level)
const cache = Object.create(null)
const keys = Object.keys(checkedKeys)
allNodes.forEach(node => node.setChecked(false, false))
for (let i = 0, j = allNodes.length; i < j; i++) {
const node = allNodes[i]
const nodeKey = node.data[key].toString()
const checked = keys.indexOf(nodeKey) > -1
if (!checked) {
if (node.checked && !cache[nodeKey]) {
node.setChecked(false, false)
}
continue
}
let parent = node.parent
while (parent && parent.level > 0) {
cache[parent.data[key]] = true
parent = parent.parent
}
if (node.isLeaf || this.checkStrictly) {
node.setChecked(true, false)
continue
}
node.setChecked(true, true)
if (leafOnly) {
node.setChecked(false, false)
const traverse = function(node: Node): void {
const childNodes = node.childNodes
childNodes.forEach(child => {
if (!child.isLeaf) {
child.setChecked(false, false)
}
traverse(child)
})
}
traverse(node)
}
}
}
setCheckedNodes(array: Node[], leafOnly = false): void {
const key = this.key
const checkedKeys = {}
array.forEach(item => {
checkedKeys[(item || {})[key]] = true
})
this._setCheckedKeys(key, leafOnly, checkedKeys)
}
setCheckedKeys(keys: TreeKey[], leafOnly = false): void {
this.defaultCheckedKeys = keys
const key = this.key
const checkedKeys = {}
keys.forEach(key => {
checkedKeys[key] = true
})
this._setCheckedKeys(key, leafOnly, checkedKeys)
}
setDefaultExpandedKeys(keys: TreeKey[]) {
keys = keys || []
this.defaultExpandedKeys = keys
keys.forEach(key => {
const node = this.getNode(key)
if (node) node.expand(null, this.autoExpandParent)
})
}
setChecked(data: TreeKey | TreeNodeData, checked: boolean, deep: boolean): void {
const node = this.getNode(data)
if (node) {
node.setChecked(!!checked, deep)
}
}
getCurrentNode(): Node {
return this.currentNode
}
setCurrentNode(currentNode: Node): void {
const prevCurrentNode = this.currentNode
if (prevCurrentNode) {
prevCurrentNode.isCurrent = false
}
this.currentNode = currentNode
this.currentNode.isCurrent = true
}
setUserCurrentNode(node: Node): void {
const key = node[this.key]
const currNode = this.nodesMap[key]
this.setCurrentNode(currNode)
}
setCurrentNodeKey(key: TreeKey): void {
if (key === null || key === undefined) {
this.currentNode && (this.currentNode.isCurrent = false)
this.currentNode = null
return
}
const node = this.getNode(key)
if (node) {
this.setCurrentNode(node)
}
}
}

View File

@ -0,0 +1,183 @@
import mitt, { Emitter } from 'mitt'
import { inject, provide, ref } from 'vue'
import { addClass, removeClass } from '@element-plus/utils/dom'
import Node from './node'
interface TreeNode {
node: Node
$el?: HTMLElement
}
interface DragOptions {
event: DragEvent
treeNode: TreeNode
}
export function useDragNodeHandler({ props, ctx, el$, dropIndicator$, store }) {
const emitter = mitt()
provide('DragNodeEmitter', emitter)
const dragState = ref({
showDropIndicator: false,
draggingNode: null,
dropNode: null,
allowDrop: true,
dropType: null,
})
emitter.on('tree-node-drag-start', ({ event, treeNode }: DragOptions) => {
console.log(event, treeNode)
if (typeof props.allowDrag === 'function' && !props.allowDrag(treeNode.node)) {
event.preventDefault()
return false
}
event.dataTransfer.effectAllowed = 'move'
// wrap in try catch to address IE's error when first param is 'text/plain'
try {
// setData is required for draggable to work in FireFox
// the content has to be '' so dragging a node out of the tree won't open a new tab in FireFox
event.dataTransfer.setData('text/plain', '')
} catch (e) {}
dragState.value.draggingNode = treeNode
ctx.emit('node-drag-start', treeNode.node, event)
})
emitter.on('tree-node-drag-over', ({ event, treeNode }: DragOptions) => {
const dropNode = treeNode
const oldDropNode = dragState.value.dropNode
if (oldDropNode && oldDropNode !== dropNode) {
removeClass(oldDropNode.$el, 'is-drop-inner')
}
const draggingNode = dragState.value.draggingNode
if (!draggingNode || !dropNode) return
let dropPrev = true
let dropInner = true
let dropNext = true
let userAllowDropInner = true
if (typeof props.allowDrop === 'function') {
dropPrev = props.allowDrop(draggingNode.node, dropNode.node, 'prev')
userAllowDropInner = dropInner = props.allowDrop(draggingNode.node, dropNode.node, 'inner')
dropNext = props.allowDrop(draggingNode.node, dropNode.node, 'next')
}
event.dataTransfer.dropEffect = dropInner ? 'move' : 'none'
if ((dropPrev || dropInner || dropNext) && oldDropNode !== dropNode) {
if (oldDropNode) {
ctx.emit('node-drag-leave', draggingNode.node, oldDropNode.node, event)
}
ctx.emit('node-drag-enter', draggingNode.node, dropNode.node, event)
}
if (dropPrev || dropInner || dropNext) {
dragState.value.dropNode = dropNode
}
if (dropNode.node.nextSibling === draggingNode.node) {
dropNext = false
}
if (dropNode.node.previousSibling === draggingNode.node) {
dropPrev = false
}
if (dropNode.node.contains(draggingNode.node, false)) {
dropInner = false
}
if (draggingNode.node === dropNode.node || draggingNode.node.contains(dropNode.node)) {
dropPrev = false
dropInner = false
dropNext = false
}
const targetPosition = dropNode.$el.getBoundingClientRect()
const treePosition = el$.value.getBoundingClientRect()
let dropType
const prevPercent = dropPrev ? (dropInner ? 0.25 : (dropNext ? 0.45 : 1)) : -1
const nextPercent = dropNext ? (dropInner ? 0.75 : (dropPrev ? 0.55 : 0)) : 1
let indicatorTop = -9999
const distance = event.clientY - targetPosition.top
if (distance < targetPosition.height * prevPercent) {
dropType = 'before'
} else if (distance > targetPosition.height * nextPercent) {
dropType = 'after'
} else if (dropInner) {
dropType = 'inner'
} else {
dropType = 'none'
}
const iconPosition = dropNode.$el.querySelector('.el-tree-node__expand-icon').getBoundingClientRect()
const dropIndicator = dropIndicator$.value
if (dropType === 'before') {
indicatorTop = iconPosition.top - treePosition.top
} else if (dropType === 'after') {
indicatorTop = iconPosition.bottom - treePosition.top
}
dropIndicator.style.top = indicatorTop + 'px'
dropIndicator.style.left = (iconPosition.right - treePosition.left) + 'px'
if (dropType === 'inner') {
addClass(dropNode.$el, 'is-drop-inner')
} else {
removeClass(dropNode.$el, 'is-drop-inner')
}
dragState.value.showDropIndicator = dropType === 'before' || dropType === 'after'
dragState.value.allowDrop = dragState.value.showDropIndicator || userAllowDropInner
dragState.value.dropType = dropType
ctx.emit('node-drag-over', draggingNode.node, dropNode.node, event)
})
emitter.on('tree-node-drag-end', (event: DragEvent) => {
const { draggingNode, dropType, dropNode } = dragState.value
event.preventDefault()
event.dataTransfer.dropEffect = 'move'
if (draggingNode && dropNode) {
const draggingNodeCopy = { data: draggingNode.node.data }
if (dropType !== 'none') {
draggingNode.node.remove()
}
if (dropType === 'before') {
dropNode.node.parent.insertBefore(draggingNodeCopy, dropNode.node)
} else if (dropType === 'after') {
dropNode.node.parent.insertAfter(draggingNodeCopy, dropNode.node)
} else if (dropType === 'inner') {
dropNode.node.insertChild(draggingNodeCopy)
}
if (dropType !== 'none') {
store.value.registerNode(draggingNodeCopy)
}
removeClass(dropNode.$el, 'is-drop-inner')
ctx.emit('node-drag-end', draggingNode.node, dropNode.node, dropType, event)
if (dropType !== 'none') {
ctx.emit('node-drop', draggingNode.node, dropNode.node, dropType, event)
}
}
if (draggingNode && !dropNode) {
ctx.emit('node-drag-end', draggingNode.node, null, dropType, event)
}
dragState.value.showDropIndicator = false
dragState.value.draggingNode = null
dragState.value.dropNode = null
dragState.value.allowDrop = true
})
return {
dragState,
}
}
interface DragNodeEmitter {
emitter: Emitter
}
export function useDragNodeEmitter(): DragNodeEmitter {
const emitter = inject<Emitter>('DragNodeEmitter')
return {
emitter,
}
}

View File

@ -0,0 +1,69 @@
import { onMounted, onUpdated, onBeforeUnmount,ref, watch, Ref } from 'vue'
import { eventKeys } from '@element-plus/utils/aria'
import { on, off } from '@element-plus/utils/dom'
interface UseKeydownOption {
el$: Ref<HTMLElement>
}
export function useKeydown({ el$ }: UseKeydownOption) {
const treeItems = ref<Nullable<HTMLElement>[]>([])
const checkboxItems = ref<Nullable<HTMLElement>[]>([])
onMounted(() => {
initTabIndex()
on(el$.value, 'keydown', handleKeydown)
})
onBeforeUnmount(() => {
off(el$.value, 'keydown', handleKeydown)
})
onUpdated(() => {
treeItems.value = Array.from(el$.value.querySelectorAll('[role=treeitem]'))
checkboxItems.value = Array.from(el$.value.querySelectorAll('input[type=checkbox]'))
})
watch(checkboxItems, val => {
val.forEach(checkbox => {
checkbox.setAttribute('tabindex', '-1')
})
})
const handleKeydown = (ev: KeyboardEvent): void => {
const currentItem = ev.target as HTMLElement
if (currentItem.className.indexOf('el-tree-node') === -1) return
const keyCode = ev.keyCode
treeItems.value = Array.from(el$.value.querySelectorAll('.is-focusable[role=treeitem]'))
const currentIndex = treeItems.value.indexOf(currentItem)
let nextIndex
if ([eventKeys.up, eventKeys.down].indexOf(keyCode) > -1) {
ev.preventDefault()
if (keyCode === eventKeys.up) {
nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0
} else {
nextIndex = (currentIndex < treeItems.value.length - 1) ? currentIndex + 1 : 0
}
treeItems.value[nextIndex].focus()
}
if ([eventKeys.left, eventKeys.right].indexOf(keyCode) > -1) {
ev.preventDefault()
currentItem.click()
}
const hasInput = currentItem.querySelector('[type="checkbox"]') as Nullable<HTMLInputElement>
if ([eventKeys.enter, eventKeys.space].indexOf(keyCode) > -1 && hasInput) {
ev.preventDefault()
hasInput.click()
}
}
const initTabIndex = (): void => {
treeItems.value = Array.from(el$.value.querySelectorAll('.is-focusable[role=treeitem]'))
checkboxItems.value = Array.from(el$.value.querySelectorAll('input[type=checkbox]'))
const checkedItem = el$.value.querySelectorAll('.is-checked[role=treeitem]')
if (checkedItem.length) {
checkedItem[0].setAttribute('tabindex', '0')
return
}
treeItems.value[0]?.setAttribute('tabindex', '0')
}
}

View File

@ -0,0 +1,34 @@
import { inject, provide } from 'vue'
import Node from '../model/node'
interface NodeMap {
treeNodeExpand(node: Node) : void
children: NodeMap[]
}
export function useNodeExpandEventBroadcast(props){
const parentNodeMap = inject<NodeMap>('TreeNodeMap', null)
const currentNodeMap: NodeMap = {
treeNodeExpand: node => {
if (props.node !== node) {
props.node.collapse()
}
},
children: [],
}
if (parentNodeMap) {
parentNodeMap.children.push(currentNodeMap)
}
provide('TreeNodeMap', currentNodeMap)
return {
broadcastExpanded: (node: Node): void => {
if (!props.accordion) return
for(const childNode of currentNodeMap.children) {
childNode.treeNodeExpand(node)
}
},
}
}

View File

@ -0,0 +1,19 @@
import Node from './node'
import { TreeKey, TreeNodeData } from '../tree.d'
export const NODE_KEY = '$treeNodeId'
export const markNodeData = function(node: Node, data: TreeNodeData): void {
if (!data || data[NODE_KEY]) return
Object.defineProperty(data, NODE_KEY, {
value: node.id,
enumerable: false,
configurable: false,
writable: false,
})
}
export const getNodeKey = function(key: TreeKey, data: TreeNodeData): any {
if (!key) return data[NODE_KEY]
return data[key]
}

View File

@ -0,0 +1,32 @@
<script lang='ts'>
import { h, defineComponent, inject, ComponentInternalInstance } from 'vue'
import { RootTreeType } from './tree.d'
export default defineComponent({
name: 'ElTreeNodeContent',
props: {
node: {
type: Object,
required: true,
},
renderContent: Function,
},
setup(props) {
const nodeInstance = inject<ComponentInternalInstance>('NodeInstance')
const tree = inject<RootTreeType>('RootTree')
return () => {
const node = props.node
const { data, store } = node
return (
props.renderContent
? props.renderContent(h, { _self: nodeInstance, node, data, store })
: tree.ctx.slots.default
? tree.ctx.slots.default({ node, data })
: h('span', { class: 'el-tree-node__label' }, [node.label])
)
}
},
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,265 @@
<template>
<div
v-show="node.visible"
ref="node$"
class="el-tree-node"
:class="{
'is-expanded': expanded,
'is-current': node.isCurrent,
'is-hidden': !node.visible,
'is-focusable': !node.disabled,
'is-checked': !node.disabled && node.checked,
}"
role="treeitem"
tabindex="-1"
:aria-expanded="expanded"
:aria-disabled="node.disabled"
:aria-checked="node.checked"
:draggable="tree.props.draggable"
@click.stop="handleClick"
@contextmenu="handleContextMenu"
@dragstart.stop="handleDragStart"
@dragover.stop="handleDragOver"
@dragend.stop="handleDragEnd"
@drop.stop="handleDrop"
>
<div
class="el-tree-node__content"
:style="{ 'padding-left': (node.level - 1) * tree.props.indent + 'px' }"
>
<span
:class="[
{
'is-leaf': node.isLeaf,
expanded: !node.isLeaf && expanded,
},
'el-tree-node__expand-icon',
tree.props.iconClass ? tree.props.iconClass : 'el-icon-caret-right',
]"
@click.stop="handleExpandIconClick"
>
</span>
<el-checkbox
v-if="showCheckbox"
:model-value="node.checked"
:indeterminate="node.indeterminate"
:disabled="!!node.disabled"
@click.stop
@change="handleCheckChange"
/>
<span
v-if="node.loading"
class="el-tree-node__loading-icon el-icon-loading"
>
</span>
<node-content :node="node" :render-content="renderContent" />
</div>
<el-collapse-transition>
<div
v-if="!renderAfterExpand || childNodeRendered"
v-show="expanded"
class="el-tree-node__children"
role="group"
:aria-expanded="expanded"
>
<el-tree-node
v-for="child in node.childNodes"
:key="getNodeKey(child)"
:render-content="renderContent"
:render-after-expand="renderAfterExpand"
:show-checkbox="showCheckbox"
:node="child"
@node-expand="handleChildNodeExpand"
/>
</div>
</el-collapse-transition>
</div>
</template>
<script lang='ts'>
import { defineComponent, getCurrentInstance, ref, watch, nextTick, inject, provide, PropType, ComponentInternalInstance } from 'vue'
import ElCollapseTransition from '@element-plus/transition/collapse-transition/index.vue'
import { Checkbox as ElCheckbox } from '@element-plus/checkbox'
import NodeContent from './tree-node-content.vue'
import { getNodeKey as getNodeKeyUtil } from './model/util'
import { useNodeExpandEventBroadcast } from './model/useNodeExpandEventBroadcast'
import { useDragNodeEmitter } from './model/useDragNode'
import Node from './model/node'
import { TreeOptionProps, TreeNodeData, RootTreeType } from './tree.d'
export default defineComponent({
name: 'ElTreeNode',
components: {
ElCollapseTransition,
ElCheckbox,
NodeContent,
},
props: {
node: {
type: Node,
default: () => ({}),
},
props: {
type: Object as PropType<TreeOptionProps>,
default: () => ({}),
},
renderContent: Function,
renderAfterExpand: Boolean,
showCheckbox: {
type: Boolean,
default: false,
},
},
emits: ['node-expand'],
setup(props, ctx) {
const { broadcastExpanded } = useNodeExpandEventBroadcast(props)
const tree = inject<RootTreeType>('RootTree')
const expanded = ref(false)
const childNodeRendered = ref(false)
const oldChecked = ref<boolean>(null)
const oldIndeterminate = ref<boolean>(null)
const node$ = ref<Nullable<HTMLElement>>(null)
const { emitter } = useDragNodeEmitter()
const instance = getCurrentInstance()
provide('NodeInstance', instance)
if(!tree) {
console.warn('Can not find node\'s tree.')
}
if(props.node.expanded) {
expanded.value = true
childNodeRendered.value = true
}
const childrenKey = tree.props['children'] || 'children'
watch(() => props.node.data[childrenKey], () => {
props.node.updateChildren()
})
watch(() => props.node.indeterminate, val => {
handleSelectChange(props.node.checked, val)
})
watch(() => props.node.checked, val => {
handleSelectChange(val, props.node.indeterminate)
})
watch(() => props.node.expanded, val => {
nextTick(() => expanded.value = val)
if(val) {
childNodeRendered.value = true
}
})
const getNodeKey = (node: Node): any => {
return getNodeKeyUtil(tree.props.nodeKey, node.data)
}
const handleSelectChange = (checked: boolean, indeterminate: boolean) => {
if (oldChecked.value !== checked && oldIndeterminate.value !== indeterminate) {
tree.ctx.emit('check-change', props.node.data, checked, indeterminate)
}
oldChecked.value = checked
oldIndeterminate.value = indeterminate
}
const handleClick = () => {
const store = tree.store.value
store.setCurrentNode(props.node)
tree.ctx.emit('current-change', store.currentNode ? store.currentNode.data : null, store.currentNode)
tree.currentNode.value = props.node
if(tree.props.expandOnClickNode) {
handleExpandIconClick()
}
if(tree.props.checkOnClickNode && !props.node.disabled) {
handleCheckChange(null, {
target: { checked: !props.node.checked },
})
}
tree.ctx.emit('node-click', props.node.data, props.node, instance)
}
const handleContextMenu = (event: Event) => {
if (tree.instance.vnode.props['onNode-contextmenu']) {
event.stopPropagation()
event.preventDefault()
}
tree.ctx.emit('node-contextmenu', event, props.node.data, props.node, instance)
}
const handleExpandIconClick = () => {
if (props.node.isLeaf) return
if (expanded.value) {
tree.ctx.emit('node-collapse', props.node.data, props.node, instance)
props.node.collapse()
} else {
props.node.expand()
ctx.emit('node-expand', props.node.data, props.node, instance)
}
}
const handleCheckChange = (value, ev) => {
props.node.setChecked(ev.target.checked, !tree.props.checkStrictly)
nextTick(() => {
const store = tree.store.value
tree.ctx.emit('check', props.node.data, {
checkedNodes: store.getCheckedNodes(),
checkedKeys: store.getCheckedKeys(),
halfCheckedNodes: store.getHalfCheckedNodes(),
halfCheckedKeys: store.getHalfCheckedKeys(),
})
})
}
const handleChildNodeExpand = (nodeData: TreeNodeData, node: Node, instance: ComponentInternalInstance) => {
broadcastExpanded(node)
tree.ctx.emit('node-expand', nodeData, node, instance)
}
const handleDragStart = (event: DragEvent) => {
if (!tree.props.draggable) return
emitter.emit('tree-node-drag-start', { event, treeNode: props })
}
const handleDragOver = (event: DragEvent) => {
if (!tree.props.draggable) return
emitter.emit('tree-node-drag-over', { event, treeNode: { $el: node$.value, node: props.node } })
event.preventDefault()
}
const handleDrop = (event: DragEvent) => {
event.preventDefault()
}
const handleDragEnd = (event: DragEvent) => {
if (!tree.props.draggable) return
emitter.emit('tree-node-drag-end', event)
}
return {
node$,
tree,
expanded,
childNodeRendered,
oldChecked,
oldIndeterminate,
emitter,
parent,
getNodeKey,
handleSelectChange,
handleClick,
handleContextMenu,
handleExpandIconClick,
handleCheckChange,
handleChildNodeExpand,
handleDragStart,
handleDragOver,
handleDrop,
handleDragEnd,
}
},
})
</script>

101
packages/tree/src/tree.d.ts vendored Normal file
View File

@ -0,0 +1,101 @@
import { ComponentInternalInstance, VNode, h, SetupContext, Ref } from 'vue'
import Node from './model/node'
import TreeStore from './model/tree-store'
export interface RootTreeType {
ctx: SetupContext<any>
props: TreeComponentProps
store: Ref<TreeStore>
root: Ref<Node>
currentNode: Ref<Node>
instance: ComponentInternalInstance
}
export declare type hType = typeof h
export declare type TreeData = TreeNodeData[]
export declare type TreeKey = string | number
export declare interface FakeNode {
data: TreeNodeData
}
export declare interface TreeNodeData {
[key: string]: any
}
export declare interface TreeNodeLoadedDefaultProps {
checked?: boolean
}
export declare interface TreeNodeChildState {
all: boolean
none: boolean
allWithoutDisable: boolean
half: boolean
}
export declare interface TreeNodeOptions {
data: TreeNodeData
store: TreeStore
parent?: Node
}
export declare interface TreeStoreNodesMap {
[key: string]: Node
}
export declare interface TreeStoreOptions {
key: TreeKey
data: TreeData
lazy: boolean
props: TreeOptionProps
load: LoadFunction
currentNodeKey: TreeKey
checkStrictly: boolean
checkDescendants: boolean
defaultCheckedKeys: TreeKey[]
defaultExpandedKeys: TreeKey[]
autoExpandParent: boolean
defaultExpandAll: boolean
filterNodeMethod: FilterNodeMethodFunction
}
export declare interface TreeOptionProps {
children: string
label: string
disabled: string
isLeaf?: boolean
}
export declare type RenderContentFunction = (h: hType, context: RenderContentContext) => (VNode | VNode[])
export declare interface RenderContentContext {
_self: ComponentInternalInstance
node: Node
data: TreeNodeData
store: TreeStore
}
export declare type AllowDragFunction = (node: Node) => boolean
export declare type DropType = 'inner' | 'prev' | 'next'
export declare type AllowDropFunction = (draggingNode: Node, dropNode: Node, type: DropType) => boolean
export declare type LoadFunction = (rootNode: Node, loadedCallback: (data: TreeData) => void) => void
export declare type FilterValue = any
export declare type FilterNodeMethodFunction = (value: FilterValue, data: TreeNodeData, child: Node) => boolean
export declare interface TreeComponentProps {
data: TreeData
emptyText: string
renderAfterExpand: boolean
nodeKey: string
checkStrictly: boolean
expandOnClickNode: boolean
defaultExpandAll: boolean
checkOnClickNode: boolean
checkDescendants: boolean
autoExpandParent: boolean
defaultCheckedKeys: TreeKey[]
defaultExpandedKeys: TreeKey[]
currentNodeKey: TreeKey
renderContent: RenderContentFunction
showCheckbox: boolean
draggable: boolean
allowDrag: AllowDragFunction
allowDrop: AllowDropFunction
props: TreeOptionProps
lazy: boolean
highlightCurrent: boolean
load: LoadFunction
filterNodeMethod: FilterNodeMethodFunction
accordion: boolean
indent: number
iconClass: string
}

339
packages/tree/src/tree.vue Normal file
View File

@ -0,0 +1,339 @@
<template>
<div
ref="el$"
class="el-tree"
:class="{
'el-tree--highlight-current': highlightCurrent,
'is-dragging': !!dragState.draggingNode,
'is-drop-not-allow': !dragState.allowDrop,
'is-drop-inner': dragState.dropType === 'inner'
}"
role="tree"
>
<el-tree-node
v-for="child in root.childNodes"
:key="getNodeKey(child)"
:node="child"
:props="props"
:render-after-expand="renderAfterExpand"
:show-checkbox="showCheckbox"
:render-content="renderContent"
@node-expand="handleNodeExpand"
/>
<div v-if="isEmpty" class="el-tree__empty-block">
<span class="el-tree__empty-text">{{ emptyText }}</span>
</div>
<div
v-show="dragState.showDropIndicator"
ref="dropIndicator$"
class="el-tree__drop-indicator"
>
</div>
</div>
</template>
<script lang='ts'>
import { defineComponent, ref, provide, computed, watch, PropType, getCurrentInstance, ComponentInternalInstance } from 'vue'
import TreeStore from './model/tree-store'
import { getNodeKey as getNodeKeyUtil } from './model/util'
import ElTreeNode from './tree-node.vue'
import { useNodeExpandEventBroadcast } from './model/useNodeExpandEventBroadcast'
import { useDragNodeHandler } from './model/useDragNode'
import { useKeydown } from './model/useKeydown'
import Node from './model/node'
import { t } from '@element-plus/locale'
import {
TreeComponentProps,
TreeNodeData,
TreeKey,
TreeData,
RootTreeType,
} from './tree.d'
export default defineComponent({
name: 'ElTree',
components: { ElTreeNode },
props: {
data: {
type: Array,
},
emptyText: {
type: String,
default() {
return t('el.tree.emptyText')
},
},
renderAfterExpand: {
type: Boolean,
default: true,
},
nodeKey: String,
checkStrictly: Boolean,
defaultExpandAll: Boolean,
expandOnClickNode: {
type: Boolean,
default: true,
},
checkOnClickNode: Boolean,
checkDescendants: {
type: Boolean,
default: false,
},
autoExpandParent: {
type: Boolean,
default: true,
},
defaultCheckedKeys: Array,
defaultExpandedKeys: Array,
currentNodeKey: [String, Number] as PropType<string | number>,
renderContent: Function,
showCheckbox: {
type: Boolean,
default: false,
},
draggable: {
type: Boolean,
default: false,
},
allowDrag: Function,
allowDrop: Function,
props: {
type: Object,
default() {
return {
children: 'children',
label: 'label',
disabled: 'disabled',
}
},
},
lazy: {
type: Boolean,
default: false,
},
highlightCurrent: Boolean,
load: Function,
filterNodeMethod: Function,
accordion: Boolean,
indent: {
type: Number,
default: 18,
},
iconClass: String,
},
emits: [
'check-change',
'current-change',
'node-click',
'node-contextmenu',
'node-collapse',
'node-expand',
'check',
'node-drag-start',
'node-drag-end',
'node-drop',
'node-drag-leave',
'node-drag-enter',
'node-drag-over',
],
setup(props: TreeComponentProps, ctx) {
const store = ref<TreeStore>(new TreeStore({
key: props.nodeKey,
data: props.data,
lazy: props.lazy,
props: props.props,
load: props.load,
currentNodeKey: props.currentNodeKey,
checkStrictly: props.checkStrictly,
checkDescendants: props.checkDescendants,
defaultCheckedKeys: props.defaultCheckedKeys,
defaultExpandedKeys: props.defaultExpandedKeys,
autoExpandParent: props.autoExpandParent,
defaultExpandAll: props.defaultExpandAll,
filterNodeMethod: props.filterNodeMethod,
}))
const root = ref<Node>(store.value.root)
const currentNode = ref<Node>(null)
const el$ = ref<Nullable<HTMLElement>>(null)
const dropIndicator$ = ref<Nullable<HTMLElement>>(null)
const { broadcastExpanded } = useNodeExpandEventBroadcast(props)
const { dragState } = useDragNodeHandler({
props, ctx, el$, dropIndicator$, store,
})
useKeydown({ el$ })
const isEmpty = computed(() => {
const { childNodes } = root.value
return !childNodes || childNodes.length === 0 || childNodes.every(({ visible }) => !visible)
})
watch(() => props.defaultCheckedKeys, newVal => {
store.value.setDefaultCheckedKey(newVal)
})
watch(() => props.defaultExpandedKeys, newVal => {
store.value.defaultExpandedKeys = newVal
store.value.setDefaultExpandedKeys(newVal)
})
watch(() => props.data, newVal => {
store.value.setData(newVal)
})
watch(() => props.checkStrictly, newVal => {
store.value.checkStrictly = newVal
})
const filter = value => {
if (!props.filterNodeMethod) throw new Error('[Tree] filterNodeMethod is required when filter')
store.value.filter(value)
}
const getNodeKey = (node: Node) => {
return getNodeKeyUtil(props.nodeKey, node.data)
}
const getNodePath = (data: TreeKey | TreeNodeData) => {
if (!props.nodeKey) throw new Error('[Tree] nodeKey is required in getNodePath')
const node = store.value.getNode(data)
if (!node) return []
const path = [node.data]
let parent = node.parent
while (parent && parent !== root.value) {
path.push(parent.data)
parent = parent.parent
}
return path.reverse()
}
const getCheckedNodes = (leafOnly: boolean, includeHalfChecked: boolean): TreeNodeData[] => {
return store.value.getCheckedNodes(leafOnly, includeHalfChecked)
}
const getCheckedKeys = (leafOnly: boolean): TreeKey[] => {
return store.value.getCheckedKeys(leafOnly)
}
const getCurrentNode = (): TreeNodeData => {
const currentNode = store.value.getCurrentNode()
return currentNode ? currentNode.data : null
}
const getCurrentKey = (): any => {
if (!props.nodeKey) throw new Error('[Tree] nodeKey is required in getCurrentKey')
const currentNode = getCurrentNode()
return currentNode ? currentNode[props.nodeKey] : null
}
const setCheckedNodes = (nodes: Node[], leafOnly: boolean) => {
if (!props.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedNodes')
store.value.setCheckedNodes(nodes, leafOnly)
}
const setCheckedKeys = (keys, leafOnly: boolean) => {
if (!props.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedKeys')
store.value.setCheckedKeys(keys, leafOnly)
}
const setChecked = (data: TreeKey | TreeNodeData, checked: boolean, deep: boolean) => {
store.value.setChecked(data, checked, deep)
}
const getHalfCheckedNodes = (): TreeNodeData[] => {
return store.value.getHalfCheckedNodes()
}
const getHalfCheckedKeys = (): TreeKey[] => {
return store.value.getHalfCheckedKeys()
}
const setCurrentNode = (node: Node) => {
if (!props.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentNode')
store.value.setUserCurrentNode(node)
}
const setCurrentKey = (key: TreeKey) => {
if (!props.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentKey')
store.value.setCurrentNodeKey(key)
}
const getNode = (data: TreeKey | TreeNodeData): Node => {
return store.value.getNode(data)
}
const remove = (data: TreeNodeData | Node) => {
store.value.remove(data)
}
const append = (data: TreeNodeData, parentNode: TreeNodeData | TreeKey | Node) => {
store.value.append(data, parentNode)
}
const insertBefore = (data: TreeNodeData, refNode: TreeKey | TreeNodeData) => {
store.value.insertBefore(data, refNode)
}
const insertAfter = (data: TreeNodeData, refNode: TreeKey | TreeNodeData) => {
store.value.insertAfter(data, refNode)
}
const handleNodeExpand = (nodeData: TreeNodeData, node: Node, instance: ComponentInternalInstance) => {
broadcastExpanded(node)
ctx.emit('node-expand', nodeData, node, instance)
}
const updateKeyChildren = (key: TreeKey, data: TreeData) => {
if (!props.nodeKey) throw new Error('[Tree] nodeKey is required in updateKeyChild')
store.value.updateChildren(key, data)
}
provide('RootTree', {
ctx,
props,
store,
root,
currentNode,
instance: getCurrentInstance(),
} as RootTreeType)
return {
// ref
store,
root,
currentNode,
dragState,
el$,
dropIndicator$,
// computed
isEmpty,
// methods
filter,
getNodeKey,
getNodePath,
getCheckedNodes,
getCheckedKeys,
getCurrentNode,
getCurrentKey,
setCheckedNodes,
setCheckedKeys,
setChecked,
getHalfCheckedNodes,
getHalfCheckedKeys,
setCurrentNode,
setCurrentKey,
getNode,
remove,
append,
insertBefore,
insertAfter,
handleNodeExpand,
updateKeyChildren,
}
},
})
</script>

14
packages/utils/merge.ts Normal file
View File

@ -0,0 +1,14 @@
export default function objectAssign(target, ...agrvs) {
for (let i = 0, j = agrvs.length; i < j; i++) {
const source = agrvs[i] || {}
for (const prop in source) {
if (source.hasOwnProperty(prop)) {
const value = source[prop]
if (value !== undefined) {
target[prop] = value
}
}
}
}
return target
}

View File

@ -188,8 +188,7 @@ The checkbox of a node can be set as disabled.
<el-tree
:data="data"
:props="defaultProps"
show-checkbox
@check-change="handleCheckChange">
show-checkbox>
</el-tree>
<script>
@ -421,23 +420,21 @@ The content of tree nodes can be customized, so you can add icons or buttons as
node-key="id"
default-expand-all
:expand-on-click-node="false">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
type="text"
size="mini"
@click="() => append(data)">
Append
</el-button>
<el-button
type="text"
size="mini"
@click="() => remove(node, data)">
Delete
</el-button>
<template #default="{ node, data }">
<span class="custom-tree-node">
<span>{{ node.label }}</span>
<span>
<a
@click="append(data)">
Append
</a>
<a
@click="remove(node, data)">
Delete
</a>
</span>
</span>
</span>
</template>
</el-tree>
</div>
</div>
@ -492,9 +489,10 @@ The content of tree nodes can be customized, so you can add icons or buttons as
append(data) {
const newChild = { id: id++, label: 'testtest', children: [] };
if (!data.children) {
this.$set(data, 'children', []);
data.children = [];
}
data.children.push(newChild);
this.data = [...this.data]
},
remove(node, data) {
@ -502,20 +500,17 @@ The content of tree nodes can be customized, so you can add icons or buttons as
const children = parent.data.children || parent.data;
const index = children.findIndex(d => d.id === data.id);
children.splice(index, 1);
this.data = [...this.data]
},
renderContent(h, { node, data, store }) {
return h("span", {
class: "custom-tree-node"
}, h("span", null, node.label), h("span", null, h("el-button", {
size: "mini",
type: "text",
"on-click":this.append(data)
}, "Append"), h("el-button", {
size: "mini",
type: "text",
"on-click": this.remove(node, data)
}, "Delete")))
}, h("span", null, node.label), h("span", null, h("a", {
onClick: () => this.append(data)
}, "Append "), h("a", {
onClick: () => this.remove(node, data)
}, "Delete")));
}
}
};

View File

@ -188,8 +188,7 @@ El checkbox de un nodo se puede poner como desactivado.
<el-tree
:data="data"
:props="defaultProps"
show-checkbox
@check-change="handleCheckChange">
show-checkbox>
</el-tree>
<script>
@ -421,23 +420,21 @@ El contenido de los nodos puede ser personalizado, así que puede añadir iconos
node-key="id"
default-expand-all
:expand-on-click-node="false">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
type="text"
size="mini"
@click="() => append(data)">
Append
</el-button>
<el-button
type="text"
size="mini"
@click="() => remove(node, data)">
Delete
</el-button>
<template #default="{ node, data }">
<span class="custom-tree-node">
<span>{{ node.label }}</span>
<span>
<a
@click="append(data)">
Append
</a>
<a
@click="remove(node, data)">
Delete
</a>
</span>
</span>
</span>
</template>
</el-tree>
</div>
</div>
@ -492,9 +489,10 @@ El contenido de los nodos puede ser personalizado, así que puede añadir iconos
append(data) {
const newChild = { id: id++, label: 'testtest', children: [] };
if (!data.children) {
this.$set(data, 'children', []);
data.children = []
}
data.children.push(newChild);
this.data = [...this.data]
},
remove(node, data) {
@ -502,20 +500,17 @@ El contenido de los nodos puede ser personalizado, así que puede añadir iconos
const children = parent.data.children || parent.data;
const index = children.findIndex(d => d.id === data.id);
children.splice(index, 1);
this.data = [...this.data]
},
renderContent(h, { node, data, store }) {
return h("span", {
class: "custom-tree-node"
}, h("span", null, node.label), h("span", null, h("el-button", {
size: "mini",
type: "text",
"on-click":this.append(data)
}, "Append"), h("el-button", {
size: "mini",
type: "text",
"on-click": this.remove(node, data)
}, "Delete")));
class: "custom-tree-node"
}, h("span", null, node.label), h("span", null, h("a", {
onClick: () => this.append(data)
}, "Append "), h("a", {
onClick: () => this.remove(node, data)
}, "Delete")));
}
}
};

View File

@ -188,8 +188,7 @@ Les checkbox des noeuds peuvent être désactivées individuellement.
<el-tree
:data="data"
:props="defaultProps"
show-checkbox
@check-change="handleCheckChange">
show-checkbox>
</el-tree>
<script>
@ -423,23 +422,21 @@ Le contenu des noeuds peut être personnalisé, afin de pouvoir ajouter des icô
node-key="id"
default-expand-all
:expand-on-click-node="false">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
type="text"
size="mini"
@click="() => append(data)">
Ajouter
</el-button>
<el-button
type="text"
size="mini"
@click="() => remove(node, data)">
Supprimer
</el-button>
<template #default="{ node, data }">
<span class="custom-tree-node">
<span>{{ node.label }}</span>
<span>
<a
@click="append(data)">
Append
</a>
<a
@click="remove(node, data)">
Delete
</a>
</span>
</span>
</span>
</template>
</el-tree>
</div>
</div>
@ -494,9 +491,10 @@ Le contenu des noeuds peut être personnalisé, afin de pouvoir ajouter des icô
append(data) {
const newChild = { id: id++, label: 'testtest', children: [] };
if (!data.children) {
this.$set(data, 'children', []);
data.children = []
}
data.children.push(newChild);
this.data = [...this.data]
},
remove(node, data) {
@ -504,20 +502,17 @@ Le contenu des noeuds peut être personnalisé, afin de pouvoir ajouter des icô
const children = parent.data.children || parent.data;
const index = children.findIndex(d => d.id === data.id);
children.splice(index, 1);
this.data = [...this.data]
},
renderContent(h, { node, data, store }) {
return h("span", {
class: "custom-tree-node"
}, h("span", null, node.label), h("span", null, h("el-button", {
size: "mini",
type: "text",
"on-click":this.append(data)
}, "Append"), h("el-button", {
size: "mini",
type: "text",
"on-click": this.remove(node, data)
}, "Delete")))
}, h("span", null, node.label), h("span", null, h("a", {
onClick: () => this.append(data)
}, "Append "), h("a", {
onClick: () => this.remove(node, data)
}, "Delete")));
}
}
};

View File

@ -420,23 +420,21 @@
node-key="id"
default-expand-all
:expand-on-click-node="false">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button
type="text"
size="mini"
@click="() => append(data)">
Append
</el-button>
<el-button
type="text"
size="mini"
@click="() => remove(node, data)">
Delete
</el-button>
<template #default="{ node, data }">
<span class="custom-tree-node">
<span>{{ node.label }}</span>
<span>
<a
@click="append(data)">
Append
</a>
<a
@click="remove(node, data)">
Delete
</a>
</span>
</span>
</span>
</template>
</el-tree>
</div>
</div>
@ -491,9 +489,10 @@
append(data) {
const newChild = { id: id++, label: 'testtest', children: [] };
if (!data.children) {
this.$set(data, 'children', []);
data.children = []
}
data.children.push(newChild);
this.data = [...this.data]
},
remove(node, data) {
@ -501,19 +500,16 @@
const children = parent.data.children || parent.data;
const index = children.findIndex(d => d.id === data.id);
children.splice(index, 1);
this.data = [...this.data]
},
renderContent(h, { node, data, store }) {
return h("span", {
class: "custom-tree-node"
}, h("span", null, node.label), h("span", null, h("el-button", {
size: "mini",
type: "text",
"on-click":this.append(data)
}, "Append"), h("el-button", {
size: "mini",
type: "text",
"on-click": this.remove(node, data)
}, h("span", null, node.label), h("span", null, h("a", {
onClick: () => this.append(data)
}, "Append "), h("a", {
onClick: () => this.remove(node, data)
}, "Delete")));
}
}