@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/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();
@@ -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 = '#f3f4f6';
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
- // 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) {
2938
- fixedRow.style.height = `${maxHeight}px`;
2939
- scrollRow.style.height = `${maxHeight}px`;
2940
- }
2941
-
2942
- // Update estimated height for future rows
2943
- if (maxHeight > this.estimatedRowHeight) {
2944
- this.estimatedRowHeight = maxHeight;
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
- const total = this.virtualScrolling ? this.totalRecords : this.data.length;
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
- const selected = this.selectedRows.size;
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
- const total = this.totalRecords;
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
- const selected = this.selectedRows.size;
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.