├── .commitlintrc.yml ├── .czrc ├── .eslintrc ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .huskyrc.yml ├── .npmignore ├── LICENSE ├── babel.config.js ├── dev ├── App.vue └── index.js ├── examples ├── demo1.html └── img │ ├── 08 usher plus.png │ ├── edit.png │ ├── group-add.png │ ├── group.png │ ├── personal.png │ └── trash.png ├── img └── demo.gif ├── jest.config.js ├── package-lock.json ├── package.json ├── prettier.config.js ├── public └── index.html ├── readme.md ├── src ├── Tree.js ├── VueTreeList.vue ├── fonts │ ├── icomoon.eot │ ├── icomoon.svg │ ├── icomoon.ttf │ └── icomoon.woff ├── index.js └── tools.js ├── tests └── unit │ ├── __snapshots__ │ ├── render.spec.js.snap │ └── slot.spec.js.snap │ ├── drag.spec.js │ ├── operation.spec.js │ ├── render.spec.js │ └── slot.spec.js └── vue.config.js /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - "@commitlint/config-conventional" -------------------------------------------------------------------------------- /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "cz-conventional-changelog" 3 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true 5 | }, 6 | "extends": [ 7 | "plugin:vue/essential", 8 | "plugin:prettier/recommended", 9 | "eslint:recommended" 10 | ], 11 | "rules": { 12 | "prettier/prettier": "error", 13 | "no-console": "warn" 14 | }, 15 | "parserOptions": { 16 | "parser": "babel-eslint" 17 | }, 18 | "overrides": [ 19 | { 20 | "files": [ 21 | "**/__tests__/*.{j,t}s?(x)", 22 | "**/tests/unit/**/*.spec.{j,t}s?(x)" 23 | ], 24 | "env": { 25 | "jest": true 26 | } 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: npm install, lint and test 12 | run: | 13 | npm install 14 | npm run lint 15 | npm run test:coverage 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | coverage 24 | -------------------------------------------------------------------------------- /.huskyrc.yml: -------------------------------------------------------------------------------- 1 | hooks: 2 | pre-commit: npm run lint-staged 3 | commit-msg: commitlint -E HUSKY_GIT_PARAMS -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .idea 4 | coverage 5 | examples 6 | img 7 | src 8 | test 9 | .babelrc 10 | .travis.yml 11 | karma.conf.js 12 | build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ayou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'] 3 | } 4 | -------------------------------------------------------------------------------- /dev/App.vue: -------------------------------------------------------------------------------- 1 | 54 | 167 | 180 | 181 | 193 | -------------------------------------------------------------------------------- /dev/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by ayou on 18/3/7. 3 | */ 4 | import Vue from 'vue' 5 | import App from './App.vue' 6 | 7 | new Vue({ 8 | render: h => h(App) 9 | }).$mount('#app') 10 | -------------------------------------------------------------------------------- /examples/demo1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | vue-tree 8 | 9 | 10 | 84 | 85 | 86 |
87 | 88 |

89 | 92 | vue-tree 93 |

94 |
95 |
96 | 102 | 103 |
104 | 105 |
106 |
107 | 108 |
109 |
110 |         {{newTree}}
111 |       
112 |
113 |
114 | 115 | 116 | 117 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /examples/img/08 usher plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadeTo/vue-tree-list/727ab3a69c070389e79410dc1750bcdcec1039fc/examples/img/08 usher plus.png -------------------------------------------------------------------------------- /examples/img/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadeTo/vue-tree-list/727ab3a69c070389e79410dc1750bcdcec1039fc/examples/img/edit.png -------------------------------------------------------------------------------- /examples/img/group-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadeTo/vue-tree-list/727ab3a69c070389e79410dc1750bcdcec1039fc/examples/img/group-add.png -------------------------------------------------------------------------------- /examples/img/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadeTo/vue-tree-list/727ab3a69c070389e79410dc1750bcdcec1039fc/examples/img/group.png -------------------------------------------------------------------------------- /examples/img/personal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadeTo/vue-tree-list/727ab3a69c070389e79410dc1750bcdcec1039fc/examples/img/personal.png -------------------------------------------------------------------------------- /examples/img/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadeTo/vue-tree-list/727ab3a69c070389e79410dc1750bcdcec1039fc/examples/img/trash.png -------------------------------------------------------------------------------- /img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadeTo/vue-tree-list/727ab3a69c070389e79410dc1750bcdcec1039fc/img/demo.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest', 3 | snapshotSerializers: ['jest-serializer-vue'], 4 | collectCoverageFrom: ['src/**/*.{js,vue}'], 5 | coveragePathIgnorePatterns: ['src/index.js'] 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-tree-list", 3 | "version": "1.5.0", 4 | "description": "A vue component for tree structure. Support adding treenode/leafnode, editing node's name and dragging.", 5 | "author": "ayou", 6 | "scripts": { 7 | "serve": "vue-cli-service serve dev", 8 | "build": "vue-cli-service build --target lib src/index.js", 9 | "test:unit": "vue-cli-service test:unit --watch", 10 | "test:coverage": "vue-cli-service test:unit --coverage", 11 | "lint": "vue-cli-service lint", 12 | "lint-staged": "lint-staged", 13 | "commit": "git-cz", 14 | "prepublish": "npm run build" 15 | }, 16 | "main": "dist/vue-tree-list.umd.min.js", 17 | "dependencies": {}, 18 | "devDependencies": { 19 | "@vue/cli-plugin-babel": "^4.1.0", 20 | "@vue/cli-plugin-eslint": "^4.1.0", 21 | "@vue/cli-plugin-unit-jest": "^4.1.1", 22 | "@vue/cli-service": "^4.1.0", 23 | "@vue/test-utils": "1.0.0-beta.29", 24 | "babel-eslint": "^10.0.3", 25 | "core-js": "^3.4.3", 26 | "eslint": "^5.16.0", 27 | "eslint-config-prettier": "^6.10.0", 28 | "eslint-plugin-prettier": "^3.1.2", 29 | "eslint-plugin-vue": "^5.0.0", 30 | "git-cz": "^4.7.4", 31 | "husky": "^4.2.1", 32 | "jest-serializer-vue": "^2.0.2", 33 | "less": "^3.10.3", 34 | "less-loader": "^5.0.0", 35 | "lint-staged": "^10.0.4", 36 | "prettier": "^1.19.1", 37 | "prettier-eslint-cli": "^5.0.0", 38 | "vue": "^2.6.10", 39 | "vue-template-compiler": "^2.6.10" 40 | }, 41 | "lint-staged": { 42 | "**/*.{js,json,md,vue}": [ 43 | "prettier --write" 44 | ] 45 | }, 46 | "browserslist": [ 47 | "> 1%", 48 | "last 2 versions" 49 | ], 50 | "bugs": { 51 | "url": "https://github.com/ParadeTo/vue-tree-list/issues" 52 | }, 53 | "homepage": "https://github.com/ParadeTo/vue-tree-list#readme", 54 | "keywords": [ 55 | "vue", 56 | "tree" 57 | ], 58 | "license": "ISC", 59 | "repository": { 60 | "type": "git", 61 | "url": "git+https://github.com/ParadeTo/vue-tree-list.git" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: false, 6 | singleQuote: true, 7 | jsxSingleQuote: true, 8 | bracketSpacing: true, 9 | jsxBracketSameLine: false, 10 | rangeStart: 0, 11 | rangeEnd: Infinity, 12 | requirePragma: false, 13 | insertPragma: false, 14 | htmlWhitespaceSensitivity: 'css' 15 | } 16 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-tree-list 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Actions Status](https://github.com/ParadeTo/vue-tree-list/workflows/Test/badge.svg)](https://github.com/ParadeTo/vue-tree-list/actions) 2 | 3 | # vue-tree-list 4 | 5 | A vue component for tree structure. Support adding treenode/leafnode, editing node's name and dragging. 6 | 7 | ![vue-tree-demo.gif](https://raw.githubusercontent.com/ParadeTo/vue-tree-list/master/img/demo.gif) 8 | 9 | [Live Demo](http://paradeto.com/vue-tree-list/) 10 | 11 | # install 12 | 13 | Install the plugin then you can use the component globally. 14 | 15 | ```js 16 | import Vue from 'vue' 17 | import VueTreeList from 'vue-tree-list' 18 | 19 | Vue.use(VueTreeList) 20 | ``` 21 | 22 | Or just register locally like the example below. 23 | 24 | # use 25 | 26 | `npm install vue-tree-list` 27 | 28 | ```html 29 | 60 | 61 | 152 | 153 | 166 | 167 | 179 | ``` 180 | 181 | # props 182 | 183 | ## props of vue-tree-list 184 | 185 | | name | type | default | description | 186 | | :--------------------: | :------: | :-----------: | :-----------------------------------------------------------------------------------------: | 187 | | model | TreeNode | - | You can use `const head = new Tree([])` to generate a tree with the head of `TreeNode` type | 188 | | default-tree-node-name | string | New node node | Default name for new treenode | 189 | | default-leaf-node-name | string | New leaf node | Default name for new leafnode | 190 | | default-expanded | boolean | true | Tree is expanded or not | 191 | 192 | ## props of TreeNode 193 | 194 | ### attributes 195 | 196 | | name | type | default | description | 197 | | :-----------------: | :------------: | :---------------: | :------------------------------: | 198 | | id | string, number | current timestamp | The node's id | 199 | | isLeaf | boolean | false | The node is leaf or not | 200 | | dragDisabled | boolean | false | Forbid dragging tree node | 201 | | addTreeNodeDisabled | boolean | false | Show `addTreeNode` button or not | 202 | | addLeafNodeDisabled | boolean | false | Show `addLeafNode` button or not | 203 | | editNodeDisabled | boolean | false | Show `editNode` button or not | 204 | | delNodeDisabled | boolean | false | Show `delNode` button or not | 205 | | children | array | null | The children of node | 206 | 207 | ### methods 208 | 209 | | name | params | description | 210 | | :----------: | :---------------------: | :---------------------------: | 211 | | changeName | name | Change node's name | 212 | | addChildren | children: object, array | Add children to node | 213 | | remove | - | Remove node from the tree | 214 | | moveInto | target: TreeNode | Move node into another node | 215 | | insertBefore | target: TreeNode | Move node before another node | 216 | | insertAfter | target: TreeNode | Move node after another node | 217 | 218 | # events 219 | 220 | | name | params | description | 221 | | :---------: | :--------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------: | 222 | | click | TreeNode | Trigger when clicking a tree node. You can call `toggle` of `TreeNode` to toggle the folder node. | 223 | | change-name | {'id', 'oldName', 'newName'} | Trigger after changing a node's name | 224 | | delete-node | TreeNode | Trigger when clicking `delNode` button. You can call `remove` of `TreeNode` to remove the node. | 225 | | add-node | TreeNode | Trigger after adding a new node | 226 | | drop | {node, src, target} | Trigger after dropping a node into another. node: the draggable node, src: the draggable node's parent, target: the node that draggable node will drop into | 227 | | drop-before | {node, src, target} | Trigger after dropping a node before another. node: the draggable node, src: the draggable node's parent, target: the node that draggable node will drop before | 228 | | 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 | 229 | 230 | # customize operation icons 231 | 232 | 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: 233 | 234 | ```html 235 | 238 | 241 | 244 | 247 | 250 | 253 | 259 | ``` 260 | -------------------------------------------------------------------------------- /src/Tree.js: -------------------------------------------------------------------------------- 1 | import { traverseTree } from './tools' 2 | /** 3 | * Tree data struct 4 | * Created by ayou on 2017/7/20. 5 | * @param data: treenode's params 6 | * name: treenode's name 7 | * isLeaf: treenode is leaf node or not 8 | * id: id 9 | * dragDisabled: decide if it can be dragged 10 | * disabled: desabled all operation 11 | */ 12 | export class TreeNode { 13 | constructor(data) { 14 | const { id, isLeaf } = data 15 | this.id = typeof id === 'undefined' ? new Date().valueOf() : id 16 | this.parent = null 17 | this.children = null 18 | this.isLeaf = !!isLeaf 19 | 20 | // other params 21 | for (var k in data) { 22 | if (k !== 'id' && k !== 'children' && k !== 'isLeaf') { 23 | this[k] = data[k] 24 | } 25 | } 26 | } 27 | 28 | changeName(name) { 29 | this.name = name 30 | } 31 | 32 | addChildren(children) { 33 | if (!this.children) { 34 | this.children = [] 35 | } 36 | 37 | if (Array.isArray(children)) { 38 | for (let i = 0, len = children.length; i < len; i++) { 39 | const child = children[i] 40 | child.parent = this 41 | child.pid = this.id 42 | } 43 | this.children.concat(children) 44 | } else { 45 | const child = children 46 | child.parent = this 47 | child.pid = this.id 48 | this.children.push(child) 49 | } 50 | } 51 | 52 | // remove self 53 | remove() { 54 | const parent = this.parent 55 | const index = parent.findChildIndex(this) 56 | parent.children.splice(index, 1) 57 | } 58 | 59 | // remove child 60 | _removeChild(child) { 61 | for (var i = 0, len = this.children.length; i < len; i++) { 62 | if (this.children[i] === child) { 63 | this.children.splice(i, 1) 64 | break 65 | } 66 | } 67 | } 68 | 69 | isTargetChild(target) { 70 | let parent = target.parent 71 | while (parent) { 72 | if (parent === this) { 73 | return true 74 | } 75 | parent = parent.parent 76 | } 77 | return false 78 | } 79 | 80 | moveInto(target) { 81 | if (this.name === 'root' || this === target) { 82 | return 83 | } 84 | 85 | // cannot move ancestor to child 86 | if (this.isTargetChild(target)) { 87 | return 88 | } 89 | 90 | // cannot move to leaf node 91 | if (target.isLeaf) { 92 | return 93 | } 94 | 95 | this.parent._removeChild(this) 96 | this.parent = target 97 | this.pid = target.id 98 | if (!target.children) { 99 | target.children = [] 100 | } 101 | target.children.unshift(this) 102 | } 103 | 104 | findChildIndex(child) { 105 | var index 106 | for (let i = 0, len = this.children.length; i < len; i++) { 107 | if (this.children[i] === child) { 108 | index = i 109 | break 110 | } 111 | } 112 | return index 113 | } 114 | 115 | _canInsert(target) { 116 | if (this.name === 'root' || this === target) { 117 | return false 118 | } 119 | 120 | // cannot insert ancestor to child 121 | if (this.isTargetChild(target)) { 122 | return false 123 | } 124 | 125 | this.parent._removeChild(this) 126 | this.parent = target.parent 127 | this.pid = target.parent.id 128 | return true 129 | } 130 | 131 | insertBefore(target) { 132 | if (!this._canInsert(target)) return 133 | 134 | const pos = target.parent.findChildIndex(target) 135 | target.parent.children.splice(pos, 0, this) 136 | } 137 | 138 | insertAfter(target) { 139 | if (!this._canInsert(target)) return 140 | 141 | const pos = target.parent.findChildIndex(target) 142 | target.parent.children.splice(pos + 1, 0, this) 143 | } 144 | 145 | toString() { 146 | return JSON.stringify(traverseTree(this)) 147 | } 148 | } 149 | 150 | export class Tree { 151 | constructor(data) { 152 | this.root = new TreeNode({ name: 'root', isLeaf: false, id: 0 }) 153 | this.initNode(this.root, data) 154 | return this.root 155 | } 156 | 157 | initNode(node, data) { 158 | for (let i = 0, len = data.length; i < len; i++) { 159 | var _data = data[i] 160 | 161 | var child = new TreeNode(_data) 162 | if (_data.children && _data.children.length > 0) { 163 | this.initNode(child, _data.children) 164 | } 165 | node.addChildren(child) 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/VueTreeList.vue: -------------------------------------------------------------------------------- 1 | 141 | 142 | 385 | 386 | 493 | -------------------------------------------------------------------------------- /src/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadeTo/vue-tree-list/727ab3a69c070389e79410dc1750bcdcec1039fc/src/fonts/icomoon.eot -------------------------------------------------------------------------------- /src/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadeTo/vue-tree-list/727ab3a69c070389e79410dc1750bcdcec1039fc/src/fonts/icomoon.ttf -------------------------------------------------------------------------------- /src/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadeTo/vue-tree-list/727ab3a69c070389e79410dc1750bcdcec1039fc/src/fonts/icomoon.woff -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by ayou on 17/7/21. 3 | */ 4 | 5 | import VueTreeList from './VueTreeList' 6 | import { Tree, TreeNode } from './Tree' 7 | 8 | VueTreeList.install = Vue => { 9 | Vue.component(VueTreeList.name, VueTreeList) 10 | } 11 | 12 | export default VueTreeList 13 | export { Tree, TreeNode, VueTreeList } 14 | -------------------------------------------------------------------------------- /src/tools.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by ayou on 18/2/6. 3 | */ 4 | 5 | var handlerCache 6 | 7 | export const addHandler = function(element, type, handler) { 8 | handlerCache = handler 9 | if (element.addEventListener) { 10 | element.addEventListener(type, handler, false) 11 | } else if (element.attachEvent) { 12 | element.attachEvent('on' + type, handler) 13 | } else { 14 | element['on' + type] = handler 15 | } 16 | } 17 | 18 | export const removeHandler = function(element, type) { 19 | if (element.removeEventListener) { 20 | element.removeEventListener(type, handlerCache, false) 21 | } else if (element.detachEvent) { 22 | element.detachEvent('on' + type, handlerCache) 23 | } else { 24 | element['on' + type] = null 25 | } 26 | } 27 | 28 | // depth first search 29 | export const traverseTree = root => { 30 | var newRoot = {} 31 | 32 | for (var k in root) { 33 | if (k !== 'children' && k !== 'parent') { 34 | newRoot[k] = root[k] 35 | } 36 | } 37 | 38 | if (root.children && root.children.length > 0) { 39 | newRoot.children = [] 40 | for (var i = 0, len = root.children.length; i < len; i++) { 41 | newRoot.children.push(traverseTree(root.children[i])) 42 | } 43 | } 44 | return newRoot 45 | } 46 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/render.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Render render correctly 1`] = ` 4 |
5 | 6 |
7 |
8 |
9 |
10 |
11 |
12 | Node 1 13 |
14 | 20 |
21 | 22 |
23 | 41 |
42 |
43 |
44 |
45 |
46 | 47 |
48 | Node 2 49 |
50 | 51 |
52 | 53 |
54 | 55 |
56 |
57 |
58 |
59 |
60 | 61 |
62 | Node 3 63 |
64 | 65 |
66 | 67 |
68 | 69 |
70 |
71 |
72 | `; 73 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/slot.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Slot render slot correctly 1`] = ` 4 |
5 | 6 |
7 |
8 |
9 |
10 |
11 |
12 | Node 1 13 |
14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 🍃 24 |
25 | Node 1-1 26 |
27 | 30 |
31 | 32 |
33 | 34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 |
43 | Node 2 44 |
45 | 46 |
47 | 48 |
49 | 50 |
51 |
52 |
53 | `; 54 | -------------------------------------------------------------------------------- /tests/unit/drag.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { mount } from '@vue/test-utils' 3 | import { Tree, VueTreeList } from '@/index' 4 | 5 | describe('Drag', () => { 6 | let wrapper 7 | 8 | beforeEach(() => { 9 | const tree = new Tree([ 10 | { 11 | name: 'Node 1', 12 | id: 't1', 13 | pid: 0, 14 | children: [ 15 | { 16 | name: 'Node 1-1', 17 | id: 't11', 18 | isLeaf: true, 19 | pid: 't1' 20 | }, 21 | { 22 | name: 'Node 1-2', 23 | id: 't12', 24 | pid: 't1' 25 | } 26 | ] 27 | }, 28 | { 29 | name: 'Node 2', 30 | id: 't2', 31 | pid: 0 32 | }, 33 | { 34 | name: 'Node 3', 35 | id: 't3', 36 | pid: 0 37 | } 38 | ]) 39 | wrapper = mount(VueTreeList, { propsData: { model: new Tree([]) } }) 40 | wrapper.setProps({ model: tree }) 41 | }) 42 | 43 | it('drag before', done => { 44 | const $tree2 = wrapper.find('#t2 .vtl-node-main') 45 | const $tree1Up = wrapper.find('#t1 .vtl-up') 46 | $tree2.trigger('dragstart', { dataTransfer: { setData: () => {} } }) 47 | $tree1Up.trigger('drop') 48 | Vue.nextTick(() => { 49 | expect(wrapper.find('.vtl-node').attributes('id')).toBe('t2') 50 | done() 51 | }) 52 | }) 53 | 54 | it('drag after', done => { 55 | const $tree3 = wrapper.find('#t3 .vtl-node-main') 56 | const $tree1Bottom = wrapper.find('#t1 .vtl-bottom') 57 | $tree3.trigger('dragstart', { dataTransfer: { setData: () => {} } }) 58 | $tree1Bottom.trigger('drop') 59 | Vue.nextTick(() => { 60 | expect( 61 | wrapper 62 | .findAll('.vtl-tree-node') 63 | .at(2) 64 | .attributes('id') 65 | ).toBe('t3') 66 | done() 67 | }) 68 | }) 69 | 70 | it('drag into', done => { 71 | const $tree3 = wrapper.find('#t3 .vtl-node-main') 72 | const $tree1 = wrapper.find('#t1 .vtl-node-main') 73 | $tree3.trigger('dragstart', { dataTransfer: { setData: () => {} } }) 74 | $tree1.trigger('drop') 75 | Vue.nextTick(() => { 76 | expect(wrapper.find('#t1 + .vtl-tree-margin .vtl-node').attributes('id')).toBe('t3') 77 | done() 78 | }) 79 | }) 80 | 81 | it('cannot drag ancestor into child', done => { 82 | const snapshot = wrapper.html() 83 | const $tree1 = wrapper.find('#t1 .vtl-node-main') 84 | const $tree1Child = wrapper.find('#t12 .vtl-node-main') 85 | $tree1.trigger('dragstart', { dataTransfer: { setData: () => {} } }) 86 | $tree1Child.trigger('drop') 87 | Vue.nextTick(() => { 88 | expect(wrapper.html()).toBe(snapshot) 89 | done() 90 | }) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /tests/unit/operation.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { mount } from '@vue/test-utils' 3 | import { Tree, VueTreeList } from '@/index' 4 | 5 | describe('Operation', () => { 6 | let wrapper 7 | 8 | beforeEach(() => { 9 | const tree = new Tree([ 10 | { 11 | name: 'Node 1', 12 | id: 't1', 13 | pid: 0, 14 | children: [ 15 | { 16 | name: 'Node 1-1', 17 | id: 't11', 18 | isLeaf: true, 19 | pid: 't1' 20 | } 21 | ] 22 | }, 23 | { 24 | name: 'Node 2', 25 | id: 't2', 26 | pid: 0 27 | } 28 | ]) 29 | wrapper = mount(VueTreeList, { propsData: { model: new Tree([]) } }) 30 | wrapper.setProps({ model: tree }) 31 | }) 32 | 33 | it('delete leaf node', done => { 34 | const $node11Trash = wrapper.find('#t11 [title="delete"]') 35 | $node11Trash.trigger('click') 36 | wrapper.emitted('delete-node')[0][0].remove() 37 | Vue.nextTick(() => { 38 | expect(wrapper.findAll('.vtl-node').length).toBe(2) 39 | done() 40 | }) 41 | }) 42 | 43 | it('delete tree node', done => { 44 | const $node11Trash = wrapper.find('#t1 [title="delete"]') 45 | $node11Trash.trigger('click') 46 | wrapper.emitted('delete-node')[0][0].remove() 47 | Vue.nextTick(() => { 48 | expect(wrapper.findAll('.vtl-node').length).toBe(1) 49 | done() 50 | }) 51 | }) 52 | 53 | it('add leaf node', done => { 54 | const $node1AddLeafNode = wrapper.find('#t1 [title="Add Leaf Node"]') 55 | $node1AddLeafNode.trigger('click') 56 | Vue.nextTick(() => { 57 | expect(wrapper.findAll('.vtl-leaf-node').length).toBe(2) 58 | done() 59 | }) 60 | }) 61 | 62 | it('add tree node', done => { 63 | const $node1AddTreeNode = wrapper.find('#t1 [title="Add Tree Node"]') 64 | $node1AddTreeNode.trigger('click') 65 | Vue.nextTick(() => { 66 | expect(wrapper.findAll('.vtl-tree-node').length).toBe(3) 67 | done() 68 | }) 69 | }) 70 | 71 | it('change node name', done => { 72 | const $node1Edit = wrapper.find('#t1 [title="edit"]') 73 | $node1Edit.trigger('click') 74 | Vue.nextTick(() => { 75 | const $input = wrapper.find('#t1 .vtl-input') 76 | $input.element.value = 'New Node 1' 77 | $input.trigger('input') 78 | $input.trigger('blur') 79 | Vue.nextTick(() => { 80 | expect(wrapper.find('#t1').text()).toBe('New Node 1') 81 | done() 82 | }) 83 | }) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /tests/unit/render.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { Tree, VueTreeList } from '@/index' 3 | 4 | describe('Render', () => { 5 | it('render correctly', () => { 6 | const tree = new Tree([ 7 | { 8 | name: 'Node 1', 9 | id: 1, 10 | pid: 0, 11 | dragDisabled: true, 12 | addTreeNodeDisabled: true, 13 | addLeafNodeDisabled: true, 14 | editNodeDisabled: true, 15 | delNodeDisabled: true, 16 | children: [ 17 | { 18 | name: 'Node 1-2', 19 | id: 2, 20 | isLeaf: true, 21 | pid: 1 22 | } 23 | ] 24 | }, 25 | { 26 | name: 'Node 2', 27 | id: 3, 28 | pid: 0, 29 | disabled: true 30 | }, 31 | { 32 | name: 'Node 3', 33 | id: 4, 34 | pid: 0 35 | } 36 | ]) 37 | 38 | const wrapper = mount(VueTreeList, { 39 | propsData: { 40 | model: tree, 41 | defaultTreeNodeName: 'new node', 42 | defaultLeafNodeName: 'new leaf', 43 | defaultExpanded: false 44 | } 45 | }) 46 | 47 | expect(wrapper).toMatchSnapshot() 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /tests/unit/slot.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { mount } from '@vue/test-utils' 3 | import { Tree, VueTreeList } from '@/index' 4 | 5 | describe('Slot', () => { 6 | let wrapper 7 | 8 | beforeEach(() => { 9 | const tree = new Tree([ 10 | { 11 | name: 'Node 1', 12 | id: 't1', 13 | pid: 0, 14 | children: [ 15 | { 16 | name: 'Node 1-1', 17 | id: 't11', 18 | isLeaf: true, 19 | pid: 't1' 20 | } 21 | ] 22 | }, 23 | { 24 | name: 'Node 2', 25 | id: 't2', 26 | pid: 0 27 | } 28 | ]) 29 | wrapper = mount(VueTreeList, { 30 | propsData: { model: new Tree([]) }, 31 | scopedSlots: { 32 | addTreeNodeIcon() { 33 | return 📂 34 | }, 35 | addLeafNodeIcon() { 36 | return 37 | }, 38 | editNodeIcon() { 39 | return 📃 40 | }, 41 | delNodeIcon(slotProps) { 42 | return slotProps.model.isLeaf || !slotProps.model.children ? ( 43 | ✂️ 44 | ) : ( 45 | 46 | ) 47 | }, 48 | leafNodeIcon() { 49 | return 🍃 50 | }, 51 | treeNodeIcon(slotProps) { 52 | return ( 53 | 54 | {slotProps.model.children && 55 | slotProps.model.children.length > 0 && 56 | !slotProps.expanded 57 | ? '🌲' 58 | : '❀'} 59 | 60 | ) 61 | } 62 | } 63 | }) 64 | wrapper.setProps({ model: tree }) 65 | }) 66 | 67 | it('render slot correctly', () => { 68 | expect(wrapper).toMatchSnapshot() 69 | }) 70 | 71 | it('toggle tree node show different icon', done => { 72 | const $caretDown = wrapper.find('.vtl-icon-caret-down') 73 | expect(wrapper.find('#t1 .tree-node-icon').text()).toBe('❀') 74 | $caretDown.trigger('click') 75 | Vue.nextTick(() => { 76 | expect(wrapper.exists('.vtl-icon-caret-right')).toBe(true) 77 | expect(wrapper.find('#t1 .tree-node-icon').text()).toBe('🌲') 78 | done() 79 | }) 80 | }) 81 | 82 | it('dont show ✂️ after add child ', done => { 83 | const $addTreeNodeIcon = wrapper.find('#t2 .add-tree-node-icon') 84 | expect(wrapper.find('#t2 .del-node-icon').exists()).toBe(true) 85 | $addTreeNodeIcon.trigger('click') 86 | Vue.nextTick(() => { 87 | expect(wrapper.find('#t2 .del-node-icon').exists()).toBe(false) 88 | done() 89 | }) 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | css: { 3 | extract: false 4 | } 5 | } 6 | --------------------------------------------------------------------------------