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/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 };