From abcf257be4554aa66708f4103059a25d287712a7 Mon Sep 17 00:00:00 2001 From: WumaCoder <1829913225@qq.com> Date: Sat, 16 Jan 2021 00:01:39 +0800 Subject: [PATCH] refactor(tree): Realize tree structured data display and dynamic update --- .../element3/packages/tree/NodeContent.vue | 28 - packages/element3/packages/tree/Tree.vue | 372 ------------- packages/element3/packages/tree/TreeNode.vue | 142 ----- .../tree/__tests__/entity/tree-node.spec.js | 238 --------- .../tree/__tests__/entity/tree.spec.js | 69 --- .../packages/tree/__tests__/libs/util.spec.js | 203 ------- .../packages/tree/__tests__/tree.spec.js | 209 -------- .../element3/packages/tree/entity/Action.js | 25 - .../element3/packages/tree/entity/Tree.js | 203 ------- .../element3/packages/tree/entity/TreeNode.js | 499 ------------------ packages/element3/packages/tree/libs/util.js | 119 ----- .../tree => src/components/Tree}/index.js | 4 +- .../element3/src/components/Tree/src/Tree.vue | 91 ++++ .../src/components/Tree/src/TreeNode.vue | 63 +++ .../components/Tree/src/TreeNodeContent.vue | 28 + .../components/Tree/src/entity/TreeNode.ts | 187 +++++++ .../element3/src/components/Tree/src/props.ts | 26 + .../element3/src/components/Tree/src/types.ts | 38 ++ .../src/components/Tree/src/utils/effect.ts | 40 ++ .../src/components/Tree/src/utils/useTools.ts | 14 + .../src/components/Tree/tests/Tree.spec.ts | 128 +++++ .../components/Tree/tests/TreeNode.spec.ts | 25 + .../Tree/tests/TreeNodeContent.spec.ts | 16 + .../Tree/tests/entity/TreeNode.spec.ts | 88 +++ .../src/components/Tree/tests/props.spec.ts | 3 + .../Tree/tests/utils/effect.spec.ts | 32 ++ .../Tree/tests/utils/useTools.spec.ts | 27 + packages/element3/src/index.js | 2 +- packages/website/src/i18n/component.json | 58 +- packages/website/src/play/index.vue | 62 ++- packages/website/src/route/nav.config.json | 2 +- 31 files changed, 887 insertions(+), 2154 deletions(-) delete mode 100644 packages/element3/packages/tree/NodeContent.vue delete mode 100644 packages/element3/packages/tree/Tree.vue delete mode 100644 packages/element3/packages/tree/TreeNode.vue delete mode 100644 packages/element3/packages/tree/__tests__/entity/tree-node.spec.js delete mode 100644 packages/element3/packages/tree/__tests__/entity/tree.spec.js delete mode 100644 packages/element3/packages/tree/__tests__/libs/util.spec.js delete mode 100644 packages/element3/packages/tree/__tests__/tree.spec.js delete mode 100644 packages/element3/packages/tree/entity/Action.js delete mode 100644 packages/element3/packages/tree/entity/Tree.js delete mode 100644 packages/element3/packages/tree/entity/TreeNode.js delete mode 100644 packages/element3/packages/tree/libs/util.js rename packages/element3/{packages/tree => src/components/Tree}/index.js (65%) create mode 100644 packages/element3/src/components/Tree/src/Tree.vue create mode 100644 packages/element3/src/components/Tree/src/TreeNode.vue create mode 100644 packages/element3/src/components/Tree/src/TreeNodeContent.vue create mode 100644 packages/element3/src/components/Tree/src/entity/TreeNode.ts create mode 100644 packages/element3/src/components/Tree/src/props.ts create mode 100644 packages/element3/src/components/Tree/src/types.ts create mode 100644 packages/element3/src/components/Tree/src/utils/effect.ts create mode 100644 packages/element3/src/components/Tree/src/utils/useTools.ts create mode 100644 packages/element3/src/components/Tree/tests/Tree.spec.ts create mode 100644 packages/element3/src/components/Tree/tests/TreeNode.spec.ts create mode 100644 packages/element3/src/components/Tree/tests/TreeNodeContent.spec.ts create mode 100644 packages/element3/src/components/Tree/tests/entity/TreeNode.spec.ts create mode 100644 packages/element3/src/components/Tree/tests/props.spec.ts create mode 100644 packages/element3/src/components/Tree/tests/utils/effect.spec.ts create mode 100644 packages/element3/src/components/Tree/tests/utils/useTools.spec.ts diff --git a/packages/element3/packages/tree/NodeContent.vue b/packages/element3/packages/tree/NodeContent.vue deleted file mode 100644 index b71824ca0..000000000 --- a/packages/element3/packages/tree/NodeContent.vue +++ /dev/null @@ -1,28 +0,0 @@ - diff --git a/packages/element3/packages/tree/Tree.vue b/packages/element3/packages/tree/Tree.vue deleted file mode 100644 index 1b2d7cb17..000000000 --- a/packages/element3/packages/tree/Tree.vue +++ /dev/null @@ -1,372 +0,0 @@ - - - diff --git a/packages/element3/packages/tree/TreeNode.vue b/packages/element3/packages/tree/TreeNode.vue deleted file mode 100644 index 801bcbffa..000000000 --- a/packages/element3/packages/tree/TreeNode.vue +++ /dev/null @@ -1,142 +0,0 @@ - - - diff --git a/packages/element3/packages/tree/__tests__/entity/tree-node.spec.js b/packages/element3/packages/tree/__tests__/entity/tree-node.spec.js deleted file mode 100644 index 0a3ac5473..000000000 --- a/packages/element3/packages/tree/__tests__/entity/tree-node.spec.js +++ /dev/null @@ -1,238 +0,0 @@ -import { TreeNode } from '../../entity/TreeNode' - -describe('TreeNode.js', () => { - it('test init options', () => { - const treeNode = new TreeNode(1, 'root', [ - new TreeNode(11, 'root-1', [ - new TreeNode(111, 'root-1-1'), - new TreeNode( - 112, - 'root-1-2', - [ - new TreeNode(1121, 'root-1-2-1'), - new TreeNode(1122, 'root-1-2-2'), - new TreeNode(1123, 'root-1-2-3') - ], - { isChecked: true, isExpanded: true } - ) - ]), - new TreeNode(12, 'root-2', [], { isExpanded: true, isChecked: true }) - ]) - - expect(treeNode.findOne(11).isExpanded).toBeTruthy() - expect(treeNode.findOne(11).isChecked).toBeTruthy() - expect(treeNode.findOne(11).isIndeterminate).toBeTruthy() - - expect(treeNode.findOne(1122).isChecked).toBeTruthy() - expect(treeNode.findOne(1122).isIndeterminate).toBeFalsy() - - expect(treeNode.isExpanded).toBeTruthy() - expect(treeNode.isChecked).toBeTruthy() - expect(treeNode.isIndeterminate).toBeFalsy() - }) - - it('test find node', () => { - const treeNode = new TreeNode(1, 'root', [ - new TreeNode(11, 'root-1', [ - new TreeNode(111, 'root-1-1'), - new TreeNode(112, 'root-1-2') - ]), - new TreeNode(12, 'root-2') - ]) - const node12 = treeNode.findOne(12) - - expect(node12.label).toBe('root-2') - - const nodes = treeNode.findMany('root-1') - - expect(nodes).toHaveLength(3) - }) - - it('append a node', () => { - const treeNode = new TreeNode(1, 'root', [ - new TreeNode(11, 'root-1', [ - new TreeNode(111, 'root-1-1'), - new TreeNode(112, 'root-1-2') - ]), - new TreeNode(12, 'root-2') - ]) - treeNode.appendChild(new TreeNode(13, 'root-3')) - expect(treeNode.findOne(13)).toBeTruthy() - }) - - it('insert a node', () => { - const treeNode = new TreeNode(1, 'root', [ - new TreeNode(11, 'root-1', [ - new TreeNode(111, 'root-1-1'), - new TreeNode(112, 'root-1-2') - ]), - new TreeNode(12, 'root-2') - ]) - treeNode.insertChild(1, new TreeNode(10, 'root-0')) - expect(treeNode.findOne(10)).toBeTruthy() - }) - - it('remove a node', () => { - const treeNode = new TreeNode(1, 'root', [ - new TreeNode(11, 'root-1', [ - new TreeNode(111, 'root-1-1'), - new TreeNode(112, 'root-1-2') - ]), - new TreeNode(12, 'root-2') - ]) - const oldLen = treeNode.childNodes.length - treeNode.removeChild(1) - expect(treeNode.childNodes).toHaveLength(oldLen - 1) - }) - - it('get root', () => { - const treeNode = new TreeNode(1, 'root', [ - new TreeNode(11, 'root-1', [ - new TreeNode(111, 'root-1-1'), - new TreeNode(112, 'root-1-2') - ]), - new TreeNode(12, 'root-2') - ]) - const node112 = treeNode.findOne(112) - - expect(node112.root).toBe(treeNode) - }) - - it('set checked', () => { - const treeNode = new TreeNode(1, 'root', [ - new TreeNode(11, 'root-1', [ - new TreeNode(111, 'root-1-1'), - new TreeNode(112, 'root-1-2') - ]), - new TreeNode(12, 'root-2') - ]) - const node111 = treeNode.findOne(111) - node111.setChecked(true) - expect(node111.isChecked).toBeTruthy() - expect(node111.parent.isIndeterminate).toBeTruthy() - expect(node111.root.isIndeterminate).toBeTruthy() - - const node112 = treeNode.findOne(112) - node112.setChecked(true) - expect(node112.isChecked).toBeTruthy() - expect(node112.parent.isChecked).toBeTruthy() - expect(node112.parent.isIndeterminate).toBeFalsy() - expect(node112.root.isIndeterminate).toBeTruthy() - }) - - it('test expand', () => { - const treeNode = new TreeNode(1, 'root', [ - new TreeNode(11, 'root-1', [ - new TreeNode(111, 'root-1-1'), - new TreeNode(112, 'root-1-2') - ]), - new TreeNode(12, 'root-2') - ]) - const node111 = treeNode.findOne(111) - node111.expand() - expect(node111.isExpanded).toBeTruthy() - expect(node111.root.isExpanded).toBeTruthy() - }) - - it('separation a node', () => { - const treeNode = new TreeNode(1, 'root', [ - new TreeNode(11, 'root-1', [ - new TreeNode(111, 'root-1-1'), - new TreeNode(112, 'root-1-2') - ]), - new TreeNode(12, 'root-2') - ]) - const node11 = treeNode.findOne(11) - node11.separation() - - expect(node11.parent).toBe(null) - expect(treeNode.findOne(11)).toBe(null) - expect(treeNode.childNodes).toHaveLength(1) - expect(node11.childNodes).toHaveLength(2) - }) - - it('move a node', () => { - const treeNode = new TreeNode(1, 'root', [ - new TreeNode(11, 'root-1', [ - new TreeNode(111, 'root-1-1'), - new TreeNode(112, 'root-1-2') - ]), - new TreeNode(12, 'root-2') - ]) - const node11 = treeNode.findOne(11) - - expect(node11.move(treeNode.findOne(12), 'bottom')).toBeTruthy() - expect(node11.move(treeNode.findOne(12), 'inner')).toBeTruthy() - expect(treeNode.childNodes).toHaveLength(1) - expect(treeNode.findOne(12).childNodes).toHaveLength(1) - }) - - it('async load data', () => { - const treeNode = new TreeNode(1, 'root', [ - new TreeNode(11, 'root-1', [ - new TreeNode(111, 'root-1-1'), - new TreeNode(112, 'root-1-2') - ]), - new TreeNode(12, 'root-2', [], { - isAsync: true, - asyncLoadFn: (node, resolve) => { - expect(node.asyncState).toBe('loading') - setTimeout(() => { - expect(node === treeNode.findOne(12)).toBeTruthy() - resolve([ - new TreeNode(121, '异步加载的节点'), - new TreeNode(122, '异步加载的节点') - ]) - expect(node.asyncState).toBe('loaded') - expect(node.childNodes).toHaveLength(2) - }, 1) - } - }) - ]) - - const node12 = treeNode.findOne(12) - expect(node12.isLeaf).toBeFalsy() - expect(node12.asyncState).toBe('notload') - node12.loadAsync() - }) - - it('collapse', () => { - const treeNode = new TreeNode(1, 'root', [ - new TreeNode(11, 'root-1', [ - new TreeNode(111, 'root-1-1'), - new TreeNode(112, 'root-1-2') - ]), - new TreeNode(12, 'root-2') - ]) - - const node11 = treeNode.findOne(11) - const node12 = treeNode.findOne(12) - node11.collapse() - expect(node11.isExpanded).toBeTruthy() - expect(node12.isExpanded).toBeFalsy() - node12.collapse() - expect(node11.isExpanded).toBeFalsy() - expect(node12.isExpanded).toBeTruthy() - }) - - it('filter', () => { - const treeNode = new TreeNode(1, 'root', [ - new TreeNode(11, 'root-1', [ - new TreeNode(111, 'root-1-1'), - new TreeNode(112, 'root-1-2') - ]), - new TreeNode(12, 'root-2') - ]) - const node111 = treeNode.findOne(111) - const node112 = treeNode.findOne(112) - const node12 = treeNode.findOne(12) - - treeNode.filter((node) => { - return node === node111 - }) - - expect(node111.isVisable).toBeTruthy() - expect(node112.isVisable).toBeFalsy() - expect(node12.isVisable).toBeFalsy() - }) -}) diff --git a/packages/element3/packages/tree/__tests__/entity/tree.spec.js b/packages/element3/packages/tree/__tests__/entity/tree.spec.js deleted file mode 100644 index e6fee551e..000000000 --- a/packages/element3/packages/tree/__tests__/entity/tree.spec.js +++ /dev/null @@ -1,69 +0,0 @@ -import { Tree } from '../../entity/Tree' - -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' - } - ] - } -] - -describe('Tree.js', () => { - it('instantiate Tree', () => { - const tree = new Tree(data /*, default */) - - expect(tree.root.childNodes).toHaveLength(data.length) - expect(tree.root.childNodes[0].label).toBe(data[0].label) - }) - - it('update', () => { - const tree = new Tree(data) - data[0].label = '更新后' - expect(tree.root.childNodes[0].label).toBe('一级 1') // update before - tree.update() - expect(tree.root.childNodes[0].label).toBe('更新后') // update after - }) -}) diff --git a/packages/element3/packages/tree/__tests__/libs/util.spec.js b/packages/element3/packages/tree/__tests__/libs/util.spec.js deleted file mode 100644 index 08474cbe4..000000000 --- a/packages/element3/packages/tree/__tests__/libs/util.spec.js +++ /dev/null @@ -1,203 +0,0 @@ -import { - nodeMap, - transitionObjectKey, - methodInterceptor, - extractMethods -} from '../../libs/util' - -describe('utils.js', () => { - it('nodeMap', () => { - const a = [ - { - 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' - } - ] - } - ] - - const b = nodeMap( - a[0], - (node) => { - return node - }, - { childKey: 'children', mapChildKey: 'children' } - ) - - expect(b === a[0]).toBeFalsy() - }) - - it('transitionObjectKey', () => { - const obj = { - id: 1, - label: '一级 1' - } - - transitionObjectKey(obj, { key: 'id', text: 'label' }) - - expect(obj).toStrictEqual({ - key: 1, - text: '一级 1' - }) - }) - - it('deep clone a and rename property', () => { - const a = [ - { - 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' - } - ] - } - ] - - const b = a.map((item) => { - return nodeMap( - item, - (node) => { - transitionObjectKey(node, { key: 'id', text: 'label' }) - return node - }, - { childKey: 'children', mapChildKey: 'childNodes' } - ) - }) - - expect(b[0].key).toBe(a[0].id) - expect(b[1].key).toBe(a[1].id) - expect(b[1].childNodes[0].key).toBe(a[1].children[0].id) - }) - - it('methodInterceptor', () => { - class O { - constructor(a) { - this.a = a - } - - sum(b) { - return this.a + b - } - } - - const o = new O(15) - - methodInterceptor( - O, - 'sum', - function (b) { - expect(this.a).toBe(15) - expect(b).toBe(10) - return [b] - }, - function (ret, [b]) { - expect(b).toBe(10) - expect(ret).toBe(25) - return 'change' - } - ) - - const num = o.sum(10) - expect(num).toBe('change') - }) - - it('extractMethods', () => { - class O { - constructor(a) { - this.a = a - } - - sum(b) { - return this.a + b - } - } - - const o = new O(15) - - const { sum } = extractMethods(o, ['sum']) - - expect(sum(10)).toBe(25) - }) -}) diff --git a/packages/element3/packages/tree/__tests__/tree.spec.js b/packages/element3/packages/tree/__tests__/tree.spec.js deleted file mode 100644 index 1939b1007..000000000 --- a/packages/element3/packages/tree/__tests__/tree.spec.js +++ /dev/null @@ -1,209 +0,0 @@ -import { mount } from '@vue/test-utils' -import { nextTick, ref } from 'vue' -import Tree from '../Tree.vue' - -function createData() { - 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', - isAsync: true - } - ] - } - ] - return data -} - -describe('Tree.vue', () => { - describe('props', () => { - it('data', async () => { - const tree = mount(Tree, { - props: { - data: createData(), - defaultNodeKey: { - childNodes: 'children' - }, - defaultExpandAll: true - } - }) - - const node10 = tree.find('#TreeNode10') - const node6 = tree.find('#TreeNode6') - - expect(node10.exists()).toBeTruthy() - expect(node6.exists()).toBeTruthy() - }) - - it('async', async () => { - let id = 1 - const asyncLoadFn = (node, resolve) => { - if (node.level < 3) { - resolve([ - { - id: ++id, - label: 'T' + id - }, - { - id: ++id, - label: 'T' + id - }, - { - id: ++id, - label: 'T' + id - } - ]) - } - } - const tree = mount(Tree, { - props: { - data: [ - { - id: 1, - label: 'T1' - } - ], - defaultExpandAll: true, - async: true, - asyncLoadFn: asyncLoadFn - } - }) - const root = tree.vm.root - root.childNodes[0].expand() - expect(root.childNodes[0].childNodes).toHaveLength(3) - }) - - it('async and checked', async () => { - let id = 1 - const asyncLoadFn = (node, resolve) => { - if (node.level < 3) { - resolve([ - { - id: ++id, - label: 'T' + id - }, - { - id: ++id, - label: 'T' + id - }, - { - id: ++id, - label: 'T' + id - } - ]) - } - } - const tree = mount(Tree, { - props: { - data: [ - { - id: 1, - label: 'T1' - } - ], - defaultExpandAll: true, - async: true, - showCheckbox: true, - asyncLoadFn: asyncLoadFn - } - }) - const root = tree.vm.root - const t1 = root.childNodes[0] - t1.expand() - t1.setChecked(true) - expect(t1.checkedNodes.length).toBe(3) - t1.childNodes[0].setChecked(false) - expect(t1.checkedNodes.length).toBe(2) - expect(t1.isChecked && !t1.isIndeterminate).toBe(false) // when isIndeterminate=false and isChecked=true, t1 checkbox is checked state - }) - }) - - describe('events', () => { - it('node-click', () => { - const tree = mount(Tree, { - props: { - data: createData(), - defaultNodeKey: { - childNodes: 'children' - }, - defaultExpandAll: true, - 'onNode-click'(node) { - expect(node.id).toBe(6) - } - } - }) - - const node6 = tree.find('#TreeNode6') - - expect(node6.exists()).toBeTruthy() - - node6.trigger('click') - }) - }) - - describe('v-model', () => { - it('checked', async () => { - const checked = ref([2]) - - const tree = mount(Tree, { - props: { - data: createData(), - defaultNodeKey: { - childNodes: 'children' - }, - defaultExpandAll: true, - showCheckbox: true, - checked: checked.value, - 'onUpdate:checked': (e) => (checked.value = e) - } - }) - await nextTick() - const node4 = tree.find('#TreeNode4 input') - await node4.trigger('click') - await nextTick() - expect(checked.value).toStrictEqual(['1', '2', '4', '9', '10']) - }) - }) -}) diff --git a/packages/element3/packages/tree/entity/Action.js b/packages/element3/packages/tree/entity/Action.js deleted file mode 100644 index ab736f744..000000000 --- a/packages/element3/packages/tree/entity/Action.js +++ /dev/null @@ -1,25 +0,0 @@ -import { TreeNode } from './TreeNode' - -export function createAction(tree) { - // this.root changed update this.raw - const appendChild = function (node) { - if (TreeNode.isType(node)) { - tree.appendRawNode(this, node.data.raw) // 这里的this表示当前的TreeNode - return [node] - } - tree.appendRawNode(this, node) - return [tree.rawNodeToTreeNode(node)] - } - const removeChild = function (index) { - tree.removeChildRawNode(this, index) // 这里的this表示当前的TreeNode - } - const insertChild = function (index, node) { - if (TreeNode.isType(node)) { - tree.insertRawNode(this, index, node.data.raw) // 这里的this表示当前的TreeNode - return [index, node] - } - tree.insertRawNode(this, index, node) - return [index, tree.rawNodeToTreeNode(node)] - } - return { appendChild, removeChild, insertChild } -} diff --git a/packages/element3/packages/tree/entity/Tree.js b/packages/element3/packages/tree/entity/Tree.js deleted file mode 100644 index 81feb57bc..000000000 --- a/packages/element3/packages/tree/entity/Tree.js +++ /dev/null @@ -1,203 +0,0 @@ -import { TreeNode } from './TreeNode' -import { nodeMap, nodeEach, transitionObjectKey } from '../libs/util' -import { createAction } from './Action' - -export class Tree { - /** - * - * @param {object[]} list - * @param {object} defaultNodeKey The incoming Node proprtry name maps to the default name - */ - constructor(list, defaultNodeKey = {}, defaultNodeValue = {}) { - this.isUpdateRaw = true - this.raw = list - this.injectAction = createAction(this) // The core method is injected with interceptor functions, the insert RowNode is automatically converted to TreeNode - this.root = new TreeNode( - Date.now(), - 'root', - [], - defaultNodeValue, - this.injectAction - ) - this.defaultNodeKey = Object.assign( - { - id: 'id', - label: 'label', - childNodes: 'childNodes', - isDisabled: 'isDisabled', - isAsync: 'isAsync', - isChecked: 'isChecked', - isVisable: 'isVisable', - isExpanded: 'isExpanded' - }, - defaultNodeKey - ) - this.defaultNodeValue = Object.assign({}, defaultNodeValue) - // this.checked = [] - // this.expanded = [] - this.initRoot() - } - - get isEmpty() { - for (let i = 0; i < this.root.childNodes.length; i++) { - const node = this.root.childNodes[i] - if (node.isVisable) { - return false - } - } - return true - } - - get checked() { - const t = {} - this.root.depthEach((node) => { - node.isChecked && !node.isIndeterminate && (t[node.id] = true) - }) - return Object.keys(t) - } - - set checked(v) { - this.setCheckedByIdList(v) - } - - get expanded() { - const t = {} - this.root.depthEach((node) => { - node.isExpanded && (t[node.id] = true) - }) - return Object.keys(t) - } - - set expanded(v) { - this.setExpandedByIdList(v, true) - } - - initRoot() { - // rekeyname and create TreeNode - this.isUpdateRaw = false - this.root.childNodes = [] - this.root.append(...this.raw) - this.isUpdateRaw = true - } - - rawNodeToTreeNode(rawNode) { - const { childNodes } = this.defaultNodeKey - return nodeMap( - rawNode, - (_node, node) => { - const handledNode = transitionObjectKey(_node, this.defaultNodeKey) - // debugger - const treeNode = TreeNode.create( - Object.assign( - {}, - this.defaultNodeValue, - { data: { raw: node }, interceptHandler: this.injectAction }, - handledNode, - { childNodes: [] } - ) - ) - return treeNode - }, - { childKey: childNodes, mapChildKey: 'childNodes' } - ) - } - - update() { - // rebuild tree - // TODO: 这里可以做diff优化 - this.initRoot() - } - - removeChildRawNode(target, index) { - const { childNodes } = this.defaultNodeKey - if (!this.isUpdateRaw) { - return - } - let rawChild = target.data.raw ? target.data.raw[childNodes] : this.raw - if (!rawChild) { - target.data.raw[childNodes] = [] - rawChild = target.data.raw[childNodes] - } - rawChild.splice(index, 1) - } - - appendRawNode(target, rawNode) { - const { childNodes } = this.defaultNodeKey - if (!this.isUpdateRaw) { - return - } - let rawChild = target.data.raw ? target.data.raw[childNodes] : this.raw - if (!rawChild) { - target.data.raw[childNodes] = [] - rawChild = target.data.raw[childNodes] - } - rawChild.push(rawNode) - } - - insertRawNode(target, index, rawNode) { - const { childNodes } = this.defaultNodeKey - if (!this.isUpdateRaw) { - return - } - let rawChild = target.data.raw ? target.data.raw[childNodes] : this.raw - if (!rawChild) { - target.data.raw[childNodes] = [] - rawChild = target.data.raw[childNodes] - } - rawChild.splice(index, 0, rawNode) - } - - getParentRawNode(rawNode) { - let parentNode = null - const { childNodes } = this.defaultNodeKey - nodeEach( - { [childNodes]: this.raw }, - (current, parent) => { - if (current === rawNode || current.id === rawNode.id) { - parentNode = parent - return true - } - }, - { childKey: childNodes } - ) - return parentNode - } - - showAll() { - this.root.setSubTreeVisable(true) - } - - checkedAll() { - this.root.setChecked(true) - } - - expandAll() { - this.root.depthEach((node) => { - if (node.isLeaf) { - node.expand(true) - } - }) - } - - setCheckedByIdList(idList = []) { - console.log(idList.toString()) - this.root.depthEach((node) => { - if (node === this.root) { - return - } - if (idList.indexOf(node.id) !== -1) { - node.isChecked = true - } else { - // node.isChecked = false - } - }) - } - - setExpandedByIdList(idList = [], value = true) { - this.root.depthEach((node) => { - if (idList.indexOf(node.id) !== -1) { - node.expand(value) - } - }) - } -} diff --git a/packages/element3/packages/tree/entity/TreeNode.js b/packages/element3/packages/tree/entity/TreeNode.js deleted file mode 100644 index 5d90a3be1..000000000 --- a/packages/element3/packages/tree/entity/TreeNode.js +++ /dev/null @@ -1,499 +0,0 @@ -/* eslint-disable @typescript-eslint/no-this-alias */ -const typeFlag = Symbol('TREE_NODE') - -/** - * - * @param {*} id - * @param {string} label - * @param {TreeNode[]} childNodes - * @param {object} param3 - */ -export class TreeNode { - constructor( - id, - label, - childNodes = [], - { - /* 默认值 */ - parent = null, - isAsync = false, - isVisable = true, - isChecked = false, - isIndeterminate = false, - isExpanded = false, - isDisabled = false, - isDraggable = false, - isLeaf = false, - data = {}, - asyncLoadFn = () => null - } = {}, - { - /* 拦截函数 */ - insertChild = null, - appendChild = null, - removeChild = null - } = {} - ) { - this.id = id || label - this.label = label - this.parent = parent - this.childNodes = childNodes - this.isVisable = isVisable - this.isChecked = isChecked - this.isIndeterminate = isIndeterminate - this.isExpanded = isExpanded - this.isDisabled = isDisabled - this.isDraggable = isDraggable - this.isRendered = false - this.data = data // Additional data carried by the node - this.isLeaf = isLeaf - - this.isAsync = isAsync // Load child only at expand time - this.asyncState = 'notload' // notload || loaded || loading - this.asyncLoadFn = asyncLoadFn // (currentNode, resolveFn) async load child node - - this.interceptHandler = { - insertChild, - appendChild, - removeChild - } - - // this.cache = { - // level: null - // } - // not use, You can cache read-only properties here - - this.updateChildParent() - this.updateChildChecked() - this.updateCheckedState() - this.updateExpandedState() - } - - get root() { - // readonly - let root = this - this.upwardEach((node) => { - root = node - }) - return root - } - - get isLeaf() { - return this.isAsync - ? this.asyncState === 'loaded' && this.childNodes.length === 0 - : this.childNodes.length === 0 - } - - set isLeaf(v) { - if (v) this.asyncState = 'loaded' - } - - get isRoot() { - // readonly - return this.root === this - } - - get level() { - // readonly - if (!this.parent) return 0 - return this.parent.level + 1 - } - - get type() { - // readonly - return typeFlag - } - - get index() { - // readonly - const parent = this.parent - if (!parent) return -1 - return parent.findChildIndex(this) - } - - get checkedNodes() { - return this.childNodes.filter( - (treeNode) => treeNode.isChecked && !treeNode.isIndeterminate - ) - } - - loadAsync() { - if (!this.isAsync || this.asyncState !== 'notload') { - return - } - const resolveFn = (childNodes = []) => { - // this.childNodes = [] - this.append(...childNodes) - this.asyncState = 'loaded' - } - this.asyncState = 'loading' - this.asyncLoadFn(this, resolveFn) - } - - updateChildParent() { - this.childNodes.forEach((node) => { - node.parent = this - }) - } - - updateChildChecked() { - if (this.isChecked) { - this.setChildChecked(true) - } - } - - updateCheckedState() { - if (this.isLeaf) { - return - } - // not leaf node exec - const checkedNodeLen = this.getCheckedNode().length - const childrenNodeLen = this.childNodes.length - if (childrenNodeLen === 0) { - return - } - if (checkedNodeLen === childrenNodeLen) { - // full select - this.setCheckedState(1) - } else if (checkedNodeLen === 0) { - // not full select - this.setCheckedState(0) - } else { - // Half select - this.setCheckedState(2) - } - } - - updateExpandedState() { - const childNodes = this.childNodes - for (let i = 0; i < childNodes.length; i++) { - const node = childNodes[i] - if (node.isExpanded) { - this.isExpanded = true - return - } - } - } - - /** - * - * @param {TreeNode} node - */ - appendChild(node) { - if (this.interceptHandler.appendChild) { - const [_node] = this.interceptHandler.appendChild.apply(this, arguments) - if (typeof _node !== 'undefined') node = _node - } - if (!TreeNode.isType(node)) { - return false - } - node.parent = this - if (this.isChecked) { - node.setChecked(true) - } - this.childNodes.push(node) - return true - } - - append(...nodes) { - nodes.forEach((node) => { - this.appendChild(node) - }) - return true - } - - /** - * - * @param {number} index - * @param {TreeNode} node - */ - insertChild(index, node) { - if (this.interceptHandler.appendChild) { - const [_index, _node] = this.interceptHandler.insertChild.apply( - this, - arguments - ) - if (typeof _index !== 'undefined') index = _index - if (typeof _node !== 'undefined') node = _node - } - if (!TreeNode.isType(node)) { - return false - } - if (this.isChecked) { - node.setChecked(true) - } - node.parent = this - this.childNodes.splice(index, 0, node) - return true - } - - insert() { - this.insertChild.apply(this, arguments) - } - - /** - * - * @param {number} index - */ - removeChild(index) { - if (this.interceptHandler.appendChild) { - this.interceptHandler.removeChild.apply(this, arguments) - } - if (index < 0 || index >= this.childNodes.length) { - return false - } - this.childNodes.splice(index, 1) - return true - } - - remove() { - if (!this.parent) { - return false - } - return this.parent.removeChild(this.index) - } - - /** - * - * @param {boolean} value - */ - setChecked(value, strictly = false) { - this.isIndeterminate = false - let _value = !this.isChecked - if (typeof value !== 'undefined') { - _value = value - } - - this.isChecked = _value - if (!strictly) { - this.upwardEach((node) => { - if (!node.isDisabled) this.updateCheckedState.call(node) - }) - this.depthEach((node) => { - if (!node.isDisabled && node.isVisable) node.isChecked = _value - }) - } - - return this.isChecked - } - - setChildChecked(value) { - this.childNodes.forEach((node) => (node.isChecked = value)) - } - - /** - * Traverse upward - * @param {Function} callback( node:TreeNode ) if returns true then stop each, else not stop - */ - upwardEach(callback, { isSkipSelf = true } = {}) { - let current = isSkipSelf ? this.parent : this - while (current) { - if (callback(current)) { - return - } - current = current.parent - } - } - - /** - * from current node start, down each - * @param {Function} callback( node:TreeNode, parentNode:TreeNode, deep: number) if returns true then stop each, else not stop - */ - depthEach(upToDownCallBack = () => false, downToUpCallBack = () => false) { - const dfs = (node, deep) => { - if (!TreeNode.isType(node)) { - return - } - for (let i = 0; i < node.childNodes.length; i++) { - const _node = node.childNodes[i] - if (upToDownCallBack(_node, node, deep)) return - dfs(_node, deep + 1) - if (downToUpCallBack(_node, node, deep)) return - } - } - upToDownCallBack(this, this.parent, 0) - dfs(this, 1) - downToUpCallBack(this, this.parent, 0) - } - - getCheckedNode() { - return this.childNodes.filter((item) => item.isChecked) - } - - /** - * - * @param {number} state 0:not checked, 1:checked, 2:indeterminate - */ - setCheckedState(state) { - const eunmState = [ - { isChecked: false, isIndeterminate: false }, - { isChecked: true, isIndeterminate: false }, - { isChecked: true, isIndeterminate: true } - ] - if (!eunmState[state]) { - return false - } - - this.isChecked = eunmState[state].isChecked - this.isIndeterminate = eunmState[state].isIndeterminate - return true - } - - /** - * Look for node in the subtree - * @param {ID|TreeNode} target - */ - findOne(target) { - let res = null - this.depthEach((node) => { - if (node.id == target || node === target) { - res = node - return true - } - }) - return res - } - - /** - * find nodes - * @param {Function | string} target - */ - findMany(target) { - const res = [] - this.depthEach((node, parent, deep) => { - if ( - (typeof target === 'function' && target(node, parent, deep)) || - (typeof target === 'string' && node.label.search(target) !== -1) - ) { - res.push(node) - } - }) - return res - } - - findChildIndex(target) { - for (let i = 0; i < this.childNodes.length; i++) { - const node = this.childNodes[i] - if (node.id == target || node === target) { - return i - } - } - return -1 - } - - expand(value, ...extraNodes) { - let _value = this.isExpanded - _value = typeof value === 'undefined' ? !_value : value - this.isExpanded = _value - this.upwardEach((node) => { - node.expand(true) - }) - - this.loadAsync() - this.append(...extraNodes) - this.isRendered = true - } - - filter(callback = () => true) { - const arr = [] - this.setSubTreeVisable(false) - this.depthEach((node, parentNode, deep) => { - const isShow = callback(node, parentNode, deep) - if (isShow) { - node.setVsiable(true) - arr.push(node) - } - }) - return arr - } - - setSubTreeVisable(value) { - this.depthEach((node) => { - node.isVisable = value - }) - } - - setVsiable(value) { - this.isVisable = value - this.upwardEach((node) => { - node.isVisable = true - }) - } - - separation() { - const parent = this.parent - if (!parent) return null - parent.removeChild(this.index) - this.parent = null - return this - } - - /** - * is allow move to target - * @param {TreeNode} target - * @param {string} relative top, bottom, inner - */ - isAllowMove(target) { - if (target === this) { - return false - } - - if (this.findOne(target)) { - return false - } - - return true - } - - /** - * currentNode move to targetNode relative location - * @param {TreeNode} target - * @param {string} relative top, bottom, inner - */ - move(target, relative) { - if (!this.isAllowMove(target, relative)) { - return false - } - - this.separation() - - switch (relative) { - case 'top': // top - target.parent.insertChild(target.index, this) - return true - case 'inner': // inner - target.expand(true, this) - return true - case 'bottom': // bottom - target.parent.insertChild(target.index + 1, this) - return true - } - } - - collapse() { - const parent = this.parent - if (!parent) { - return - } - - parent.childNodes.forEach((node) => { - if (node === this) { - node.expand() - } else { - node.expand(false) - } - }) - } - - static isType(node) { - if (typeof node !== 'object') { - return false - } - - return node.type === typeFlag - } - - static create({ id, label, childNodes, interceptHandler, ...otherParams }) { - return new TreeNode(id, label, childNodes, otherParams, interceptHandler) - } -} diff --git a/packages/element3/packages/tree/libs/util.js b/packages/element3/packages/tree/libs/util.js deleted file mode 100644 index 6ea5b0a9d..000000000 --- a/packages/element3/packages/tree/libs/util.js +++ /dev/null @@ -1,119 +0,0 @@ -import { isArray, isObject } from '../../../src/utils/types' - -/** - * Deep traversal of the object - * @param {object} target - * @param {function} callback(cloneRawNode, rawNode, isLeaf) - */ -export function nodeMap( - target, - callback = () => null, - { childKey = 'children', mapChildKey = 'children' } = { - /* TreeNode */ - } -) { - const dfs = (node) => { - if (isObject(node) && !isArray(node[childKey])) { - const _cloneNode = { ...node } - return callback(_cloneNode, node, true) - } - const cloneNode = { ...node } - const newNode = callback(cloneNode, node, false) - if (typeof newNode[childKey] !== 'undefined') delete newNode[childKey] - newNode[mapChildKey] = [] - for (let i = 0; i < node[childKey].length; i++) { - const _node = node[childKey][i] - const ret = dfs(_node) - ret.parent = newNode - newNode[mapChildKey].push(ret) - } - - return newNode - } - - return dfs(target) -} - -export function nodeEach( - target, - callback = () => false, - { childKey = 'children', root = null } = {} -) { - const dfs = (node) => { - if (!isObject(node) || isArray(node)) { - return - } - - const child = node[childKey] || [] - - for (let i = 0; i < child.length; i++) { - const _node = child[i] - if (callback(_node /* Current */, node /* Parent */)) return - dfs(_node) - } - } - if (callback(target /* Current */, root /* Parent */)) return - return dfs(target) -} - -/** - * Modify the property name of the object - * @param {object} obj - * @param {object} keyMap newKey mapping oldKey - */ -export function transitionObjectKey(obj, keyMap = {}) { - const transitionKeyList = Object.keys(keyMap) - transitionKeyList.forEach((key) => { - if (key !== keyMap[key]) { - obj[key] = obj[keyMap[key]] - delete obj[keyMap[key]] - } - }) - return obj -} - -export function isBrotherNode(nodeA, nodeB) { - return nodeA.parent === nodeB.parent -} - -export function isGreatGrandfather(nodeA, nodeB) { - let flag = false - nodeB.upwardEach((node) => { - if (node === nodeA) { - flag = true - return true - } - }) - - return flag -} - -/** - * - * @param {*} _class 构造函数 - * @param {*} funName 方法名 - * @param {*} beforeCall 在之前执行 - * @param {*} afterCall 在之后执行 - */ -export function methodInterceptor( - _class, - funName, - beforeCall = () => null, - afterCall = () => null -) { - const fun = _class.prototype[funName] - _class.prototype[funName] = function (...args) { - const _args = beforeCall.apply(this, args) || args - const _return = fun.apply(this, _args) - const ret = afterCall.apply(this, [_return, _args]) || _return - return ret - } -} - -export const extractMethods = (obj, methods) => { - const methodList = {} - methods.forEach((method) => { - methodList[method] = obj[method].bind(obj) - }) - return methodList -} diff --git a/packages/element3/packages/tree/index.js b/packages/element3/src/components/Tree/index.js similarity index 65% rename from packages/element3/packages/tree/index.js rename to packages/element3/src/components/Tree/index.js index 9682eb50b..89f226706 100644 --- a/packages/element3/packages/tree/index.js +++ b/packages/element3/src/components/Tree/index.js @@ -1,8 +1,8 @@ -import ElTree from './Tree.vue' +import ElTree from './src/Tree.vue' /* istanbul ignore next */ ElTree.install = function (app) { app.component(ElTree.name, ElTree) } -export default ElTree +export { ElTree } diff --git a/packages/element3/src/components/Tree/src/Tree.vue b/packages/element3/src/components/Tree/src/Tree.vue new file mode 100644 index 000000000..9297f90ad --- /dev/null +++ b/packages/element3/src/components/Tree/src/Tree.vue @@ -0,0 +1,91 @@ + + + diff --git a/packages/element3/src/components/Tree/src/TreeNode.vue b/packages/element3/src/components/Tree/src/TreeNode.vue new file mode 100644 index 000000000..3d61b5081 --- /dev/null +++ b/packages/element3/src/components/Tree/src/TreeNode.vue @@ -0,0 +1,63 @@ + + + diff --git a/packages/element3/src/components/Tree/src/TreeNodeContent.vue b/packages/element3/src/components/Tree/src/TreeNodeContent.vue new file mode 100644 index 000000000..d2b0a128a --- /dev/null +++ b/packages/element3/src/components/Tree/src/TreeNodeContent.vue @@ -0,0 +1,28 @@ + diff --git a/packages/element3/src/components/Tree/src/entity/TreeNode.ts b/packages/element3/src/components/Tree/src/entity/TreeNode.ts new file mode 100644 index 000000000..a919e732b --- /dev/null +++ b/packages/element3/src/components/Tree/src/entity/TreeNode.ts @@ -0,0 +1,187 @@ +import { + DefaultPropKey, + TreeNodeKeys, + TreeNodeInstance, + TreeNodeProps, + ID +} from '../types' +import { watch } from '../utils/effect' + +export class TreeNode implements TreeNodeProps { + private dk: DefaultPropKey = { + id: 'id', + label: 'label', + children: 'children', + parent: 'parent', + isVisible: 'isVisible', + isDisabled: 'isDisabled', + isExpanded: 'isExpanded', + isChecked: 'isChecked' + } + private instance: TreeNodeInstance = { + id: 0, + label: '', + children: [], + parent: null, + isVisible: true, + isDisabled: false, + isExpanded: true, + isChecked: false + } + + raw: RawNode + + constructor( + rawNode: RawNode, + parent: TreeNode, + dk: DefaultPropKey, + dv: TreeNodeInstance + ) { + Object.assign(this.dk, dk) + Object.assign(this.instance, dv) + + this.raw = rawNode + this.instance.parent = parent + + this.effect(rawNode, dk, dv) + } + + // readonly + get level(): number { + return (this.parent?.level ?? -1) + 1 + } + + // readonly + get isLeaf(): boolean { + return this.get('children').length === 0 + } + + get id(): ID { + return this.get('id') + } + + set id(v: ID) { + this.set('id', v) + } + + get label(): string { + return this.get('label') + } + + set label(v: string) { + this.set('label', v) + } + + // readonly + get parent(): TreeNode { + return this.instance.parent + } + + // readonly + get children(): TreeNode[] { + return this.instance.children + } + + get isVisible(): boolean { + return this.get('isVisible') + } + + set isVisible(v: boolean) { + this.set('isVisible', v) + } + + get isChecked(): boolean { + return this.get('isChecked') + } + + set isChecked(v: boolean) { + this.set('isChecked', v) + } + + get isExpanded(): boolean { + return this.get('isExpanded') + } + + set isExpanded(v: boolean) { + this.set('isExpanded', v) + } + + get isDisabled(): boolean { + return this.get('isDisabled') + } + + set isDisabled(v: boolean) { + this.set('isDisabled', v) + } + + get(tk: TreeNodeKeys): any { + if (!Reflect.has(this.dk, tk)) { + return this.raw[tk] + } + return this.raw[this.dk[tk as string]] ?? this.instance[tk] + } + + set(tk: TreeNodeKeys, val: unknown): void { + if (!Reflect.has(this.dk, tk)) { + this.raw[tk] = val + return + } + if (Reflect.has(this.raw as any, this.dk[tk])) { + this.raw[this.dk[tk as string]] = val + return + } + this.instance[tk as string] = val + } + + /** + * reactive raw to this + */ + effect( + raw: RawNode, + dk: DefaultPropKey, + dv: TreeNodeInstance + ): void { + // when raw change + this.raw = watch(raw, (e) => { + if (e.type === 'set') { + if (e.key === this.dk.children && e.value instanceof Array) { + this.instance.children = e.value.map( + (node) => new TreeNode(node, this, dk, dv) + ) + } else { + this.instance[this.findDkByRk(e.key as keyof RawNode)] = e.value + } + } + }) + + // when raw children change + if (raw[this.dk.children as string]) + this.raw[this.dk.children as string] = watch( + raw[this.dk.children as string], + (e) => { + if (e.type === 'set') { + this.instance.children[e.key] = + typeof e.value === 'object' + ? new TreeNode(e.value, this, dk, dv) + : e.value + } + } + ) + } + + expand(is = !this.isExpanded): void { + this.isExpanded = is + } + + findDkByRk(rk: keyof RawNode): string | null { + for (const key in this.dk) { + if (Object.prototype.hasOwnProperty.call(this.dk, key)) { + const v = this.dk[key] + if (v === rk) { + return key + } + } + } + return null + } +} diff --git a/packages/element3/src/components/Tree/src/props.ts b/packages/element3/src/components/Tree/src/props.ts new file mode 100644 index 000000000..00930ca53 --- /dev/null +++ b/packages/element3/src/components/Tree/src/props.ts @@ -0,0 +1,26 @@ +import { DefaultPropKey } from './types' + +export const treeProps = { + defaultNodeKey: { + type: Object, + default: (): DefaultPropKey => ({ + id: 'id', + label: 'label', + parent: 'parent', + children: 'children', + isDisabled: 'isDisabled', + isVisible: 'isVisible', + isExpanded: 'isExpanded' + }) + }, + props: { + type: Object + }, + data: { type: Array }, + modelValue: { type: Array }, + indent: { type: Number, default: 14 }, + nodeKey: { type: String }, + emptyText: { type: String, default: '内容为空' }, + + iconClass: { type: String, default: 'el-icon-caret-right' } +} diff --git a/packages/element3/src/components/Tree/src/types.ts b/packages/element3/src/components/Tree/src/types.ts new file mode 100644 index 000000000..35dc9211d --- /dev/null +++ b/packages/element3/src/components/Tree/src/types.ts @@ -0,0 +1,38 @@ +import { TreeNode } from './entity/TreeNode' +import ElTree from './Tree.vue' +import ElTreeNode from './TreeNode.vue' + +export type ID = number | string + +export interface TreeNodeInstance { + id: ID + label: string + + children: TreeNode[] + parent: TreeNode + isVisible: boolean + isDisabled: boolean + isExpanded: boolean + isChecked: boolean +} +export interface TreeNodeProps extends TreeNodeInstance { + readonly level: number + readonly children: TreeNode[] + readonly parent: TreeNode +} + +export type TreeNodeKeys = keyof TreeNodeInstance + +export type RawNodeKeys = keyof RawNode + +export type DefaultPropKey = { + [treeNodeKey in TreeNodeKeys]?: keyof RawNode | TreeNodeKeys +} + +export type DefaultPropValue = { + [treeNodeKey in TreeNodeKeys]?: TreeNodeInstance[treeNodeKey] +} + +export type ElTreeCtx = typeof ElTree + +export type ElTreeNodeCtx = typeof ElTreeNode diff --git a/packages/element3/src/components/Tree/src/utils/effect.ts b/packages/element3/src/components/Tree/src/utils/effect.ts new file mode 100644 index 000000000..67c3eb821 --- /dev/null +++ b/packages/element3/src/components/Tree/src/utils/effect.ts @@ -0,0 +1,40 @@ +export type WatchCbEv = { + type: 'get' | 'set' | 'del' + target: any + key: string | number | symbol + value?: any + receiver?: any +} + +export function watch(obj: T, callback: (e: WatchCbEv) => void): T { + const proxy = new Proxy(obj as any, { + get(target, key, receiver) { + callback({ + type: 'get', + target, + key, + receiver + }) + return Reflect.get(target, key, receiver) + }, + set(target, key, value, receiver) { + callback({ + type: 'set', + target, + key, + value, + receiver + }) + return Reflect.set(target, key, value, receiver) + }, + deleteProperty(target, key) { + callback({ + type: 'del', + target, + key + }) + return Reflect.deleteProperty(target, key) + } + }) + return proxy as T +} diff --git a/packages/element3/src/components/Tree/src/utils/useTools.ts b/packages/element3/src/components/Tree/src/utils/useTools.ts new file mode 100644 index 000000000..733b7a3d6 --- /dev/null +++ b/packages/element3/src/components/Tree/src/utils/useTools.ts @@ -0,0 +1,14 @@ +import { onBeforeUpdate } from 'vue' + +export function useForRefs( + arr: any[], + map = (v: any) => v +): { (component: any): void } { + onBeforeUpdate(() => { + arr.length = 0 + }) + + return (component: any) => { + if (component) arr.push(map(component)) + } +} diff --git a/packages/element3/src/components/Tree/tests/Tree.spec.ts b/packages/element3/src/components/Tree/tests/Tree.spec.ts new file mode 100644 index 000000000..9eeebd353 --- /dev/null +++ b/packages/element3/src/components/Tree/tests/Tree.spec.ts @@ -0,0 +1,128 @@ +import { mount } from '@vue/test-utils' +import { nextTick, ref } from 'vue' +import Tree from '../src/Tree.vue' + +describe('Tree.vue', () => { + it('render tree data by `props.modelValue`.', async () => { + const data = [ + { + key: '1', + text: 'node1', + childs: [ + { + key: '3', + text: 'node3', + childs: [ + { + key: '5', + text: 'node5' + } + ] + }, + { + key: '4', + text: 'node4' + } + ] + }, + { + key: '2', + text: 'node2', + childs: [] + } + ] + const wrapper = mount(Tree, { + props: { + modelValue: data, + defaultNodeKey: { + id: 'key', + label: 'text', + children: 'childs' + } + } + }) + + await nextTick() + expect(wrapper.find('#TN1').exists()).toBeTruthy() + expect(wrapper.find('#TN2').exists()).toBeTruthy() + expect(wrapper.find('#TN3').exists()).toBeTruthy() + expect(wrapper.find('#TN4').exists()).toBeTruthy() + expect(wrapper.find('#TN5').exists()).toBeTruthy() + }) + + it('when empty tree data.', async () => { + const data = [] + const wrapper = mount(Tree, { + props: { + modelValue: data, + defaultNodeKey: { + id: 'key', + label: 'text', + children: 'childs' + } + } + }) + + await nextTick() + expect(wrapper.text()).toEqual('内容为空') + }) + + it('vModel tree data by `props.modelValue`.', async () => { + const data = ref([ + { + key: '1', + text: 'node1', + childs: [ + { + key: '3', + text: 'node3', + childs: [ + { + key: '5', + text: 'node5' + } + ] + }, + { + key: '4', + text: 'node4' + } + ] + }, + { + key: '2', + text: 'node2', + childs: [] + } + ]) + const wrapper = mount(Tree, { + props: { + modelValue: data, + 'onUpdate:modelValue': (v) => { + data.value = v + }, + defaultNodeKey: { + id: 'key', + label: 'text', + children: 'childs' + } + } + }) + + data.value.splice(0, 1) + await nextTick() + expect(wrapper.find('#TN1').exists()).toBeFalsy() + expect(wrapper.find('#TN2').exists()).toBeTruthy() + expect(wrapper.find('#TN3').exists()).toBeFalsy() + expect(wrapper.find('#TN4').exists()).toBeFalsy() + expect(wrapper.find('#TN5').exists()).toBeFalsy() + + data.value.push({ + key: '6', + text: 'node6', + childs: [] + }) + await nextTick() + expect(wrapper.find('#TN6').exists()).toBeTruthy() + }) +}) diff --git a/packages/element3/src/components/Tree/tests/TreeNode.spec.ts b/packages/element3/src/components/Tree/tests/TreeNode.spec.ts new file mode 100644 index 000000000..2f7492fb5 --- /dev/null +++ b/packages/element3/src/components/Tree/tests/TreeNode.spec.ts @@ -0,0 +1,25 @@ +import { createTestData } from './entity/TreeNode.spec' +import TreeNode from '../src/TreeNode.vue' +import { mount } from '@vue/test-utils' + +describe('TreeNode.vue', () => { + it('render treeNode', () => { + const { treeNode } = createTestData() + const wrapper = mount(TreeNode, { + props: { + node: treeNode + }, + global: { + provide: { + tree: { + indent: 10 + } + } + } + }) + + expect(wrapper.find('#TN1').exists()).toBeTruthy() + expect(wrapper.find('#TN11').exists()).toBeTruthy() + expect(wrapper.find('#TN111').exists()).toBeTruthy() + }) +}) diff --git a/packages/element3/src/components/Tree/tests/TreeNodeContent.spec.ts b/packages/element3/src/components/Tree/tests/TreeNodeContent.spec.ts new file mode 100644 index 000000000..1db10bea0 --- /dev/null +++ b/packages/element3/src/components/Tree/tests/TreeNodeContent.spec.ts @@ -0,0 +1,16 @@ +import TreeNodeContent from '../src/TreeNodeContent.vue' +import { mount } from '@vue/test-utils' +import { createTestData } from './entity/TreeNode.spec' + +describe('TreeNodeContent.vue', () => { + it('show label', () => { + const { treeNode } = createTestData() + const tnc = mount(TreeNodeContent, { + props: { + node: treeNode + } + }) + + expect(tnc).toHaveTextContent('hello') + }) +}) diff --git a/packages/element3/src/components/Tree/tests/entity/TreeNode.spec.ts b/packages/element3/src/components/Tree/tests/entity/TreeNode.spec.ts new file mode 100644 index 000000000..799db4bcd --- /dev/null +++ b/packages/element3/src/components/Tree/tests/entity/TreeNode.spec.ts @@ -0,0 +1,88 @@ +import { reactive } from 'vue' +import { TreeNode } from '../../src/entity/TreeNode' +import { DefaultPropKey, TreeNodeInstance } from '../../src/types' + +export function createTestData(): any { + const rawNode = reactive({ + key: 1, + text: 'hello', + isChecked: false, + isExpanded: false, + childs: [ + { + key: 11, + text: 'child11', + childs: [ + { + key: 111, + text: 'child111', + childs: [] + } + ] + } + ] + }) + + const dk: DefaultPropKey = { + id: 'key', + label: 'text', + children: 'childs', + isChecked: 'isChecked' + } + + const dv: TreeNodeInstance = { + id: 0, + label: '', + children: [], + parent: null, + isChecked: false, + isDisabled: false, + isExpanded: false, + isVisible: true + } + + const treeNode = new TreeNode(rawNode, null, dk, dv) + + return { rawNode, treeNode, dk, dv } +} + +describe('TreeNode.ts', () => { + it('test reactive result', () => { + const { rawNode, treeNode } = createTestData() + treeNode.label = 'test' + treeNode.isChecked = true + treeNode.isExpanded = true + treeNode.raw.key = 666 + expect(rawNode.text).toEqual('test') + expect(rawNode.isChecked).toEqual(true) + expect(treeNode.isExpanded).toEqual(true) + expect(rawNode.key).toEqual(666) + + // add a node + treeNode.raw.childs.push({ key: 999, text: 'js', childs: [] }) + expect(treeNode.children).toHaveLength(2) + expect(treeNode.children[1].id).toEqual(999) + + // delete a node + treeNode.raw.childs.splice(0, 1) + expect(treeNode.children).toHaveLength(1) + expect(treeNode.children[0].id).toEqual(999) + + // cover childs + treeNode.raw.childs = [{ key: 888, text: 'ts', childs: [] }] + expect(treeNode.children).toHaveLength(1) + expect(treeNode.children[0].id).toEqual(888) + }) + + it('find treeNodeKey by rawNodeKey', () => { + const { treeNode } = createTestData() + expect(treeNode.findDkByRk('childs')).toEqual('children') + }) + + it('expand node', () => { + const { rawNode, treeNode } = createTestData() + treeNode.expand() + expect(treeNode.isExpanded).toEqual(true) + expect(rawNode.isExpanded).toEqual(true) + }) +}) diff --git a/packages/element3/src/components/Tree/tests/props.spec.ts b/packages/element3/src/components/Tree/tests/props.spec.ts new file mode 100644 index 000000000..73ed4941c --- /dev/null +++ b/packages/element3/src/components/Tree/tests/props.spec.ts @@ -0,0 +1,3 @@ +describe('props.ts', () => { + it.todo('coding...') +}) diff --git a/packages/element3/src/components/Tree/tests/utils/effect.spec.ts b/packages/element3/src/components/Tree/tests/utils/effect.spec.ts new file mode 100644 index 000000000..e1f00b2dd --- /dev/null +++ b/packages/element3/src/components/Tree/tests/utils/effect.spec.ts @@ -0,0 +1,32 @@ +import { watch } from '../../src/utils/effect' + +describe('effect.ts', () => { + it('watch a obj', () => { + const call = jest.fn() + const obj = { + id: 1, + label: 'hello' + } + const _obj = watch(obj, call) + + _obj.id = 2 + expect(call).toHaveBeenCalledWith({ + type: 'set', + key: 'id', + value: 2, + target: obj, + receiver: _obj + }) + + console.log(_obj.id) + expect(call).toHaveBeenCalledWith({ + type: 'get', + key: 'id', + target: obj, + receiver: _obj + }) + + delete _obj.id + expect(call).toHaveBeenCalledWith({ type: 'del', key: 'id', target: obj }) + }) +}) diff --git a/packages/element3/src/components/Tree/tests/utils/useTools.spec.ts b/packages/element3/src/components/Tree/tests/utils/useTools.spec.ts new file mode 100644 index 000000000..50cffce37 --- /dev/null +++ b/packages/element3/src/components/Tree/tests/utils/useTools.spec.ts @@ -0,0 +1,27 @@ +import { mount } from '@vue/test-utils' +import { reactive } from 'vue' +import { useForRefs } from '../../src/utils/useTools' + +describe('useTools.ts', () => { + it('useForRefs', () => { + /** + * Implement the Ref array in v-for + * https://v3.cn.vuejs.org/guide/migration/array-refs.html + */ + const warpper = mount({ + template: ` +
{{item}}
+ `, + setup() { + const children = reactive([]) + const itemRefs = useForRefs(children) + return { + arr: [1, 2, 5, 8], + itemRefs, + children + } + } + }) + expect(warpper.vm.children).toHaveLength(4) + }) +}) diff --git a/packages/element3/src/index.js b/packages/element3/src/index.js index 46c25fbb3..d6676378a 100644 --- a/packages/element3/src/index.js +++ b/packages/element3/src/index.js @@ -42,7 +42,7 @@ import ElTable from '../packages/table' import ElTableColumn from '../packages/table-column' import { ElTag } from './components/Tag' import { ElProgress } from './components/Progress' -import ElTree from '../packages/tree' +import { ElTree } from './components/Tree' import ElPagination from '../packages/pagination' import { ElBadge } from './components/Badge' import { ElAvatar } from './components/Avatar' diff --git a/packages/website/src/i18n/component.json b/packages/website/src/i18n/component.json index 361c60d01..ea96214d2 100644 --- a/packages/website/src/i18n/component.json +++ b/packages/website/src/i18n/component.json @@ -1,30 +1,30 @@ { - "demo-block": { - "hide-text": "隐藏代码", - "show-text": "显示代码", - "button-text": "在线运行", - "tooltip-text": "前往 codepen.io 运行此示例" - }, - "footer": { - "links": "链接", - "repo": "代码仓库", - "community": "社区", - "changelog": "更新日志", - "theme": "在线主题生成器", - "faq": "常见问题", - "gitter": "在线讨论", - "starter": "脚手架", - "feedback": "反馈建议", - "contribution": "贡献指南", - "eleme": "饿了么" - }, - "header": { - "guide": "指南", - "components": "组件", - "theme": "主题", - "resource": "资源" - }, - "nav": { - "dropdown": "版本:" - } -} \ No newline at end of file + "demo-block": { + "hide-text": "隐藏代码", + "show-text": "显示代码", + "button-text": "在线运行", + "tooltip-text": "前往 codepen.io 运行此示例" + }, + "footer": { + "links": "链接", + "repo": "代码仓库", + "community": "社区", + "changelog": "更新日志", + "theme": "在线主题生成器", + "faq": "常见问题", + "gitter": "在线讨论", + "starter": "脚手架", + "feedback": "反馈建议", + "contribution": "贡献指南", + "eleme": "饿了么" + }, + "header": { + "guide": "指南", + "components": "组件", + "theme": "主题", + "resource": "资源" + }, + "nav": { + "dropdown": "版本:" + } +} diff --git a/packages/website/src/play/index.vue b/packages/website/src/play/index.vue index e369cabf7..813c2d0a0 100644 --- a/packages/website/src/play/index.vue +++ b/packages/website/src/play/index.vue @@ -1,25 +1,59 @@ diff --git a/packages/website/src/route/nav.config.json b/packages/website/src/route/nav.config.json index ed6469a4e..b021ab276 100644 --- a/packages/website/src/route/nav.config.json +++ b/packages/website/src/route/nav.config.json @@ -263,4 +263,4 @@ } ] } -] \ No newline at end of file +]