diff --git a/dev/App.vue b/dev/App.vue index e163fd7..9fd5020 100644 --- a/dev/App.vue +++ b/dev/App.vue @@ -1,159 +1,174 @@ diff --git a/readme.md b/readme.md index 382c761..002bc9c 100644 --- a/readme.md +++ b/readme.md @@ -199,13 +199,26 @@ drop-before | {node, src, target} | Trigger after dropping a node before another drop-after | {node, src, target} | Trigger after dropping a node after another. node: the draggable node, src: the draggable node's parent, target: the node that draggable node will drop after # customize operation icons -The component has default icons for `addTreeNodeIcon`, `addLeafNodeIcon`, `editNodeIcon`, `delNodeIcon`, `leafNodeIcon`, `treeNodeIcon` button, but you can also customize them: +The component has default icons for `addTreeNodeIcon`, `addLeafNodeIcon`, `editNodeIcon`, `delNodeIcon`, `leafNodeIcon`, `treeNodeIcon` button, but you can also customize them and can access `model`, `root`, `expanded` as below: ```html - 📂 - - 📃 - ✂️ - 🍃 - 🌲 + + + + + + ``` diff --git a/src/Tree.js b/src/Tree.js index facc879..902484b 100644 --- a/src/Tree.js +++ b/src/Tree.js @@ -1,3 +1,4 @@ +import {traverseTree} from './tools' /** * Tree data struct * Created by ayou on 2017/7/20. @@ -8,155 +9,160 @@ * dragDisabled: decide if it can be dragged * disabled: desabled all operation */ -const TreeNode = function (data) { - const { id, isLeaf } = data - this.id = (typeof id === 'undefined') ? new Date().valueOf() : id - this.parent = null - this.children = null - this.isLeaf = !!isLeaf +export class TreeNode { + constructor(data) { + const {id, isLeaf} = data + this.id = typeof id === 'undefined' ? new Date().valueOf() : id + this.parent = null + this.children = null + this.isLeaf = !!isLeaf - // other params - for (var k in data) { - if (k !== 'id' && k !== 'children' && k !== 'isLeaf') { - this[k] = data[k] + // other params + for (var k in data) { + if (k !== 'id' && k !== 'children' && k !== 'isLeaf') { + this[k] = data[k] + } } } -} -TreeNode.prototype.changeName = function (name) { - this.name = name -} - -TreeNode.prototype.addChildren = function (children) { - if (!this.children) { - this.children = [] + changeName(name) { + this.name = name } - if (Array.isArray(children)) { - for (let i = 0, len = children.length; i < len; i++) { - const child = children[i] + addChildren(children) { + if (!this.children) { + this.children = [] + } + + if (Array.isArray(children)) { + for (let i = 0, len = children.length; i < len; i++) { + const child = children[i] + child.parent = this + child.pid = this.id + } + this.children.concat(children) + } else { + const child = children child.parent = this child.pid = this.id - } - this.children.concat(children) - } else { - const child = children - child.parent = this - child.pid = this.id - this.children.push(child) - } -} - -// remove self -TreeNode.prototype.remove = function () { - const parent = this.parent - const index = parent.findChildIndex(this) - parent.children.splice(index, 1) -} - -// remove child -TreeNode.prototype._removeChild = function (child) { - for (var i = 0, len = this.children.length; i < len; i++) { - if (this.children[i] === child) { - this.children.splice(i, 1) - break + this.children.push(child) } } -} -TreeNode.prototype.isTargetChild = function (target) { - let parent = target.parent - while (parent) { - if (parent === this) { - return true - } - parent = parent.parent - } - return false -} - -TreeNode.prototype.moveInto = function (target) { - if (this.name === 'root' || this === target) { - return + // remove self + remove() { + const parent = this.parent + const index = parent.findChildIndex(this) + parent.children.splice(index, 1) } - // cannot move ancestor to child - if (this.isTargetChild(target)) { - return - } - - // cannot move to leaf node - if (target.isLeaf) { - return - } - - this.parent._removeChild(this) - this.parent = target - this.pid = target.id - if (!target.children) { - target.children = [] - } - target.children.unshift(this) -} - -TreeNode.prototype.findChildIndex = function (child) { - var index - for (let i = 0, len = this.children.length; i < len; i++) { - if (this.children[i] === child) { - index = i - break + // remove child + _removeChild(child) { + for (var i = 0, len = this.children.length; i < len; i++) { + if (this.children[i] === child) { + this.children.splice(i, 1) + break + } } } - return index -} -TreeNode.prototype._beforeInsert = function (target) { - if (this.name === 'root' || this === target) { + isTargetChild(target) { + let parent = target.parent + while (parent) { + if (parent === this) { + return true + } + parent = parent.parent + } return false } - // cannot insert ancestor to child - if (this.isTargetChild(target)) { - return false - } - - this.parent._removeChild(this) - this.parent = target.parent - this.pid = target.parent.id - return true -} - -TreeNode.prototype.insertBefore = function (target) { - if (!this._beforeInsert(target)) return - - const pos = target.parent.findChildIndex(target) - target.parent.children.splice(pos, 0, this) -} - -TreeNode.prototype.insertAfter = function (target) { - if (!this._beforeInsert(target)) return - - const pos = target.parent.findChildIndex(target) - target.parent.children.splice(pos + 1, 0, this) -} - -function Tree (data) { - this.root = new TreeNode({ name: 'root', isLeaf: false, id: 0 }) - this.initNode(this.root, data) - return this.root -} - -Tree.prototype.initNode = function (node, data) { - for (let i = 0, len = data.length; i < len; i++) { - var _data = data[i] - - var child = new TreeNode(_data) - if (_data.children && _data.children.length > 0) { - this.initNode(child, _data.children) + moveInto(target) { + if (this.name === 'root' || this === target) { + return } - node.addChildren(child) + + // cannot move ancestor to child + if (this.isTargetChild(target)) { + return + } + + // cannot move to leaf node + if (target.isLeaf) { + return + } + + this.parent._removeChild(this) + this.parent = target + this.pid = target.id + if (!target.children) { + target.children = [] + } + target.children.unshift(this) + } + + findChildIndex(child) { + var index + for (let i = 0, len = this.children.length; i < len; i++) { + if (this.children[i] === child) { + index = i + break + } + } + return index + } + + _canInsert(target) { + if (this.name === 'root' || this === target) { + return false + } + + // cannot insert ancestor to child + if (this.isTargetChild(target)) { + return false + } + + this.parent._removeChild(this) + this.parent = target.parent + this.pid = target.parent.id + return true + } + + insertBefore(target) { + if (!this._canInsert(target)) return + + const pos = target.parent.findChildIndex(target) + target.parent.children.splice(pos, 0, this) + } + + insertAfter(target) { + if (!this._canInsert(target)) return + + const pos = target.parent.findChildIndex(target) + target.parent.children.splice(pos + 1, 0, this) + } + + toString() { + return JSON.stringify(traverseTree(this)) } } -exports.Tree = Tree -exports.TreeNode = TreeNode +export class Tree { + constructor(data) { + this.root = new TreeNode({name: 'root', isLeaf: false, id: 0}) + this.initNode(this.root, data) + return this.root + } + + initNode(node, data) { + for (let i = 0, len = data.length; i < len; i++) { + var _data = data[i] + + var child = new TreeNode(_data) + if (_data.children && _data.children.length > 0) { + this.initNode(child, _data.children) + } + node.addChildren(child) + } + } +} diff --git a/src/VueTreeList.vue b/src/VueTreeList.vue index 9cb5cca..100b34d 100644 --- a/src/VueTreeList.vue +++ b/src/VueTreeList.vue @@ -1,422 +1,510 @@ diff --git a/src/tools.js b/src/tools.js index fc0524d..024a5e4 100644 --- a/src/tools.js +++ b/src/tools.js @@ -4,7 +4,7 @@ var handlerCache -exports.addHandler = function (element, type, handler) { +export const addHandler = function (element, type, handler) { handlerCache = handler if (element.addEventListener) { element.addEventListener(type, handler, false) @@ -15,7 +15,7 @@ exports.addHandler = function (element, type, handler) { } } -exports.removeHandler = function (element, type) { +export const removeHandler = function (element, type) { if (element.removeEventListener) { element.removeEventListener(type, handlerCache, false) } else if (element.detachEvent) { @@ -25,7 +25,21 @@ exports.removeHandler = function (element, type) { } } -// exports.fireFocusEvent = function (ele) { -// var event = new FocusEvent() -// ele.dispatch(event) -// } +// depth first search +export const traverseTree = (root) => { + var newRoot = {} + + for (var k in root) { + if (k !== 'children' && k !== 'parent') { + newRoot[k] = root[k] + } + } + + if (root.children && root.children.length > 0) { + newRoot.children = [] + for (var i = 0, len = root.children.length; i < len; i++) { + newRoot.children.push(traverseTree(root.children[i])) + } + } + return newRoot +} diff --git a/tests/unit/__snapshots__/slot.spec.js.snap b/tests/unit/__snapshots__/slot.spec.js.snap new file mode 100644 index 0000000..784d2fc --- /dev/null +++ b/tests/unit/__snapshots__/slot.spec.js.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Slot render slot correctly 1`] = ` +
+ +
+
+
+
+
+
+ Node 1 +
+ +
+
+
+
+
+
+
+
+ 🍃 +
+ Node 1-1 +
+ +
+ +
+ +
+
+
+
+
+
+
+ +
+ Node 2 +
+ +
+ +
+ +
+
+
+`; diff --git a/tests/unit/slot.spec.js b/tests/unit/slot.spec.js new file mode 100644 index 0000000..6cdd585 --- /dev/null +++ b/tests/unit/slot.spec.js @@ -0,0 +1,79 @@ +import Vue from 'vue' +import {mount} from '@vue/test-utils' +import {Tree, VueTreeList} from '@/index' + +describe('Slot', () => { + let wrapper + + beforeEach(() => { + const tree = new Tree([ + { + name: 'Node 1', + id: 't1', + pid: 0, + children: [ + { + name: 'Node 1-1', + id: 't11', + isLeaf: true, + pid: 't1' + } + ] + }, + { + name: 'Node 2', + id: 't2', + pid: 0 + } + ]) + wrapper = mount(VueTreeList, { + propsData: {model: new Tree([])}, + scopedSlots: { + addTreeNodeIcon() { + return 📂 + }, + addLeafNodeIcon() { + return + }, + editNodeIcon() { + return 📃 + }, + delNodeIcon(slotProps) { + return (slotProps.model.isLeaf || !slotProps.model.children) ? ✂️ : + }, + leafNodeIcon() { + return 🍃 + }, + treeNodeIcon(slotProps) { + return { slotProps.model.children && slotProps.model.children.length > 0 && !slotProps.expanded ? '🌲' : '❀' } + } + } + }) + wrapper.setProps({model: tree}) + }) + + it('render slot correctly', () => { + expect(wrapper).toMatchSnapshot() + }) + + it('toggle tree node show different icon', done => { + const $caretDown = wrapper.find('.vtl-icon-caret-down') + expect(wrapper.find('#t1 .tree-node-icon').text()).toBe('❀') + $caretDown.trigger('click') + Vue.nextTick(() => { + expect(wrapper.exists('.vtl-icon-caret-right')).toBe(true) + expect(wrapper.find('#t1 .tree-node-icon').text()).toBe('🌲') + done() + }) + }) + + it('dont show ✂️ after add child ', done => { + const $addTreeNodeIcon = wrapper.find('#t2 .add-tree-node-icon') + expect(wrapper.find('#t2 .del-node-icon').exists()).toBe(true) + $addTreeNodeIcon.trigger('click') + Vue.nextTick(() => { + expect(wrapper.find('#t2 .del-node-icon').exists()).toBe(false) + done() + }) + }) +})