@vii7/div-table-widget 1.1.0 → 1.1.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.
- package/README.md +129 -10
- package/dist/divtable.min.css +1 -0
- package/dist/divtable.min.js +1 -1
- package/package.json +12 -8
- package/src/div-table.css +251 -147
- package/src/div-table.js +131 -49
- 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();
|
|
@@ -2393,6 +2442,11 @@ class DivTable {
|
|
|
2393
2442
|
this.scrollBodyContainer.scrollLeft = scrollLeft;
|
|
2394
2443
|
});
|
|
2395
2444
|
}
|
|
2445
|
+
|
|
2446
|
+
// Adjust fixed body padding for horizontal scrollbar (after content is rendered)
|
|
2447
|
+
requestAnimationFrame(() => {
|
|
2448
|
+
this.adjustFixedBodyForHorizontalScrollbar();
|
|
2449
|
+
});
|
|
2396
2450
|
}
|
|
2397
2451
|
|
|
2398
2452
|
renderRegularRowsWithFixedColumns(dataToRender = this.filteredData) {
|
|
@@ -2802,14 +2856,19 @@ class DivTable {
|
|
|
2802
2856
|
// Mark as populated
|
|
2803
2857
|
row.dataset.populated = 'true';
|
|
2804
2858
|
|
|
2805
|
-
//
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
// Measure actual height and update estimate for future rows (only once when height increases)
|
|
2859
|
+
// Measure actual height and set it explicitly to prevent bouncing on re-renders
|
|
2860
|
+
// Use requestAnimationFrame to ensure DOM has updated
|
|
2809
2861
|
requestAnimationFrame(() => {
|
|
2810
2862
|
const actualHeight = row.offsetHeight;
|
|
2811
|
-
if (actualHeight >
|
|
2812
|
-
|
|
2863
|
+
if (actualHeight > 0) {
|
|
2864
|
+
// Set explicit height to prevent layout recalculation bouncing
|
|
2865
|
+
row.style.minHeight = `${actualHeight}px`;
|
|
2866
|
+
row.style.height = `${actualHeight}px`;
|
|
2867
|
+
|
|
2868
|
+
// Update estimate for future unpopulated rows
|
|
2869
|
+
if (actualHeight > this.estimatedRowHeight) {
|
|
2870
|
+
this.estimatedRowHeight = actualHeight;
|
|
2871
|
+
}
|
|
2813
2872
|
}
|
|
2814
2873
|
});
|
|
2815
2874
|
|
|
@@ -2918,31 +2977,38 @@ class DivTable {
|
|
|
2918
2977
|
fixedRow.dataset.populated = 'true';
|
|
2919
2978
|
scrollRow.dataset.populated = 'true';
|
|
2920
2979
|
|
|
2921
|
-
// Remove min-height constraints and synchronize row heights
|
|
2922
|
-
fixedRow.style.minHeight = '';
|
|
2923
|
-
scrollRow.style.minHeight = '';
|
|
2924
|
-
|
|
2925
2980
|
// Synchronize heights between fixed and scroll row parts after cell population
|
|
2981
|
+
// Use double requestAnimationFrame to ensure layout is fully complete
|
|
2926
2982
|
requestAnimationFrame(() => {
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2983
|
+
requestAnimationFrame(() => {
|
|
2984
|
+
// Get natural heights after cell content is fully rendered
|
|
2985
|
+
// Temporarily remove any height constraints to get true natural height
|
|
2986
|
+
const prevFixedHeight = fixedRow.style.height;
|
|
2987
|
+
const prevScrollHeight = scrollRow.style.height;
|
|
2988
|
+
const prevFixedMinHeight = fixedRow.style.minHeight;
|
|
2989
|
+
const prevScrollMinHeight = scrollRow.style.minHeight;
|
|
2990
|
+
|
|
2991
|
+
fixedRow.style.height = '';
|
|
2992
|
+
scrollRow.style.height = '';
|
|
2993
|
+
fixedRow.style.minHeight = '40px';
|
|
2994
|
+
scrollRow.style.minHeight = '40px';
|
|
2995
|
+
|
|
2996
|
+
// Force layout recalculation
|
|
2997
|
+
const fixedHeight = fixedRow.offsetHeight;
|
|
2998
|
+
const scrollHeight = scrollRow.offsetHeight;
|
|
2999
|
+
|
|
3000
|
+
// Set both to the maximum height to keep rows in sync
|
|
3001
|
+
const maxHeight = Math.max(fixedHeight, scrollHeight, 40); // Ensure minimum 44px
|
|
3002
|
+
fixedRow.style.minHeight = `${maxHeight}px`;
|
|
2938
3003
|
fixedRow.style.height = `${maxHeight}px`;
|
|
3004
|
+
scrollRow.style.minHeight = `${maxHeight}px`;
|
|
2939
3005
|
scrollRow.style.height = `${maxHeight}px`;
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
}
|
|
3006
|
+
|
|
3007
|
+
// Update estimate for future unpopulated rows
|
|
3008
|
+
if (maxHeight > this.estimatedRowHeight) {
|
|
3009
|
+
this.estimatedRowHeight = maxHeight;
|
|
3010
|
+
}
|
|
3011
|
+
});
|
|
2946
3012
|
});
|
|
2947
3013
|
|
|
2948
3014
|
// Update tab indexes after population
|
|
@@ -4115,10 +4181,15 @@ class DivTable {
|
|
|
4115
4181
|
updateInfoSection() {
|
|
4116
4182
|
if (!this.infoSection) return;
|
|
4117
4183
|
|
|
4118
|
-
|
|
4184
|
+
// For virtual scrolling, use the max of totalRecords and actual loaded data
|
|
4185
|
+
// This handles cases where loaded data exceeds the reported total
|
|
4186
|
+
const total = this.virtualScrolling
|
|
4187
|
+
? Math.max(this.totalRecords, this.data.length)
|
|
4188
|
+
: this.data.length;
|
|
4119
4189
|
const loaded = this.data.length;
|
|
4120
4190
|
const filtered = this.filteredData.length;
|
|
4121
|
-
|
|
4191
|
+
// Count only valid selected rows (detail records, not stale IDs)
|
|
4192
|
+
const selected = this.getValidSelectedCount();
|
|
4122
4193
|
|
|
4123
4194
|
// Clear existing content
|
|
4124
4195
|
this.infoSection.innerHTML = '';
|
|
@@ -4554,10 +4625,12 @@ class DivTable {
|
|
|
4554
4625
|
updateInfoSectionWithAnticipatedProgress() {
|
|
4555
4626
|
if (!this.infoSection || !this.virtualScrolling) return;
|
|
4556
4627
|
|
|
4557
|
-
|
|
4628
|
+
// Use max of totalRecords and actual loaded data to handle inconsistencies
|
|
4629
|
+
const total = Math.max(this.totalRecords, this.data.length);
|
|
4558
4630
|
const currentLoaded = this.data.length;
|
|
4559
4631
|
const filtered = this.filteredData.length;
|
|
4560
|
-
|
|
4632
|
+
// Count only valid selected rows (detail records, not stale IDs)
|
|
4633
|
+
const selected = this.getValidSelectedCount();
|
|
4561
4634
|
|
|
4562
4635
|
// Calculate anticipated progress (assume we'll get a full page of data)
|
|
4563
4636
|
const anticipatedLoaded = Math.min(currentLoaded + this.pageSize, total);
|
|
@@ -4835,6 +4908,15 @@ class DivTable {
|
|
|
4835
4908
|
return Array.from(this.selectedRows).map(id => this.findRowData(id)).filter(Boolean);
|
|
4836
4909
|
}
|
|
4837
4910
|
|
|
4911
|
+
/**
|
|
4912
|
+
* Get the count of selected rows that have valid data records
|
|
4913
|
+
* This excludes any stale selections (IDs that no longer exist in data)
|
|
4914
|
+
* @returns {number} The count of valid selected rows
|
|
4915
|
+
*/
|
|
4916
|
+
getValidSelectedCount() {
|
|
4917
|
+
return this.getSelectedRows().length;
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4838
4920
|
/**
|
|
4839
4921
|
* Toggle the filter to show only selected rows or all rows
|
|
4840
4922
|
* @param {boolean} [showOnlySelected] - Optional: explicitly set the filter state (true = show only selected, false = show all). If omitted, toggles current state.
|