├── .eslintignore ├── .eslintrc.js ├── .github ├── release.yml └── workflows │ └── deploy.yml ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── dev ├── App.vue ├── main.ts ├── plugins │ └── element.ts └── tests │ ├── all-tests.vue │ ├── big-data-multiple-select.vue │ ├── big-data-table.vue │ ├── big-data.vue │ ├── bug-74.vue │ ├── check-or-expand.vue │ ├── set-value-in-mounted.vue │ ├── show-unrender-label.vue │ └── slots.vue ├── docs ├── .vitepress │ ├── .eslintrc.js │ ├── components │ │ ├── check-strictly.vue │ │ ├── custom-props.vue │ │ ├── custom-render.vue │ │ ├── custom-slot.vue │ │ ├── filterable.vue │ │ ├── lazy-load-cache-data.vue │ │ ├── lazy-load.vue │ │ ├── multiple.vue │ │ ├── slots-menu.vue │ │ ├── virtual.vue │ │ └── with-form.vue │ ├── config.mts │ └── theme │ │ ├── enhanceApp.ts │ │ └── index.ts ├── README.md ├── index.md └── public │ └── logo.svg ├── env.d.ts ├── index.html ├── jetbrains.svg ├── package-lock.json ├── package.json ├── public └── logo.svg ├── rollupx.config.js ├── src ├── components │ ├── CacheOption.ts │ ├── ElSelect.ts │ ├── ElSelectTree.vue │ ├── ElSelectTreeOption.ts │ ├── ElSelectTreeVirtual.vue │ ├── ElSelectVirtual.ts │ ├── ElTreeNodeVirtual.ts │ ├── ElTreeVirtual.ts │ ├── style.scss │ ├── utils.spec.ts │ ├── utils.ts │ └── virtual-list.ts ├── dark-base.scss ├── dark.scss ├── element-ui.ts ├── index.ts ├── plugins │ └── element.ts └── style.scss ├── tsconfig.app.json ├── tsconfig.config.json ├── tsconfig.json ├── tsconfig.vitest.json └── vite.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | !.eslintrc.js 2 | lib 3 | dist 4 | stat 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | root: true, 7 | extends: [ 8 | 'plugin:vue/essential', 9 | 'eslint:recommended', 10 | '@vue/eslint-config-typescript/recommended', 11 | '@vue/eslint-config-prettier', 12 | ], 13 | rules: { 14 | // eslint http://eslint.cn/docs/rules/ 15 | 'no-debugger': 'error', 16 | 'no-console': 'error', 17 | eqeqeq: ['error', 'always'], 18 | 19 | // prettier https://prettier.io/docs/en/options.html 20 | 'prettier/prettier': [ 21 | 'error', 22 | { 23 | singleQuote: true, 24 | arrowParens: 'always', 25 | semi: true, 26 | trailingComma: 'all', 27 | }, 28 | ], 29 | 30 | // vue 31 | 'vue/multi-word-component-names': 'off', 32 | 'vue/no-mutating-props': 'off', 33 | 'vue/no-reserved-component-names': 'off', 34 | 35 | // typescript https://typescript-eslint.io/rules/ 36 | '@typescript-eslint/no-explicit-any': 'off', 37 | '@typescript-eslint/ban-types': [ 38 | 'error', 39 | { 40 | types: { 41 | '{}': false, 42 | }, 43 | }, 44 | ], 45 | 46 | // import https://github.com/import-js/eslint-plugin-import#rules 47 | 'import/no-useless-path-segments': 'error', 48 | 'import/first': 'error', 49 | 'import/no-duplicates': 'error', 50 | 'import/order': [ 51 | 'error', 52 | { 53 | groups: [ 54 | ['builtin', 'external'], 55 | ['type'], 56 | ['internal', 'parent', 'sibling', 'index'], 57 | ], 58 | pathGroups: [ 59 | { 60 | pattern: '@/**', 61 | group: 'internal', 62 | }, 63 | ], 64 | 'newlines-between': 'always', 65 | alphabetize: { order: 'asc' }, 66 | }, 67 | ], 68 | 'import/newline-after-import': 'error', 69 | }, 70 | 71 | plugins: [ 72 | // https://github.com/import-js/eslint-import-resolver-typescript#configuration 73 | 'import', 74 | ], 75 | 76 | settings: { 77 | // https://github.com/import-js/eslint-import-resolver-typescript#configuration 78 | 'import/parsers': { 79 | '@typescript-eslint/parser': ['.ts', '.tsx'], 80 | }, 81 | 'import/resolver': { 82 | typescript: { 83 | alwaysTryTypes: true, 84 | project: path.resolve(__dirname, 'tsconfig.json'), 85 | }, 86 | }, 87 | }, 88 | }; 89 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - octocat 7 | categories: 8 | - title: Fixes 9 | labels: 10 | - fix 11 | - title: Features 12 | labels: 13 | - feat 14 | - title: Performs 15 | labels: 16 | - perf 17 | - title: Other Changes 18 | labels: 19 | - "*" -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: 16 14 | cache: npm 15 | - run: npm i 16 | - name: Build 17 | run: npm run docs:build 18 | - name: Deploy 19 | uses: peaceiris/actions-gh-pages@v3 20 | with: 21 | github_token: ${{ secrets.GITHUB_TOKEN }} 22 | publish_dir: docs/.vitepress/dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | stat 4 | lib 5 | types 6 | dist 7 | docs/.vitepress/cache 8 | docs/.vitepress/dist 9 | 10 | # local env files 11 | .env.local 12 | .env.*.local 13 | 14 | # Log files 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.1.1-beta.17](https://github.com/yujinpan/el-select-tree/compare/v2.1.1-beta.16...v2.1.1-beta.17) (2024-11-14) 2 | 3 | ### Bug Fixes 4 | 5 | - cache option does not work and makes the check very slow ([77b7eb2](https://github.com/yujinpan/el-select-tree/commit/77b7eb236ff4c7c456c50dfb1f01a95b5922b439)) 6 | 7 | ### Performance Improvements 8 | 9 | - optimizing array comparison efficiency ([faa870b](https://github.com/yujinpan/el-select-tree/commit/faa870b388f7dc781b7585a89b8957b0f81b4ee6)) 10 | 11 | ## [2.1.1-beta.16](https://github.com/yujinpan/el-select-tree/compare/v2.1.1-beta.15...v2.1.1-beta.16) (2024-09-30) 12 | 13 | ### Bug Fixes 14 | 15 | - slowly when many items checked ([d8516e7](https://github.com/yujinpan/el-select-tree/commit/d8516e7c6506acb5849f175cf0707a6e30012016)) 16 | 17 | ## [2.1.1-beta.15](https://github.com/yujinpan/el-select-tree/compare/v2.1.1-beta.14...v2.1.1-beta.15) (2024-06-18) 18 | 19 | ### Bug Fixes 20 | 21 | - always focus when use filter ([af87fe0](https://github.com/yujinpan/el-select-tree/commit/af87fe028581b7a91138ef0c80ce38df0caea6eb)) 22 | - popper position incorrect when menu display or use filter ([405b457](https://github.com/yujinpan/el-select-tree/commit/405b457984eb5aab000fc16a32219282f20a6417)) 23 | 24 | ### Features 25 | 26 | - add uppercase/lowercase matching ([c78b64d](https://github.com/yujinpan/el-select-tree/commit/c78b64d6961758e7e0a4a3baa9e61ac8a098b746)) 27 | 28 | ## [2.1.1-beta.14](https://github.com/yujinpan/el-select-tree/compare/v2.1.1-beta.13...v2.1.1-beta.14) (2024-05-29) 29 | 30 | ### Features 31 | 32 | - add component optional styles, support dark mode ([1dc1ea6](https://github.com/yujinpan/el-select-tree/commit/1dc1ea675db2e742e55f8b260d834c2cc77951cf)) 33 | 34 | ## [2.1.1-beta.13](https://github.com/yujinpan/el-select-tree/compare/v2.1.1-beta.12...v2.1.1-beta.13) (2024-05-28) 35 | 36 | ### Bug Fixes 37 | 38 | - filter data empty when scroll to bottom ([d52a4b6](https://github.com/yujinpan/el-select-tree/commit/d52a4b6622a8bbd86fe670fa7d01e547ed9e4138)) 39 | 40 | ## [2.1.1-beta.12](https://github.com/yujinpan/el-select-tree/compare/v2.1.1-beta.11...v2.1.1-beta.12) (2024-05-17) 41 | 42 | ### Bug Fixes 43 | 44 | - `getParentKeys` does not include multi-level nodes ([c88e722](https://github.com/yujinpan/el-select-tree/commit/c88e72236c0caf236bc2c9b58a80e09f4972a250)) 45 | 46 | ## [2.1.1-beta.11](https://github.com/yujinpan/el-select-tree/compare/v2.1.1-beta.10...v2.1.1-beta.11) (2024-05-14) 47 | 48 | ### Bug Fixes 49 | 50 | - use `Vue.observable` instead of `reactive` (vue2.6) ([7c82ecb](https://github.com/yujinpan/el-select-tree/commit/7c82ecb8f8bc76c81a7801d1cfa4f42c73ad8f66)) 51 | 52 | ## [2.1.1-beta.10](https://github.com/yujinpan/el-select-tree/compare/v2.1.1-beta.9...v2.1.1-beta.10) (2024-04-25) 53 | 54 | ### Bug Fixes 55 | 56 | - checkedKeys sort with selection order ([277dab4](https://github.com/yujinpan/el-select-tree/commit/277dab40d76c05fc712cb658f0e2921ae5e0f133)) 57 | 58 | # 2.0.3 2021-11-24 59 | 60 | ## Feature 61 | 62 | - add props can resolve callback function type 63 | 64 | # 2.0.2 2021-11-23 65 | 66 | ## Feature 67 | 68 | - new document and examples 69 | - add the option `show-checkbox` 70 | 71 | ## Fix 72 | 73 | - read `label` un parsed 74 | - el-select props un merged 75 | - filter text not clear when visible change 76 | 77 | # 2.0.1 2021-10-11 78 | 79 | ## Feature 80 | 81 | - add the support with `renderContent` and `#option` slot 82 | 83 | ## Refactor 84 | 85 | - auto expanded with selected 86 | - auto merge props 87 | 88 | # 2.0.0 2021-09-29 89 | 90 | ## Refactor 91 | 92 | - development env update to TS 93 | - make full use of the two components and combine them 94 | - better support for almost all attributes of components, like `filterable`, `multiple` 95 | 96 | # 1.1.1 2021-01-13 97 | 98 | ## Feature 99 | 100 | - add `popoverWidth` props 101 | 102 | ## Fix 103 | 104 | - popover position update when tree node expanded 105 | 106 | ## Refactor 107 | 108 | - external style-inject and vue-runtime-helpers package 109 | 110 | # 1.1.0 2021-01-08 111 | 112 | ## Remove 113 | 114 | - `disabledValues` props removed(use node's disabled props) 115 | 116 | ## Fix 117 | 118 | - lazy load be ignored when data is empty 119 | - cannot selected option when lazy load 120 | - `defaultExpandedKeys` is invalid when value not set 121 | - `autoExpandParent` to default true 122 | 123 | # 1.0.26 2020-09-25 124 | 125 | ## Feature 126 | 127 | - add inherited properties from ElTree 128 | 129 | ## Fix 130 | 131 | - disabled values invalid in children 132 | 133 | # 1.0.25 2020-09-15 134 | 135 | ## Fix 136 | 137 | - label not change when data to empty 138 | 139 | # 1.0.24 2020-08-29 140 | 141 | ## Fix 142 | 143 | - rollup-plugin-vue normalizer not parse https://github.com/vuejs/rollup-plugin-vue/issues/262 144 | 145 | # 1.0.23 2020-07-22 146 | 147 | ## Refactor 148 | 149 | - remove props `popover-min-width` and auto compute min-width 150 | 151 | ## Fix 152 | 153 | - min-width needs to subtract the border 154 | - `props.children` is not work with multiple disabled 155 | 156 | # 1.0.22 2020-06-01 157 | 158 | ## Fix 159 | 160 | - set selected when options init async and repeat prevent it 161 | 162 | # 1.0.21 2020-06-01 163 | 164 | ## Fix 165 | 166 | - add pinter style in expand icon and remove the text flex attribute(unused and width is not auto in ie) 167 | 168 | # 1.0.20 2020-05-28 169 | 170 | ## Fix 171 | 172 | - cannot clear in disabled state 173 | 174 | # 1.0.19 2020-05-27 175 | 176 | ## Fix 177 | 178 | - import compiled version with ElementUI's `Emitter` mixin 179 | - `check-strictly` does not valid in multiple check 180 | 181 | # 1.0.17 2020-05-19 182 | 183 | ## Fix 184 | 185 | - Handle verification timing error on el-form 186 | 187 | # 1.0.15 2020-05-13 188 | 189 | ## Features 190 | 191 | - add the `clearable` 192 | 193 | ## Fix 194 | 195 | - fix the motion problem of the arrow icon of the selector 196 | 197 | # 1.0.14 2020-01-07 198 | 199 | ## Features 200 | 201 | - add new attr `defaultExpandAll`, control expand all tree node 202 | 203 | # 1.0.13 2019-12-31 204 | 205 | ## Bug Fixes 206 | 207 | - `checkStrictly` not work 208 | - tree node child `label` unbind 209 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 yujinpan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # el-select-tree 2 | 3 | ElementUI's el-select combined with el-tree. 4 | 5 | - Online examples [https://yujinpan.github.io/el-select-tree/](https://yujinpan.github.io/el-select-tree/) 6 | 7 | > - 2.1 Support custom menu header/footer. 8 | > - 2.1 `check-on-click-node` is available with `show-checkobx`, default is `false`. **Broken Change** 9 | > - 2.1 `expand-on-click-node` is available, default is `true`. 10 | > - 2.1 Support [virtual list](https://yujinpan.github.io/el-select-tree/#virtual). 11 | > - 2.0 Comprehensively improve the utilization rate of original parts. 12 | 13 | ## Usage 14 | 15 | ### Install 16 | 17 | ``` 18 | npm install --save el-select-tree 19 | ``` 20 | 21 | ### Require element-ui 22 | 23 | If your project does not use element-ui, 24 | you need to introduce a separate element-ui package, like this: 25 | 26 | ```js 27 | import "el-select-tree/lib/element-ui"; 28 | ``` 29 | 30 | ### Global registration 31 | 32 | ```js 33 | import Vue from "vue"; 34 | import ElSelectTree from "el-select-tree"; 35 | 36 | Vue.use(ElSelectTree); 37 | ``` 38 | 39 | ### In-component registration 40 | 41 | ```js 42 | import ElSelectTree from "el-select-tree"; 43 | 44 | export default { 45 | components: { 46 | ElSelectTree, 47 | }, 48 | }; 49 | ``` 50 | 51 | ### Complete example 52 | 53 | ```vue 54 | 62 | 63 | 90 | ``` 91 | 92 | ## Component API 93 | 94 | **Extends `ElTree` And `ElSelect` All Props/Methods/Events/Slots.** 95 | 96 | | props | methods | events | slots | 97 | | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------- | 98 | | [el-select](https://element.eleme.io/#/zh-CN/component/select#select-attributes) | [el-select](https://element.eleme.io/#/zh-CN/component/select#methods) | [el-select](https://element.eleme.io/#/zh-CN/component/select#select-events) | [el-select](https://element.eleme.io/#/zh-CN/component/select#select-slots) | 99 | | [el-tree](https://element.eleme.io/#/zh-CN/component/tree#attributes) | [el-tree](https://element.eleme.io/#/zh-CN/component/tree#fang-fa) | [el-tree](https://element.eleme.io/#/zh-CN/component/tree#events) | [el-tree](https://element.eleme.io/#/zh-CN/component/tree#scoped-slot) | 100 | 101 | # Special Thanks 102 | 103 | Special thanks to [JetBrains](https://www.jetbrains.com/?from=el-table-infinite-scroll) 104 | for letting me use the free license. 105 | 106 | ![JetBrains](./jetbrains.svg) 107 | -------------------------------------------------------------------------------- /dev/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /dev/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import App from './App.vue'; 4 | 5 | import './plugins/element'; 6 | 7 | Vue.config.productionTip = false; 8 | 9 | new Vue({ 10 | el: '#app', 11 | render: (h) => h(App), 12 | }); 13 | -------------------------------------------------------------------------------- /dev/plugins/element.ts: -------------------------------------------------------------------------------- 1 | import '@/element-ui.ts'; 2 | import 'element-ui/lib/theme-chalk/index.css'; 3 | 4 | import { 5 | Form, 6 | FormItem, 7 | Button, 8 | Table, 9 | TableColumn, 10 | Loading, 11 | Link, 12 | Divider, 13 | Checkbox, 14 | } from 'element-ui'; 15 | import Vue from 'vue'; 16 | 17 | import ElTreeSelect, { ElSelectTreeVirtual } from '@/index'; 18 | 19 | Vue.use(Form); 20 | Vue.use(FormItem); 21 | Vue.use(Button); 22 | Vue.use(Table); 23 | Vue.use(TableColumn); 24 | Vue.use(Loading); 25 | Vue.use(Link); 26 | Vue.use(Divider); 27 | Vue.use(Checkbox); 28 | Vue.use(ElTreeSelect); 29 | Vue.use(ElSelectTreeVirtual); 30 | -------------------------------------------------------------------------------- /dev/tests/all-tests.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 89 | -------------------------------------------------------------------------------- /dev/tests/big-data-multiple-select.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 42 | -------------------------------------------------------------------------------- /dev/tests/big-data-table.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 263 | 264 | 275 | -------------------------------------------------------------------------------- /dev/tests/big-data.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 55 | -------------------------------------------------------------------------------- /dev/tests/bug-74.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | -------------------------------------------------------------------------------- /dev/tests/check-or-expand.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 36 | -------------------------------------------------------------------------------- /dev/tests/set-value-in-mounted.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 26 | -------------------------------------------------------------------------------- /dev/tests/show-unrender-label.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | -------------------------------------------------------------------------------- /dev/tests/slots.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 44 | -------------------------------------------------------------------------------- /docs/.vitepress/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | extends: ['../../.eslintrc'], 5 | env: { 6 | node: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /docs/.vitepress/components/check-strictly.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 35 | -------------------------------------------------------------------------------- /docs/.vitepress/components/custom-props.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 29 | -------------------------------------------------------------------------------- /docs/.vitepress/components/custom-render.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | -------------------------------------------------------------------------------- /docs/.vitepress/components/custom-slot.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | -------------------------------------------------------------------------------- /docs/.vitepress/components/filterable.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 25 | -------------------------------------------------------------------------------- /docs/.vitepress/components/lazy-load-cache-data.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | -------------------------------------------------------------------------------- /docs/.vitepress/components/lazy-load.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 28 | -------------------------------------------------------------------------------- /docs/.vitepress/components/multiple.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 36 | -------------------------------------------------------------------------------- /docs/.vitepress/components/slots-menu.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 50 | -------------------------------------------------------------------------------- /docs/.vitepress/components/virtual.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 68 | -------------------------------------------------------------------------------- /docs/.vitepress/components/with-form.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 38 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import vue2Jsx from "@vitejs/plugin-vue2-jsx"; 2 | import { defineConfig } from "vitepress"; 3 | import viteDemoPlugin from "vitepress-plugin-component-demo/vite-plugin"; 4 | import viteConfig from "../../vite.config"; 5 | 6 | export default defineConfig({ 7 | base: "/el-select-tree/", 8 | title: "el-select-tree", 9 | description: "ElementUI's el-select combined with el-tree.", 10 | lastUpdated: true, 11 | themeConfig: { 12 | logo: "/logo.svg", 13 | search: { provider: "local" }, 14 | socialLinks: [ 15 | { icon: "github", link: "https://github.com/yujinpan/el-select-tree/" }, 16 | ], 17 | nav: [{ text: "Guide", link: "/" }], 18 | outline: 'deep', 19 | footer: { 20 | message: "Released under the MIT License.", 21 | copyright: "Copyright © 2024 yujinpan", 22 | }, 23 | }, 24 | vite: { 25 | plugins: [ 26 | vue2Jsx({ 27 | // fork from @vue/babel-preset-app 28 | babelPlugins: [ 29 | ["@babel/plugin-proposal-decorators", { legacy: true }], 30 | "@babel/plugin-proposal-class-properties", 31 | ], 32 | }), 33 | viteDemoPlugin(), 34 | ], 35 | resolve: viteConfig.resolve, 36 | css: viteConfig.css, 37 | ssr: { 38 | noExternal: ["element-ui"], 39 | }, 40 | }, 41 | async transformHtml(code) { 42 | return code.replace( 43 | "", 44 | ` 45 | 46 | 47 | 54 | `, 55 | ); 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/enhanceApp.ts: -------------------------------------------------------------------------------- 1 | import '@/element-ui'; 2 | import { 3 | Form, 4 | FormItem, 5 | Button, 6 | Table, 7 | TableColumn, 8 | Loading, 9 | Link, 10 | Checkbox, 11 | } from 'element-ui'; 12 | 13 | import type { EnhanceAppContext } from 'vitepress'; 14 | 15 | import ElSelectTree, { ElSelectTreeVirtual } from '@/index'; 16 | import '@/style.scss'; 17 | import 'element-ui/packages/theme-chalk/src/button.scss'; 18 | import 'element-ui/packages/theme-chalk/src/form.scss'; 19 | import 'element-ui/packages/theme-chalk/src/form-item.scss'; 20 | 21 | export default (ctx: EnhanceAppContext) => { 22 | const { app: Vue } = ctx; 23 | 24 | Vue.use(Form); 25 | Vue.use(FormItem); 26 | Vue.use(Button); 27 | Vue.use(Table); 28 | Vue.use(TableColumn); 29 | Vue.use(Loading); 30 | Vue.use(Link); 31 | Vue.use(ElSelectTree); 32 | Vue.use(ElSelectTreeVirtual); 33 | Vue.use(Checkbox); 34 | }; 35 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme'; 2 | import { enhanceApp as enhanceAppDemo } from 'vitepress-plugin-component-demo'; 3 | 4 | import type { Theme } from 'vitepress'; 5 | 6 | import enhanceApp from './enhanceApp'; 7 | 8 | export default { 9 | extends: DefaultTheme, 10 | async enhanceApp(context) { 11 | enhanceAppDemo(context); 12 | enhanceApp(context); 13 | }, 14 | } as Theme; 15 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # el-select-tree 2 | 3 | ElementUI's el-select combined with el-tree. 4 | 5 | > - 2.1 Support custom menu [header/footer](#slots-menu). 6 | > - 2.1 `check-on-click-node` is available with `show-checkobx`, default is `false`. 7 | > - 2.1 `expand-on-click-node` is available, default is `true`. 8 | > - 2.1 Support [virtual list](#virtual). 9 | > - 2.0 Comprehensively improve the utilization rate of original parts. 10 | 11 | ## Installation 12 | 13 | ```shell 14 | npm install --save el-select-tree 15 | ``` 16 | 17 | ```ts 18 | import Vue from "vue"; 19 | import ElSelectTree from "el-select-tree"; 20 | 21 | Vue.use(ElSelectTree); 22 | ``` 23 | 24 | > If your project does not depend on element-ui, you need to introduce additional component packages. 25 | > 26 | > ```ts 27 | > import "el-select-tree/lib/element-ui"; 28 | > // If the project is loaded on demand, modify babel.config.js according to the official configuration. 29 | > // The complete style file is imported here. 30 | > // https://element.eleme.io/#/zh-CN/component/quickstart#an-xu-yin-ru 31 | > import "element-ui/lib/theme-chalk/index.css"; 32 | > ``` 33 | 34 | ## Usage 35 | 36 | ### `check-strictly` 37 | 38 | Any node can be selected, and the normal mode can only select leaf nodes. 39 | 40 | 41 | 42 | ### `filterable` 43 | 44 | Support for filtering tree nodes. 45 | 46 | 47 | 48 | ### `multiple` 49 | 50 | Multiple selection. 51 | 52 | 53 | 54 | ### `props` 55 | 56 | Custom tree data props. 57 | 58 | 59 | 60 | ### `lazy-load` 61 | 62 | Lazy load tree nodes. 63 | 64 | 65 | 66 | ### `lazy-load-cache-data` 67 | 68 | Use cached data to display unloaded node names. 69 | 70 | 71 | 72 | ### `slot` 73 | 74 | Custom option content. 75 | 76 | 77 | 78 | ### `slots-menu` 79 | 80 | Custom menu header/footer. 81 | 82 | 83 | 84 | ### `render` 85 | 86 | Custom option content use `render`. 87 | 88 | 89 | 90 | ### `with-form` 91 | 92 | Use with `el-form`. 93 | 94 | 95 | 96 | ### `virtual` 97 | 98 | Use virtual list. `ElSelectTreeVirtual` is optional, need register first. 99 | 100 | ```shell 101 | npm i el-select-tree@2.1 102 | ``` 103 | 104 | ```ts 105 | import { ElSelectTreeVirtual } from "el-select-tree"; 106 | 107 | export default { 108 | components: { 109 | ElSelectTreeVirtual, 110 | }, 111 | }; 112 | ``` 113 | 114 | 115 | 116 | 117 | 118 | :::warning 119 | Use `banReactive` when the amount of data is large. Otherwise, performance will be slower. 120 | 121 | ```ts 122 | const empty = reactive({}); 123 | const banReactive = (obj) => { 124 | // skip vue reactive 125 | obj.__ob__ = empty.__ob__; 126 | return obj; 127 | }; 128 | 129 | export default { 130 | data() { 131 | return { 132 | data: banReactive( 133 | Array(100000) 134 | .fill("") 135 | .map((item, index) => ({ value: index, label: index + "" })) 136 | ), 137 | }; 138 | }, 139 | }; 140 | ``` 141 | 142 | ::: 143 | 144 | ## Props 145 | 146 | `el-select-tree` inherits `el-tree` and `el-select` all props/methods/events/slots. 147 | 148 | So no repeat here. 149 | 150 | | props | methods | events | slots | 151 | | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------- | 152 | | [el-select](https://element.eleme.io/#/zh-CN/component/select#select-attributes) | [el-select](https://element.eleme.io/#/zh-CN/component/select#methods) | [el-select](https://element.eleme.io/#/zh-CN/component/select#select-events) | [el-select](https://element.eleme.io/#/zh-CN/component/select#select-slots) | 153 | | [el-tree](https://element.eleme.io/#/zh-CN/component/tree#attributes) | [el-tree](https://element.eleme.io/#/zh-CN/component/tree#fang-fa) | [el-tree](https://element.eleme.io/#/zh-CN/component/tree#events) | [el-tree](https://element.eleme.io/#/zh-CN/component/tree#scoped-slot) | 154 | 155 | ## Own Props 156 | 157 | | Name | Type | Desc | 158 | | ---------- | ----- | ------------------------------------------------------------------------------------------- | 159 | | cache-data | Array | The cache data for lazy load tree nodes, it can resolved correct label with unloaded nodes. | 160 | 161 | ## Own Slots 162 | 163 | | Name | Desc | 164 | | ------ | ----------- | 165 | | header | menu header | 166 | | footer | menu footer | 167 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | el-select-tree 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /jetbrains.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 45 | 47 | 48 | 51 | 54 | 56 | 57 | 59 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "el-select-tree", 3 | "version": "2.1.1-beta.17", 4 | "author": "yujinpan", 5 | "publishConfig": { 6 | "registry": "https://registry.npmjs.org" 7 | }, 8 | "main": "lib/cjs/index.js", 9 | "module": "lib/es/index.js", 10 | "types": "types/index.d.ts", 11 | "keywords": [ 12 | "el-select", 13 | "el-tree", 14 | "el-select-tree" 15 | ], 16 | "files": [ 17 | "lib", 18 | "types" 19 | ], 20 | "scripts": { 21 | "publish:beta": "release-ops beta", 22 | "publish:patch": "release-ops patch", 23 | "version": "npm run build", 24 | "dev": "vite", 25 | "build": "cross-env NODE_ENV=production rollupx", 26 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 27 | "start": "npm run dev", 28 | "test": "vitest --environment jsdom", 29 | "docs:dev": "vitepress dev docs", 30 | "docs:build": "vitepress build docs" 31 | }, 32 | "dependencies": { 33 | "@babel/runtime": "^7.24.7", 34 | "core-js": "^3.28.0", 35 | "style-inject": "^0.3.0", 36 | "vue-class-component": "^7.x", 37 | "vue-property-decorator": "^8.x", 38 | "vue-runtime-helpers": "^1.1.2" 39 | }, 40 | "devDependencies": { 41 | "@rushstack/eslint-patch": "^1.1.0", 42 | "@types/jsdom": "*", 43 | "@types/node": "*", 44 | "@vitejs/plugin-vue2": "^2", 45 | "@vitejs/plugin-vue2-jsx": "^1", 46 | "@vue/eslint-config-prettier": "^9", 47 | "@vue/eslint-config-typescript": "^13", 48 | "@vue/test-utils": "^2", 49 | "@vue/tsconfig": "^0.1.3", 50 | "async-validator": "^1.11.5", 51 | "cross-env": "^7.0.3", 52 | "dotenv": "^8.0.0", 53 | "element-ui": "^2.15.10", 54 | "eslint": "^8", 55 | "eslint-import-resolver-typescript": "^3.5.1", 56 | "eslint-plugin-import": "^2.26.0", 57 | "eslint-plugin-vue": "^9.4.0", 58 | "jsdom": "^20.0.0", 59 | "npm-run-all": "^4.1.5", 60 | "path-ops": "^1.0.0", 61 | "prettier": "^3", 62 | "release-ops": "^1.0.1", 63 | "rollup": "^4.14.1", 64 | "rollup-plugin-vue": "^5.1.9", 65 | "rollupx": "^3.1.12", 66 | "sass": "^1.54.9", 67 | "typescript": "^5", 68 | "vite": "^5", 69 | "vitepress": "npm:vitepress-v2@^5", 70 | "vitepress-plugin-component-demo": "^1.0.1", 71 | "vitest": "^1", 72 | "vue": "^2.7.16", 73 | "vue-tsc": "^2" 74 | }, 75 | "peerDependencies": { 76 | "element-ui": "^2.15.6", 77 | "vue": "^2.x" 78 | }, 79 | "browserslist": [ 80 | "> 1%", 81 | "last 2 versions" 82 | ], 83 | "bugs": { 84 | "url": "https://github.com/yujinpan/el-select-tree/issues" 85 | }, 86 | "homepage": "https://github.com/yujinpan/el-select-tree#readme", 87 | "license": "MIT", 88 | "repository": { 89 | "type": "git", 90 | "url": "git+ssh://git@github.com/yujinpan/el-select-tree.git" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /rollupx.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | banner: 4 | '/*!\n' + 5 | ` * el-select-tree v${require('./package.json').version}\n` + 6 | ` * (c) 2019-${new Date().getFullYear()} yujinpan\n` + 7 | ' * Released under the MIT License.\n' + 8 | ' */\n', 9 | inputFiles: ['**/*'], 10 | outputDir: 'lib', 11 | 12 | formats: [ 13 | { 14 | format: 'es', 15 | inputFiles: ['**/*'], 16 | outputDir: 'lib/es', 17 | outputFile: '[name][ext]', 18 | }, 19 | { 20 | format: 'cjs', 21 | inputFiles: ['**/*'], 22 | outputDir: 'lib/cjs', 23 | outputFile: '[name][ext]', 24 | }, 25 | ], 26 | 27 | typesOutputDir: 'types', 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/CacheOption.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import type { PropType } from 'vue'; 4 | 5 | export type CacheOption = { 6 | value: string | number | boolean | object; 7 | currentLabel: string | number; 8 | isDisabled: boolean; 9 | }; 10 | 11 | const CacheOptions = Vue.extend({ 12 | inject: ['select'], 13 | data() { 14 | return { 15 | oldValues: [], 16 | }; 17 | }, 18 | props: { 19 | data: { 20 | type: Array as PropType, 21 | default: () => [], 22 | }, 23 | }, 24 | watch: { 25 | data() { 26 | this.update(); 27 | }, 28 | }, 29 | methods: { 30 | update() { 31 | const select = this['select'] as { 32 | $el: HTMLElement; 33 | cachedOptions: CacheOption[]; 34 | setSelected: () => any; 35 | }; 36 | 37 | let changed = false; 38 | 39 | this.data.forEach((item) => { 40 | if ( 41 | !select.cachedOptions.some((cached) => cached.value === item.value) 42 | ) { 43 | select.cachedOptions.push(item); 44 | changed = true; 45 | } 46 | }); 47 | 48 | if (changed) { 49 | select.setSelected(); 50 | } 51 | }, 52 | }, 53 | render() { 54 | return undefined; 55 | }, 56 | mounted() { 57 | this.update(); 58 | }, 59 | }); 60 | 61 | export default CacheOptions; 62 | -------------------------------------------------------------------------------- /src/components/ElSelect.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export default function getElSelect() { 4 | if (getElSelect._cache) return getElSelect._cache; 5 | 6 | const ElSelect = Vue.component('ElSelect'); 7 | 8 | return (getElSelect._cache = { 9 | extends: ElSelect, 10 | components: { 11 | ElSelectMenu: getElSelectMenu(), 12 | }, 13 | }); 14 | } 15 | 16 | getElSelect._cache = null; 17 | 18 | export function getElSelectMenu() { 19 | if (getElSelectMenu._cache) return getElSelectMenu._cache; 20 | 21 | const ElSelect = Vue.component('ElSelect'); 22 | const ElSelectMenu = ElSelect.options.components.ElSelectMenu; 23 | 24 | return (getElSelectMenu._cache = { 25 | extends: ElSelectMenu, 26 | render(h) { 27 | return h( 28 | 'div', 29 | { 30 | class: [ 31 | { 32 | 'el-select-dropdown': true, 33 | 'el-popper': true, 34 | 'is-multiple': this.$parent.multiple, 35 | }, 36 | this.popperClass, 37 | ], 38 | style: { minWidth: this.minWidth }, 39 | }, 40 | [ 41 | this.$parent.$scopedSlots.header && 42 | h( 43 | 'div', 44 | { staticClass: 'el-select-dropdown__header' }, 45 | this.$parent.$scopedSlots.header(), 46 | ), 47 | this.$scopedSlots.default(), 48 | this.$parent.$scopedSlots.footer && 49 | h( 50 | 'div', 51 | { staticClass: 'el-select-dropdown__footer' }, 52 | this.$parent.$scopedSlots.footer(), 53 | ), 54 | ], 55 | ); 56 | }, 57 | }); 58 | } 59 | 60 | getElSelectMenu._cache = null; 61 | -------------------------------------------------------------------------------- /src/components/ElSelectTree.vue: -------------------------------------------------------------------------------- 1 | 435 | 436 | 439 | -------------------------------------------------------------------------------- /src/components/ElSelectTreeOption.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | /** 4 | * create when use 5 | */ 6 | export default function getElSelectTreeOption() { 7 | if (getElSelectTreeOption._cache) return getElSelectTreeOption._cache; 8 | 9 | return (getElSelectTreeOption._cache = { 10 | extends: Vue.component('ElOption'), 11 | methods: { 12 | // 拦截点击事件,事件移至 node 节点上 13 | selectOptionClick() { 14 | // $parent === slot-scope 15 | // $parent.$parent === el-tree-node 16 | this.$parent.$parent.handleClick(); 17 | }, 18 | }, 19 | }); 20 | } 21 | 22 | getElSelectTreeOption._cache = null; 23 | -------------------------------------------------------------------------------- /src/components/ElSelectTreeVirtual.vue: -------------------------------------------------------------------------------- 1 | 198 | -------------------------------------------------------------------------------- /src/components/ElSelectVirtual.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import { getElSelectMenu } from '@/components/ElSelect'; 4 | 5 | /** 6 | * create when use 7 | */ 8 | export default function getElSelectVirtual() { 9 | if (getElSelectVirtual._cache) return getElSelectVirtual._cache; 10 | 11 | const ElSelect = Vue.component('ElSelect'); 12 | 13 | return (getElSelectVirtual._cache = { 14 | extends: { 15 | ...ElSelect.options, 16 | watch: { 17 | ...ElSelect.options.watch, 18 | options: undefined, 19 | }, 20 | }, 21 | components: { 22 | ElSelectMenu: getElSelectMenu(), 23 | }, 24 | props: {}, 25 | watch: { 26 | // fork from node_modules/element-ui/packages/select/src/select.vue#427 27 | options() { 28 | if (this.$isServer) return; 29 | this.$nextTick(() => { 30 | this.broadcast('ElSelectDropdown', 'updatePopper'); 31 | }); 32 | if (this.multiple) { 33 | this.resetInputHeight(); 34 | } 35 | const inputs = this.$el.querySelectorAll('input'); 36 | if ( 37 | [].indexOf.call(inputs, document.activeElement) === -1 && 38 | // fix: virtual filter keywords lose when any operations 39 | !this.multiple && 40 | !this.query 41 | ) { 42 | this.setSelected(); 43 | } 44 | if ( 45 | this.defaultFirstOption && 46 | (this.filterable || this.remote) && 47 | this.filteredOptionsCount 48 | ) { 49 | this.checkDefaultFirstOption(); 50 | } 51 | }, 52 | }, 53 | }); 54 | } 55 | 56 | getElSelectVirtual._cache = null; 57 | -------------------------------------------------------------------------------- /src/components/ElTreeNodeVirtual.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | /** 4 | * create when use 5 | */ 6 | export default function getElTreeNodeVirtual() { 7 | if (getElTreeNodeVirtual._cache) return getElTreeNodeVirtual._cache; 8 | 9 | const ElTreeNode = Vue.component('ElTree').options.components.ElTreeNode; 10 | 11 | return (getElTreeNodeVirtual._cache = { 12 | name: 'ElTreeNode', 13 | extends: ElTreeNode, 14 | methods: { 15 | getDataKeys(data) { 16 | const storeVirtual = this.tree.storeVirtual; 17 | return data.map((item) => storeVirtual.getNode(item).key); 18 | }, 19 | handleCheckChange(value, ev) { 20 | this.node.setChecked(ev.target.checked, !this.tree.checkStrictly); 21 | 22 | // use virtual check 23 | 24 | const storeVirtual = this.tree.storeVirtual; 25 | const nodeVirtual = storeVirtual.getNode(this.node.key); 26 | nodeVirtual.setChecked(ev.target.checked, !this.tree.checkStrictly); 27 | 28 | this.$nextTick(() => { 29 | const checkedNodes = storeVirtual.getCheckedNodes(); 30 | const checkedKeys = this.getDataKeys(checkedNodes); 31 | 32 | const halfCheckedNodes = storeVirtual.getHalfCheckedNodes(); 33 | const halfCheckedKeys = this.getDataKeys(halfCheckedNodes); 34 | 35 | this.tree.$emit('check', this.node.data, { 36 | checkedNodes, 37 | checkedKeys, 38 | halfCheckedNodes, 39 | halfCheckedKeys, 40 | }); 41 | }); 42 | }, 43 | }, 44 | }); 45 | } 46 | 47 | getElTreeNodeVirtual._cache = null; 48 | -------------------------------------------------------------------------------- /src/components/ElTreeVirtual.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import getElTreeNodeVirtual from '@/components/ElTreeNodeVirtual'; 4 | import { banReactive } from '@/components/utils'; 5 | 6 | /** 7 | * create when use 8 | */ 9 | export default function getElTreeVirtual() { 10 | if (getElTreeVirtual._cache) return getElTreeVirtual._cache; 11 | 12 | const ElTree = Vue.component('ElTree'); 13 | 14 | return (getElTreeVirtual._cache = { 15 | extends: ElTree.options, 16 | components: { 17 | ElTreeNode: getElTreeNodeVirtual(), 18 | }, 19 | props: { 20 | dataVirtual: Array, 21 | }, 22 | data() { 23 | return { 24 | storeVirtual: null, 25 | }; 26 | }, 27 | watch: { 28 | dataVirtual(newVal) { 29 | this.storeVirtual.setData(newVal); 30 | }, 31 | }, 32 | methods: { 33 | setCheckedNodes(nodes, leafOnly) { 34 | if (!this.nodeKey) 35 | throw new Error('[Tree] nodeKey is required in setCheckedNodes'); 36 | this.store.setCheckedNodes(nodes, leafOnly); 37 | this.storeVirtual.setCheckedNodes(nodes, leafOnly); 38 | }, 39 | setCheckedKeys(keys, leafOnly) { 40 | if (!this.nodeKey) 41 | throw new Error('[Tree] nodeKey is required in setCheckedKeys'); 42 | this.store.setCheckedKeys(keys, leafOnly); 43 | this.storeVirtual.setCheckedKeys(keys, leafOnly); 44 | }, 45 | setChecked(data, checked, deep) { 46 | this.store.setChecked(data, checked, deep); 47 | this.storeVirtual.setChecked(data, checked, deep); 48 | }, 49 | getCheckedNodes(leafOnly, includeHalfChecked) { 50 | return this.storeVirtual.getCheckedNodes(leafOnly, includeHalfChecked); 51 | }, 52 | getCheckedKeys(leafOnly) { 53 | return this.storeVirtual.getCheckedKeys(leafOnly); 54 | }, 55 | getHalfCheckedNodes() { 56 | return this.storeVirtual.getHalfCheckedNodes(); 57 | }, 58 | getHalfCheckedKeys() { 59 | return this.storeVirtual.getHalfCheckedKeys(); 60 | }, 61 | }, 62 | mounted() { 63 | const TreeStore = this.store.constructor; 64 | 65 | this.storeVirtual = banReactive( 66 | new TreeStore({ 67 | ...this.$props, 68 | key: this.nodeKey, 69 | data: this.dataVirtual, 70 | }), 71 | ); 72 | }, 73 | }); 74 | } 75 | 76 | getElTreeVirtual._cache = null; 77 | -------------------------------------------------------------------------------- /src/components/style.scss: -------------------------------------------------------------------------------- 1 | @import "element-ui/packages/theme-chalk/src/common/var"; 2 | 3 | .el-select-tree { 4 | &__popper { 5 | .el-tree { 6 | // fix: checkbox 在展示下拉框时跳动问题 7 | .el-checkbox__input { 8 | display: flex; 9 | } 10 | } 11 | 12 | .el-select-dropdown__item { 13 | flex: 1; 14 | padding: 0 30px 0 0; 15 | background: transparent !important; 16 | 17 | // fix: 节点展开时 popper 底部抖动问题 18 | // https://github.com/yujinpan/el-select-tree/pull/33 19 | height: 20px; 20 | line-height: 20px; 21 | 22 | &.selected:after { 23 | right: 10px; 24 | } 25 | } 26 | 27 | .el-select-dropdown { 28 | &__header, 29 | &__footer { 30 | padding: 10px; 31 | } 32 | &__header { 33 | border-bottom: 1px solid $--border-color-light; 34 | } 35 | &__footer { 36 | border-top: 1px solid $--border-color-light; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { compareArrayChanges, getParentKeys } from './utils'; 4 | 5 | describe('utils', () => { 6 | it('should getParentKeys', () => { 7 | expect( 8 | getParentKeys(['1'], [{ value: '1' }], (prop, data) => data[prop]), 9 | ).toEqual([]); 10 | expect( 11 | getParentKeys( 12 | ['2'], 13 | [{ value: '1', children: [{ value: '2' }] }], 14 | (prop, data) => data[prop], 15 | ), 16 | ).toEqual(['1']); 17 | expect( 18 | getParentKeys( 19 | ['3'], 20 | [ 21 | { 22 | value: '1', 23 | children: [{ value: '2', children: [{ value: '3' }] }], 24 | }, 25 | ], 26 | (prop, data) => data[prop], 27 | ), 28 | ).toEqual(['1', '2']); 29 | expect( 30 | getParentKeys( 31 | ['3'], 32 | [ 33 | { value: 'else1', children: [{ value: 'else2' }] }, 34 | { 35 | value: '1', 36 | children: [ 37 | { value: 'else3', children: [{ value: 'else4' }] }, 38 | { value: '2', children: [{ value: '3' }] }, 39 | ], 40 | }, 41 | ], 42 | (prop, data) => data[prop], 43 | ), 44 | ).toEqual(['1', '2']); 45 | }); 46 | it('should compareArrayChanges', () => { 47 | expect(compareArrayChanges([], [])).toEqual({ 48 | add: [], 49 | remove: [], 50 | }); 51 | expect(compareArrayChanges([], [1])).toEqual({ 52 | add: [1], 53 | remove: [], 54 | }); 55 | expect(compareArrayChanges([], [1, 2])).toEqual({ 56 | add: [1, 2], 57 | remove: [], 58 | }); 59 | expect(compareArrayChanges([1], [1, 2])).toEqual({ 60 | add: [2], 61 | remove: [], 62 | }); 63 | expect(compareArrayChanges([1, 2], [1, 2])).toEqual({ 64 | add: [], 65 | remove: [], 66 | }); 67 | expect(compareArrayChanges([1, 2], [1])).toEqual({ 68 | add: [], 69 | remove: [2], 70 | }); 71 | expect(compareArrayChanges([1, 2], [1])).toEqual({ 72 | add: [], 73 | remove: [2], 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/components/utils.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | export const ElSelectMixinOptions = { 4 | model: { 5 | prop: 'value', 6 | event: 'change', 7 | }, 8 | props: { 9 | name: String, 10 | id: String, 11 | value: { 12 | required: true, 13 | }, 14 | autocomplete: String, 15 | autoComplete: String, 16 | automaticDropdown: Boolean, 17 | size: String, 18 | disabled: Boolean, 19 | clearable: Boolean, 20 | filterable: Boolean, 21 | allowCreate: Boolean, 22 | loading: Boolean, 23 | popperClass: String, 24 | remote: Boolean, 25 | loadingText: String, 26 | noMatchText: String, 27 | noDataText: String, 28 | remoteMethod: Function, 29 | filterMethod: Function, 30 | multiple: Boolean, 31 | multipleLimit: Number, 32 | placeholder: String, 33 | defaultFirstOption: Boolean, 34 | reserveKeyword: Boolean, 35 | valueKey: String, 36 | collapseTags: Boolean, 37 | popperAppendToBody: { 38 | type: Boolean, 39 | default: true, 40 | }, 41 | }, 42 | }; 43 | export const ElSelectMixin = Vue.extend(ElSelectMixinOptions); 44 | 45 | export const ElTreeMixinOptions = { 46 | props: { 47 | data: { 48 | type: Array, 49 | default: () => [], 50 | }, 51 | emptyText: String, 52 | renderAfterExpand: { 53 | type: Boolean, 54 | default: true, 55 | }, 56 | nodeKey: String, 57 | checkStrictly: Boolean, 58 | defaultExpandAll: Boolean, 59 | expandOnClickNode: { 60 | type: Boolean, 61 | default: true, 62 | }, 63 | checkOnClickNode: Boolean, 64 | checkDescendants: Boolean, 65 | autoExpandParent: { type: Boolean, default: true }, 66 | defaultCheckedKeys: Array, 67 | defaultExpandedKeys: Array, 68 | currentNodeKey: [String, Number], 69 | renderContent: Function, 70 | showCheckbox: Boolean, 71 | draggable: Boolean, 72 | allowDrag: Function, 73 | allowDrop: Function, 74 | props: Object, 75 | lazy: Boolean, 76 | highlightCurrent: Boolean, 77 | load: Function, 78 | filterNodeMethod: Function, 79 | accordion: Boolean, 80 | indent: Number, 81 | iconClass: String, 82 | }, 83 | }; 84 | export const ElTreeMixin = Vue.extend(ElTreeMixinOptions); 85 | 86 | export function propsPick(props: Obj, keys: string[]) { 87 | const result: Obj = {}; 88 | keys.forEach((key) => { 89 | key in props && (result[key] = props[key]); 90 | }); 91 | return result; 92 | } 93 | 94 | export function toArr(val: any) { 95 | return Array.isArray(val) ? val : val || val === 0 ? [val] : []; 96 | } 97 | 98 | export function isValidArr(val: any) { 99 | return Array.isArray(val) && !!val.length; 100 | } 101 | 102 | export function isValidValue(val: any) { 103 | return val || val === 0; 104 | } 105 | 106 | export function getParentKeys( 107 | currentKeys: (number | string)[], 108 | data: Obj[], 109 | getValByProp: (prop: 'value' | 'children', data: Obj) => any, 110 | ) { 111 | const getKeys = (tree) => { 112 | const result = []; 113 | tree.forEach((node) => { 114 | const children = getValByProp('children', node); 115 | if (children && children.length) { 116 | if ( 117 | children.find((item) => 118 | currentKeys.includes(getValByProp('value', item)), 119 | ) 120 | ) { 121 | result.push(getValByProp('value', node)); 122 | } 123 | const childrenKeys = getKeys(children); 124 | if (childrenKeys.length) { 125 | result.push(getValByProp('value', node), ...childrenKeys); 126 | } 127 | } 128 | }); 129 | return result; 130 | }; 131 | 132 | const result = getKeys(data); 133 | 134 | return Array.from(new Set(result)); 135 | } 136 | 137 | type Value = string | number | (string | number)[]; 138 | 139 | export function cloneValue(val: Value) { 140 | return Array.isArray(val) ? [...val] : val; 141 | } 142 | 143 | export function isEqualsValue(val1: Value, val2: Value) { 144 | return ( 145 | val1 === val2 || 146 | (Array.isArray(val1) && 147 | Array.isArray(val2) && 148 | val1.toString() === val2.toString()) 149 | ); 150 | } 151 | 152 | export type Obj = { [p: string]: any }; 153 | 154 | type TreeCallback = ( 155 | data: T, 156 | index: number, 157 | array: T[], 158 | parent?: T, 159 | ) => R; 160 | 161 | type TreeNodeData = Obj; 162 | 163 | type TreeFindCallback = TreeCallback; 164 | 165 | export function treeFind( 166 | treeData: T[], 167 | findCallback: TreeFindCallback, 168 | getChildren: (data: T) => T[], 169 | ): T | undefined; 170 | export function treeFind( 171 | treeData: T[], 172 | findCallback: TreeFindCallback, 173 | getChildren: (data: T) => T[], 174 | resultCallback?: TreeCallback, 175 | parent?: T, 176 | ): R | undefined; 177 | export function treeFind( 178 | treeData: T[], 179 | findCallback: TreeFindCallback, 180 | getChildren: (data: T) => T[], 181 | resultCallback?: TreeCallback, 182 | parent?: T, 183 | ): T | R | undefined { 184 | for (let i = 0; i < treeData.length; i++) { 185 | const data = treeData[i]; 186 | if (findCallback(data, i, treeData, parent)) { 187 | return resultCallback ? resultCallback(data, i, treeData, parent) : data; 188 | } else { 189 | const children = getChildren(data); 190 | if (isValidArr(children)) { 191 | const find = treeFind( 192 | children, 193 | findCallback, 194 | getChildren, 195 | resultCallback, 196 | data, 197 | ); 198 | if (find) return find; 199 | } 200 | } 201 | } 202 | } 203 | 204 | export function treeEach( 205 | treeData: T[], 206 | callback: TreeCallback, 207 | getChildren: (data: T) => T[], 208 | parent?: T, 209 | ) { 210 | for (let i = 0; i < treeData.length; i++) { 211 | const data = treeData[i]; 212 | callback(data, i, treeData, parent); 213 | 214 | const children = getChildren(data); 215 | if (isValidArr(children)) { 216 | treeEach(children, callback, getChildren, data); 217 | } 218 | } 219 | } 220 | 221 | export async function treeFilter( 222 | data: Obj[], 223 | callback: (node: Obj) => boolean, 224 | propChildren = 'children', 225 | state: { stop: boolean }, 226 | ) { 227 | let startTime = Date.now(); 228 | const handleData = async (data: Obj[], result: Obj[] = []) => { 229 | if (state.stop) return Promise.reject(); 230 | if (!data.length) return result; 231 | 232 | // rest/50ms 233 | const endTime = Date.now(); 234 | if (endTime - startTime > 50) { 235 | await new Promise((resolve) => { 236 | startTime = endTime; 237 | setTimeout(resolve); 238 | }); 239 | } 240 | 241 | const node = data[0]; 242 | let hasChildren = false; 243 | const newItem = { ...node }; 244 | const children = node[propChildren]; 245 | if (isValidArr(children)) { 246 | const newItemChildren = await handleData(children); 247 | if ((hasChildren = isValidArr(newItemChildren))) { 248 | newItem[propChildren] = newItemChildren; 249 | } else { 250 | newItem[propChildren] = null; 251 | } 252 | } 253 | if (callback(node) || hasChildren) { 254 | result.push(newItem); 255 | } 256 | 257 | return handleData(data.slice(1), result); 258 | }; 259 | 260 | return handleData(data); 261 | } 262 | 263 | export function compareArrayChangesAdd(source: any[], target: any[]) { 264 | source = source.concat([]); 265 | return target.filter((item) => { 266 | const index = source.indexOf(item); 267 | if (index !== -1) { 268 | source.splice(index, 1); 269 | return false; 270 | } else { 271 | return true; 272 | } 273 | }); 274 | } 275 | 276 | export function compareArrayChanges(source: any[], target: any[]) { 277 | return { 278 | add: compareArrayChangesAdd(source, target), 279 | remove: compareArrayChangesAdd(target, source), 280 | }; 281 | } 282 | 283 | export function spliceItem(array: any[], remove: any, ...add: any[]) { 284 | const index = array.indexOf(remove); 285 | if (index !== -1) { 286 | array.splice(index, 1, ...add); 287 | } 288 | } 289 | 290 | export function getCompoundVal( 291 | data: Obj, 292 | prop: any | ((...args: any[]) => any), 293 | ...args: any[] 294 | ) { 295 | if (prop instanceof Function) { 296 | return prop(data, ...args); 297 | } else { 298 | return data[prop]; 299 | } 300 | } 301 | 302 | const empty = Vue.observable({}); 303 | export const banReactive = (obj: Obj) => { 304 | obj.__ob__ = empty['__ob__']; 305 | return obj; 306 | }; 307 | 308 | export function debounce any>( 309 | cb: T, 310 | duration: number, 311 | ): T { 312 | let timeId: any; 313 | return ((...args) => { 314 | if (timeId) clearTimeout(timeId); 315 | timeId = setTimeout(() => { 316 | cb(...args); 317 | }, duration); 318 | }) as T; 319 | } 320 | 321 | export function throttle any>( 322 | cb: T, 323 | duration: number, 324 | ): T { 325 | let timeId: any; 326 | let stop = true; 327 | return ((...args) => { 328 | if (timeId) { 329 | stop = false; 330 | } else { 331 | cb(...args); 332 | timeId = setTimeout(() => { 333 | timeId = null; 334 | if (!stop) { 335 | cb(...args); 336 | stop = true; 337 | } 338 | }, duration); 339 | } 340 | }) as T; 341 | } 342 | -------------------------------------------------------------------------------- /src/components/virtual-list.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectDirective } from 'vue'; 2 | 3 | import { getCompoundVal, isValidArr, throttle } from '@/components/utils'; 4 | import type { Obj } from '@/components/utils'; 5 | 6 | export const virtualList: ObjectDirective< 7 | HTMLElement, 8 | { 9 | target: string; 10 | virtualStore: VirtualStore; 11 | } 12 | > = { 13 | inserted(el, bindings, vNode) { 14 | const { target, virtualStore } = bindings.value; 15 | const targetElem = el.querySelector(target) as HTMLElement; 16 | 17 | virtualStore.mount(targetElem); 18 | 19 | targetElem.prepend(virtualStore.sketchTopElem); 20 | targetElem.append(virtualStore.sketchBottomElem); 21 | 22 | const handleScroll = () => { 23 | const old = targetElem.scrollTop; 24 | virtualStore.updateScroll( 25 | targetElem.scrollTop, 26 | targetElem.clientHeight, 27 | () => { 28 | targetElem.scrollTop = old; 29 | vNode.componentInstance?.$nextTick(() => { 30 | targetElem.scrollTop = old; 31 | }); 32 | }, 33 | ); 34 | }; 35 | 36 | targetElem.addEventListener('scroll', handleScroll); 37 | }, 38 | }; 39 | 40 | export type VirtualStoreOptions = { 41 | sourceData: Obj[]; 42 | expandedKeys: any[]; 43 | itemHeight: number; 44 | valueProp: any | ((node: Obj) => any); 45 | childrenProp: string; 46 | }; 47 | 48 | export class VirtualStore { 49 | public data: Obj[] = []; 50 | 51 | constructor(private readonly options: VirtualStoreOptions) { 52 | this.updateScroll = throttle(this.updateScroll.bind(this), 15); 53 | } 54 | 55 | setOptions(options: Partial) { 56 | this.setScrollTop( 57 | options.sourceData && this.options.sourceData !== options.sourceData 58 | ? 0 59 | : undefined, 60 | ); 61 | 62 | Object.assign(this.options, options); 63 | 64 | this.updateScroll(); 65 | } 66 | 67 | setScrollTop(scrollTop: number = this.scrollTop) { 68 | this.scrollTop = scrollTop; 69 | if (this.scrollElem) this.scrollElem.scrollTop = scrollTop; 70 | this.updateScroll(); 71 | } 72 | 73 | scrollElem: HTMLElement; 74 | mount(el: HTMLElement) { 75 | this.scrollElem = el; 76 | } 77 | 78 | readonly sketchTopElem = document.createElement('div'); 79 | readonly sketchBottomElem = document.createElement('div'); 80 | 81 | private scrollTop = 0; 82 | private clientHeight = 0; 83 | public updateScroll( 84 | scrollTop: number = this.scrollTop, 85 | clientHeight: number = this.clientHeight, 86 | callback?: () => any, 87 | ) { 88 | this.scrollTop = scrollTop; 89 | this.clientHeight = clientHeight || this.options.itemHeight * 15; 90 | 91 | const result: Obj[] = []; 92 | let height = 0; 93 | let heightTop = 0; 94 | let heightBottom = 0; 95 | 96 | const add = ( 97 | node: Obj, 98 | data = result, 99 | minHeight = scrollTop - this.options.itemHeight * 3, 100 | maxHeight = scrollTop + this.clientHeight + this.options.itemHeight * 3, 101 | ) => { 102 | height += this.options.itemHeight; 103 | 104 | const nodeChildren = node[this.options.childrenProp] || []; 105 | const newNode = { ...node }; 106 | const newChildren = (newNode[this.options.childrenProp] = [].concat( 107 | nodeChildren, 108 | )); 109 | 110 | if (height < minHeight) { 111 | heightTop += this.options.itemHeight; 112 | } else if ( 113 | height >= minHeight && 114 | height - this.options.itemHeight < maxHeight 115 | ) { 116 | data.push(newNode); 117 | } else { 118 | heightBottom += this.options.itemHeight; 119 | } 120 | 121 | if (isValidArr(newChildren)) { 122 | if ( 123 | this.options.expandedKeys?.includes( 124 | getCompoundVal(newNode, this.options.valueProp), 125 | ) 126 | ) { 127 | newChildren.length = 0; 128 | nodeChildren.forEach((child) => 129 | add( 130 | child, 131 | newChildren, 132 | // fix parent will show when scroll fast 133 | scrollTop - this.options.itemHeight * 15, 134 | ), 135 | ); 136 | if (newChildren.length && !data.includes(newNode)) { 137 | heightTop -= this.options.itemHeight; 138 | height += this.options.itemHeight; 139 | data.push(newNode); 140 | } 141 | } else { 142 | // show less children when expanded 143 | newChildren.length = Math.min( 144 | Math.ceil(this.clientHeight / this.options.itemHeight), 145 | newChildren.length, 146 | ); 147 | } 148 | } 149 | }; 150 | this.options.sourceData.forEach((item) => add(item)); 151 | 152 | this.data = result; 153 | 154 | this.sketchTopElem.style.height = heightTop + 'px'; 155 | this.sketchBottomElem.style.height = heightBottom + 'px'; 156 | 157 | callback?.(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/dark-base.scss: -------------------------------------------------------------------------------- 1 | @use "sass:meta"; 2 | 3 | $font-color: #e8eaed !default; 4 | $bg-color: #202123 !default; 5 | $bg-color-regular: #282a2d !default; 6 | $hover-bg-color: rgba($bg-color, 0.8) !default; 7 | 8 | $--color-text-primary: $font-color !default; 9 | $--color-text-regular: rgba(255, 255, 255, 0.75) !default; 10 | $--color-text-secondary: rgba(255, 255, 255, 0.65) !default; 11 | $--color-text-placeholder: rgba(255, 255, 255, 0.55) !default; 12 | 13 | $--border-color-base: #434343 !default; 14 | $--border-color-light: rgba(255, 255, 255, 0.3) !default; 15 | $--border-color-lighter: rgba(255, 255, 255, 0.2) !default; 16 | $--border-color-extra-light: rgba(255, 255, 255, 0.1) !default; 17 | 18 | $--color-white: $bg-color-regular !default; 19 | $--color-black: $--color-text-placeholder !default; 20 | 21 | $--background-color-base: $bg-color-regular !default; 22 | 23 | $--select-option-hover-background: $hover-bg-color !default; 24 | 25 | $--box-shadow-light: 0 2px 12px 0 rgba(255, 255, 255, 0.1); 26 | 27 | /* required */ 28 | $--font-path: "element-ui/lib/theme-chalk/fonts"; 29 | 30 | @import "element-ui/packages/theme-chalk/src/base"; 31 | @import "element-ui/packages/theme-chalk/src/tree"; 32 | @import "element-ui/packages/theme-chalk/src/select"; 33 | @import "element-ui/packages/theme-chalk/src/option"; 34 | @import "./components/style"; 35 | -------------------------------------------------------------------------------- /src/dark.scss: -------------------------------------------------------------------------------- 1 | @use "sass:meta"; 2 | 3 | .dark { 4 | @include meta.load-css("./dark-base.scss"); 5 | } 6 | -------------------------------------------------------------------------------- /src/element-ui.ts: -------------------------------------------------------------------------------- 1 | // element-ui 所需包单独打包,不然会与主项目中的代码重复 2 | import './plugins/element'; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // 主组件 2 | import type { PluginObject } from 'vue'; 3 | 4 | import ElSelectTree from './components/ElSelectTree.vue'; 5 | import ElSelectTreeVirtualComponent from './components/ElSelectTreeVirtual.vue'; 6 | 7 | // Vue.use() 8 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 9 | // @ts-ignore 10 | ElSelectTree.install = (Vue) => { 11 | Vue.component('ElSelectTree', ElSelectTree); 12 | }; 13 | 14 | export const ElSelectTreeVirtual = 15 | ElSelectTreeVirtualComponent as typeof ElSelectTreeVirtualComponent & 16 | PluginObject; 17 | 18 | ElSelectTreeVirtual.install = (Vue) => { 19 | Vue.component('ElSelectTreeVirtual', ElSelectTreeVirtual); 20 | }; 21 | 22 | // Vue.component() 23 | export default ElSelectTree as typeof ElSelectTree & PluginObject; 24 | -------------------------------------------------------------------------------- /src/plugins/element.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 按需加载 element-ui 组件 3 | * @description 4 | * [参考官网 - 按需加载](https://element.eleme.cn/#/zh-CN/component/quickstart#an-xu-yin-ru) 5 | */ 6 | import Vue from 'vue'; 7 | import { Tree, Select, Option } from 'element-ui'; 8 | 9 | Vue.use(Tree); 10 | Vue.use(Select); 11 | Vue.use(Option); 12 | -------------------------------------------------------------------------------- /src/style.scss: -------------------------------------------------------------------------------- 1 | @import "element-ui/packages/theme-chalk/src/base"; 2 | @import "element-ui/packages/theme-chalk/src/tree"; 3 | @import "element-ui/packages/theme-chalk/src/select"; 4 | @import "element-ui/packages/theme-chalk/src/option"; 5 | 6 | @import "./dark"; 7 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.json", 3 | "include": [ 4 | "env.d.ts", 5 | "src/**/*", 6 | "src/**/*.vue", 7 | "dev/**/*", 8 | "dev/**/*.vue", 9 | "docs/**/*", 10 | "docs/**/*.vue", 11 | "package.json", 12 | ], 13 | "exclude": ["src/**/__tests__/*", "src/**/*.spec.*"], 14 | "compilerOptions": { 15 | "strict": false, 16 | "composite": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["./src/*"] 20 | }, 21 | "experimentalDecorators": true 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.node.json", 3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.json", 3 | "files": [], 4 | "compilerOptions": { 5 | "experimentalDecorators": true, 6 | }, 7 | "references": [ 8 | { 9 | "path": "./tsconfig.config.json" 10 | }, 11 | { 12 | "path": "./tsconfig.app.json" 13 | }, 14 | { 15 | "path": "./tsconfig.vitest.json" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "exclude": [], 4 | "compilerOptions": { 5 | "composite": true, 6 | "lib": ["esnext", "dom"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import vue2 from '@vitejs/plugin-vue2'; 4 | import vue2Jsx from '@vitejs/plugin-vue2-jsx'; 5 | import { resolve } from 'path'; 6 | import { resolveWithAlias } from 'path-ops'; 7 | import { defineConfig } from 'vite'; 8 | 9 | const alias = { 10 | '@': resolve('src'), 11 | }; 12 | 13 | // https://vitejs.dev/config/ 14 | export default defineConfig({ 15 | base: '/el-select-tree/', 16 | plugins: [ 17 | vue2(), 18 | vue2Jsx({ 19 | // fork from @vue/babel-preset-app 20 | babelPlugins: [ 21 | ['@babel/plugin-proposal-decorators', { legacy: true }], 22 | '@babel/plugin-proposal-class-properties', 23 | ], 24 | }), 25 | ], 26 | resolve: { 27 | alias, 28 | extensions: ['.vue', '.js', '.ts', '.jsx', '.tsx', '.json'], 29 | }, 30 | css: { 31 | preprocessorOptions: { 32 | scss: { 33 | // ignore external sass warnings for "10px / 2px" 34 | quietDeps: true, 35 | // resolve start path for "~", like: "~external/style/var.scss" 36 | importer: (url: string) => { 37 | return { 38 | file: resolveWithAlias( 39 | url.startsWith('~') ? url.slice(1) : url, 40 | alias, 41 | ), 42 | }; 43 | }, 44 | }, 45 | }, 46 | }, 47 | // https://vitest.dev/config/ 48 | test: { 49 | dir: 'src', 50 | }, 51 | }); 52 | --------------------------------------------------------------------------------