├── .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 |
2 |
3 |
4 |
18 |
19 |
20 | {{ slotProps.model.name }} #{{ slotProps.model.id }}
21 |
22 |
23 |
24 | 📂
25 |
26 |
27 | +
28 |
29 |
30 | 📃
31 |
32 |
33 | ✂️
34 |
35 |
36 | 🍃
37 |
38 |
39 |
40 | {{
41 | slotProps.model.children && slotProps.model.children.length > 0 && !slotProps.expanded
42 | ? '🌲'
43 | : ''
44 | }}
46 |
47 |
48 |
49 |
50 | {{ newTree }}
51 |
52 |
53 |
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 | [](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 | 
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 |
30 |
31 |
32 |
42 |
43 |
44 | {{ slotProps.model.name }} #{{ slotProps.model.id }}
45 |
46 |
47 | 📂
48 | +
49 | 📃
50 | ✂️
51 | 🍃
52 | 🌲
53 |
54 |
55 |
56 | {{newTree}}
57 |
58 |
59 |
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 |
236 | {{ slotProps.model.name }} #{{ slotProps.model.id }}
237 |
238 |
239 | 📂
240 |
241 |
242 | +
243 |
244 |
245 | 📃
246 |
247 |
248 | ✂️
249 |
250 |
251 | 🍃
252 |
253 |
254 |
255 | {{ (slotProps.model.children && slotProps.model.children.length > 0 && !slotProps.expanded) ?
256 | '🌲' : '' }}
258 |
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 |
2 |
3 |
9 |
17 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {{ model.name }}
48 |
49 |
50 |
60 |
61 |
66 |
67 |
68 |
69 |
70 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
101 |
102 |
103 |
108 | -
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
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 |
--------------------------------------------------------------------------------
/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 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Node 1-2
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
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 |
28 |
29 | 📃 ✂️
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 |
--------------------------------------------------------------------------------