@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/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
- bodyContainer.addEventListener('focus', () => {
725
- if (!this.focusedRowId) {
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
- if (this.groupByField) {
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
- this.bodyContainer.querySelectorAll('.div-table-row.group-header').forEach((groupRow) => {
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 instead of relying on index
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
- if (this.groupByField) {
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
- this.fixedBodyContainer.querySelectorAll('.div-table-row.group-header').forEach((groupRow) => {
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 || e.target.indeterminate) {
1516
- // If checked or indeterminate, select all
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 || e.target.indeterminate) {
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
- // 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)
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 > this.estimatedRowHeight) {
2812
- this.estimatedRowHeight = actualHeight;
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
- // Reset any previously set explicit heights
2928
- fixedRow.style.height = '';
2929
- scrollRow.style.height = '';
2930
-
2931
- // Get natural heights after cell content is rendered
2932
- const fixedHeight = fixedRow.offsetHeight;
2933
- const scrollHeight = scrollRow.offsetHeight;
2934
-
2935
- // Set both to the maximum height
2936
- const maxHeight = Math.max(fixedHeight, scrollHeight);
2937
- if (maxHeight > 0) {
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
- // Update estimated height for future rows
2943
- if (maxHeight > this.estimatedRowHeight) {
2944
- this.estimatedRowHeight = maxHeight;
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
- const total = this.virtualScrolling ? this.totalRecords : this.data.length;
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
- const selected = this.selectedRows.size;
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
- const total = this.totalRecords;
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
- const selected = this.selectedRows.size;
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.