├── .github
└── workflows
│ ├── build.yml
│ ├── docs.yml
│ └── tests.yml
├── .gitignore
├── LICENSE
├── README.md
├── babel.config.js
├── deploy.sh
├── docs
├── .vitepress
│ ├── config.js
│ └── theme
│ │ └── index.js
├── guide
│ ├── async.md
│ ├── events.md
│ ├── getting-started.md
│ ├── properties.md
│ ├── slots.md
│ └── transitions.md
├── index.md
└── public
│ ├── add.png
│ ├── minus.png
│ └── screenshot.png
├── index.html
├── jest.config.js
├── package.json
├── public
├── favicon.ico
└── screenshot.png
├── src
├── assets
│ └── logo.png
├── components
│ ├── Icon.vue
│ ├── Tree.vue
│ ├── TreeIcons.vue
│ ├── TreeLevel.vue
│ └── TreeNode.vue
├── css
│ └── material.css
├── dev.js
├── dev.vue
├── index.ts
├── misc
│ ├── default.ts
│ ├── helpers.ts
│ └── nodeEvents.ts
├── setup
│ ├── checkbox
│ │ ├── auto.ts
│ │ └── manual.ts
│ ├── store.ts
│ ├── useCheckBox.ts
│ ├── useCommon.ts
│ ├── useDragAndDrop.ts
│ ├── useIcon.ts
│ ├── useInput.ts
│ ├── useLevel.ts
│ ├── useNode.ts
│ └── useTree.ts
├── shims-vue.d.ts
└── structure
│ ├── IConfiguration.ts
│ ├── IDragContext.ts
│ ├── IIcon.ts
│ ├── INode.ts
│ ├── INodeProps.ts
│ ├── INodeState.ts
│ ├── ITreeProps.ts
│ ├── IUseCheck.ts
│ ├── IUseCommon.ts
│ └── IUseNode.ts
├── tests
└── unit
│ ├── auto.spec.ts
│ ├── default.spec.ts
│ ├── helpers.spec.ts
│ ├── nodeEvents.spec.ts
│ ├── store.spec.ts
│ ├── useCheckBox.spec.ts
│ ├── useCommon.spec.ts
│ ├── useDragAndDrop.spec.ts
│ ├── useIcon.spec.ts
│ ├── useInput.spec.ts
│ ├── useLevel.spec.ts
│ ├── useNode.spec.ts
│ └── useTree.spec.ts
├── todo.txt
├── tsconfig.json
├── vite.config.js
└── yarn.lock
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Use Node.js ${{ matrix.node-version }}
13 | uses: actions/setup-node@v1
14 | with:
15 | node-version: ${{ matrix.node-version }}
16 | - run: yarn install
17 | - run: yarn bundle
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: docs
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | workflow_dispatch:
7 |
8 | jobs:
9 | build-and-deploy:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout Git repository
14 | uses: actions/checkout@v2
15 |
16 | - name: Build and Deploy
17 | uses: jenkey2011/vuepress-deploy@master
18 | env:
19 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
20 | TARGET_BRANCH: gh-pages
21 | BUILD_SCRIPT: npm install && npm run docs:build
22 | BUILD_DIR: docs/.vuepress/dist
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | tests:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Use Node.js ${{ matrix.node-version }}
13 | uses: actions/setup-node@v1
14 | with:
15 | node-version: ${{ matrix.node-version }}
16 | - run: yarn install
17 | - run: yarn test
18 | - run: yarn istanbul-badges-readme
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # VuePress files
2 | docs/.vitepress/dist/
3 |
4 | .DS_Store
5 | node_modules
6 | /dist
7 | /coverage
8 |
9 | # local env files
10 | .env.local
11 | .env.*.local
12 |
13 | # Log files
14 | npm-debug.log*
15 | yarn-debug.log*
16 | yarn-error.log*
17 | pnpm-debug.log*
18 |
19 | # Editor directories and files
20 | .idea
21 | .vscode
22 | *.suo
23 | *.ntvs*
24 | *.njsproj
25 | *.sln
26 | *.sw?
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021-present, Tristan Fontaine
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
13 | all 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
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## vue3-treeview
2 |
3 |    [](https://lbesson.mit-license.org/)
4 |
5 | 
6 |
7 | Vue3 treeview is a treeview project.
8 |
9 | [Documentation](https://n00ts.github.io/vue3-treeview)
10 |
11 | __Features:__
12 |
13 | - Made with vue3
14 | - Fully reactive
15 | - Typings
16 | - Normalized data (not deeply nested)
17 | - Full composition API
18 | - Lazy load
19 | - Editable text
20 | - Events for all actions
21 | - Node slots
22 | - Loading slot
23 | - Keyboard navigation
24 | - Checkboxes (auto / manual)
25 | - Drag and drop
26 | - Customizable style
27 | - Customizable icons
28 |
29 | ## Installation
30 |
31 | ```shell
32 | npm install vue3-treeview
33 | yarn add vue3-treeview
34 | ```
35 |
36 | ## Play with repo
37 |
38 | * Clone this repository
39 | * `yarn install`
40 | * `yarn dev`
41 |
42 | ## License
43 |
44 | [MIT](https://github.com/N00ts/vue3-treeview/blob/master/LICENSE)
45 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env', {targets: {node: 'current'}}],
5 | '@babel/preset-typescript',
6 | ],
7 | plugins: ["dynamic-import-node"]
8 | };
9 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | # abort on errors
4 | set -e
5 |
6 | # build
7 | npm run docs:build
8 |
9 | # navigate into the build output directory
10 | cd docs/.vitepress/dist
11 |
12 | git init
13 | git add -A
14 | git commit -m 'deploy'
15 | git push -f git@github.com:N00ts/vue3-treeview.git master:gh-pages
16 |
17 | cd -
--------------------------------------------------------------------------------
/docs/.vitepress/config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 |
3 | module.exports = {
4 | title: "vue3-treeview",
5 | description: "vue3-treeview documentation",
6 | base: "/vue3-treeview/",
7 | themeConfig: {
8 | repo: "N00ts/vue3-treeview",
9 | docsDir: "docs",
10 | editLinks: true,
11 | editLinkText: "Edit this page on GitHub",
12 | nav: [
13 | {
14 | text: "Guide",
15 | link: "/guide/getting-started",
16 | }
17 | ],
18 | sidebar: {
19 | "/guide/": getSidebar()
20 | },
21 | },
22 | alias: {
23 | "@docs": path.resolve(__dirname, "..")
24 | },
25 | };
26 |
27 | function getSidebar() {
28 | return [
29 | {
30 | text: "Introduction",
31 | link: "/guide/getting-started",
32 | },
33 | {
34 | text: "Properties",
35 | link: "/guide/properties",
36 | },
37 | {
38 | text: "Events",
39 | link: "/guide/events",
40 | },
41 | {
42 | text: "Slots",
43 | link: "/guide/slots",
44 | },
45 | {
46 | text: "Async loading",
47 | link: "/guide/async",
48 | },
49 | {
50 | text: "Transitions",
51 | link: '/guide/transitions'
52 | }
53 | ];
54 | }
55 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.js:
--------------------------------------------------------------------------------
1 | import DefaultTheme from 'vitepress/theme';
2 |
3 | export default {
4 | ...DefaultTheme
5 | }
6 |
--------------------------------------------------------------------------------
/docs/guide/async.md:
--------------------------------------------------------------------------------
1 | ## Async loading
2 |
3 | In some case you might have a very large amount of nodes to display.
4 | In this case you can to use "async loading" and update treeview with data coming from the server and have infinite nodes.
5 |
6 | ::: warning
7 | When using async loading, checkbox mode auto will reload all parents state. If the node coming from the server is unchecked it will uncheck all parents when added
8 | :::
9 |
10 |
--------------------------------------------------------------------------------
/docs/guide/events.md:
--------------------------------------------------------------------------------
1 | ## Event list
2 |
3 | | Event | Arguments | Description |
4 | |----------------|------------------------------|-------------------------------------------------|
5 | | node-opened | [node](./properties.md#node) | Triggered when node is opened |
6 | | node-closed | [node](./properties.md#node) | Triggered when node is closed |
7 | | node-focus | [node](./properties.md#node) | Triggered when node is focused |
8 | | node-toggle | [node](./properties.md#node) | Triggered when node is opened or node is closed |
9 | | node-blur | [node](./properties.md#node) | Triggered when blur node text |
10 | | node-edit | [node](./properties.md#node) | Triggered when node text is getting focused |
11 | | node-checked | [node](./properties.md#node) | Triggered when node is checked |
12 | | node-unchecked | [node](./properties.md#node) | Triggered when node is unchecked |
13 | | node-dragstart | [context](#context) | Trigerred when drag start |
14 | | node-dragenter | [context](#context) | Triggered when drag enter |
15 | | node-dragleave | [context](#context) | Triggered when drag leave |
16 | | node-dragend | [context](#context) | Triggered when drag end |
17 | | node-over | [context](#context) | Triggered when drag over |
18 | | node-drop | [context](#context) | Triggered when drop |
19 |
20 | ::: tip
21 | Try events yourself (go check the console with F12)
22 | :::
23 |
24 |
30 |
31 | ## Context
32 | Drag context, is composed of dragged element and target element which are both "IDragContext"
33 |
34 | | Variable | Type | Description |
35 | |--------------|-------------------------------|------------------------------------|
36 | | dragged | [IDragContext](#idragcontext) | Dragged context |
37 | | target | [IDragContext](#idragcontext) | Target context |
38 | | evt | DragEvent | Drag Event |
39 | | external | Boolean | Determine if the node is external |
40 | | dataTransfer | Object | String | null | Event dataTransfer content |
41 |
42 | ::: warning
43 | In function of the drag event, target may be null
44 | :::
45 |
46 | ## IDragContext
47 |
48 | | Variable | Type | Description |
49 | |----------|------------------------------|---------------------------------------|
50 | | node | [node](./properties.md#node) | Related node |
51 | | element | Element | Html element of the related node |
52 | | wrapper | Element | Wrapper of the related node's element |
53 | | parentId | String | Id of parent node |
54 |
55 |
--------------------------------------------------------------------------------
/docs/guide/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | a vue 3 pluggin to display treeview
4 |
5 | ### Installation
6 |
7 | ``` sh
8 | npm install vue3-treeview
9 | ```
10 |
11 | ### Usage
12 |
13 | ``` js
14 | import Tree from "vue3-treeview";
15 | ```
16 |
17 | ### Styling
18 |
19 | ::: tip
20 | By default the project comes without any styling. A default stylesheet is included in the project.
21 | :::
22 |
23 | ::: warning
24 | In the following documentation all examples will use default styling
25 | :::
26 |
27 | To import the default stylesheet:
28 |
29 | ``` js
30 | import "vue3-treeview/dist/style.css";
31 | ```
32 |
--------------------------------------------------------------------------------
/docs/guide/properties.md:
--------------------------------------------------------------------------------
1 | ## Tree
2 |
3 | | Prop | Type | Default | Required | Description |
4 | |--------|----------------------------------|---------|----------|--------------------|
5 | | nodes | [Object](#nodes) | {} | true | Nodes |
6 | | config | [IConfiguration](#configuration) | {} | true | Tree configuration |
7 |
8 |
14 |
15 | ## Nodes
16 | Type: `{ [id]: Node }`
17 | Default: Empty Object
18 |
19 | > If no node defined nothing will be displayed
20 | ``` js
21 | {
22 | id1: {
23 | text: "textid1",
24 | children: ["id11"],
25 | state: { ... }
26 | },
27 | id11: {
28 | text: "textid11",
29 | children: [],
30 | state: { ... }
31 | },
32 | id2: {
33 | text
34 | }
35 | }
36 | ```
37 |
38 | ## Node
39 | Type: `Object`
40 | Default: Empty Object
41 |
42 | > A node has the following structure:
43 |
44 | | Prop | Type | Default | Required | Description |
45 | |----------|----------------------|---------|----------|----------------------------|
46 | | text | String | "" | false | Text displayed in the node |
47 | | children | Array | [] | false | Array of children |
48 | | state | [INodeState](#state) | null | false | State of the node |
49 |
50 | ``` js
51 | {
52 | text: "text example",
53 | children: [ "childrenid1", "childrenid2" ],
54 | state: {
55 | opened: true,
56 | disabled: false
57 | ...
58 | }
59 | }
60 | ```
61 |
62 | ## State
63 | Type: `Object`
64 | Default: Empty Object
65 |
66 | | Prop | Type | Default | Required | Description |
67 | |---------------|---------|---------|----------|--------------------------------------------------|
68 | | opened | Boolean | false | false | Open or close the node |
69 | | disabled | Boolean | false | false | Disable checkbox, node edition and Drag and drop |
70 | | editable | Boolean | true | false | Node field can be edited |
71 | | draggable | Boolean | true | false | Determine if a node is draggable or not |
72 | | dropable | Boolean | true | false | Determine if a node is dropable or not |
73 | | checked | Boolean | false | false | Node checkbox state |
74 | | indeterminate | Boolean | false | false | Node checkbox indeterminate state |
75 | | isLoading | Boolean | false | false | Used for [async loading](./async.md) |
76 |
77 | ## Configuration
78 |
79 | Tree Configuration
80 | Type: `Object`
81 | Default: Empty Object
82 |
83 | | Prop | Type | Default | Required | Description |
84 | |--------------------|-----------------|-----------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
85 | | roots | Array | [] | true | Roots of the tree, this property is mandaroty to build the tree |
86 | | leaves | Array | [] | false | Leaves of the tree, if empty leaves will be nodes without children. Leaves does not have any Open / Close icon |
87 | | padding | Number | 25 | false | Padding for each new level |
88 | | editable | Boolean | false | false | Determine globally if nodes are editable. When false, no node is editable even if node state is editable |
89 | | editing | String | null | false | The id of the current editing node |
90 | | editableClass | String | "editable" | false | Customize node class when node is editable |
91 | | checkboxes | Boolean | false | false | Show or hide checkboxes |
92 | | checkMode | String | "manual" | false | Checkmode can be **"manual"** or **"auto"**. When auto mode is enabled, it triggers an event to check children |
93 | | dragAndDrop | Boolean | false | false | Enable or disable globally drag and drop |
94 | | keyboardNavigation | Boolean | false | false | Enable or disable keyboard navigation.
**enter**: edit
**esc**: stop edit
**up**: previous
**down**: next
**space**: check |
95 | | disabled | Boolean | false | false | Disable all tree nodes |
96 | | disabledClass | String | "disabled" | false | Customize node class when node is disabled |
97 | | openedIcon | [IIcon](#icons) | {} | false | Customize icon when node is opened |
98 | | closedIcon | [IIcon](#icons) | {} | false | Customize icon when node is closed |
99 | | focusClass | String | "focused" | false | Customize node class when node is focused |
100 | | checkedClass | String | "checked" | false | Customize node class when node is checked |
101 | | indeterminateClass | String | "indeterminate" | false | Customize node class when node is indeterminate |
102 |
103 |
109 |
110 | ## Icons
111 | Type: `Object`
112 | Default: Empty Object
113 |
114 | Open and close icons are customizable
115 | Icons can be:
116 | - "shape" you can import a custom SVG shape.
117 | - "class" class svg icon (fontawsome like)
118 | - "image" a classic image coming from web or local image
119 |
120 | The following table describe properties by "type" but they are all included in the same interface
121 |
122 | ### shape
123 |
124 | Shape is a custom drawn shape.
125 | For more information you can consult [SVG icon docs](https://developer.mozilla.org/en-US/docs/Web/SVG).
126 |
127 | | Prop | Type | Default | Required | Description |
128 | |---------|-----------------|---------|----------|--------------------------------------------------------------------|
129 | | type | String | "shape" | false | type can be "shape", "class", "img" |
130 | | width | Number | null | false | width of the icon |
131 | | height | Number | null | false | height of the icon |
132 | | class | String / Array | null | false | Even if your icon is drawn you can add a class to it |
133 | | style | String / Object | null | false | Inline icon style |
134 | | viewbox | String | null | false | Viewbox of the drawn icon, for more information check svg icon doc |
135 | | d | String | null | false | Icon drawn coordinates |
136 | | fill | String | null | false | Fill color of the svg icon |
137 | | stroke | String | null | false | SVG icon stroke property |
138 |
139 |
145 |
146 | ### Class
147 |
148 | A simple way to use svg icons is to design them with classes.
149 | To se how it work you can check [Front Awsome](https://fontawesome.com/)
150 |
151 | | Prop | Type | Default | Required | Description |
152 | |-------|-----------------|---------|----------|-------------------------------------|
153 | | type | String | "class" | false | type can be "shape", "class", "img" |
154 | | class | String / Array | null | false | The corresponding svg class |
155 | | style | String / Object | | | |
156 |
157 |
163 |
164 | ### img
165 |
166 | You can also decide to use an image as Icon.
167 |
168 | | Prop | Type | Default | Required | Description |
169 | |--------|-----------------|---------|----------|--------------------------------------------------------|
170 | | type | String | "img" | false | type can be "shape", "class", "img" |
171 | | src | String | null | false | The image source |
172 | | alt | String | null | false | The alt tag |
173 | | width | Number | null | false | width of the icon |
174 | | height | Number | null | false | height of the icon |
175 | | class | String / Array | null | false | ven if your icon is an image you can add a class to it |
176 | | style | String / Object | null | false | Inline icon style |
177 |
178 |
--------------------------------------------------------------------------------
/docs/guide/slots.md:
--------------------------------------------------------------------------------
1 | ## Slots
2 |
3 | The treeview has two slots:
4 | - before-input
5 | - after-input
6 | - [loading-slot](./async.md)
7 |
8 | ### before-input / after-input
9 |
10 | They are normal slots that can be used to add some inline content to the treeview.
11 |
12 | The argument of the slots is a [node](./properties#node).
13 |
14 |
--------------------------------------------------------------------------------
/docs/guide/transitions.md:
--------------------------------------------------------------------------------
1 | ## Transitions
2 |
3 | :::warning
4 | If you are not comfortable with transition in vue 3 please check [this link](https://v3.vuejs.org/guide/transitions-enterleave.html#transitioning-single-elements-components) first.
5 | :::
6 |
7 | If you want to make some fancy open/close effects. vue3-treeview provide a transition on levels.
8 |
9 |
15 |
16 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Vue-3-Treeview
2 |
3 | 
4 |
5 | `Vue-3-Treeview` is a simple treeview with all the required feature.
6 |
7 | To get start, please navigate to the [Guide](guide/getting-started.md) section.
8 |
--------------------------------------------------------------------------------
/docs/public/add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/N00ts/vue3-treeview/817381498255900a94e2a32c84ebf459ec11c3b2/docs/public/add.png
--------------------------------------------------------------------------------
/docs/public/minus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/N00ts/vue3-treeview/817381498255900a94e2a32c84ebf459ec11c3b2/docs/public/minus.png
--------------------------------------------------------------------------------
/docs/public/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/N00ts/vue3-treeview/817381498255900a94e2a32c84ebf459ec11c3b2/docs/public/screenshot.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
3 | moduleDirectories: [
4 | 'node_modules'
5 | ],
6 | moduleFileExtensions: [
7 | 'js',
8 | 'ts',
9 | 'json',
10 | 'vue'
11 | ],
12 | transform: {
13 | '^.+\\.vue$': 'vue-jest',
14 | '\\.(js|ts|jsx|tsx)$': 'babel-jest',
15 | },
16 | transformIgnorePatterns: [
17 | "[/\\\\]node_modules[/\\\\](?!lodash-es/).+\\.js$"
18 | ],
19 | collectCoverage: true,
20 | collectCoverageFrom: [
21 | "src/**/*.ts",
22 | "!src/dev.js",
23 | "!src/dev.vue",
24 | "!src/index.ts",
25 | "!src/structure/**",
26 | "!src/shims-vue.d.ts",
27 | "!**/node_modules/**",
28 | ],
29 | coverageReporters: ["json-summary", "json", "lcov", "text", 'html'],
30 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue3-treeview",
3 | "version": "0.4.2",
4 | "private": false,
5 | "library": "vue3-treeview",
6 | "license": "MIT",
7 | "description": "A simple vue js 3 treeview component",
8 | "author": "Tristan Fontaine ",
9 | "scripts": {
10 | "test": "jest",
11 | "dev": "vite",
12 | "build": "vite build",
13 | "types": "tsc --emitDeclarationOnly",
14 | "bundle": "yarn run build && yarn run types",
15 | "lint": "eslint ./src --ext .vue,.js,.ts",
16 | "lint-fix": "eslint --fix ./src --ext .vue,.js,.ts",
17 | "all": "yarn run test && yarn run lint && yarn run bundle",
18 | "docs:dev": "vitepress dev docs",
19 | "docs:build": "vitepress build docs"
20 | },
21 | "main": "dist/vue3-treeview.umd.js",
22 | "module": "dist/vue3-treeview.es.js",
23 | "style": "dist/style.css",
24 | "types": "dist/**.d.ts",
25 | "files": [
26 | "dist/*"
27 | ],
28 | "dependencies": {
29 | "lodash.eq": "^4.0.0",
30 | "lodash.isnil": "^4.0.0",
31 | "lodash.tointeger": "^4.0.4",
32 | "lodash.uniqueid": "^4.0.1",
33 | "vue": "^3.4.4"
34 | },
35 | "devDependencies": {
36 | "@babel/preset-typescript": "^7.14.5",
37 | "@types/jest": "^24.0.19",
38 | "@typescript-eslint/eslint-plugin": "^6.17.0",
39 | "@typescript-eslint/parser": "^6.17.0",
40 | "@vue/cli-plugin-babel": "^4.5.12",
41 | "@vue/cli-plugin-eslint": "^4.5.12",
42 | "@vue/cli-plugin-typescript": "^4.5.12",
43 | "@vue/cli-plugin-unit-jest": "~4.5.0",
44 | "@vue/cli-service": "^4.5.12",
45 | "@vue/compiler-sfc": "^3.0.11",
46 | "@vue/eslint-config-typescript": "^5.0.2",
47 | "@vue/test-utils": "^2.0.0-0",
48 | "eslint": "^8.56.0",
49 | "eslint-plugin-vue": "^7.10.0",
50 | "istanbul-badges-readme": "^1.4.0",
51 | "typescript": "^5.3.3",
52 | "vite": "^2.6.1",
53 | "vitepress": "^0.15.6",
54 | "vue-jest": "^5.0.0-alpha.10"
55 | },
56 | "eslintConfig": {
57 | "root": true,
58 | "env": {
59 | "node": true
60 | },
61 | "extends": [
62 | "plugin:vue/vue3-strongly-recommended",
63 | "eslint:recommended",
64 | "@vue/typescript"
65 | ],
66 | "parserOptions": {
67 | "parser": "@typescript-eslint/parser"
68 | },
69 | "rules": {
70 | "no-unused-vars": "off",
71 | "vue/no-ref-as-operand": "off"
72 | }
73 | },
74 | "browserslist": [
75 | "> 1%",
76 | "last 2 versions",
77 | "not dead"
78 | ],
79 | "css": "dist/vue3-treeview.css",
80 | "keywords": [
81 | "treeview",
82 | "vue",
83 | "vue3",
84 | "javascript"
85 | ],
86 | "repository": {
87 | "type": "git",
88 | "url": "git+https://github.com/N00ts/vue3-treeview.git"
89 | },
90 | "bugs": {
91 | "url": "https://github.com/N00ts/vue3-treeview/issues"
92 | },
93 | "homepage": "https://github.com/N00ts/vue3-treeview#readme",
94 | "directories": {
95 | "doc": "docs",
96 | "test": "tests"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/N00ts/vue3-treeview/817381498255900a94e2a32c84ebf459ec11c3b2/public/favicon.ico
--------------------------------------------------------------------------------
/public/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/N00ts/vue3-treeview/817381498255900a94e2a32c84ebf459ec11c3b2/public/screenshot.png
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/N00ts/vue3-treeview/817381498255900a94e2a32c84ebf459ec11c3b2/src/assets/logo.png
--------------------------------------------------------------------------------
/src/components/Icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
25 |
26 |
35 |
36 |
46 |
--------------------------------------------------------------------------------
/src/components/Tree.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
12 |
13 |
17 |
18 |
19 |
20 |
24 |
25 |
26 |
27 |
31 |
32 |
33 |
34 |
35 |
36 |
68 |
69 |
--------------------------------------------------------------------------------
/src/components/TreeIcons.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
14 |
15 |
16 |
20 |
21 |
--------------------------------------------------------------------------------
/src/components/TreeLevel.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
15 |
16 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
65 |
--------------------------------------------------------------------------------
/src/components/TreeNode.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
31 |
36 |
40 |
41 |
42 |
48 |
56 |
57 |
58 |
62 |
63 |
64 |
74 |
75 |
80 | {{ text }}
81 |
82 |
83 |
84 |
88 |
89 |
90 |
95 |
96 |
97 |
103 |
104 |
108 |
109 |
110 |
111 |
115 |
116 |
117 |
118 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/src/css/material.css:
--------------------------------------------------------------------------------
1 | .tree {
2 | font-family: Roboto,Helvetica Neue,sans-serif;
3 | width: 100%;
4 | }
5 |
6 | .tree-level {
7 | flex: 1;
8 | }
9 |
10 | .node-checkbox {
11 | display: none;
12 | }
13 |
14 | .icon-wrapper {
15 | padding: 0;
16 | min-width: 0;
17 | flex-shrink: 0;
18 | border-radius: 50%;
19 | display: flex;
20 | align-items: center;
21 | justify-content: center;
22 | height: 24px;
23 | width: 24px;
24 | }
25 |
26 | .node-wrapper:hover {
27 | background-color: #EEEEEE;
28 | }
29 |
30 | .node-wrapper:focus {
31 | border: 0;
32 | background-color: #E0E0E0;
33 | }
34 |
35 | .input-wrapper {
36 | margin-left: 0.75em;
37 | }
38 |
39 | .icon-wrapper svg {
40 | height: 12px;
41 | width: 12px;
42 | }
43 |
44 | .node-wrapper {
45 | min-height: 32px;
46 | display: flex;
47 | align-items: center;
48 | flex: 1;
49 | word-wrap: break-word;
50 | font-weight: 400;
51 | font-size: 14px;
52 | }
53 |
54 | .checkbox-wrapper {
55 | width: 1.25em;
56 | height: 1.25em;
57 | }
58 |
59 | .checkbox-wrapper {
60 | position: relative;
61 | margin-left: 0.3em;
62 | overflow: hidden;
63 | }
64 |
65 | .checkbox-wrapper {
66 | background: #fff;
67 | border: 2px solid rgba(0, 0, 0, 0.54);
68 | border-radius: 0.125em;
69 | cursor: pointer;
70 | transition: background 0.3s;
71 | }
72 |
73 | .checkbox-wrapper.checked, .checkbox-wrapper.indeterminate{
74 | background: #3f51b5;
75 | border: 2px solid #3f51b5;
76 | }
77 |
78 | .node-wrapper.disabled .checkbox-wrapper.checked {
79 | background: rgba(0, 0, 0, 0.26);
80 | }
81 |
82 | .node-wrapper.disabled .checkbox-wrapper.indeterminate {
83 | background: rgba(0, 0, 0, 0.26);
84 | }
85 |
86 | .node-wrapper.disabled .checkbox-wrapper {
87 | border-color: rgba(0, 0, 0, 0.26);
88 | }
89 |
90 | .checkbox-wrapper.checked:after {
91 | transform: translate(0.25em, 0.3365384615em) rotate(-45deg);
92 | width: 0.7em;
93 | height: 0.3em;
94 | border: 0.125em solid #fff;
95 | border-top-style: none;
96 | border-right-style: none;
97 | }
98 |
99 | .checkbox-wrapper.indeterminate:after {
100 | transform: translate(0.25em, 0.3365384615em) rotate(0deg);
101 | width: 0.7em;
102 | height: 0.3em;
103 | border: 0.125em solid #fff;
104 | border-top-style: none;
105 | border-right-style: none;
106 | border-left-style: none;
107 | }
108 |
109 | .checkbox-wrapper:after {
110 | content: "";
111 | position: absolute;
112 | left: 0;
113 | top: 0;
114 | }
115 |
116 | .node-over {
117 | border-top: solid 2px #5C6BC0;
118 | }
119 |
120 | .node-in {
121 | background-color: #BDBDBD;
122 | }
123 |
124 | .node-under {
125 | border-bottom: solid 2px #5C6BC0;
126 | }
127 |
128 | .input-wrapper {
129 | font-size: 14px;
130 | position: relative;
131 | color: #000000;
132 | display: flex;
133 | align-items: center;
134 | }
135 |
136 | .node-input, .node-text {
137 | font-family: 'Roboto', sans-serif;
138 | }
139 |
140 | .input-wrapper:after {
141 | content: "\00A0";
142 | display: block;
143 | color: #d50000;
144 | font-size: 12px;
145 | padding-top: 5px;
146 | }
147 |
148 | .input-wrapper input{
149 | padding: 4px 0 4px 0;
150 | display: block;
151 | width: 100%;
152 | border: none;
153 | font-size: 14px;
154 | color: #000000;
155 | border-bottom: 1px solid #d2d2d2;
156 | outline: none;
157 | background: transparent;
158 | transition: border-color 0.2s;
159 | }
160 |
161 | .input-wrapper input:focus {
162 | outline:none;
163 | color: #3f51b5 ;
164 | padding-bottom: 4px;
165 | border-bottom-width: 3px;
166 | border-bottom-color: #3f51b5;
167 | }
168 |
--------------------------------------------------------------------------------
/src/dev.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue';
2 | import App from './dev.vue';
3 |
4 | createApp(App).mount('#app');
5 |
--------------------------------------------------------------------------------
/src/dev.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
16 |
17 |
18 |
23 |
24 |
25 |
30 |
31 |
32 |
37 |
38 |
39 |
44 |
45 |
46 |
52 |
53 |
54 |
76 |
89 |
90 |
91 |
92 |
93 |
98 |
99 |
100 |
105 |
106 |
107 |
112 |
113 |
116 |
117 | External items
118 |
119 | -
125 | {{ item.title }}
126 |
127 |
128 |
129 |
130 |
317 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import Tree from './components/Tree.vue';
2 |
3 | export default Tree;
--------------------------------------------------------------------------------
/src/misc/default.ts:
--------------------------------------------------------------------------------
1 | import { IConfiguration } from "../structure/IConfiguration";
2 | import { IIcon } from "../structure/IIcon";
3 | import { INodeState } from '../structure/INodeState';
4 |
5 | export const defaultSize = 8;
6 |
7 | export const defaultDisabledClass = "disabled";
8 |
9 | export const defaultFocusClass = "focused";
10 |
11 | export const defaultDragClass = "draggable";
12 |
13 | export const defaultDropClass = "droppable";
14 |
15 | export const defaultOverClass = "node-over";
16 |
17 | export const defaultInClass = "node-in";
18 |
19 | export const defaultUnderClass = "node-under";
20 |
21 | const defaultViewBox = "0 0 123.958 123.959";
22 |
23 | const defaultOpenDraw = `M117.979,28.017h-112c-5.3,0-8,6.4-4.2,10.2l56,56c2.3,2.3,6.1,2.3,8.401,0l56-56C125.979,34.417,123.279,28.017,117.979,28.017z`;
24 |
25 | const defaultCloseDraw = `M38.217,1.779c-3.8-3.8-10.2-1.1-10.2,4.2v112c0,5.3,6.4,8,10.2,4.2l56-56c2.3-2.301,2.3-6.101,0-8.401L38.217,1.779z`;
26 |
27 | const defaultColor = "black";
28 |
29 | export function createDefaultIcon(draw: string): IIcon {
30 | return {
31 | type: "shape",
32 | width: defaultSize,
33 | height: defaultSize,
34 | viewBox: defaultViewBox,
35 | stroke: defaultColor,
36 | fill: defaultColor,
37 | draw: draw,
38 | name: null,
39 | src: null,
40 | alt: null,
41 | style: null,
42 | class: null
43 | };
44 | }
45 |
46 | export const defaultConfig: IConfiguration = {
47 | roots: [],
48 | padding: 16,
49 | editable: false,
50 | editing: null,
51 | checkboxes: false,
52 | dragAndDrop: false,
53 | keyboardNavigation: false,
54 | openedIcon: createDefaultIcon(defaultOpenDraw),
55 | closedIcon: createDefaultIcon(defaultCloseDraw)
56 | };
57 |
58 | export const defaultState: INodeState = {
59 | opened: false,
60 | disabled: false,
61 | draggable: false,
62 | droppable: false,
63 | checked: false,
64 | indeterminate: false
65 | };
--------------------------------------------------------------------------------
/src/misc/helpers.ts:
--------------------------------------------------------------------------------
1 | import isNil from "lodash.isnil";
2 | import { INode } from '../structure/INode';
3 |
4 | export function ensureState(node: INode): void {
5 | if (isNil(node.state)) {
6 | node.state = {};
7 | node.state.checked = false;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/misc/nodeEvents.ts:
--------------------------------------------------------------------------------
1 | export const nodeEvents = {
2 | opened: "nodeOpened",
3 | closed: "nodeClosed",
4 | focus: "nodeFocus",
5 | toggle: "nodeToggle",
6 | blur: "nodeBlur",
7 | edit: "nodeEdit",
8 | };
9 |
10 | export const checkboxEvents = {
11 | checked: "nodeChecked",
12 | unchecked: "nodeUnchecked"
13 | };
14 |
15 | export const dragEvents = {
16 | start: "nodeDragstart",
17 | end: "nodeDragend",
18 | enter: "nodeDragenter",
19 | leave: "nodeDragleave",
20 | over: "nodeOver",
21 | drop: "nodeDrop"
22 | };
--------------------------------------------------------------------------------
/src/setup/checkbox/auto.ts:
--------------------------------------------------------------------------------
1 | import { computed, Ref } from 'vue';
2 | import { INode } from '../../structure/INode';
3 | import IUseCheck from '../../structure/IUseCheck';
4 | import isNil from "lodash.isnil";
5 | import { ensureState } from '../../misc/helpers';
6 |
7 | export default function auto(node: Ref, nodes: Ref<{ [id: string]: INode }>): IUseCheck {
8 | const check = ((v: boolean) => {
9 | node.value.state.checked = v;
10 | });
11 |
12 | const setIndeterminate = ((v: boolean) => {
13 | node.value.state.indeterminate = v;
14 | });
15 |
16 | const ensureCheck = (() => {
17 | ensureState(node.value);
18 | if (node.value.state.checked || !hasChildren.value) {
19 | setIndeterminate(false);
20 | }
21 | })
22 |
23 | const children = computed(() => {
24 | return node.value.children;
25 | });
26 |
27 | const hasChildren = computed(() => {
28 | return !isNil(children.value) && children.value.length > 0 || false;
29 | });
30 |
31 | const states = computed(() => {
32 | if (!hasChildren.value) {
33 | return [];
34 | }
35 |
36 | const res = [];
37 |
38 | for (const c of children.value) {
39 | const child = nodes.value[c];
40 |
41 | if (!isNil(child)) {
42 | ensureState(child);
43 | res.push(child.state);
44 | }
45 | }
46 |
47 | return res;
48 | });
49 |
50 | const checked = computed(() => {
51 | return node.value.state.checked;
52 | });
53 |
54 | const indeterminate = computed(() => {
55 | return node.value.state.indeterminate;
56 | });
57 |
58 | const allChecked = computed(() => {
59 | return states.value.every((x) => x.checked);
60 | });
61 |
62 | const noneChecked = computed(() => {
63 | return states.value.every((x) => !x.checked);
64 | });
65 |
66 | const someChecked = computed(() => {
67 | return !allChecked.value && !noneChecked.value;
68 | });
69 |
70 | const someIndeterminate = computed(() => {
71 | return states.value.some((x) => x.indeterminate);
72 | });
73 |
74 | const recurseDown = ((n: INode) => {
75 | if (!isNil(n.state) && !isNil(n.children)) {
76 | for (const id of n.children) {
77 | const child = nodes.value[id];
78 |
79 | if (!isNil(child)) {
80 | ensureState(child);
81 | child.state.indeterminate = false;
82 | child.state.checked = n.state.checked;
83 | recurseDown(child);
84 | }
85 | }
86 | }
87 | });
88 |
89 | const updateState = (() => {
90 | if (!hasChildren.value) {
91 | return;
92 | }
93 |
94 | if (noneChecked.value && !someIndeterminate.value) {
95 | setIndeterminate(false);
96 | check(false);
97 | return;
98 | }
99 |
100 | if (allChecked.value) {
101 | setIndeterminate(false);
102 | check(true);
103 | return;
104 | }
105 |
106 | setIndeterminate(true);
107 | check(false);
108 | });
109 |
110 | const rebuild = (() => {
111 | ensureCheck();
112 | recurseDown(node.value);
113 | updateState();
114 | });
115 |
116 | const click = (() => {
117 | setIndeterminate(false);
118 | check(!node.value.state.checked);
119 | });
120 |
121 | return {
122 | checked,
123 | indeterminate,
124 | noneChecked,
125 | someChecked,
126 | allChecked,
127 | someIndeterminate,
128 | click,
129 | rebuild,
130 | updateState,
131 | recurseDown
132 | };
133 | }
--------------------------------------------------------------------------------
/src/setup/checkbox/manual.ts:
--------------------------------------------------------------------------------
1 | import { computed, Ref } from 'vue';
2 | import { INode } from '../../structure/INode';
3 | import IUseCheck from '../../structure/IUseCheck';
4 |
5 | export default function manual(node: Ref): IUseCheck {
6 |
7 | const checked = computed(() => {
8 | return node.value.state.checked;
9 | });
10 |
11 | const indeterminate = computed(() => {
12 | return node.value.state.indeterminate || false;
13 | });
14 |
15 | const noneChecked = computed(() => {
16 | return false;
17 | });
18 |
19 | const someChecked = computed(() => {
20 | return false;
21 | });
22 |
23 | const allChecked = computed(() => {
24 | return false;
25 | });
26 |
27 | const someIndeterminate = computed(() => {
28 | return false;
29 | });
30 |
31 | const click = (() => {
32 | node.value.state.checked = !node.value.state.checked;
33 | });
34 |
35 | const rebuild = (() => {
36 | });
37 |
38 | const updateState = (() => {
39 | });
40 |
41 | const recurseDown = (() => {
42 | });
43 |
44 | return {
45 | checked,
46 | indeterminate,
47 | noneChecked,
48 | someChecked,
49 | allChecked,
50 | someIndeterminate,
51 | click,
52 | rebuild,
53 | updateState,
54 | recurseDown
55 | };
56 | }
--------------------------------------------------------------------------------
/src/setup/store.ts:
--------------------------------------------------------------------------------
1 | import { INode } from "../structure/INode";
2 | import { IConfiguration } from '../structure/IConfiguration';
3 | import { toRefs, computed, ComputedRef, Ref, ref } from 'vue';
4 | import { ITreeProps } from '../structure/ITreeProps';
5 | import { IDragContext } from '../structure/IDragContext';
6 | import uniqueId from "lodash.uniqueid";
7 |
8 | export interface IState {
9 | id: string;
10 | nodes: ComputedRef<{ [id: string]: INode }>;
11 | config: ComputedRef;
12 | dragged: Ref;
13 | focusable: Ref;
14 | focusFunc: Map,
15 | }
16 |
17 | export const states: Map = new Map();
18 |
19 | export function createState(props: ITreeProps): string {
20 | const { nodes, config } = toRefs(props);
21 |
22 | const state: IState = {
23 | id: uniqueId(),
24 | nodes: computed(() => nodes.value),
25 | config: computed(() => config.value),
26 | focusable: ref(null),
27 | focusFunc: new Map(),
28 | dragged: ref({
29 | node: null,
30 | element: null,
31 | wrapper: null,
32 | parentId: null
33 | }),
34 | };
35 | states.set(state.id, state);
36 |
37 | return state.id;
38 | }
39 |
--------------------------------------------------------------------------------
/src/setup/useCheckBox.ts:
--------------------------------------------------------------------------------
1 | import { computed, watch, ComputedRef, onMounted, onUpdated } from 'vue';
2 | import { defaultConfig } from '../misc/default';
3 | import { checkboxEvents } from '../misc/nodeEvents';
4 | import auto from '../setup/checkbox/auto';
5 | import eq from "lodash.eq";
6 | import manual from '../setup/checkbox/manual';
7 | import { checkMode } from '../structure/IConfiguration';
8 | import IUseCommon from '../structure/IUseCommon';
9 |
10 | export function useCheckBox(cmn: IUseCommon): {} {
11 | const node = cmn.node;
12 | const config = cmn.config;
13 | const nodes = cmn.state.nodes;
14 |
15 | const mode = computed(() => {
16 | return config.value.checkMode === checkMode.auto ? checkMode.auto : checkMode.manual;
17 | });
18 |
19 | const factory = computed(() => {
20 | return mode.value === checkMode.auto ?
21 | auto(node, nodes) :
22 | manual(node);
23 | });
24 |
25 | watch(mode, (nv: number, ov: number) => {
26 | if (!eq(nv, ov)) {
27 | factory.value.rebuild();
28 | }
29 | });
30 |
31 | const checked = computed(() => {
32 | return factory.value.checked.value;
33 | });
34 |
35 | const indeterminate = computed(() => {
36 | return factory.value.indeterminate.value;
37 | });
38 |
39 | const hasCheckbox = computed(() => {
40 | return config.value.checkboxes || defaultConfig.checkboxes;
41 | });
42 |
43 | const checkedClass = computed(() => {
44 | return [
45 | factory.value.checked.value ? config.value.checkedClass ? config.value.checkedClass : "checked" : null,
46 | factory.value.indeterminate.value ? config.value.indeterminateClass ? config.value.indeterminateClass : "indeterminate" : null
47 | ];
48 | });
49 |
50 | const allChecked = computed(() => {
51 | return factory.value.allChecked.value;
52 | });
53 |
54 | const noneChecked = computed(() => {
55 | return factory.value.noneChecked.value;
56 | });
57 |
58 | const someChecked = computed(() => {
59 | return factory.value.someChecked.value;
60 | });
61 |
62 | const someIndeterminate = computed(() => {
63 | return factory.value.someIndeterminate.value;
64 | });
65 |
66 | watch>([allChecked, noneChecked, someChecked], ([ov1, ov2, ov3]) => {
67 | if (ov1 || ov2 || ov3) {
68 | factory.value.updateState();
69 | }
70 | }, { deep: true });
71 |
72 | watch(someIndeterminate, (nv: boolean, ov: boolean) => {
73 | if (!eq(nv, ov)) {
74 | factory.value.updateState();
75 | }
76 | }, { deep: true });
77 |
78 | const clickCheckbox = (): void => {
79 | if (!cmn.disabled.value) {
80 | factory.value.click();
81 | factory.value.recurseDown(node.value);
82 | cmn.root.emit(checked.value ? checkboxEvents.checked : checkboxEvents.unchecked, node.value);
83 | }
84 | };
85 |
86 | const space = (() => {
87 | if (!cmn.editing.value && config.value.checkboxes && config.value.keyboardNavigation) {
88 | clickCheckbox();
89 | }
90 | });
91 |
92 | return {
93 | checked,
94 | hasCheckbox,
95 | indeterminate,
96 | checkedClass,
97 | space,
98 | clickCheckbox
99 | };
100 | }
--------------------------------------------------------------------------------
/src/setup/useCommon.ts:
--------------------------------------------------------------------------------
1 | import { INodeProps } from "../structure/INodeProps";
2 | import isNil from "lodash.isnil";
3 | import { computed, toRefs, ref, inject } from 'vue';
4 | import IUseCommon from '../structure/IUseCommon';
5 | import { defaultConfig } from '../misc/default';
6 | import { nodeEvents } from '../misc/nodeEvents';
7 | import { ensureState } from '../misc/helpers';
8 | import { IState } from './store';
9 |
10 | export default function useCommon(props: INodeProps): IUseCommon {
11 | const { node } = toRefs(props);
12 | const state = inject("state");
13 | const config = state.config;
14 | const wrapper = ref(null);
15 | const focused = ref(false);
16 |
17 | const root = {
18 | emit: inject<(event: string, ...args: any[]) => void>("emitter")
19 | };
20 |
21 | ensureState(node.value);
22 |
23 | const hasNode = computed(() => {
24 | return !isNil(node);
25 | });
26 |
27 | const hasConfig = computed(() => {
28 | return !isNil(config.value);
29 | });
30 |
31 | const hasState = computed(() => {
32 | return hasNode.value && !isNil(node.value.state);
33 | });
34 |
35 | const disabled = computed(() => {
36 | return config.value.disabled || node.value.state.disabled;
37 | });
38 |
39 | const editable = computed(() => {
40 | return config.value.editable &&
41 | (!isNil(node.value.state.editable) ? node.value.state.editable : true) || defaultConfig.editable;
42 | });
43 |
44 | const editing = computed(() => {
45 | return editable.value && (config.value.editing === node.value.id);
46 | });
47 |
48 | const blur = ((e: MouseEvent) => {
49 | if (e.type === "blur") {
50 | const current = e.currentTarget as HTMLElement;
51 | const related = e.relatedTarget as HTMLElement;
52 |
53 | if (!current.contains(related)) {
54 | config.value.editing = null;
55 | focused.value = false;
56 | root.emit(nodeEvents.blur, e, node.value);
57 | }
58 | }
59 | });
60 |
61 | return {
62 | state,
63 | node,
64 | config,
65 | hasNode,
66 | hasState,
67 | hasConfig,
68 | disabled,
69 | wrapper,
70 | editable,
71 | editing,
72 | focused,
73 | blur,
74 | root,
75 | };
76 | }
--------------------------------------------------------------------------------
/src/setup/useDragAndDrop.ts:
--------------------------------------------------------------------------------
1 | import { INodeProps } from "../structure/INodeProps";
2 | import { computed, ref } from 'vue';
3 | import isNil from "lodash.isnil";
4 | import { INode } from '../structure/INode';
5 | import { dragEvents } from '../misc/nodeEvents';
6 | import IUseCommon from '../structure/IUseCommon';
7 | import { defaultDragClass, defaultDropClass, defaultOverClass, defaultInClass, defaultUnderClass } from '../misc/default';
8 |
9 | export enum DragPosition {
10 | over,
11 | in,
12 | under
13 | }
14 |
15 | export default function useDragAndDrop(cmn: IUseCommon, props: INodeProps): {} {
16 | const node = cmn.node;
17 | const state = cmn.state;
18 | const parentId = ref(props.parentId);
19 | const config = cmn.config;
20 | const nodes = state.nodes;
21 | const dragged = ref(state.dragged);
22 | const wrapper = cmn.wrapper;
23 | const element = ref(null);
24 | const pos = ref(null);
25 |
26 | const draggable = computed(() => {
27 | return !cmn.disabled.value && config.value.dragAndDrop && node.value.state.draggable !== false;
28 | });
29 |
30 | const droppable = computed(() => {
31 | return config.value.dragAndDrop && node.value.state.droppable !== false;
32 | });
33 |
34 | const isDragging = computed(() => {
35 | return !isNil(dragged.value) && !isNil(dragged.value.node);
36 | });
37 |
38 | const isSameNode = computed(() => {
39 | return isDragging.value && dragged.value.node.id === node.value.id;
40 | });
41 |
42 | const isSameParent = computed(() => {
43 | return targetParent.value === draggedParent.value;
44 | });
45 |
46 | const draggedParent = computed(() => {
47 | return !isDragging.value || !dragged.value.parentId ? null : getParent(dragged.value.parentId);
48 | });
49 |
50 | const draggedLvl = computed(() => {
51 | return getLevel(draggedParent.value);
52 | });
53 |
54 | const targetParent = computed(() => {
55 | return !isNil(parentId.value) ? getParent(parentId.value) : null;
56 | });
57 |
58 | const targetLvl = computed(() => {
59 | return getLevel(targetParent.value);
60 | });
61 |
62 | const dragContain = computed(() => {
63 | if (!isDragging.value || !dragged.value.wrapper) {
64 | return false;
65 | }
66 |
67 | return dragged.value.element.contains(element.value);
68 | });
69 |
70 | const context = computed(() => {
71 | return {
72 | dragged: dragged.value,
73 | target: {
74 | node: node.value.id,
75 | element: element.value,
76 | wrapper: wrapper.value,
77 | parentId: parentId.value
78 | }
79 | };
80 | });
81 |
82 | const dragClass = computed(() => {
83 | return [
84 | draggable.value ? defaultDragClass : null,
85 | droppable.value ? defaultDropClass : null,
86 | pos.value === DragPosition.over ? defaultOverClass : null,
87 | pos.value === DragPosition.in ? defaultInClass : null,
88 | pos.value === DragPosition.under ? defaultUnderClass : null
89 | ];
90 | });
91 |
92 | const getParent = ((id: string) => {
93 | return !isNil(id) ? nodes.value[id] : null;
94 | });
95 |
96 | const getLevel = ((node: INode) => {
97 | return !isNil(node) ? node.children : config.value.roots;
98 | });
99 |
100 | const getDataTransfer = (evt: DragEvent) : string | object | null => {
101 | if (!evt || !evt.dataTransfer) return null;
102 | const jsonPayload = evt.dataTransfer.getData("application/json");
103 | if (jsonPayload) return JSON.parse(jsonPayload);
104 | return evt.dataTransfer.getData("text/plain");
105 | };
106 |
107 | const isExternalSrc = (evt: DragEvent) : boolean => {
108 | return evt?.dataTransfer?.items?.length > 0;
109 | };
110 |
111 | const dragstart = (evt: DragEvent): void => {
112 | if (draggable.value) {
113 | dragged.value = {
114 | node: node.value,
115 | element: element.value,
116 | wrapper: wrapper.value,
117 | parentId: parentId.value
118 | };
119 | cmn.root.emit(dragEvents.start, {...context.value, ...eventContext(evt)});
120 | }
121 | };
122 |
123 | const eventContext = (evt: DragEvent) => {
124 | return {
125 | evt,
126 | external: isExternalSrc(evt),
127 | dataTransfer: getDataTransfer(evt)
128 | }
129 | }
130 |
131 | const dragend = (evt: DragEvent): void => {
132 | cmn.root.emit(dragEvents.end, {...context.value, ...eventContext(evt)});
133 | dragged.value = null;
134 | };
135 |
136 | const dragenter = (evt: DragEvent): void => {
137 | cmn.root.emit(dragEvents.enter, {...context.value, ...eventContext(evt)});
138 | };
139 |
140 | const dragleave = (evt: DragEvent): void => {
141 | cmn.root.emit(dragEvents.leave, {...context.value, ...eventContext(evt)});
142 | pos.value = null;
143 | };
144 |
145 | const dragover = (evt: DragEvent): void => {
146 | if (!isSameNode.value && !dragContain.value) {
147 | const external = isExternalSrc(evt);
148 | if (!isDragging.value && !external) return;
149 |
150 | cmn.root.emit(dragEvents.over, {...context.value, ...eventContext(evt)});
151 |
152 | if (!external && wrapper.value) {
153 | const factor = .3;
154 | const y = evt.pageY;
155 | const r = wrapper.value.getBoundingClientRect();
156 | const midPoint = r.top + (r.height / 2);
157 | const midRange = [
158 | midPoint - r.height * factor,
159 | midPoint + r.height * factor
160 | ];
161 |
162 | const idx = draggedLvl.value.indexOf(node.value.id);
163 | const idxDrag = draggedLvl.value.indexOf(dragged.value.node.id);
164 |
165 | if (y < midRange[0] &&
166 | (!isSameParent.value ||
167 | (isSameParent.value && idx !== idxDrag + 1))) {
168 | pos.value = DragPosition.over;
169 | } else if (y > midRange[1] &&
170 | (!isSameParent.value ||
171 | (isSameParent.value && idx !== idxDrag - 1))) {
172 | pos.value = DragPosition.under;
173 | } else {
174 | pos.value = DragPosition.in;
175 | }
176 | }
177 | }
178 | };
179 |
180 | const drop = (evt: DragEvent): void => {
181 | if (!isSameNode.value && !dragContain.value) {
182 | switch(pos.value) {
183 | case DragPosition.over: {
184 | insertAt(0);
185 | break;
186 | }
187 | case DragPosition.under: {
188 | insertAt(1);
189 | break;
190 | }
191 | case DragPosition.in: {
192 | insertIn();
193 | }
194 | }
195 | }
196 |
197 | cmn.root.emit(dragEvents.drop, {...context.value, ...eventContext(evt)});
198 |
199 | pos.value = null;
200 | };
201 |
202 | const insertAt = (i: 0 | 1) => {
203 | if (isDragging.value) {
204 | const dragId = dragged.value.node.id;
205 | const dragIdx = draggedLvl.value.indexOf(dragId);
206 | draggedLvl.value.splice(dragIdx, 1);
207 |
208 | const targetId = node.value.id;
209 | const idx = targetLvl.value.indexOf(targetId);
210 | targetLvl.value.splice(idx + i, 0, dragId);
211 | }
212 | };
213 |
214 | const insertIn = () => {
215 | if (isDragging.value && droppable.value) {
216 | const dragId = dragged.value.node.id;
217 |
218 | if (draggedLvl.value) {
219 | const idx = draggedLvl.value.indexOf(dragId);
220 | draggedLvl.value.splice(idx, 1);
221 | }
222 |
223 | node.value.children.unshift(dragId);
224 | }
225 | };
226 |
227 | return {
228 | pos,
229 | element,
230 | dragClass,
231 | draggable,
232 | droppable,
233 | dragstart,
234 | dragend,
235 | dragenter,
236 | dragleave,
237 | dragover,
238 | drop
239 | };
240 | }
241 |
--------------------------------------------------------------------------------
/src/setup/useIcon.ts:
--------------------------------------------------------------------------------
1 | import { computed, inject, toRefs } from 'vue';
2 | import { defaultConfig, defaultSize } from '../misc/default';
3 | import isNil from 'lodash.isnil';
4 | import { IState } from './store';
5 |
6 | export default function useIcon(props: Record): {} {
7 | const { isLeaf } = toRefs(props);
8 |
9 | const state = inject("state")
10 |
11 | const config = state.config;
12 |
13 | const openedIcon = computed(() => {
14 | return config.value.openedIcon || defaultConfig.openedIcon;
15 | });
16 |
17 | const closedIcon = computed(() => {
18 | return config.value.closedIcon || defaultConfig.closedIcon;
19 | });
20 |
21 | const hasIcons = computed(() => {
22 | return !isNil(closedIcon.value) && !isNil(openedIcon.value);
23 | });
24 |
25 | const useIcons = computed(() => {
26 | return !isLeaf.value && hasIcons.value;
27 | });
28 |
29 | const fakeNodeStyle = computed(() => {
30 | return {
31 | width: `${defaultSize}px`,
32 | height: `${defaultSize}px`
33 | };
34 | });
35 |
36 | return {
37 | hasIcons,
38 | openedIcon,
39 | closedIcon,
40 | useIcons,
41 | fakeNodeStyle
42 | };
43 | }
--------------------------------------------------------------------------------
/src/setup/useInput.ts:
--------------------------------------------------------------------------------
1 | import { computed, nextTick, ref, watch } from 'vue';
2 | import eq from "lodash.eq";
3 | import { nodeEvents } from '../misc/nodeEvents';
4 | import IUseCommon from '../structure/IUseCommon';
5 |
6 | export default function useInput(cmn: IUseCommon): {} {
7 | const node = cmn.node;
8 | const config = cmn.config;
9 | const wrapper = cmn.wrapper;
10 | const editable = cmn.editable;
11 | const editing = cmn.editing;
12 | const input = ref(null);
13 |
14 | const text = computed({
15 | get: () => node.value.text,
16 | set: (val: string) => node.value.text = val
17 | });
18 |
19 | const editableClass = computed(() => {
20 | if (!cmn.editable.value) {
21 | return null;
22 | }
23 |
24 | return config.value.editableClass ? config.value.editableClass : "editable";
25 | });
26 |
27 | watch(editing, (nv: boolean, ov: boolean) => {
28 | if (!eq(nv, ov) && nv) {
29 | nextTick(() => {
30 | input.value.focus();
31 | });
32 | }
33 | });
34 |
35 | const focusInput = (() => {
36 | if (editable.value && !cmn.disabled.value) {
37 | config.value.editing = node.value.id;
38 | cmn.root.emit(nodeEvents.edit, node.value);
39 | }
40 | });
41 |
42 | const esc = ((event: Event) => {
43 | if (editable.value && config.value.keyboardNavigation) {
44 | cmn.blur(event);
45 | wrapper.value.focus();
46 | }
47 | });
48 |
49 | const enter = (() => {
50 | if (editable.value && !cmn.disabled.value && config.value.keyboardNavigation) {
51 | focusInput();
52 | }
53 | });
54 |
55 | return {
56 | text,
57 | input,
58 | editing,
59 | editable,
60 | editableClass,
61 | focusInput,
62 | esc,
63 | enter
64 | };
65 | }
--------------------------------------------------------------------------------
/src/setup/useLevel.ts:
--------------------------------------------------------------------------------
1 | import { computed, inject, onBeforeUpdate, onMounted, onRenderTriggered, ref } from "vue";
2 | import isNil from "lodash.isnil";
3 | import toInteger from "lodash.tointeger";
4 | import { defaultConfig } from '../misc/default';
5 | import { INode } from "../structure/INode";
6 | import { IState } from "./store";
7 |
8 | export default function useLevel(props: {parentId: string; depth: number}): {} {
9 | const state = inject("state")
10 | const config = state.config;
11 | const nodes = state.nodes;
12 | const depth = ref(props.depth);
13 | const parent = ref(props.parentId);
14 |
15 | const level = computed(() => {
16 | const res: INode[] = [];
17 |
18 | if (isNil(parent.value) && config.value.roots && depth.value === 0) {
19 | for (const id of config.value.roots) {
20 | addNode(id, res);
21 | }
22 |
23 | return res;
24 | }
25 |
26 | if (!isNil(parent.value)) {
27 | const node = nodes.value[parent.value];
28 |
29 | if (node && node.children && node.children.length > 0) {
30 | for (const id of node.children) {
31 | addNode(id, res);
32 | }
33 | }
34 |
35 | return res;
36 | }
37 |
38 | return [];
39 | });
40 |
41 | const addNode = ((id: string, a: INode[]) => {
42 | if (nodes.value[id]) {
43 | nodes.value[id].id = id;
44 | nodes.value[id].parent = parent.value;
45 | a.push(nodes.value[id]);
46 | }
47 | });
48 |
49 | const id = computed(() => {
50 | return new Date().valueOf();
51 | });
52 |
53 | const padding = computed(() => {
54 | if (depth.value === 0) {
55 | return 0;
56 | }
57 |
58 | if (isNil(config.value.padding)) {
59 | return defaultConfig.padding;
60 | }
61 |
62 | const p = toInteger(config.value.padding);
63 |
64 | return p >=0 ? p : 0;
65 | });
66 |
67 | const style = computed(() => {
68 | return {
69 | "padding-left": `${padding.value}px`,
70 | "list-style": "none"
71 | };
72 | });
73 |
74 | return {
75 | id,
76 | level,
77 | padding,
78 | style
79 | };
80 | }
--------------------------------------------------------------------------------
/src/setup/useNode.ts:
--------------------------------------------------------------------------------
1 | import { INode } from "../structure/INode";
2 | import { INodeProps } from "../structure/INodeProps";
3 | import IUseNode from "../structure/IUseNode";
4 | import isNil from "lodash.isnil";
5 | import { computed, ref, watch, nextTick, onMounted, onUnmounted } from 'vue';
6 | import { nodeEvents } from '../misc/nodeEvents';
7 | import IUseCommon from '../structure/IUseCommon';
8 | import { defaultDisabledClass, defaultFocusClass } from '../misc/default';
9 |
10 | export function useNode(cmn: IUseCommon, props: INodeProps): IUseNode {
11 | const state = cmn.state;
12 | const node = cmn.node;
13 | const config = cmn.config;
14 | const wrapper = cmn.wrapper;
15 | const editing = cmn.editing;
16 | const level = ref(null);
17 | const depth = ref(props.depth);
18 | const index = ref(props.index);
19 |
20 | if (!node.value.children) {
21 | node.value.children = ref([]).value;
22 | }
23 |
24 | const id = computed(() => {
25 | return hasNode.value && node.value.id;
26 | });
27 |
28 | const hasNode = computed(() => {
29 | return !isNil(node);
30 | });
31 |
32 | const hasState = computed(() => {
33 | return hasNode.value && !isNil(node.value.state);
34 | });
35 |
36 | const roots = computed(() => {
37 | return config.value.roots || [];
38 | });
39 |
40 | const children = computed(() => {
41 | return isNil(node.value.children) ? [] : node.value.children;
42 | });
43 |
44 | const nbChildren = computed(() => {
45 | return children.value.length;
46 | });
47 |
48 | const hasChildren = computed(() => {
49 | return nbChildren.value > 0;
50 | });
51 |
52 | const opened = computed(() => {
53 | return hasState.value && node.value.state.opened || false;
54 | });
55 |
56 | const isLoading = computed(() => {
57 | return hasState.value && node.value.state.isLoading || false;
58 | });
59 |
60 | const displayLoading = computed(() => {
61 | return isLoading.value && !hasChildren.value && opened.value;
62 | });
63 |
64 | const displayLevel = computed(() => {
65 | return !isLoading.value && hasChildren.value && opened.value;
66 | });
67 |
68 | const style = computed(() => {
69 | return {
70 | display: "flex"
71 | };
72 | });
73 |
74 | const disabledClass = computed(() => {
75 | if (!cmn.disabled.value) {
76 | return null;
77 | }
78 |
79 | return config.value.disabledClass ? config.value.disabledClass : defaultDisabledClass;
80 | });
81 |
82 | const hideIcons = computed(() => {
83 | for (const id of roots.value) {
84 | const node = state.nodes.value[id];
85 |
86 | if (node.children && node.children.length > 0) {
87 | return false;
88 | }
89 | }
90 |
91 | return true;
92 | });
93 |
94 | const isLeaf = computed(() => {
95 | if (config.value.leaves instanceof Array) {
96 | const arr: string[] = config.value.leaves;
97 | const idx = arr.indexOf(id.value);
98 | return Number.isFinite(idx) && idx >= 0;
99 | }
100 |
101 | return !hasChildren.value;
102 | });
103 |
104 | const isFocusable = computed(() => {
105 | return state.focusable.value === node.value.id;
106 | });
107 |
108 | const tabIndex = computed(() => {
109 | if (depth.value === 0 && index.value === 0 && isNil(state.focusable.value)) {
110 | return 0;
111 | }
112 |
113 | return isFocusable.value ? 0 : -1;
114 | });
115 |
116 | const focusClass = computed(() => {
117 | if (!cmn.focused.value) {
118 | return null;
119 | }
120 |
121 | return config.value.focusClass ? config.value.focusClass : defaultFocusClass;
122 | });
123 |
124 | watch(opened, (nv: boolean) => {
125 | nv ? cmn.root.emit(nodeEvents.opened, node.value) : cmn.root.emit(nodeEvents.closed, node.value);
126 | });
127 |
128 | const focus = (() => {
129 | state.focusable.value = node.value.id;
130 |
131 | nextTick(() => {
132 | wrapper.value.focus();
133 | cmn.focused.value = true;
134 | cmn.root.emit(nodeEvents.focus, node.value);
135 | });
136 | });
137 |
138 | const toggle = (() => {
139 | node.value.state.opened = !node.value.state.opened;
140 | cmn.root.emit(nodeEvents.toggle, node.value);
141 | });
142 |
143 | const right = (() => {
144 | if (!editing.value && config.value.keyboardNavigation) {
145 | node.value.state.opened = true;
146 | }
147 | });
148 |
149 | const left = (() => {
150 | if (!editing.value && config.value.keyboardNavigation) {
151 | node.value.state.opened = false;
152 | }
153 | });
154 |
155 | const move = ((getFunc: (s: string) => string) => {
156 | const id = getFunc(node.value.id);
157 |
158 | if (!isNil(id) && config.value.keyboardNavigation) {
159 | const f = state.focusFunc.get(id);
160 |
161 | if(f) {
162 | f();
163 | }
164 | }
165 | });
166 |
167 | const up = () => move(prevVisible);
168 |
169 | const prev = ((id: string): string => {
170 | const n = state.nodes.value[id];
171 |
172 | if (n.children && n.children.length > 0) {
173 | const idx = n.children.indexOf(node.value.id);
174 | const prev = n.children[idx - 1];
175 |
176 | if (!isNil(prev)) {
177 | return lastChild(prev);
178 | }
179 | }
180 |
181 | return n.id;
182 | });
183 |
184 | const prevVisible = ((id: string) => {
185 | const n = state.nodes.value[id];
186 | const p = state.nodes.value[n.parent];
187 |
188 | if (!p) {
189 | const idx = roots.value.indexOf(id);
190 | return lastChild(roots.value[idx - 1]) || null;
191 | }
192 |
193 | return prev(p.id);
194 | });
195 |
196 | const lastChild = ((id: string): string => {
197 | const n = state.nodes.value[id];
198 |
199 | if (!n) {
200 | return null;
201 | }
202 |
203 | if (n.children && n.children.length > 0 && n.state.opened) {
204 | const last = n.children[n.children.length - 1];
205 |
206 | if (!isNil(last)) {
207 | return lastChild(last);
208 | }
209 | }
210 |
211 | return n.id;
212 | });
213 |
214 | const down = () => move(nextVisible);
215 |
216 | const nextRoot = ((id: string) => {
217 | const idx = roots.value.indexOf(id);
218 | return roots.value[idx + 1] || null;
219 | });
220 |
221 | const next = ((p: INode, id: string): string => {
222 | const idx = p.children.indexOf(id);
223 |
224 | if (p.children[idx + 1]) {
225 | return p.children[idx + 1];
226 | }
227 |
228 | if (p.parent) {
229 | return next(state.nodes.value[p.parent], p.id);
230 | }
231 |
232 | return nextRoot(p.id);
233 | });
234 |
235 | const nextVisible = ((id: string): string => {
236 | const n = state.nodes.value[id];
237 |
238 | if (n.children && n.children.length > 0 && n.state.opened) {
239 | return n.children[0];
240 | }
241 |
242 | const p = state.nodes.value[n.parent];
243 |
244 | return p ? next(p, id) : nextRoot(id);
245 | });
246 |
247 | onMounted(() => {
248 | state.focusFunc.set(node.value.id, focus);
249 | });
250 |
251 | onUnmounted(() => {
252 | state.focusFunc.delete(node.value.id);
253 | });
254 |
255 | return {
256 | id,
257 | level,
258 | style,
259 | opened,
260 | hasNode,
261 | hideIcons,
262 | hasChildren,
263 | tabIndex,
264 | focusClass,
265 | disabledClass,
266 | isLeaf,
267 | isLoading,
268 | displayLoading,
269 | displayLevel,
270 | right,
271 | left,
272 | up,
273 | down,
274 | toggle,
275 | focus,
276 | prevVisible,
277 | nextVisible
278 | };
279 | }
--------------------------------------------------------------------------------
/src/setup/useTree.ts:
--------------------------------------------------------------------------------
1 | import { ref, computed, provide, onUnmounted } from 'vue';
2 | import { createState, states } from './store';
3 |
4 | export default function useTree(props: any, emit: (event: string, ...args: any[]) => void): {} {
5 | const element = ref(null);
6 |
7 | const id = createState(props);
8 |
9 | const state = states.get(id);
10 |
11 | provide("emitter", emit);
12 |
13 | provide("state", state);
14 |
15 | const style = computed(() => {
16 | return {
17 | "display": "flex",
18 | "align-items": "center"
19 | };
20 | });
21 |
22 | onUnmounted(() => {
23 | states.delete(id);
24 | })
25 |
26 | return {
27 | element,
28 | style
29 | };
30 | }
--------------------------------------------------------------------------------
/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import { defineComponent } from 'vue';
3 | const component: ReturnType;
4 | export default component;
5 | }
6 |
--------------------------------------------------------------------------------
/src/structure/IConfiguration.ts:
--------------------------------------------------------------------------------
1 | import { IIcon } from './IIcon';
2 |
3 | export enum checkMode {
4 | auto,
5 | manual
6 | }
7 |
8 | export interface IConfiguration {
9 | roots: string[];
10 | leaves?: string[];
11 | padding?: number;
12 | editable?: boolean;
13 | editing?: string;
14 | editableClass?: string;
15 | checkboxes?: boolean;
16 | checkMode?: checkMode;
17 | dragAndDrop?: boolean;
18 | keyboardNavigation?: boolean;
19 | disabled?: boolean;
20 | disabledClass?: string;
21 | openedIcon?: IIcon;
22 | closedIcon?: IIcon;
23 | focusClass?: string;
24 | checkedClass?: string;
25 | indeterminateClass?: string;
26 | }
27 |
--------------------------------------------------------------------------------
/src/structure/IDragContext.ts:
--------------------------------------------------------------------------------
1 | import { INode } from './INode';
2 |
3 | export interface IDragContext {
4 | node: INode;
5 | element: HTMLElement;
6 | wrapper: HTMLElement;
7 | parentId: string;
8 | }
9 |
--------------------------------------------------------------------------------
/src/structure/IIcon.ts:
--------------------------------------------------------------------------------
1 | export interface IIcon {
2 | width: number;
3 | height: number;
4 | draw?: string;
5 | name?: string;
6 | stroke?: string;
7 | strokeWidth?: number;
8 | src?: string;
9 | alt?: string;
10 | style?: string | {};
11 | type?: IconType;
12 | class?: string | string[];
13 | viewBox?: string;
14 | fill?: string;
15 | }
16 |
17 | export type IconType = "shape" | "class" | "img";
--------------------------------------------------------------------------------
/src/structure/INode.ts:
--------------------------------------------------------------------------------
1 | import { INodeState } from './INodeState';
2 |
3 | export interface INode {
4 | id?: string;
5 | parent?: string;
6 | text?: string;
7 | children?: string[];
8 | state?: INodeState;
9 | }
10 |
--------------------------------------------------------------------------------
/src/structure/INodeProps.ts:
--------------------------------------------------------------------------------
1 |
2 | import { INode } from './INode';
3 |
4 | export interface INodeProps {
5 | depth: number;
6 | node: INode;
7 | index: number;
8 | parentId: string;
9 | }
--------------------------------------------------------------------------------
/src/structure/INodeState.ts:
--------------------------------------------------------------------------------
1 | export interface INodeState {
2 | opened?: boolean;
3 | disabled?: boolean;
4 | editable?: boolean;
5 | draggable?: boolean;
6 | droppable?: boolean;
7 | checked?: boolean;
8 | indeterminate?: boolean;
9 | isLoading?: boolean;
10 | }
11 |
--------------------------------------------------------------------------------
/src/structure/ITreeProps.ts:
--------------------------------------------------------------------------------
1 | import { IConfiguration } from "./IConfiguration";
2 | import { INode } from "./INode";
3 |
4 | export interface ITreeProps {
5 | nodes: { [id: string]: INode };
6 | config: IConfiguration;
7 | }
--------------------------------------------------------------------------------
/src/structure/IUseCheck.ts:
--------------------------------------------------------------------------------
1 | import { ComputedRef } from "vue";
2 | import { INode } from "./INode";
3 |
4 | export default interface IUseCheck {
5 | indeterminate: ComputedRef;
6 | checked: ComputedRef;
7 | noneChecked: ComputedRef;
8 | someChecked: ComputedRef;
9 | allChecked: ComputedRef;
10 | someIndeterminate: ComputedRef;
11 | click: () => void;
12 | rebuild: () => void;
13 | updateState: () => void;
14 | recurseDown: (node: INode) => void;
15 | }
--------------------------------------------------------------------------------
/src/structure/IUseCommon.ts:
--------------------------------------------------------------------------------
1 | import { ComputedRef, Ref } from '@vue/reactivity';
2 | import { INode } from './INode';
3 | import { IConfiguration } from './IConfiguration';
4 | import { IState } from '../setup/store';
5 |
6 | export default interface IUseCommon {
7 | state: IState;
8 | node: Ref;
9 | config: ComputedRef;
10 | hasNode: ComputedRef;
11 | hasState: ComputedRef;
12 | hasConfig: ComputedRef;
13 | disabled: ComputedRef;
14 | wrapper: Ref;
15 | editable: Ref;
16 | editing: Ref;
17 | focused: Ref;
18 | blur: (e: Event) => void;
19 | root: { emit: (event: string, ...args: any[]) => void };
20 | }
--------------------------------------------------------------------------------
/src/structure/IUseNode.ts:
--------------------------------------------------------------------------------
1 | import { ComputedRef, computed, Component } from 'vue';
2 | import { toRef, Ref } from "@vue/reactivity";
3 |
4 | export default interface IUseNode {
5 | id: ComputedRef;
6 | level: Ref;
7 | style: ComputedRef>;
8 | opened: ComputedRef;
9 | hasNode: ComputedRef;
10 | hideIcons: ComputedRef;
11 | hasChildren: ComputedRef;
12 | tabIndex: ComputedRef;
13 | focusClass: ComputedRef;
14 | disabledClass: ComputedRef;
15 | isLeaf: ComputedRef;
16 | isLoading: ComputedRef;
17 | displayLoading: ComputedRef;
18 | displayLevel: ComputedRef;
19 | toggle: (nv: boolean, ov: boolean) => void;
20 | focus: () => void;
21 | left: () => void;
22 | right: () => void;
23 | up: () => void;
24 | down: () => void;
25 | prevVisible: (id: string) => string;
26 | nextVisible: (id: string) => string;
27 | }
28 |
--------------------------------------------------------------------------------
/tests/unit/auto.spec.ts:
--------------------------------------------------------------------------------
1 | import { reactive, ref } from "vue";
2 | import auto from '../../src/setup/checkbox/auto';
3 |
4 | describe("test auto checkbox", () => {
5 | let node = null;
6 |
7 | let nodes = null;
8 |
9 | let storeProps = null;
10 |
11 | let mode = null;
12 |
13 | beforeEach(() => {
14 | node = ref({
15 | children: ["c1", "c2"],
16 | state: {
17 | checked: false,
18 | indeterminate: false
19 | }
20 | });
21 | nodes = ref({
22 | c1: {
23 | text: "c1",
24 | state: {
25 | checked: false,
26 | indeterminate: false
27 | }
28 | },
29 | c2: {
30 | text: "c2",
31 | state: {
32 | checked: false,
33 | indeterminate: false
34 | }
35 | }
36 | });
37 | storeProps = reactive({
38 | nodes
39 | });
40 | mode = auto(node, nodes);
41 | });
42 |
43 | it("Expect not to be checked", () => {
44 | expect(mode.checked.value).toBeFalsy();
45 | });
46 |
47 | it("Expect not to be indeterminate", () => {
48 | expect(mode.indeterminate.value).toBeFalsy();
49 | });
50 |
51 | it("Expect none to be checked", () => {
52 | expect(mode.noneChecked.value).toBeTruthy();
53 | });
54 |
55 | it("Expect not all to be checked" , () => {
56 | expect(mode.allChecked.value).toBeFalsy();
57 | });
58 |
59 | it("Expect to no have some indeterminate", () => {
60 | expect(mode.someIndeterminate.value).toBeFalsy();
61 | });
62 |
63 | it("Expect not some checked" , () => {
64 | expect(mode.someChecked.value).toBeFalsy();
65 | });
66 |
67 | it("Expect children to have state", () => {
68 | expect(nodes.value.c1.state).toBeDefined();
69 | expect(nodes.value.c2.state).toBeDefined();
70 | });
71 |
72 | it("Expect to check node", () => {
73 | mode.click();
74 | expect(node.value.state.checked).toBeTruthy();
75 | expect(node.value.state.indeterminate).toBeFalsy();
76 | });
77 |
78 | it("Expect to rebuild", () => {
79 | node.value.state.checked = true;
80 | mode.rebuild();
81 | expect(nodes.value.c1.state.checked).toBeTruthy();
82 | expect(nodes.value.c2.state.checked).toBeTruthy();
83 | });
84 |
85 | it("Expect not to update state", () => {
86 | node.value.children = [];
87 | mode.updateState();
88 | expect(nodes.value.c1.state.checked).toBeFalsy();
89 | expect(nodes.value.c2.state.checked).toBeFalsy();
90 | });
91 |
92 | it("Expect to reset node checked on update state", () => {
93 | node.value.state.checked = true;
94 | node.value.state.indeterminate = true;
95 | mode.updateState();
96 | expect(node.value.state.checked).toBeFalsy();
97 | expect(node.value.state.indeterminate).toBeFalsy();
98 | });
99 |
100 | it("Expect node to be indeterminate after update", () => {
101 | nodes.value.c1.state.indeterminate = true;
102 | mode.updateState();
103 | expect(node.value.state.checked).toBeFalsy();
104 | expect(node.value.state.indeterminate).toBeTruthy();
105 | });
106 |
107 | it("Expect to update nothing", () => {
108 | node.value.children = null;
109 | mode.updateState();
110 | expect(node.value.state.checked).toBeFalsy();
111 | expect(node.value.state.indeterminate).toBeFalsy();
112 | });
113 | });
--------------------------------------------------------------------------------
/tests/unit/default.spec.ts:
--------------------------------------------------------------------------------
1 | import { defaultConfig, defaultState } from '../../src/misc/default';
2 |
3 | test("Check default config", () => {
4 | expect(defaultConfig).toMatchObject({
5 | roots: [],
6 | padding: 16,
7 | editable: false,
8 | editing: null,
9 | checkboxes: false,
10 | dragAndDrop: false,
11 | keyboardNavigation: false,
12 | closedIcon: {
13 | alt: null,
14 | class: null,
15 | draw: `M38.217,1.779c-3.8-3.8-10.2-1.1-10.2,4.2v112c0,5.3,6.4,8,10.2,4.2l56-56c2.3-2.301,2.3-6.101,0-8.401L38.217,1.779z`,
16 | fill: "black",
17 | height: 8,
18 | name: null,
19 | src: null,
20 | stroke: "black",
21 | style: null,
22 | type: "shape",
23 | viewBox: "0 0 123.958 123.959",
24 | width: 8
25 | },
26 | openedIcon: {
27 | alt: null,
28 | class: null,
29 | draw: "M117.979,28.017h-112c-5.3,0-8,6.4-4.2,10.2l56,56c2.3,2.3,6.1,2.3,8.401,0l56-56C125.979,34.417,123.279,28.017,117.979,28.017z",
30 | fill: "black",
31 | height: 8,
32 | name: null,
33 | src: null,
34 | stroke: "black",
35 | style: null,
36 | type: "shape",
37 | viewBox: "0 0 123.958 123.959",
38 | width: 8,
39 | },
40 | });
41 | });
42 |
43 | test("Check default state", () => {
44 | expect(defaultState).toMatchObject({
45 | opened: false,
46 | disabled: false,
47 | draggable: false,
48 | droppable: false,
49 | checked: false,
50 | indeterminate: false
51 | });
52 | });
--------------------------------------------------------------------------------
/tests/unit/helpers.spec.ts:
--------------------------------------------------------------------------------
1 | import { INode } from "../../src/structure/INode";
2 | import { ensureState } from '../../src/misc/helpers';
3 |
4 | test("Expect to initialize state", () => {
5 | const n : INode = {};
6 | ensureState(n);
7 | expect(n.state).toBeDefined();
8 | });
9 |
10 | test("Expect not to initalize state", () => {
11 | const n: INode = {
12 | state: {
13 | checked: true
14 | }
15 | };
16 | ensureState(n);
17 | expect(n.state).toMatchObject(n.state);
18 | });
--------------------------------------------------------------------------------
/tests/unit/nodeEvents.spec.ts:
--------------------------------------------------------------------------------
1 | import { nodeEvents, checkboxEvents, dragEvents } from '../../src/misc/nodeEvents';
2 |
3 | test("Check node Events", () => {
4 | expect(nodeEvents).toMatchObject({
5 | opened: "nodeOpened",
6 | closed: "nodeClosed",
7 | focus: "nodeFocus",
8 | toggle: "nodeToggle",
9 | blur: "nodeBlur",
10 | edit: "nodeEdit",
11 | });
12 | });
13 |
14 | test("Check checkbox Events", () => {
15 | expect(checkboxEvents).toMatchObject({
16 | checked: "nodeChecked",
17 | unchecked: "nodeUnchecked"
18 | });
19 | });
20 |
21 | test("Check drag Events", () => {
22 | expect(dragEvents).toMatchObject({
23 | start: "nodeDragstart",
24 | end: "nodeDragend",
25 | enter: "nodeDragenter",
26 | leave: "nodeDragleave",
27 | over: "nodeOver",
28 | drop: "nodeDrop"
29 | });
30 | });
--------------------------------------------------------------------------------
/tests/unit/store.spec.ts:
--------------------------------------------------------------------------------
1 | import { isRef, isReadonly } from 'vue';
2 | import { createState, states } from '../../src/setup/store';
3 | import { mount } from '@vue/test-utils';
4 |
5 | test("Expect to create store", () => {
6 | const nodes = {
7 | id1: {
8 | text: "text1"
9 | }
10 | };
11 |
12 | const config = {
13 | roots: ["id1"]
14 | };
15 |
16 | const wrapper = mount({
17 | template: "",
18 | props: ["nodes", "config"]
19 | }, {
20 | propsData: {
21 | nodes,
22 | config
23 | }
24 | });
25 |
26 | const id = createState(wrapper.props() as any);
27 |
28 | const state = states.get(id);
29 |
30 | expect(isRef(state.nodes)).toBe(true);
31 | expect(isReadonly(state.nodes)).toBe(true);
32 | expect(state.nodes.value.id1).toMatchObject({text: "text1"});
33 |
34 | expect(isRef(state.config)).toBe(true);
35 | expect(isReadonly(state.config)).toBe(true);
36 | expect(state.config.value).toMatchObject({roots:["id1"]});
37 |
38 | expect(isRef(state.focusable)).toBe(true);
39 | expect(state.focusable.value).toBeNull();
40 |
41 | expect(isRef(state.dragged)).toBe(true);
42 | expect(state.dragged.value).toMatchObject({
43 | node: null,
44 | element: null,
45 | wrapper: null,
46 | parentId: null
47 | });
48 | });
--------------------------------------------------------------------------------
/tests/unit/useCheckBox.spec.ts:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue';
2 | import { useCheckBox } from '../../src/setup/useCheckBox';
3 | import { checkMode } from '../../src/structure/IConfiguration';
4 | import { checkboxEvents } from '../../src/misc/nodeEvents';
5 |
6 | describe("use checkbox tests", () => {
7 | let fakeCmn = null;
8 |
9 | let useTest = null;
10 |
11 | beforeEach(() => {
12 | fakeCmn = {
13 | node: ref({
14 | state: {
15 | checked: false
16 | }
17 | }),
18 | state: {
19 | nodes: ref({})
20 | },
21 | disabled: ref(false),
22 | editing: ref(false),
23 | config: ref({
24 | checkMode: checkMode.manual,
25 | keyboardNavigation: true,
26 | checkedClass: null,
27 | indeterminateClass: null,
28 | checkboxes: true
29 | }),
30 | root: {
31 | emit: jest.fn()
32 | }
33 | };
34 | useTest = useCheckBox(fakeCmn);
35 | });
36 |
37 | it("Expect not to be checked", () => {
38 | expect(useTest.checked.value).toBeFalsy();
39 | });
40 |
41 | it("Expect not to have checkbox", () => {
42 | expect(useTest.hasCheckbox.value).toBeTruthy();
43 | });
44 |
45 | it("Exoect not to be undeterminate", () => {
46 | expect(useTest.indeterminate.value).toBeFalsy();
47 | });
48 |
49 | it("Expect to have no check class", () => {
50 | expect(useTest.checkedClass.value).toMatchObject([
51 | null,
52 | null
53 | ]);
54 | });
55 |
56 | it("Expect to click checkbox", () => {
57 | const spy = jest.spyOn(fakeCmn.root, "emit");
58 | useTest.clickCheckbox();
59 | expect(fakeCmn.node.value.state.checked).toBeTruthy();
60 | expect(spy).toBeCalledWith(checkboxEvents.checked, fakeCmn.node.value);
61 | });
62 |
63 | it("Expect to click on space", () => {
64 | const spy = jest.spyOn(fakeCmn.root, "emit");
65 | useTest.space();
66 | expect(fakeCmn.node.value.state.checked).toBeTruthy();
67 | expect(spy).toBeCalledWith(checkboxEvents.checked, fakeCmn.node.value);
68 | });
69 |
70 | it("Expect to have default check class", () => {
71 | fakeCmn.node.value.state.checked = true;
72 | fakeCmn.node.value.state.indeterminate = true;
73 | expect(useTest.checkedClass.value).toMatchObject([
74 | "checked",
75 | "indeterminate"
76 | ]);
77 | });
78 |
79 | it("Expect to have custom check class", () => {
80 | fakeCmn.config.value.checkedClass = "checkedClass";
81 | fakeCmn.config.value.indeterminateClass = "indeterminateClass";
82 | fakeCmn.node.value.state.checked = true;
83 | fakeCmn.node.value.state.indeterminate = true;
84 | expect(useTest.checkedClass.value).toMatchObject([
85 | "checkedClass",
86 | "indeterminateClass"
87 | ]);
88 | });
89 | });
--------------------------------------------------------------------------------
/tests/unit/useCommon.spec.ts:
--------------------------------------------------------------------------------
1 | import { reactive, ref, inject } from 'vue';
2 | import { createState, states } from '../../src/setup/store';
3 | import useCommon from '../../src/setup/useCommon';
4 |
5 | describe("test useCommon", () => {
6 | let props = null;
7 |
8 | let useTest = null;
9 |
10 | const config = {
11 | roots: ["id1"],
12 | disabled: false,
13 | editing: null
14 | };
15 |
16 | const storeProps = reactive({
17 | nodes: {},
18 | config
19 | });
20 |
21 | const v = require("vue");
22 |
23 | let state = null;
24 |
25 | v.inject = jest.fn((s) => {
26 | return s === "emitter" ? jest.fn() : {
27 | config: ref(config)
28 | }
29 | });
30 |
31 | beforeEach(() => {
32 | props = reactive({
33 | node: ref({
34 | id: "test"
35 | })
36 | });
37 | const id = createState(storeProps);
38 | state = states.get(id);
39 | useTest = useCommon(props);
40 | });
41 |
42 | it("Expect to have state", () => {
43 | expect(props.node.state).toBeDefined();
44 | });
45 |
46 | it("Expect to have node", () => {
47 | expect(useTest.hasNode.value).toBeTruthy();
48 | });
49 |
50 | it("Expect to have config", () => {
51 | expect(useTest.hasConfig.value).toBeTruthy();
52 | });
53 |
54 | it("Expect to have state", () => {
55 | expect(useTest.hasState.value).toBeTruthy();
56 | });
57 |
58 | it("Expect not to be disabled", () => {
59 | expect(useTest.disabled.value).toBeFalsy();
60 | });
61 |
62 | it("Expect not to be editable", () => {
63 | expect(useTest.editable.value).toBeFalsy();
64 | });
65 |
66 | it("Expect not to be edited", () => {
67 | expect(useTest.editing.value).toBeFalsy();
68 | });
69 |
70 | it("Expect to be editable", () => {
71 | state.config.value.editable = true;
72 | props.node.state.editable = true;
73 | expect(useTest.editable.value).toBeTruthy();
74 | });
75 |
76 | it("Expect to be editing", () => {
77 | state.config.value.editable = true;
78 | state.config.value.editing = "test";
79 | props.node.state.editable = true;
80 | expect(useTest.editable.value).toBeTruthy();
81 | });
82 |
83 | it("Expect to blur", () => {
84 | state.config.value.editing = "tata";
85 | const e = {
86 | currentTarget: document.createElement("div"),
87 | relatedTarget: document.createElement("div"),
88 | type: "blur"
89 | };
90 | useTest.blur(e);
91 | expect(state.config.value.editing).toBeNull();
92 | });
93 | });
--------------------------------------------------------------------------------
/tests/unit/useDragAndDrop.spec.ts:
--------------------------------------------------------------------------------
1 | import { reactive, ref } from 'vue';
2 | import useDragAndDrop from '../../src/setup/useDragAndDrop';
3 | import { dragEvents } from '../../src/misc/nodeEvents';
4 | import { DragPosition } from '../../src/setup/useDragAndDrop';
5 | import { createState, states } from '../../src/setup/store';
6 |
7 | describe("test use Drag and Drop", () => {
8 | let fakeCmn = null;
9 |
10 | let useTest = null;
11 |
12 | let wrapper = null;
13 |
14 | let props = null;
15 |
16 | let node = null;
17 |
18 | let nodes = null;
19 |
20 | let node2 = null;
21 |
22 | let c1 = null;
23 |
24 | let c2 = null;
25 |
26 | let c3 = null;
27 |
28 | let config = null;
29 |
30 | let storeProps = null;
31 |
32 | let fakeDragged = null;
33 |
34 | let fakeTarget = null;
35 |
36 | let fakeContext = null;
37 |
38 | let state = null;
39 |
40 | beforeEach(() => {
41 | node = {
42 | text: "id1",
43 | id: "id1",
44 | children: ["id11", "id12"],
45 | state: {}
46 | };
47 | node2 = {
48 | text: "id2",
49 | id: "id2",
50 | children: ["id21"],
51 | state:{}
52 | };
53 | c1 = {
54 | id: "id11",
55 | text: "id11",
56 | parent: "id1",
57 | state: {}
58 | };
59 | c2 = {
60 | id: "id12",
61 | text: "id12",
62 | parent: "id1",
63 | state: {}
64 | };
65 | c3 = {
66 | id: "id21",
67 | text: "id21",
68 | parent: "id2",
69 | state: {}
70 | };
71 | config = ref({
72 | roots: ["id1", "id2"]
73 | });
74 | nodes = {
75 | id1: node,
76 | id11: c1,
77 | id12: c2,
78 | id2: node2,
79 | id21: c3
80 | };
81 | storeProps = reactive({
82 | nodes,
83 | config
84 | });
85 | const id = createState(storeProps);
86 | state = states.get(id);
87 | wrapper = ref(document.createElement("div"));
88 | fakeCmn = {
89 | node: ref(node),
90 | config,
91 | wrapper,
92 | disabled: ref(false),
93 | root: {
94 | emit: jest.fn()
95 | },
96 | state
97 | };
98 | fakeDragged = {
99 | element: null,
100 | node: null,
101 | parentId: null,
102 | wrapper: null
103 | };
104 | fakeTarget = {
105 | element: null,
106 | node: 'id1',
107 | parentId: null,
108 | wrapper: wrapper.value
109 | };
110 | fakeContext = {
111 | dataTransfer: null,
112 | dragged: fakeDragged,
113 | target: fakeTarget,
114 | evt: undefined,
115 | external: false
116 | };
117 | props = {
118 | parentId: ref(null)
119 | };
120 | useTest = useDragAndDrop(fakeCmn, props);
121 | });
122 |
123 | it("Expect not to be draggable", () => {
124 | expect(useTest.draggable.value).toBeFalsy();
125 | });
126 |
127 | it("Expect not to be droppable", () => {
128 | expect(useTest.droppable.value).toBeFalsy();
129 | });
130 |
131 | it("Expect element to be null", () => {
132 | expect(useTest.element.value).toBeNull();
133 | });
134 |
135 | it("Expect to have have basic class", () => {
136 | expect(useTest.dragClass.value).toMatchObject([
137 | null,
138 | null,
139 | null,
140 | null,
141 | null,
142 | ]);
143 | });
144 |
145 | it("Expect to start drag", () => {
146 | config.value.dragAndDrop = ref(true);
147 | const spy = jest.spyOn(fakeCmn.root, "emit");
148 | fakeCmn.node.value.state.draggable = true;
149 | useTest.dragstart();
150 | fakeDragged.node = node;
151 | fakeDragged.wrapper = wrapper.value;
152 | expect(state.dragged.value).toMatchObject(fakeDragged);
153 | expect(spy).toBeCalledWith(dragEvents.start, fakeContext);
154 | });
155 |
156 | it("Expect to emit event on drag end when nothing started", () => {
157 | config.value.dragAndDrop = ref(true);
158 | const spy = jest.spyOn(fakeCmn.root, "emit");
159 | useTest.dragend();
160 | expect(spy).toBeCalledWith(dragEvents.end, fakeContext);
161 | });
162 |
163 | it("Expect to emit on drag enter", () => {
164 | const spy = jest.spyOn(fakeCmn.root, "emit");
165 | useTest.dragenter();
166 | expect(spy).toBeCalledWith(dragEvents.enter, fakeContext);
167 | });
168 |
169 | it("Expect to emit on drag leave", () => {
170 | const spy = jest.spyOn(fakeCmn.root, "emit");
171 | useTest.dragleave();
172 | expect(spy).toBeCalledWith(dragEvents.leave, fakeContext);
173 | });
174 |
175 | it("Expect to be same node", () => {
176 | state.dragged.value = fakeDragged;
177 | fakeDragged.node = node;
178 | useTest.dragover();
179 | });
180 |
181 | it("Expect to drag node 2 in node 1", () => {
182 | node2.state.draggable = true;
183 | state.dragged.value = fakeDragged;
184 | fakeDragged.node = node2;
185 | wrapper.value.getBoundingClientRect = jest.fn(() => {
186 | return {
187 | bottom: 0,
188 | height: 40,
189 | left: 0,
190 | right: 0,
191 | top: 0,
192 | width: 0
193 | };
194 | });
195 | useTest.dragover({ pageY: 20 });
196 | expect(useTest.pos.value).toBe(DragPosition.in);
197 | });
198 |
199 | it("Expect to drag node 2 over node 1", () => {
200 | node2.state.draggable = true;
201 | state.dragged.value = fakeDragged;
202 | fakeDragged.node = node2;
203 | wrapper.value.getBoundingClientRect = jest.fn(() => {
204 | return {
205 | bottom: 0,
206 | height: 40,
207 | left: 0,
208 | right: 0,
209 | top: 0,
210 | width: 0
211 | };
212 | });
213 | useTest.dragover({ pageY: 1 });
214 | expect(useTest.pos.value).toBe(DragPosition.over);
215 | });
216 |
217 | it("Expect to drag child node 3 under node 1", () => {
218 | c3.state.draggable = true;
219 | state.dragged.value = fakeDragged;
220 | fakeDragged.node = c3;
221 | wrapper.value.getBoundingClientRect = jest.fn(() => {
222 | return {
223 | bottom: 0,
224 | height: 40,
225 | left: 0,
226 | right: 0,
227 | top: 0,
228 | width: 0
229 | };
230 | });
231 | useTest.dragover({ pageY: 35 });
232 | expect(useTest.pos.value).toBe(DragPosition.under);
233 | });
234 |
235 | it("Expect to insert child node 2 over node 1", () => {
236 | c3.state.draggable = true;
237 | state.dragged.value = fakeDragged;
238 | fakeDragged.parentId = "id2";
239 | fakeDragged.node = c3;
240 | props.parentId.value = null;
241 | useTest.pos.value = DragPosition.over;
242 | useTest.drop();
243 | expect(config.value.roots).toMatchObject([
244 | "id21", "id1", "id2"
245 | ])
246 | expect(node2.children).toMatchObject([]);
247 | });
248 |
249 | it("Expect to insert child node 2 under node 1", () => {
250 | c3.state.draggable = true;
251 | state.dragged.value = fakeDragged;
252 | fakeDragged.parentId = "id2";
253 | fakeDragged.node = c3;
254 | props.parentId.value = null;
255 | useTest.pos.value = DragPosition.under;
256 | useTest.drop();
257 | expect(config.value.roots).toMatchObject([
258 | "id1", "id21", "id2"
259 | ])
260 | expect(node2.children).toMatchObject([]);
261 | });
262 |
263 | it("Expect to insert child node 2 in node 1", () => {
264 | c3.state.draggable = true;
265 | state.dragged.value = fakeDragged;
266 | fakeDragged.parentId = "id2";
267 | fakeDragged.node = c3;
268 | config.value.dragAndDrop = true;
269 | props.parentId.value = null;
270 | useTest.pos.value = DragPosition.in;
271 | useTest.drop();
272 | expect(node.children).toMatchObject([
273 | "id21", "id11", "id12"
274 | ])
275 | expect(node2.children).toMatchObject([]);
276 | });
277 | });
--------------------------------------------------------------------------------
/tests/unit/useIcon.spec.ts:
--------------------------------------------------------------------------------
1 | import { reactive, ref } from "vue";
2 | import useIcon from '../../src/setup/useIcon';
3 | import { defaultConfig } from '../../src/misc/default';
4 | import { createState, states } from '../../src/setup/store';
5 |
6 | describe("test use icon", () => {
7 | const config = {
8 | openedIcon: null,
9 | closedIcon: null
10 | };
11 |
12 | const nodes = {};
13 |
14 | const storeProps = reactive({
15 | nodes,
16 | config
17 | });
18 |
19 | let props = null;
20 |
21 | let useFake = null;
22 |
23 | let state = null;
24 |
25 | let v = require("vue");
26 |
27 | v.inject = jest.fn(() => state);
28 |
29 | beforeEach(() => {
30 | props = reactive({
31 | isLeaf: ref(false)
32 | });
33 | state = states.get(createState(storeProps as any));
34 | useFake = useIcon(props);
35 | });
36 |
37 | it("Expect to have default openIcon", () => {
38 | expect(useFake.openedIcon.value).toMatchObject(defaultConfig.openedIcon);
39 | });
40 |
41 | it("Expect to have default closeIcon", () => {
42 | expect(useFake.closedIcon.value).toMatchObject(defaultConfig.closedIcon);
43 | });
44 |
45 | it("Expec to have icons", () => {
46 | expect(useFake.hasIcons.value).toBeTruthy();
47 | });
48 |
49 | it("Expect to use icons", () => {
50 | expect(useFake.useIcons.value).toBeTruthy();
51 | });
52 |
53 | it("Expect to have fake no style", () => {
54 | expect(useFake.fakeNodeStyle.value).toMatchObject({
55 | height: "8px",
56 | width: "8px"
57 | });
58 | });
59 |
60 | it("Expect to set opened icons", () => {
61 | config.openedIcon = {
62 | test: "test"
63 | };
64 | expect(useFake.openedIcon.value).toMatchObject({
65 | test: "test"
66 | });
67 | });
68 |
69 | it("Expect to set closed icons", () => {
70 | config.closedIcon = {
71 | test: "test"
72 | };
73 | expect(useFake.closedIcon.value).toMatchObject({
74 | test: "test"
75 | });
76 | });
77 |
78 | it("Expect not to use icons", () => {
79 | props.isLeaf = true;
80 | expect(useFake.useIcons.value).toBeFalsy();
81 | });
82 | });
--------------------------------------------------------------------------------
/tests/unit/useInput.spec.ts:
--------------------------------------------------------------------------------
1 | import { nextTick, ref, Ref } from 'vue';
2 | import useInput from "../../src/setup/useInput";
3 | import { nodeEvents } from '../../src/misc/nodeEvents';
4 |
5 | describe("test useInput", () => {
6 | let fakeCmn = null;
7 |
8 | let useTest = null;
9 |
10 | const wrapper = ref(document.createElement("div"));
11 |
12 | beforeEach(() => {
13 | fakeCmn = {
14 | node: ref({
15 | id: "id",
16 | text: "test"
17 | }),
18 | config: ref({
19 | editableClass: null,
20 | editing: "",
21 | keyboardNavigation: true
22 | }),
23 | wrapper,
24 | editable: ref(false),
25 | editing: ref(false),
26 | disabled: ref(false),
27 | blur: jest.fn(),
28 | root: {
29 | emit: jest.fn()
30 | }
31 | };
32 |
33 | useTest = useInput(fakeCmn);
34 | });
35 |
36 | it("Expect to have test", () => {
37 | expect(useTest.text.value).toBe("test");
38 | });
39 |
40 | it("Expect to set text", () => {
41 | useTest.text.value = "newVal";
42 | expect(fakeCmn.node.value.text).toBe("newVal");
43 | });
44 |
45 | it("Expect to have no class", () => {
46 | expect(useTest.editableClass.value).toBeNull();
47 | });
48 |
49 | it("Expect to have default editableClass", () => {
50 | fakeCmn.editable.value = true;
51 | expect(useTest.editableClass.value).toBe("editable");
52 | });
53 |
54 | it("Expect to have editable class", () => {
55 | fakeCmn.editable.value = true;
56 | fakeCmn.config.value.editableClass = "editableClass";
57 | expect(useTest.editableClass.value).toBe("editableClass");
58 | });
59 |
60 | it("Epect to focus input element", async () => {
61 | const input = document.createElement("input");
62 | const spy = jest.spyOn(input, "focus");
63 | useTest.input.value = input;
64 | fakeCmn.editing.value = true;
65 | nextTick(() => nextTick(() => {
66 | expect(spy).toBeCalled();
67 | }));
68 | });
69 |
70 | it("Expect to focus input", () => {
71 | const spy = jest.spyOn(fakeCmn.root, "emit");
72 | fakeCmn.editable.value = true;
73 | useTest.focusInput();
74 | expect(fakeCmn.config.value.editing).toBe("id");
75 | expect(spy).toBeCalledWith(nodeEvents.edit, fakeCmn.node.value);
76 | });
77 |
78 | it("Expect to focus wrapper on esc", () => {
79 | const spy = jest.spyOn(fakeCmn, "blur");
80 | const spy1 = jest.spyOn(wrapper.value, "focus");
81 | fakeCmn.editable.value = true;
82 | const fakeEvent = { event: "test" };
83 | useTest.esc(fakeEvent);
84 | expect(spy).toBeCalledWith(fakeEvent);
85 | expect(spy1).toBeCalled();
86 | });
87 |
88 | it("Expect to focus input on press enter", () => {
89 | const spy = jest.spyOn(fakeCmn.root, "emit");
90 | fakeCmn.editable.value = true;
91 | useTest.enter();
92 | expect(fakeCmn.config.value.editing).toBe("id");
93 | expect(spy).toBeCalledWith(nodeEvents.edit, fakeCmn.node.value);
94 | });
95 | });
--------------------------------------------------------------------------------
/tests/unit/useLevel.spec.ts:
--------------------------------------------------------------------------------
1 | import { defaultConfig } from '../../src/misc/default';
2 | import { reactive, ref } from 'vue';
3 | import useLevel from '../../src/setup/useLevel';
4 | import { createState, states } from '../../src/setup/store';
5 |
6 | describe("test useLevel", () => {
7 | let props = null;
8 |
9 | let useTest = null;
10 |
11 | const nodes = {
12 | id1: {
13 | text: "test",
14 | children: ["id11"]
15 | },
16 | id11: {
17 | text: "test"
18 | }
19 | };
20 |
21 | const config = {
22 | roots: ["id1"]
23 | };
24 |
25 | const storeProps = reactive({
26 | nodes,
27 | config
28 | });
29 |
30 | let state = null
31 |
32 | let v = require("vue");
33 |
34 | v.inject = jest.fn(() => state);
35 |
36 | beforeEach(() => {
37 | const id = createState(storeProps);
38 | state = states.get(id);
39 | props = {
40 | depth: ref(0),
41 | parentId: ref(null)
42 | };
43 | useTest = useLevel(props);
44 | });
45 |
46 | it("Expect to have id", () => {
47 | expect(useTest.id.value).toBeDefined();
48 | });
49 |
50 | it("Expect to have a level", () => {
51 | expect(useTest.level.value).toBeInstanceOf(Array);
52 | expect(useTest.level.value[0]).toMatchObject(nodes["id1"]);
53 | });
54 |
55 | it("Expect to have no padding", () => {
56 | expect(useTest.padding.value).toBe(0);
57 | });
58 |
59 | it("Expect to have default style", () => {
60 | expect(useTest.style.value).toMatchObject({
61 | "list-style": "none",
62 | "padding-left": "0px"
63 | });
64 | });
65 |
66 | it("Expect to create level with parent", () => {
67 | props.parentId.value = "id1";
68 | expect(useTest.level.value[0]).toMatchObject({
69 | id: "id11",
70 | parent: "id1",
71 | text: "test"
72 | });
73 | });
74 |
75 | it("Expect level to be empty", () => {
76 | props.depth.value = 1;
77 | expect(useTest.level.value).toMatchObject([]);
78 | });
79 |
80 | it("Expect to have default padding", () => {
81 | props.depth.value = 1;
82 | expect(useTest.padding.value).toBe(defaultConfig.padding);
83 | });
84 |
85 | it("Expect to have config padding", () => {
86 | props.depth.value = 1;
87 | state.config.value.padding = 15;
88 | expect(useTest.padding.value).toBe(15);
89 | });
90 |
91 | it("Expect padding to be 0", () => {
92 | props.depth.value = 1;
93 | state.config.value.padding = -100;
94 | expect(useTest.padding.value).toBe(0);
95 | });
96 | });
--------------------------------------------------------------------------------
/tests/unit/useNode.spec.ts:
--------------------------------------------------------------------------------
1 | import { reactive, ref, isRef, nextTick } from 'vue';
2 | import { useNode } from '../../src/setup/useNode';
3 | import { defaultDisabledClass, defaultFocusClass } from "../../src/misc/default";
4 | import { nodeEvents } from '../../src/misc/nodeEvents';
5 |
6 | describe("test use node", () => {
7 | let fakeCmn = null;
8 |
9 | let useTest = null;
10 |
11 | let wrapper = null;
12 |
13 | let props = null;
14 |
15 | let node = null;
16 |
17 | let nodes = null;
18 |
19 | let node2 = null;
20 |
21 | let c1 = null;
22 |
23 | let c2 = null;
24 |
25 | let c3 = null;
26 |
27 | let config = null;
28 |
29 | let storeProps = null;
30 |
31 | let state = null;
32 |
33 | let v = require("vue");
34 |
35 | v.onMounted = jest.fn();
36 |
37 | v.onUnmounted = jest.fn();
38 |
39 | beforeEach(() => {
40 | node = {
41 | text: "id1",
42 | id: "id1",
43 | children: ["id11", "id12"],
44 | state: {}
45 | };
46 | node2 = {
47 | text: "id2",
48 | id: "id2",
49 | children: ["id21"],
50 | state:{}
51 | };
52 | c1 = {
53 | id: "id11",
54 | text: "id11",
55 | parent: "id1",
56 | state: {}
57 | };
58 | c2 = {
59 | id: "id12",
60 | text: "id12",
61 | parent: "id1",
62 | state: {}
63 | };
64 | c3 = {
65 | id: "id21",
66 | text: "id21",
67 | parent: "id2",
68 | state: {}
69 | };
70 | config = ref({
71 | roots: ["id1", "id2"]
72 | });
73 | nodes = {
74 | id1: node,
75 | id11: c1,
76 | id12: c2,
77 | id2: node2,
78 | id21: c3
79 | };
80 | storeProps = reactive({
81 | nodes,
82 | config
83 | });
84 | wrapper = ref(document.createElement("div"));
85 | state = {
86 | nodes: ref(nodes),
87 | config: ref(config),
88 | focusable: ref(null),
89 | focusFunc: new Map()
90 | };
91 | fakeCmn = {
92 | node: ref(node),
93 | config,
94 | wrapper,
95 | editing: ref(false),
96 | disabled: ref(false),
97 | focused: ref(false),
98 | root: {
99 | emit: jest.fn()
100 | },
101 | state
102 | };
103 | props = {
104 | depth: ref(0),
105 | index: ref(0)
106 | };
107 | useTest = useNode(fakeCmn, props);
108 | });
109 |
110 | it("Expect to have id", () => {
111 | expect(useTest.id.value).toBe("id1");
112 | });
113 |
114 | it("Expect to have node", () => {
115 | expect(useTest.hasNode.value).toBeTruthy();
116 | });
117 |
118 | it("Expect not to be loading", () => {
119 | expect(useTest.isLoading.value).toBeFalsy();
120 | });
121 |
122 | it("Expect level to be null", () => {
123 | expect(useTest.level.value).toBeNull();
124 | });
125 |
126 | it("Expect node to be closed", () => {
127 | expect(useTest.opened.value).toBeFalsy();
128 | });
129 |
130 | it("Expect disabledClass to be null", () => {
131 | expect(useTest.disabledClass.value).toBeNull();
132 | });
133 |
134 | it("Expect to have default disabled class", () => {
135 | fakeCmn.disabled.value = true;
136 | expect(useTest.disabledClass.value).toBe(defaultDisabledClass);
137 | });
138 |
139 | it("Expect to have config disalbed class", () => {
140 | fakeCmn.disabled.value = true;
141 | fakeCmn.config.value.disabledClass = "test";
142 | expect(useTest.disabledClass.value).toBe("test");
143 | });
144 |
145 | it("Expect icons not to be hidden", () => {
146 | expect(useTest.hideIcons.value).toBeFalsy();
147 | });
148 |
149 | it("Expect to have icon hidden", () => {
150 | fakeCmn.node.value.children = [];
151 | fakeCmn.config.value.roots = ["id1"];
152 | expect(useTest.hideIcons.value).toBeTruthy();
153 | });
154 |
155 | it("Expect not to be leaf", () => {
156 | expect(useTest.isLeaf.value).toBeFalsy();
157 | });
158 |
159 | it("Expect to be leaf", () => {
160 | fakeCmn.node.value.children = [];
161 | expect(useTest.isLeaf.value).toBeTruthy();
162 | });
163 |
164 | it("Expect to be leaf by config", () => {
165 | fakeCmn.config.value.leaves = ["id1"];
166 | fakeCmn.node.value.id = "id1";
167 | expect(useTest.isLeaf.value).toBeTruthy();
168 | });
169 |
170 | it("Expect tabindex to be 0", () => {
171 | expect(useTest.tabIndex.value).toBe(0);
172 | });
173 |
174 | it("Expect tabindex to be -1", () => {
175 | props.depth.value = 1;
176 | expect(useTest.tabIndex.value).toBe(-1);
177 | });
178 |
179 | it("Expect tabindex to be 0 and to be focused", () => {
180 | fakeCmn.node.value.id = "test";
181 | useTest.focus();
182 | nextTick(() => {
183 | nextTick(() => {
184 | expect(useTest.tabIndex.value).toBe(0);
185 | expect(useTest.focusClass.value).toBe(defaultFocusClass);
186 | })
187 | })
188 | });
189 |
190 | it("Expect to have config focus class", () => {
191 | fakeCmn.node.value.id = "test";
192 | fakeCmn.config.value.focusClass = "focusClass";
193 | useTest.focus();
194 | nextTick(() => {
195 | nextTick(() => {
196 | expect(useTest.focusClass.value).toBe("focusClass");
197 | })
198 | });
199 | });
200 |
201 | it("Expect focusClass to be null", () => {
202 | expect(useTest.focusClass.value).toBeNull();
203 | });
204 |
205 | it("Expect to have style", () => {
206 | expect(useTest.style.value).toMatchObject({
207 | display: "flex"
208 | });
209 | });
210 |
211 | it("Expect not to display level", () => {
212 | expect(useTest.displayLevel.value).toBeFalsy();
213 | });
214 |
215 | it("Expect not to display loading", () => {
216 | expect(useTest.displayLoading.value).toBeFalsy();
217 | });
218 |
219 | it("Expect to focus node", () => {
220 | const spy = jest.spyOn(fakeCmn.root, "emit");
221 | const focusSpy = jest.spyOn(wrapper.value, "focus");
222 | fakeCmn.node.value.id = "id";
223 | useTest.focus();
224 | nextTick(() => {
225 | nextTick(() => {
226 | expect(fakeCmn.focused.value).toBeTruthy();
227 | expect(focusSpy).toBeCalled();
228 | expect(spy).toBeCalledWith(nodeEvents.focus, fakeCmn.node.value);
229 | });
230 | });
231 | });
232 |
233 | it("Expect to toggle", () => {
234 | const spy = jest.spyOn(fakeCmn.root, "emit");
235 | useTest.toggle();
236 | expect(fakeCmn.node.value.state.opened).toBeTruthy();
237 | expect(spy).toBeCalledWith(nodeEvents.toggle, fakeCmn.node.value);
238 | nextTick(() => {
239 | expect(spy).toBeCalledWith(nodeEvents.opened, fakeCmn.node.value);
240 | useTest.toggle();
241 | expect(fakeCmn.node.value.state.opened).toBeFalsy();
242 | expect(spy).toBeCalledWith(nodeEvents.toggle, fakeCmn.node.value);
243 | nextTick(() => {
244 | expect(spy).toBeCalledWith(nodeEvents.closed, fakeCmn.node.value);
245 | });
246 | });
247 | });
248 |
249 | it("Expect to do nothing on right if no keyboard navigation", () => {
250 | useTest.right();
251 | expect(useTest.opened.value).toBeFalsy();
252 | });
253 |
254 | it("Expect to do nothing on left if no keyboard navigation", () => {
255 | fakeCmn.node.value.state.opened = true;
256 | useTest.left();
257 | expect(useTest.opened.value).toBeTruthy();
258 | });
259 |
260 | it("Expect to open node on right arrow and close on left", () => {
261 | fakeCmn.config.value.keyboardNavigation = true;
262 | useTest.right();
263 | expect(useTest.opened.value).toBeTruthy();
264 | useTest.left();
265 | expect(useTest.opened.value).toBeFalsy();
266 | });
267 |
268 | it("Expect to exec function on down", () => {
269 | fakeCmn.config.value.keyboardNavigation = true;
270 | const f = jest.fn();
271 | fakeCmn.state.focusFunc.set("id2", f);
272 | useTest.down();
273 | expect(f).toHaveBeenCalled();
274 | });
275 |
276 | it("Expect to exec function on up", () => {
277 | const f = jest.fn();
278 | fakeCmn.state.focusFunc.set("id1", f);
279 | fakeCmn.config.value.keyboardNavigation = true;
280 | fakeCmn.node.value = c1;
281 | fakeCmn.node.value.state.opened = true;
282 | useTest.up();
283 | expect(f).toHaveBeenCalled();
284 | });
285 |
286 | it("Expect to go to next root on down", () => {
287 | const next = useTest.nextVisible("id1");
288 | expect(next).toBe("id2");
289 | });
290 |
291 | it("Expect to go next child on down", () => {
292 | fakeCmn.node.value.state.opened = true;
293 | const next = useTest.nextVisible("id1");
294 | expect(next).toBe("id11");
295 | });
296 |
297 | it("Expect to go next root on last node", () => {
298 | fakeCmn.node.value = c2;
299 | fakeCmn.node.value.state.opened = true;
300 | const next = useTest.nextVisible("id12");
301 | expect(next).toBe("id2");
302 | });
303 |
304 | it("Expect to go to next children on down", () => {
305 | fakeCmn.node.value = c1;
306 | fakeCmn.node.value.state.opened = true;
307 | const next = useTest.nextVisible("id11");
308 | expect(next).toBe("id12");
309 | });
310 |
311 | it("Expect to stay on last child on down", () => {
312 | fakeCmn.node.value = c3;
313 | state.nodes.value.id2.state.opened = true;
314 | const next = useTest.nextVisible("id2");
315 | expect(next).toBe("id21");
316 | });
317 |
318 | it("Expect to focus nothing when first root", () => {
319 | const prev = useTest.prevVisible("id1");
320 | expect(prev).toBe(null);
321 | });
322 |
323 | it("Expect to go first root on up", () => {
324 | fakeCmn.node.value = c1;
325 | fakeCmn.node.value.state.opened = true;
326 | const prev = useTest.prevVisible("id11");
327 | expect(prev).toBe("id1");
328 | });
329 |
330 | it("Expect to go first root on up", () => {
331 | fakeCmn.node.value = node2;
332 | const prev = useTest.prevVisible("id2");
333 | expect(prev).toBe("id1");
334 | });
335 |
336 | it("Expect to go first child on up", () => {
337 | fakeCmn.node.value = c2;
338 | fakeCmn.node.value.state.opened = true;
339 | const prev = useTest.prevVisible("id12");
340 | expect(prev).toBe("id11");
341 | });
342 | });
--------------------------------------------------------------------------------
/tests/unit/useTree.spec.ts:
--------------------------------------------------------------------------------
1 | import { reactive, isRef } from 'vue';
2 | import useTree from "../../src/setup/useTree";
3 |
4 | describe("test useTree", () => {
5 | let useTest = null;
6 |
7 | let props = null;
8 |
9 | const fakeEmit = (evt: string, ...args: any[]) => {};
10 |
11 | const v = require("vue");
12 |
13 | v.onUnmounted = jest.fn();
14 |
15 | const spy = jest.spyOn(v, "provide").mockImplementation(() => () => {});
16 |
17 | beforeEach(() => {
18 | props = reactive({
19 | config: {
20 | root: ["id1"]
21 | },
22 | nodes: {
23 | id1: {
24 | text: "test"
25 | }
26 | }
27 | });
28 |
29 | useTest = useTree(props, fakeEmit);
30 | });
31 |
32 | it("Expect to call provide", () => {
33 | expect(spy).toBeCalledWith("emitter", fakeEmit);
34 | });
35 |
36 | it("Expect to create element ref", () => {
37 | expect(isRef(useTest.element)).toBeTruthy();
38 | expect(useTest.element.value).toBeNull();
39 | });
40 |
41 | it("Expect to have basic style", () => {
42 | expect(useTest.style.value).toMatchObject({
43 | "align-items": "center",
44 | "display": "flex"
45 | });
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/todo.txt:
--------------------------------------------------------------------------------
1 | FEATURE to implement:
2 | - Drag and drop => done
3 | - Customize icons => done
4 | - keyboard navigation => done
5 | - check => done
6 | - disabled => done
7 | - lazy load => done
8 | - autoCheck => done
9 | - customizable effects => done
10 | - emit events => done
11 | - example material css => done
12 | - unit tests => done
13 | - typings => done
14 | - documentation => in progress
15 | - ARIA => in progress
16 | - Mobile touch events
17 | - Node focusable or not
18 | - allow dnd with function
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "jsx": "preserve",
6 | "declaration": true,
7 | "outDir": "./dist",
8 | "importHelpers": true,
9 | "moduleResolution": "node",
10 | "experimentalDecorators": true,
11 | "strictFunctionTypes": false,
12 | "allowJs": true,
13 | "skipLibCheck": true,
14 | "esModuleInterop": true,
15 | "allowSyntheticDefaultImports": true,
16 | "strictNullChecks": false,
17 | "strict": false,
18 | "sourceMap": true,
19 | "baseUrl": ".",
20 | "types": [
21 | "webpack-env",
22 | "jest"
23 | ],
24 | "paths": {
25 | "/@src/*": ["src/*"]
26 | },
27 | "lib": [
28 | "esnext",
29 | "dom",
30 | "dom.iterable",
31 | "scripthost"
32 | ]
33 | },
34 | "include": [
35 | "src/**/*.ts",
36 | "src/**/*.tsx",
37 | "src/**/*.vue"
38 | ],
39 | "exclude": [
40 | "node_modules",
41 | "tests"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import vue from "@vitejs/plugin-vue";
3 | const path = require("path");
4 |
5 | export default defineConfig({
6 | resolve: {
7 | alias: [
8 | {
9 | find: "@",
10 | replacement: path.resolve(__dirname, "src"),
11 | },
12 | ],
13 | },
14 | plugins: [vue()],
15 | build: {
16 | lib: {
17 | entry: path.resolve(__dirname, "src/index.ts"),
18 | name: "treeview",
19 | },
20 | rollupOptions: {
21 | external: ["vue"],
22 | output: {
23 | inlineDynamicImports: true,
24 | globals: {
25 | vue: "Vue",
26 | },
27 | },
28 | },
29 | },
30 | });
31 |
--------------------------------------------------------------------------------