├── .browserslistrc ├── src ├── index.js ├── main.ts ├── shims-vue.d.ts ├── components │ ├── helper.ts │ ├── interface.ts │ ├── TreeRow.vue │ └── Tree.vue └── App.vue ├── vue.config.js ├── babel.config.js ├── .editorconfig ├── jest.config.js ├── .gitignore ├── .eslintrc.js ├── tsconfig.json ├── LICENSE ├── tests └── unit │ └── Tree │ ├── data.js │ ├── Data.spec.js │ └── Methods.spec.js ├── package.json └── README.md /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Tree from './components/Tree.vue' 2 | export default Tree 3 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | // vue.config.js 2 | module.exports = { 3 | css: { extract: false }, 4 | }; -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | '@vue/standard', 9 | '@vue/typescript/recommended' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 17 | 'comma-dangle': ['error', 'never'], 18 | '@typescript-eslint/explicit-module-boundary-types': 'off', 19 | '@typescript-eslint/no-explicit-any': 'off', 20 | '@typescript-eslint/no-non-null-assertion': 'off' 21 | }, 22 | overrides: [ 23 | { 24 | files: [ 25 | '**/__tests__/*.{j,t}s?(x)', 26 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 27 | ], 28 | env: { 29 | jest: true 30 | } 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "webpack-env", 17 | "jest" 18 | ], 19 | "paths": { 20 | "@/*": [ 21 | "src/*" 22 | ] 23 | }, 24 | "lib": [ 25 | "esnext", 26 | "dom", 27 | "dom.iterable", 28 | "scripthost" 29 | ] 30 | }, 31 | "include": [ 32 | "src/**/*.ts", 33 | "src/**/*.tsx", 34 | "src/**/*.vue", 35 | "tests/**/*.ts", 36 | "tests/**/*.tsx" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Scalia 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 | -------------------------------------------------------------------------------- /src/components/helper.ts: -------------------------------------------------------------------------------- 1 | import { NodeData } from './interface' 2 | 3 | // Used to merge the tree options/styles with customOptions/customStyles 4 | export const copyOptions = (src: any, dst: any) => { 5 | for (const key in src) { 6 | if (!dst[key]) { 7 | dst[key] = src[key] 8 | } else if (typeof (src[key]) === 'object') { 9 | copyOptions(src[key], dst[key]) 10 | } else { 11 | dst[key] = src[key] 12 | } 13 | } 14 | } 15 | 16 | const disabledState = { expanded: 'expandable', checked: 'checkable', selected: 'selectable' } 17 | 18 | // Recursive function to change node's state 19 | export const recCallNodes = (state: boolean, event: string, nodes: NodeData[]|undefined, pathIds: string[] = []) => { 20 | if (nodes === undefined) { return } 21 | 22 | const targetId = pathIds.shift() 23 | nodes.forEach((node) => { 24 | if (targetId !== undefined && targetId !== node.id) { 25 | return 26 | } 27 | const disabledStateKey = (disabledState as any)[event] 28 | if (targetId === node.id && pathIds.length === 0) { 29 | node.state[event] = state 30 | return 31 | } else if (disabledStateKey && node[disabledStateKey] !== false) { 32 | node.state[event] = state 33 | } 34 | recCallNodes(state, event, node.nodes, pathIds) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /tests/unit/Tree/data.js: -------------------------------------------------------------------------------- 1 | export default { 2 | nodes: [ 3 | { 4 | text: 'Root 1', 5 | id: '1', 6 | state: { checked: false, selected: false, expanded: false }, 7 | nodes: [ 8 | { 9 | text: 'Child 1', 10 | id: '3', 11 | state: { checked: false, selected: false, expanded: false }, 12 | state: { 13 | checked: true, 14 | }, 15 | nodes: [ 16 | { 17 | text: 'Grandchild 1', 18 | id: '5', 19 | state: { checked: false, selected: false, expanded: false } 20 | }, 21 | { 22 | text: 'Grandchild 2', 23 | id: '6', 24 | state: { checked: false, selected: false, expanded: false } 25 | } 26 | ] 27 | }, 28 | { 29 | text: 'Child 2', 30 | id: '4', 31 | state: { checked: false, selected: false, expanded: false } 32 | } 33 | ] 34 | }, 35 | { 36 | text: 'Root 2', 37 | id: '2', 38 | state: { checked: false, selected: false, expanded: false } 39 | } 40 | ], 41 | customStyles: { 42 | tree: { 43 | style: { 44 | height: '300px', 45 | maxHeight: '300px', 46 | overflowY: 'visible', 47 | display: 'block' 48 | } 49 | } 50 | }, 51 | newNodes: [ 52 | { 53 | text: 'Root 1', 54 | id: 1, 55 | state: { checked: false, selected: false, expanded: false } 56 | } 57 | ] 58 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuejs-tree", 3 | "description": "A highly customizable vuejs tree", 4 | "version": "3.0.2", 5 | "main": "dist/vuejs-tree.common.js", 6 | "keywords": [ 7 | "vuejs", 8 | "tree", 9 | "custom" 10 | ], 11 | "repository": "git@github.com:vinz3872/vuejs-tree.git", 12 | "bugs": "git@github.com:vinz3872/vuejs-tree/issues", 13 | "author": "vinz3872 ", 14 | "license": "MIT", 15 | "scripts": { 16 | "serve": "vue-cli-service serve", 17 | "build": "vue-cli-service build --target lib --name vuejs-tree ./src/index.js", 18 | "test:unit": "vue-cli-service test:unit", 19 | "lint": "vue-cli-service lint" 20 | }, 21 | "dependencies": { 22 | "core-js": "^3.6.5", 23 | "vue": "^3.0.0", 24 | "vue-class-component": "^8.0.0-0" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^29.2.0", 28 | "@types/webpack-env": "^1.18.0", 29 | "@typescript-eslint/eslint-plugin": "^5.40.1", 30 | "@typescript-eslint/parser": "^5.40.1", 31 | "@vue/cli-plugin-babel": "~5.0.8", 32 | "@vue/cli-plugin-eslint": "~5.0.8", 33 | "@vue/cli-plugin-typescript": "~5.0.8", 34 | "@vue/cli-plugin-unit-jest": "~5.0.8", 35 | "@vue/cli-service": "~5.0.8", 36 | "@vue/compiler-sfc": "^3.2.41", 37 | "@vue/eslint-config-standard": "^8.0.1", 38 | "@vue/eslint-config-typescript": "^11.0.2", 39 | "@vue/test-utils": "^2.1.0", 40 | "eslint": "^8.26.0", 41 | "eslint-plugin-import": "^2.26.0", 42 | "eslint-plugin-node": "^11.1.0", 43 | "eslint-plugin-promise": "^6.1.1", 44 | "eslint-plugin-standard": "^5.0.0", 45 | "eslint-plugin-vue": "^9.6.0", 46 | "node-sass": "^6.0.0", 47 | "sass-loader": "^13.1.0", 48 | "typescript": "~4.8.4", 49 | "vue-jest": "^5.0.0-0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/interface.ts: -------------------------------------------------------------------------------- 1 | export interface NodeData { 2 | state: { 3 | checked: boolean; 4 | expanded: boolean; 5 | selected: boolean; 6 | [key: string]: any; 7 | } 8 | nodes?: NodeData[]; 9 | id: string; 10 | depth: number; 11 | [key: string]: any; 12 | } 13 | 14 | export interface NodesProperties { 15 | [key: string]: any; 16 | } 17 | 18 | export interface EventParams { 19 | state: boolean; 20 | fn: any; 21 | appearOnHover?: boolean; 22 | calledEvent?: string|null; 23 | } 24 | 25 | export interface TreeCustomStyles { 26 | tree: { 27 | style: { 28 | [key: string]: string; 29 | } 30 | } 31 | } 32 | 33 | export interface TreeRowCustomStyles { 34 | row: { 35 | style: { 36 | [key: string]: string; 37 | } 38 | child: { 39 | class: string; 40 | style: { 41 | [key: string]: string; 42 | } 43 | active: { 44 | class: string; 45 | style: { 46 | [key: string]: string; 47 | } 48 | } 49 | } 50 | } 51 | rowIndent: { 52 | [key: string]: string; 53 | } 54 | expanded: { 55 | class: string; 56 | } 57 | addNode: { 58 | class: string; 59 | style: { 60 | [key: string]: string; 61 | } 62 | } 63 | editNode: { 64 | class: string; 65 | style: { 66 | [key: string]: string; 67 | } 68 | } 69 | deleteNode: { 70 | class: string; 71 | style: { 72 | [key: string]: string; 73 | } 74 | } 75 | selectIcon: { 76 | class: string; 77 | style: { 78 | [key: string]: string; 79 | }, 80 | active: { 81 | class: string; 82 | style: { 83 | [key: string]: string; 84 | } 85 | } 86 | } 87 | text: { 88 | style: { 89 | [key: string]: string; 90 | }, 91 | class: string; 92 | active: { 93 | style: { 94 | [key: string]: string; 95 | } 96 | } 97 | } 98 | } 99 | 100 | export interface TreeCustomOptions { 101 | treeEvents: { 102 | expanded: EventParams; 103 | collapsed: EventParams; 104 | selected: EventParams; 105 | checked: EventParams; 106 | } 107 | } 108 | 109 | export interface TreeRowCustomOptions { 110 | events: { 111 | expanded: EventParams; 112 | selected: EventParams; 113 | checked: EventParams; 114 | editableName: EventParams; 115 | } 116 | addNode: EventParams; 117 | editNode: EventParams; 118 | deleteNode: EventParams; 119 | showTags: boolean 120 | } 121 | -------------------------------------------------------------------------------- /tests/unit/Tree/Data.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import Tree from '@/components/Tree.vue' 3 | 4 | import data from './data.js' 5 | 6 | describe('Data', () => { 7 | it('has default values', () => { 8 | const { nodes } = data 9 | const propsData = { 10 | nodes 11 | } 12 | const wrapper = mount(Tree, { 13 | propsData: propsData 14 | }) 15 | const tree = wrapper.vm 16 | expect(tree.selectedNodeData.id).toEqual(''); 17 | expect(tree.force).toBeTruthy(); 18 | expect(tree.styles.tree.style).toHaveProperty('height', 'auto'); 19 | expect(tree.styles.tree.style).toHaveProperty('maxHeight', '500px'); 20 | expect(tree.styles.tree.style).toHaveProperty('overflowY', 'scroll'); 21 | expect(tree.styles.tree.style).toHaveProperty('display', 'inline-block'); 22 | expect(tree.options.treeEvents.expanded).toHaveProperty('state', false); 23 | expect(tree.options.treeEvents.expanded).toHaveProperty('fn', null); 24 | expect(tree.options.treeEvents.collapsed).toHaveProperty('state', false); 25 | expect(tree.options.treeEvents.collapsed).toHaveProperty('fn', null); 26 | expect(tree.options.treeEvents.selected).toHaveProperty('state', false); 27 | expect(tree.options.treeEvents.selected).toHaveProperty('fn', null); 28 | expect(tree.options.treeEvents.checked).toHaveProperty('state', false); 29 | expect(tree.options.treeEvents.checked).toHaveProperty('fn', null); 30 | }) 31 | 32 | it('can be overwriten', () => { 33 | const fn = jest.fn() 34 | const customOptions = { 35 | treeEvents: { 36 | expanded: { 37 | state: true, 38 | fn: fn 39 | }, 40 | collapsed: { 41 | state: true, 42 | fn: fn 43 | }, 44 | selected: { 45 | state: true, 46 | fn: fn 47 | }, 48 | checked: { 49 | state: true, 50 | fn: fn 51 | } 52 | } 53 | } 54 | let { customStyles, nodes } = data 55 | const propsData = { 56 | customOptions, 57 | customStyles, 58 | nodes 59 | } 60 | const wrapper = mount(Tree, { 61 | propsData: propsData 62 | }) 63 | const tree = wrapper.vm 64 | expect(tree.selectedNodeData.id).toEqual(''); 65 | expect(tree.force).toBeTruthy(); 66 | expect(tree.styles.tree.style).toHaveProperty('height', customStyles.tree.style.height); 67 | expect(tree.styles.tree.style).toHaveProperty('maxHeight', customStyles.tree.style.maxHeight); 68 | expect(tree.styles.tree.style).toHaveProperty('overflowY', customStyles.tree.style.overflowY); 69 | expect(tree.styles.tree.style).toHaveProperty('display', customStyles.tree.style.display); 70 | expect(tree.options.treeEvents.expanded).toHaveProperty('state', true); 71 | expect(tree.options.treeEvents.expanded).toHaveProperty('fn', fn); 72 | expect(tree.options.treeEvents.collapsed).toHaveProperty('state', true); 73 | expect(tree.options.treeEvents.collapsed).toHaveProperty('fn', fn); 74 | expect(tree.options.treeEvents.selected).toHaveProperty('state', true); 75 | expect(tree.options.treeEvents.selected).toHaveProperty('fn', fn); 76 | expect(tree.options.treeEvents.checked).toHaveProperty('state', true); 77 | expect(tree.options.treeEvents.checked).toHaveProperty('fn', fn); 78 | }) 79 | 80 | }) 81 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 183 | -------------------------------------------------------------------------------- /tests/unit/Tree/Methods.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import Tree from '@/components/Tree.vue' 3 | 4 | import data from './data.js' 5 | 6 | let wrapper; 7 | 8 | beforeEach(() => { 9 | let { customOptions, customStyles, nodes } = data 10 | let propsData = { 11 | customOptions, 12 | customStyles, 13 | nodes 14 | } 15 | wrapper = mount(Tree, { 16 | propsData: JSON.parse(JSON.stringify(propsData)) 17 | }) 18 | return wrapper 19 | }) 20 | 21 | describe('findNodePath', () => { 22 | it('find a node path by id with one depth', () => { 23 | const tree = wrapper.vm 24 | const path = tree.findNodePath('1') 25 | expect(path).toEqual(['1']) 26 | }) 27 | 28 | it('find a node path by id with multiple depth', () => { 29 | const tree = wrapper.vm 30 | const path = tree.findNodePath('3') 31 | expect(path).toEqual(['1', '3']) 32 | }) 33 | }) 34 | 35 | describe('findNode', () => { 36 | it('find a node by id with one depth', () => { 37 | const tree = wrapper.vm 38 | const node = tree.findNode(tree.nodes[0].id) 39 | expect(node).toEqual(tree.nodes[0]) 40 | }) 41 | 42 | it('find a node by id with multiple depth', () => { 43 | const tree = wrapper.vm 44 | const node = tree.findNode(tree.nodes[0].nodes[0].id) 45 | expect(node).toEqual(tree.nodes[0].nodes[0]) 46 | }) 47 | }) 48 | 49 | describe('onNodeSelected', () => { 50 | it('store the selected node', () => { 51 | const tree = wrapper.vm 52 | // store a node 53 | tree.nodes[0].state.selected = true 54 | tree.onNodeSelected(tree.nodes[0]) 55 | expect(tree.selectedNodeData.id).toEqual(tree.nodes[0].id) 56 | 57 | // store the new node 58 | tree.nodes[1].state.selected = true 59 | tree.onNodeSelected(tree.nodes[1]) 60 | expect(tree.selectedNodeData.id).toEqual(tree.nodes[1].id) 61 | 62 | // don't store the node 63 | tree.nodes[1].state.selected = false 64 | tree.onNodeSelected(tree.nodes[1]) 65 | expect(tree.selectedNodeData.id).toEqual('') 66 | }) 67 | }) 68 | 69 | describe('onNodeExpanded', () => { 70 | it('return the expanded nodes', () => { 71 | const tree = wrapper.vm 72 | const actualExpandedNodeIds = tree.getExpandedNodes('id') 73 | tree.nodes[0].state.expanded = true 74 | tree.onNodeExpanded(tree.nodes[0], true) 75 | expect(tree.getExpandedNodes('id').length).toBeGreaterThan(actualExpandedNodeIds.length) 76 | }) 77 | }) 78 | 79 | describe('onNodeChecked', () => { 80 | it('return the checked nodes', () => { 81 | const tree = wrapper.vm 82 | const actualCheckedNodeIds = tree.getCheckedNodes('id') 83 | tree.nodes[0].state.checked = true 84 | tree.onNodeChecked(tree.nodes[0], true) 85 | expect(tree.getCheckedNodes('id').length).toBeGreaterThan(actualCheckedNodeIds.length) 86 | }) 87 | }) 88 | 89 | describe('checkNode', () => { 90 | it('check a node', () => { 91 | const tree = wrapper.vm 92 | 93 | tree.checkNode('1') 94 | expect(tree.nodes[0].state.checked).toEqual(true) 95 | }) 96 | }) 97 | 98 | describe('uncheckNode', () => { 99 | it('uncheck a node', () => { 100 | const tree = wrapper.vm 101 | 102 | tree.checkNode('1') 103 | expect(tree.nodes[0].state.checked).toEqual(true) 104 | tree.uncheckNode('1') 105 | expect(tree.nodes[0].state.checked).toEqual(false) 106 | }) 107 | }) 108 | 109 | describe('getSelectedNode', () => { 110 | it('return selected node', () => { 111 | const tree = wrapper.vm 112 | 113 | tree.nodes[0].state.selected = true 114 | tree.onNodeSelected(tree.nodes[0]) 115 | const selectedNode = tree.getSelectedNode() 116 | expect(selectedNode).toEqual(tree.nodes[0]) 117 | }) 118 | }) 119 | 120 | describe('getCheckedNodes', () => { 121 | it('return checked node', () => { 122 | const tree = wrapper.vm 123 | 124 | const checkedNodes = tree.getCheckedNodes('id') 125 | expect(checkedNodes.length).toEqual(1) 126 | expect(checkedNodes[0]).toEqual(tree.nodes[0].nodes[0].id) 127 | }) 128 | }) 129 | 130 | describe('getExpandedNodes', () => { 131 | it('return expanded node', () => { 132 | const tree = wrapper.vm 133 | 134 | tree.expandNode(tree.nodes[0].id) 135 | const expandedNodes = tree.getExpandedNodes('id') 136 | expect(expandedNodes.length).toEqual(1) 137 | expect(expandedNodes[0]).toEqual(tree.nodes[0].id) 138 | }) 139 | 140 | it('return expanded node with parents', () => { 141 | const tree = wrapper.vm 142 | 143 | tree.expandNode(tree.nodes[0].id) 144 | tree.expandNode(tree.nodes[0].nodes[0].id) 145 | 146 | process.nextTick(() => { 147 | const expandedNodes = tree.getExpandedNodes('id') 148 | expect(expandedNodes.length).toEqual(2) 149 | expect(expandedNodes[0]).toEqual(tree.nodes[0].id) 150 | expect(expandedNodes[1]).toEqual(tree.nodes[0].nodes[0].id) 151 | }) 152 | }) 153 | }) 154 | 155 | describe('checkAllNodes', () => { 156 | it('check all nodes', () => { 157 | const tree = wrapper.vm 158 | 159 | tree.checkAllNodes() 160 | expect(tree.nodes[0].state.checked).toEqual(true) 161 | expect(tree.nodes[1].state.checked).toEqual(true) 162 | }) 163 | }) 164 | 165 | describe('uncheckAllNodes', () => { 166 | it('uncheck all nodes', () => { 167 | const tree = wrapper.vm 168 | 169 | tree.checkNode(tree.nodes[0].id) 170 | tree.checkNode(tree.nodes[1].id) 171 | expect(tree.nodes[0].state.checked).toEqual(true) 172 | tree.uncheckAllNodes() 173 | expect(tree.nodes[0].state.checked).toEqual(false) 174 | expect(tree.nodes[1].state.checked).toEqual(false) 175 | }) 176 | }) 177 | 178 | describe('expandAllNodes', () => { 179 | it('expand all nodes', () => { 180 | const tree = wrapper.vm 181 | 182 | tree.expandAllNodes() 183 | expect(tree.nodes[0].state.expanded).toEqual(true) 184 | process.nextTick(() => 185 | expect(tree.nodes[0].nodes[0].state.expanded).toEqual(true) 186 | ) 187 | }) 188 | }) 189 | 190 | describe('collapseAllNodes', () => { 191 | it('collapse all nodes', () => { 192 | const tree = wrapper.vm 193 | 194 | tree.expandAllNodes() 195 | expect(tree.nodes[0].state.expanded).toEqual(true) 196 | tree.collapseAllNodes() 197 | expect(tree.nodes[0].state.expanded).toEqual(false) 198 | expect(tree.getExpandedNodes('id').length).toEqual(0) 199 | }) 200 | }) 201 | 202 | describe('deselectAllNodes', () => { 203 | it('deselect all nodes', () => { 204 | const tree = wrapper.vm 205 | 206 | tree.nodes[0].state.selected = true 207 | tree.onNodeSelected(tree.nodes[0]) 208 | tree.deselectAllNodes() 209 | expect(tree.selectedNodeData).toBeNull 210 | }) 211 | }) 212 | 213 | describe('selectNode', () => { 214 | it('select a node', () => { 215 | const tree = wrapper.vm 216 | 217 | // select a node with depth 0 218 | tree.selectNode(tree.nodes[0].id) 219 | expect(tree.selectedNodeData.id).toEqual(tree.nodes[0].id) 220 | 221 | // select a nested node 222 | tree.selectNode(tree.nodes[0].nodes[0].id) 223 | expect(tree.selectedNodeData.id).toEqual(tree.nodes[0].nodes[0].id) 224 | 225 | // select an other node 226 | tree.selectNode(tree.nodes[1].id) 227 | expect(tree.nodes[0].state.selected).toEqual(false) 228 | expect(tree.nodes[0].nodes[0].state.selected).toEqual(false) 229 | expect(tree.selectedNodeData.id).toEqual(tree.nodes[1].id) 230 | }) 231 | }) 232 | 233 | describe('getVisibleNodes', () => { 234 | it('get all visible nodes', () => { 235 | const tree = wrapper.vm 236 | 237 | tree.collapseAllNodes() 238 | // return visible node ids 239 | expect(tree.getVisibleNodes()).toEqual(['1','2']) 240 | 241 | // return visible node ids 242 | expect(tree.getVisibleNodes(true)[0]).toEqual(tree.nodes[0]) 243 | 244 | tree.expandAllNodes() 245 | expect(tree.getVisibleNodes().length).not.toEqual(['1','2']) 246 | }) 247 | }) 248 | 249 | describe('getNodesData', () => { 250 | it('get expanded node ids', () => { 251 | const tree = wrapper.vm 252 | 253 | tree.expandNode(tree.nodes[0].id) 254 | tree.expandNode(tree.nodes[0].nodes[0].id) 255 | // return visible node ids 256 | process.nextTick(() => 257 | expect(tree.getNodesData('id', { expanded: true })).toEqual(['1','3']) 258 | ) 259 | }) 260 | 261 | it('get expanded nodes ids and text', () => { 262 | const tree = wrapper.vm 263 | 264 | tree.expandNode(tree.nodes[0].id) 265 | tree.expandNode(tree.nodes[0].nodes[0].id) 266 | // return visible node ids 267 | process.nextTick(() => { 268 | const nodes = tree.getNodesData(['id', 'text'], { expanded: true }) 269 | expect(nodes.length).toEqual(2) 270 | expect(nodes[0]).toEqual({'id': '1', 'text': 'Root 1'}) 271 | }) 272 | }) 273 | 274 | it('get expanded node tree ids', () => { 275 | const tree = wrapper.vm 276 | 277 | tree.expandNode(tree.nodes[0].id) 278 | tree.expandNode(tree.nodes[0].nodes[0].id) 279 | // return visible node ids 280 | process.nextTick(() => 281 | expect(tree.getNodesData('id', { expanded: true }, true)).toEqual({'1': {'3': {}}}) 282 | ) 283 | }) 284 | }) 285 | -------------------------------------------------------------------------------- /src/components/TreeRow.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 291 | 292 | 361 | -------------------------------------------------------------------------------- /src/components/Tree.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 352 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | VueJS Tree 2 | ========== 3 | 4 | [![npm version](https://img.shields.io/npm/v/vuejs-tree.svg?style=flat)](https://www.npmjs.com/package/vuejs-tree) 5 | [![npm version](https://img.shields.io/github/package-json/dependency-version/vinz3872/vuejs-tree/vue?style=flat)](https://www.npmjs.com/package/vue) 6 | 7 | Vue2 version [here](https://github.com/vinz3872/vuejs-tree/tree/vue2) 8 | 9 | A highly customizable vuejs tree viewer 10 | 11 | 12 | ![tree](https://raw.githubusercontent.com/vinz3872/vuejs-tree/7b6d80f7362cdaf0da9eba9997a9b132c5b99e7b/images/tree.png) 13 | 14 | #### Example 15 | 16 | [codesandbox](https://codesandbox.io/s/vuejs-tree-sandbox-v3x-lmbyx) 17 | 18 | ## Getting Started 19 | 20 | ### Install 21 | 22 | You can install using yarn: 23 | 24 | ```bash 25 | $ yarn add vuejs-tree 26 | ``` 27 | 28 | or with npm: 29 | 30 | ```bash 31 | $ npm install vuejs-tree 32 | ``` 33 | 34 | 35 | ### Usage 36 | 37 | Add the following lines at the top of your .js file which contains your Vue instance. 38 | 39 | ```javascript 40 | import Tree from 'vuejs-tree' 41 | 42 | 43 | // in your vue instance 44 | components: { 45 | 'Tree': Tree 46 | }, 47 | ``` 48 | 49 | 50 | Then add the following line in your html file to generate a tree. You can have as many trees per page as you want. 51 | ```html 52 | 53 | ``` 54 | 55 | 56 | ## Data Structure 57 | 58 | You need to define data to display which is a nested array of hash. 59 | 60 | Example: 61 | 62 | ```javascript 63 | data: { 64 | treeDisplayData: [ 65 | { 66 | text: 'Root 1', 67 | state: { checked: false, selected: false, expanded: false }, 68 | nodes: [ 69 | { 70 | text: 'Child 1', 71 | state: { checked: false, selected: false, expanded: false }, 72 | nodes: [ 73 | { 74 | text: 'Grandchild 1', 75 | state: { checked: false, selected: false, expanded: false } 76 | }, 77 | { 78 | text: 'Grandchild 2', 79 | state: { checked: false, selected: false, expanded: false } 80 | } 81 | ] 82 | }, 83 | { 84 | text: 'Child 2', 85 | state: { checked: false, selected: false, expanded: false } 86 | } 87 | ] 88 | }, 89 | { 90 | text: 'Root 2', 91 | state: { checked: false, selected: false, expanded: false } 92 | } 93 | ] 94 | } 95 | ``` 96 | 97 | ### Node properties 98 | 99 | Here is a fully customized node: 100 | 101 | ```javascript 102 | { 103 | id: 1, 104 | text: 'Root 1', 105 | definition: 'First node', 106 | depth: 1, 107 | checkable: false, 108 | selectable: false, 109 | expandable: true, 110 | tags: [42], 111 | state: { 112 | checked: false, 113 | expanded: false, 114 | selected: false 115 | }, 116 | nodes: [ 117 | {}, 118 | ... 119 | ] 120 | } 121 | ``` 122 | 123 | The Following properties define a node level css and behavior. 124 | 125 | | key | type | Detail | 126 | |------------------|--------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| 127 | | id | String or Integer --> Mandatory | Used in the tree to differentiate each node | 128 | | text | String --> Mandatory | The text value displayed at the right of the node icons | 129 | | definition | String --> Optional | If some text is given, then it will show as a tooltip | 130 | | depth | Integer --> Optional | It corresponds to the node depth, starting from 0, 1 or anything. It's advisable to fill these fields if some of your nodes have the same id | 131 | | tags | [Integer] --> Optional | The tag is displayed at the right end of the line | 132 | | checkable | Boolean --> Optional, default: true | Used to enable or disable the node's check event | 133 | | selectable | Boolean --> Optional, default: true | Used to enable or disable the node's select event | 134 | | expandable | Boolean --> Optional, default: true | Used to enable or disable the node's expand event | 135 | | state | | nodes's state | 136 | | state.checked | Boolean --> Mandatory, default: false | Change the node's default state (at initialize) | 137 | | state.selected | Boolean --> Mandatory, default: false | Change the node's default state (at initialize) | 138 | | state.expanded | Boolean --> Mandatory, default: false | Change the node's default state (at initialize) | 139 | | nodes | Object --> Optional | Used to display the node's children. *Look above for a structure example* | 140 | 141 | ## Options / Styles 142 | 143 | Here is an example of a customOptions hash the tree can take. 144 | I suggest you to use a vuejs computed function if you want to use a function pointer. 145 | 146 | ```javascript 147 | computed: { 148 | myCustomStyles() { 149 | return { 150 | tree: { 151 | style: { 152 | height: 'auto', 153 | maxHeight: '300px', 154 | overflowY: 'visible', 155 | display: 'inline-block', 156 | textAlign: 'left' 157 | } 158 | }, 159 | row: { 160 | style: { 161 | width: '500px', 162 | cursor: 'pointer' 163 | }, 164 | child: { 165 | class: '', 166 | style: { 167 | height: '35px' 168 | }, 169 | active: { 170 | class: 'custom_row_active_class', 171 | style: { 172 | height: '35px' 173 | } 174 | } 175 | } 176 | }, 177 | addNode: { 178 | class: 'custom_class', 179 | style: { 180 | color: '#007AD5' 181 | } 182 | }, 183 | editNode: { 184 | class: 'custom_class', 185 | style: { 186 | color: '#007AD5' 187 | } 188 | }, 189 | deleteNode: { 190 | class: 'custom_class', 191 | style: { 192 | color: '#EE5F5B' 193 | } 194 | }, 195 | selectIcon: { 196 | class: 'custom_class', 197 | style: { 198 | color: '#007AD5' 199 | }, 200 | active: { 201 | class: 'custom_class', 202 | style: { 203 | color: '#2ECC71' 204 | } 205 | } 206 | }, 207 | text: { 208 | style: {}, 209 | class: 'capitalize', 210 | active: { 211 | style: { 212 | 'font-weight': 'bold', 213 | color: '#2ECC71' 214 | } 215 | } 216 | } 217 | }; 218 | }, 219 | myCustomOptions() { 220 | return { 221 | treeEvents: { 222 | expanded: { 223 | state: true, 224 | fn: null, 225 | }, 226 | collapsed: { 227 | state: false, 228 | fn: null, 229 | }, 230 | selected: { 231 | state: false, 232 | fn: null, 233 | }, 234 | checked: { 235 | state: true, 236 | fn: this.myCheckedFunction, 237 | } 238 | }, 239 | events: { 240 | expanded: { 241 | state: true, 242 | fn: null, 243 | }, 244 | selected: { 245 | state: false, 246 | fn: null, 247 | }, 248 | checked: { 249 | state: false, 250 | fn: null, 251 | }, 252 | editableName: { 253 | state: false, 254 | fn: null, 255 | calledEvent: null, 256 | } 257 | }, 258 | addNode: { state: false, fn: null, appearOnHover: false }, 259 | editNode: { state: true, fn: null, appearOnHover: true }, 260 | deleteNode: { state: true, fn: null, appearOnHover: true }, 261 | showTags: true, 262 | }; 263 | } 264 | }, 265 | ``` 266 | 267 | #### Styles 268 | 269 | | Option name | Detail | 270 | |-------------|----------------------------------------------------------------------------------------------------| 271 | | tree | Object - override default tree css | 272 | | row.style | Object - override default tree node css | 273 | | row.child | Object - override style of `
` into the `
  • ` row (e.g. you can manage the height of the row)
    Keys:
    - `style`: css style applied when the node is not selected
    - `class`: class applied when the node is not selected
    - `active.style`: css style applied when the node is selected
    - `active.class`: class applied when the node is selected | 274 | | rowIndent | Object - override style of `
      ` (e.g. you can manage the child node indent) | 275 | | expanded | Object - contains the class of the expanded icon | 276 | | addNode | Object - contains the class and the style of the addNode button
      Keys:
      - `class`: addNode icon class, required to display the icon
      - `style`: addNode icon style | 277 | | editNode | Object - contains the class and the style of the editNode button
      Keys:
      - `class`: editNode icon class, required to display the icon
      - `style`: editNode icon style | 278 | | deleteNode | Object - contains the class and the style of the deleteNode button
      Keys:
      - `class`: deleteNode icon class, required to display the icon
      - `style`: deleteNode icon style | 279 | | selectIcon | Object - contains the class and the style for the select node icon
      Keys:
      - `style`: unselected icon style
      - `class`: unselected icon class, required to display the icon
      - `active.style`: selected icon style
      - `active.class`: selected icon class, required to display the icon | 280 | | text | Object - contains the class and the style for the node's text
      Keys:
      - `style`: css style applied when the node is not selected
      - `class`: class always applied to the text
      - `active.style`: css style applied when the node is selected | 281 | 282 | #### Options 283 | ##### Tree options 284 | | Option name | Detail | 285 | |----------------------|--------------------------------------------------------------------------------------------------------------------------| 286 | | treeEvents | Object - contains the callback tree events, called **after** the tree row events | 287 | | treeEvents.expanded | Object - enable or disable the callback when a node is expanded. If enabled, **fn** (function pointer) must be present. | 288 | | treeEvents.collapsed | Object - enable or disable the callback when a node is collasped. If enabled, **fn** (function pointer) must be present. | 289 | | treeEvents.selected | Object - enable or disable the callback when a node is selected. If enabled, **fn** (function pointer) must be present. | 290 | | treeEvents.checked | Object - enable or disable the callback when a node is checked. If enabled, **fn** (function pointer) must be present. | 291 | 292 | ##### Row Options 293 | | Option name | Detail | 294 | |---------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 295 | | showTags | Boolean - Show the node's tag if given | 296 | | addNode | Object - enable or disable the add node button. If enabled, **fn** must be present. If *appearOnHover* is true, the button will appear only if the row is hovered | 297 | | editNode | Object - enable or disable the edit node button. If enabled, **fn** must be present. If *appearOnHover* is true, the button will appear only if the row is hovered | 298 | | deleteNode | Object - enable or disable the delete node button. If enabled, **fn** must be present. If *appearOnHover* is true, the button will appear only if the row is hovered | 299 | | events | Object - contains the node events, **override** the tree row events behavior | 300 | | events.expanded | Object - enable or disable the expanded node event. The **fn** key is optional, if present, it will **replace** the native behavior | 301 | | events.selected | Object - enable or disable the selected node event. The **fn** key is optional, if present, it will **replace** the native behavior | 302 | | events.checked | Object - enable or disable the checked node event. The **fn** key is optional, if present, it will **replace** the native behavior | 303 | | events.editableName | Object - enable or disable the event when the node's name is clicked. If enabled, the key **fn** or **calledEvent** must be present. **calledEvent** is a string and it value must be an existing event (e.g. 'selected') | 304 | 305 | ## Events 306 | 307 | ### Tree 308 | You can call your own function here by assigning a function pointer in the tree options and changing its state to true. 309 | These functions are called after all tree modifications. 310 | 311 | #### onNodeSelected 312 | Called when a node is selected. 313 | `myCustomOptions.treeEvents.selected.fn` 314 | 315 | #### onNodeExpanded 316 | Called when a node is expanded. 317 | `myCustomOptions.treeEvents.expanded.fn` 318 | 319 | Or called when a node is collapsed. 320 | `myCustomOptions.treeEvents.collapsed.fn` 321 | 322 | #### onNodeChecked 323 | Called when a node is collapsed. 324 | `myCustomOptions.treeEvents.checked.fn` 325 | 326 | ### Tree row 327 | You can call your own function here by assigning a function pointer in the tree options. It will replace the existing behavior of the tree for this event. 328 | You can also disabled an event by changing it's state to false. 329 | 330 | #### toggleSelected 331 | Called when a node is selected. `myCustomOptions.events.selected.fn` 332 | 333 | #### toggleExpanded 334 | Called when a node is expanded or collapsed. `myCustomOptions.events.expanded.fn` 335 | 336 | #### toggleChecked 337 | Called when a node is checked. `myCustomOptions.events.checked.fn` 338 | 339 | #### editableName 340 | You can call a special function if you assign it's pointer in `myCustomOptions.events.editableName.fn` 341 | Or you can call an existing event by assigining it's name in `myCustomOptions.events.editableName.calledEvent` 342 | 343 | example : `myCustomOptions.events.editableName.calledEvent = 'selected'` 344 | 345 | ## Methods 346 | 347 | Methods Params: 348 | 349 | `depth` --> Optional but help distinguish nodes with the same id. 350 | 351 | `argWanted` --> It can either be a node attribute name (string) or a array of node attribute name (like ['id', 'name']). 352 | 353 | `format` --> If you want the function to return an plain array (false) or a hash tree (true). 354 | 355 | `maxDepth` --> The function will only access nodes within the maxDepth. 356 | 357 | `fullNode` --> Return only node ids or node objects. 358 | 359 | `conditions` --> It's used to affect only the nodes which match it. For example if the condition is {checked: true}, the function will affect only the nodes which are checked. You can use all nodes attribute that are present in the node object. 360 | 361 | | Function | Detail | 362 | |----------------------------------------------------------|----------------------------------------------------------------------| 363 | | checkNode(nodeId, depth) | Check a node | 364 | | uncheckNode(nodeId, depth) | Uncheck a node | 365 | | getSelectedNode() | Return the selected node if you have selected a node | 366 | | getCheckedNodes(argWanted, format = false) | Return all checked nodes | 367 | | getExpandedNodes(argWanted, format = false) | Return all expanded nodes | 368 | | checkAllNodes() | Check all nodes | 369 | | uncheckAllNodes() | Uncheck all nodes | 370 | | expandNode(nodeId, depth) | Expand a node | 371 | | collapseNode(nodeId, depth) | Collapse a node | 372 | | selectNode(nodeId, depth) | Select a node and deselect the previously selected node if it exists | 373 | | expandAllNodes() | Expand all nodes | 374 | | collapseAllNodes() | Collapse all nodes | 375 | | deselectAllNodes() | Deselect all nodes | 376 | | findNode(nodeId, maxDepth = 9999) | Find and return a node | 377 | | getVisibleNodes(fullNode = false) | Get all visible nodes | 378 | | getNodesData(argWanted, conditions = {}, format = false) | Customizable function that returns nodes | 379 | 380 | ### Get the tree instance 381 | 382 | If you want to call any tree method, you need to get the instance. 383 | 384 | To get the tree instance you just need to be in the vue instance and use `this.$refs['my-tree-ref']` 385 | Then you can use a method like that: `this.$refs['my-tree-ref'].myMethod()` 386 | --------------------------------------------------------------------------------