mirror of
https://github.com/element-plus/element-plus.git
synced 2024-12-09 02:31:33 +08:00
feat(tree): tree component
This commit is contained in:
parent
4b7f671774
commit
567da19429
@ -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)
|
||||
|
@ -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 = {
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
837
packages/tree/__tests__/tree.spec.ts
Normal file
837
packages/tree/__tests__/tree.spec.ts
Normal 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)
|
||||
})
|
||||
|
||||
})
|
94
packages/tree/doc/basic.vue
Normal file
94
packages/tree/doc/basic.vue
Normal 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>
|
104
packages/tree/doc/basic10.vue
Normal file
104
packages/tree/doc/basic10.vue
Normal 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>
|
60
packages/tree/doc/basic2.vue
Normal file
60
packages/tree/doc/basic2.vue
Normal 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>
|
40
packages/tree/doc/basic3.vue
Normal file
40
packages/tree/doc/basic3.vue
Normal 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>
|
59
packages/tree/doc/basic4.vue
Normal file
59
packages/tree/doc/basic4.vue
Normal 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>
|
49
packages/tree/doc/basic5.vue
Normal file
49
packages/tree/doc/basic5.vue
Normal 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>
|
92
packages/tree/doc/basic6.vue
Normal file
92
packages/tree/doc/basic6.vue
Normal 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>
|
127
packages/tree/doc/basic7.vue
Normal file
127
packages/tree/doc/basic7.vue
Normal 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>
|
78
packages/tree/doc/basic8.vue
Normal file
78
packages/tree/doc/basic8.vue
Normal 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>
|
62
packages/tree/doc/basic9.vue
Normal file
62
packages/tree/doc/basic9.vue
Normal 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>
|
6
packages/tree/doc/index.stories.ts
Normal file
6
packages/tree/doc/index.stories.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export { default as BasicUsage } from './index.vue'
|
||||
|
||||
export default {
|
||||
title: 'Tree',
|
||||
}
|
||||
|
77
packages/tree/doc/index.vue
Normal file
77
packages/tree/doc/index.vue
Normal 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
5
packages/tree/index.ts
Normal 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)
|
||||
}
|
12
packages/tree/package.json
Normal file
12
packages/tree/package.json
Normal 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"
|
||||
}
|
||||
}
|
514
packages/tree/src/model/node.ts
Normal file
514
packages/tree/src/model/node.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
367
packages/tree/src/model/tree-store.ts
Normal file
367
packages/tree/src/model/tree-store.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
183
packages/tree/src/model/useDragNode.ts
Normal file
183
packages/tree/src/model/useDragNode.ts
Normal 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,
|
||||
}
|
||||
}
|
69
packages/tree/src/model/useKeydown.ts
Normal file
69
packages/tree/src/model/useKeydown.ts
Normal 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')
|
||||
}
|
||||
}
|
34
packages/tree/src/model/useNodeExpandEventBroadcast.ts
Normal file
34
packages/tree/src/model/useNodeExpandEventBroadcast.ts
Normal 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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
19
packages/tree/src/model/util.ts
Normal file
19
packages/tree/src/model/util.ts
Normal 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]
|
||||
}
|
32
packages/tree/src/tree-node-content.vue
Normal file
32
packages/tree/src/tree-node-content.vue
Normal 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>
|
265
packages/tree/src/tree-node.vue
Normal file
265
packages/tree/src/tree-node.vue
Normal 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
101
packages/tree/src/tree.d.ts
vendored
Normal 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
339
packages/tree/src/tree.vue
Normal 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
14
packages/utils/merge.ts
Normal 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
|
||||
}
|
@ -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")));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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")));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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")));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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")));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user