@kupola/kupola 1.4.2 → 1.4.3

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.
Files changed (63) hide show
  1. package/README.md +3 -3
  2. package/css/kupola.css +3 -2
  3. package/css/table.css +74 -0
  4. package/dist/css/kupola.css +12 -1
  5. package/dist/css/kupola.min.css +1 -1
  6. package/dist/kupola.cjs.js +1800 -3609
  7. package/dist/kupola.cjs.js.map +1 -1
  8. package/dist/kupola.css +100 -3
  9. package/dist/kupola.esm.js +1791 -3610
  10. package/dist/kupola.esm.js.map +1 -1
  11. package/dist/kupola.min.css +1 -1
  12. package/dist/kupola.min.js +1 -1
  13. package/dist/kupola.min.js.map +1 -1
  14. package/dist/kupola.umd.js +1800 -3609
  15. package/dist/kupola.umd.js.map +1 -1
  16. package/dist/types/kupola.d.ts +41 -25
  17. package/js/calendar.js +3 -10
  18. package/js/carousel.js +3 -10
  19. package/js/collapse.js +3 -10
  20. package/js/color-picker.js +5 -16
  21. package/js/component.js +4 -18
  22. package/js/countdown.js +3 -10
  23. package/js/data-bind.js +56 -36
  24. package/js/datepicker.js +16 -24
  25. package/js/depends.js +27 -16
  26. package/js/dialog.js +1 -5
  27. package/js/drawer.js +3 -11
  28. package/js/dropdown.js +11 -18
  29. package/js/dynamic-tags.js +3 -10
  30. package/js/fileupload.js +10 -12
  31. package/js/form.js +26 -20
  32. package/js/global-events.js +1 -14
  33. package/js/heatmap.js +3 -12
  34. package/js/i18n.js +1 -14
  35. package/js/icons.js +2 -2
  36. package/js/image-preview.js +1 -7
  37. package/js/initializer.js +0 -43
  38. package/js/kupola-core.js +1 -13
  39. package/js/kupola-lifecycle.js +1 -9
  40. package/js/message.js +1 -6
  41. package/js/modal.js +22 -20
  42. package/js/notification.js +1 -6
  43. package/js/numberinput.js +3 -10
  44. package/js/pagination.js +0 -5
  45. package/js/registry.js +0 -4
  46. package/js/select.js +5 -16
  47. package/js/slide-captcha.js +8 -21
  48. package/js/slider.js +3 -10
  49. package/js/statcard.js +3 -11
  50. package/js/table.js +240 -262
  51. package/js/tag.js +3 -10
  52. package/js/theme.js +1 -12
  53. package/js/timepicker.js +6 -19
  54. package/js/tooltip.js +3 -10
  55. package/js/utils.js +281 -1439
  56. package/js/validation.js +2 -7
  57. package/js/virtual-list.js +13 -38
  58. package/js/web-components.js +0 -9
  59. package/package.json +10 -5
  60. package/scripts/build-css.cjs +3 -2
  61. package/types/kupola.d.ts +41 -25
  62. package/version.json +10 -0
  63. /package/css/{colors_and_type.css → colors-and-type.css} +0 -0
package/js/table.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { ref } from './data-bind.js';
9
+ import { kupolaInitializer } from './initializer.js';
9
10
 
