├── .babelrc
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── .prettierrc
├── LICENSE
├── README.md
├── _config.yml
├── dist
├── expand.svg
├── leaf.svg
├── oval.svg
├── parent.svg
├── vs-tree.css
├── vs-tree.esm.browser.js
└── vs-tree.js
├── package.json
├── public
├── .DS_Store
├── index.html
├── static
│ ├── css
│ │ ├── icon1.svg
│ │ ├── icon2.svg
│ │ └── index.css
│ ├── data.txt
│ ├── js
│ │ └── work.js
│ └── qq-group.jpg
├── 初始数据为数组.html
├── 加载动画.html
├── 单选示例.html
├── 基础示例.html
├── 复杂示例.html
├── 复选示例.html
├── 大数据量.html
├── 展开收起图标.html
├── 展示图标.html
├── 开启动画.html
├── 异步加载.html
├── 手风琴模式.html
├── 拖拽节点.html
├── 显示连接线.html
├── 最大可选.html
├── 格式化数据.html
├── 清空选中节点.html
├── 结合vue.html
├── 结合worker+indexDb.html
├── 自定义搜索节点.html
├── 自定义节点内容.html
├── 自定义节点内容2.html
├── 节点过滤.html
├── 获取选中节点.html
├── 面包屑.html
├── 项目实战.html
└── 鼠标右键事件.html
├── rollup.config.js
├── src
├── breadcrumb
│ ├── breadcrumb-item.js
│ └── index.ts
├── core
│ ├── index.js
│ ├── node.js
│ ├── store.js
│ └── utils.ts
├── less
│ └── vs-tree.less
├── main.js
├── virtual-list
│ ├── index.js
│ └── virtual.js
└── vue-plugin
│ └── index.js
├── tsconfig.json
├── types
└── src
│ ├── breadcrumb
│ ├── breadcrumb-item.d.ts
│ └── index.d.ts
│ ├── core
│ ├── index.d.ts
│ ├── node.d.ts
│ ├── store.d.ts
│ └── utils.d.ts
│ ├── main.d.ts
│ ├── virtual-list
│ ├── index.d.ts
│ └── virtual.d.ts
│ └── vue-plugin
│ └── index.d.ts
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-typescript"
5 | ]
6 | }
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /dist
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "extends": [
7 | "standard"
8 | ],
9 | "globals": {
10 | "Atomics": "readonly",
11 | "SharedArrayBuffer": "readonly"
12 | },
13 | "parserOptions": {
14 | "ecmaVersion": 2020,
15 | "sourceType": "module"
16 | },
17 | "rules": {
18 | "space-before-function-paren": "off"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .vscode
3 | .DS_Store
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | src
3 | public
4 | # yarn.lock should be ignored -> https://github.com/yarnpkg/yarn/issues/5406
5 | yarn.lock
6 | yarn-error.log
7 | .editorconfig
8 | .DS_Store
9 | .babelrc
10 | .eslintrc.json
11 | .eslintignore
12 | .gitignore
13 | CHANGELOG.md
14 |
15 | # GitHub pages
16 | CNAME
17 | _config.yml
18 |
19 | # build
20 | rollup.config.js
21 | tsconfig.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": false,
6 | "singleQuote": true,
7 | "trailingComma": "none",
8 | "bracketSpacing": true,
9 | "jsxBracketSameLine": true,
10 | "arrowParens": "avoid",
11 | "proseWrap": "never",
12 | "format": {
13 | "insertSpaceBeforeFunctionParenthesis": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 yangjingyu
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 | # vs-tree2.0
2 |
3 | 极简树组件, 无任何依赖【麻雀虽小,五脏俱全】
4 |
5 | ## 浏览器支持
6 |
7 | |  |  |  |  |  |
8 | | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
9 | | Latest ✔ | Latest ✔ | Latest ✔ | Latest ✔ | Latest ✔ |
10 |
11 | ## 功能点
12 |
13 | * [x] 基础树组件
14 | * [x] 层级面包屑
15 | * [x] 复选框
16 | * [x] 单选框
17 | * [x] 异步加载数据
18 | * [x] 虚拟列表
19 | * [x] 拖拽节点
20 | * [x] 手风琴
21 | * [x] 树内搜索
22 | * [x] 自定义图标
23 | * [x] 连接线
24 | * [x] 最大可选
25 | * [x] 节点右键事件
26 | * [x] 自定义格式化数据
27 | * [x] 支持Vue组件
28 |
29 | ## DEMO
30 |
31 | [跳转到DEMO](https://yangjingyu.github.io/vs-tree/public/index.html)
32 |
33 | ## 安装
34 |
35 | ```shell
36 | npm install vs-tree
37 | ```
38 |
39 | 或
40 |
41 | ```shell
42 | yarn add vs-tree
43 | ```
44 |
45 | ## use
46 |
47 | ```html
48 |
49 | ```
50 |
51 | ```js
52 | import vsTree from 'vs-tree'
53 | import 'vs-tree/dist/vs-tree.css'
54 |
55 | const tree = new vsTree('#tree', {
56 | data: {id: 1, name: 'tree1', children: []} // [{id, name}, {id, name, children}]
57 | });
58 | ```
59 |
60 | ---
61 |
62 | 直接引入js
63 |
64 | ```html
65 |
66 |
67 |
68 | ```
69 |
70 | ```js
71 | const tree = new vsTree.default('#tree', {
72 | data: {id: 1, name: 'tree1', children: []} // [{id, name}, {id, name, children}]
73 | });
74 | ```
75 |
76 | ---
77 |
78 | 支持浏览器模块
79 |
80 | ```html
81 |
87 | ```
88 |
89 | ## Vue2.x use
90 |
91 | ```js
92 | // main.js
93 | import { install } from 'vs-tree'
94 | import 'vs-tree/dist/vs-tree.css'
95 |
96 | Vue.use(install)
97 | ```
98 |
99 | ```html
100 |
101 |
102 |
103 |
104 |
105 |
106 |
141 | ```
142 |
143 | ### Options
144 |
145 | | Input | Desc | Type | Default |
146 | | ---------------- | ---------------------------------------------------- | --------------------- | ------------------- |
147 | | el | 选择器, 或 HTMLElement | string 或 HTMLElement | |
148 | | data | 展示数据 | Object、 Array | |
149 | | async | 延时渲染 | Boolean | false |
150 | | hideRoot | 是否展示根节点 | Boolean | false |
151 | | showLine | 是否展示连接线 | Boolean | false |
152 | | showIcon | 是否显示图标 | Boolean | false |
153 | | onlyShowLeafIcon | 是否仅显示叶子节点图标 | Boolean | false |
154 | | showCheckbox | 是否显示复选框 | Boolean | false |
155 | | checkboxType | 父子节点关联关系 | Object | checkboxTypeOptions |
156 | | checkInherit | 新加入节点时自动继承父节点选中状态 | Boolean | false |
157 | | showRadio | 是否显示单选框,会覆盖复选框 | Boolean | false |
158 | | radioType | 分组范围 | String | 'all' |
159 | | disabledInherit | 新加入节点时自动继承父节点禁用状态 | Boolean | false |
160 | | highlightCurrent | 是否高亮选中当前项 | Boolean | false |
161 | | accordion | 手风琴模式 | Boolean | false |
162 | | animation | 开启动画 | Boolean | false |
163 | | draggable | 开启拖拽 | Boolean | false |
164 | | dropable | 允许放置 | Boolean | false |
165 | | nocheckParent | 禁止父节点选中 | Boolean | false |
166 | | sort | 对选中列表排序 | Boolean | false |
167 | | checkOnClickNode | 是否在点击节点的时候选中节点 | Boolean | false |
168 | | lazy | 异步加载节点 | Boolean | false |
169 | | strictLeaf | 严格依赖isLeaf,不提供时如无子节点则不渲染展开图标 | Boolean | false |
170 | | max | 最大可选数量 | Number | 0 |
171 | | checkFilterLeaf | 选中结果过滤掉叶子节点, 异步加载时需手需提供 isLeaf | Boolean | false |
172 | | rootName | 根节点名称,仅 data 为数组时有效,此时不会默认 | String | null |
173 | | expandClass | 展开收起图标class | String | vs-expand-icon |
174 | | theme | 皮肤风格,仅支持 'element' | String | null |
175 | | breadcrumb | 面包屑功能,只展示一层节点 | Object | null |
176 | | disabledKeys | 禁止操作 | Array | null |
177 | | checkedKeys | 默认选中 | Array | null |
178 | | expandKeys | 默认展开 | Array | null |
179 | | expandLevel | 默认展开级数, 0 不展开 -1 全部展开 | Number | 1 |
180 | | indent | 缩进 | Number | 10 |
181 | | virtual | 虚拟列表配置信息 | Object | virtualOptions |
182 | | maxHeight | 组件最大高度 | String、Number | 400px |
183 | | minHeight | 组件最大高度 | String、Number | 0px |
184 |
185 | ### checkboxTypeOptions
186 |
187 | | options | Desc | 默认 |
188 | | ------- | ------------ | ---- |
189 | | Y | 勾选后情况 | 'ps' |
190 | | N | 取消勾选情况 | 'ps' |
191 |
192 | > p 表示操作影响父节点
193 | > s 表示操作影响子节点
194 |
195 | ### radioType
196 |
197 | > all 表示全局范围内分组
198 | > level 表示每级节点内分组
199 |
200 | ### virtualOptions
201 |
202 | | options | Desc | 默认 |
203 | | ---------- | -------------------- | ---- |
204 | | showCount | 视图内展示多少条数据 | 20 |
205 | | itemHeight | 每条的高度 | 26 |
206 |
207 | ### breadcrumb
208 |
209 |
210 | | options | Desc | 默认 |
211 | | --------- | ------------------------- | -------------------- |
212 | | el | Selector, HtmlElement | 内部创建根节点 |
213 | | icon | string, ELement, Function | null |
214 | | link | string, ELement, Function | null |
215 | | separator | string, ELement, Function | null |
216 | | change | Event | dom, node[], current |
217 |
218 | ### 方法
219 |
220 | `Tree` 内部使用了 Node 类型的对象来包装用户传入的数据,用来保存目前节点的状态。
221 | `Tree` 拥有如下方法:
222 |
223 | | Methods | 说明 | 参数 |
224 | | ----------------- | ---------------------- | ----------------------- |
225 | | getCheckedNodes | 获取选中节点 | - |
226 | | getNodeById | 根据 ID 获取 Node 节点 | id |
227 | | setMaxValue | 设置最大可选 | number |
228 | | scrollToIndex | 滚动到索引位置 | number |
229 | | clearCheckedNodes | 清除选中节点 | - |
230 | | filter | 过滤节点 | keyword, onlySearchLeaf |
231 |
232 | > onlySearchLeaf 只过滤叶子节点
233 |
234 | ### Node 方法
235 |
236 | `Node` 拥有如下方法:
237 |
238 | | Methods | 说明 | 参数 |
239 | | ----------- | ------------ | ---------- |
240 | | setChecked | 设置是否选中 | true,false |
241 | | setDisabled | 设置禁止操作 | true,false |
242 | | remove | 删除当前节点 | - |
243 | | append | 追加节点 | data |
244 |
245 | ### Events
246 |
247 | | 事件名称 | 说明 | 回调参数 | 返回值 |
248 | | ------------- | ---------------------- | ------------------- | --------------------------- |
249 | | click | 节点点击事件 | event, node | void |
250 | | beforeCheck | 节点选择前触发 | node | true,false |
251 | | check | 复选框被点击时触发 | event, node | void |
252 | | change | 复选框改变时触发 | [ node ] | void |
253 | | limitAlert | 超过 max 配置时触发 | - | void |
254 | | renderContent | 自定义节点内容 | h,node | h() 或 Dom |
255 | | load | lazy=true 时有效 | node, resolve | void |
256 | | checkFilter | 过滤掉的节点不计入统计 | node | true, false |
257 | | format | 格式化数据 | data | {name,children,isLeaf,icon} |
258 | | contextmenu | 鼠标右键事件 | event, node | void |
259 | | searchFilter | 搜索过滤 | keyword, node, data | node[] |
260 | | searchRender | 搜索渲染 | node, cloneNode | Element |
261 | | onDragstart | 开始拖拽 | e, node | void |
262 | | onDragenter | 进入放置目标 | e, node, dragPos | void |
263 | | onDrop | 放置目标 | e, node, dragPos | void |
264 |
265 | > searchRender 返回的 Element 不会影响原有dom
266 |
267 | #### renderContent
268 |
269 | h: 生成简单 dom 节点,当前仅支持以下配置
270 |
271 | ```js
272 | renderContent: function (h, node) {
273 | return h("div", {
274 | className: "tree-action",
275 | children: [
276 | h("a", {
277 | text: 'append',
278 | click: function (e, node) {
279 | node.append({
280 | id: id++,
281 | name: 'append'
282 | })
283 | }
284 | }),
285 | h("a", {
286 | text: 'remove',
287 | click: function (e, node) {
288 | node.remove()
289 | }
290 | })
291 | ]
292 | })
293 | }
294 | ```
295 | 或
296 |
297 | ```js
298 | renderContent: function(h, node) {
299 | const append = document.createElement('a')
300 | append.innerText = 'append'
301 | dom.appendChild(append)
302 | append.onclick = () => {
303 | node.append({
304 | id: id++,
305 | name: 'append'
306 | })
307 | }
308 | return append
309 | }
310 | ```
311 |
312 | ### load
313 |
314 | resolve 异步加载完成后回调
315 |
316 | ```js
317 | lazy: true,
318 | load: function (node, resolve) {
319 | setTimeout(() => {
320 | resolve([{
321 | id: id++,
322 | name: '新叶子节点' + id,
323 | isLeaf: true
324 | }])
325 | }, 1000)
326 | }
327 | ```
328 |
329 | ### format
330 |
331 | 目前仅支持,id, name、children、isLeaf、icon、extra
332 |
333 | ```js
334 | format: function(data) {
335 | return {
336 | name: data.title,
337 | children: data.child,
338 | isLeaf: !data.child,
339 | icon: 'custom-icon' || document.createElement
340 | }
341 | }
342 | ```
343 |
344 |
345 | ## Tips
346 |
347 | 1. maxHeight 高度变大后 `showCount` 也要相应变大,不然滑动到底部后数据展示不全,会出现空白.
348 | 2. minHeight 可以配置最小高度,当 minHeight 和 maxHeight 配置相同的高度时,可以固定高度
349 | 3. 如果发现vs-tree组件不显示数据渲染结果为空,则在vs-tree组件上加v-if="list.length > 0" 判断下等数据加载完毕后进行渲染
350 | 4. itemHeight 是用于内部计算,dom元素真是高度需要用css指定
351 | 5. lazy为true时需手动添加isLeaf标识
352 |
353 | ## License
354 |
355 | [MIT License](https://github.com/yangjingyu/vs-tree/blob/master/LICENSE).
356 |
357 | ## QQ交流群(860150548)
358 |
359 |
360 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/dist/expand.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dist/leaf.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dist/oval.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dist/parent.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dist/vs-tree.css:
--------------------------------------------------------------------------------
1 | .vs-loading {
2 | min-height: 100px;
3 | background-image: url(./oval.svg);
4 | background-position: center center;
5 | background-repeat: no-repeat;
6 | }
7 | .vs-tree-node {
8 | height: 26px;
9 | cursor: pointer;
10 | color: #606266;
11 | font-size: 14px;
12 | display: -webkit-box;
13 | display: -ms-flexbox;
14 | display: flex;
15 | -webkit-box-align: center;
16 | -ms-flex-align: center;
17 | align-items: center;
18 | -webkit-box-pack: justify;
19 | -ms-flex-pack: justify;
20 | justify-content: space-between;
21 | white-space: nowrap;
22 | padding: 0 0 2px;
23 | -webkit-box-sizing: border-box;
24 | box-sizing: border-box;
25 | }
26 | .vs-tree-node:hover {
27 | background-color: #eee;
28 | }
29 | .vs-tree-node:first-child .expand::before {
30 | height: 0;
31 | }
32 | .vs-indent-unit {
33 | position: relative;
34 | display: inline-block;
35 | width: 14px;
36 | height: 14px;
37 | vertical-align: middle;
38 | }
39 | .vs-indent-unit::after {
40 | content: "";
41 | width: 0;
42 | height: 160%;
43 | position: absolute;
44 | left: 50%;
45 | border-left: 1px dashed #ddd;
46 | top: -8px;
47 | }
48 | .vs-loading-unit.is-loading {
49 | width: 14px;
50 | height: 14px;
51 | margin-right: 5px;
52 | display: inline-block;
53 | vertical-align: middle;
54 | }
55 | .vs-loading-unit.is-loading::after {
56 | content: "";
57 | width: 14px;
58 | height: 14px;
59 | display: inline-block;
60 | vertical-align: top;
61 | }
62 | .expand,
63 | .expand-empty {
64 | width: 14px;
65 | height: 14px;
66 | line-height: 10px;
67 | display: inline-block;
68 | margin-right: 5px;
69 | color: #bbb;
70 | text-align: center;
71 | -webkit-box-sizing: border-box;
72 | box-sizing: border-box;
73 | vertical-align: middle;
74 | }
75 | .expand {
76 | position: relative;
77 | cursor: pointer;
78 | }
79 | .expand.vs-expand-icon::after {
80 | content: "";
81 | width: 14px;
82 | height: 14px;
83 | display: inline-block;
84 | background-image: url(./expand.svg);
85 | background-size: 10px 10px;
86 | background-repeat: no-repeat;
87 | background-position: center center;
88 | -webkit-transform: rotate(-90deg);
89 | transform: rotate(-90deg);
90 | -webkit-transition: -webkit-transform 0.3s;
91 | transition: -webkit-transform 0.3s;
92 | transition: transform 0.3s;
93 | transition: transform 0.3s, -webkit-transform 0.3s;
94 | }
95 | .expanded {
96 | color: #bbb;
97 | }
98 | .expanded.vs-expand-icon::after {
99 | -webkit-transform: rotate(0);
100 | transform: rotate(0);
101 | }
102 | .expand.is-loading::after,
103 | .vs-loading-unit.is-loading::after {
104 | background-image: url(./oval.svg);
105 | background-repeat: no-repeat;
106 | background-size: 14px 14px;
107 | border: none;
108 | color: transparent;
109 | }
110 | .vs-indent-unit ~ .expand::before {
111 | content: "";
112 | position: absolute;
113 | top: -50%;
114 | left: 50%;
115 | width: 0;
116 | height: 50%;
117 | margin-top: -25%;
118 | border-left: 1px dashed #ddd;
119 | }
120 | .vs-tree-node:not([vs-child]) + .vs-tree-node .vs-indent-unit ~ .expand::before {
121 | display: none;
122 | }
123 | .vs-indent-unit ~ .expand-empty {
124 | position: relative;
125 | }
126 | .vs-indent-unit ~ .expand-empty::after {
127 | content: "";
128 | position: absolute;
129 | top: 50%;
130 | left: 50%;
131 | width: 50%;
132 | margin-top: -1px;
133 | border-bottom: 1px dashed #ddd;
134 | }
135 | .vs-indent-unit ~ .expand-empty::before {
136 | content: "";
137 | position: absolute;
138 | top: -50%;
139 | left: 50%;
140 | height: 200%;
141 | border-left: 1px dashed #ddd;
142 | }
143 | .selected {
144 | background-color: #eee;
145 | }
146 | .vs-checkbox,
147 | .vs-radio {
148 | position: relative;
149 | color: #606266;
150 | font-weight: 500;
151 | cursor: pointer;
152 | display: inline-block;
153 | white-space: nowrap;
154 | -webkit-user-select: none;
155 | -moz-user-select: none;
156 | -ms-user-select: none;
157 | user-select: none;
158 | margin-right: 8px;
159 | vertical-align: middle;
160 | font-size: 0;
161 | }
162 | .vs-checkbox__input,
163 | .vs-radio__input {
164 | white-space: nowrap;
165 | cursor: pointer;
166 | outline: none;
167 | display: inline-block;
168 | line-height: 1;
169 | position: relative;
170 | vertical-align: middle;
171 | }
172 | .vs-checkbox__inner,
173 | .vs-radio__inner {
174 | display: inline-block;
175 | position: relative;
176 | border: 1px solid #d9d9d9;
177 | border-radius: 2px;
178 | -webkit-box-sizing: border-box;
179 | box-sizing: border-box;
180 | width: 14px;
181 | height: 14px;
182 | background-color: #FFFFFF;
183 | z-index: 1;
184 | -webkit-transition: border-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46), background-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46);
185 | transition: border-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46), background-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46);
186 | }
187 | .is-indeterminate .vs-checkbox__inner::before {
188 | content: '';
189 | position: absolute;
190 | display: block;
191 | background-color: #1989fa;
192 | height: 12px;
193 | -webkit-transform: scale(0.6);
194 | transform: scale(0.6);
195 | left: 0;
196 | right: 0;
197 | top: 0;
198 | border-radius: 2px;
199 | }
200 | .vs-checkbox__original:checked ~ .vs-checkbox__inner {
201 | background-color: #1989fa;
202 | border-color: #1989fa;
203 | }
204 | .vs-checkbox__original:checked ~ .vs-checkbox__inner::after {
205 | -webkit-transform: rotate(45deg) scaleY(1);
206 | transform: rotate(45deg) scaleY(1);
207 | }
208 | .vs-checkbox__inner::after {
209 | -webkit-box-sizing: content-box;
210 | box-sizing: content-box;
211 | content: "";
212 | border: 1px solid #FFFFFF;
213 | border-left: 0;
214 | border-top: 0;
215 | height: 7px;
216 | left: 4px;
217 | position: absolute;
218 | top: 1px;
219 | -webkit-transform: rotate(45deg) scaleY(0);
220 | transform: rotate(45deg) scaleY(0);
221 | width: 3px;
222 | -webkit-transition: all 0.2s cubic-bezier(0.12, 0.4, 0.29, 1.46) 0.1s;
223 | transition: all 0.2s cubic-bezier(0.12, 0.4, 0.29, 1.46) 0.1s;
224 | -webkit-transform-origin: center;
225 | transform-origin: center;
226 | }
227 | .vs-checkbox__original:disabled ~ .vs-checkbox__inner {
228 | background-color: #edf2fc;
229 | border-color: #dcdfe6;
230 | cursor: not-allowed;
231 | }
232 | .vs-checkbox__original:checked:disabled ~ .vs-checkbox__inner:after {
233 | border-color: #c0c4cc;
234 | }
235 | .vs-checkbox__original,
236 | .vs-radio__original {
237 | opacity: 0;
238 | outline: none;
239 | position: absolute;
240 | margin: 0;
241 | width: 0;
242 | height: 0;
243 | z-index: -1;
244 | }
245 | .vs-radio__inner {
246 | border-radius: 100%;
247 | }
248 | .vs-radio__inner:after {
249 | -webkit-box-sizing: content-box;
250 | box-sizing: content-box;
251 | content: "";
252 | left: 3px;
253 | position: absolute;
254 | top: 3px;
255 | width: 6px;
256 | height: 6px;
257 | -webkit-transform: scale(0);
258 | transform: scale(0);
259 | -webkit-transition: -webkit-transform 0.15s ease-in 0.05s;
260 | transition: -webkit-transform 0.15s ease-in 0.05s;
261 | transition: transform 0.15s ease-in 0.05s;
262 | transition: transform 0.15s ease-in 0.05s, -webkit-transform 0.15s ease-in 0.05s;
263 | -webkit-transform-origin: center;
264 | transform-origin: center;
265 | border-radius: 100%;
266 | }
267 | .vs-radio__original:checked ~ .vs-radio__inner {
268 | border-color: #1989fa;
269 | }
270 | .vs-radio__original:checked ~ .vs-radio__inner:after {
271 | background-color: #1989fa;
272 | -webkit-transform: scale(1);
273 | transform: scale(1);
274 | }
275 | .vs-radio__original:checked:disabled ~ .vs-radio__inner:after {
276 | border-color: #c0c4cc;
277 | }
278 | .vs-icon-leaf,
279 | .vs-icon-parent {
280 | width: 14px;
281 | height: 14px;
282 | margin-right: 5px;
283 | display: inline-block;
284 | vertical-align: middle;
285 | background-image: url(./leaf.svg);
286 | background-size: 12px 12px;
287 | background-repeat: no-repeat;
288 | background-position: center;
289 | }
290 | .vs-icon-leaf > img,
291 | .vs-icon-parent > img {
292 | width: 100%;
293 | height: 100%;
294 | }
295 | .vs-icon-parent {
296 | background-image: url(./parent.svg);
297 | }
298 | .vs-transition {
299 | height: 0;
300 | -webkit-transition: all 0.3s ease;
301 | transition: all 0.3s ease;
302 | overflow-y: hidden;
303 | }
304 | .vs-tree-node.vs-drag-enter {
305 | background-color: rgba(25, 137, 250, 0.8);
306 | color: #fff;
307 | }
308 | .vs-tree-node.vs-drag-over-gap-top,
309 | .vs-tree-node.vs-drag-over-gap-bottom {
310 | position: relative;
311 | }
312 | .vs-tree-node.vs-drag-over-gap-top::before,
313 | .vs-tree-node.vs-drag-over-gap-bottom::before {
314 | content: '';
315 | position: absolute;
316 | left: 0;
317 | width: 100%;
318 | height: 2px;
319 | background-color: #1989fa;
320 | }
321 | .vs-tree-node.vs-drag-over-gap-top::before {
322 | top: 0;
323 | }
324 | .vs-tree-node.vs-drag-over-gap-bottom::before {
325 | bottom: 0;
326 | }
327 | .vs-search-only-leaf .vs-tree-inner {
328 | padding-left: 0!important;
329 | }
330 | .vs-search-only-leaf .vs-tree-inner .expand-empty {
331 | display: none;
332 | }
333 | .vs-theme-element .is-indeterminate .vs-checkbox__inner {
334 | background-color: #1989fa;
335 | border-color: #1989fa;
336 | }
337 | .vs-theme-element .is-indeterminate .vs-checkbox__inner::before {
338 | background-color: #fff;
339 | height: 1px;
340 | width: 50%;
341 | top: 50%;
342 | left: 50%;
343 | -webkit-transform: translate(-50%, -50%) scale(1);
344 | transform: translate(-50%, -50%) scale(1);
345 | }
346 | .vs-breadcrumb {
347 | -webkit-box-sizing: border-box;
348 | box-sizing: border-box;
349 | margin: 0;
350 | padding: 0;
351 | font-variant: tabular-nums;
352 | line-height: 1.5715;
353 | list-style: none;
354 | -webkit-font-feature-settings: "tnum";
355 | font-feature-settings: "tnum";
356 | color: rgba(0, 0, 0, 0.45);
357 | font-size: 14px;
358 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
359 | }
360 | .vs-breadcrumb > span {
361 | display: inline-block;
362 | }
363 | .vs-breadcrumb > span:not(:last-child) {
364 | cursor: pointer;
365 | color: #1989fa;
366 | }
367 | .vs-breadcrumb > span:not(:last-child):hover {
368 | color: rgba(25, 137, 250, 0.8);
369 | }
370 | .vs-breadcrumb > span:last-child {
371 | color: rgba(0, 0, 0, 0.85);
372 | }
373 | .vs-breadcrumb-separator {
374 | margin: 0 8px;
375 | color: rgba(0, 0, 0, 0.45);
376 | }
377 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vs-tree",
3 | "version": "2.1.14",
4 | "license": "MIT",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/yangjingyu/vs-tree"
8 | },
9 | "homepage": "https://yangjingyu.github.io/vs-tree/",
10 | "bugs": "https://github.com/yangjingyu/vs-tree/issues",
11 | "author": {
12 | "name": "Yangjingyu",
13 | "url": "https://github.com/yangjingyu"
14 | },
15 | "main": "./dist/vs-tree.js",
16 | "types": "./types/src/main.d.ts",
17 | "module": "./dist/vs-tree.js",
18 | "unpkg": "./dist/vs-tree.js",
19 | "style": "./dist/vs-tree.css",
20 | "scripts": {
21 | "dev": "rollup -c --watch",
22 | "build": "NODE_ENV=production rollup -c",
23 | "lint": "eslint ./ --fix"
24 | },
25 | "devDependencies": {
26 | "@babel/core": "^7.12.9",
27 | "@babel/preset-env": "^7.12.7",
28 | "@babel/preset-typescript": "^7.12.7",
29 | "@rollup/plugin-babel": "^5.2.2",
30 | "@rollup/plugin-node-resolve": "^11.0.1",
31 | "autoprefixer": "8.0.0",
32 | "cssnano": "^4.1.10",
33 | "eslint-config-standard": "14.1.1",
34 | "eslint-plugin-standard": "^5.0.0",
35 | "less": "^3.12.2",
36 | "rollup": "^2.34.1",
37 | "rollup-plugin-commonjs": "^10.1.0",
38 | "rollup-plugin-eslint": "^7.0.0",
39 | "rollup-plugin-json": "^4.0.0",
40 | "rollup-plugin-postcss": "^4.0.0",
41 | "rollup-plugin-terser": "^7.0.2",
42 | "rollup-plugin-typescript2": "^0.29.0",
43 | "standard": "^16.0.3",
44 | "typescript": "^4.1.3"
45 | },
46 | "browserslist": [
47 | "last 2 version",
48 | "> 1%",
49 | "IE 10"
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/public/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangjingyu/vs-tree/7550e78ba2f3b5851e55055e70bbd7317cfe04d4/public/.DS_Store
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0
8 |
9 |
10 |
11 |
12 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/public/static/css/icon1.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/static/css/icon2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/static/css/index.css:
--------------------------------------------------------------------------------
1 | .menu a {
2 | line-height: 30px;
3 | color: #2a8ae2;
4 | }
5 |
6 | .box {
7 | height: 400px;
8 | display: flex;
9 | overflow: hidden;
10 | justify-content: space-between;
11 | }
12 |
13 | #list {
14 | height: 100%;
15 | padding: 0 20px;
16 | overflow-y: auto;
17 | }
18 |
19 | #search {
20 | width: 400px;
21 | height: 28px;
22 | padding: 0 12px;
23 | margin: 0 0 10px;
24 | font-size: 12px;
25 | box-sizing: border-box;
26 | }
27 |
28 | #popup {
29 | position: fixed;
30 | width: 100%;
31 | height: 100%;
32 | top: 0;
33 | left: 0;
34 | display: none;
35 | background-color: transparent;
36 | z-index: 9999;
37 | }
38 |
39 | .content {
40 | position: absolute;
41 | width: 300px;
42 | height: 500px;
43 | background-color: #333;
44 | }
45 |
46 | p {
47 | font-size: 12px;
48 | color: #333;
49 | }
50 |
51 | .custom-icon {
52 | background-image: url(./icon1.svg);
53 | }
54 |
55 | .tree-demo .vs-tree-node {
56 | justify-content: start;
57 | }
58 |
59 | .custom-expand {
60 | margin-right: 8px;
61 | }
62 |
63 | .custom-expand.expand::after {
64 | content: "+";
65 | border: 1px solid #bbb;
66 | width: 14px;
67 | height: 14px;
68 | display: inline-block;
69 | box-sizing: border-box;
70 | }
71 |
72 | .custom-expand.expanded::after {
73 | content: "-";
74 | }
--------------------------------------------------------------------------------
/public/static/js/work.js:
--------------------------------------------------------------------------------
1 | let list = []
2 | const umap = {}
3 | const infomap = {}
4 | const depts = []
5 | const root = []
6 | var xhr = new XMLHttpRequest()
7 | xhr.open('GET', typeof window === 'object' ? './static/data.txt' : '../data.txt', true)
8 | xhr.send()
9 | xhr.onload = function (e) {
10 | list = xhr.response.split('\r\n').map(v => v && JSON.parse(v))
11 | for (let i = 0, len = list.length; i < len; i++) {
12 | const v = list[i]
13 | if (v.obj === 'department_user') {
14 | if (umap[v.data.did]) {
15 | umap[v.data.did].push(v.data)
16 | } else {
17 | umap[v.data.did] = [v.data]
18 | }
19 | } else if (v.obj === 'user') {
20 | infomap[v.data.uid] = v.data
21 | } else if (v.obj === 'department') {
22 | if (v.data.pdid === '-1') {
23 | root.push(v)
24 | } else {
25 | depts.push(v)
26 | }
27 | }
28 | }
29 | console.log(infomap['100002955460'])
30 | postMessage && postMessage({
31 | id: 1,
32 | list: list,
33 | depts: depts,
34 | root: root,
35 | umap: umap,
36 | infomap: infomap
37 | })
38 | typeof window !== 'object' && close()
39 | }
40 |
--------------------------------------------------------------------------------
/public/static/qq-group.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangjingyu/vs-tree/7550e78ba2f3b5851e55055e70bbd7317cfe04d4/public/static/qq-group.jpg
--------------------------------------------------------------------------------
/public/初始数据为数组.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_初始数据为数组
8 |
9 |
10 |
11 |
12 |
13 | 原始数据为数组时,自动为数据包裹一层根节点,如果不设置rootName则showRoot默认设置为false
14 |
15 |
16 |
17 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/public/加载动画.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_加载动画Loading
8 |
9 |
10 |
11 |
12 |
13 | 20万+用时:
14 |
15 |
16 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/public/单选示例.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_单选示例
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/public/基础示例.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_基础示例
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
20 |
21 |
22 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/public/复杂示例.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_复杂示例
8 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/public/复选示例.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_复选示例
8 |
9 |
10 |
11 |
12 |
13 |
14 | checkboxType: {
15 | Y: '', // p 选中操作只影响父节点
16 | N: 'p' // s 取消操作只影响子节点
17 | }
18 |
19 |
20 |
21 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/public/大数据量.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_大数据量
8 |
9 |
10 |
11 |
12 |
13 | 20万+用时:
14 |
15 |
16 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/public/展开收起图标.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_展示图标
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/public/展示图标.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_展示图标
8 |
9 |
10 |
11 |
12 |
13 | showIcon: true 显示图标
14 | onlyShowLeafIcon: true 仅叶子节点显示图标,showIcon:true时有效
15 |
16 |
17 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/public/开启动画.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_基础示例
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/public/异步加载.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_异步加载
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/public/手风琴模式.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_手风琴模式
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/public/拖拽节点.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_基础示例
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/public/显示连接线.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_显示连接线
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/public/最大可选.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_最大可选
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/public/格式化数据.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_格式化数据
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/public/清空选中节点.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_清空选中节点
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/public/结合vue.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_基础示例
8 |
9 |
10 |
11 |
12 |
13 | 配置项优先级,全局 < props < options
14 |
36 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/public/结合worker+indexDb.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_结合worker+indexDb
8 |
9 |
10 |
11 |
12 |
13 | 此示例在github中速度受限,请下载代码到本地试验!!!,其中用时为忽略下载时间
14 | 用时:
15 |
16 |
17 |
18 |
57 |
58 |
142 |
143 |
144 |
--------------------------------------------------------------------------------
/public/自定义搜索节点.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_自定义搜索节点
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/public/自定义节点内容.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_自定义节点内容
8 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/public/自定义节点内容2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_自定义节点内容
8 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/public/节点过滤.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_节点过滤
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/public/获取选中节点.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_获取选中节点
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/public/面包屑.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_面包屑
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/public/项目实战.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_项目实战
8 |
9 |
10 |
11 |
12 |
13 | 此示例在github中速度受限,请下载代码到本地试验!!!,其中用时为忽略下载时间
14 | 用时:
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/public/鼠标右键事件.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | vs-tree2.0_基础示例
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import json from 'rollup-plugin-json'
2 | import commonjs from 'rollup-plugin-commonjs'
3 | import { terser } from 'rollup-plugin-terser'
4 | import babel from '@rollup/plugin-babel'
5 | import postcss from 'rollup-plugin-postcss'
6 | import autoprefixer from 'autoprefixer'
7 | import cssnano from 'cssnano'
8 | import { eslint } from 'rollup-plugin-eslint'
9 | import ts from 'rollup-plugin-typescript2'
10 | import resolve from '@rollup/plugin-node-resolve'
11 |
12 | const isDev = process.env.NODE_ENV !== 'production'
13 |
14 | const postcssPlugin = [
15 | autoprefixer
16 | ]
17 |
18 | const extensions = [
19 | '.js',
20 | '.ts',
21 | '.tsx'
22 | ]
23 |
24 | const tsPlugin = ts({
25 | tsconfig: './tsconfig.json',
26 | useTsconfigDeclarationDir: true,
27 | extensions
28 | })
29 |
30 | if (!isDev) {
31 | postcssPlugin.push(cssnano)
32 | }
33 |
34 | export default {
35 | input: 'src/main.js',
36 | output: [
37 | {
38 | file: 'dist/vs-tree.esm.browser.js',
39 | format: 'es'
40 | },
41 | {
42 | file: 'dist/vs-tree.js',
43 | format: 'umd',
44 | name: 'vsTree',
45 | exports: 'named'
46 | }
47 | ],
48 | plugins: [
49 | resolve({
50 | extensions,
51 | modulesOnly: true
52 | }),
53 | json(),
54 | tsPlugin,
55 | commonjs(),
56 | eslint({
57 | // fix: true,
58 | include: ['./src/**/**.js'],
59 | exclude: ['node_modules/**', 'src/less/**']
60 | }),
61 | babel({
62 | babelHelpers: 'bundled',
63 | extensions: extensions
64 | }),
65 | postcss({
66 | plugins: postcssPlugin,
67 | extract: 'vs-tree.css'
68 | }),
69 | !isDev && terser()
70 | ]
71 | }
72 |
--------------------------------------------------------------------------------
/src/breadcrumb/breadcrumb-item.js:
--------------------------------------------------------------------------------
1 | export default class BreadcrumbItem {
2 | constructor (node, parent) {
3 | this.node = node
4 | this.data = node.data
5 | this.store = node.store
6 | this.parent = parent
7 |
8 | const { icon, link, separator = '/' } = this.parent.options
9 | this.renderIcon = icon
10 | this.renderLink = link
11 | this.renderSeparator = separator
12 | }
13 |
14 | createDom () {
15 | const breads = this.parent.list
16 | const index = breads.findIndex(v => v === this.node)
17 | const last = index === breads.length - 1
18 | const dom = document.createElement('span')
19 |
20 | if (this.renderIcon) {
21 | const icon = this.createIcon()
22 | icon && dom.appendChild(icon)
23 | }
24 |
25 | dom.appendChild(this.createLink(breads, index, last))
26 |
27 | if (!last) {
28 | dom.appendChild(this.createSeparator())
29 | }
30 |
31 | return dom
32 | }
33 |
34 | createIcon () {
35 | let _iconInner
36 | if (typeof this.renderIcon === 'function') {
37 | _iconInner = this.renderIcon(this.node, this.data)
38 | } else {
39 | _iconInner = this.renderIcon
40 | }
41 | if (!_iconInner) return false
42 |
43 | const icon = document.createElement('span')
44 | icon.className = 'vs-breadcrumb-icon'
45 | if (typeof this.renderIcon === 'function') {
46 | if (_iconInner instanceof HTMLElement) {
47 | icon.appendChild(_iconInner)
48 | } else {
49 | icon.innerHTML = _iconInner
50 | }
51 | } else {
52 | icon.innerHTML = this.renderIcon
53 | }
54 | return icon
55 | }
56 |
57 | createLink (breads, index, last) {
58 | const link = document.createElement('span')
59 | link.className = 'vs-breadcrumb-link'
60 |
61 | if (typeof this.renderLink === 'function') {
62 | const _linkR = this.renderLink(this.node, this.data)
63 | if (_linkR instanceof HTMLElement) {
64 | link.appendChild(_linkR)
65 | } else {
66 | link.innerHTML = _linkR
67 | }
68 | } else {
69 | link.innerHTML = this.data.name
70 | }
71 |
72 | link.addEventListener('click', (e) => {
73 | e.preventDefault()
74 | e.stopPropagation()
75 | if (last) return
76 | breads.splice(index + 1)
77 | this.store.update()
78 | })
79 | return link
80 | }
81 |
82 | createSeparator () {
83 | const separator = document.createElement('span')
84 | separator.className = 'vs-breadcrumb-separator'
85 | if (typeof this.renderSeparator === 'function') {
86 | separator.innerHTML = this.renderSeparator(this.node, this.data)
87 | } else {
88 | separator.innerHTML = this.renderSeparator
89 | }
90 | return separator
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/breadcrumb/index.ts:
--------------------------------------------------------------------------------
1 |
2 | import BreadcrumbItem from "./breadcrumb-item"
3 |
4 | export interface BreadcrumbOptions {
5 | el: string | HTMLElement,
6 | icon?: Function | string,
7 | link?: Function | string,
8 | separator?: Function | string,
9 | change?: Function
10 | }
11 |
12 | export default class Breadcrumb {
13 | store: any
14 | list = []
15 | options: BreadcrumbOptions
16 | constructor(options: BreadcrumbOptions) {
17 | this.options = options
18 | }
19 |
20 | get current(): any {
21 | return this.list[this.list.length - 1]
22 | }
23 |
24 | renderBreadcrumb() {
25 | this.store = this.current.store
26 | const { el, change = () => { } } = this.options
27 | let _el: any
28 | if (el instanceof HTMLElement) {
29 | _el = el
30 | } else if (el && typeof el === 'string') {
31 | _el = document.querySelector(el)
32 | }
33 | if (!_el) {
34 | _el = document.createElement('section')
35 | }
36 | _el.classList.add('vs-breadcrumb')
37 |
38 | const bs = this.list.map((node: any) => {
39 | return new BreadcrumbItem(node, this).createDom()
40 | })
41 |
42 | _el.innerHTML = ''
43 | bs.forEach((html: HTMLElement) => {
44 | _el.appendChild(html)
45 | })
46 | change(_el, this.list, this.current)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/core/index.js:
--------------------------------------------------------------------------------
1 | import TreeStore from './store'
2 | import Vlist from '../virtual-list'
3 | import Breadcrumb from '../breadcrumb'
4 |
5 | const noop = () => { }
6 | export default class Tree {
7 | constructor(selector, ops) {
8 | if (typeof selector === 'string') {
9 | this.$el = document.querySelector(selector)
10 | } else {
11 | this.$el = selector
12 | }
13 |
14 | if (!(this.$el instanceof HTMLElement)) {
15 | throw Error('请为组件提供根节点')
16 | }
17 |
18 | this.$el.classList.add('vs-tree')
19 |
20 | const delimiters = ['#\\[\\[', '\\]\\]']
21 |
22 | const [open, close] = delimiters
23 | var interpolate = open + '([\\s\\S]+?)' + close
24 | this.interpolate = new RegExp(interpolate, 'igm')
25 | const slotsMap = {}
26 | const slots = this.$el.querySelectorAll('[tree-slot]')
27 | if (slots && slots.length) {
28 | slots.forEach(v => {
29 | const name = v.attributes['tree-slot'].value
30 | const scope = v.attributes['tree-slot-scope'].value
31 |
32 | slotsMap[name] = {
33 | scope,
34 | node: v,
35 | interpolate: this.interpolate,
36 | text: v.innerText,
37 | inner: v.outerHTML
38 | }
39 |
40 | v.parentNode.removeChild(v)
41 | })
42 | }
43 |
44 | // 默认清空根节点
45 | // this.$el.innerHTML = ''
46 |
47 | if (ops.theme) {
48 | this.$el.classList.add('vs-theme-' + ops.theme)
49 | }
50 |
51 | if (Array.isArray(ops.data)) {
52 | this._data = {
53 | _vsroot: true,
54 | name: ops.rootName || '---',
55 | children: ops.data
56 | }
57 | if (!ops.rootName) {
58 | ops.hideRoot = true
59 | }
60 | } else if (typeof ops.data === 'object') {
61 | this._data = ops.data
62 | } else {
63 | throw Error('参数data仅支持对象或数组!')
64 | }
65 |
66 | this.nodes = []
67 | const { showCount = 20, itemHeight = 26, maxHeight = '400px', minHeight = '0px' } = ops.virtual || {}
68 | // 每一项的高度
69 | this.itemHeight = itemHeight
70 | // 当前可见数量
71 | this.showCount = showCount
72 | // 最大高度
73 | this.maxHeight = ops.maxHeight || maxHeight
74 | // 最小高度
75 | this.minHeight = ops.minHeight || minHeight
76 | // 当前可见列表
77 | this.data = []
78 | // 关键字过滤
79 | this.keyword = ''
80 | this.searchFilter = ops.searchFilter
81 | this.ready = ops.ready || noop
82 |
83 | if (Object.prototype.toString.call(ops.breadcrumb) === '[object Object]') {
84 | this.$$breadcrumb = new Breadcrumb(ops.breadcrumb)
85 | }
86 |
87 | const start = () => {
88 | this.store = new TreeStore({
89 | data: this._data,
90 | max: ops.max,
91 | slots: slotsMap,
92 | breadcrumb: this.$$breadcrumb || null,
93 | strictLeaf: ops.strictLeaf || false,
94 | showCount: this.showCount,
95 | itemHeight: this.itemHeight,
96 | hideRoot: ops.hideRoot || false,
97 | animation: ops.animation || false, // 动画
98 | expandLevel: typeof ops.expandLevel === 'number' ? ops.expandLevel : 1, // 默认展开1级节点
99 | beforeCheck: ops.beforeCheck || null,
100 | showLine: ops.showLine || false, // 是否显示连接线
101 | showIcon: ops.showIcon || false,
102 | onlyShowLeafIcon: ops.onlyShowLeafIcon || false,
103 | showCheckbox: ops.showCheckbox || false,
104 | checkboxType: ops.checkboxType || { Y: 'ps', N: 'ps' },
105 | checkInherit: ops.checkInherit || false, // 新加入节点时自动继承父节点选中状态
106 | disabledInherit: ops.disabledInherit || false, // 新加入节点时自动继承父节点禁用状态
107 | showRadio: ops.showRadio || false,
108 | highlightCurrent: ops.highlightCurrent || false,
109 | checkFilterLeaf: ops.checkFilterLeaf || false, // 过滤非叶子节点
110 | checkFilter: ops.checkFilter || null, // 过滤选中节点
111 | accordion: ops.accordion || false, // 手风琴模式
112 | draggable: ops.draggable || false,
113 | dropable: ops.dropable || false,
114 | lazy: ops.lazy || false,
115 | sort: ops.sort || false,
116 | indent: ops.indent || 10,
117 | checkedKeys: ops.checkedKeys || [],
118 | expandKeys: ops.expandKeys || [],
119 | disabledKeys: ops.disabledKeys || [],
120 | limitAlert: ops.limitAlert || noop,
121 | click: ops.click || noop,
122 | check: ops.check || noop, // 复选框被点击时出发
123 | change: ops.change || noop,
124 | load: ops.load || noop,
125 | contextmenu: ops.contextmenu || null,
126 | radioParentoOnly: ops.radioType === 'level' ? 'level' : 'all', // 每个父节点下唯一,仅raido模式有效
127 | renderContent: ops.renderContent || null,
128 | nocheckParent: ops.nocheckParent || false, // 只允许叶子节点选中
129 | checkOnClickNode: ops.checkOnClickNode || false,
130 | format: ops.format || null,
131 | searchRender: ops.searchRender || null,
132 | searchDisabledChecked: ops.searchDisabledChecked || false,
133 | expandClass: ops.expandClass || 'vs-expand-icon',
134 | onDragstart: ops.onDragstart || noop,
135 | onDragenter: ops.onDragenter || noop,
136 | onDrop: ops.onDrop || noop,
137 | update: () => {
138 | this._render()
139 | },
140 | nodesChange: (nodes) => {
141 | this.nodes = nodes
142 | this.vlist && this._render()
143 | }
144 | })
145 |
146 | if (this.store.hideRoot) {
147 | // 根节点创建dom
148 | this.store.root.createNode()
149 | }
150 |
151 | this._init()
152 |
153 | // 设置默认选中
154 | this.store.setDefaultChecked()
155 | }
156 |
157 | if (ops.async) {
158 | setTimeout(() => {
159 | start()
160 | }, 0)
161 | } else {
162 | start()
163 | }
164 | }
165 |
166 | _init() {
167 | this.vlist = new Vlist({
168 | root: this.$el,
169 | data: [],
170 | maxHeight: this.maxHeight,
171 | minHeight: this.minHeight,
172 | estimateSize: this.itemHeight,
173 | keeps: this.showCount
174 | })
175 | this._render()
176 | this.ready && this.ready(this)
177 | }
178 |
179 | _render(update = true) {
180 | if (this.$$breadcrumb) {
181 | const { current } = this.$$breadcrumb
182 | this.data = this.nodes.filter(v => v.parent && v.parent.id === current.id)
183 | // 当前仅过滤面包屑当前层级
184 | this._keywordFilter(this.data)
185 | this.$$breadcrumb.renderBreadcrumb()
186 | } else {
187 | this._keywordFilter(this.nodes)
188 | }
189 | update && this.vlist.update(this.data)
190 | }
191 |
192 | _keywordFilter(data) {
193 | this.data = data.filter(v => {
194 | // 过滤隐藏节点 | 隐藏root节点
195 | return this._hasKeyword(v) && v.visbile && !(this.store.hideRoot && v.level === 0)
196 | })
197 | }
198 |
199 | _hasKeyword(v) {
200 | if (!this.keyword) return true
201 | let boo = this._checkFilter(v)
202 | if (!boo) {
203 | v.childNodes.forEach(node => {
204 | if (!boo) {
205 | boo = this._hasKeyword(node)
206 | }
207 | })
208 | } else {
209 | v.parent && (v.parent.requireExpand = true)
210 | }
211 | return boo
212 | }
213 |
214 | _checkFilter(v) {
215 | if (!this.keyword) return
216 | if (typeof this.searchFilter === 'function') {
217 | return this.searchFilter(this.keyword, v, v.data)
218 | }
219 | return v.data.name && v.data.name.includes(this.keyword)
220 | }
221 |
222 | // 过滤节点
223 | filter(keyword = '', onlySearchLeaf) {
224 | this.keyword = keyword
225 | this.store.onlySearchLeaf = onlySearchLeaf && !!keyword
226 | this.store.isSearch = !!keyword
227 | if (this.store.onlySearchLeaf) {
228 | const data = this.nodes.filter(v => !v.childNodes.length && this._checkFilter(v) && !(this.store.hideRoot && v.level === 0))
229 | this.vlist.update(data)
230 | return data
231 | }
232 |
233 | this._render(false)
234 | for (let i = 0, len = this.data.length; i < len; i++) {
235 | const v = this.data[i]
236 | if (v.requireExpand) {
237 | v.requireExpand = false
238 | v.setExpand(true, true)
239 | }
240 | }
241 | this._render()
242 | return this.data
243 | }
244 |
245 | // 根据ID获取节点
246 | getNodeById(id) {
247 | return this.store.getNodeById(id)
248 | }
249 |
250 | // 获取选中节点
251 | getCheckedNodes() {
252 | return this.store.getCheckedNodes(...arguments)
253 | }
254 |
255 | // 设置最大可选
256 | setMaxValue(value = 0) {
257 | this.store.max = value
258 | }
259 |
260 | // 滚动到索引位置
261 | scrollToIndex(index = 0) {
262 | this.vlist.scrollToIndex(index)
263 | }
264 |
265 | // 清空选中元素
266 | clearCheckedNodes() {
267 | const nodes = this.getCheckedNodes(true)
268 | nodes.forEach(node => {
269 | node.setChecked(false)
270 | })
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/src/core/node.js:
--------------------------------------------------------------------------------
1 |
2 | import { insterAfter, onDragEnterGap, parseTemplate } from './utils.ts'
3 |
4 | let setepId = 0
5 |
6 | export default class Node {
7 | constructor (ops) {
8 | this.id = setepId++
9 | this.checked = false
10 | this.expanded = false
11 | this.indeterminate = false
12 | this.visbile = false
13 | this.disabled = false
14 | this.loaded = false
15 | this.isLeaf = false
16 |
17 | this.level = 0
18 | this.childNodes = []
19 |
20 | this.store = ops.store
21 |
22 | this.parent = ops.parent
23 |
24 | this.originData = ops.data
25 |
26 | this.__buffer = {}
27 |
28 | this.data = Object.assign({}, ops.data)
29 | if (typeof this.store.format === 'function' && !ops.data._vsroot) {
30 | const _data = this.store.format(Object.assign({}, ops.data), this)
31 | if (typeof _data !== 'object') {
32 | throw new Error('format must return object! \nformat: function(data) {\n return {id, name, children, isLeaf}\n}')
33 | }
34 | const props = ['id', 'name', 'children', 'isLeaf', 'icon', 'extra']
35 | for (let i = 0, len = props.length; i < len; i++) {
36 | if (Object.prototype.hasOwnProperty.call(_data, props[i])) {
37 | this.data[props[i]] = _data[props[i]]
38 | }
39 | }
40 | }
41 |
42 | if (this.store.checkInherit && this.parent) {
43 | this.checked = this.parent.checked
44 | }
45 |
46 | if (this.store.disabledInherit && this.parent) {
47 | this.disabled = this.parent.disabled
48 | }
49 |
50 | if (this.store.expandKeys.includes(this.data.id)) {
51 | this.expanded = true
52 | }
53 |
54 | if (this.store.disabledKeys.includes(this.data.id)) {
55 | this.disabled = true
56 | }
57 |
58 | if (this.parent) {
59 | this.level = this.parent.level + 1
60 | }
61 |
62 | if (this.data) {
63 | this.setData(this.data)
64 | }
65 |
66 | this.initData()
67 | }
68 |
69 | initData () {
70 | if (this.level > this.store.expandLevel && this.store.expandLevel !== -1 && !(this.parent?.expanded)) {
71 | this.visbile = false
72 | return
73 | }
74 | this.visbile = true
75 | }
76 |
77 | createNode () {
78 | if (this.dom) {
79 | this.checkboxNode && (this.checkboxNode.checked = this.checked)
80 | this.radioNode && (this.radioNode.checked = this.checked)
81 | if (this.indeterminate) this.dom.classList.add('is-indeterminate')
82 | return this.dom
83 | }
84 |
85 | const dom = document.createElement('div')
86 | dom.className = 'vs-tree-node'
87 | dom.setAttribute('vs-index', this.id)
88 | if (this.indeterminate) dom.classList.add('is-indeterminate')
89 |
90 | !this.isLeaf && this.childNodes.length && dom.setAttribute('vs-child', true)
91 |
92 | dom.appendChild(this.createInner())
93 |
94 | const slotAppend = parseTemplate('append', this)
95 | if (slotAppend) {
96 | dom.appendChild(slotAppend)
97 | } else if (this.store.renderContent) {
98 | dom.appendChild(this.createContent())
99 | }
100 |
101 | dom.addEventListener('click', (e) => {
102 | e.stopPropagation()
103 | if (this.store.highlightCurrent) {
104 | if (this.store.selectedCurrent) {
105 | this.store.selectedCurrent.dom.classList.remove('selected')
106 | }
107 | dom.classList.add('selected')
108 | }
109 |
110 | if (this.store.checkOnClickNode && !this.disabled && !(this.store.breadcrumb && !this.isLeaf)) {
111 | this.handleCheckChange({
112 | target: { checked: !this.checked }
113 | })
114 | }
115 |
116 | this.store.selectedCurrent = this
117 |
118 | if (this.store.breadcrumb && !this.isLeaf) {
119 | this.store.breadcrumb.list.push(this)
120 | this.setExpand(true)
121 | }
122 |
123 | this.store.click(e, this)
124 | }, {
125 | passive: false
126 | })
127 |
128 | dom.addEventListener('contextmenu', (e) => {
129 | if (this.store.contextmenu && typeof this.store.contextmenu === 'function') {
130 | e.stopPropagation()
131 | e.preventDefault()
132 | this.store.contextmenu(e, this)
133 | }
134 | })
135 | if (this.store.draggable) {
136 | this.createDragable(dom)
137 | }
138 |
139 | this.dom = dom
140 | return dom
141 | }
142 |
143 | createInner () {
144 | const dom = document.createElement('div')
145 | dom.className = 'vs-tree-inner'
146 | // 当隐藏根节点时减少一级缩进
147 | let level = this.level + (this.store.hideRoot ? -1 : 0)
148 |
149 | if (this.store.breadcrumb) {
150 | level = 0
151 | }
152 |
153 | if (this.store.showLine) {
154 | for (let i = 0; i < level; i++) {
155 | const indent = document.createElement('span')
156 | indent.className = 'vs-indent-unit'
157 | dom.appendChild(indent)
158 | }
159 | } else {
160 | dom.style.paddingLeft = level * this.store.indent + 'px'
161 | }
162 |
163 | let expandDom
164 | if (!this.store.breadcrumb) {
165 | if (this.store.strictLeaf) {
166 | expandDom = !this.isLeaf ? this.createExpand() : this.createExpandEmpty()
167 | } else {
168 | expandDom = (this.childNodes?.length || this.store.lazy) && !this.isLeaf ? this.createExpand() : this.createExpandEmpty()
169 | }
170 | dom.appendChild(expandDom)
171 | } else {
172 | this.loadingEl = document.createElement('span')
173 | this.loadingEl.className = 'vs-loading-unit'
174 | dom.appendChild(this.loadingEl)
175 | }
176 |
177 | if (this.store.showCheckbox || this.store.showRadio) {
178 | if ((!this.store.nocheckParent) || (this.isLeaf && !this.childNodes.length)) {
179 | dom.appendChild(this.createCheckbox())
180 | }
181 | }
182 |
183 | if (this.store.showIcon) {
184 | if (!this.store.onlyShowLeafIcon || (!this.childNodes.length || this.isLeaf)) {
185 | dom.appendChild(this.createIcon())
186 | }
187 | }
188 |
189 | dom.appendChild(this.createText())
190 | return dom
191 | }
192 |
193 | // 自定义Dom 节点
194 | cusmtomNode (name, info) {
195 | const box = document.createElement(name)
196 | info.text && (box.innerText = info.text)
197 | info.className && (box.className = info.className)
198 | if (info.children) {
199 | info.children.forEach(v => {
200 | box.appendChild(v)
201 | })
202 | }
203 | if (typeof info.click === 'function') {
204 | box.addEventListener('click', (e) => {
205 | e.stopPropagation()
206 | info.click(e, this)
207 | }, { passive: false })
208 | }
209 | return box
210 | }
211 |
212 | // 自定义内容
213 | createContent () {
214 | const tpl = this.store.renderContent(this.cusmtomNode.bind(this), this)
215 | if (!tpl) {
216 | return document.createElement('span')
217 | }
218 | tpl.addEventListener('click', (e) => {
219 | e.stopPropagation()
220 | }, { passive: false })
221 | return tpl
222 | }
223 |
224 | // 叶子节点-无需展开
225 | createExpandEmpty () {
226 | const dom = document.createElement('span')
227 | dom.className = 'expand-empty ' + this.store.expandClass
228 | return dom
229 | }
230 |
231 | // 有子元素-需要展开
232 | createExpand () {
233 | const dom = document.createElement('span')
234 | dom.className = 'expand ' + this.store.expandClass
235 |
236 | if (this.level < this.store.expandLevel || this.store.expandLevel === -1 || this.expanded) {
237 | dom.classList.add('expanded')
238 | this.expanded = true
239 | }
240 |
241 | dom.addEventListener('click', (e) => {
242 | e.stopPropagation()
243 | if (this.loading) return
244 | const expand = !dom.classList.contains('expanded')
245 | // dom.classList.toggle('expanded')
246 | this.setExpand(expand)
247 | }, {
248 | passive: false
249 | })
250 | this.expandEl = dom
251 | return dom
252 | }
253 |
254 | createCheckbox () {
255 | let label = 'checkbox'
256 | if (this.store.showRadio) {
257 | label = 'radio'
258 | }
259 | const dom = document.createElement('label')
260 | dom.className = `vs-${label}`
261 | const inner = document.createElement('span')
262 | inner.className = `vs-${label}__inner`
263 | const checkbox = document.createElement('input')
264 | checkbox.type = label
265 | checkbox.checked = this.checked
266 | checkbox.disabled = this.disabled
267 | checkbox.className = `vs-${label}__original`
268 | checkbox.name = label === 'radio' ? 'vs-radio' + (this.store.radioParentoOnly && this.parent ? this.parent.id : '') : 'vs-checkbox'
269 |
270 | if (label === 'radio') {
271 | if (this.store.radioParentoOnly === 'level') {
272 | checkbox.name = 'vs-radio' + (this.store.radioParentoOnly && this.parent ? this.parent.id : '')
273 | } else {
274 | checkbox.name = 'vs-radio'
275 | }
276 | this.radioNode = checkbox
277 | } else {
278 | checkbox.name = 'vs-checkbox'
279 | this.checkboxNode = checkbox
280 | }
281 |
282 | dom.appendChild(checkbox)
283 | dom.appendChild(inner)
284 |
285 | // label 点击会出发两次
286 | dom.addEventListener('click', (e) => {
287 | e.stopPropagation()
288 | }, { passive: false })
289 |
290 | // 点击回调
291 | checkbox.addEventListener('click', (e) => {
292 | console.log('==========')
293 | this.store.check(e, this)
294 | }, { passive: false })
295 |
296 | checkbox.addEventListener('change', (e) => {
297 | e.stopPropagation()
298 | console.log('handleCheckChange', e)
299 | this.handleCheckChange(e)
300 | })
301 |
302 | this.checkboxEl = checkbox
303 |
304 | return dom
305 | }
306 |
307 | handleCheckChange (e) {
308 | const checked = e.target.checked
309 |
310 | if (typeof this.store.beforeCheck === 'function') {
311 | if (!this.store.beforeCheck(this)) {
312 | e.target.checked = !checked
313 | return
314 | }
315 | }
316 |
317 | if (checked && this.store.checkMaxNodes(this)) {
318 | this.store.limitAlert()
319 | e.target.checked = false
320 | return
321 | }
322 |
323 | if (this.store.showRadio) {
324 | this.updateRadioChecked(checked)
325 | } else {
326 | this.updateChecked(checked)
327 | this.updateCheckedParent(checked)
328 | }
329 | this.store._change(this)
330 | }
331 |
332 | createText () {
333 | const slot = parseTemplate('name', this)
334 | if (slot) {
335 | return slot
336 | }
337 |
338 | const dom = document.createElement('span')
339 | dom.innerText = this.data.name
340 | dom.className = 'vs-tree-text'
341 | return dom
342 | }
343 |
344 | createIcon () {
345 | const icon = document.createElement('span')
346 | icon.className = (this.isLeaf && !this.childNodes.length) ? 'vs-icon-leaf' : 'vs-icon-parent'
347 | if (this.data.icon) {
348 | if (this.data.icon instanceof HTMLElement) {
349 | icon.style.backgroundImage = 'none'
350 | icon.appendChild(this.data.icon)
351 | } else {
352 | icon.classList.add(this.data.icon)
353 | }
354 | }
355 | return icon
356 | }
357 |
358 | setData (data) {
359 | this.store.dataMap.set(data.id, this)
360 | this.store.nodeMap.set(this.id, this)
361 | this.data = data
362 | this.childNodes = []
363 |
364 | if (typeof data.isLeaf === 'boolean') {
365 | this.isLeaf = data.isLeaf
366 | } else if (!data.children && !this.store.lazy) {
367 | this.isLeaf = true
368 | }
369 |
370 | let children
371 | if (this.level === 0 && this.data instanceof Node) {
372 | children = this.data
373 | } else {
374 | children = this.data.children || []
375 | }
376 |
377 | if (children.length) {
378 | this.loaded = true
379 | }
380 |
381 | for (let i = 0, j = children.length; i < j; i++) {
382 | this.insertChild({ data: children[i] })
383 | }
384 | }
385 |
386 | insertChild (child, index) {
387 | if (!(child instanceof Node)) {
388 | Object.assign(child, {
389 | parent: this,
390 | store: this.store
391 | })
392 | child = new Node(child)
393 | }
394 |
395 | child.level = this.level + 1
396 |
397 | if (typeof index === 'undefined' || index < 0) {
398 | this.childNodes.push(child)
399 | } else {
400 | this.childNodes.splice(index, 0, child)
401 | }
402 | return child
403 | }
404 |
405 | insertBefore (child, ref) {
406 | let index
407 | if (ref) {
408 | index = this.childNodes.indexOf(ref)
409 | }
410 | this.insertChild(child, index)
411 | }
412 |
413 | insertAfter (child, ref) {
414 | let index
415 | if (ref) {
416 | index = this.childNodes.indexOf(ref)
417 | if (index !== -1) index += 1
418 | }
419 | this.insertChild(child, index)
420 | }
421 |
422 | // 设置展开状态
423 | updateExpand (expand) {
424 | if (this.childNodes.length) {
425 | this.childNodes.forEach(v => {
426 | if (expand && this.expanded) {
427 | v.visbile = true
428 | } else {
429 | v.visbile = false
430 | }
431 | v.updateExpand(expand)
432 | })
433 | }
434 | }
435 |
436 | // 更新本身及子节点状态
437 | updateChecked (check, isInitDefault) {
438 | if ((!isInitDefault && this.disabled)) return
439 | if (!this.store.showCheckbox) return
440 | // if (this.disabled) return
441 | this.checked = check
442 | this.sortId = Date.now()
443 | this.checkboxNode && (this.checkboxNode.checked = check)
444 | this.dom && this.dom.classList.remove('is-indeterminate')
445 |
446 | // 验证关联关系
447 | if (this.store.allowEmit(check, 'p')) {
448 | this.parent && (this.parent.indeterminate = false)
449 | }
450 |
451 | if (!this.store.allowEmit(check, 's')) {
452 | return
453 | }
454 |
455 | if (this.childNodes.length) {
456 | this.childNodes.forEach(v => {
457 | v.updateChecked(check)
458 | })
459 | }
460 | }
461 |
462 | // 更新父节点状态
463 | updateCheckedParent (_checked, isInitDefault) {
464 | if ((!isInitDefault && this.disabled)) return
465 | if (!this.store.showCheckbox) return
466 | if (!this.store.allowEmit(_checked, 'p')) {
467 | return
468 | }
469 |
470 | if (!this.parent || this.store.nocheckParent) return
471 | const allChecked = this.parent.childNodes.every(v => v.checked)
472 | const someChecked = this.parent.childNodes.some(v => v.checked || v.indeterminate)
473 | if (allChecked) {
474 | this.parent.checked = true
475 | this.parent.indeterminate = false
476 | this.parent.checkboxNode && (this.parent.checkboxNode.checked = true)
477 | this.parent.dom && this.parent.dom.classList.remove('is-indeterminate')
478 | } else if (someChecked) {
479 | this.parent.checked = false
480 | this.parent.indeterminate = true
481 | this.parent.checkboxNode && (this.parent.checkboxNode.checked = false)
482 | this.parent.dom && this.parent.dom.classList.add('is-indeterminate')
483 | } else {
484 | this.parent.checked = false
485 | this.parent.indeterminate = false
486 | this.parent.checkboxNode && (this.parent.checkboxNode.checked = false)
487 | this.parent.dom && this.parent.dom.classList.remove('is-indeterminate')
488 | }
489 |
490 | this.parent.updateCheckedParent()
491 | }
492 |
493 | // 更新单选节点选中
494 | updateRadioChecked (checked, isInitDefault) {
495 | if ((!isInitDefault && this.disabled)) return
496 |
497 | if (this.store.nocheckParent && (this.childNodes.length || !this.isLeaf)) return
498 | // 父节点下唯一
499 | if (this.store.radioParentoOnly === 'level') {
500 | if (this.store.radioMap[this.parent.id]) {
501 | this.store.radioMap[this.parent.id].checked = false
502 | }
503 | this.store.radioMap[this.parent.id] = this
504 | } else {
505 | if (this.store.radioNode) {
506 | this.store.radioNode.checked = false
507 | this.store.radioNode = false
508 | }
509 | this.store.radioNode = this
510 | }
511 |
512 | this.checked = checked
513 | this.radioNode && (this.radioNode.checked = checked)
514 | }
515 |
516 | // 设置是否选中
517 | setChecked (checked, isInitDefault) {
518 | if (checked && this.store.checkMaxNodes(this)) {
519 | this.store.limitAlert()
520 | return
521 | }
522 |
523 | if (this.store.showRadio) {
524 | this.updateRadioChecked(checked, isInitDefault)
525 | return
526 | }
527 | if (!this.store.showCheckbox) return
528 |
529 | this.updateChecked(checked, isInitDefault)
530 | this.updateCheckedParent(checked, isInitDefault)
531 |
532 | this.store._change(this)
533 | }
534 |
535 | // 设置禁止选中
536 | setDisabled (disabled = true) {
537 | this.disabled = disabled
538 | this.checkboxEl && (this.checkboxEl.disabled = disabled)
539 | }
540 |
541 | // 设置默认展开
542 | setExpand (expand, noUpdate) {
543 | this.expanded = expand
544 | this.updateExpand(this.expanded)
545 | this.setAccordion(expand)
546 |
547 | if (this.expandEl) {
548 | if (expand) {
549 | this.expandEl.classList.add('expanded')
550 | } else {
551 | this.expandEl.classList.remove('expanded')
552 | }
553 | }
554 |
555 | if (this.store.lazy && !this.loaded) {
556 | this.loadData((data) => {
557 | if (data) {
558 | !noUpdate && this.storeUpdate()
559 | }
560 | })
561 | } else {
562 | !noUpdate && this.storeUpdate()
563 | }
564 | }
565 |
566 | storeUpdate () {
567 | if (this.store.animation) {
568 | this.createAnimation()
569 | } else {
570 | this.store.update()
571 | }
572 | }
573 |
574 | // 创建动画
575 | createAnimation () {
576 | this.transitionNode && this.transitionNode.parentNode && this.transitionNode.parentNode.removeChild(this.transitionNode)
577 | const tg = document.createElement('div')
578 | tg.className = 'vs-transition'
579 |
580 | if (this.childNodes.length > this.store.showCount) {
581 | for (let i = 0; i < this.store.showCount - 1; i++) {
582 | const _v = this.childNodes[i]
583 | tg.appendChild(_v.dom || _v.createNode())
584 | }
585 | } else {
586 | this.childNodes.forEach((_v) => {
587 | tg.appendChild(_v.dom || _v.createNode())
588 | })
589 | }
590 |
591 | insterAfter(tg, this.dom)
592 |
593 | const animatHeight = ((this.childNodes.length > this.store.showCount ? this.store.showCount : this.childNodes.length) * this.store.itemHeight) + 'px'
594 | if (this.expanded) {
595 | setTimeout(() => {
596 | tg.style.height = animatHeight
597 | }, 0)
598 | } else {
599 | tg.style.height = animatHeight
600 | setTimeout(() => {
601 | tg.style.height = 0
602 | }, 0)
603 | }
604 |
605 | const transend = () => {
606 | tg.removeEventListener('transitionend', transend)
607 | tg.parentNode && tg.parentNode.removeChild(tg)
608 | tg.removeEventListener('transitionend', transend)
609 | this.store.update()
610 | }
611 |
612 | tg.addEventListener('transitionend', transend)
613 |
614 | this.transitionNode = tg
615 | }
616 |
617 | // 创建拖拽
618 | createDragable (dom) {
619 | dom.draggable = true
620 |
621 | dom.addEventListener('dragstart', (e) => {
622 | e.stopPropagation()
623 | this.store.dragNode = this
624 | this.store.onDragstart(e, this)
625 | // wrap in try catch to address IE's error when first param is 'text/plain'
626 | try {
627 | // setData is required for draggable to work in FireFox
628 | // the content has to be '' so dragging a node out of the tree won't open a new tab in FireFox
629 | e.dataTransfer.setData('text/plain', '')
630 | } catch (e) { }
631 | })
632 |
633 | // Chorme下,拖拽必须禁止默认事件否则drop事件不会触发
634 | dom.addEventListener('dragover', (e) => {
635 | e.preventDefault()
636 | })
637 |
638 | dom.addEventListener('dragenter', (e) => {
639 | e.stopPropagation()
640 | e.preventDefault()
641 |
642 | removeClass(this.store.dropNode)
643 |
644 | const dropNode = this.dom
645 | if (!dropNode) return
646 |
647 | const enterGap = onDragEnterGap(e, dropNode)
648 | if (this.store.dragNode.dom === dropNode && enterGap === 0) return
649 |
650 | this.store.dropPostion = enterGap
651 |
652 | this.store.dropNode = dropNode
653 |
654 | this.store.onDragenter(e, this, dropNode, enterGap)
655 |
656 | if (this.store.dropable) {
657 | if (!this.expanded && !this.isLeaf) {
658 | this.setExpand(true)
659 | }
660 | if (enterGap === -1) {
661 | dropNode.classList.add('vs-drag-over-gap-top')
662 | return
663 | }
664 |
665 | if (enterGap === 1) {
666 | dropNode.classList.add('vs-drag-over-gap-bottom')
667 | return
668 | }
669 | if (!this.isLeaf) {
670 | dropNode.classList.add('vs-drag-enter')
671 | }
672 | }
673 | })
674 |
675 | function removeClass (dom) {
676 | if (!dom) return
677 | dom.classList.remove('vs-drag-enter')
678 | dom.classList.remove('vs-drag-over-gap-bottom')
679 | dom.classList.remove('vs-drag-over-gap-top')
680 | }
681 |
682 | dom.addEventListener('dragleave', (e) => {
683 | if (this.store.dropable) {
684 | removeClass(e.target)
685 | }
686 | })
687 |
688 | dom.addEventListener('drop', (e) => {
689 | e.stopPropagation()
690 | this.store.onDrop(e, this, this.store.dropPostion)
691 | if (this.store.dropable) {
692 | removeClass(this.store.dropNode)
693 | const dragNode = this.store.dragNode
694 | if (dragNode && this.parent) {
695 | const data = Object.assign({}, dragNode.data)
696 | dragNode.remove()
697 | if (!data) return
698 | if (this.store.dropPostion === -1) {
699 | this.parent.insertBefore({ data }, this)
700 | this.updateCheckedParent()
701 | this.store.updateNodes()
702 | } else if (this.store.dropPostion === 1) {
703 | this.parent.insertAfter({ data }, this)
704 | this.updateCheckedParent()
705 | this.store.updateNodes()
706 | } else if (!this.isLeaf) {
707 | this.append(data)
708 | }
709 | }
710 | }
711 | })
712 | }
713 |
714 | // 更新手风琴状态
715 | setAccordion (expand) {
716 | if (this.store.accordion && this.parent && expand) {
717 | const preExpand = this.store.expandMap[this.parent.id]
718 | if (preExpand === this) return
719 | if (preExpand) {
720 | preExpand.setExpand(false)
721 | }
722 | this.store.expandMap[this.parent.id] = this
723 | }
724 | }
725 |
726 | // 加载数据
727 | loadData (callback) {
728 | if (this.loading) return
729 | this.loading = true
730 | if (this.expandEl) {
731 | this.expandEl.classList.add('is-loading')
732 | } else if (this.loadingEl) {
733 | this.loadingEl.classList.add('is-loading')
734 | }
735 |
736 | const resolve = (children = []) => {
737 | this.loaded = true
738 | this.loading = false
739 | if (this.expandEl) {
740 | this.expandEl.classList.remove('is-loading')
741 | } else if (this.loadingEl) {
742 | this.loadingEl.classList.remove('is-loading')
743 | }
744 |
745 | if (children.length) {
746 | children.forEach(data => {
747 | this.insertChild({
748 | data: data,
749 | store: this.store
750 | })
751 | })
752 | this.childNodes[0].updateCheckedParent()
753 | this.store.updateNodes()
754 | }
755 |
756 | if (callback) {
757 | callback.call(this, children)
758 | }
759 | }
760 |
761 | this.store.load(this, resolve)
762 | }
763 |
764 | // 删除节点
765 | remove () {
766 | const parent = this.parent
767 | if (!parent) return
768 | const children = parent.childNodes || []
769 | const index = children.findIndex(d => d.id === this.id)
770 | if (index > -1) {
771 | children.splice(index, 1)
772 | }
773 | this.store.updateNodes()
774 | }
775 |
776 | // 添加节点
777 | append (data) {
778 | if (!data || typeof data !== 'object') return
779 | let olddom = this.dom
780 | if (this.childNodes.length !== 0) {
781 | olddom = null
782 | }
783 | const node = this.insertChild({
784 | data: data,
785 | store: this.store
786 | })
787 | this.data.children ? this.data.children.push(data) : this.data.children = [data]
788 | this.isLeaf = false
789 | if (olddom) {
790 | delete this.dom
791 | olddom.parentNode.replaceChild(this.createNode(), olddom)
792 | }
793 | node.updateCheckedParent()
794 | this.store.updateNodes()
795 | }
796 | }
797 |
--------------------------------------------------------------------------------
/src/core/store.js:
--------------------------------------------------------------------------------
1 | import Node from './node'
2 | export default class TreeStore {
3 | constructor (options) {
4 | for (const option in options) {
5 | if (Object.prototype.hasOwnProperty.call(options, option)) {
6 | this[option] = options[option]
7 | }
8 | }
9 |
10 | this.nodes = []
11 |
12 | this.dataMap = new Map()
13 | this.nodeMap = new Map()
14 |
15 | // 当前选中节点
16 | this.radioMap = {}
17 |
18 | // 当前展开节点
19 | this.expandMap = {}
20 |
21 | this.root = new Node({
22 | data: this.data,
23 | store: this
24 | })
25 |
26 | this.updateNodes()
27 |
28 | // 面包屑
29 | if (this.breadcrumb) {
30 | this.breadcrumb.list.push(this.root)
31 | }
32 |
33 | this.changeNodes = []
34 | }
35 |
36 | setData (val) {
37 | this.root.childNodes = []
38 | this.root.setData(val)
39 | this.updateNodes()
40 | }
41 |
42 | // 更新节点列表
43 | updateNodes () {
44 | this.nodes = this.flattenTreeData()
45 | this.nodesChange(this.nodes)
46 | }
47 |
48 | // 获取节点列表
49 | flattenTreeData () {
50 | const nodes = []
51 | const dig = (val) => {
52 | nodes.push(val)
53 | if (val.childNodes && val.childNodes.length) {
54 | for (let i = 0, len = val.childNodes.length; i < len; i++) {
55 | dig(val.childNodes[i])
56 | }
57 | }
58 | }
59 | dig(this.root)
60 | return nodes
61 | }
62 |
63 | // 根据ID获取节点
64 | getNodeById (id) {
65 | return this.dataMap.get(id)
66 | }
67 |
68 | // 获取选中节点
69 | getCheckedNodes (isTreeNode = false) {
70 | const nodes = this.nodes.filter(v => v.checked && !v.data._vsroot && this._checkVerify(v) && (!this.nocheckParent || !v.childNodes.length))
71 | if (this.sort) {
72 | const sortNodes = nodes.sort((a, b) => a.sortId - b.sortId)
73 | if (isTreeNode) {
74 | return sortNodes
75 | }
76 | return sortNodes.map(v => v.data)
77 | }
78 | if (isTreeNode) {
79 | return nodes
80 | }
81 | return nodes.map(v => v.data)
82 | }
83 |
84 | // 设置默认选中
85 | setDefaultChecked () {
86 | this.checkedKeys.forEach(id => {
87 | const node = this.getNodeById(id)
88 | if (node) {
89 | node.setChecked(true, true)
90 | } else {
91 | console.warn('not found node by ' + id)
92 | }
93 | })
94 | }
95 |
96 | // 验证是否已经选到最大
97 | checkMaxNodes (node) {
98 | if (!this.max) {
99 | return false
100 | }
101 |
102 | if (!node.checked && node.hasChildCount > this.max) {
103 | return true
104 | }
105 |
106 | const len = this.getCheckedNodes().length
107 |
108 | if (!node.checked && len + (node.isLeaf ? 1 : this.getUnCheckLeafsCount(node)) > this.max) {
109 | return true
110 | }
111 |
112 | return false
113 | }
114 |
115 | getUnCheckLeafsCount (node) {
116 | let count = this._checkVerify(node) && !node.checked ? 1 : 0
117 | node.childNodes.forEach(v => {
118 | count += this.getUnCheckLeafsCount(v)
119 | })
120 | return count
121 | }
122 |
123 | // 关联判断
124 | allowEmit (check, type) {
125 | const { Y, N } = this.checkboxType
126 | if (check) {
127 | if (!Y.includes(type)) {
128 | return false
129 | }
130 | } else {
131 | if (!N.includes(type)) {
132 | return false
133 | }
134 | }
135 | return true
136 | }
137 |
138 | _checkVerify (node) {
139 | if (typeof this.checkFilter === 'function') {
140 | return this.checkFilter(node)
141 | } else if (this.checkFilterLeaf) {
142 | return node.isLeaf
143 | } else {
144 | return true
145 | }
146 | }
147 |
148 | // 节点切换选中时触发
149 | _change(node) {
150 | this.changeNodes.push(node)
151 | if (this._changeTimer) clearTimeout(this._changeTimer)
152 | this._changeTimer = setTimeout(() => {
153 | this.change(this.changeNodes)
154 | this.changeNodes = []
155 | }, 0)
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/core/utils.ts:
--------------------------------------------------------------------------------
1 | import Node from "./node"
2 |
3 | export function insterAfter(newElement: HTMLElement, targetElement: HTMLElement) {
4 | const parent = targetElement.parentNode
5 | if (!parent) { return }
6 | if (parent.lastChild === targetElement) {
7 | parent.appendChild(newElement)
8 | } else {
9 | parent.insertBefore(newElement, targetElement.nextSibling)
10 | }
11 | }
12 |
13 | export function onDragEnterGap(e: MouseEvent, treeNode: HTMLElement) {
14 | var offsetTop = treeNode.getBoundingClientRect().top
15 | var offsetHeight = treeNode.offsetHeight
16 | var pageY = e.pageY
17 | var gapHeight = 2
18 | if (pageY > offsetTop + offsetHeight - offsetHeight) {
19 | return 1 // bottom
20 | }
21 | if (pageY < offsetTop + gapHeight) {
22 | return -1 // top
23 | }
24 | return 0
25 | }
26 |
27 | export const findNearestNode = (element: HTMLElement, name: string) => {
28 | let target: any = element
29 | while (target && target?.tagName !== 'BODY') {
30 | if (target.className && target.className.includes(name)) {
31 | return target
32 | }
33 | target = target.parentNode
34 | }
35 | return null
36 | }
37 |
38 |
39 | export const parseTemplate = (name: string, ctx: Node) => {
40 | const slotName = ctx.store.slots[name]
41 | if (slotName) {
42 | const node = slotName.node.cloneNode(true)
43 | node.classList.add('vs-tree-text')
44 | node.setAttribute('tree-node-id', ctx.id)
45 | ctx.__buffer = {}
46 |
47 | var prefix = `
48 | var ${slotName.scope} = _;
49 | `
50 | slotName.text
51 | .replace(slotName.interpolate, (a: string, b: string) => {
52 | prefix += `_.__buffer['${a}'] = ${b};`
53 | })
54 |
55 | // eslint-disable-next-line no-new-func
56 | const render = new Function('_', prefix)
57 |
58 | render.call(ctx, ctx)
59 |
60 | node.innerText = node.innerText.replace(slotName.interpolate, (a: any) => {
61 | return (ctx as any).__buffer[a]
62 | }).replace(/\n/g, '')
63 |
64 | return node
65 | }
66 | return false
67 | }
--------------------------------------------------------------------------------
/src/less/vs-tree.less:
--------------------------------------------------------------------------------
1 | .vs-loading {
2 | min-height: 100px;
3 | background-image: url(./oval.svg);
4 | // background-size: 30px 30px;
5 | background-position: center center;
6 | background-repeat: no-repeat;
7 | }
8 |
9 | .vs-tree-node {
10 | height: 26px;
11 | cursor: pointer;
12 | color: #606266;
13 | font-size: 14px;
14 | display: flex;
15 | align-items: center;
16 | justify-content: space-between;
17 | white-space: nowrap;
18 | padding: 0 0 2px;
19 | box-sizing: border-box;
20 |
21 | &:hover {
22 | background-color: #eee;
23 | }
24 |
25 | &:first-child {
26 | .expand {
27 | &::before {
28 | height: 0;
29 | }
30 | }
31 | }
32 | }
33 |
34 | .vs-indent-unit {
35 | position: relative;
36 | display: inline-block;
37 | width: 14px;
38 | height: 14px;
39 | vertical-align: middle;
40 |
41 | &::after {
42 | content: "";
43 | width: 0;
44 | height: 160%;
45 | position: absolute;
46 | left: 50%;
47 | border-left: 1px dashed #ddd;
48 | top: -8px;
49 | }
50 | }
51 |
52 | .vs-loading-unit.is-loading {
53 | width: 14px;
54 | height: 14px;
55 | margin-right: 5px;
56 | display: inline-block;
57 | vertical-align: middle;
58 |
59 | &::after {
60 | content: "";
61 | width: 14px;
62 | height: 14px;
63 | display: inline-block;
64 | vertical-align: top;
65 | }
66 | }
67 |
68 | .expand,
69 | .expand-empty {
70 | width: 14px;
71 | height: 14px;
72 | line-height: 10px;
73 | display: inline-block;
74 | margin-right: 5px;
75 | color: #bbb;
76 | text-align: center;
77 | box-sizing: border-box;
78 | vertical-align: middle;
79 | }
80 |
81 | .expand {
82 | position: relative;
83 | cursor: pointer;
84 |
85 | &.vs-expand-icon {
86 | &::after {
87 | content: "";
88 | width: 14px;
89 | height: 14px;
90 | display: inline-block;
91 | background-image: url(./expand.svg);
92 | background-size: 10px 10px;
93 | background-repeat: no-repeat;
94 | background-position: center center;
95 | transform: rotate(-90deg);
96 | transition: transform .3s;
97 | }
98 | }
99 | }
100 |
101 | .expanded {
102 | color: #bbb;
103 |
104 | &.vs-expand-icon::after {
105 | transform: rotate(0);
106 | }
107 | }
108 |
109 | .expand.is-loading, .vs-loading-unit.is-loading {
110 | &::after {
111 | background-image: url(./oval.svg);
112 | background-repeat: no-repeat;
113 | background-size: 14px 14px;
114 | border: none;
115 | color: transparent;
116 | }
117 | }
118 |
119 | .vs-indent-unit ~ .expand {
120 | &::before {
121 | content: "";
122 | position: absolute;
123 | top: -50%;
124 | left: 50%;
125 | width: 0;
126 | height: 50%;
127 | margin-top: -25%;
128 | border-left: 1px dashed #ddd;
129 | }
130 | }
131 |
132 | .vs-tree-node:not([vs-child]) + .vs-tree-node{
133 | .vs-indent-unit ~ .expand {
134 | &::before {
135 | display: none;
136 | }
137 | }
138 | }
139 |
140 | .vs-indent-unit ~ .expand-empty {
141 | position: relative;
142 |
143 | &::after {
144 | content: "";
145 | position: absolute;
146 | top: 50%;
147 | left: 50%;
148 | width: 50%;
149 | margin-top: -1px;
150 | border-bottom: 1px dashed #ddd;
151 | }
152 |
153 | &::before {
154 | content: "";
155 | position: absolute;
156 | top: -50%;
157 | left: 50%;
158 | height: 200%;
159 | border-left: 1px dashed #ddd;
160 | }
161 | }
162 |
163 | .selected {
164 | background-color: #eee;
165 | }
166 |
167 | .vs-checkbox, .vs-radio {
168 | position: relative;
169 | color: #606266;
170 | font-weight: 500;
171 | cursor: pointer;
172 | display: inline-block;
173 | white-space: nowrap;
174 | user-select: none;
175 | margin-right: 8px;
176 | vertical-align: middle;
177 | font-size: 0;
178 | }
179 |
180 | .vs-checkbox__input, .vs-radio__input {
181 | white-space: nowrap;
182 | cursor: pointer;
183 | outline: none;
184 | display: inline-block;
185 | line-height: 1;
186 | position: relative;
187 | vertical-align: middle;
188 | }
189 |
190 | .vs-checkbox__inner, .vs-radio__inner {
191 | display: inline-block;
192 | position: relative;
193 | border: 1px solid #d9d9d9;
194 | border-radius: 2px;
195 | box-sizing: border-box;
196 | width: 14px;
197 | height: 14px;
198 | background-color: #FFFFFF;
199 | z-index: 1;
200 | transition: border-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46), background-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46);
201 | }
202 |
203 | .is-indeterminate .vs-checkbox__inner::before {
204 | content: '';
205 | position: absolute;
206 | display: block;
207 | background-color: #1989fa;
208 | height: 12px;
209 | transform: scale(0.6);
210 | left: 0;
211 | right: 0;
212 | top: 0;
213 | border-radius: 2px;
214 | }
215 |
216 | .vs-checkbox__original:checked ~ .vs-checkbox__inner {
217 | background-color: #1989fa;
218 | border-color: #1989fa;
219 | }
220 |
221 | .vs-checkbox__original:checked ~ .vs-checkbox__inner::after {
222 | transform: rotate(45deg) scaleY(1);
223 | }
224 |
225 | .vs-checkbox__inner::after {
226 | box-sizing: content-box;
227 | content: "";
228 | border: 1px solid #FFFFFF;
229 | border-left: 0;
230 | border-top: 0;
231 | height: 7px;
232 | left: 4px;
233 | position: absolute;
234 | top: 1px;
235 | transform: rotate(45deg) scaleY(0);
236 | width: 3px;
237 | transition: all .2s cubic-bezier(.12,.4,.29,1.46) .1s;
238 | transform-origin: center;
239 | }
240 |
241 | .vs-checkbox__original:disabled ~ .vs-checkbox__inner {
242 | background-color: #edf2fc;
243 | border-color: #dcdfe6;
244 | cursor: not-allowed;
245 | }
246 |
247 | .vs-checkbox__original:checked:disabled ~ .vs-checkbox__inner:after {
248 | border-color: #c0c4cc;
249 | }
250 |
251 | .vs-checkbox__original, .vs-radio__original {
252 | opacity: 0;
253 | outline: none;
254 | position: absolute;
255 | margin: 0;
256 | width: 0;
257 | height: 0;
258 | z-index: -1;
259 | }
260 |
261 | .vs-radio__inner {
262 | border-radius: 100%;
263 | }
264 |
265 | .vs-radio__inner:after {
266 | box-sizing: content-box;
267 | content: "";
268 | left: 3px;
269 | position: absolute;
270 | top: 3px;
271 | width: 6px;
272 | height: 6px;
273 | transform: scale(0);
274 | transition: transform .15s ease-in .05s;
275 | transform-origin: center;
276 | border-radius: 100%;
277 | }
278 |
279 | .vs-radio__original:checked ~ .vs-radio__inner {
280 | border-color: #1989fa;
281 | }
282 |
283 | .vs-radio__original:checked ~ .vs-radio__inner:after {
284 | background-color: #1989fa;
285 | transform: scale(1);
286 | }
287 |
288 | .vs-radio__original:checked:disabled ~ .vs-radio__inner:after {
289 | border-color: #c0c4cc;
290 | }
291 |
292 | .vs-icon-leaf, .vs-icon-parent {
293 | width: 14px;
294 | height: 14px;
295 | margin-right: 5px;
296 | display: inline-block;
297 | vertical-align: middle;
298 | background-image: url(./leaf.svg);
299 | background-size: 12px 12px;
300 | background-repeat: no-repeat;
301 | background-position: center;
302 |
303 | > img {
304 | width: 100%;
305 | height: 100%;
306 | }
307 | }
308 |
309 | .vs-icon-parent {
310 | background-image: url(./parent.svg);
311 | }
312 |
313 | .vs-transition {
314 | height: 0;
315 | transition: all .3s ease;
316 | overflow-y: hidden;
317 | }
318 |
319 | .vs-tree-node {
320 | &.vs-drag-enter {
321 | background-color: rgba(#1989fa, .8);
322 | color: #fff;
323 | }
324 | &.vs-drag-over-gap-top,
325 | &.vs-drag-over-gap-bottom {
326 | position: relative;
327 | &::before {
328 | content: '';
329 | position: absolute;
330 | left: 0;
331 | width: 100%;
332 | height: 2px;
333 | background-color: #1989fa;
334 | }
335 | }
336 |
337 | &.vs-drag-over-gap-top {
338 | &::before {
339 | top: 0;
340 | }
341 | }
342 |
343 | &.vs-drag-over-gap-bottom {
344 | &::before {
345 | bottom: 0;
346 | }
347 | }
348 | }
349 |
350 |
351 | .vs-search-only-leaf {
352 | .vs-tree-inner {
353 | padding-left: 0!important;
354 | .expand-empty {
355 | display: none;
356 | }
357 | }
358 | }
359 |
360 | .vs-theme-element {
361 | .is-indeterminate .vs-checkbox__inner {
362 | background-color: #1989fa;
363 | border-color: #1989fa;
364 | }
365 | .is-indeterminate .vs-checkbox__inner::before {
366 | background-color: #fff;
367 | height: 1px;
368 | width: 50%;
369 | top: 50%;
370 | left: 50%;
371 | transform: translate(-50%, -50%) scale(1);
372 | }
373 | }
374 |
375 | // 面包屑
376 | .vs-breadcrumb {
377 | box-sizing: border-box;
378 | margin: 0;
379 | padding: 0;
380 | font-variant: tabular-nums;
381 | line-height: 1.5715;
382 | list-style: none;
383 | font-feature-settings: "tnum";
384 | color: rgba(0,0,0,.45);
385 | font-size: 14px;
386 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
387 |
388 | & > span {
389 | display: inline-block;
390 |
391 | &:not(:last-child) {
392 | cursor: pointer;
393 | color: #1989fa;
394 | &:hover {
395 | color: rgba(#1989fa, .8);
396 | }
397 | }
398 |
399 | &:last-child {
400 | color: rgba(0,0,0,.85);
401 | }
402 | }
403 |
404 | &-separator {
405 | margin: 0 8px;
406 | color: rgba(0,0,0,.45);
407 | }
408 | }
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { version as _v } from '../package.json'
2 | import './less/vs-tree.less'
3 | import vsTree from './core'
4 | import plugin from './vue-plugin'
5 |
6 | export default vsTree
7 |
8 | // 版本号
9 | export const version = _v
10 |
11 | // Vue 插件
12 | export const install = plugin(vsTree)
13 |
--------------------------------------------------------------------------------
/src/virtual-list/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * virtual list default component
3 | */
4 |
5 | import Virtual from './virtual'
6 |
7 | export default class Vlist {
8 | constructor (opts) {
9 | this.range = null
10 |
11 | this.$el = opts.root
12 |
13 | this.$el.style.maxHeight = typeof opts.maxHeight === 'number' ? opts.maxHeight + 'px' : opts.maxHeight
14 | this.$el.style.minHeight = typeof opts.minHeight === 'number' ? opts.minHeight + 'px' : opts.minHeight
15 | this.$el.style.overflowY = 'auto'
16 |
17 | this.dataSources = opts.data
18 |
19 | this.wrapper = document.createElement('div')
20 | this.wrapper.className = 'vs-virtual-list'
21 | this.$el.appendChild(this.wrapper)
22 |
23 | this.$el.addEventListener('scroll', this.onScroll.bind(this), {
24 | passive: false
25 | })
26 |
27 | this.keeps = opts.keeps || 20
28 |
29 | this.estimateSize = opts.estimateSize || 26
30 |
31 | this.dataKey = 'id'
32 |
33 | this.installVirtual()
34 | }
35 |
36 | // return current scroll offset
37 | getOffset () {
38 | const root = this.$el
39 | return root ? Math.ceil(root.scrollTop) : 0
40 | }
41 |
42 | // return client viewport size
43 | getClientSize () {
44 | const root = this.$el
45 | return root ? Math.ceil(root.clientHeight) : 0
46 | }
47 |
48 | // return all scroll size
49 | getScrollSize () {
50 | const root = this.$el
51 | return root ? Math.ceil(root.scrollHeight) : 0
52 | }
53 |
54 | // set current scroll position to a expectant index
55 | scrollToIndex (index) {
56 | // scroll to bottom
57 | if (index >= this.dataSources.length - 1) {
58 | this.scrollToBottom()
59 | } else {
60 | const offset = this.virtual.getOffset(index)
61 | this.scrollToOffset(offset)
62 | }
63 | }
64 |
65 | // reset all state back to initial
66 | reset () {
67 | this.virtual.destroy()
68 | this.scrollToOffset(0)
69 | this.installVirtual()
70 | }
71 |
72 | // ----------- public method end -----------
73 |
74 | installVirtual () {
75 | this.virtual = new Virtual({
76 | slotHeaderSize: 0,
77 | slotFooterSize: 0,
78 | keeps: this.keeps,
79 | estimateSize: this.estimateSize,
80 | buffer: Math.round(this.keeps / 3), // recommend for a third of keeps
81 | uniqueIds: this.getUniqueIdFromDataSources()
82 | }, this.onRangeChanged.bind(this))
83 |
84 | // sync initial range
85 | this.range = this.virtual.getRange()
86 | this.render()
87 | }
88 |
89 | getUniqueIdFromDataSources () {
90 | const { dataKey } = this
91 | return this.dataSources.map((dataSource) => typeof dataKey === 'function' ? dataKey(dataSource) : dataSource[dataKey])
92 | }
93 |
94 | // here is the rerendering entry
95 | onRangeChanged (range) {
96 | this.range = range
97 | this.render()
98 | }
99 |
100 | onScroll () {
101 | const offset = this.getOffset()
102 | const clientSize = this.getClientSize()
103 | const scrollSize = this.getScrollSize()
104 |
105 | // iOS scroll-spring-back behavior will make direction mistake
106 | if (offset < 0 || (offset + clientSize > scrollSize + 1) || !scrollSize) {
107 | return
108 | }
109 |
110 | this.virtual.handleScroll(offset)
111 | }
112 |
113 | getRenderSlots () {
114 | const { start, end } = this.range
115 | const { dataSources, dataKey } = this
116 | this.wrapper.innerHTML = ''
117 | for (let index = start; index <= end; index++) {
118 | const dataSource = dataSources[index]
119 | if (dataSource) {
120 | const uniqueKey = typeof dataKey === 'function' ? dataKey(dataSource) : dataSource[dataKey]
121 | if (typeof uniqueKey === 'string' || typeof uniqueKey === 'number') {
122 | const dom = dataSource.createNode()
123 | if (dataSource.store.onlySearchLeaf) {
124 | dom.classList.add('vs-search-only-leaf')
125 | } else {
126 | dom.classList.remove('vs-search-only-leaf')
127 | }
128 |
129 | if (dataSource.store.isSearch && dataSource.store.searchRender) {
130 | const searchNode = dataSource.store.searchRender(dataSource, dom.cloneNode(true))
131 | if (!(searchNode instanceof HTMLElement)) {
132 | throw Error('searchRender must return HTMLElement')
133 | }
134 | this.wrapper.appendChild(searchNode)
135 | } else {
136 | this.wrapper.appendChild(dom)
137 | }
138 | } else {
139 | console.warn(`Cannot get the data-key '${dataKey}' from data-sources.`)
140 | }
141 | } else {
142 | console.warn(`Cannot get the index '${index}' from data-sources.`)
143 | }
144 | }
145 | }
146 |
147 | update (data) {
148 | this.dataSources = data
149 | this.wrapper.innerHTML = ''
150 | this.virtual.updateParam('uniqueIds', this.getUniqueIdFromDataSources())
151 | this.virtual.handleDataSourcesChange()
152 | }
153 |
154 | render () {
155 | const { padFront, padBehind } = this.range
156 |
157 | const paddingStyle = `${padFront}px 0px ${padBehind}px`
158 |
159 | this.wrapper.style.padding = paddingStyle
160 |
161 | this.getRenderSlots()
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/virtual-list/virtual.js:
--------------------------------------------------------------------------------
1 | /**
2 | * virtual list core calculating center
3 | */
4 |
5 | const DIRECTION_TYPE = {
6 | FRONT: 'FRONT', // scroll up or left
7 | BEHIND: 'BEHIND' // scroll down or right
8 | }
9 | const CALC_TYPE = {
10 | INIT: 'INIT',
11 | FIXED: 'FIXED',
12 | DYNAMIC: 'DYNAMIC'
13 | }
14 | const LEADING_BUFFER = 2
15 |
16 | export default class Virtual {
17 | constructor (param, callUpdate) {
18 | this.init(param, callUpdate)
19 | }
20 |
21 | init (param, callUpdate) {
22 | // param data
23 | this.param = param
24 | this.callUpdate = callUpdate
25 |
26 | // size data
27 | this.sizes = new Map()
28 | this.firstRangeTotalSize = 0
29 | this.firstRangeAverageSize = 0
30 | this.lastCalcIndex = 0
31 | this.fixedSizeValue = 0
32 | this.calcType = CALC_TYPE.INIT
33 |
34 | // scroll data
35 | this.offset = 0
36 | this.direction = ''
37 |
38 | // range data
39 | this.range = Object.create(null)
40 | if (param) {
41 | this.checkRange(0, param.keeps - 1)
42 | }
43 |
44 | // benchmark test data
45 | // this.__bsearchCalls = 0
46 | // this.__getIndexOffsetCalls = 0
47 | }
48 |
49 | destroy () {
50 | this.init(null, null)
51 | }
52 |
53 | // return current render range
54 | getRange () {
55 | const range = Object.create(null)
56 | range.start = this.range.start
57 | range.end = this.range.end
58 | range.padFront = this.range.padFront
59 | range.padBehind = this.range.padBehind
60 | return range
61 | }
62 |
63 | isBehind () {
64 | return this.direction === DIRECTION_TYPE.BEHIND
65 | }
66 |
67 | isFront () {
68 | return this.direction === DIRECTION_TYPE.FRONT
69 | }
70 |
71 | // return start index offset
72 | getOffset (start) {
73 | return (start < 1 ? 0 : this.getIndexOffset(start)) + this.param.slotHeaderSize
74 | }
75 |
76 | updateParam (key, value) {
77 | if (this.param && (key in this.param)) {
78 | // if uniqueIds change, find out deleted id and remove from size map
79 | if (key === 'uniqueIds') {
80 | this.sizes.forEach((v, key) => {
81 | if (!value.includes(key)) {
82 | this.sizes.delete(key)
83 | }
84 | })
85 | }
86 | this.param[key] = value
87 | }
88 | }
89 |
90 | // in some special situation (e.g. length change) we need to update in a row
91 | // try goiong to render next range by a leading buffer according to current direction
92 | handleDataSourcesChange () {
93 | let start = this.range.start
94 |
95 | if (this.isFront()) {
96 | start = start - LEADING_BUFFER
97 | } else if (this.isBehind()) {
98 | start = start + LEADING_BUFFER
99 | }
100 |
101 | start = Math.max(start, 0)
102 |
103 | this.updateRange(this.range.start, this.getEndByStart(start))
104 | }
105 |
106 | // when slot size change, we also need force update
107 | handleSlotSizeChange () {
108 | this.handleDataSourcesChange()
109 | }
110 |
111 | // calculating range on scroll
112 | handleScroll (offset) {
113 | this.direction = offset < this.offset ? DIRECTION_TYPE.FRONT : DIRECTION_TYPE.BEHIND
114 | this.offset = offset
115 |
116 | if (!this.param) {
117 | return
118 | }
119 |
120 | if (this.direction === DIRECTION_TYPE.FRONT) {
121 | this.handleFront()
122 | } else if (this.direction === DIRECTION_TYPE.BEHIND) {
123 | this.handleBehind()
124 | }
125 | }
126 |
127 | // ----------- public method end -----------
128 |
129 | handleFront () {
130 | const overs = this.getScrollOvers()
131 | // should not change range if start doesn't exceed overs
132 | if (overs > this.range.start) {
133 | return
134 | }
135 |
136 | // move up start by a buffer length, and make sure its safety
137 | const start = Math.max(overs - this.param.buffer, 0)
138 | this.checkRange(start, this.getEndByStart(start))
139 | }
140 |
141 | handleBehind () {
142 | const overs = this.getScrollOvers()
143 | // range should not change if scroll overs within buffer
144 | if (overs < this.range.start + this.param.buffer) {
145 | return
146 | }
147 |
148 | this.checkRange(overs, this.getEndByStart(overs))
149 | }
150 |
151 | // return the pass overs according to current scroll offset
152 | getScrollOvers () {
153 | // if slot header exist, we need subtract its size
154 | const offset = this.offset - this.param.slotHeaderSize
155 | if (offset <= 0) {
156 | return 0
157 | }
158 |
159 | // if is fixed type, that can be easily
160 | if (this.isFixedType()) {
161 | return Math.floor(offset / this.fixedSizeValue)
162 | }
163 |
164 | let low = 0
165 | let middle = 0
166 | let middleOffset = 0
167 | let high = this.param.uniqueIds.length
168 |
169 | while (low <= high) {
170 | // this.__bsearchCalls++
171 | middle = low + Math.floor((high - low) / 2)
172 | middleOffset = this.getIndexOffset(middle)
173 |
174 | if (middleOffset === offset) {
175 | return middle
176 | } else if (middleOffset < offset) {
177 | low = middle + 1
178 | } else if (middleOffset > offset) {
179 | high = middle - 1
180 | }
181 | }
182 |
183 | return low > 0 ? --low : 0
184 | }
185 |
186 | // return a scroll offset from given index, can efficiency be improved more here?
187 | // although the call frequency is very high, its only a superposition of numbers
188 | getIndexOffset (givenIndex) {
189 | if (!givenIndex) {
190 | return 0
191 | }
192 |
193 | let offset = 0
194 | let indexSize = 0
195 | for (let index = 0; index < givenIndex; index++) {
196 | // this.__getIndexOffsetCalls++
197 | indexSize = this.sizes.get(this.param.uniqueIds[index])
198 | offset = offset + (typeof indexSize === 'number' ? indexSize : this.getEstimateSize())
199 | }
200 |
201 | // remember last calculate index
202 | this.lastCalcIndex = Math.max(this.lastCalcIndex, givenIndex - 1)
203 | this.lastCalcIndex = Math.min(this.lastCalcIndex, this.getLastIndex())
204 |
205 | return offset
206 | }
207 |
208 | // is fixed size type
209 | isFixedType () {
210 | return this.calcType === CALC_TYPE.FIXED
211 | }
212 |
213 | // return the real last index
214 | getLastIndex () {
215 | return this.param.uniqueIds.length - 1
216 | }
217 |
218 | // in some conditions range is broke, we need correct it
219 | // and then decide whether need update to next range
220 | checkRange (start, end) {
221 | const keeps = this.param.keeps
222 | const total = this.param.uniqueIds.length
223 |
224 | // datas less than keeps, render all
225 | if (total <= keeps) {
226 | start = 0
227 | end = this.getLastIndex()
228 | } else if (end - start < keeps - 1) {
229 | // if range length is less than keeps, corrent it base on end
230 | start = end - keeps + 1
231 | }
232 |
233 | if (this.range.start !== start) {
234 | this.updateRange(start, end)
235 | }
236 | }
237 |
238 | // setting to a new range and rerender
239 | updateRange (start, end) {
240 | this.range.start = start
241 | this.range.end = end
242 | this.range.padFront = this.getPadFront()
243 | this.range.padBehind = this.getPadBehind()
244 | this.callUpdate(this.getRange())
245 | }
246 |
247 | // return end base on start
248 | getEndByStart (start) {
249 | const theoryEnd = start + this.param.keeps - 1
250 | const truelyEnd = Math.min(theoryEnd, this.getLastIndex())
251 | return truelyEnd
252 | }
253 |
254 | // return total front offset
255 | getPadFront () {
256 | if (this.isFixedType()) {
257 | return this.fixedSizeValue * this.range.start
258 | } else {
259 | return this.getIndexOffset(this.range.start)
260 | }
261 | }
262 |
263 | // return total behind offset
264 | getPadBehind () {
265 | const end = this.range.end
266 | const lastIndex = this.getLastIndex()
267 |
268 | if (this.isFixedType()) {
269 | return (lastIndex - end) * this.fixedSizeValue
270 | }
271 |
272 | // if it's all calculated, return the exactly offset
273 | if (this.lastCalcIndex === lastIndex) {
274 | return this.getIndexOffset(lastIndex) - this.getIndexOffset(end)
275 | } else {
276 | // if not, use a estimated value
277 | return (lastIndex - end) * this.getEstimateSize()
278 | }
279 | }
280 |
281 | // get the item estimate size
282 | getEstimateSize () {
283 | return this.isFixedType() ? this.fixedSizeValue : (this.firstRangeAverageSize || this.param.estimateSize)
284 | }
285 | }
286 |
--------------------------------------------------------------------------------
/src/vue-plugin/index.js:
--------------------------------------------------------------------------------
1 | export default (VsTree) => {
2 | return (Vue, options = {}) => {
3 | Vue.component('vs-tree', {
4 | props: {
5 | data: Array | Object,
6 | options: Object,
7 | async: Boolean,
8 | animation: Boolean,
9 | draggable: Boolean,
10 | dropable: Boolean,
11 | hideRoot: Boolean,
12 | showCheckbox: Boolean,
13 | checkboxType: Object,
14 | showRadio: Boolean,
15 | radioType: String,
16 | showLine: Boolean,
17 | showIcon: Boolean,
18 | onlyShowLeafIcon: Boolean,
19 | highlightCurrent: Boolean,
20 | accordion: Boolean,
21 | nocheckParent: Boolean,
22 | sort: Boolean,
23 | checkOnClickNode: Boolean,
24 | checkFilterLeaf: Boolean,
25 | strictLeaf: Boolean,
26 | rootName: String,
27 | max: Number,
28 | lazy: Boolean,
29 | load: Function,
30 | format: Function,
31 | disabledKeys: Array,
32 | checkedKeys: Array,
33 | expandKeys: Array,
34 | keyword: String,
35 | expandClass: String,
36 | theme: String,
37 | breadcrumb: Object,
38 | virtual: Object,
39 | expandLevel: {
40 | type: Number,
41 | default: 1
42 | },
43 | indent: {
44 | type: Number,
45 | default: 10
46 | },
47 | showCount: {
48 | type: Number,
49 | default: 20
50 | },
51 | itemHeight: {
52 | type: Number,
53 | default: 26
54 | },
55 |
56 | maxHeight: String,
57 | minHeight: String,
58 |
59 | beforeCheck: Function,
60 | renderContent: Function,
61 | checkFilter: Function,
62 | searchFilter: Function,
63 | searchRender: Function,
64 | onDragstart: Function,
65 | onDragenter: Function,
66 | onDrop: Function
67 | },
68 | data () {
69 | return {
70 | tree: {}
71 | }
72 | },
73 | watch: {
74 | max (newVal = 0) {
75 | this.setMaxValue(newVal)
76 | },
77 | keyword (newVal) {
78 | this.filter(newVal)
79 | }
80 | },
81 | mounted () {
82 | this.$nextTick(() => {
83 | this._vsinit()
84 | })
85 | },
86 | methods: {
87 | _vsinit () {
88 | console.time('render:tree')
89 | this.tree.tree = new VsTree(this.$refs.tree, Object.assign({}, options, this.$props, {
90 | ...this.options,
91 | data: this.data,
92 | click: (event, node) => {
93 | this.$emit('click', event, node)
94 | },
95 | check: (event, node) => {
96 | this.$emit('check', event, node)
97 | },
98 | change: (node) => {
99 | this.$emit('change', node)
100 | },
101 | contextmenu: (event, node) => {
102 | this.$emit('node-contextmenu', event, node)
103 | },
104 | limitAlert: () => {
105 | this.$emit('limit-alert')
106 | }
107 | }))
108 | console.timeEnd('render:tree')
109 | },
110 | getNodeById (id) {
111 | return this.tree.tree.getNodeById(id)
112 | },
113 | getCheckedNodes () {
114 | return this.tree.tree.getCheckedNodes()
115 | },
116 | filter (value) {
117 | return this.tree.tree.filter(value)
118 | },
119 | setMaxValue (value = 0) {
120 | this.tree.tree.setMaxValue(value)
121 | }
122 | },
123 | render (h) {
124 | return h('div', {
125 | ref: 'tree'
126 | }, this.$slots.default)
127 | }
128 | })
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "outDir": "./lib", // 输出目录
5 | "sourceMap": false, // 是否生成sourceMap
6 | "target": "esnext", // 编译目标
7 | "module": "esnext", // 模块类型
8 | "moduleResolution": "node",
9 | "allowJs": true, // 是否编辑js文件
10 | "strict": true, // 严格模式
11 | "noUnusedLocals": true, // 未使用变量报错
12 | "experimentalDecorators": true, // 启动装饰器
13 | "resolveJsonModule": true, // 加载json
14 | "esModuleInterop": true,
15 | "removeComments": false, // 删除注释
16 | "declaration": true, // 生成定义文件
17 | "declarationMap": false, // 生成定义sourceMap
18 | "declarationDir": "./types", // 定义文件输出目录
19 | "lib": ["esnext", "dom"], // 导入库类型定义
20 | "types": ["node"] // 导入指定类型包
21 | },
22 | "include": [
23 | "src/" // 导入目录
24 | ]
25 | }
--------------------------------------------------------------------------------
/types/src/breadcrumb/breadcrumb-item.d.ts:
--------------------------------------------------------------------------------
1 | export default class BreadcrumbItem {
2 | constructor(node: any, parent: any);
3 | node: any;
4 | data: any;
5 | store: any;
6 | parent: any;
7 | renderIcon: any;
8 | renderLink: any;
9 | renderSeparator: any;
10 | createDom(): HTMLSpanElement;
11 | createIcon(): false | HTMLSpanElement;
12 | createLink(breads: any, index: any, last: any): HTMLSpanElement;
13 | createSeparator(): HTMLSpanElement;
14 | }
15 |
--------------------------------------------------------------------------------
/types/src/breadcrumb/index.d.ts:
--------------------------------------------------------------------------------
1 | export interface BreadcrumbOptions {
2 | el: string | HTMLElement;
3 | icon?: Function | string;
4 | link?: Function | string;
5 | separator?: Function | string;
6 | change?: Function;
7 | }
8 | export default class Breadcrumb {
9 | store: any;
10 | list: never[];
11 | options: BreadcrumbOptions;
12 | constructor(options: BreadcrumbOptions);
13 | get current(): any;
14 | renderBreadcrumb(): void;
15 | }
16 |
--------------------------------------------------------------------------------
/types/src/core/index.d.ts:
--------------------------------------------------------------------------------
1 | export default class Tree {
2 | constructor(selector: any, ops: any);
3 | $el: HTMLElement;
4 | interpolate: RegExp;
5 | _data: any;
6 | nodes: any[];
7 | itemHeight: any;
8 | showCount: any;
9 | maxHeight: any;
10 | minHeight: any;
11 | data: any[];
12 | keyword: string;
13 | searchFilter: any;
14 | ready: any;
15 | $$breadcrumb: Breadcrumb | undefined;
16 | store: TreeStore;
17 | _init(): void;
18 | vlist: Vlist | undefined;
19 | _render(update?: boolean): void;
20 | _keywordFilter(data: any): void;
21 | _hasKeyword(v: any): any;
22 | _checkFilter(v: any): any;
23 | filter(keyword: string | undefined, onlySearchLeaf: any): any[];
24 | getNodeById(id: any): any;
25 | getCheckedNodes(...args: any[]): any[];
26 | setMaxValue(value?: number): void;
27 | scrollToIndex(index?: number): void;
28 | clearCheckedNodes(): void;
29 | }
30 | import Breadcrumb from "../breadcrumb";
31 | import TreeStore from "./store";
32 | import Vlist from "../virtual-list";
33 |
--------------------------------------------------------------------------------
/types/src/core/node.d.ts:
--------------------------------------------------------------------------------
1 | export default class Node {
2 | constructor(ops: any);
3 | id: number;
4 | checked: any;
5 | expanded: boolean;
6 | indeterminate: boolean;
7 | visbile: boolean;
8 | disabled: any;
9 | loaded: boolean;
10 | isLeaf: boolean;
11 | level: any;
12 | childNodes: any[];
13 | store: any;
14 | parent: any;
15 | originData: any;
16 | __buffer: {};
17 | data: any;
18 | initData(): void;
19 | createNode(): HTMLDivElement;
20 | dom: HTMLDivElement | undefined;
21 | createInner(): HTMLDivElement;
22 | loadingEl: HTMLSpanElement | undefined;
23 | cusmtomNode(name: any, info: any): any;
24 | createContent(): any;
25 | createExpandEmpty(): HTMLSpanElement;
26 | createExpand(): HTMLSpanElement;
27 | expandEl: HTMLSpanElement | undefined;
28 | createCheckbox(): HTMLLabelElement;
29 | radioNode: HTMLInputElement | undefined;
30 | checkboxNode: HTMLInputElement | undefined;
31 | checkboxEl: HTMLInputElement | undefined;
32 | handleCheckChange(e: any): void;
33 | createText(): any;
34 | createIcon(): HTMLSpanElement;
35 | setData(data: any): void;
36 | insertChild(child: any, index: any): any;
37 | insertBefore(child: any, ref: any): void;
38 | insertAfter(child: any, ref: any): void;
39 | updateExpand(expand: any): void;
40 | updateChecked(check: any, isInitDefault: any): void;
41 | sortId: number | undefined;
42 | updateCheckedParent(_checked: any, isInitDefault: any): void;
43 | updateRadioChecked(checked: any, isInitDefault: any): void;
44 | setChecked(checked: any, isInitDefault: any): void;
45 | setDisabled(disabled?: boolean): void;
46 | setExpand(expand: any, noUpdate: any): void;
47 | storeUpdate(): void;
48 | createAnimation(): void;
49 | transitionNode: HTMLDivElement | undefined;
50 | createDragable(dom: any): void;
51 | setAccordion(expand: any): void;
52 | loadData(callback: any): void;
53 | loading: boolean | undefined;
54 | remove(): void;
55 | append(data: any): void;
56 | }
57 |
--------------------------------------------------------------------------------
/types/src/core/store.d.ts:
--------------------------------------------------------------------------------
1 | export default class TreeStore {
2 | constructor(options: any);
3 | nodes: any[];
4 | dataMap: Map;
5 | nodeMap: Map;
6 | radioMap: {};
7 | expandMap: {};
8 | root: Node;
9 | changeNodes: any[];
10 | setData(val: any): void;
11 | updateNodes(): void;
12 | flattenTreeData(): any[];
13 | getNodeById(id: any): any;
14 | getCheckedNodes(isTreeNode?: boolean): any[];
15 | setDefaultChecked(): void;
16 | checkMaxNodes(node: any): boolean;
17 | getUnCheckLeafsCount(node: any): number;
18 | allowEmit(check: any, type: any): boolean;
19 | _checkVerify(node: any): any;
20 | _change(node: any): void;
21 | _changeTimer: NodeJS.Timeout | undefined;
22 | }
23 | import Node from "./node";
24 |
--------------------------------------------------------------------------------
/types/src/core/utils.d.ts:
--------------------------------------------------------------------------------
1 | import Node from "./node";
2 | export declare function insterAfter(newElement: HTMLElement, targetElement: HTMLElement): void;
3 | export declare function onDragEnterGap(e: MouseEvent, treeNode: HTMLElement): 1 | -1 | 0;
4 | export declare const findNearestNode: (element: HTMLElement, name: string) => any;
5 | export declare const parseTemplate: (name: string, ctx: Node) => any;
6 |
--------------------------------------------------------------------------------
/types/src/main.d.ts:
--------------------------------------------------------------------------------
1 | export default vsTree;
2 | export const version: string;
3 | export const install: (Vue: any, options?: {}) => void;
4 | import vsTree from "./core";
5 |
--------------------------------------------------------------------------------
/types/src/virtual-list/index.d.ts:
--------------------------------------------------------------------------------
1 | export default class Vlist {
2 | constructor(opts: any);
3 | range: any;
4 | $el: any;
5 | dataSources: any;
6 | wrapper: HTMLDivElement;
7 | keeps: any;
8 | estimateSize: any;
9 | dataKey: string;
10 | getOffset(): number;
11 | getClientSize(): number;
12 | getScrollSize(): number;
13 | scrollToIndex(index: any): void;
14 | reset(): void;
15 | installVirtual(): void;
16 | virtual: Virtual | undefined;
17 | getUniqueIdFromDataSources(): any;
18 | onRangeChanged(range: any): void;
19 | onScroll(): void;
20 | getRenderSlots(): void;
21 | update(data: any): void;
22 | render(): void;
23 | }
24 | import Virtual from "./virtual";
25 |
--------------------------------------------------------------------------------
/types/src/virtual-list/virtual.d.ts:
--------------------------------------------------------------------------------
1 | export default class Virtual {
2 | constructor(param: any, callUpdate: any);
3 | init(param: any, callUpdate: any): void;
4 | param: any;
5 | callUpdate: any;
6 | sizes: Map | undefined;
7 | firstRangeTotalSize: number | undefined;
8 | firstRangeAverageSize: number | undefined;
9 | lastCalcIndex: any;
10 | fixedSizeValue: number | undefined;
11 | calcType: string | undefined;
12 | offset: any;
13 | direction: string | undefined;
14 | range: any;
15 | destroy(): void;
16 | getRange(): any;
17 | isBehind(): boolean;
18 | isFront(): boolean;
19 | getOffset(start: any): any;
20 | updateParam(key: any, value: any): void;
21 | handleDataSourcesChange(): void;
22 | handleSlotSizeChange(): void;
23 | handleScroll(offset: any): void;
24 | handleFront(): void;
25 | handleBehind(): void;
26 | getScrollOvers(): number;
27 | getIndexOffset(givenIndex: any): number;
28 | isFixedType(): boolean;
29 | getLastIndex(): number;
30 | checkRange(start: any, end: any): void;
31 | updateRange(start: any, end: any): void;
32 | getEndByStart(start: any): number;
33 | getPadFront(): number;
34 | getPadBehind(): number;
35 | getEstimateSize(): any;
36 | }
37 |
--------------------------------------------------------------------------------
/types/src/vue-plugin/index.d.ts:
--------------------------------------------------------------------------------
1 | declare function _default(VsTree: any): (Vue: any, options?: {}) => void;
2 | export default _default;
3 |
--------------------------------------------------------------------------------