hy-virtual-tree 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +300 -0
- package/dist/index.css +278 -0
- package/dist/index.js +1218 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* js-booster - High-performance frontend library
|
|
3
|
+
* VirtualScroll - Virtual scrolling implementation
|
|
4
|
+
* @version "1.1.4"
|
|
5
|
+
* @author https://cg-zhou.top/
|
|
6
|
+
* @license MIT
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class VirtualScroll {
|
|
10
|
+
/**
|
|
11
|
+
* Create a virtual scroll instance
|
|
12
|
+
* @param {Object} options Configuration options
|
|
13
|
+
* @param {HTMLElement} options.container Scroll container element
|
|
14
|
+
* @param {Array} options.items Data items to display
|
|
15
|
+
* @param {number} [options.itemHeight=20] Height of each list item (pixels)
|
|
16
|
+
* @param {number} [options.bufferSize=10] Number of buffer items outside the visible area
|
|
17
|
+
* @param {Function} [options.renderItem] Custom item rendering function
|
|
18
|
+
* @param {Function} [options.renderHeader] Custom header rendering function
|
|
19
|
+
* @param {number} [options.maxHeight=26840000] Maximum height in pixels for the content wrapper
|
|
20
|
+
*/
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.container = options.container;
|
|
23
|
+
this.items = options.items || [];
|
|
24
|
+
this.itemHeight = options.itemHeight || 20;
|
|
25
|
+
this.bufferSize = options.bufferSize || 10;
|
|
26
|
+
this.customRenderItem = options.renderItem;
|
|
27
|
+
this.customRenderHeader = options.renderHeader;
|
|
28
|
+
this.maxHeight = options.maxHeight || 26840000; // Add maximum height limit to prevent DOM height overflow
|
|
29
|
+
|
|
30
|
+
this.visibleStartIndex = 0;
|
|
31
|
+
this.visibleEndIndex = 0;
|
|
32
|
+
this.scrollContainer = null;
|
|
33
|
+
this.contentWrapper = null;
|
|
34
|
+
this.contentContainer = null;
|
|
35
|
+
this.totalHeight = this.items.length * this.itemHeight;
|
|
36
|
+
this.heightScale = 1; // Height scaling factor
|
|
37
|
+
|
|
38
|
+
// If total height exceeds maximum height, calculate scaling factor
|
|
39
|
+
if (this.totalHeight > this.maxHeight) {
|
|
40
|
+
this.heightScale = this.maxHeight / this.totalHeight;
|
|
41
|
+
}
|
|
42
|
+
this.initialize();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Initialize virtual scroll component
|
|
47
|
+
* @private
|
|
48
|
+
*/
|
|
49
|
+
initialize() {
|
|
50
|
+
// Clear container
|
|
51
|
+
this.container.innerHTML = '';
|
|
52
|
+
|
|
53
|
+
// Create scroll container
|
|
54
|
+
this.scrollContainer = document.createElement('div');
|
|
55
|
+
// Add inline styles
|
|
56
|
+
Object.assign(this.scrollContainer.style, {
|
|
57
|
+
flex: '1',
|
|
58
|
+
overflow: 'auto',
|
|
59
|
+
position: 'relative',
|
|
60
|
+
minHeight: '0',
|
|
61
|
+
height: '100%',
|
|
62
|
+
boxSizing: 'border-box'
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// If there's a custom header render function, render the header
|
|
66
|
+
if (this.customRenderHeader) {
|
|
67
|
+
const header = this.customRenderHeader();
|
|
68
|
+
if (header) {
|
|
69
|
+
this.scrollContainer.appendChild(header);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Create content wrapper
|
|
74
|
+
this.contentWrapper = document.createElement('div');
|
|
75
|
+
// Add inline styles
|
|
76
|
+
Object.assign(this.contentWrapper.style, {
|
|
77
|
+
position: 'relative',
|
|
78
|
+
width: '100%'
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Use scaled height to ensure it doesn't exceed browser limits
|
|
82
|
+
const scaledHeight = this.totalHeight * this.heightScale;
|
|
83
|
+
this.contentWrapper.style.height = `${scaledHeight}px`;
|
|
84
|
+
|
|
85
|
+
// Create content container
|
|
86
|
+
this.contentContainer = document.createElement('div');
|
|
87
|
+
// Add inline styles
|
|
88
|
+
Object.assign(this.contentContainer.style, {
|
|
89
|
+
position: 'absolute',
|
|
90
|
+
width: '100%',
|
|
91
|
+
left: '0'
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Add scroll event listener
|
|
95
|
+
this.scrollContainer.addEventListener('scroll', this.handleScroll.bind(this));
|
|
96
|
+
|
|
97
|
+
// Assemble DOM
|
|
98
|
+
this.contentWrapper.appendChild(this.contentContainer);
|
|
99
|
+
this.scrollContainer.appendChild(this.contentWrapper);
|
|
100
|
+
this.container.appendChild(this.scrollContainer);
|
|
101
|
+
|
|
102
|
+
// Render initial visible items
|
|
103
|
+
this.renderVisibleItems(0, Math.min(100, this.items.length));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Handle scroll event
|
|
108
|
+
* @private
|
|
109
|
+
*/
|
|
110
|
+
handleScroll() {
|
|
111
|
+
const scrollTop = this.scrollContainer.scrollTop;
|
|
112
|
+
const containerHeight = this.scrollContainer.clientHeight;
|
|
113
|
+
|
|
114
|
+
// Consider scaling factor in calculations
|
|
115
|
+
const realScrollTop = scrollTop / this.heightScale;
|
|
116
|
+
|
|
117
|
+
// Calculate visible range
|
|
118
|
+
const startIndex = Math.max(0, Math.floor(realScrollTop / this.itemHeight) - this.bufferSize);
|
|
119
|
+
const endIndex = Math.min(this.items.length, Math.ceil((realScrollTop + containerHeight / this.heightScale) / this.itemHeight) + this.bufferSize);
|
|
120
|
+
|
|
121
|
+
// Only update when visible range changes
|
|
122
|
+
if (startIndex !== this.visibleStartIndex || endIndex !== this.visibleEndIndex || endIndex === 0) {
|
|
123
|
+
this.renderVisibleItems(startIndex, endIndex);
|
|
124
|
+
this.visibleStartIndex = startIndex;
|
|
125
|
+
this.visibleEndIndex = endIndex;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Render visible items
|
|
131
|
+
* @param {number} startIndex Start index
|
|
132
|
+
* @param {number} endIndex End index
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
renderVisibleItems(startIndex, endIndex) {
|
|
136
|
+
// Clear content container
|
|
137
|
+
this.contentContainer.innerHTML = '';
|
|
138
|
+
|
|
139
|
+
// Set position considering scaling factor
|
|
140
|
+
this.contentContainer.style.transform = `translateY(${startIndex * this.itemHeight * this.heightScale}px)`;
|
|
141
|
+
|
|
142
|
+
// Render visible items
|
|
143
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
144
|
+
const item = this.items[i];
|
|
145
|
+
if (this.customRenderItem) {
|
|
146
|
+
// Use custom render function
|
|
147
|
+
const itemElement = this.customRenderItem(item, i);
|
|
148
|
+
if (itemElement) {
|
|
149
|
+
// Only set necessary height styles, other styles are determined by the caller
|
|
150
|
+
itemElement.style.height = `${this.itemHeight * this.heightScale}px`;
|
|
151
|
+
itemElement.style.boxSizing = 'border-box';
|
|
152
|
+
itemElement.style.width = '100%';
|
|
153
|
+
this.contentContainer.appendChild(itemElement);
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
// Use default rendering - very simple default implementation
|
|
157
|
+
const row = document.createElement('div');
|
|
158
|
+
Object.assign(row.style, {
|
|
159
|
+
height: `${this.itemHeight * this.heightScale}px`,
|
|
160
|
+
width: '100%',
|
|
161
|
+
boxSizing: 'border-box',
|
|
162
|
+
padding: '8px',
|
|
163
|
+
borderBottom: '1px solid #eee'
|
|
164
|
+
});
|
|
165
|
+
row.textContent = JSON.stringify(item);
|
|
166
|
+
this.contentContainer.appendChild(row);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Update data items and re-render
|
|
173
|
+
* @param {Array} items New data items array
|
|
174
|
+
* @public
|
|
175
|
+
*/
|
|
176
|
+
updateItems(items) {
|
|
177
|
+
this.items = items || [];
|
|
178
|
+
this.totalHeight = this.items.length * this.itemHeight;
|
|
179
|
+
|
|
180
|
+
// Recalculate scaling factor
|
|
181
|
+
this.heightScale = 1;
|
|
182
|
+
if (this.totalHeight > this.maxHeight) {
|
|
183
|
+
this.heightScale = this.maxHeight / this.totalHeight;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Ensure height is set correctly
|
|
187
|
+
if (this.contentWrapper) {
|
|
188
|
+
this.contentWrapper.style.height = `${this.totalHeight * this.heightScale}px`;
|
|
189
|
+
}
|
|
190
|
+
this.visibleStartIndex = 0;
|
|
191
|
+
this.visibleEndIndex = 0;
|
|
192
|
+
|
|
193
|
+
// Force recalculation of visible items
|
|
194
|
+
this.handleScroll();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Scroll to specified index
|
|
199
|
+
* @param {number} index Index of the item to scroll to
|
|
200
|
+
* @public
|
|
201
|
+
*/
|
|
202
|
+
scrollToIndex(index) {
|
|
203
|
+
if (index >= 0 && index < this.items.length) {
|
|
204
|
+
// Apply scaling factor when scrolling
|
|
205
|
+
this.scrollContainer.scrollTop = index * this.itemHeight * this.heightScale;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Destroy component, remove event listeners, etc.
|
|
211
|
+
* @public
|
|
212
|
+
*/
|
|
213
|
+
destroy() {
|
|
214
|
+
if (this.scrollContainer) {
|
|
215
|
+
this.scrollContainer.removeEventListener('scroll', this.handleScroll);
|
|
216
|
+
}
|
|
217
|
+
if (this.container) {
|
|
218
|
+
this.container.innerHTML = '';
|
|
219
|
+
}
|
|
220
|
+
this.items = null;
|
|
221
|
+
this.container = null;
|
|
222
|
+
this.scrollContainer = null;
|
|
223
|
+
this.contentWrapper = null;
|
|
224
|
+
this.contentContainer = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Refresh virtual scroll, re-render current visible items
|
|
229
|
+
* @public
|
|
230
|
+
*/
|
|
231
|
+
refresh() {
|
|
232
|
+
this.handleScroll();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get scroll container element
|
|
237
|
+
* @returns {HTMLElement} Scroll container element
|
|
238
|
+
* @public
|
|
239
|
+
*/
|
|
240
|
+
getScrollContainer() {
|
|
241
|
+
return this.scrollContainer;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* js-booster - High-performance frontend library
|
|
247
|
+
* @version "1.1.4"
|
|
248
|
+
* @author https://cg-zhou.top/
|
|
249
|
+
* @license MIT
|
|
250
|
+
*/
|
|
251
|
+
|
|
252
|
+
// If in browser environment, add to global object
|
|
253
|
+
if (typeof window !== 'undefined') {
|
|
254
|
+
window.JsBooster = {
|
|
255
|
+
VirtualScroll
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const isString = (e) => typeof e === 'string';
|
|
260
|
+
const isNumber = (e) => typeof e === 'number';
|
|
261
|
+
const isObject = (e) => Object.prototype.toString.call(e) === '[object Object]';
|
|
262
|
+
const isElement = (e) => e instanceof Element;
|
|
263
|
+
const isFunction = (e) => typeof e === 'function';
|
|
264
|
+
const isArray = (e) => Array.isArray(e);
|
|
265
|
+
const isBoolean = (e) => typeof e === 'boolean';
|
|
266
|
+
|
|
267
|
+
class Checkbox {
|
|
268
|
+
el;
|
|
269
|
+
checked = false;
|
|
270
|
+
label = '';
|
|
271
|
+
disabled = false;
|
|
272
|
+
indeterminate = false;
|
|
273
|
+
constructor(config) {
|
|
274
|
+
config = this.generateConfig(config);
|
|
275
|
+
this.label = config.label;
|
|
276
|
+
this.render(config);
|
|
277
|
+
}
|
|
278
|
+
generateConfig(config) {
|
|
279
|
+
return {
|
|
280
|
+
checked: false,
|
|
281
|
+
disabled: false,
|
|
282
|
+
indeterminate: false,
|
|
283
|
+
...config
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
render(config) {
|
|
287
|
+
const el = document.createElement('span');
|
|
288
|
+
el.classList.add('hy-checkbox');
|
|
289
|
+
const content = document.createElement('span');
|
|
290
|
+
content.classList.add('hy-checkbox__input');
|
|
291
|
+
const inner = document.createElement('span');
|
|
292
|
+
inner.classList.add('hy-checkbox__inner');
|
|
293
|
+
content.appendChild(inner);
|
|
294
|
+
if (config.label) {
|
|
295
|
+
const label = document.createElement('span');
|
|
296
|
+
label.classList.add('hy-checkbox__label');
|
|
297
|
+
content.appendChild(label);
|
|
298
|
+
}
|
|
299
|
+
el.appendChild(content);
|
|
300
|
+
el.addEventListener('click', (e) => {
|
|
301
|
+
config.onClick && config.onClick(e);
|
|
302
|
+
e.stopPropagation();
|
|
303
|
+
});
|
|
304
|
+
this.el = el;
|
|
305
|
+
this.setStatus(config);
|
|
306
|
+
}
|
|
307
|
+
setStatus(config) {
|
|
308
|
+
if (!this.el)
|
|
309
|
+
return;
|
|
310
|
+
const classList = this.el.classList;
|
|
311
|
+
if (isBoolean(config.disabled)) {
|
|
312
|
+
classList[config.disabled ? 'add' : 'remove']('is-disabled');
|
|
313
|
+
this.disabled = config.disabled;
|
|
314
|
+
if (config.disabled) {
|
|
315
|
+
classList.remove('is-checked');
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (isBoolean(config.indeterminate)) {
|
|
320
|
+
classList[config.indeterminate ? 'add' : 'remove']('is-indeterminate');
|
|
321
|
+
this.indeterminate = config.indeterminate;
|
|
322
|
+
if (config.indeterminate) {
|
|
323
|
+
classList.remove('is-checked');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (isBoolean(config.checked)) {
|
|
328
|
+
classList[config.checked ? 'add' : 'remove']('is-checked');
|
|
329
|
+
this.checked = config.checked;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
mount(el) {
|
|
333
|
+
if (!this.el)
|
|
334
|
+
return;
|
|
335
|
+
el.appendChild(this.el);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
class Radio {
|
|
340
|
+
el;
|
|
341
|
+
checked = false;
|
|
342
|
+
label = '';
|
|
343
|
+
disabled = false;
|
|
344
|
+
constructor(config) {
|
|
345
|
+
config = this.generateConfig(config);
|
|
346
|
+
this.label = config.label;
|
|
347
|
+
this.render(config);
|
|
348
|
+
}
|
|
349
|
+
generateConfig(config) {
|
|
350
|
+
return {
|
|
351
|
+
checked: false,
|
|
352
|
+
disabled: false,
|
|
353
|
+
...config
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
render(config) {
|
|
357
|
+
const el = document.createElement('span');
|
|
358
|
+
el.classList.add('hy-radio');
|
|
359
|
+
const content = document.createElement('span');
|
|
360
|
+
content.classList.add('hy-radio__input');
|
|
361
|
+
const inner = document.createElement('span');
|
|
362
|
+
inner.classList.add('hy-radio__inner');
|
|
363
|
+
content.appendChild(inner);
|
|
364
|
+
if (config.label) {
|
|
365
|
+
const label = document.createElement('span');
|
|
366
|
+
label.classList.add('hy-radio__label');
|
|
367
|
+
content.appendChild(label);
|
|
368
|
+
}
|
|
369
|
+
el.appendChild(content);
|
|
370
|
+
el.addEventListener('click', (e) => {
|
|
371
|
+
config.onClick && config.onClick(e);
|
|
372
|
+
e.stopPropagation();
|
|
373
|
+
});
|
|
374
|
+
this.el = el;
|
|
375
|
+
this.setStatus(config);
|
|
376
|
+
}
|
|
377
|
+
setStatus(config) {
|
|
378
|
+
if (!this.el)
|
|
379
|
+
return;
|
|
380
|
+
const classList = this.el.classList;
|
|
381
|
+
if (isBoolean(config.disabled)) {
|
|
382
|
+
classList[config.disabled ? 'add' : 'remove']('is-disabled');
|
|
383
|
+
this.disabled = config.disabled;
|
|
384
|
+
}
|
|
385
|
+
if (isBoolean(config.checked)) {
|
|
386
|
+
classList[config.checked ? 'add' : 'remove']('is-checked');
|
|
387
|
+
this.checked = config.checked;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
mount(el) {
|
|
391
|
+
if (!this.el)
|
|
392
|
+
return;
|
|
393
|
+
el.appendChild(this.el);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function useCheck(props, tree) {
|
|
398
|
+
const checkedKeys = new Set();
|
|
399
|
+
let indeterminateKeys = new Set();
|
|
400
|
+
const updateCheckedKeys = () => {
|
|
401
|
+
const { showSelect, checkStrictly } = props.rowSelection;
|
|
402
|
+
if (!tree || !showSelect || checkStrictly) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const { levelTreeNodeMap, maxLevel } = tree;
|
|
406
|
+
const checkedKeySet = checkedKeys;
|
|
407
|
+
const indeterminateKeySet = new Set();
|
|
408
|
+
for (let level = maxLevel - 1; level >= 1; --level) {
|
|
409
|
+
const nodes = levelTreeNodeMap.get(level);
|
|
410
|
+
if (!nodes)
|
|
411
|
+
continue;
|
|
412
|
+
nodes.forEach((node) => {
|
|
413
|
+
const children = node.children;
|
|
414
|
+
if (children) {
|
|
415
|
+
let allChecked = true;
|
|
416
|
+
let hasChecked = false;
|
|
417
|
+
for (const childNode of children) {
|
|
418
|
+
const key = childNode.key;
|
|
419
|
+
if (checkedKeySet.has(key)) {
|
|
420
|
+
hasChecked = true;
|
|
421
|
+
}
|
|
422
|
+
else if (indeterminateKeySet.has(key)) {
|
|
423
|
+
allChecked = false;
|
|
424
|
+
hasChecked = true;
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
allChecked = false;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (allChecked) {
|
|
432
|
+
checkedKeySet.add(node.key);
|
|
433
|
+
}
|
|
434
|
+
else if (hasChecked) {
|
|
435
|
+
indeterminateKeySet.add(node.key);
|
|
436
|
+
checkedKeySet.delete(node.key);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
checkedKeySet.delete(node.key);
|
|
440
|
+
indeterminateKeySet.delete(node.key);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
indeterminateKeys = indeterminateKeySet;
|
|
446
|
+
};
|
|
447
|
+
const isChecked = (node) => checkedKeys.has(node.key);
|
|
448
|
+
const isIndeterminate = (node) => indeterminateKeys.has(node.key);
|
|
449
|
+
const toggleCheckbox = (node, isChecked, nodeClick = true, immediateUpdate = true) => {
|
|
450
|
+
const { type } = props.rowSelection;
|
|
451
|
+
const checkedKeySet = checkedKeys;
|
|
452
|
+
// 单选
|
|
453
|
+
if (type === 'radio') {
|
|
454
|
+
if (checkedKeySet.has(node.key))
|
|
455
|
+
return;
|
|
456
|
+
checkedKeySet.clear();
|
|
457
|
+
checkedKeySet.add(node.key);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
// 多选
|
|
461
|
+
const toggle = (node, checked) => {
|
|
462
|
+
checkedKeySet[checked ? 'add' : 'delete'](node.key);
|
|
463
|
+
const children = node.children;
|
|
464
|
+
if (!props.rowSelection.checkStrictly && children) {
|
|
465
|
+
children.forEach((childNode) => {
|
|
466
|
+
if (!childNode.disabled) {
|
|
467
|
+
toggle(childNode, checked);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
toggle(node, isChecked);
|
|
473
|
+
if (immediateUpdate) {
|
|
474
|
+
updateCheckedKeys();
|
|
475
|
+
}
|
|
476
|
+
if (nodeClick) {
|
|
477
|
+
afterNodeCheck(node, isChecked);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
const afterNodeCheck = (node, checked) => {
|
|
481
|
+
const { checkedNodes, checkedKeys } = getChecked();
|
|
482
|
+
const { halfCheckedNodes, halfCheckedKeys } = getHalfChecked();
|
|
483
|
+
const { onCheckChange } = props.rowSelection;
|
|
484
|
+
// emit(NODE_CHECK, node.data, {
|
|
485
|
+
// checkedKeys,
|
|
486
|
+
// checkedNodes,
|
|
487
|
+
// halfCheckedKeys,
|
|
488
|
+
// halfCheckedNodes,
|
|
489
|
+
// })
|
|
490
|
+
// emit(NODE_CHECK_CHANGE, node.data, checked)
|
|
491
|
+
onCheckChange &&
|
|
492
|
+
onCheckChange(node.data, {
|
|
493
|
+
checkedKeys,
|
|
494
|
+
checkedNodes,
|
|
495
|
+
halfCheckedKeys,
|
|
496
|
+
halfCheckedNodes
|
|
497
|
+
}, checked);
|
|
498
|
+
};
|
|
499
|
+
// expose
|
|
500
|
+
function getCheckedKeys(leafOnly = false) {
|
|
501
|
+
return getChecked(leafOnly).checkedKeys;
|
|
502
|
+
}
|
|
503
|
+
function getCheckedNodes(leafOnly = false) {
|
|
504
|
+
return getChecked(leafOnly).checkedNodes;
|
|
505
|
+
}
|
|
506
|
+
function getHalfCheckedKeys() {
|
|
507
|
+
return getHalfChecked().halfCheckedKeys;
|
|
508
|
+
}
|
|
509
|
+
function getHalfCheckedNodes() {
|
|
510
|
+
return getHalfChecked().halfCheckedNodes;
|
|
511
|
+
}
|
|
512
|
+
function getChecked(leafOnly = false) {
|
|
513
|
+
const checkedNodes = [];
|
|
514
|
+
const keys = [];
|
|
515
|
+
if (tree && props.rowSelection.showSelect) {
|
|
516
|
+
const { treeNodeMap } = tree;
|
|
517
|
+
checkedKeys.forEach((key) => {
|
|
518
|
+
const node = treeNodeMap.get(key);
|
|
519
|
+
if (node && (!leafOnly || (leafOnly && node.isLeaf))) {
|
|
520
|
+
keys.push(key);
|
|
521
|
+
checkedNodes.push(node.data);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
checkedKeys: keys,
|
|
527
|
+
checkedNodes
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
function getHalfChecked() {
|
|
531
|
+
const halfCheckedNodes = [];
|
|
532
|
+
const halfCheckedKeys = [];
|
|
533
|
+
if (tree && props.rowSelection.showSelect) {
|
|
534
|
+
const { treeNodeMap } = tree;
|
|
535
|
+
indeterminateKeys.forEach((key) => {
|
|
536
|
+
const node = treeNodeMap.get(key);
|
|
537
|
+
if (node) {
|
|
538
|
+
halfCheckedKeys.push(key);
|
|
539
|
+
halfCheckedNodes.push(node.data);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
halfCheckedNodes,
|
|
545
|
+
halfCheckedKeys
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function setCheckedKeys(keys) {
|
|
549
|
+
const { type } = props.rowSelection;
|
|
550
|
+
checkedKeys.clear();
|
|
551
|
+
indeterminateKeys.clear();
|
|
552
|
+
if (type === 'radio' && keys.length > 1) {
|
|
553
|
+
keys = [keys[0]];
|
|
554
|
+
}
|
|
555
|
+
_setCheckedKeys(keys);
|
|
556
|
+
}
|
|
557
|
+
function setChecked(key, isChecked) {
|
|
558
|
+
if (tree && props.rowSelection.showSelect) {
|
|
559
|
+
const node = tree.treeNodeMap.get(key);
|
|
560
|
+
if (node) {
|
|
561
|
+
toggleCheckbox(node, isChecked, false);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function _setCheckedKeys(keys) {
|
|
566
|
+
if (tree) {
|
|
567
|
+
const { treeNodeMap } = tree;
|
|
568
|
+
if (props.rowSelection.showSelect && treeNodeMap && keys?.length > 0) {
|
|
569
|
+
for (const key of keys) {
|
|
570
|
+
const node = treeNodeMap.get(key);
|
|
571
|
+
if (node && !isChecked(node)) {
|
|
572
|
+
toggleCheckbox(node, true, false, false);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
updateCheckedKeys();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
checkedKeys,
|
|
581
|
+
updateCheckedKeys,
|
|
582
|
+
toggleCheckbox,
|
|
583
|
+
isChecked,
|
|
584
|
+
isIndeterminate,
|
|
585
|
+
// expose
|
|
586
|
+
getCheckedKeys,
|
|
587
|
+
getCheckedNodes,
|
|
588
|
+
getHalfCheckedKeys,
|
|
589
|
+
getHalfCheckedNodes,
|
|
590
|
+
setChecked,
|
|
591
|
+
setCheckedKeys
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function useFilter(filterMethod, tree) {
|
|
596
|
+
const hiddenNodeKeySet = new Set([]);
|
|
597
|
+
const hiddenExpandIconKeySet = new Set([]);
|
|
598
|
+
function doFilter(params) {
|
|
599
|
+
if (!isFunction(filterMethod)) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const expandKeySet = new Set();
|
|
603
|
+
const hiddenExpandIconKeys = hiddenExpandIconKeySet;
|
|
604
|
+
const hiddenKeys = hiddenNodeKeySet;
|
|
605
|
+
const family = [];
|
|
606
|
+
const nodes = tree?.treeNodes || [];
|
|
607
|
+
const filter = filterMethod;
|
|
608
|
+
hiddenKeys.clear();
|
|
609
|
+
function traverse(nodes) {
|
|
610
|
+
nodes.forEach((node) => {
|
|
611
|
+
family.push(node);
|
|
612
|
+
if (filter?.(params, node.data, node)) {
|
|
613
|
+
family.forEach((member) => {
|
|
614
|
+
expandKeySet.add(member.key);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
else if (node.isLeaf) {
|
|
618
|
+
hiddenKeys.add(node.key);
|
|
619
|
+
}
|
|
620
|
+
const children = node.children;
|
|
621
|
+
if (children) {
|
|
622
|
+
traverse(children);
|
|
623
|
+
}
|
|
624
|
+
if (!node.isLeaf) {
|
|
625
|
+
if (!expandKeySet.has(node.key)) {
|
|
626
|
+
hiddenKeys.add(node.key);
|
|
627
|
+
}
|
|
628
|
+
else if (children) {
|
|
629
|
+
// If all child nodes are hidden, then the expand icon will be hidden
|
|
630
|
+
let allHidden = true;
|
|
631
|
+
for (const childNode of children) {
|
|
632
|
+
if (!hiddenKeys.has(childNode.key)) {
|
|
633
|
+
allHidden = false;
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (allHidden) {
|
|
638
|
+
hiddenExpandIconKeys.add(node.key);
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
hiddenExpandIconKeys.delete(node.key);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
family.pop();
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
traverse(nodes);
|
|
649
|
+
return expandKeySet;
|
|
650
|
+
}
|
|
651
|
+
function isForceHiddenExpandIcon(node) {
|
|
652
|
+
return hiddenExpandIconKeySet.has(node.key);
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
hiddenExpandIconKeySet,
|
|
656
|
+
hiddenNodeKeySet,
|
|
657
|
+
doFilter,
|
|
658
|
+
isForceHiddenExpandIcon
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function isShowCount(show, node) {
|
|
663
|
+
if (isBoolean(show))
|
|
664
|
+
return show;
|
|
665
|
+
if (isFunction(show))
|
|
666
|
+
return show(node.data, node);
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
class VirtualTree {
|
|
670
|
+
_virtualScroll;
|
|
671
|
+
_el = null;
|
|
672
|
+
_props = {
|
|
673
|
+
value: 'value',
|
|
674
|
+
label: 'string',
|
|
675
|
+
children: 'children',
|
|
676
|
+
disabled: 'disabled',
|
|
677
|
+
class: '',
|
|
678
|
+
showCount: false,
|
|
679
|
+
total: 'total',
|
|
680
|
+
count: 'count'
|
|
681
|
+
};
|
|
682
|
+
_tree;
|
|
683
|
+
_expandedKeySet = new Set(); // 展开的key
|
|
684
|
+
_hiddenNodeKeySet = new Set(); // 隐藏的key
|
|
685
|
+
_flattenTree = []; // 平铺树列表
|
|
686
|
+
_filterMethod;
|
|
687
|
+
_isForceHiddenExpandIcon;
|
|
688
|
+
_onNodeExpand;
|
|
689
|
+
_onNodeCollapse;
|
|
690
|
+
getCheckedKeys;
|
|
691
|
+
getCheckedNodes;
|
|
692
|
+
setChecked;
|
|
693
|
+
setCheckedKeys;
|
|
694
|
+
setRowSelection;
|
|
695
|
+
constructor(config) {
|
|
696
|
+
config = this.generateConfig(config);
|
|
697
|
+
this._el = this.getContainer(config.container);
|
|
698
|
+
if (!this._el) {
|
|
699
|
+
throw Error('【container参数错误】:请传入id或者class或者元素节点');
|
|
700
|
+
}
|
|
701
|
+
this._el.classList.add('hy-tree');
|
|
702
|
+
this._filterMethod = config.filterMethod;
|
|
703
|
+
this._onNodeExpand = config.onNodeExpand;
|
|
704
|
+
this._onNodeCollapse = config.onNodeCollapse;
|
|
705
|
+
this.render(config);
|
|
706
|
+
}
|
|
707
|
+
/** 生成config */
|
|
708
|
+
generateConfig(config) {
|
|
709
|
+
const props = Object.assign({
|
|
710
|
+
value: 'value',
|
|
711
|
+
label: 'string',
|
|
712
|
+
children: 'children',
|
|
713
|
+
disabled: 'disabled',
|
|
714
|
+
class: '',
|
|
715
|
+
showCount: false,
|
|
716
|
+
total: 'total',
|
|
717
|
+
count: 'count'
|
|
718
|
+
}, config.props);
|
|
719
|
+
const rowSelection = Object.assign({
|
|
720
|
+
type: 'checkbox',
|
|
721
|
+
checkStrictly: false,
|
|
722
|
+
showSelect: false
|
|
723
|
+
}, config.rowSelection);
|
|
724
|
+
config = {
|
|
725
|
+
itemHeight: 30,
|
|
726
|
+
bufferSize: 10,
|
|
727
|
+
expandOnClickNode: true,
|
|
728
|
+
checkOnClickNode: false,
|
|
729
|
+
indent: 16,
|
|
730
|
+
...config,
|
|
731
|
+
props,
|
|
732
|
+
rowSelection
|
|
733
|
+
};
|
|
734
|
+
this._props = props;
|
|
735
|
+
return config;
|
|
736
|
+
}
|
|
737
|
+
/** 获取容器 */
|
|
738
|
+
getContainer(container) {
|
|
739
|
+
if (isString(container)) {
|
|
740
|
+
return document.querySelector(container);
|
|
741
|
+
}
|
|
742
|
+
if (isElement(container)) {
|
|
743
|
+
return container;
|
|
744
|
+
}
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
/** 创建树数据 */
|
|
748
|
+
createTree(data) {
|
|
749
|
+
const treeNodeMap = new Map();
|
|
750
|
+
const levelTreeNodeMap = new Map();
|
|
751
|
+
let maxLevel = 1;
|
|
752
|
+
const { countFilter, totalFilter } = this._props;
|
|
753
|
+
const isCountFiler = countFilter && isFunction(countFilter);
|
|
754
|
+
const isTotalFiler = totalFilter && isFunction(totalFilter);
|
|
755
|
+
const traverse = (nodes, level = 1, parent = undefined) => {
|
|
756
|
+
const siblings = [];
|
|
757
|
+
let count = 0;
|
|
758
|
+
let total = 0;
|
|
759
|
+
for (const rawNode of nodes) {
|
|
760
|
+
const value = this.getKey(rawNode);
|
|
761
|
+
const node = {
|
|
762
|
+
level,
|
|
763
|
+
key: value,
|
|
764
|
+
data: rawNode
|
|
765
|
+
};
|
|
766
|
+
node.label = this.getLabel(rawNode);
|
|
767
|
+
node.parent = parent;
|
|
768
|
+
const children = this.getChildren(rawNode);
|
|
769
|
+
node.disabled = this.getDisabled(rawNode);
|
|
770
|
+
node.isLeaf = !children || children.length === 0;
|
|
771
|
+
node.expanded = this._expandedKeySet.has(value);
|
|
772
|
+
if (children && children.length) {
|
|
773
|
+
const { list, count: childCount, total: childTotal } = traverse(children, level + 1, node);
|
|
774
|
+
node.children = list;
|
|
775
|
+
node.count = isCountFiler ? childCount : this.getCount(rawNode);
|
|
776
|
+
node.total = isTotalFiler ? childTotal : this.getTotal(rawNode);
|
|
777
|
+
count += childCount;
|
|
778
|
+
total += childTotal;
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
node.count = 0;
|
|
782
|
+
node.total = 0;
|
|
783
|
+
// 统计
|
|
784
|
+
if (isCountFiler) {
|
|
785
|
+
count += countFilter(rawNode) ? 1 : 0;
|
|
786
|
+
}
|
|
787
|
+
if (isTotalFiler) {
|
|
788
|
+
total += totalFilter(rawNode) ? 1 : 0;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
siblings.push(node);
|
|
792
|
+
treeNodeMap.set(value, node);
|
|
793
|
+
if (!levelTreeNodeMap.has(level)) {
|
|
794
|
+
levelTreeNodeMap.set(level, []);
|
|
795
|
+
}
|
|
796
|
+
levelTreeNodeMap.get(level)?.push(node);
|
|
797
|
+
}
|
|
798
|
+
if (level > maxLevel) {
|
|
799
|
+
maxLevel = level;
|
|
800
|
+
}
|
|
801
|
+
return { list: siblings, count, total };
|
|
802
|
+
};
|
|
803
|
+
const { list } = traverse(data || []);
|
|
804
|
+
const treeNodes = list;
|
|
805
|
+
return {
|
|
806
|
+
treeNodeMap,
|
|
807
|
+
levelTreeNodeMap,
|
|
808
|
+
maxLevel,
|
|
809
|
+
treeNodes
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
/** 生成平铺树列表 */
|
|
813
|
+
genereateFlattenTree() {
|
|
814
|
+
const expandedKeys = this._expandedKeySet;
|
|
815
|
+
const hiddenKeys = this._hiddenNodeKeySet;
|
|
816
|
+
const flattenNodes = [];
|
|
817
|
+
const nodes = this._tree?.treeNodes || [];
|
|
818
|
+
const stack = [];
|
|
819
|
+
for (let i = nodes.length - 1; i >= 0; --i) {
|
|
820
|
+
stack.push(nodes[i]);
|
|
821
|
+
}
|
|
822
|
+
while (stack.length) {
|
|
823
|
+
const node = stack.pop();
|
|
824
|
+
if (hiddenKeys.has(node.key))
|
|
825
|
+
continue;
|
|
826
|
+
flattenNodes.push(node);
|
|
827
|
+
if (node.children && expandedKeys.has(node.key)) {
|
|
828
|
+
for (let i = node.children.length - 1; i >= 0; --i) {
|
|
829
|
+
stack.push(node.children[i]);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return flattenNodes;
|
|
834
|
+
}
|
|
835
|
+
/** 获取key值 */
|
|
836
|
+
getKey(node) {
|
|
837
|
+
if (!node)
|
|
838
|
+
return '';
|
|
839
|
+
return node[this._props.value];
|
|
840
|
+
}
|
|
841
|
+
/** 获取label值 */
|
|
842
|
+
getLabel(node) {
|
|
843
|
+
if (!node)
|
|
844
|
+
return '';
|
|
845
|
+
return node[this._props.label];
|
|
846
|
+
}
|
|
847
|
+
/** 获取children值 */
|
|
848
|
+
getChildren(node) {
|
|
849
|
+
if (!node)
|
|
850
|
+
return '';
|
|
851
|
+
return node[this._props.children];
|
|
852
|
+
}
|
|
853
|
+
/** 获取disabled值 */
|
|
854
|
+
getDisabled(node) {
|
|
855
|
+
if (!node)
|
|
856
|
+
return '';
|
|
857
|
+
return node[this._props.disabled];
|
|
858
|
+
}
|
|
859
|
+
/** 获取count值 */
|
|
860
|
+
getCount(node) {
|
|
861
|
+
if (!node)
|
|
862
|
+
return;
|
|
863
|
+
return node[this._props.count];
|
|
864
|
+
}
|
|
865
|
+
/** 获取total值 */
|
|
866
|
+
getTotal(node) {
|
|
867
|
+
if (!node)
|
|
868
|
+
return;
|
|
869
|
+
return node[this._props.total];
|
|
870
|
+
}
|
|
871
|
+
/** 重排虚拟滚动列表,并刷新页面 */
|
|
872
|
+
refreshVirtualScroll() {
|
|
873
|
+
this._flattenTree = this.genereateFlattenTree();
|
|
874
|
+
this.refreshRender();
|
|
875
|
+
}
|
|
876
|
+
/** 重新渲染,isReal是否实时渲染 */
|
|
877
|
+
refreshRender = (() => {
|
|
878
|
+
let timer;
|
|
879
|
+
let timeout = 0;
|
|
880
|
+
return (isReal = true) => {
|
|
881
|
+
timer && clearTimeout(timer);
|
|
882
|
+
if (isReal || timeout >= 600) {
|
|
883
|
+
this.refresh();
|
|
884
|
+
timeout = 0;
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
timeout += 200;
|
|
888
|
+
timer = setTimeout(() => {
|
|
889
|
+
this.refresh();
|
|
890
|
+
}, 200);
|
|
891
|
+
};
|
|
892
|
+
})();
|
|
893
|
+
/** 节点展开/收起处理 */
|
|
894
|
+
expandedHandle(node) {
|
|
895
|
+
const key = node.key;
|
|
896
|
+
if (this._expandedKeySet.has(key)) {
|
|
897
|
+
this._expandedKeySet.delete(key);
|
|
898
|
+
node.expanded = false;
|
|
899
|
+
this._onNodeCollapse && this._onNodeCollapse(node.data, node);
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
this._expandedKeySet.add(key);
|
|
903
|
+
node.expanded = true;
|
|
904
|
+
this._onNodeExpand && this._onNodeExpand(node.data, node);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/** 渲染 */
|
|
908
|
+
render(config) {
|
|
909
|
+
if (this._virtualScroll)
|
|
910
|
+
return;
|
|
911
|
+
this._tree = this.createTree(config.data);
|
|
912
|
+
this._flattenTree = this.genereateFlattenTree();
|
|
913
|
+
const { checkedKeys, isIndeterminate, isChecked, toggleCheckbox, getCheckedKeys, getCheckedNodes,
|
|
914
|
+
// getHalfCheckedKeys,
|
|
915
|
+
// getHalfCheckedNodes,
|
|
916
|
+
setChecked, setCheckedKeys } = useCheck(config, this._tree);
|
|
917
|
+
this.getCheckedKeys = getCheckedKeys;
|
|
918
|
+
this.getCheckedNodes = getCheckedNodes;
|
|
919
|
+
this.setChecked = setChecked;
|
|
920
|
+
this.setCheckedKeys = (keys) => {
|
|
921
|
+
setCheckedKeys(keys);
|
|
922
|
+
this.refreshRender();
|
|
923
|
+
};
|
|
924
|
+
this.setRowSelection = (rowSelection) => {
|
|
925
|
+
if (!rowSelection)
|
|
926
|
+
return;
|
|
927
|
+
Object.assign(config.rowSelection, rowSelection);
|
|
928
|
+
if (checkedKeys.size) {
|
|
929
|
+
setCheckedKeys([]);
|
|
930
|
+
}
|
|
931
|
+
this.refreshRender();
|
|
932
|
+
};
|
|
933
|
+
const useRenderItems = (config) => {
|
|
934
|
+
const { renderIcon, renderItem, renderStatus, onNodeClick, onNodeContextmenu } = config;
|
|
935
|
+
/** 生成图标 */
|
|
936
|
+
const generateExpandIcon = (item) => {
|
|
937
|
+
const el = document.createElement('div');
|
|
938
|
+
el.classList.add('hy-expand');
|
|
939
|
+
if (this._expandedKeySet.has(item.key)) {
|
|
940
|
+
el.classList.add('expanded');
|
|
941
|
+
}
|
|
942
|
+
const icon = document.createElement('div');
|
|
943
|
+
icon.classList.add('hy-expand-icon');
|
|
944
|
+
el.appendChild(icon);
|
|
945
|
+
if (item.isLeaf) {
|
|
946
|
+
el.style.setProperty('visibility', 'hidden');
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
el.addEventListener('click', (e) => {
|
|
950
|
+
this.expandedHandle(item);
|
|
951
|
+
this.refreshVirtualScroll();
|
|
952
|
+
e.stopPropagation();
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
return el;
|
|
956
|
+
};
|
|
957
|
+
/** 生成内容 */
|
|
958
|
+
const generateContent = (item) => {
|
|
959
|
+
if (renderItem)
|
|
960
|
+
return renderItem(item.data, item);
|
|
961
|
+
const el = document.createElement('div');
|
|
962
|
+
el.classList.add('hy-tree-content');
|
|
963
|
+
if (item.label) {
|
|
964
|
+
el.innerHTML = item.label;
|
|
965
|
+
}
|
|
966
|
+
// 生成统计
|
|
967
|
+
if (isShowCount(this._props.showCount, item)) {
|
|
968
|
+
const count = document.createElement('span');
|
|
969
|
+
count.classList.add('hy-tree-statistics');
|
|
970
|
+
count.innerHTML = `(<span>${item.count || 0}</span>/${item.total || 0})`;
|
|
971
|
+
el.appendChild(count);
|
|
972
|
+
}
|
|
973
|
+
return el;
|
|
974
|
+
};
|
|
975
|
+
/** 设置节点class */
|
|
976
|
+
const setNodeClass = (el, item) => {
|
|
977
|
+
let className = ['hy-tree-node'];
|
|
978
|
+
if (isString(this._props.class)) {
|
|
979
|
+
className.push(...this._props.class.split(' '));
|
|
980
|
+
}
|
|
981
|
+
else if (isFunction(this._props.class)) {
|
|
982
|
+
const value = this._props.class(item.data, item);
|
|
983
|
+
if (isString(value)) {
|
|
984
|
+
className.push(...value.split(' '));
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
className = className.filter(Boolean);
|
|
988
|
+
el.classList.add(...className);
|
|
989
|
+
};
|
|
990
|
+
/** 渲染项 */
|
|
991
|
+
const generateItem = (item) => {
|
|
992
|
+
// console.log('item', item)
|
|
993
|
+
const { type, showSelect } = config.rowSelection;
|
|
994
|
+
const el = document.createElement('div');
|
|
995
|
+
setNodeClass(el, item);
|
|
996
|
+
const content = document.createElement('div');
|
|
997
|
+
content.classList.add('hy-tree-node__content');
|
|
998
|
+
content.style.setProperty('padding-left', `${(item.level - 1) * (config.indent || 0)}px`);
|
|
999
|
+
content.appendChild(generateExpandIcon(item));
|
|
1000
|
+
// 多选框/单选框
|
|
1001
|
+
if (isBoolean(showSelect)
|
|
1002
|
+
? showSelect
|
|
1003
|
+
: isFunction(showSelect) && showSelect(item.data, item)) {
|
|
1004
|
+
if (type === 'checkbox') {
|
|
1005
|
+
const checkbox = new Checkbox({
|
|
1006
|
+
checked: isChecked(item),
|
|
1007
|
+
disabled: item.disabled,
|
|
1008
|
+
indeterminate: isIndeterminate(item),
|
|
1009
|
+
onClick: () => {
|
|
1010
|
+
toggleCheckbox(item, !isChecked(item), true, true);
|
|
1011
|
+
this.refresh();
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
checkbox.mount(content);
|
|
1015
|
+
}
|
|
1016
|
+
else if (type === 'radio') {
|
|
1017
|
+
const radio = new Radio({
|
|
1018
|
+
checked: isChecked(item),
|
|
1019
|
+
disabled: item.disabled,
|
|
1020
|
+
onClick: () => {
|
|
1021
|
+
toggleCheckbox(item, true, true, false);
|
|
1022
|
+
this.refresh();
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
radio.mount(content);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
// 图标
|
|
1029
|
+
if (renderIcon) {
|
|
1030
|
+
const icon = renderIcon(item.data, item);
|
|
1031
|
+
if (isElement(icon)) {
|
|
1032
|
+
const iconContainer = document.createElement('div');
|
|
1033
|
+
iconContainer.classList.add('hy-tree-icon');
|
|
1034
|
+
iconContainer.appendChild(icon);
|
|
1035
|
+
content.appendChild(iconContainer);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
// 内容
|
|
1039
|
+
content.appendChild(generateContent(item));
|
|
1040
|
+
// 状态
|
|
1041
|
+
if (renderStatus) {
|
|
1042
|
+
const status = renderStatus(item.data, item);
|
|
1043
|
+
isElement(status) && content.appendChild(status);
|
|
1044
|
+
}
|
|
1045
|
+
el.appendChild(content);
|
|
1046
|
+
// 鼠标左键点击事件
|
|
1047
|
+
el.addEventListener('click', (e) => {
|
|
1048
|
+
if (config.checkOnClickNode) {
|
|
1049
|
+
toggleCheckbox(item, !isChecked(item), true, true);
|
|
1050
|
+
if (!config.expandOnClickNode) {
|
|
1051
|
+
this.refresh();
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
if (config.expandOnClickNode) {
|
|
1055
|
+
this.expandedHandle(item);
|
|
1056
|
+
this.refreshVirtualScroll();
|
|
1057
|
+
}
|
|
1058
|
+
onNodeClick && onNodeClick(item.data, item, e);
|
|
1059
|
+
});
|
|
1060
|
+
// 鼠标右键点击事件
|
|
1061
|
+
el.addEventListener('contextmenu', (e) => {
|
|
1062
|
+
if (onNodeContextmenu) {
|
|
1063
|
+
onNodeContextmenu(item.data, item, e);
|
|
1064
|
+
e.preventDefault();
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
return el;
|
|
1068
|
+
};
|
|
1069
|
+
return { renderItem: generateItem };
|
|
1070
|
+
};
|
|
1071
|
+
const { renderItem } = useRenderItems(config);
|
|
1072
|
+
this._virtualScroll = new VirtualScroll({
|
|
1073
|
+
container: this._el,
|
|
1074
|
+
items: this._flattenTree,
|
|
1075
|
+
itemHeight: config.itemHeight || 30,
|
|
1076
|
+
bufferSize: config.bufferSize || 10,
|
|
1077
|
+
renderItem
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
/** 全量更新数据 */
|
|
1081
|
+
setData = (data) => {
|
|
1082
|
+
this._tree = this.createTree(data);
|
|
1083
|
+
this.refreshVirtualScroll();
|
|
1084
|
+
};
|
|
1085
|
+
/** 局部更新数据,仅可更新已有的数据,无法做到添加删除 */
|
|
1086
|
+
updateData = (() => {
|
|
1087
|
+
let cacheTimer;
|
|
1088
|
+
let cacheTime = 0;
|
|
1089
|
+
const cacheData = new Map();
|
|
1090
|
+
const updateParnetCount = (keySet) => {
|
|
1091
|
+
if (!this._tree)
|
|
1092
|
+
return;
|
|
1093
|
+
const { countFilter, totalFilter } = this._props;
|
|
1094
|
+
const isCountFiler = countFilter && isFunction(countFilter);
|
|
1095
|
+
const isTotalFiler = totalFilter && isFunction(totalFilter);
|
|
1096
|
+
const parentKeySet = new Set();
|
|
1097
|
+
for (const key of keySet) {
|
|
1098
|
+
const target = this._tree.treeNodeMap.get(key);
|
|
1099
|
+
if (!target)
|
|
1100
|
+
continue;
|
|
1101
|
+
let count = isCountFiler ? 0 : target.data.count;
|
|
1102
|
+
let total = isTotalFiler ? 0 : target.data.total;
|
|
1103
|
+
if (isCountFiler || isTotalFiler) {
|
|
1104
|
+
for (let i = 0; i < target.children.length; i++) {
|
|
1105
|
+
const item = target.children[i];
|
|
1106
|
+
count = isCountFiler
|
|
1107
|
+
? count + (item.children ? item.count : countFilter(item.data))
|
|
1108
|
+
: count;
|
|
1109
|
+
total = isTotalFiler
|
|
1110
|
+
? total + (item.children ? item.total : totalFilter(item.data))
|
|
1111
|
+
: total;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
target.count = count;
|
|
1115
|
+
target.total = total;
|
|
1116
|
+
target.parent && parentKeySet.add(target.parent.key);
|
|
1117
|
+
}
|
|
1118
|
+
parentKeySet.size && updateParnetCount(parentKeySet);
|
|
1119
|
+
};
|
|
1120
|
+
return (data) => {
|
|
1121
|
+
if (!this._tree)
|
|
1122
|
+
return;
|
|
1123
|
+
cacheTimer && clearTimeout(cacheTimer);
|
|
1124
|
+
data.forEach((item) => cacheData.set(this.getKey(item), item));
|
|
1125
|
+
// 150毫秒仅更新一次数据
|
|
1126
|
+
if (Date.now() - cacheTime >= 150) {
|
|
1127
|
+
cacheTime = Date.now();
|
|
1128
|
+
const { treeNodeMap } = this.createTree(Array.from(cacheData.values()));
|
|
1129
|
+
cacheData.clear();
|
|
1130
|
+
const parentKeySet = new Set();
|
|
1131
|
+
for (const [key, value] of treeNodeMap) {
|
|
1132
|
+
const target = this._tree.treeNodeMap.get(key);
|
|
1133
|
+
if (!target)
|
|
1134
|
+
continue;
|
|
1135
|
+
this._tree.treeNodeMap.set(key, Object.assign(target, {
|
|
1136
|
+
data: value.data,
|
|
1137
|
+
disabled: value.disabled,
|
|
1138
|
+
label: value.label
|
|
1139
|
+
}));
|
|
1140
|
+
target.parent && parentKeySet.add(target.parent.key);
|
|
1141
|
+
}
|
|
1142
|
+
updateParnetCount(parentKeySet);
|
|
1143
|
+
this.refreshRender(false);
|
|
1144
|
+
}
|
|
1145
|
+
// 若仅更新一次,150毫秒后也会强制更新数据
|
|
1146
|
+
else {
|
|
1147
|
+
cacheTimer = setTimeout(() => {
|
|
1148
|
+
this.updateData([]);
|
|
1149
|
+
}, 150);
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
})();
|
|
1153
|
+
/** 获取指定节点 */
|
|
1154
|
+
getNode = (data) => {
|
|
1155
|
+
const key = isObject(data)
|
|
1156
|
+
? this.getKey(data)
|
|
1157
|
+
: data;
|
|
1158
|
+
return this._tree?.treeNodeMap.get(key);
|
|
1159
|
+
};
|
|
1160
|
+
/** 滚动到指定位置 */
|
|
1161
|
+
scrollToIndex = (index) => {
|
|
1162
|
+
if (!isNumber(index))
|
|
1163
|
+
return;
|
|
1164
|
+
this._virtualScroll && this._virtualScroll.scrollToIndex(index);
|
|
1165
|
+
};
|
|
1166
|
+
/** 滚动到指定key的位置 */
|
|
1167
|
+
scrollToNode = (key) => {
|
|
1168
|
+
if (!key || !this._flattenTree?.length)
|
|
1169
|
+
return;
|
|
1170
|
+
const node = this._tree?.treeNodeMap.get(key);
|
|
1171
|
+
if (!node) {
|
|
1172
|
+
console.warn(`找不到key为【${key}】的节点`);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (this._hiddenNodeKeySet.has(key)) {
|
|
1176
|
+
console.warn(`key为【${key}】的节点已隐藏`);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
if (!this._expandedKeySet.has(key) && node.parent) {
|
|
1180
|
+
let parent = node.parent;
|
|
1181
|
+
while (parent && !this._expandedKeySet.has(parent.key)) {
|
|
1182
|
+
this._expandedKeySet.add(parent.key);
|
|
1183
|
+
parent = parent.parent;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
this.refreshVirtualScroll();
|
|
1187
|
+
setTimeout(() => {
|
|
1188
|
+
this.scrollToIndex(this._flattenTree.indexOf(node));
|
|
1189
|
+
});
|
|
1190
|
+
};
|
|
1191
|
+
/** 过滤方法 */
|
|
1192
|
+
filter = (params) => {
|
|
1193
|
+
const { doFilter, hiddenNodeKeySet, isForceHiddenExpandIcon } = useFilter(this._filterMethod, this._tree);
|
|
1194
|
+
const keys = doFilter(params);
|
|
1195
|
+
console.log('keys', this._filterMethod);
|
|
1196
|
+
if (keys) {
|
|
1197
|
+
this._hiddenNodeKeySet = hiddenNodeKeySet;
|
|
1198
|
+
this._isForceHiddenExpandIcon = isForceHiddenExpandIcon;
|
|
1199
|
+
this.refreshVirtualScroll();
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
/** 刷新视图 */
|
|
1203
|
+
refresh = () => {
|
|
1204
|
+
this._virtualScroll && this._virtualScroll.updateItems(this._flattenTree);
|
|
1205
|
+
};
|
|
1206
|
+
/** 销毁组件 */
|
|
1207
|
+
destroy = () => {
|
|
1208
|
+
this._virtualScroll && this._virtualScroll.destroy();
|
|
1209
|
+
this._virtualScroll = null;
|
|
1210
|
+
this._el = null;
|
|
1211
|
+
this._tree = undefined;
|
|
1212
|
+
this._expandedKeySet = new Set();
|
|
1213
|
+
this._hiddenNodeKeySet = new Set();
|
|
1214
|
+
this._flattenTree = [];
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
export { VirtualTree, isArray, isBoolean, isElement, isFunction, isNumber, isObject, isString };
|