10
11
  class KupolaTable {
11
12
  constructor(element, options = {}) {
@@ -70,6 +71,10 @@ class KupolaTable {
70
71
  // Virtual scroll
71
72
  this.virtualScroll = options.virtualScroll || null; // { rowHeight: 40, overscan: 5 }
72
73
  this._scrollContainer = null;
74
+ this._scrollHandler = null; // 保存引用以便 destroy 移除
75
+ this._resizeCleanups = []; // 列宽拖拽清理函数
76
+ this._filterDebounceTimer = null;
77
+ this._reactiveCleanups = []; // ref() subscribe 清理
73
78
 
74
79
  // Merge cells
75
80
  this.mergeCells = options.mergeCells || null; // function(data) => [{row, col, rowSpan, colSpan}]
@@ -109,11 +114,15 @@ class KupolaTable {
109
114
  setData(data) {
110
115
  if (data && typeof data === 'object' && 'value' in data) {
111
116
  this._data = Array.isArray(data.value) ? data.value : [];
112
- data._subscribers?.add((newVal) => {
113
- this._data = Array.isArray(newVal) ? newVal : [];
114
- this._total = this._data.length;
115
- this.render();
116
- });
117
+ if (data.subscribe) {
118
+ this._reactiveCleanups.push(
119
+ data.subscribe((newVal) => {
120
+ this._data = Array.isArray(newVal) ? newVal : [];
121
+ this._total = this._data.length;
122
+ this.render();
123
+ })
124
+ );
125
+ }
117
126
  } else if (Array.isArray(data)) {
118
127
  this._data = data;
119
128
  } else {
@@ -129,7 +138,11 @@ class KupolaTable {
129
138
  setLoading(loading) {
130
139
  if (loading && typeof loading === 'object' && 'value' in loading) {
131
140
  this._loading = loading.value;
132
- loading._subscribers?.add((val) => { this._loading = val; this.render(); });
141
+ if (loading.subscribe) {
142
+ this._reactiveCleanups.push(
143
+ loading.subscribe((val) => { this._loading = val; this.render(); })
144
+ );
145
+ }
133
146
  } else {
134
147
  this._loading = !!loading;
135
148
  }
@@ -305,67 +318,15 @@ class KupolaTable {
305
318
  const thead = document.createElement('thead');
306
319
  const tr = document.createElement('tr');
307
320
 
308
- // Selection column
309
- if (this.selection) {
310
- const th = document.createElement('th');
311
- th.className = 'kupola-table-col-selection';
312
- if (this.selection === 'checkbox') {
313
- const cb = document.createElement('input');
314
- cb.type = 'checkbox';
315
- const pageData = this.getProcessedData();
316
- const allKeys = pageData.map(r => r[this.rowKey]);
317
- cb.checked = allKeys.length > 0 && allKeys.every(k => this._selectedKeys.has(k));
318
- cb.addEventListener('change', () => cb.checked ? this.selectAll() : this.deselectAll());
319
- th.appendChild(cb);
320
- }
321
- tr.appendChild(th);
322
- }
323
-
324
- // Expand column
321
+ if (this.selection) this._renderSelectionHeader(tr);
325
322
  if (this.expandable) {
326
323
  const th = document.createElement('th');
327
324
  th.className = 'kupola-table-col-expand';
328
325
  tr.appendChild(th);
329
326
  }
330
327
 
331
- // Data columns
332
328
  this.columns.forEach(col => {
333
- const th = document.createElement('th');
334
- th.textContent = col.title || col.key;
335
- if (col.width) th.style.width = typeof col.width === 'number' ? col.width + 'px' : col.width;
336
- if (col.minWidth) th.style.minWidth = typeof col.minWidth === 'number' ? col.minWidth + 'px' : col.minWidth;
337
- if (col.align) th.style.textAlign = col.align;
338
- if (col.fixed) th.setAttribute('data-fixed', col.fixed);
339
-
340
- // Sortable
341
- if (col.sortable) {
342
- th.classList.add('kupola-table-sortable');
343
- const sortInfo = this._sorts.find(s => s.key === col.key);
344
- if (sortInfo) th.classList.add(`kupola-table-sort-${sortInfo.order}`);
345
- th.addEventListener('click', (e) => {
346
- if (this.resizable && e.target.classList.contains('kupola-table-resize-handle')) return;
347
- this._handleSort(col.key);
348
- });
349
- const indicator = document.createElement('span');
350
- indicator.className = 'kupola-table-sort-icon';
351
- if (sortInfo) {
352
- indicator.textContent = this.multiSort
353
- ? ` ${this._sorts.indexOf(sortInfo) + 1}${sortInfo.order === 'asc' ? '▲' : '▼'}`
354
- : (sortInfo.order === 'asc' ? ' ▲' : ' ▼');
355
- } else {
356
- indicator.textContent = ' ⇅';
357
- }
358
- th.appendChild(indicator);
359
- }
360
-
361
- // Resize handle
362
- if (this.resizable && col.key !== this.columns[this.columns.length - 1]?.key) {
363
- const handle = document.createElement('span');
364
- handle.className = 'kupola-table-resize-handle';
365
- handle.setAttribute('data-col-key', col.key);
366
- th.appendChild(handle);
367
- }
368
-
329
+ const th = this._renderColumnHeader(col);
369
330
  tr.appendChild(th);
370
331
  });
371
332
 
@@ -373,6 +334,62 @@ class KupolaTable {
373
334
  return thead;
374
335
  }
375
336
 
337
+ _renderSelectionHeader(tr) {
338
+ const th = document.createElement('th');
339
+ th.className = 'kupola-table-col-selection';
340
+ if (this.selection === 'checkbox') {
341
+ const cb = document.createElement('input');
342
+ cb.type = 'checkbox';
343
+ const pageData = this.getProcessedData();
344
+ const allKeys = pageData.map(r => r[this.rowKey]);
345
+ cb.checked = allKeys.length > 0 && allKeys.every(k => this._selectedKeys.has(k));
346
+ cb.addEventListener('change', () => cb.checked ? this.selectAll() : this.deselectAll());
347
+ th.appendChild(cb);
348
+ }
349
+ tr.appendChild(th);
350
+ }
351
+
352
+ _renderColumnHeader(col) {
353
+ const th = document.createElement('th');
354
+ th.textContent = col.title || col.key;
355
+ if (col.width) th.style.width = typeof col.width === 'number' ? col.width + 'px' : col.width;
356
+ if (col.minWidth) th.style.minWidth = typeof col.minWidth === 'number' ? col.minWidth + 'px' : col.minWidth;
357
+ if (col.align) th.style.textAlign = col.align;
358
+ if (col.fixed) th.setAttribute('data-fixed', col.fixed);
359
+
360
+ if (col.sortable) this._renderSortIndicator(th, col);
361
+
362
+ // Resize handle
363
+ if (this.resizable && col.key !== this.columns[this.columns.length - 1]?.key) {
364
+ const handle = document.createElement('span');
365
+ handle.className = 'kupola-table-resize-handle';
366
+ handle.setAttribute('data-col-key', col.key);
367
+ th.appendChild(handle);
368
+ }
369
+
370
+ return th;
371
+ }
372
+
373
+ _renderSortIndicator(th, col) {
374
+ th.classList.add('kupola-table-sortable');
375
+ const sortInfo = this._sorts.find(s => s.key === col.key);
376
+ if (sortInfo) th.classList.add(`kupola-table-sort-${sortInfo.order}`);
377
+ th.addEventListener('click', (e) => {
378
+ if (this.resizable && e.target.classList.contains('kupola-table-resize-handle')) return;
379
+ this._handleSort(col.key);
380
+ });
381
+ const indicator = document.createElement('span');
382
+ indicator.className = 'kupola-table-sort-icon';
383
+ if (sortInfo) {
384
+ indicator.textContent = this.multiSort
385
+ ? ` ${this._sorts.indexOf(sortInfo) + 1}${sortInfo.order === 'asc' ? '▲' : '▼'}`
386
+ : (sortInfo.order === 'asc' ? ' ▲' : ' ▼');
387
+ } else {
388
+ indicator.textContent = ' ⇅';
389
+ }
390
+ th.appendChild(indicator);
391
+ }
392
+
376
393
  _renderTbody(data) {
377
394
  const tbody = document.createElement('tbody');
378
395
 
@@ -424,122 +441,129 @@ class KupolaTable {
424
441
  tr.classList.add('kupola-table-draggable');
425
442
  }
426
443
 
427
- // Selection cell
428
- if (this.selection) {
429
- const td = document.createElement('td');
430
- td.className = 'kupola-table-col-selection';
431
- const input = document.createElement('input');
432
- input.type = this.selection;
433
- input.checked = isSelected;
434
- input.addEventListener('change', () => {
435
- if (this.selection === 'radio') {
436
- this._selectedKeys.clear();
437
- this._selectedKeys.add(key);
438
- } else {
439
- isSelected ? this._selectedKeys.delete(key) : this._selectedKeys.add(key);
440
- }
441
- this.selectedKeys.value = [...this._selectedKeys];
442
- if (this.onSelect) this.onSelect([...this._selectedKeys], this.getSelectedRows());
443
- this.render();
444
- });
445
- td.appendChild(input);
446
- tr.appendChild(td);
447
- }
448
-
449
- // Expand cell
450
- if (this.expandable) {
451
- const td = document.createElement('td');
452
- td.className = 'kupola-table-col-expand';
453
- const btn = document.createElement('button');
454
- btn.className = 'kupola-table-expand-btn';
455
- const isExpanded = this._expandedKeys.has(key);
456
- btn.textContent = isExpanded ? '▼' : '▶';
457
- btn.type = 'button';
458
- btn.addEventListener('click', () => this._toggleExpand(key));
459
- td.appendChild(btn);
460
- tr.appendChild(td);
461
- }
444
+ if (this.selection) this._renderSelectionCell(tr, key, isSelected);
445
+ if (this.expandable) this._renderExpandCell(tr, key);
462
446
 
463
447
  // Data cells
464
448
  this.columns.forEach((col, colIndex) => {
465
- // Skip merged cells
466
449
  if (skipCells.has(`${rowIndex}-${colIndex}`)) return;
450
+ const td = this._renderDataCell(row, rowIndex, key, col, colIndex, skipCells, mergeMap);
451
+ tr.appendChild(td);
452
+ });
467
453
 
468
- const td = document.createElement('td');
469
- if (col.align) td.style.textAlign = col.align;
470
- if (col.fixed) {
471
- td.setAttribute('data-fixed', col.fixed);
472
- td.classList.add(`kupola-table-fixed-${col.fixed}`);
473
- }
454
+ if (this.onRowClick) {
455
+ tr.style.cursor = 'pointer';
456
+ tr.addEventListener('click', (e) => {
457
+ if (e.target.closest('.kupola-table-expand-btn, .kupola-table-tree-toggle, input, button')) return;
458
+ this.onRowClick(row, rowIndex, e);
459
+ });
460
+ }
474
461
 
475
- // Merge cells
476
- const mergeInfo = mergeMap.get(`${rowIndex}-${colIndex}`);
477
- if (mergeInfo) {
478
- if (mergeInfo.rowSpan > 1) td.rowSpan = mergeInfo.rowSpan;
479
- if (mergeInfo.colSpan > 1) td.colSpan = mergeInfo.colSpan;
480
- for (let r = 0; r < (mergeInfo.rowSpan || 1); r++) {
481
- for (let c = 0; c < (mergeInfo.colSpan || 1); c++) {
482
- if (r === 0 && c === 0) continue;
483
- skipCells.add(`${rowIndex + r}-${colIndex + c}`);
484
- }
485
- }
462
+ return tr;
463
+ }
464
+
465
+ _renderSelectionCell(tr, key, isSelected) {
466
+ const td = document.createElement('td');
467
+ td.className = 'kupola-table-col-selection';
468
+ const input = document.createElement('input');
469
+ input.type = this.selection;
470
+ input.checked = isSelected;
471
+ input.addEventListener('change', () => {
472
+ if (this.selection === 'radio') {
473
+ this._selectedKeys.clear();
474
+ this._selectedKeys.add(key);
475
+ } else {
476
+ isSelected ? this._selectedKeys.delete(key) : this._selectedKeys.add(key);
486
477
  }
478
+ this.selectedKeys.value = [...this._selectedKeys];
479
+ if (this.onSelect) this.onSelect([...this._selectedKeys], this.getSelectedRows());
480
+ this.render();
481
+ });
482
+ td.appendChild(input);
483
+ tr.appendChild(td);
484
+ }
485
+
486
+ _renderExpandCell(tr, key) {
487
+ const td = document.createElement('td');
488
+ td.className = 'kupola-table-col-expand';
489
+ const btn = document.createElement('button');
490
+ btn.className = 'kupola-table-expand-btn';
491
+ btn.textContent = this._expandedKeys.has(key) ? '▼' : '▶';
492
+ btn.type = 'button';
493
+ btn.addEventListener('click', () => this._toggleExpand(key));
494
+ td.appendChild(btn);
495
+ tr.appendChild(td);
496
+ }
487
497
 
488
- // Tree indent
489
- if (this.tree && colIndex === 0 && row._level > 0) {
490
- const indent = document.createElement('span');
491
- indent.className = 'kupola-table-tree-indent';
492
- indent.style.paddingLeft = (row._level * 20) + 'px';
493
- td.appendChild(indent);
494
- if (row._hasChildren) {
495
- const toggle = document.createElement('button');
496
- toggle.className = 'kupola-table-tree-toggle';
497
- toggle.textContent = this._treeExpandedKeys.has(row[this.rowKey]) ? '▼' : '▶';
498
- toggle.type = 'button';
499
- toggle.addEventListener('click', (e) => {
500
- e.stopPropagation();
501
- this._toggleTreeExpand(row[this.rowKey]);
502
- });
503
- td.appendChild(toggle);
504
- } else {
505
- const spacer = document.createElement('span');
506
- spacer.className = 'kupola-table-tree-toggle-placeholder';
507
- td.appendChild(spacer);
498
+ _renderDataCell(row, rowIndex, key, col, colIndex, skipCells, mergeMap) {
499
+ const td = document.createElement('td');
500
+ if (col.align) td.style.textAlign = col.align;
501
+ if (col.fixed) {
502
+ td.setAttribute('data-fixed', col.fixed);
503
+ td.classList.add(`kupola-table-fixed-${col.fixed}`);
504
+ }
505
+
506
+ // Merge cells
507
+ const mergeInfo = mergeMap.get(`${rowIndex}-${colIndex}`);
508
+ if (mergeInfo) {
509
+ if (mergeInfo.rowSpan > 1) td.rowSpan = mergeInfo.rowSpan;
510
+ if (mergeInfo.colSpan > 1) td.colSpan = mergeInfo.colSpan;
511
+ for (let r = 0; r < (mergeInfo.rowSpan || 1); r++) {
512
+ for (let c = 0; c < (mergeInfo.colSpan || 1); c++) {
513
+ if (r === 0 && c === 0) continue;
514
+ skipCells.add(`${rowIndex + r}-${colIndex + c}`);
508
515
  }
509
516
  }
517
+ }
510
518
 
511
- // Editable cell
512
- const isEditing = this._editingCell &&
513
- this._editingCell.rowKey === key && this._editingCell.colKey === col.key;
519
+ // Tree indent (first column only)
520
+ if (this.tree && colIndex === 0 && row._level > 0) {
521
+ this._renderTreeIndent(td, row);
522
+ }
514
523
 
515
- if (isEditing) {
516
- td.appendChild(this._renderEditCell(col, row));
517
- } else if (col.render) {
518
- const content = col.render(row[col.key], row, rowIndex);
519
- if (typeof content === 'string') td.innerHTML = content;
520
- else if (content instanceof HTMLElement) td.appendChild(content);
521
- } else {
522
- td.textContent = row[col.key] ?? '';
523
- }
524
+ // Cell content: editing / custom render / plain text
525
+ const isEditing = this._editingCell &&
526
+ this._editingCell.rowKey === key && this._editingCell.colKey === col.key;
524
527
 
525
- // Double-click to edit
526
- if (this.editable && !isEditing && col.editable !== false) {
527
- td.classList.add('kupola-table-editable-cell');
528
- td.addEventListener('dblclick', () => this._startEdit(key, col.key, row[col.key]));
529
- }
528
+ if (isEditing) {
529
+ td.appendChild(this._renderEditCell(col, row));
530
+ } else if (col.render) {
531
+ const content = col.render(row[col.key], row, rowIndex);
532
+ if (typeof content === 'string') td.innerHTML = content;
533
+ else if (content instanceof HTMLElement) td.appendChild(content);
534
+ } else {
535
+ td.textContent = row[col.key] ?? '';
536
+ }
530
537
 
531
- tr.appendChild(td);
532
- });
538
+ // Double-click to edit
539
+ if (this.editable && !isEditing && col.editable !== false) {
540
+ td.classList.add('kupola-table-editable-cell');
541
+ td.addEventListener('dblclick', () => this._startEdit(key, col.key, row[col.key]));
542
+ }
533
543
 
534
- if (this.onRowClick) {
535
- tr.style.cursor = 'pointer';
536
- tr.addEventListener('click', (e) => {
537
- if (e.target.closest('.kupola-table-expand-btn, .kupola-table-tree-toggle, input, button')) return;
538
- this.onRowClick(row, rowIndex, e);
544
+ return td;
545
+ }
546
+
547
+ _renderTreeIndent(td, row) {
548
+ const indent = document.createElement('span');
549
+ indent.className = 'kupola-table-tree-indent';
550
+ indent.style.paddingLeft = (row._level * 20) + 'px';
551
+ td.appendChild(indent);
552
+ if (row._hasChildren) {
553
+ const toggle = document.createElement('button');
554
+ toggle.className = 'kupola-table-tree-toggle';
555
+ toggle.textContent = this._treeExpandedKeys.has(row[this.rowKey]) ? '▼' : '▶';
556
+ toggle.type = 'button';
557
+ toggle.addEventListener('click', (e) => {
558
+ e.stopPropagation();
559
+ this._toggleTreeExpand(row[this.rowKey]);
539
560
  });
561
+ td.appendChild(toggle);
562
+ } else {
563
+ const spacer = document.createElement('span');
564
+ spacer.className = 'kupola-table-tree-toggle-placeholder';
565
+ td.appendChild(spacer);
540
566
  }
541
-
542
- return tr;
543
567
  }
544
568
 
545
569
  _renderStatusRow(text, className) {
@@ -583,12 +607,16 @@ class KupolaTable {
583
607
  bottomSpacer.style.height = '0px';
584
608
  tbody.appendChild(bottomSpacer);
585
609
 
586
- // Attach scroll listener
610
+ // Attach scroll listener (移除旧的再绑新的,避免重复)
587
611
  const wrapper = this.element.querySelector('.kupola-table-container');
588
612
  if (wrapper) {
589
613
  wrapper.style.maxHeight = this.virtualScroll.maxHeight || '400px';
590
614
  wrapper.style.overflowY = 'auto';
591
- wrapper.addEventListener('scroll', () => this._updateVirtualScroll());
615
+ if (this._scrollHandler) {
616
+ wrapper.removeEventListener('scroll', this._scrollHandler);
617
+ }
618
+ this._scrollHandler = () => this._updateVirtualScroll();
619
+ wrapper.addEventListener('scroll', this._scrollHandler);
592
620
  }
593
621
 
594
622
  return tbody;
@@ -807,6 +835,8 @@ class KupolaTable {
807
835
  };
808
836
  document.addEventListener('mousemove', onMouseMove);
809
837
  document.addEventListener('mouseup', onMouseUp);
838
+ // 注册清理函数,destroy 时可强制移除
839
+ this._resizeCleanups.push(onMouseUp);
810
840
  });
811
841
  });
812
842
  }
@@ -829,21 +859,7 @@ class KupolaTable {
829
859
  row.classList.add('kupola-table-drag-over');
830
860
  });
831
861
  row.addEventListener('dragleave', () => row.classList.remove('kupola-table-drag-over'));
832
- row.addEventListener('drop', (e) => {
833
- e.preventDefault();
834
- row.classList.remove('kupola-table-drag-over');
835
- if (!this._dragState) return;
836
- const toKey = row.getAttribute('data-row-key');
837
- if (this._dragState.fromKey === toKey) return;
838
- const fromIdx = this._data.findIndex(r => String(r[this.rowKey]) === this._dragState.fromKey);
839
- const toIdx = this._data.findIndex(r => String(r[this.rowKey]) === toKey);
840
- if (fromIdx >= 0 && toIdx >= 0) {
841
- const [moved] = this._data.splice(fromIdx, 1);
842
- this._data.splice(toIdx, 0, moved);
843
- if (this.onRowDragEnd) this.onRowDragEnd(moved, fromIdx, toIdx, this._data);
844
- this.render();
845
- }
846
- });
862
+ row.addEventListener('drop', (e) => this._handleRowDrop(e, row));
847
863
  row.addEventListener('dragend', () => {
848
864
  row.classList.remove('kupola-table-dragging');
849
865
  this._dragState = null;
@@ -851,6 +867,22 @@ class KupolaTable {
851
867
  });
852
868
  }
853
869
 
870
+ _handleRowDrop(e, targetRow) {
871
+ e.preventDefault();
872
+ targetRow.classList.remove('kupola-table-drag-over');
873
+ if (!this._dragState) return;
874
+ const toKey = targetRow.getAttribute('data-row-key');
875
+ if (this._dragState.fromKey === toKey) return;
876
+ const fromIdx = this._data.findIndex(r => String(r[this.rowKey]) === this._dragState.fromKey);
877
+ const toIdx = this._data.findIndex(r => String(r[this.rowKey]) === toKey);
878
+ if (fromIdx >= 0 && toIdx >= 0) {
879
+ const [moved] = this._data.splice(fromIdx, 1);
880
+ this._data.splice(toIdx, 0, moved);
881
+ if (this.onRowDragEnd) this.onRowDragEnd(moved, fromIdx, toIdx, this._data);
882
+ this.render();
883
+ }
884
+ }
885
+
854
886
  // ================================================================
855
887
  // STICKY COLUMNS
856
888
  // ================================================================
@@ -914,10 +946,9 @@ class KupolaTable {
914
946
  filterInput.className = 'ds-input kupola-table-filter-input';
915
947
  filterInput.placeholder = this.options.filterPlaceholder || '搜索...';
916
948
  filterInput.value = this._filterText;
917
- let debounceTimer;
918
949
  filterInput.addEventListener('input', () => {
919
- clearTimeout(debounceTimer);
920
- debounceTimer = setTimeout(() => {
950
+ clearTimeout(this._filterDebounceTimer);
951
+ this._filterDebounceTimer = setTimeout(() => {
921
952
  this._filterText = filterInput.value;
922
953
  this._currentPage = 1;
923
954
  this.filterText.value = this._filterText;
@@ -1105,91 +1136,41 @@ class KupolaTable {
1105
1136
  }
1106
1137
 
1107
1138
  destroy() {
1139
+ // 移除虚拟滚动 scroll listener
1140
+ if (this._scrollHandler) {
1141
+ const wrapper = this.element.querySelector('.kupola-table-container');
1142
+ if (wrapper) wrapper.removeEventListener('scroll', this._scrollHandler);
1143
+ this._scrollHandler = null;
1144
+ }
1145
+
1146
+ // 清除 filter debounce timer
1147
+ if (this._filterDebounceTimer) {
1148
+ clearTimeout(this._filterDebounceTimer);
1149
+ this._filterDebounceTimer = null;
1150
+ }
1151
+
1152
+ // 清理列宽拖拽残留的 document listener
1153
+ this._resizeCleanups.forEach(fn => fn());
1154
+ this._resizeCleanups = [];
1155
+
1156
+ // 清理 ref() 订阅
1157
+ this._reactiveCleanups.forEach(sub => sub.unsubscribe());
1158
+ this._reactiveCleanups = [];
1159
+
1160
+ // 清理 DOM 和 class
1108
1161
  this.element.innerHTML = '';
1109
1162
  this.element.classList.remove('kupola-table-wrapper', 'kupola-table-virtual-wrapper');
1110
- }
1111
- }
1112
1163
 
1113
- // ================================================================
1114
- // CSS
1115
- // ================================================================
1116
-
1117
- let stylesInjected = false;
1118
- function injectTableStyles() {
1119
- if (stylesInjected || typeof document === 'undefined') return;
1120
- const style = document.createElement('style');
1121
- style.textContent = `
1122
- .kupola-table-wrapper { width: 100%; }
1123
- .kupola-table-container { overflow-x: auto; }
1124
- .kupola-table { width: 100%; border-collapse: collapse; font-size: 14px; }
1125
- .kupola-table th, .kupola-table td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #e8e8e8; }
1126
- .kupola-table th { background: #fafafa; font-weight: 600; color: #333; white-space: nowrap; }
1127
- .kupola-table-striped tbody tr:nth-child(even) { background: #fafafa; }
1128
- .kupola-table-hover tbody tr:hover { background: #f0f7ff; }
1129
- .kupola-table-bordered { border: 1px solid #e8e8e8; }
1130
- .kupola-table-bordered th, .kupola-table-bordered td { border: 1px solid #e8e8e8; }
1131
- .kupola-table-compact th, .kupola-table-compact td { padding: 8px 12px; }
1132
- .kupola-table-sortable { cursor: pointer; user-select: none; position: relative; }
1133
- .kupola-table-sortable:hover { background: #f0f0f0; }
1134
- .kupola-table-sort-icon { font-size: 12px; opacity: 0.5; margin-left: 4px; }
1135
- .kupola-table-sort-asc .kupola-table-sort-icon,
1136
- .kupola-table-sort-desc .kupola-table-sort-icon { opacity: 1; color: #1890ff; }
1137
- .kupola-table-empty, .kupola-table-loading { text-align: center; padding: 40px 16px !important; color: #999; }
1138
- .kupola-table-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; flex-wrap: wrap; gap: 8px; }
1139
- .kupola-table-toolbar-right { display: flex; align-items: center; gap: 8px; }
1140
- .kupola-table-filter-input { padding: 6px 12px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 14px; width: 240px; }
1141
- .kupola-table-filter-input:focus { outline: none; border-color: #1890ff; box-shadow: 0 0 0 2px rgba(24,144,255,0.1); }
1142
- .kupola-table-info { color: #999; font-size: 13px; }
1143
- .kupola-table-selection-info { color: #1890ff; font-size: 13px; font-weight: 500; }
1144
- .kupola-table-col-selection { width: 40px; text-align: center; }
1145
- .kupola-table-col-expand { width: 40px; text-align: center; }
1146
- .kupola-table-expand-btn { background: none; border: none; cursor: pointer; font-size: 12px; padding: 2px 6px; color: #666; }
1147
- .kupola-table-expand-btn:hover { color: #1890ff; }
1148
- .kupola-table-expand-row td { background: #fafafa; padding: 16px; }
1149
- .kupola-table-row-selected { background: #e6f7ff !important; }
1150
- .kupola-table-row-selected:hover { background: #bae7ff !important; }
1151
- /* Resize */
1152
- .kupola-table-resize-handle { position: absolute; right: 0; top: 0; bottom: 0; width: 6px; cursor: col-resize; background: transparent; }
1153
- .kupola-table-resize-handle:hover { background: #1890ff; opacity: 0.3; }
1154
- /* Drag */
1155
- .kupola-table-draggable { transition: opacity 0.2s; }
1156
- .kupola-table-dragging { opacity: 0.4; }
1157
- .kupola-table-drag-over { border-top: 2px solid #1890ff !important; }
1158
- /* Edit */
1159
- .kupola-table-editable-cell { cursor: text; }
1160
- .kupola-table-editable-cell:hover { background: #e6f7ff; }
1161
- .kupola-table-edit-cell { display: flex; gap: 4px; align-items: center; }
1162
- .kupola-table-edit-input { flex: 1; padding: 2px 6px; font-size: 13px; }
1163
- .kupola-table-edit-actions { display: flex; gap: 2px; }
1164
- .kupola-table-edit-save, .kupola-table-edit-cancel { background: none; border: 1px solid #d9d9d9; border-radius: 3px; cursor: pointer; padding: 2px 6px; font-size: 12px; }
1165
- .kupola-table-edit-save { color: #52c41a; border-color: #52c41a; }
1166
- .kupola-table-edit-cancel { color: #ff4d4f; border-color: #ff4d4f; }
1167
- .kupola-table-edit-save:hover { background: #f6ffed; }
1168
- .kupola-table-edit-cancel:hover { background: #fff2f0; }
1169
- /* Tree */
1170
- .kupola-table-tree-indent { display: inline-block; }
1171
- .kupola-table-tree-toggle { background: none; border: none; cursor: pointer; font-size: 10px; padding: 0 4px; color: #666; }
1172
- .kupola-table-tree-toggle:hover { color: #1890ff; }
1173
- .kupola-table-tree-toggle-placeholder { display: inline-block; width: 18px; }
1174
- /* Virtual */
1175
- .kupola-table-virtual-wrapper .kupola-table-container { overflow-y: auto; }
1176
- /* Pagination */
1177
- .kupola-table-pagination { display: flex; justify-content: flex-end; align-items: center; gap: 12px; margin-top: 16px; padding: 8px 0; }
1178
- .kupola-table-pages { display: flex; gap: 4px; align-items: center; }
1179
- .kupola-table-page-btn { min-width: 32px; height: 32px; border: 1px solid #d9d9d9; border-radius: 4px; background: #fff; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; }
1180
- .kupola-table-page-btn:hover:not(:disabled):not(.active) { border-color: #1890ff; color: #1890ff; }
1181
- .kupola-table-page-btn.active { background: #1890ff; color: #fff; border-color: #1890ff; }
1182
- .kupola-table-page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
1183
- .kupola-table-page-ellipsis { padding: 0 4px; color: #999; }
1184
- .kupola-table-page-size { padding: 4px 8px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 13px; }
1185
- .kupola-table-page-info { color: #999; font-size: 13px; }
1186
- `;
1187
- document.head.appendChild(style);
1188
- stylesInjected = true;
1164
+ // 清理数据引用
1165
+ this._data = [];
1166
+ this._virtualData = null;
1167
+ this._dragState = null;
1168
+ this._editingCell = null;
1169
+ this._editBuffer = {};
1170
+ }
1189
1171
  }
1190
1172
 
1191
1173
  function initTable(element, options) {
1192
- injectTableStyles();
1193
1174
  return new KupolaTable(element, options);
1194
1175
  }
1195
1176
 
@@ -1204,7 +1185,4 @@ function initAllTables() {
1204
1185
 
1205
1186
  export { KupolaTable, initTable, initAllTables };
1206
1187
 
1207
- if (typeof window !== 'undefined') {
1208
- window.KupolaTable = KupolaTable;
1209
- window.initTable = initTable;
1210
- }
1188
+ kupolaInitializer.register('table', initTable);
package/js/tag.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { kupolaInitializer } from './initializer.js';
2
+
1
3
  class Tag {
2
4
  constructor(element, options = {}) {
3
5
  this.element = element;
@@ -325,13 +327,4 @@ function initTags() {
325
327
 
326
328
  export { Tag, initTags, initTag, cleanupTag };
327
329
 
328
- if (typeof window !== 'undefined') {
329
- window.Tag = Tag;
330
- window.initTag = initTag;
331
- window.cleanupTag = cleanupTag;
332
- window.initTags = initTags;
333
-
334
- if (window.kupolaInitializer) {
335
- window.kupolaInitializer.register('tag', initTag, cleanupTag);
336
- }
337
- }
330
+ kupolaInitializer.register('tag', initTag, cleanupTag);
package/js/theme.js CHANGED
@@ -320,15 +320,4 @@ export {
320
320
  getTheme, setTheme, initTheme, createThemeToggle,
321
321
  getBrand, setBrand, BRAND_OPTIONS, createBrandPicker,
322
322
  THEME_KEY, BRAND_KEY
323
- };
324
-
325
- if (typeof window !== 'undefined') {
326
- window.getTheme = getTheme;
327
- window.setTheme = setTheme;
328
- window.initTheme = initTheme;
329
- window.createThemeToggle = createThemeToggle;
330
- window.getBrand = getBrand;
331
- window.setBrand = setBrand;
332
- window.BRAND_OPTIONS = BRAND_OPTIONS;
333
- window.createBrandPicker = createBrandPicker;
334
- }
323
+ };