@vii7/div-table-widget 1.1.0 → 1.2.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/README.md +300 -22
- package/dist/divtable-theme-dark.min.css +1 -0
- package/dist/divtable-theme-dark.min.js +1 -0
- package/dist/divtable.min.css +1 -0
- package/dist/divtable.min.js +1 -1
- package/package.json +15 -9
- package/src/div-table-theme-dark.css +104 -0
- package/src/div-table.css +258 -147
- package/src/div-table.js +133 -73
- package/dist/editor.worker.js +0 -1
- package/dist/json.worker.js +0 -1
- package/dist/ts.worker.js +0 -6
package/src/div-table.js
CHANGED
|
@@ -352,6 +352,32 @@ class DivTable {
|
|
|
352
352
|
this.scrollBodyContainer.scrollLeft = this.scrollHeaderContainer.scrollLeft;
|
|
353
353
|
requestAnimationFrame(() => { isSyncingScroll = false; });
|
|
354
354
|
});
|
|
355
|
+
|
|
356
|
+
// Adjust fixed body padding for horizontal scrollbar height
|
|
357
|
+
this.adjustFixedBodyForHorizontalScrollbar();
|
|
358
|
+
|
|
359
|
+
// Re-adjust on window resize
|
|
360
|
+
window.addEventListener('resize', () => {
|
|
361
|
+
this.adjustFixedBodyForHorizontalScrollbar();
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Adjust fixed body padding to account for horizontal scrollbar in scroll body
|
|
367
|
+
* This prevents row misalignment at the bottom when scrolled to the end
|
|
368
|
+
*/
|
|
369
|
+
adjustFixedBodyForHorizontalScrollbar() {
|
|
370
|
+
if (!this.fixedBodyContainer || !this.scrollBodyContainer) return;
|
|
371
|
+
|
|
372
|
+
// Calculate horizontal scrollbar height
|
|
373
|
+
const scrollbarHeight = this.scrollBodyContainer.offsetHeight - this.scrollBodyContainer.clientHeight;
|
|
374
|
+
|
|
375
|
+
// Add padding to fixed body to compensate for scrollbar
|
|
376
|
+
if (scrollbarHeight > 0) {
|
|
377
|
+
this.fixedBodyContainer.style.paddingBottom = `${scrollbarHeight}px`;
|
|
378
|
+
} else {
|
|
379
|
+
this.fixedBodyContainer.style.paddingBottom = '';
|
|
380
|
+
}
|
|
355
381
|
}
|
|
356
382
|
|
|
357
383
|
getEffectiveFixedColumnCount() {
|
|
@@ -721,8 +747,25 @@ class DivTable {
|
|
|
721
747
|
this.handleKeyDown(e);
|
|
722
748
|
});
|
|
723
749
|
|
|
724
|
-
|
|
725
|
-
|
|
750
|
+
// Track whether focus came from keyboard (Tab) vs mouse click
|
|
751
|
+
// Only auto-focus to first record on Tab navigation, not mouse clicks
|
|
752
|
+
let lastInputWasKeyboard = false;
|
|
753
|
+
|
|
754
|
+
bodyContainer.addEventListener('keydown', () => {
|
|
755
|
+
lastInputWasKeyboard = true;
|
|
756
|
+
}, { capture: true });
|
|
757
|
+
|
|
758
|
+
bodyContainer.addEventListener('mousedown', () => {
|
|
759
|
+
lastInputWasKeyboard = false;
|
|
760
|
+
}, { capture: true });
|
|
761
|
+
|
|
762
|
+
// Use focusin to detect when focus enters the container
|
|
763
|
+
bodyContainer.addEventListener('focusin', (e) => {
|
|
764
|
+
// Only auto-focus to first record if:
|
|
765
|
+
// 1. There's no currently focused row
|
|
766
|
+
// 2. Focus came from keyboard navigation (Tab), not mouse click
|
|
767
|
+
// This prevents auto-scrolling when clicking the scrollbar
|
|
768
|
+
if (!this.focusedRowId && lastInputWasKeyboard && e.target === bodyContainer) {
|
|
726
769
|
this.focusFirstRecord();
|
|
727
770
|
}
|
|
728
771
|
});
|
|
@@ -1288,15 +1331,17 @@ class DivTable {
|
|
|
1288
1331
|
}
|
|
1289
1332
|
});
|
|
1290
1333
|
|
|
1291
|
-
// Update group header checkbox states
|
|
1292
|
-
|
|
1334
|
+
// Update group header checkbox states (always check for group headers in DOM)
|
|
1335
|
+
const groupHeaders = this.bodyContainer.querySelectorAll('.div-table-row.group-header');
|
|
1336
|
+
if (groupHeaders.length > 0) {
|
|
1337
|
+
// Group headers exist, so groupByField must be set - get the groups
|
|
1293
1338
|
const groups = this.groupData(this.sortData(this.filteredData));
|
|
1294
1339
|
|
|
1295
|
-
|
|
1340
|
+
groupHeaders.forEach((groupRow) => {
|
|
1296
1341
|
const checkbox = groupRow.querySelector('input[type="checkbox"]');
|
|
1297
1342
|
if (!checkbox) return;
|
|
1298
1343
|
|
|
1299
|
-
// Find the group by matching the groupKey
|
|
1344
|
+
// Find the group by matching the groupKey
|
|
1300
1345
|
const groupKey = groupRow.dataset.groupKey;
|
|
1301
1346
|
const group = groups.find(g => g.key === groupKey);
|
|
1302
1347
|
if (!group) return;
|
|
@@ -1305,15 +1350,16 @@ class DivTable {
|
|
|
1305
1350
|
const groupItemIds = group.items.map(item => String(item[this.primaryKeyField]));
|
|
1306
1351
|
const selectedInGroup = groupItemIds.filter(id => this.selectedRows.has(id));
|
|
1307
1352
|
|
|
1353
|
+
// Set indeterminate BEFORE checked to ensure proper visual update
|
|
1308
1354
|
if (selectedInGroup.length === 0) {
|
|
1309
|
-
checkbox.checked = false;
|
|
1310
1355
|
checkbox.indeterminate = false;
|
|
1356
|
+
checkbox.checked = false;
|
|
1311
1357
|
} else if (selectedInGroup.length === groupItemIds.length) {
|
|
1312
|
-
checkbox.checked = true;
|
|
1313
1358
|
checkbox.indeterminate = false;
|
|
1359
|
+
checkbox.checked = true;
|
|
1314
1360
|
} else {
|
|
1315
|
-
checkbox.checked = false;
|
|
1316
1361
|
checkbox.indeterminate = true;
|
|
1362
|
+
checkbox.checked = false;
|
|
1317
1363
|
}
|
|
1318
1364
|
});
|
|
1319
1365
|
}
|
|
@@ -1351,11 +1397,13 @@ class DivTable {
|
|
|
1351
1397
|
}
|
|
1352
1398
|
});
|
|
1353
1399
|
|
|
1354
|
-
// Update group header checkbox states
|
|
1355
|
-
|
|
1400
|
+
// Update group header checkbox states (always check for group headers in DOM)
|
|
1401
|
+
const groupHeaders = this.fixedBodyContainer.querySelectorAll('.div-table-row.group-header');
|
|
1402
|
+
if (groupHeaders.length > 0) {
|
|
1403
|
+
// Group headers exist, so groupByField must be set - get the groups
|
|
1356
1404
|
const groups = this.groupData(this.sortData(this.filteredData));
|
|
1357
1405
|
|
|
1358
|
-
|
|
1406
|
+
groupHeaders.forEach((groupRow) => {
|
|
1359
1407
|
const checkbox = groupRow.querySelector('input[type="checkbox"]');
|
|
1360
1408
|
if (!checkbox) return;
|
|
1361
1409
|
|
|
@@ -1366,15 +1414,16 @@ class DivTable {
|
|
|
1366
1414
|
const groupItemIds = group.items.map(item => String(item[this.primaryKeyField]));
|
|
1367
1415
|
const selectedInGroup = groupItemIds.filter(id => this.selectedRows.has(id));
|
|
1368
1416
|
|
|
1417
|
+
// Set indeterminate BEFORE checked to ensure proper visual update
|
|
1369
1418
|
if (selectedInGroup.length === 0) {
|
|
1370
|
-
checkbox.checked = false;
|
|
1371
1419
|
checkbox.indeterminate = false;
|
|
1420
|
+
checkbox.checked = false;
|
|
1372
1421
|
} else if (selectedInGroup.length === groupItemIds.length) {
|
|
1373
|
-
checkbox.checked = true;
|
|
1374
1422
|
checkbox.indeterminate = false;
|
|
1423
|
+
checkbox.checked = true;
|
|
1375
1424
|
} else {
|
|
1376
|
-
checkbox.checked = false;
|
|
1377
1425
|
checkbox.indeterminate = true;
|
|
1426
|
+
checkbox.checked = false;
|
|
1378
1427
|
}
|
|
1379
1428
|
});
|
|
1380
1429
|
}
|
|
@@ -1512,8 +1561,8 @@ class DivTable {
|
|
|
1512
1561
|
const checkbox = document.createElement('input');
|
|
1513
1562
|
checkbox.type = 'checkbox';
|
|
1514
1563
|
checkbox.addEventListener('change', (e) => {
|
|
1515
|
-
if (e.target.checked
|
|
1516
|
-
// If checked
|
|
1564
|
+
if (e.target.checked) {
|
|
1565
|
+
// If checked, select all
|
|
1517
1566
|
this.selectAll();
|
|
1518
1567
|
} else {
|
|
1519
1568
|
// If unchecked, clear selection
|
|
@@ -1594,7 +1643,7 @@ class DivTable {
|
|
|
1594
1643
|
const checkbox = document.createElement('input');
|
|
1595
1644
|
checkbox.type = 'checkbox';
|
|
1596
1645
|
checkbox.addEventListener('change', (e) => {
|
|
1597
|
-
if (e.target.checked
|
|
1646
|
+
if (e.target.checked) {
|
|
1598
1647
|
this.selectAll();
|
|
1599
1648
|
} else {
|
|
1600
1649
|
this.clearSelection();
|
|
@@ -1663,12 +1712,21 @@ class DivTable {
|
|
|
1663
1712
|
fixedRow.style.height = '';
|
|
1664
1713
|
scrollRow.style.height = '';
|
|
1665
1714
|
|
|
1666
|
-
// Get natural heights
|
|
1667
|
-
const fixedHeight = fixedRow.offsetHeight;
|
|
1668
|
-
const scrollHeight = scrollRow.offsetHeight;
|
|
1715
|
+
// Get natural heights including any cell content overflow
|
|
1716
|
+
const fixedHeight = Math.max(fixedRow.offsetHeight, fixedRow.scrollHeight);
|
|
1717
|
+
const scrollHeight = Math.max(scrollRow.offsetHeight, scrollRow.scrollHeight);
|
|
1718
|
+
|
|
1719
|
+
// Also check individual cell heights
|
|
1720
|
+
let maxCellHeight = 0;
|
|
1721
|
+
fixedRow.querySelectorAll('.div-table-cell').forEach(cell => {
|
|
1722
|
+
maxCellHeight = Math.max(maxCellHeight, cell.offsetHeight, cell.scrollHeight);
|
|
1723
|
+
});
|
|
1724
|
+
scrollRow.querySelectorAll('.div-table-cell').forEach(cell => {
|
|
1725
|
+
maxCellHeight = Math.max(maxCellHeight, cell.offsetHeight, cell.scrollHeight);
|
|
1726
|
+
});
|
|
1669
1727
|
|
|
1670
1728
|
// Set both to the maximum height
|
|
1671
|
-
const maxHeight = Math.max(fixedHeight, scrollHeight);
|
|
1729
|
+
const maxHeight = Math.max(fixedHeight, scrollHeight, maxCellHeight);
|
|
1672
1730
|
if (maxHeight > 0) {
|
|
1673
1731
|
fixedRow.style.height = `${maxHeight}px`;
|
|
1674
1732
|
scrollRow.style.height = `${maxHeight}px`;
|
|
@@ -1844,7 +1902,6 @@ class DivTable {
|
|
|
1844
1902
|
mainLabel.className = 'composite-main-header';
|
|
1845
1903
|
mainLabel.innerHTML = col.label || col.field;
|
|
1846
1904
|
mainLabel.style.fontWeight = '600';
|
|
1847
|
-
mainLabel.style.color = '#374151';
|
|
1848
1905
|
mainLabel.style.textAlign = 'left';
|
|
1849
1906
|
mainLabel.style.flex = '1';
|
|
1850
1907
|
mainLabelContainer.appendChild(mainLabel);
|
|
@@ -1911,7 +1968,6 @@ class DivTable {
|
|
|
1911
1968
|
|
|
1912
1969
|
const subLabel = document.createElement('span');
|
|
1913
1970
|
subLabel.innerHTML = col.subLabel;
|
|
1914
|
-
subLabel.style.color = '#6b7280';
|
|
1915
1971
|
subLabel.style.textAlign = 'left';
|
|
1916
1972
|
subLabel.style.flex = '1';
|
|
1917
1973
|
subLabelContainer.appendChild(subLabel);
|
|
@@ -1931,9 +1987,9 @@ class DivTable {
|
|
|
1931
1987
|
|
|
1932
1988
|
subLabelContainer.appendChild(subSortIndicator);
|
|
1933
1989
|
|
|
1934
|
-
// Add hover effect
|
|
1990
|
+
// Add hover effect - use CSS variable for theming support
|
|
1935
1991
|
subLabelContainer.addEventListener('mouseenter', () => {
|
|
1936
|
-
subLabelContainer.style.backgroundColor = '
|
|
1992
|
+
subLabelContainer.style.backgroundColor = 'var(--dt-bg-disabled)';
|
|
1937
1993
|
});
|
|
1938
1994
|
subLabelContainer.addEventListener('mouseleave', () => {
|
|
1939
1995
|
subLabelContainer.style.backgroundColor = 'transparent';
|
|
@@ -2393,6 +2449,11 @@ class DivTable {
|
|
|
2393
2449
|
this.scrollBodyContainer.scrollLeft = scrollLeft;
|
|
2394
2450
|
});
|
|
2395
2451
|
}
|
|
2452
|
+
|
|
2453
|
+
// Adjust fixed body padding for horizontal scrollbar (after content is rendered)
|
|
2454
|
+
requestAnimationFrame(() => {
|
|
2455
|
+
this.adjustFixedBodyForHorizontalScrollbar();
|
|
2456
|
+
});
|
|
2396
2457
|
}
|
|
2397
2458
|
|
|
2398
2459
|
renderRegularRowsWithFixedColumns(dataToRender = this.filteredData) {
|
|
@@ -2711,9 +2772,6 @@ class DivTable {
|
|
|
2711
2772
|
|
|
2712
2773
|
if (composite.compositeName) {
|
|
2713
2774
|
cell.classList.add('composite-cell');
|
|
2714
|
-
cell.style.display = 'flex';
|
|
2715
|
-
cell.style.flexDirection = 'column';
|
|
2716
|
-
cell.style.gap = '4px';
|
|
2717
2775
|
|
|
2718
2776
|
composite.columns.forEach((col, index) => {
|
|
2719
2777
|
const subCell = document.createElement('div');
|
|
@@ -2802,17 +2860,6 @@ class DivTable {
|
|
|
2802
2860
|
// Mark as populated
|
|
2803
2861
|
row.dataset.populated = 'true';
|
|
2804
2862
|
|
|
2805
|
-
// Remove min-height constraint and update estimated height
|
|
2806
|
-
row.style.minHeight = '';
|
|
2807
|
-
|
|
2808
|
-
// Measure actual height and update estimate for future rows (only once when height increases)
|
|
2809
|
-
requestAnimationFrame(() => {
|
|
2810
|
-
const actualHeight = row.offsetHeight;
|
|
2811
|
-
if (actualHeight > this.estimatedRowHeight) {
|
|
2812
|
-
this.estimatedRowHeight = actualHeight;
|
|
2813
|
-
}
|
|
2814
|
-
});
|
|
2815
|
-
|
|
2816
2863
|
// Update tab indexes after population
|
|
2817
2864
|
this.updateTabIndexes();
|
|
2818
2865
|
}
|
|
@@ -2918,31 +2965,34 @@ class DivTable {
|
|
|
2918
2965
|
fixedRow.dataset.populated = 'true';
|
|
2919
2966
|
scrollRow.dataset.populated = 'true';
|
|
2920
2967
|
|
|
2921
|
-
// Remove min-height constraints and synchronize row heights
|
|
2922
|
-
fixedRow.style.minHeight = '';
|
|
2923
|
-
scrollRow.style.minHeight = '';
|
|
2924
|
-
|
|
2925
2968
|
// Synchronize heights between fixed and scroll row parts after cell population
|
|
2969
|
+
// Use double requestAnimationFrame to ensure layout is fully complete
|
|
2926
2970
|
requestAnimationFrame(() => {
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2971
|
+
requestAnimationFrame(() => {
|
|
2972
|
+
// Reset any fixed heights to get natural content height
|
|
2973
|
+
fixedRow.style.height = '';
|
|
2974
|
+
scrollRow.style.height = '';
|
|
2975
|
+
|
|
2976
|
+
// Get the maximum height from both rows, including any cell content
|
|
2977
|
+
// Use scrollHeight to capture content that might overflow
|
|
2978
|
+
const fixedHeight = Math.max(fixedRow.offsetHeight, fixedRow.scrollHeight);
|
|
2979
|
+
const scrollHeight = Math.max(scrollRow.offsetHeight, scrollRow.scrollHeight);
|
|
2980
|
+
|
|
2981
|
+
// Also check individual cell heights
|
|
2982
|
+
let maxCellHeight = 0;
|
|
2983
|
+
fixedRow.querySelectorAll('.div-table-cell').forEach(cell => {
|
|
2984
|
+
maxCellHeight = Math.max(maxCellHeight, cell.offsetHeight, cell.scrollHeight);
|
|
2985
|
+
});
|
|
2986
|
+
scrollRow.querySelectorAll('.div-table-cell').forEach(cell => {
|
|
2987
|
+
maxCellHeight = Math.max(maxCellHeight, cell.offsetHeight, cell.scrollHeight);
|
|
2988
|
+
});
|
|
2989
|
+
|
|
2990
|
+
const maxHeight = Math.max(fixedHeight, scrollHeight, maxCellHeight);
|
|
2991
|
+
if (maxHeight > 0) {
|
|
2992
|
+
fixedRow.style.height = `${maxHeight}px`;
|
|
2993
|
+
scrollRow.style.height = `${maxHeight}px`;
|
|
2994
|
+
}
|
|
2995
|
+
});
|
|
2946
2996
|
});
|
|
2947
2997
|
|
|
2948
2998
|
// Update tab indexes after population
|
|
@@ -3140,9 +3190,6 @@ class DivTable {
|
|
|
3140
3190
|
if (composite.compositeName) {
|
|
3141
3191
|
// Composite cell with multiple columns stacked vertically
|
|
3142
3192
|
cell.classList.add('composite-cell');
|
|
3143
|
-
cell.style.display = 'flex';
|
|
3144
|
-
cell.style.flexDirection = 'column';
|
|
3145
|
-
cell.style.gap = '4px';
|
|
3146
3193
|
|
|
3147
3194
|
composite.columns.forEach((col, index) => {
|
|
3148
3195
|
const subCell = document.createElement('div');
|
|
@@ -3520,9 +3567,6 @@ class DivTable {
|
|
|
3520
3567
|
if (composite.compositeName) {
|
|
3521
3568
|
// Composite cell with multiple columns stacked vertically
|
|
3522
3569
|
cell.classList.add('composite-cell');
|
|
3523
|
-
cell.style.display = 'flex';
|
|
3524
|
-
cell.style.flexDirection = 'column';
|
|
3525
|
-
cell.style.gap = '4px';
|
|
3526
3570
|
|
|
3527
3571
|
composite.columns.forEach((col, index) => {
|
|
3528
3572
|
const subCell = document.createElement('div');
|
|
@@ -4115,10 +4159,15 @@ class DivTable {
|
|
|
4115
4159
|
updateInfoSection() {
|
|
4116
4160
|
if (!this.infoSection) return;
|
|
4117
4161
|
|
|
4118
|
-
|
|
4162
|
+
// For virtual scrolling, use the max of totalRecords and actual loaded data
|
|
4163
|
+
// This handles cases where loaded data exceeds the reported total
|
|
4164
|
+
const total = this.virtualScrolling
|
|
4165
|
+
? Math.max(this.totalRecords, this.data.length)
|
|
4166
|
+
: this.data.length;
|
|
4119
4167
|
const loaded = this.data.length;
|
|
4120
4168
|
const filtered = this.filteredData.length;
|
|
4121
|
-
|
|
4169
|
+
// Count only valid selected rows (detail records, not stale IDs)
|
|
4170
|
+
const selected = this.getValidSelectedCount();
|
|
4122
4171
|
|
|
4123
4172
|
// Clear existing content
|
|
4124
4173
|
this.infoSection.innerHTML = '';
|
|
@@ -4554,10 +4603,12 @@ class DivTable {
|
|
|
4554
4603
|
updateInfoSectionWithAnticipatedProgress() {
|
|
4555
4604
|
if (!this.infoSection || !this.virtualScrolling) return;
|
|
4556
4605
|
|
|
4557
|
-
|
|
4606
|
+
// Use max of totalRecords and actual loaded data to handle inconsistencies
|
|
4607
|
+
const total = Math.max(this.totalRecords, this.data.length);
|
|
4558
4608
|
const currentLoaded = this.data.length;
|
|
4559
4609
|
const filtered = this.filteredData.length;
|
|
4560
|
-
|
|
4610
|
+
// Count only valid selected rows (detail records, not stale IDs)
|
|
4611
|
+
const selected = this.getValidSelectedCount();
|
|
4561
4612
|
|
|
4562
4613
|
// Calculate anticipated progress (assume we'll get a full page of data)
|
|
4563
4614
|
const anticipatedLoaded = Math.min(currentLoaded + this.pageSize, total);
|
|
@@ -4835,6 +4886,15 @@ class DivTable {
|
|
|
4835
4886
|
return Array.from(this.selectedRows).map(id => this.findRowData(id)).filter(Boolean);
|
|
4836
4887
|
}
|
|
4837
4888
|
|
|
4889
|
+
/**
|
|
4890
|
+
* Get the count of selected rows that have valid data records
|
|
4891
|
+
* This excludes any stale selections (IDs that no longer exist in data)
|
|
4892
|
+
* @returns {number} The count of valid selected rows
|
|
4893
|
+
*/
|
|
4894
|
+
getValidSelectedCount() {
|
|
4895
|
+
return this.getSelectedRows().length;
|
|
4896
|
+
}
|
|
4897
|
+
|
|
4838
4898
|
/**
|
|
4839
4899
|
* Toggle the filter to show only selected rows or all rows
|
|
4840
4900
|
* @param {boolean} [showOnlySelected] - Optional: explicitly set the filter state (true = show only selected, false = show all). If omitted, toggles current state.
|