@kupola/kupola 1.2.0 → 1.3.1

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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +330 -286
  3. package/adapters/axios.d.ts +34 -0
  4. package/adapters/axios.js +122 -0
  5. package/adapters/navios-http.d.ts +110 -0
  6. package/adapters/navios-http.js +151 -0
  7. package/dist/css/accessibility.css +119 -0
  8. package/dist/css/animations.css +224 -0
  9. package/dist/css/brand-themes.css +300 -0
  10. package/dist/css/colors_and_type.css +441 -0
  11. package/dist/css/components-ext.css +4165 -0
  12. package/dist/css/components.css +1483 -0
  13. package/dist/css/responsive.css +697 -0
  14. package/dist/css/scaffold.css +145 -0
  15. package/dist/css/states.css +316 -0
  16. package/dist/css/theme-dark.css +296 -0
  17. package/dist/css/theme-light.css +296 -0
  18. package/dist/css/utilities.css +171 -0
  19. package/dist/kupola.cjs.js +219 -161
  20. package/dist/kupola.cjs.js.map +1 -1
  21. package/dist/kupola.esm.js +6643 -5709
  22. package/dist/kupola.esm.js.map +1 -1
  23. package/dist/kupola.umd.js +219 -161
  24. package/dist/kupola.umd.js.map +1 -1
  25. package/dist/plugins/vite-plugin-kupola.js +120 -0
  26. package/dist/types/kupola.d.ts +421 -323
  27. package/js/calendar.js +334 -25
  28. package/js/carousel.js +182 -48
  29. package/js/collapse.js +148 -34
  30. package/js/color-picker.js +416 -108
  31. package/js/component.js +8 -19
  32. package/js/countdown.js +9 -8
  33. package/js/data-bind.js +73 -16
  34. package/js/datepicker.js +488 -110
  35. package/js/depends.js +710 -0
  36. package/js/dialog.js +4 -2
  37. package/js/drawer.js +172 -8
  38. package/js/dropdown.js +272 -17
  39. package/js/dynamic-tags.js +156 -40
  40. package/js/fileupload.js +9 -8
  41. package/js/form.js +280 -254
  42. package/js/global-events.js +281 -188
  43. package/js/heatmap.js +10 -7
  44. package/js/i18n.js +18 -10
  45. package/js/icons.js +141 -161
  46. package/js/image-preview.js +146 -2
  47. package/js/initializer.js +113 -71
  48. package/js/kupola-core.js +123 -45
  49. package/js/kupola-lifecycle.js +13 -11
  50. package/js/message.js +8 -1
  51. package/js/modal.js +207 -59
  52. package/js/notification.js +8 -1
  53. package/js/numberinput.js +9 -8
  54. package/js/pagination.js +263 -0
  55. package/js/registry.js +29 -12
  56. package/js/select.js +482 -27
  57. package/js/slide-captcha.js +11 -2
  58. package/js/slider.js +442 -25
  59. package/js/statcard.js +9 -7
  60. package/js/table.js +1210 -0
  61. package/js/tag.js +268 -14
  62. package/js/theme.js +14 -43
  63. package/js/timepicker.js +335 -66
  64. package/js/tooltip.js +317 -86
  65. package/js/utils.js +6 -2
  66. package/js/validation.js +6 -2
  67. package/js/virtual-list.js +11 -7
  68. package/js/web-components.js +288 -0
  69. package/package.json +77 -67
  70. package/plugins/vite-plugin-kupola.js +120 -0
  71. package/types/kupola.d.ts +421 -323
  72. package/CHANGELOG.md +0 -130
  73. package/INTEGRATION.md +0 -440
  74. package/PROJECT_SUMMARY.md +0 -312
  75. package/SKILL.md +0 -572
  76. package/dist/utils/utils/Kupola.cs +0 -77
  77. package/dist/utils/utils/Kupola.java +0 -104
  78. package/dist/utils/utils/kupola.go +0 -120
  79. package/dist/utils/utils/kupola.js +0 -63
  80. package/dist/utils/utils/kupola.py +0 -1392
  81. package/dist/utils/utils/kupola.rb +0 -69
  82. package/js/composition-api.js +0 -458
  83. package/js/error-handler.js +0 -181
  84. package/js/http.js +0 -419
  85. package/js/kupola-devtools.js +0 -598
  86. package/js/performance.js +0 -250
  87. package/js/router.js +0 -396
  88. package/js/security.js +0 -189
  89. package/js/test-utils.js +0 -251
  90. package/templates/base.html +0 -30
  91. package/templates/base_dashboard.html +0 -99
  92. package/utils/Kupola.cs +0 -77
  93. package/utils/Kupola.java +0 -104
  94. package/utils/kupola.go +0 -120
  95. package/utils/kupola.js +0 -63
  96. package/utils/kupola.py +0 -1392
  97. package/utils/kupola.rb +0 -69
package/js/table.js ADDED
@@ -0,0 +1,1210 @@
1
+ /**
2
+ * KupolaTable - Full-featured headless table component
3
+ * Features: sort, filter, pagination, selection, expand, inline-edit,
4
+ * column-resize, row-drag, sticky columns, tree data,
5
+ * virtual scroll, merge cells, multi-sort, CSV export
6
+ */
7
+
8
+ import { ref } from './data-bind.js';
9
+
10
+ class KupolaTable {
11
+ constructor(element, options = {}) {
12
+ this.element = typeof element === 'string' ? document.querySelector(element) : element;
13
+ this.options = options;
14
+
15
+ // Columns & data
16
+ this.columns = (options.columns || []).map((c, i) => ({ ...c, _index: i }));
17
+ this.rowKey = options.rowKey || 'id';
18
+ this._data = [];
19
+ this._loading = false;
20
+
21
+ // Display
22
+ this.striped = options.striped !== false;
23
+ this.bordered = options.bordered || false;
24
+ this.hoverable = options.hoverable !== false;
25
+ this.compact = options.compact || false;
26
+ this.emptyText = options.emptyText || '暂无数据';
27
+ this.loadingText = options.loadingText || '加载中...';
28
+
29
+ // Sort (multi-sort: array of {key, order})
30
+ this.multiSort = options.multiSort || false;
31
+ this._sorts = []; // [{key, order}]
32
+
33
+ // Filter
34
+ this._filterText = '';
35
+
36
+ // Pagination
37
+ this._showPagination = options.pagination !== false;
38
+ this._pageSizes = options.pageSizes || [10, 20, 50, 100];
39
+ this._pageSize = options.pageSize || 10;
40
+ this._currentPage = 1;
41
+ this._total = 0;
42
+
43
+ // Selection
44
+ this.selection = options.selection || null; // 'checkbox' | 'radio'
45
+ this._selectedKeys = new Set();
46
+ this.selectionColumnTitle = options.selectionColumnTitle || '';
47
+
48
+ // Expand
49
+ this.expandable = options.expandable || null; // function(row) => HTML/DOM
50
+ this._expandedKeys = new Set();
51
+ this.expandColumnTitle = options.expandColumnTitle || '';
52
+
53
+ // Inline edit
54
+ this.editable = options.editable || false; // true or { onSave, onCancel }
55
+ this._editingCell = null; // { rowKey, colKey }
56
+ this._editBuffer = {}; // { colKey: value }
57
+
58
+ // Column resize
59
+ this.resizable = options.resizable || false;
60
+
61
+ // Row drag
62
+ this.draggable = options.draggable || false;
63
+ this._dragState = null;
64
+
65
+ // Tree data
66
+ this.tree = options.tree || null; // { childrenKey: 'children', defaultExpandAll: false }
67
+ this._treeExpandedKeys = new Set();
68
+ if (options.tree?.defaultExpandAll) this._treeExpandAll = true;
69
+
70
+ // Virtual scroll
71
+ this.virtualScroll = options.virtualScroll || null; // { rowHeight: 40, overscan: 5 }
72
+ this._scrollContainer = null;
73
+
74
+ // Merge cells
75
+ this.mergeCells = options.mergeCells || null; // function(data) => [{row, col, rowSpan, colSpan}]
76
+
77
+ // Callbacks
78
+ this.onSort = options.onSort || null;
79
+ this.onPageChange = options.onPageChange || null;
80
+ this.onRowClick = options.onRowClick || null;
81
+ this.onFilter = options.onFilter || null;
82
+ this.onSelect = options.onSelect || null;
83
+ this.onExpand = options.onExpand || null;
84
+ this.onEditSave = options.onEditSave || null;
85
+ this.onEditCancel = options.onEditCancel || null;
86
+ this.onRowDragEnd = options.onRowDragEnd || null;
87
+ this.onColumnResize = options.onColumnResize || null;
88
+
89
+ // Reactive refs
90
+ this.sortKey = ref(null);
91
+ this.sortOrder = ref(null);
92
+ this.currentPage = ref(1);
93
+ this.filterText = ref('');
94
+ this.selectedKeys = ref([]);
95
+
96
+ this._init();
97
+ }
98
+
99
+ _init() {
100
+ this.element.classList.add('kupola-table-wrapper');
101
+ if (this.virtualScroll) this.element.classList.add('kupola-table-virtual-wrapper');
102
+ this.render();
103
+ }
104
+
105
+ // ================================================================
106
+ // DATA
107
+ // ================================================================
108
+
109
+ setData(data) {
110
+ if (data && typeof data === 'object' && 'value' in data) {
111
+ 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
+ } else if (Array.isArray(data)) {
118
+ this._data = data;
119
+ } else {
120
+ this._data = [];
121
+ }
122
+ if (this.tree && this._treeExpandAll) {
123
+ this._flattenForExpand(this._data);
124
+ }
125
+ this._total = this._getFlatData(this._data).length;
126
+ this.render();
127
+ }
128
+
129
+ setLoading(loading) {
130
+ if (loading && typeof loading === 'object' && 'value' in loading) {
131
+ this._loading = loading.value;
132
+ loading._subscribers?.add((val) => { this._loading = val; this.render(); });
133
+ } else {
134
+ this._loading = !!loading;
135
+ }
136
+ this.render();
137
+ }
138
+
139
+ // ================================================================
140
+ // TREE HELPERS
141
+ // ================================================================
142
+
143
+ _flattenForExpand(data, level = 0, parentKey = null) {
144
+ const ck = this.tree?.childrenKey || 'children';
145
+ const result = [];
146
+ for (const row of data) {
147
+ const key = row[this.rowKey];
148
+ result.push({ ...row, _level: level, _parentKey: parentKey, _hasChildren: !!(row[ck] && row[ck].length) });
149
+ if (row[ck] && row[ck].length) {
150
+ result.push(...this._flattenForExpand(row[ck], level + 1, key));
151
+ }
152
+ }
153
+ return result;
154
+ }
155
+
156
+ _getFlatData(data) {
157
+ if (!this.tree) return data;
158
+ return this._flattenVisible(data, 0);
159
+ }
160
+
161
+ _flattenVisible(data, level) {
162
+ const ck = this.tree?.childrenKey || 'children';
163
+ const result = [];
164
+ for (const row of data) {
165
+ const key = row[this.rowKey];
166
+ result.push({ ...row, _level: level, _hasChildren: !!(row[ck] && row[ck].length) });
167
+ if (row[ck] && row[ck].length && this._treeExpandedKeys.has(key)) {
168
+ result.push(...this._flattenVisible(row[ck], level + 1));
169
+ }
170
+ }
171
+ return result;
172
+ }
173
+
174
+ // ================================================================
175
+ // PROCESSING: filter → sort → paginate
176
+ // ================================================================
177
+
178
+ getProcessedData() {
179
+ let data = this.tree ? [...this._data] : [...this._data];
180
+
181
+ // Filter
182
+ if (this._filterText) {
183
+ const text = this._filterText.toLowerCase();
184
+ if (this.tree) {
185
+ data = this._filterTree(data, text);
186
+ } else {
187
+ data = data.filter(row =>
188
+ this.columns.some(col => {
189
+ const val = row[col.key];
190
+ return val != null && String(val).toLowerCase().includes(text);
191
+ })
192
+ );
193
+ }
194
+ }
195
+
196
+ // Sort
197
+ if (this._sorts.length > 0) {
198
+ if (this.tree) {
199
+ data = this._sortTree(data);
200
+ } else {
201
+ data = this._sortFlat(data);
202
+ }
203
+ }
204
+
205
+ const flatData = this.tree ? this._flattenVisible(data) : data;
206
+ this._total = flatData.length;
207
+
208
+ // Paginate
209
+ let pageData = flatData;
210
+ if (this._showPagination && this._pageSize > 0) {
211
+ const start = (this._currentPage - 1) * this._pageSize;
212
+ pageData = flatData.slice(start, start + this._pageSize);
213
+ }
214
+
215
+ return pageData;
216
+ }
217
+
218
+ _filterTree(data, text) {
219
+ const ck = this.tree?.childrenKey || 'children';
220
+ return data.reduce((acc, row) => {
221
+ const children = row[ck] ? this._filterTree(row[ck], text) : [];
222
+ const selfMatch = this.columns.some(col => {
223
+ const v = row[col.key];
224
+ return v != null && String(v).toLowerCase().includes(text);
225
+ });
226
+ if (selfMatch || children.length > 0) {
227
+ acc.push({ ...row, [ck]: children });
228
+ if (children.length > 0) this._treeExpandedKeys.add(row[this.rowKey]);
229
+ }
230
+ return acc;
231
+ }, []);
232
+ }
233
+
234
+ _sortFlat(data) {
235
+ return [...data].sort((a, b) => {
236
+ for (const s of this._sorts) {
237
+ const col = this.columns.find(c => c.key === s.key);
238
+ let va = a[s.key], vb = b[s.key];
239
+ let cmp = 0;
240
+ if (col?.sorter) { cmp = col.sorter(va, vb, s.order); }
241
+ else {
242
+ if (va == null) cmp = 1;
243
+ else if (vb == null) cmp = -1;
244
+ else if (typeof va === 'number' && typeof vb === 'number') cmp = s.order === 'asc' ? va - vb : vb - va;
245
+ else cmp = s.order === 'asc' ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va));
246
+ }
247
+ if (cmp !== 0) return cmp;
248
+ }
249
+ return 0;
250
+ });
251
+ }
252
+
253
+ _sortTree(data) {
254
+ const sorted = this._sortFlat(data);
255
+ const ck = this.tree?.childrenKey || 'children';
256
+ return sorted.map(row => row[ck]?.length ? { ...row, [ck]: this._sortTree(row[ck]) } : row);
257
+ }
258
+
259
+ // ================================================================
260
+ // RENDER
261
+ // ================================================================
262
+
263
+ render() {
264
+ const data = this.getProcessedData();
265
+ const wrapper = this.element;
266
+ wrapper.innerHTML = '';
267
+
268
+ // Toolbar
269
+ if (this.options.showFilter || this.options.showToolbar) {
270
+ wrapper.appendChild(this._renderToolbar());
271
+ }
272
+
273
+ // Table container (for virtual scroll)
274
+ const tableContainer = document.createElement('div');
275
+ tableContainer.className = 'kupola-table-container';
276
+
277
+ const table = document.createElement('table');
278
+ table.className = this._getTableClass();
279
+
280
+ // Thead
281
+ table.appendChild(this._renderThead());
282
+
283
+ // Tbody
284
+ if (this.virtualScroll) {
285
+ table.appendChild(this._renderVirtualTbody(data));
286
+ } else {
287
+ table.appendChild(this._renderTbody(data));
288
+ }
289
+
290
+ tableContainer.appendChild(table);
291
+ wrapper.appendChild(tableContainer);
292
+
293
+ // Pagination
294
+ if (this._showPagination && this._total > 0) {
295
+ wrapper.appendChild(this._renderPagination());
296
+ }
297
+
298
+ // Post-render hooks
299
+ if (this.resizable) this._initColumnResize();
300
+ if (this.draggable) this._initRowDrag();
301
+ this._applyStickyColumns();
302
+ }
303
+
304
+ _renderThead() {
305
+ const thead = document.createElement('thead');
306
+ const tr = document.createElement('tr');
307
+
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
325
+ if (this.expandable) {
326
+ const th = document.createElement('th');
327
+ th.className = 'kupola-table-col-expand';
328
+ tr.appendChild(th);
329
+ }
330
+
331
+ // Data columns
332
+ 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
+
369
+ tr.appendChild(th);
370
+ });
371
+
372
+ thead.appendChild(tr);
373
+ return thead;
374
+ }
375
+
376
+ _renderTbody(data) {
377
+ const tbody = document.createElement('tbody');
378
+
379
+ if (this._loading) {
380
+ tbody.appendChild(this._renderStatusRow(this.loadingText, 'kupola-table-loading'));
381
+ } else if (data.length === 0) {
382
+ tbody.appendChild(this._renderStatusRow(this.emptyText, 'kupola-table-empty'));
383
+ } else {
384
+ // Merge cells config
385
+ const mergeConfig = this.mergeCells ? this.mergeCells(data) : [];
386
+ const mergeMap = new Map();
387
+ mergeConfig.forEach(m => mergeMap.set(`${m.row}-${m.col}`, m));
388
+ const skipCells = new Set();
389
+
390
+ data.forEach((row, rowIndex) => {
391
+ const key = row[this.rowKey] ?? rowIndex;
392
+ const isSelected = this._selectedKeys.has(key);
393
+ const isExpanded = this._expandedKeys.has(key);
394
+
395
+ // Data row
396
+ const tr = this._renderDataRow(row, rowIndex, key, isSelected, skipCells, mergeMap);
397
+ tbody.appendChild(tr);
398
+
399
+ // Expand row
400
+ if (this.expandable && isExpanded) {
401
+ const expandTr = document.createElement('tr');
402
+ expandTr.className = 'kupola-table-expand-row';
403
+ const expandTd = document.createElement('td');
404
+ const totalCols = this.columns.length + (this.selection ? 1 : 0) + 1;
405
+ expandTd.colSpan = totalCols;
406
+ expandTd.className = 'kupola-table-expand-content';
407
+ const content = this.expandable(row);
408
+ if (typeof content === 'string') expandTd.innerHTML = content;
409
+ else if (content instanceof HTMLElement) expandTd.appendChild(content);
410
+ expandTr.appendChild(expandTd);
411
+ tbody.appendChild(expandTr);
412
+ }
413
+ });
414
+ }
415
+ return tbody;
416
+ }
417
+
418
+ _renderDataRow(row, rowIndex, key, isSelected, skipCells, mergeMap) {
419
+ const tr = document.createElement('tr');
420
+ tr.setAttribute('data-row-key', key);
421
+ if (isSelected) tr.classList.add('kupola-table-row-selected');
422
+ if (this.draggable) {
423
+ tr.draggable = true;
424
+ tr.classList.add('kupola-table-draggable');
425
+ }
426
+
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
+ }
462
+
463
+ // Data cells
464
+ this.columns.forEach((col, colIndex) => {
465
+ // Skip merged cells
466
+ if (skipCells.has(`${rowIndex}-${colIndex}`)) return;
467
+
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
+ }
474
+
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
+ }
486
+ }
487
+
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);
508
+ }
509
+ }
510
+
511
+ // Editable cell
512
+ const isEditing = this._editingCell &&
513
+ this._editingCell.rowKey === key && this._editingCell.colKey === col.key;
514
+
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
+
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
+ }
530
+
531
+ tr.appendChild(td);
532
+ });
533
+
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);
539
+ });
540
+ }
541
+
542
+ return tr;
543
+ }
544
+
545
+ _renderStatusRow(text, className) {
546
+ const tr = document.createElement('tr');
547
+ const td = document.createElement('td');
548
+ td.colSpan = this.columns.length + (this.selection ? 1 : 0) + (this.expandable ? 1 : 0);
549
+ td.className = className;
550
+ td.textContent = text;
551
+ tr.appendChild(td);
552
+ return tr;
553
+ }
554
+
555
+ // ================================================================
556
+ // VIRTUAL SCROLL
557
+ // ================================================================
558
+
559
+ _renderVirtualTbody(data) {
560
+ const tbody = document.createElement('tbody');
561
+ const { rowHeight = 40, overscan = 5 } = this.virtualScroll;
562
+ const totalHeight = data.length * rowHeight;
563
+
564
+ if (this._loading) {
565
+ return this._renderTbody(data);
566
+ }
567
+ if (data.length === 0) {
568
+ return this._renderTbody(data);
569
+ }
570
+
571
+ // Create spacer rows for total height
572
+ const topSpacer = document.createElement('tr');
573
+ topSpacer.className = 'kupola-table-virtual-spacer-top';
574
+ topSpacer.style.height = '0px';
575
+ tbody.appendChild(topSpacer);
576
+
577
+ // We'll update positions in _updateVirtualScroll
578
+ this._virtualData = { data, rowHeight, overscan, totalHeight, tbody, topSpacer };
579
+ this._updateVirtualScroll();
580
+
581
+ const bottomSpacer = document.createElement('tr');
582
+ bottomSpacer.className = 'kupola-table-virtual-spacer-bottom';
583
+ bottomSpacer.style.height = '0px';
584
+ tbody.appendChild(bottomSpacer);
585
+
586
+ // Attach scroll listener
587
+ const wrapper = this.element.querySelector('.kupola-table-container');
588
+ if (wrapper) {
589
+ wrapper.style.maxHeight = this.virtualScroll.maxHeight || '400px';
590
+ wrapper.style.overflowY = 'auto';
591
+ wrapper.addEventListener('scroll', () => this._updateVirtualScroll());
592
+ }
593
+
594
+ return tbody;
595
+ }
596
+
597
+ _updateVirtualScroll() {
598
+ if (!this._virtualData) return;
599
+ const { data, rowHeight, overscan, tbody, topSpacer } = this._virtualData;
600
+ const wrapper = this.element.querySelector('.kupola-table-container');
601
+ if (!wrapper) return;
602
+
603
+ const scrollTop = wrapper.scrollTop;
604
+ const viewHeight = wrapper.clientHeight;
605
+ const startIdx = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
606
+ const endIdx = Math.min(data.length, Math.ceil((scrollTop + viewHeight) / rowHeight) + overscan);
607
+
608
+ // Remove existing data rows
609
+ const existingRows = tbody.querySelectorAll('.kupola-table-virtual-row');
610
+ existingRows.forEach(r => r.remove());
611
+
612
+ // Insert visible rows
613
+ const fragment = document.createDocumentFragment();
614
+ for (let i = startIdx; i < endIdx; i++) {
615
+ const row = data[i];
616
+ const key = row[this.rowKey] ?? i;
617
+ const tr = this._renderDataRow(row, i, key, this._selectedKeys.has(key), new Set(), new Map());
618
+ tr.classList.add('kupola-table-virtual-row');
619
+ tr.style.height = rowHeight + 'px';
620
+ fragment.appendChild(tr);
621
+ }
622
+
623
+ topSpacer.style.height = (startIdx * rowHeight) + 'px';
624
+ const bottomSpacer = tbody.querySelector('.kupola-table-virtual-spacer-bottom');
625
+ if (bottomSpacer) bottomSpacer.style.height = ((data.length - endIdx) * rowHeight) + 'px';
626
+
627
+ topSpacer.after(fragment);
628
+ }
629
+
630
+ // ================================================================
631
+ // INLINE EDIT
632
+ // ================================================================
633
+
634
+ _renderEditCell(col, row) {
635
+ const container = document.createElement('div');
636
+ container.className = 'kupola-table-edit-cell';
637
+
638
+ const input = document.createElement('input');
639
+ input.type = col.editType || 'text';
640
+ input.className = 'ds-input kupola-table-edit-input';
641
+ input.value = this._editBuffer[col.key] ?? row[col.key] ?? '';
642
+ if (col.editOptions) {
643
+ // Select dropdown
644
+ const select = document.createElement('select');
645
+ select.className = 'ds-input kupola-table-edit-input';
646
+ col.editOptions.forEach(opt => {
647
+ const o = document.createElement('option');
648
+ o.value = typeof opt === 'object' ? opt.value : opt;
649
+ o.textContent = typeof opt === 'object' ? opt.label : opt;
650
+ if (String(o.value) === String(input.value)) o.selected = true;
651
+ select.appendChild(o);
652
+ });
653
+ select.addEventListener('change', () => { this._editBuffer[col.key] = select.value; });
654
+ container.appendChild(select);
655
+ } else {
656
+ input.addEventListener('input', () => { this._editBuffer[col.key] = input.value; });
657
+ container.appendChild(input);
658
+ }
659
+
660
+ // Save / Cancel buttons
661
+ const actions = document.createElement('div');
662
+ actions.className = 'kupola-table-edit-actions';
663
+ const saveBtn = document.createElement('button');
664
+ saveBtn.className = 'kupola-table-edit-save';
665
+ saveBtn.textContent = '✓';
666
+ saveBtn.type = 'button';
667
+ saveBtn.addEventListener('click', () => this._saveEdit(row, col));
668
+ const cancelBtn = document.createElement('button');
669
+ cancelBtn.className = 'kupola-table-edit-cancel';
670
+ cancelBtn.textContent = '✗';
671
+ cancelBtn.type = 'button';
672
+ cancelBtn.addEventListener('click', () => this._cancelEdit());
673
+ actions.appendChild(saveBtn);
674
+ actions.appendChild(cancelBtn);
675
+ container.appendChild(actions);
676
+
677
+ // Enter to save, Escape to cancel
678
+ input.addEventListener('keydown', (e) => {
679
+ if (e.key === 'Enter') this._saveEdit(row, col);
680
+ if (e.key === 'Escape') this._cancelEdit();
681
+ });
682
+
683
+ setTimeout(() => input.focus?.(), 0);
684
+ return container;
685
+ }
686
+
687
+ _startEdit(rowKey, colKey, currentValue) {
688
+ this._editingCell = { rowKey, colKey };
689
+ this._editBuffer = { [colKey]: currentValue };
690
+ this.render();
691
+ }
692
+
693
+ _saveEdit(row, col) {
694
+ const newValue = this._editBuffer[col.key];
695
+ if (this.onEditSave) {
696
+ this.onEditSave(row, col.key, newValue, this._data);
697
+ } else {
698
+ row[col.key] = newValue;
699
+ }
700
+ this._editingCell = null;
701
+ this._editBuffer = {};
702
+ this.render();
703
+ }
704
+
705
+ _cancelEdit() {
706
+ if (this.onEditCancel) this.onEditCancel(this._editingCell);
707
+ this._editingCell = null;
708
+ this._editBuffer = {};
709
+ this.render();
710
+ }
711
+
712
+ // ================================================================
713
+ // SORT
714
+ // ================================================================
715
+
716
+ _handleSort(key) {
717
+ if (this.multiSort) {
718
+ const existing = this._sorts.findIndex(s => s.key === key);
719
+ if (existing >= 0) {
720
+ const s = this._sorts[existing];
721
+ if (s.order === 'asc') s.order = 'desc';
722
+ else this._sorts.splice(existing, 1);
723
+ } else {
724
+ this._sorts.push({ key, order: 'asc' });
725
+ }
726
+ } else {
727
+ const existing = this._sorts.find(s => s.key === key);
728
+ if (existing) {
729
+ if (existing.order === 'asc') existing.order = 'desc';
730
+ else this._sorts = [];
731
+ } else {
732
+ this._sorts = [{ key, order: 'asc' }];
733
+ }
734
+ }
735
+ this.sortKey.value = this._sorts.map(s => s.key).join(',');
736
+ this.sortOrder.value = this._sorts.map(s => s.order).join(',');
737
+ this._currentPage = 1;
738
+ if (this.onSort) this.onSort(this._sorts);
739
+ this.render();
740
+ }
741
+
742
+ // ================================================================
743
+ // EXPAND
744
+ // ================================================================
745
+
746
+ _toggleExpand(key) {
747
+ this._expandedKeys.has(key) ? this._expandedKeys.delete(key) : this._expandedKeys.add(key);
748
+ if (this.onExpand) this.onExpand(key, this._expandedKeys.has(key));
749
+ this.render();
750
+ }
751
+
752
+ _toggleTreeExpand(key) {
753
+ this._treeExpandedKeys.has(key) ? this._treeExpandedKeys.delete(key) : this._treeExpandedKeys.add(key);
754
+ this.render();
755
+ }
756
+
757
+ // ================================================================
758
+ // SELECTION API
759
+ // ================================================================
760
+
761
+ selectRow(key) { this._selectedKeys.add(key); this._syncSelected(); this.render(); }
762
+ deselectRow(key) { this._selectedKeys.delete(key); this._syncSelected(); this.render(); }
763
+ selectAll() {
764
+ this.getProcessedData().forEach(r => this._selectedKeys.add(r[this.rowKey]));
765
+ this._syncSelected(); this.render();
766
+ }
767
+ deselectAll() { this._selectedKeys.clear(); this._syncSelected(); this.render(); }
768
+ invertSelection() {
769
+ const pageData = this.getProcessedData();
770
+ pageData.forEach(r => {
771
+ const k = r[this.rowKey];
772
+ this._selectedKeys.has(k) ? this._selectedKeys.delete(k) : this._selectedKeys.add(k);
773
+ });
774
+ this._syncSelected(); this.render();
775
+ }
776
+ getSelectedKeys() { return [...this._selectedKeys]; }
777
+ getSelectedRows() {
778
+ const all = this.tree ? this._flattenForExpand(this._data) : this._data;
779
+ return all.filter(r => this._selectedKeys.has(r[this.rowKey]));
780
+ }
781
+ _syncSelected() { this.selectedKeys.value = [...this._selectedKeys]; }
782
+
783
+ // ================================================================
784
+ // COLUMN RESIZE
785
+ // ================================================================
786
+
787
+ _initColumnResize() {
788
+ const handles = this.element.querySelectorAll('.kupola-table-resize-handle');
789
+ handles.forEach(handle => {
790
+ handle.addEventListener('mousedown', (e) => {
791
+ e.preventDefault();
792
+ const colKey = handle.getAttribute('data-col-key');
793
+ const th = handle.parentElement;
794
+ const startX = e.clientX;
795
+ const startWidth = th.offsetWidth;
796
+
797
+ const onMouseMove = (ev) => {
798
+ const newWidth = Math.max(50, startWidth + (ev.clientX - startX));
799
+ th.style.width = newWidth + 'px';
800
+ const col = this.columns.find(c => c.key === colKey);
801
+ if (col) col.width = newWidth;
802
+ if (this.onColumnResize) this.onColumnResize(colKey, newWidth);
803
+ };
804
+ const onMouseUp = () => {
805
+ document.removeEventListener('mousemove', onMouseMove);
806
+ document.removeEventListener('mouseup', onMouseUp);
807
+ };
808
+ document.addEventListener('mousemove', onMouseMove);
809
+ document.addEventListener('mouseup', onMouseUp);
810
+ });
811
+ });
812
+ }
813
+
814
+ // ================================================================
815
+ // ROW DRAG
816
+ // ================================================================
817
+
818
+ _initRowDrag() {
819
+ const rows = this.element.querySelectorAll('tbody tr[data-row-key]');
820
+ rows.forEach(row => {
821
+ row.addEventListener('dragstart', (e) => {
822
+ this._dragState = { fromKey: row.getAttribute('data-row-key') };
823
+ row.classList.add('kupola-table-dragging');
824
+ e.dataTransfer.effectAllowed = 'move';
825
+ });
826
+ row.addEventListener('dragover', (e) => {
827
+ e.preventDefault();
828
+ e.dataTransfer.dropEffect = 'move';
829
+ row.classList.add('kupola-table-drag-over');
830
+ });
831
+ 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
+ });
847
+ row.addEventListener('dragend', () => {
848
+ row.classList.remove('kupola-table-dragging');
849
+ this._dragState = null;
850
+ });
851
+ });
852
+ }
853
+
854
+ // ================================================================
855
+ // STICKY COLUMNS
856
+ // ================================================================
857
+
858
+ _applyStickyColumns() {
859
+ let leftOffset = 0;
860
+ const fixedLeft = this.columns.filter(c => c.fixed === 'left');
861
+ if (this.selection) leftOffset += 40;
862
+ if (this.expandable) leftOffset += 40;
863
+
864
+ fixedLeft.forEach(col => {
865
+ const ths = this.element.querySelectorAll(`th[data-fixed="left"]`);
866
+ const tds = this.element.querySelectorAll(`td[data-fixed="left"]`);
867
+ // Apply offset per column
868
+ const colIdx = this.columns.indexOf(col);
869
+ let offset = (this.selection ? 40 : 0) + (this.expandable ? 40 : 0);
870
+ for (let i = 0; i < colIdx; i++) {
871
+ if (this.columns[i].fixed === 'left') offset += (this.columns[i]._resolvedWidth || 120);
872
+ }
873
+ ths.forEach(th => {
874
+ if (th.textContent.startsWith(col.title || col.key)) {
875
+ th.style.position = 'sticky';
876
+ th.style.left = offset + 'px';
877
+ th.style.zIndex = '2';
878
+ col._resolvedWidth = th.offsetWidth;
879
+ }
880
+ });
881
+ tds.forEach(td => {
882
+ td.style.position = 'sticky';
883
+ td.style.left = offset + 'px';
884
+ td.style.zIndex = '1';
885
+ td.style.background = 'inherit';
886
+ });
887
+ });
888
+
889
+ // Fixed right
890
+ let rightOffset = 0;
891
+ const fixedRight = [...this.columns].filter(c => c.fixed === 'right').reverse();
892
+ fixedRight.forEach(col => {
893
+ const tds = this.element.querySelectorAll(`td[data-fixed="right"]`);
894
+ tds.forEach(td => {
895
+ td.style.position = 'sticky';
896
+ td.style.right = rightOffset + 'px';
897
+ td.style.zIndex = '1';
898
+ });
899
+ rightOffset += (col._resolvedWidth || col.width || 120);
900
+ });
901
+ }
902
+
903
+ // ================================================================
904
+ // TOOLBAR
905
+ // ================================================================
906
+
907
+ _renderToolbar() {
908
+ const toolbar = document.createElement('div');
909
+ toolbar.className = 'kupola-table-toolbar';
910
+
911
+ if (this.options.showFilter) {
912
+ const filterInput = document.createElement('input');
913
+ filterInput.type = 'text';
914
+ filterInput.className = 'ds-input kupola-table-filter-input';
915
+ filterInput.placeholder = this.options.filterPlaceholder || '搜索...';
916
+ filterInput.value = this._filterText;
917
+ let debounceTimer;
918
+ filterInput.addEventListener('input', () => {
919
+ clearTimeout(debounceTimer);
920
+ debounceTimer = setTimeout(() => {
921
+ this._filterText = filterInput.value;
922
+ this._currentPage = 1;
923
+ this.filterText.value = this._filterText;
924
+ if (this.onFilter) this.onFilter(this._filterText);
925
+ this.render();
926
+ }, 300);
927
+ });
928
+ toolbar.appendChild(filterInput);
929
+ }
930
+
931
+ const right = document.createElement('div');
932
+ right.className = 'kupola-table-toolbar-right';
933
+
934
+ // Selection info
935
+ if (this.selection && this._selectedKeys.size > 0) {
936
+ const info = document.createElement('span');
937
+ info.className = 'kupola-table-selection-info';
938
+ info.textContent = `已选 ${this._selectedKeys.size} 项`;
939
+ right.appendChild(info);
940
+
941
+ const invertBtn = document.createElement('button');
942
+ invertBtn.className = 'ds-btn ds-btn--sm';
943
+ invertBtn.textContent = '反选';
944
+ invertBtn.type = 'button';
945
+ invertBtn.addEventListener('click', () => this.invertSelection());
946
+ right.appendChild(invertBtn);
947
+ }
948
+
949
+ // Export button
950
+ if (this.options.showExport) {
951
+ const exportBtn = document.createElement('button');
952
+ exportBtn.className = 'ds-btn ds-btn--sm ds-btn--secondary';
953
+ exportBtn.textContent = '导出 CSV';
954
+ exportBtn.type = 'button';
955
+ exportBtn.addEventListener('click', () => this.exportCSV());
956
+ right.appendChild(exportBtn);
957
+ }
958
+
959
+ // Total count
960
+ const totalInfo = document.createElement('span');
961
+ totalInfo.className = 'kupola-table-info';
962
+ totalInfo.textContent = `共 ${this._total} 条`;
963
+ right.appendChild(totalInfo);
964
+
965
+ toolbar.appendChild(right);
966
+ return toolbar;
967
+ }
968
+
969
+ // ================================================================
970
+ // PAGINATION
971
+ // ================================================================
972
+
973
+ _renderPagination() {
974
+ const totalPages = Math.ceil(this._total / this._pageSize);
975
+ if (totalPages <= 1) return document.createElement('div');
976
+
977
+ const nav = document.createElement('div');
978
+ nav.className = 'kupola-table-pagination';
979
+
980
+ if (this.options.showPageSize) {
981
+ const sizeSelect = document.createElement('select');
982
+ sizeSelect.className = 'kupola-table-page-size';
983
+ this._pageSizes.forEach(size => {
984
+ const opt = document.createElement('option');
985
+ opt.value = size;
986
+ opt.textContent = `${size} 条/页`;
987
+ if (size === this._pageSize) opt.selected = true;
988
+ sizeSelect.appendChild(opt);
989
+ });
990
+ sizeSelect.addEventListener('change', () => {
991
+ this._pageSize = parseInt(sizeSelect.value);
992
+ this._currentPage = 1;
993
+ this.currentPage.value = 1;
994
+ this.render();
995
+ });
996
+ nav.appendChild(sizeSelect);
997
+ }
998
+
999
+ const pages = document.createElement('div');
1000
+ pages.className = 'kupola-table-pages';
1001
+
1002
+ const prevBtn = this._createPageBtn('‹', () => this._goToPage(this._currentPage - 1));
1003
+ prevBtn.disabled = this._currentPage <= 1;
1004
+ pages.appendChild(prevBtn);
1005
+
1006
+ this._getPageRange(this._currentPage, totalPages).forEach(p => {
1007
+ if (p === '...') {
1008
+ const span = document.createElement('span');
1009
+ span.className = 'kupola-table-page-ellipsis';
1010
+ span.textContent = '...';
1011
+ pages.appendChild(span);
1012
+ } else {
1013
+ const btn = this._createPageBtn(p, () => this._goToPage(p));
1014
+ if (p === this._currentPage) btn.classList.add('active');
1015
+ pages.appendChild(btn);
1016
+ }
1017
+ });
1018
+
1019
+ const nextBtn = this._createPageBtn('›', () => this._goToPage(this._currentPage + 1));
1020
+ nextBtn.disabled = this._currentPage >= totalPages;
1021
+ pages.appendChild(nextBtn);
1022
+
1023
+ nav.appendChild(pages);
1024
+
1025
+ const info = document.createElement('span');
1026
+ info.className = 'kupola-table-page-info';
1027
+ info.textContent = `${this._currentPage} / ${totalPages}`;
1028
+ nav.appendChild(info);
1029
+
1030
+ return nav;
1031
+ }
1032
+
1033
+ _createPageBtn(text, onClick) {
1034
+ const btn = document.createElement('button');
1035
+ btn.className = 'kupola-table-page-btn';
1036
+ btn.textContent = text;
1037
+ btn.type = 'button';
1038
+ btn.addEventListener('click', onClick);
1039
+ return btn;
1040
+ }
1041
+
1042
+ _goToPage(page) {
1043
+ const totalPages = Math.ceil(this._total / this._pageSize);
1044
+ if (page < 1 || page > totalPages) return;
1045
+ this._currentPage = page;
1046
+ this.currentPage.value = page;
1047
+ if (this.onPageChange) this.onPageChange(page, this._pageSize);
1048
+ this.render();
1049
+ }
1050
+
1051
+ _getPageRange(current, total) {
1052
+ if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
1053
+ const range = [];
1054
+ if (current <= 3) { for (let i = 1; i <= 5; i++) range.push(i); range.push('...', total); }
1055
+ else if (current >= total - 2) { range.push(1, '...'); for (let i = total - 4; i <= total; i++) range.push(i); }
1056
+ else { range.push(1, '...'); for (let i = current - 1; i <= current + 1; i++) range.push(i); range.push('...', total); }
1057
+ return range;
1058
+ }
1059
+
1060
+ // ================================================================
1061
+ // EXPORT CSV
1062
+ // ================================================================
1063
+
1064
+ exportCSV(filename = 'export.csv') {
1065
+ const data = this.getProcessedData();
1066
+ const headers = this.columns.map(c => c.title || c.key);
1067
+ const rows = data.map(row =>
1068
+ this.columns.map(col => {
1069
+ let val = row[col.key];
1070
+ if (val == null) val = '';
1071
+ val = String(val).replace(/"/g, '""');
1072
+ return `"${val}"`;
1073
+ }).join(',')
1074
+ );
1075
+ const csv = '\uFEFF' + [headers.join(','), ...rows].join('\n');
1076
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
1077
+ const url = URL.createObjectURL(blob);
1078
+ const a = document.createElement('a');
1079
+ a.href = url;
1080
+ a.download = filename;
1081
+ a.click();
1082
+ URL.revokeObjectURL(url);
1083
+ }
1084
+
1085
+ // ================================================================
1086
+ // HELPERS
1087
+ // ================================================================
1088
+
1089
+ _getTableClass() {
1090
+ const classes = ['kupola-table'];
1091
+ if (this.striped) classes.push('kupola-table-striped');
1092
+ if (this.bordered) classes.push('kupola-table-bordered');
1093
+ if (this.hoverable) classes.push('kupola-table-hover');
1094
+ if (this.compact) classes.push('kupola-table-compact');
1095
+ return classes.join(' ');
1096
+ }
1097
+
1098
+ refresh() { this.render(); }
1099
+
1100
+ getPage() { return { current: this._currentPage, pageSize: this._pageSize, total: this._total }; }
1101
+
1102
+ setColumns(columns) {
1103
+ this.columns = columns.map((c, i) => ({ ...c, _index: i }));
1104
+ this.render();
1105
+ }
1106
+
1107
+ destroy() {
1108
+ this.element.innerHTML = '';
1109
+ this.element.classList.remove('kupola-table-wrapper', 'kupola-table-virtual-wrapper');
1110
+ }
1111
+ }
1112
+
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;
1189
+ }
1190
+
1191
+ function initTable(element, options) {
1192
+ injectTableStyles();
1193
+ return new KupolaTable(element, options);
1194
+ }
1195
+
1196
+ function initAllTables() {
1197
+ document.querySelectorAll('[data-kupola-table]').forEach(el => {
1198
+ const config = el.getAttribute('data-kupola-table');
1199
+ let options = {};
1200
+ if (config) { try { options = JSON.parse(config); } catch (_) { } }
1201
+ initTable(el, options);
1202
+ });
1203
+ }
1204
+
1205
+ export { KupolaTable, initTable, initAllTables };
1206
+
1207
+ if (typeof window !== 'undefined') {
1208
+ window.KupolaTable = KupolaTable;
1209
+ window.initTable = initTable;
1210
+ }