@vii7/div-table-widget 1.0.1 → 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 +237 -11
- package/dist/divtable.min.css +1 -0
- package/dist/divtable.min.js +1 -1
- package/package.json +12 -8
- package/src/div-table.css +317 -137
- package/src/div-table.js +1535 -290
- 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
|
@@ -82,6 +82,17 @@ class DivTable {
|
|
|
82
82
|
// Fixed columns option (frozen left columns)
|
|
83
83
|
this.fixedColumns = options.fixedColumns || 0;
|
|
84
84
|
|
|
85
|
+
// Lazy cell rendering option (for performance optimization)
|
|
86
|
+
this.lazyCellRendering = options.lazyCellRendering !== false; // Enabled by default
|
|
87
|
+
this.lazyRenderMargin = options.lazyRenderMargin || '200px'; // Pre-render rows slightly before visible
|
|
88
|
+
this.rowObserver = null; // IntersectionObserver for lazy rendering
|
|
89
|
+
|
|
90
|
+
// Aggregate summary row options
|
|
91
|
+
// Header summary shows totals for entire dataset (grand total)
|
|
92
|
+
// Group summary shows totals per group when data is grouped
|
|
93
|
+
this.showHeaderSummary = options.showHeaderSummary || false;
|
|
94
|
+
this.showGroupSummary = options.showGroupSummary || false;
|
|
95
|
+
|
|
85
96
|
// Find primary key field first
|
|
86
97
|
this.primaryKeyField = this.columns.find(col => col.primaryKey)?.field || 'id';
|
|
87
98
|
|
|
@@ -341,6 +352,32 @@ class DivTable {
|
|
|
341
352
|
this.scrollBodyContainer.scrollLeft = this.scrollHeaderContainer.scrollLeft;
|
|
342
353
|
requestAnimationFrame(() => { isSyncingScroll = false; });
|
|
343
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
|
+
}
|
|
344
381
|
}
|
|
345
382
|
|
|
346
383
|
getEffectiveFixedColumnCount() {
|
|
@@ -710,8 +747,25 @@ class DivTable {
|
|
|
710
747
|
this.handleKeyDown(e);
|
|
711
748
|
});
|
|
712
749
|
|
|
713
|
-
|
|
714
|
-
|
|
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) {
|
|
715
769
|
this.focusFirstRecord();
|
|
716
770
|
}
|
|
717
771
|
});
|
|
@@ -1277,15 +1331,17 @@ class DivTable {
|
|
|
1277
1331
|
}
|
|
1278
1332
|
});
|
|
1279
1333
|
|
|
1280
|
-
// Update group header checkbox states
|
|
1281
|
-
|
|
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
|
|
1282
1338
|
const groups = this.groupData(this.sortData(this.filteredData));
|
|
1283
1339
|
|
|
1284
|
-
|
|
1340
|
+
groupHeaders.forEach((groupRow) => {
|
|
1285
1341
|
const checkbox = groupRow.querySelector('input[type="checkbox"]');
|
|
1286
1342
|
if (!checkbox) return;
|
|
1287
1343
|
|
|
1288
|
-
// Find the group by matching the groupKey
|
|
1344
|
+
// Find the group by matching the groupKey
|
|
1289
1345
|
const groupKey = groupRow.dataset.groupKey;
|
|
1290
1346
|
const group = groups.find(g => g.key === groupKey);
|
|
1291
1347
|
if (!group) return;
|
|
@@ -1294,21 +1350,25 @@ class DivTable {
|
|
|
1294
1350
|
const groupItemIds = group.items.map(item => String(item[this.primaryKeyField]));
|
|
1295
1351
|
const selectedInGroup = groupItemIds.filter(id => this.selectedRows.has(id));
|
|
1296
1352
|
|
|
1353
|
+
// Set indeterminate BEFORE checked to ensure proper visual update
|
|
1297
1354
|
if (selectedInGroup.length === 0) {
|
|
1298
|
-
checkbox.checked = false;
|
|
1299
1355
|
checkbox.indeterminate = false;
|
|
1356
|
+
checkbox.checked = false;
|
|
1300
1357
|
} else if (selectedInGroup.length === groupItemIds.length) {
|
|
1301
|
-
checkbox.checked = true;
|
|
1302
1358
|
checkbox.indeterminate = false;
|
|
1359
|
+
checkbox.checked = true;
|
|
1303
1360
|
} else {
|
|
1304
|
-
checkbox.checked = false;
|
|
1305
1361
|
checkbox.indeterminate = true;
|
|
1362
|
+
checkbox.checked = false;
|
|
1306
1363
|
}
|
|
1307
1364
|
});
|
|
1308
1365
|
}
|
|
1309
1366
|
|
|
1310
1367
|
// Update main header checkbox state
|
|
1311
1368
|
this.updateHeaderCheckbox();
|
|
1369
|
+
|
|
1370
|
+
// Update summary row aggregates based on current selection
|
|
1371
|
+
this.updateSummaryRows();
|
|
1312
1372
|
}
|
|
1313
1373
|
|
|
1314
1374
|
updateSelectionStatesWithFixedColumns() {
|
|
@@ -1337,11 +1397,13 @@ class DivTable {
|
|
|
1337
1397
|
}
|
|
1338
1398
|
});
|
|
1339
1399
|
|
|
1340
|
-
// Update group header checkbox states
|
|
1341
|
-
|
|
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
|
|
1342
1404
|
const groups = this.groupData(this.sortData(this.filteredData));
|
|
1343
1405
|
|
|
1344
|
-
|
|
1406
|
+
groupHeaders.forEach((groupRow) => {
|
|
1345
1407
|
const checkbox = groupRow.querySelector('input[type="checkbox"]');
|
|
1346
1408
|
if (!checkbox) return;
|
|
1347
1409
|
|
|
@@ -1352,21 +1414,25 @@ class DivTable {
|
|
|
1352
1414
|
const groupItemIds = group.items.map(item => String(item[this.primaryKeyField]));
|
|
1353
1415
|
const selectedInGroup = groupItemIds.filter(id => this.selectedRows.has(id));
|
|
1354
1416
|
|
|
1417
|
+
// Set indeterminate BEFORE checked to ensure proper visual update
|
|
1355
1418
|
if (selectedInGroup.length === 0) {
|
|
1356
|
-
checkbox.checked = false;
|
|
1357
1419
|
checkbox.indeterminate = false;
|
|
1420
|
+
checkbox.checked = false;
|
|
1358
1421
|
} else if (selectedInGroup.length === groupItemIds.length) {
|
|
1359
|
-
checkbox.checked = true;
|
|
1360
1422
|
checkbox.indeterminate = false;
|
|
1423
|
+
checkbox.checked = true;
|
|
1361
1424
|
} else {
|
|
1362
|
-
checkbox.checked = false;
|
|
1363
1425
|
checkbox.indeterminate = true;
|
|
1426
|
+
checkbox.checked = false;
|
|
1364
1427
|
}
|
|
1365
1428
|
});
|
|
1366
1429
|
}
|
|
1367
1430
|
|
|
1368
1431
|
// Update main header checkbox state
|
|
1369
1432
|
this.updateHeaderCheckbox();
|
|
1433
|
+
|
|
1434
|
+
// Update summary row aggregates based on current selection
|
|
1435
|
+
this.updateSummaryRows();
|
|
1370
1436
|
}
|
|
1371
1437
|
|
|
1372
1438
|
updateHeaderCheckbox() {
|
|
@@ -1495,8 +1561,8 @@ class DivTable {
|
|
|
1495
1561
|
const checkbox = document.createElement('input');
|
|
1496
1562
|
checkbox.type = 'checkbox';
|
|
1497
1563
|
checkbox.addEventListener('change', (e) => {
|
|
1498
|
-
if (e.target.checked
|
|
1499
|
-
// If checked
|
|
1564
|
+
if (e.target.checked) {
|
|
1565
|
+
// If checked, select all
|
|
1500
1566
|
this.selectAll();
|
|
1501
1567
|
} else {
|
|
1502
1568
|
// If unchecked, clear selection
|
|
@@ -1577,7 +1643,7 @@ class DivTable {
|
|
|
1577
1643
|
const checkbox = document.createElement('input');
|
|
1578
1644
|
checkbox.type = 'checkbox';
|
|
1579
1645
|
checkbox.addEventListener('change', (e) => {
|
|
1580
|
-
if (e.target.checked
|
|
1646
|
+
if (e.target.checked) {
|
|
1581
1647
|
this.selectAll();
|
|
1582
1648
|
} else {
|
|
1583
1649
|
this.clearSelection();
|
|
@@ -1715,6 +1781,10 @@ class DivTable {
|
|
|
1715
1781
|
// Group headers span all columns - set explicit width to match total
|
|
1716
1782
|
row.style.gridTemplateColumns = '1fr';
|
|
1717
1783
|
row.style.minWidth = `${totalWidth}px`;
|
|
1784
|
+
} else if (row.classList.contains('summary-row')) {
|
|
1785
|
+
// Summary rows need the same grid template as regular rows
|
|
1786
|
+
row.style.gridTemplateColumns = gridTemplate;
|
|
1787
|
+
row.style.minWidth = `${totalWidth}px`;
|
|
1718
1788
|
} else {
|
|
1719
1789
|
row.style.gridTemplateColumns = gridTemplate;
|
|
1720
1790
|
}
|
|
@@ -2239,6 +2309,11 @@ class DivTable {
|
|
|
2239
2309
|
}
|
|
2240
2310
|
|
|
2241
2311
|
renderBody() {
|
|
2312
|
+
// Disconnect lazy rendering observer before clearing content
|
|
2313
|
+
if (this.rowObserver) {
|
|
2314
|
+
this.rowObserver.disconnect();
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2242
2317
|
// Handle fixed columns layout
|
|
2243
2318
|
if (this.fixedColumns > 0) {
|
|
2244
2319
|
this.renderBodyWithFixedColumns();
|
|
@@ -2284,9 +2359,24 @@ class DivTable {
|
|
|
2284
2359
|
} else {
|
|
2285
2360
|
this.renderRegularRows(dataToRender);
|
|
2286
2361
|
}
|
|
2362
|
+
|
|
2363
|
+
// Add header summary row if enabled and has aggregate columns
|
|
2364
|
+
if (this.showHeaderSummary && this.hasAggregateColumns()) {
|
|
2365
|
+
const summaryRow = this.createHeaderSummaryRow(dataToRender);
|
|
2366
|
+
// Insert at the beginning of body container (after header)
|
|
2367
|
+
this.bodyContainer.insertBefore(summaryRow, this.bodyContainer.firstChild);
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
// Setup lazy cell rendering observer if enabled
|
|
2371
|
+
if (this.lazyCellRendering) {
|
|
2372
|
+
this.setupLazyRenderingObserver();
|
|
2373
|
+
}
|
|
2287
2374
|
}
|
|
2288
2375
|
|
|
2289
2376
|
renderBodyWithFixedColumns() {
|
|
2377
|
+
// Preserve horizontal scroll position before clearing
|
|
2378
|
+
const scrollLeft = this.scrollBodyContainer?.scrollLeft || 0;
|
|
2379
|
+
|
|
2290
2380
|
this.fixedBodyContainer.innerHTML = '';
|
|
2291
2381
|
this.scrollBodyContainer.innerHTML = '';
|
|
2292
2382
|
|
|
@@ -2327,11 +2417,36 @@ class DivTable {
|
|
|
2327
2417
|
this.renderRegularRowsWithFixedColumns(dataToRender);
|
|
2328
2418
|
}
|
|
2329
2419
|
|
|
2420
|
+
// Add header summary row if enabled and has aggregate columns
|
|
2421
|
+
if (this.showHeaderSummary && this.hasAggregateColumns()) {
|
|
2422
|
+
const { fixedSummary, scrollSummary } = this.createHeaderSummaryRowWithFixedColumns(dataToRender);
|
|
2423
|
+
// Insert at the beginning of body containers (after header)
|
|
2424
|
+
this.fixedBodyContainer.insertBefore(fixedSummary, this.fixedBodyContainer.firstChild);
|
|
2425
|
+
this.scrollBodyContainer.insertBefore(scrollSummary, this.scrollBodyContainer.firstChild);
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2330
2428
|
// Synchronize column widths in scroll section (must be done before row heights)
|
|
2331
2429
|
this.syncFixedColumnsColumnWidths();
|
|
2332
2430
|
|
|
2333
2431
|
// Synchronize row heights between fixed and scroll sections
|
|
2334
2432
|
this.syncFixedColumnsRowHeights();
|
|
2433
|
+
|
|
2434
|
+
// Setup lazy cell rendering observer if enabled
|
|
2435
|
+
if (this.lazyCellRendering) {
|
|
2436
|
+
this.setupLazyRenderingObserver();
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
// Restore horizontal scroll position after rendering
|
|
2440
|
+
if (scrollLeft > 0) {
|
|
2441
|
+
requestAnimationFrame(() => {
|
|
2442
|
+
this.scrollBodyContainer.scrollLeft = scrollLeft;
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// Adjust fixed body padding for horizontal scrollbar (after content is rendered)
|
|
2447
|
+
requestAnimationFrame(() => {
|
|
2448
|
+
this.adjustFixedBodyForHorizontalScrollbar();
|
|
2449
|
+
});
|
|
2335
2450
|
}
|
|
2336
2451
|
|
|
2337
2452
|
renderRegularRowsWithFixedColumns(dataToRender = this.filteredData) {
|
|
@@ -2388,6 +2503,115 @@ class DivTable {
|
|
|
2388
2503
|
this.scrollBodyContainer.appendChild(scrollRow);
|
|
2389
2504
|
});
|
|
2390
2505
|
}
|
|
2506
|
+
|
|
2507
|
+
// Add group summary row after group (visible even when collapsed)
|
|
2508
|
+
if (this.showGroupSummary && this.hasAggregateColumns()) {
|
|
2509
|
+
const { fixedSummary, scrollSummary } = this.createGroupSummaryRowWithFixedColumns(group);
|
|
2510
|
+
this.fixedBodyContainer.appendChild(fixedSummary);
|
|
2511
|
+
this.scrollBodyContainer.appendChild(scrollSummary);
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
/**
|
|
2517
|
+
* Setup IntersectionObserver for lazy cell rendering
|
|
2518
|
+
* Observes unpopulated rows and populates them when they enter the viewport
|
|
2519
|
+
*/
|
|
2520
|
+
setupLazyRenderingObserver() {
|
|
2521
|
+
// Disconnect existing observer if any
|
|
2522
|
+
if (this.rowObserver) {
|
|
2523
|
+
this.rowObserver.disconnect();
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
// Check for IntersectionObserver support
|
|
2527
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
2528
|
+
// Fallback: populate all rows immediately
|
|
2529
|
+
console.warn('DivTable: IntersectionObserver not supported, falling back to eager rendering');
|
|
2530
|
+
this.populateAllUnpopulatedRows();
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
// Determine the root element for observation
|
|
2535
|
+
const rootElement = this.fixedColumns > 0
|
|
2536
|
+
? this.scrollBodyContainer
|
|
2537
|
+
: this.bodyContainer;
|
|
2538
|
+
|
|
2539
|
+
// Create observer with margin to pre-render rows slightly before visible
|
|
2540
|
+
this.rowObserver = new IntersectionObserver((entries) => {
|
|
2541
|
+
entries.forEach(entry => {
|
|
2542
|
+
if (entry.isIntersecting) {
|
|
2543
|
+
const row = entry.target;
|
|
2544
|
+
|
|
2545
|
+
// Only populate if not already done
|
|
2546
|
+
if (row.dataset.populated !== 'true') {
|
|
2547
|
+
const rowId = row.dataset.id;
|
|
2548
|
+
const item = this.findRowData(rowId);
|
|
2549
|
+
|
|
2550
|
+
if (item) {
|
|
2551
|
+
// Handle fixed columns layout
|
|
2552
|
+
if (this.fixedColumns > 0) {
|
|
2553
|
+
// Find the corresponding row in the other container
|
|
2554
|
+
const isFixedRow = row.classList.contains('div-table-fixed-row');
|
|
2555
|
+
let fixedRow, scrollRow;
|
|
2556
|
+
|
|
2557
|
+
if (isFixedRow) {
|
|
2558
|
+
fixedRow = row;
|
|
2559
|
+
scrollRow = this.scrollBodyContainer.querySelector(`.div-table-row[data-id="${rowId}"]`);
|
|
2560
|
+
} else {
|
|
2561
|
+
scrollRow = row;
|
|
2562
|
+
fixedRow = this.fixedBodyContainer.querySelector(`.div-table-row[data-id="${rowId}"]`);
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
if (fixedRow && scrollRow) {
|
|
2566
|
+
this.populateRowCellsWithFixedColumns(fixedRow, scrollRow, item);
|
|
2567
|
+
}
|
|
2568
|
+
} else {
|
|
2569
|
+
this.populateRowCells(row, item);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
// Unobserve after population
|
|
2575
|
+
this.rowObserver.unobserve(row);
|
|
2576
|
+
}
|
|
2577
|
+
});
|
|
2578
|
+
}, {
|
|
2579
|
+
root: rootElement,
|
|
2580
|
+
rootMargin: this.lazyRenderMargin, // Pre-render before visible
|
|
2581
|
+
threshold: 0
|
|
2582
|
+
});
|
|
2583
|
+
|
|
2584
|
+
// Observe all unpopulated rows
|
|
2585
|
+
const rows = this.fixedColumns > 0
|
|
2586
|
+
? this.scrollBodyContainer.querySelectorAll('.div-table-row[data-populated="false"]')
|
|
2587
|
+
: this.bodyContainer.querySelectorAll('.div-table-row[data-populated="false"]');
|
|
2588
|
+
|
|
2589
|
+
rows.forEach(row => {
|
|
2590
|
+
this.rowObserver.observe(row);
|
|
2591
|
+
});
|
|
2592
|
+
|
|
2593
|
+
// Also observe in fixed section if using fixed columns
|
|
2594
|
+
if (this.fixedColumns > 0 && this.fixedBodyContainer) {
|
|
2595
|
+
const fixedRows = this.fixedBodyContainer.querySelectorAll('.div-table-row[data-populated="false"]');
|
|
2596
|
+
fixedRows.forEach(row => {
|
|
2597
|
+
this.rowObserver.observe(row);
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
/**
|
|
2603
|
+
* Fallback method to populate all unpopulated rows (when IntersectionObserver not available)
|
|
2604
|
+
*/
|
|
2605
|
+
populateAllUnpopulatedRows() {
|
|
2606
|
+
const bodyContainer = this.fixedColumns > 0 ? this.scrollBodyContainer : this.bodyContainer;
|
|
2607
|
+
const rows = bodyContainer.querySelectorAll('.div-table-row[data-populated="false"]');
|
|
2608
|
+
|
|
2609
|
+
rows.forEach(row => {
|
|
2610
|
+
const rowId = row.dataset.id;
|
|
2611
|
+
const item = this.findRowData(rowId);
|
|
2612
|
+
if (item) {
|
|
2613
|
+
this.populateRowCells(row, item);
|
|
2614
|
+
}
|
|
2391
2615
|
});
|
|
2392
2616
|
}
|
|
2393
2617
|
|
|
@@ -2443,58 +2667,26 @@ class DivTable {
|
|
|
2443
2667
|
this.bodyContainer.appendChild(row);
|
|
2444
2668
|
});
|
|
2445
2669
|
}
|
|
2670
|
+
|
|
2671
|
+
// Add group summary row after group (visible even when collapsed)
|
|
2672
|
+
if (this.showGroupSummary && this.hasAggregateColumns()) {
|
|
2673
|
+
const groupSummary = this.createGroupSummaryRow(group);
|
|
2674
|
+
this.bodyContainer.appendChild(groupSummary);
|
|
2675
|
+
}
|
|
2446
2676
|
});
|
|
2447
2677
|
}
|
|
2448
2678
|
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2679
|
+
/**
|
|
2680
|
+
* Populate cells for a row that was created as an empty shell (lazy rendering)
|
|
2681
|
+
* @param {HTMLElement} row - The row element to populate
|
|
2682
|
+
* @param {Object} item - The data item for this row
|
|
2683
|
+
*/
|
|
2684
|
+
populateRowCells(row, item) {
|
|
2685
|
+
// Skip if already populated
|
|
2686
|
+
if (row.dataset.populated === 'true') return;
|
|
2454
2687
|
|
|
2455
2688
|
const compositeColumns = this.getCompositeColumns();
|
|
2456
|
-
|
|
2457
|
-
// Build grid template matching the header
|
|
2458
|
-
let gridTemplate = '';
|
|
2459
|
-
if (this.showCheckboxes) {
|
|
2460
|
-
gridTemplate = '40px '; // Checkbox column
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
|
-
// Add column templates for each composite group
|
|
2464
|
-
compositeColumns.forEach(composite => {
|
|
2465
|
-
const firstCol = composite.columns[0];
|
|
2466
|
-
const responsive = firstCol.responsive || {};
|
|
2467
|
-
switch (responsive.size) {
|
|
2468
|
-
case 'fixed-narrow':
|
|
2469
|
-
gridTemplate += '80px ';
|
|
2470
|
-
break;
|
|
2471
|
-
case 'fixed-medium':
|
|
2472
|
-
gridTemplate += '120px ';
|
|
2473
|
-
break;
|
|
2474
|
-
case 'flexible-small':
|
|
2475
|
-
gridTemplate += '1fr ';
|
|
2476
|
-
break;
|
|
2477
|
-
case 'flexible-medium':
|
|
2478
|
-
gridTemplate += '2fr ';
|
|
2479
|
-
break;
|
|
2480
|
-
case 'flexible-large':
|
|
2481
|
-
gridTemplate += '3fr ';
|
|
2482
|
-
break;
|
|
2483
|
-
default:
|
|
2484
|
-
gridTemplate += '1fr ';
|
|
2485
|
-
}
|
|
2486
|
-
});
|
|
2487
|
-
|
|
2488
|
-
row.style.gridTemplateColumns = gridTemplate.trim();
|
|
2489
|
-
|
|
2490
|
-
// Selection state
|
|
2491
2689
|
const rowId = String(item[this.primaryKeyField]);
|
|
2492
|
-
if (this.selectedRows.has(rowId)) {
|
|
2493
|
-
row.classList.add('selected');
|
|
2494
|
-
}
|
|
2495
|
-
if (this.focusedRowId === rowId) {
|
|
2496
|
-
row.classList.add('focused');
|
|
2497
|
-
}
|
|
2498
2690
|
|
|
2499
2691
|
// Checkbox column
|
|
2500
2692
|
if (this.showCheckboxes) {
|
|
@@ -2508,7 +2700,6 @@ class DivTable {
|
|
|
2508
2700
|
checkbox.addEventListener('change', (e) => {
|
|
2509
2701
|
e.stopPropagation();
|
|
2510
2702
|
|
|
2511
|
-
// Verify that the row data exists before proceeding
|
|
2512
2703
|
const rowData = this.findRowData(rowId);
|
|
2513
2704
|
if (!rowData) {
|
|
2514
2705
|
console.warn('DivTable: Could not find data for row ID:', rowId);
|
|
@@ -2525,14 +2716,10 @@ class DivTable {
|
|
|
2525
2716
|
rowData.selected = false;
|
|
2526
2717
|
row.classList.remove('selected');
|
|
2527
2718
|
|
|
2528
|
-
// If filter is active and we just deselected a row, re-render to remove it from view
|
|
2529
2719
|
if (this.showOnlySelected) {
|
|
2530
|
-
// Re-render the body to update the filtered view
|
|
2531
2720
|
this.renderBody();
|
|
2532
|
-
// Update info section after render
|
|
2533
2721
|
this.updateInfoSection();
|
|
2534
2722
|
|
|
2535
|
-
// Trigger selection change callback
|
|
2536
2723
|
const selectedData = Array.from(this.selectedRows)
|
|
2537
2724
|
.map(id => this.findRowData(id))
|
|
2538
2725
|
.filter(Boolean);
|
|
@@ -2540,17 +2727,13 @@ class DivTable {
|
|
|
2540
2727
|
if (typeof this.onSelectionChange === 'function') {
|
|
2541
2728
|
this.onSelectionChange(selectedData);
|
|
2542
2729
|
}
|
|
2543
|
-
|
|
2544
|
-
// Early return since we already updated everything
|
|
2545
2730
|
return;
|
|
2546
2731
|
}
|
|
2547
2732
|
}
|
|
2548
2733
|
|
|
2549
|
-
// Update all checkbox states (group and header)
|
|
2550
2734
|
this.updateSelectionStates();
|
|
2551
2735
|
this.updateInfoSection();
|
|
2552
2736
|
|
|
2553
|
-
// Ensure we only return valid data objects
|
|
2554
2737
|
const selectedData = Array.from(this.selectedRows)
|
|
2555
2738
|
.map(id => this.findRowData(id))
|
|
2556
2739
|
.filter(Boolean);
|
|
@@ -2560,24 +2743,14 @@ class DivTable {
|
|
|
2560
2743
|
}
|
|
2561
2744
|
});
|
|
2562
2745
|
|
|
2563
|
-
// Sync checkbox focus with row focus
|
|
2564
2746
|
checkbox.addEventListener('focus', (e) => {
|
|
2565
2747
|
this.updateFocusState(row);
|
|
2566
2748
|
});
|
|
2567
2749
|
|
|
2568
|
-
checkbox.addEventListener('blur', (e) => {
|
|
2569
|
-
// Optionally handle blur if needed - for now, keep row focused
|
|
2570
|
-
// This allows arrow key navigation to continue working
|
|
2571
|
-
});
|
|
2572
|
-
|
|
2573
2750
|
checkboxCell.appendChild(checkbox);
|
|
2574
2751
|
|
|
2575
|
-
// Make the entire checkbox cell clickable
|
|
2576
2752
|
checkboxCell.addEventListener('click', (e) => {
|
|
2577
|
-
// If clicked on the checkbox itself, let it handle naturally
|
|
2578
2753
|
if (e.target === checkbox) return;
|
|
2579
|
-
|
|
2580
|
-
// If clicked elsewhere in the cell, toggle the checkbox
|
|
2581
2754
|
e.stopPropagation();
|
|
2582
2755
|
checkbox.click();
|
|
2583
2756
|
});
|
|
@@ -2591,7 +2764,6 @@ class DivTable {
|
|
|
2591
2764
|
cell.className = 'div-table-cell';
|
|
2592
2765
|
|
|
2593
2766
|
if (composite.compositeName) {
|
|
2594
|
-
// Composite cell with multiple columns stacked vertically
|
|
2595
2767
|
cell.classList.add('composite-cell');
|
|
2596
2768
|
cell.style.display = 'flex';
|
|
2597
2769
|
cell.style.flexDirection = 'column';
|
|
@@ -2601,12 +2773,10 @@ class DivTable {
|
|
|
2601
2773
|
const subCell = document.createElement('div');
|
|
2602
2774
|
subCell.className = 'composite-sub-cell';
|
|
2603
2775
|
|
|
2604
|
-
// For grouped column, show empty
|
|
2605
2776
|
if (this.groupByField && col.field === this.groupByField) {
|
|
2606
2777
|
subCell.classList.add('grouped-column');
|
|
2607
2778
|
subCell.textContent = '';
|
|
2608
2779
|
} else {
|
|
2609
|
-
// Check if this column has subField (vertical stacking within the sub-cell)
|
|
2610
2780
|
if (col.subField) {
|
|
2611
2781
|
subCell.classList.add('composite-column');
|
|
2612
2782
|
subCell.style.display = 'flex';
|
|
@@ -2632,7 +2802,6 @@ class DivTable {
|
|
|
2632
2802
|
subCell.appendChild(mainDiv);
|
|
2633
2803
|
subCell.appendChild(subDiv);
|
|
2634
2804
|
} else {
|
|
2635
|
-
// Regular rendering
|
|
2636
2805
|
if (typeof col.render === 'function') {
|
|
2637
2806
|
subCell.innerHTML = col.render(item[col.field], item);
|
|
2638
2807
|
} else {
|
|
@@ -2644,15 +2813,12 @@ class DivTable {
|
|
|
2644
2813
|
cell.appendChild(subCell);
|
|
2645
2814
|
});
|
|
2646
2815
|
} else {
|
|
2647
|
-
// Single column
|
|
2648
2816
|
const col = composite.columns[0];
|
|
2649
2817
|
|
|
2650
|
-
// For grouped column, show empty
|
|
2651
2818
|
if (this.groupByField && col.field === this.groupByField) {
|
|
2652
2819
|
cell.classList.add('grouped-column');
|
|
2653
2820
|
cell.textContent = '';
|
|
2654
2821
|
} else {
|
|
2655
|
-
// Check if this is a composite column with subField (vertical stacking)
|
|
2656
2822
|
if (col.subField) {
|
|
2657
2823
|
cell.classList.add('composite-column');
|
|
2658
2824
|
|
|
@@ -2675,7 +2841,6 @@ class DivTable {
|
|
|
2675
2841
|
cell.appendChild(mainDiv);
|
|
2676
2842
|
cell.appendChild(subDiv);
|
|
2677
2843
|
} else {
|
|
2678
|
-
// Regular column rendering
|
|
2679
2844
|
if (typeof col.render === 'function') {
|
|
2680
2845
|
cell.innerHTML = col.render(item[col.field], item);
|
|
2681
2846
|
} else {
|
|
@@ -2688,86 +2853,42 @@ class DivTable {
|
|
|
2688
2853
|
row.appendChild(cell);
|
|
2689
2854
|
});
|
|
2690
2855
|
|
|
2691
|
-
//
|
|
2692
|
-
row.
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
const
|
|
2698
|
-
if (
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
// Find the focusable element for this row
|
|
2703
|
-
const focusableElement = this.getFocusableElementForRow(row);
|
|
2704
|
-
if (focusableElement) {
|
|
2705
|
-
// Check if this row already has focus
|
|
2706
|
-
const currentFocused = this.getCurrentFocusedElement();
|
|
2707
|
-
if (currentFocused === focusableElement) {
|
|
2708
|
-
return; // Don't trigger focus event if already focused
|
|
2709
|
-
}
|
|
2710
|
-
|
|
2711
|
-
// Get all focusable elements and find the index of this element
|
|
2712
|
-
const focusableElements = this.getAllFocusableElements();
|
|
2713
|
-
const focusIndex = focusableElements.indexOf(focusableElement);
|
|
2856
|
+
// Mark as populated
|
|
2857
|
+
row.dataset.populated = 'true';
|
|
2858
|
+
|
|
2859
|
+
// Measure actual height and set it explicitly to prevent bouncing on re-renders
|
|
2860
|
+
// Use requestAnimationFrame to ensure DOM has updated
|
|
2861
|
+
requestAnimationFrame(() => {
|
|
2862
|
+
const actualHeight = row.offsetHeight;
|
|
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`;
|
|
2714
2867
|
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
this.
|
|
2868
|
+
// Update estimate for future unpopulated rows
|
|
2869
|
+
if (actualHeight > this.estimatedRowHeight) {
|
|
2870
|
+
this.estimatedRowHeight = actualHeight;
|
|
2718
2871
|
}
|
|
2719
2872
|
}
|
|
2720
2873
|
});
|
|
2721
|
-
|
|
2722
|
-
//
|
|
2723
|
-
|
|
2724
|
-
this.updateFocusState(row);
|
|
2725
|
-
});
|
|
2726
|
-
|
|
2727
|
-
return row;
|
|
2874
|
+
|
|
2875
|
+
// Update tab indexes after population
|
|
2876
|
+
this.updateTabIndexes();
|
|
2728
2877
|
}
|
|
2729
2878
|
|
|
2730
|
-
|
|
2879
|
+
/**
|
|
2880
|
+
* Populate cells for fixed columns layout (both fixed and scroll row parts)
|
|
2881
|
+
* @param {HTMLElement} fixedRow - The fixed row element
|
|
2882
|
+
* @param {HTMLElement} scrollRow - The scroll row element
|
|
2883
|
+
* @param {Object} item - The data item for this row
|
|
2884
|
+
*/
|
|
2885
|
+
populateRowCellsWithFixedColumns(fixedRow, scrollRow, item) {
|
|
2886
|
+
// Skip if already populated
|
|
2887
|
+
if (fixedRow.dataset.populated === 'true') return;
|
|
2888
|
+
|
|
2731
2889
|
const { fixedColumns, scrollColumns } = this.splitColumnsForFixedLayout();
|
|
2732
2890
|
const rowId = String(item[this.primaryKeyField]);
|
|
2733
2891
|
|
|
2734
|
-
// Create fixed row part
|
|
2735
|
-
const fixedRow = document.createElement('div');
|
|
2736
|
-
fixedRow.className = 'div-table-row div-table-fixed-row';
|
|
2737
|
-
fixedRow.dataset.id = rowId;
|
|
2738
|
-
|
|
2739
|
-
// Build grid template for fixed row
|
|
2740
|
-
let fixedGridTemplate = '';
|
|
2741
|
-
if (this.showCheckboxes) {
|
|
2742
|
-
fixedGridTemplate = '40px ';
|
|
2743
|
-
}
|
|
2744
|
-
fixedColumns.forEach(composite => {
|
|
2745
|
-
fixedGridTemplate += this.getColumnGridSize(composite) + ' ';
|
|
2746
|
-
});
|
|
2747
|
-
fixedRow.style.gridTemplateColumns = fixedGridTemplate.trim();
|
|
2748
|
-
|
|
2749
|
-
// Create scrollable row part
|
|
2750
|
-
const scrollRow = document.createElement('div');
|
|
2751
|
-
scrollRow.className = 'div-table-row div-table-scroll-row';
|
|
2752
|
-
scrollRow.dataset.id = rowId;
|
|
2753
|
-
|
|
2754
|
-
// Build grid template for scrollable row
|
|
2755
|
-
let scrollGridTemplate = '';
|
|
2756
|
-
scrollColumns.forEach(composite => {
|
|
2757
|
-
scrollGridTemplate += this.getColumnGridSize(composite) + ' ';
|
|
2758
|
-
});
|
|
2759
|
-
scrollRow.style.gridTemplateColumns = scrollGridTemplate.trim();
|
|
2760
|
-
|
|
2761
|
-
// Apply selection state to both row parts
|
|
2762
|
-
if (this.selectedRows.has(rowId)) {
|
|
2763
|
-
fixedRow.classList.add('selected');
|
|
2764
|
-
scrollRow.classList.add('selected');
|
|
2765
|
-
}
|
|
2766
|
-
if (this.focusedRowId === rowId) {
|
|
2767
|
-
fixedRow.classList.add('focused');
|
|
2768
|
-
scrollRow.classList.add('focused');
|
|
2769
|
-
}
|
|
2770
|
-
|
|
2771
2892
|
// Checkbox column (in fixed row only)
|
|
2772
2893
|
if (this.showCheckboxes) {
|
|
2773
2894
|
const checkboxCell = document.createElement('div');
|
|
@@ -2852,21 +2973,562 @@ class DivTable {
|
|
|
2852
2973
|
scrollRow.appendChild(cell);
|
|
2853
2974
|
});
|
|
2854
2975
|
|
|
2855
|
-
//
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
const
|
|
2866
|
-
|
|
2867
|
-
|
|
2976
|
+
// Mark as populated
|
|
2977
|
+
fixedRow.dataset.populated = 'true';
|
|
2978
|
+
scrollRow.dataset.populated = 'true';
|
|
2979
|
+
|
|
2980
|
+
// Synchronize heights between fixed and scroll row parts after cell population
|
|
2981
|
+
// Use double requestAnimationFrame to ensure layout is fully complete
|
|
2982
|
+
requestAnimationFrame(() => {
|
|
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`;
|
|
3003
|
+
fixedRow.style.height = `${maxHeight}px`;
|
|
3004
|
+
scrollRow.style.minHeight = `${maxHeight}px`;
|
|
3005
|
+
scrollRow.style.height = `${maxHeight}px`;
|
|
3006
|
+
|
|
3007
|
+
// Update estimate for future unpopulated rows
|
|
3008
|
+
if (maxHeight > this.estimatedRowHeight) {
|
|
3009
|
+
this.estimatedRowHeight = maxHeight;
|
|
2868
3010
|
}
|
|
2869
|
-
}
|
|
3011
|
+
});
|
|
3012
|
+
});
|
|
3013
|
+
|
|
3014
|
+
// Update tab indexes after population
|
|
3015
|
+
this.updateTabIndexes();
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
createRow(item) {
|
|
3019
|
+
const row = document.createElement('div');
|
|
3020
|
+
row.className = 'div-table-row';
|
|
3021
|
+
row.dataset.id = item[this.primaryKeyField];
|
|
3022
|
+
// Don't set tabindex here - will be managed by updateTabIndexes() based on checkbox presence
|
|
3023
|
+
|
|
3024
|
+
const compositeColumns = this.getCompositeColumns();
|
|
3025
|
+
|
|
3026
|
+
// Build grid template matching the header
|
|
3027
|
+
let gridTemplate = '';
|
|
3028
|
+
if (this.showCheckboxes) {
|
|
3029
|
+
gridTemplate = '40px '; // Checkbox column
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// Add column templates for each composite group
|
|
3033
|
+
compositeColumns.forEach(composite => {
|
|
3034
|
+
const firstCol = composite.columns[0];
|
|
3035
|
+
const responsive = firstCol.responsive || {};
|
|
3036
|
+
switch (responsive.size) {
|
|
3037
|
+
case 'fixed-narrow':
|
|
3038
|
+
gridTemplate += '80px ';
|
|
3039
|
+
break;
|
|
3040
|
+
case 'fixed-medium':
|
|
3041
|
+
gridTemplate += '120px ';
|
|
3042
|
+
break;
|
|
3043
|
+
case 'flexible-small':
|
|
3044
|
+
gridTemplate += '1fr ';
|
|
3045
|
+
break;
|
|
3046
|
+
case 'flexible-medium':
|
|
3047
|
+
gridTemplate += '2fr ';
|
|
3048
|
+
break;
|
|
3049
|
+
case 'flexible-large':
|
|
3050
|
+
gridTemplate += '3fr ';
|
|
3051
|
+
break;
|
|
3052
|
+
default:
|
|
3053
|
+
gridTemplate += '1fr ';
|
|
3054
|
+
}
|
|
3055
|
+
});
|
|
3056
|
+
|
|
3057
|
+
row.style.gridTemplateColumns = gridTemplate.trim();
|
|
3058
|
+
|
|
3059
|
+
// Selection state
|
|
3060
|
+
const rowId = String(item[this.primaryKeyField]);
|
|
3061
|
+
if (this.selectedRows.has(rowId)) {
|
|
3062
|
+
row.classList.add('selected');
|
|
3063
|
+
}
|
|
3064
|
+
if (this.focusedRowId === rowId) {
|
|
3065
|
+
row.classList.add('focused');
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
// If lazy cell rendering is enabled, create empty shell and defer cell creation
|
|
3069
|
+
if (this.lazyCellRendering) {
|
|
3070
|
+
// Set minimum height to prevent layout shift
|
|
3071
|
+
row.style.minHeight = this.estimatedRowHeight + 'px';
|
|
3072
|
+
row.dataset.populated = 'false';
|
|
3073
|
+
|
|
3074
|
+
// Row click handler - populate cells first if needed
|
|
3075
|
+
row.addEventListener('click', (e) => {
|
|
3076
|
+
// Populate cells if not yet done
|
|
3077
|
+
if (row.dataset.populated !== 'true') {
|
|
3078
|
+
this.populateRowCells(row, item);
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
// Only handle focus if not clicking on checkbox column
|
|
3082
|
+
if (e.target.closest('.checkbox-column')) return;
|
|
3083
|
+
|
|
3084
|
+
const selection = window.getSelection();
|
|
3085
|
+
if (selection.toString().length > 0) return;
|
|
3086
|
+
|
|
3087
|
+
const focusableElement = this.getFocusableElementForRow(row);
|
|
3088
|
+
if (focusableElement) {
|
|
3089
|
+
const currentFocused = this.getCurrentFocusedElement();
|
|
3090
|
+
if (currentFocused === focusableElement) return;
|
|
3091
|
+
|
|
3092
|
+
const focusableElements = this.getAllFocusableElements();
|
|
3093
|
+
const focusIndex = focusableElements.indexOf(focusableElement);
|
|
3094
|
+
|
|
3095
|
+
if (focusIndex !== -1) {
|
|
3096
|
+
this.focusElementAtIndex(focusIndex);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
});
|
|
3100
|
+
|
|
3101
|
+
row.addEventListener('focus', (e) => {
|
|
3102
|
+
// Populate cells if not yet done
|
|
3103
|
+
if (row.dataset.populated !== 'true') {
|
|
3104
|
+
this.populateRowCells(row, item);
|
|
3105
|
+
}
|
|
3106
|
+
this.updateFocusState(row);
|
|
3107
|
+
});
|
|
3108
|
+
|
|
3109
|
+
return row;
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
// Non-lazy rendering: create cells immediately (original behavior)
|
|
3113
|
+
if (this.showCheckboxes) {
|
|
3114
|
+
const checkboxCell = document.createElement('div');
|
|
3115
|
+
checkboxCell.className = 'div-table-cell checkbox-column';
|
|
3116
|
+
|
|
3117
|
+
const checkbox = document.createElement('input');
|
|
3118
|
+
checkbox.type = 'checkbox';
|
|
3119
|
+
checkbox.checked = this.selectedRows.has(rowId);
|
|
3120
|
+
|
|
3121
|
+
checkbox.addEventListener('change', (e) => {
|
|
3122
|
+
e.stopPropagation();
|
|
3123
|
+
|
|
3124
|
+
// Verify that the row data exists before proceeding
|
|
3125
|
+
const rowData = this.findRowData(rowId);
|
|
3126
|
+
if (!rowData) {
|
|
3127
|
+
console.warn('DivTable: Could not find data for row ID:', rowId);
|
|
3128
|
+
return;
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
if (checkbox.checked) {
|
|
3132
|
+
if (!this.multiSelect) this.clearSelection();
|
|
3133
|
+
this.selectedRows.add(rowId);
|
|
3134
|
+
rowData.selected = true;
|
|
3135
|
+
row.classList.add('selected');
|
|
3136
|
+
} else {
|
|
3137
|
+
this.selectedRows.delete(rowId);
|
|
3138
|
+
rowData.selected = false;
|
|
3139
|
+
row.classList.remove('selected');
|
|
3140
|
+
|
|
3141
|
+
// If filter is active and we just deselected a row, re-render to remove it from view
|
|
3142
|
+
if (this.showOnlySelected) {
|
|
3143
|
+
// Re-render the body to update the filtered view
|
|
3144
|
+
this.renderBody();
|
|
3145
|
+
// Update info section after render
|
|
3146
|
+
this.updateInfoSection();
|
|
3147
|
+
|
|
3148
|
+
// Trigger selection change callback
|
|
3149
|
+
const selectedData = Array.from(this.selectedRows)
|
|
3150
|
+
.map(id => this.findRowData(id))
|
|
3151
|
+
.filter(Boolean);
|
|
3152
|
+
|
|
3153
|
+
if (typeof this.onSelectionChange === 'function') {
|
|
3154
|
+
this.onSelectionChange(selectedData);
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
// Early return since we already updated everything
|
|
3158
|
+
return;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
// Update all checkbox states (group and header)
|
|
3163
|
+
this.updateSelectionStates();
|
|
3164
|
+
this.updateInfoSection();
|
|
3165
|
+
|
|
3166
|
+
// Ensure we only return valid data objects
|
|
3167
|
+
const selectedData = Array.from(this.selectedRows)
|
|
3168
|
+
.map(id => this.findRowData(id))
|
|
3169
|
+
.filter(Boolean);
|
|
3170
|
+
|
|
3171
|
+
if (typeof this.onSelectionChange === 'function') {
|
|
3172
|
+
this.onSelectionChange(selectedData);
|
|
3173
|
+
}
|
|
3174
|
+
});
|
|
3175
|
+
|
|
3176
|
+
// Sync checkbox focus with row focus
|
|
3177
|
+
checkbox.addEventListener('focus', (e) => {
|
|
3178
|
+
this.updateFocusState(row);
|
|
3179
|
+
});
|
|
3180
|
+
|
|
3181
|
+
checkbox.addEventListener('blur', (e) => {
|
|
3182
|
+
// Optionally handle blur if needed - for now, keep row focused
|
|
3183
|
+
// This allows arrow key navigation to continue working
|
|
3184
|
+
});
|
|
3185
|
+
|
|
3186
|
+
checkboxCell.appendChild(checkbox);
|
|
3187
|
+
|
|
3188
|
+
// Make the entire checkbox cell clickable
|
|
3189
|
+
checkboxCell.addEventListener('click', (e) => {
|
|
3190
|
+
// If clicked on the checkbox itself, let it handle naturally
|
|
3191
|
+
if (e.target === checkbox) return;
|
|
3192
|
+
|
|
3193
|
+
// If clicked elsewhere in the cell, toggle the checkbox
|
|
3194
|
+
e.stopPropagation();
|
|
3195
|
+
checkbox.click();
|
|
3196
|
+
});
|
|
3197
|
+
|
|
3198
|
+
row.appendChild(checkboxCell);
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
// Data columns - render using composite structure
|
|
3202
|
+
compositeColumns.forEach(composite => {
|
|
3203
|
+
const cell = document.createElement('div');
|
|
3204
|
+
cell.className = 'div-table-cell';
|
|
3205
|
+
|
|
3206
|
+
if (composite.compositeName) {
|
|
3207
|
+
// Composite cell with multiple columns stacked vertically
|
|
3208
|
+
cell.classList.add('composite-cell');
|
|
3209
|
+
cell.style.display = 'flex';
|
|
3210
|
+
cell.style.flexDirection = 'column';
|
|
3211
|
+
cell.style.gap = '4px';
|
|
3212
|
+
|
|
3213
|
+
composite.columns.forEach((col, index) => {
|
|
3214
|
+
const subCell = document.createElement('div');
|
|
3215
|
+
subCell.className = 'composite-sub-cell';
|
|
3216
|
+
|
|
3217
|
+
// For grouped column, show empty
|
|
3218
|
+
if (this.groupByField && col.field === this.groupByField) {
|
|
3219
|
+
subCell.classList.add('grouped-column');
|
|
3220
|
+
subCell.textContent = '';
|
|
3221
|
+
} else {
|
|
3222
|
+
// Check if this column has subField (vertical stacking within the sub-cell)
|
|
3223
|
+
if (col.subField) {
|
|
3224
|
+
subCell.classList.add('composite-column');
|
|
3225
|
+
subCell.style.display = 'flex';
|
|
3226
|
+
subCell.style.flexDirection = 'column';
|
|
3227
|
+
subCell.style.gap = '2px';
|
|
3228
|
+
|
|
3229
|
+
const mainDiv = document.createElement('div');
|
|
3230
|
+
mainDiv.className = 'composite-main';
|
|
3231
|
+
if (typeof col.render === 'function') {
|
|
3232
|
+
mainDiv.innerHTML = col.render(item[col.field], item);
|
|
3233
|
+
} else {
|
|
3234
|
+
mainDiv.innerHTML = item[col.field] ?? '';
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
const subDiv = document.createElement('div');
|
|
3238
|
+
subDiv.className = 'composite-sub';
|
|
3239
|
+
if (typeof col.subRender === 'function') {
|
|
3240
|
+
subDiv.innerHTML = col.subRender(item[col.subField], item);
|
|
3241
|
+
} else {
|
|
3242
|
+
subDiv.innerHTML = item[col.subField] ?? '';
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
subCell.appendChild(mainDiv);
|
|
3246
|
+
subCell.appendChild(subDiv);
|
|
3247
|
+
} else {
|
|
3248
|
+
// Regular rendering
|
|
3249
|
+
if (typeof col.render === 'function') {
|
|
3250
|
+
subCell.innerHTML = col.render(item[col.field], item);
|
|
3251
|
+
} else {
|
|
3252
|
+
subCell.innerHTML = item[col.field] ?? '';
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
cell.appendChild(subCell);
|
|
3258
|
+
});
|
|
3259
|
+
} else {
|
|
3260
|
+
// Single column
|
|
3261
|
+
const col = composite.columns[0];
|
|
3262
|
+
|
|
3263
|
+
// For grouped column, show empty
|
|
3264
|
+
if (this.groupByField && col.field === this.groupByField) {
|
|
3265
|
+
cell.classList.add('grouped-column');
|
|
3266
|
+
cell.textContent = '';
|
|
3267
|
+
} else {
|
|
3268
|
+
// Check if this is a composite column with subField (vertical stacking)
|
|
3269
|
+
if (col.subField) {
|
|
3270
|
+
cell.classList.add('composite-column');
|
|
3271
|
+
|
|
3272
|
+
const mainDiv = document.createElement('div');
|
|
3273
|
+
mainDiv.className = 'composite-main';
|
|
3274
|
+
if (typeof col.render === 'function') {
|
|
3275
|
+
mainDiv.innerHTML = col.render(item[col.field], item);
|
|
3276
|
+
} else {
|
|
3277
|
+
mainDiv.innerHTML = item[col.field] ?? '';
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
const subDiv = document.createElement('div');
|
|
3281
|
+
subDiv.className = 'composite-sub';
|
|
3282
|
+
if (typeof col.subRender === 'function') {
|
|
3283
|
+
subDiv.innerHTML = col.subRender(item[col.subField], item);
|
|
3284
|
+
} else {
|
|
3285
|
+
subDiv.innerHTML = item[col.subField] ?? '';
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
cell.appendChild(mainDiv);
|
|
3289
|
+
cell.appendChild(subDiv);
|
|
3290
|
+
} else {
|
|
3291
|
+
// Regular column rendering
|
|
3292
|
+
if (typeof col.render === 'function') {
|
|
3293
|
+
cell.innerHTML = col.render(item[col.field], item);
|
|
3294
|
+
} else {
|
|
3295
|
+
cell.innerHTML = item[col.field] ?? '';
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
row.appendChild(cell);
|
|
3302
|
+
});
|
|
3303
|
+
|
|
3304
|
+
// Row click handler - set focus index for clicked row
|
|
3305
|
+
row.addEventListener('click', (e) => {
|
|
3306
|
+
// Only handle focus if not clicking on checkbox column
|
|
3307
|
+
if (e.target.closest('.checkbox-column')) return;
|
|
3308
|
+
|
|
3309
|
+
// Check if user is making a text selection
|
|
3310
|
+
const selection = window.getSelection();
|
|
3311
|
+
if (selection.toString().length > 0) {
|
|
3312
|
+
return; // Don't trigger focus if user is selecting text
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
// Find the focusable element for this row
|
|
3316
|
+
const focusableElement = this.getFocusableElementForRow(row);
|
|
3317
|
+
if (focusableElement) {
|
|
3318
|
+
// Check if this row already has focus
|
|
3319
|
+
const currentFocused = this.getCurrentFocusedElement();
|
|
3320
|
+
if (currentFocused === focusableElement) {
|
|
3321
|
+
return; // Don't trigger focus event if already focused
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
// Get all focusable elements and find the index of this element
|
|
3325
|
+
const focusableElements = this.getAllFocusableElements();
|
|
3326
|
+
const focusIndex = focusableElements.indexOf(focusableElement);
|
|
3327
|
+
|
|
3328
|
+
if (focusIndex !== -1) {
|
|
3329
|
+
// Use the existing focus system to set focus to this index
|
|
3330
|
+
this.focusElementAtIndex(focusIndex);
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
});
|
|
3334
|
+
|
|
3335
|
+
// Row focus event to sync with tabIndex navigation
|
|
3336
|
+
row.addEventListener('focus', (e) => {
|
|
3337
|
+
this.updateFocusState(row);
|
|
3338
|
+
});
|
|
3339
|
+
|
|
3340
|
+
return row;
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
createRowWithFixedColumns(item) {
|
|
3344
|
+
const { fixedColumns, scrollColumns } = this.splitColumnsForFixedLayout();
|
|
3345
|
+
const rowId = String(item[this.primaryKeyField]);
|
|
3346
|
+
|
|
3347
|
+
// Create fixed row part
|
|
3348
|
+
const fixedRow = document.createElement('div');
|
|
3349
|
+
fixedRow.className = 'div-table-row div-table-fixed-row';
|
|
3350
|
+
fixedRow.dataset.id = rowId;
|
|
3351
|
+
|
|
3352
|
+
// Build grid template for fixed row
|
|
3353
|
+
let fixedGridTemplate = '';
|
|
3354
|
+
if (this.showCheckboxes) {
|
|
3355
|
+
fixedGridTemplate = '40px ';
|
|
3356
|
+
}
|
|
3357
|
+
fixedColumns.forEach(composite => {
|
|
3358
|
+
fixedGridTemplate += this.getColumnGridSize(composite) + ' ';
|
|
3359
|
+
});
|
|
3360
|
+
fixedRow.style.gridTemplateColumns = fixedGridTemplate.trim();
|
|
3361
|
+
|
|
3362
|
+
// Create scrollable row part
|
|
3363
|
+
const scrollRow = document.createElement('div');
|
|
3364
|
+
scrollRow.className = 'div-table-row div-table-scroll-row';
|
|
3365
|
+
scrollRow.dataset.id = rowId;
|
|
3366
|
+
|
|
3367
|
+
// Build grid template for scrollable row
|
|
3368
|
+
let scrollGridTemplate = '';
|
|
3369
|
+
scrollColumns.forEach(composite => {
|
|
3370
|
+
scrollGridTemplate += this.getColumnGridSize(composite) + ' ';
|
|
3371
|
+
});
|
|
3372
|
+
scrollRow.style.gridTemplateColumns = scrollGridTemplate.trim();
|
|
3373
|
+
|
|
3374
|
+
// Apply selection state to both row parts
|
|
3375
|
+
if (this.selectedRows.has(rowId)) {
|
|
3376
|
+
fixedRow.classList.add('selected');
|
|
3377
|
+
scrollRow.classList.add('selected');
|
|
3378
|
+
}
|
|
3379
|
+
if (this.focusedRowId === rowId) {
|
|
3380
|
+
fixedRow.classList.add('focused');
|
|
3381
|
+
scrollRow.classList.add('focused');
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
// If lazy cell rendering is enabled, create empty shells and defer cell creation
|
|
3385
|
+
if (this.lazyCellRendering) {
|
|
3386
|
+
fixedRow.style.minHeight = this.estimatedRowHeight + 'px';
|
|
3387
|
+
scrollRow.style.minHeight = this.estimatedRowHeight + 'px';
|
|
3388
|
+
fixedRow.dataset.populated = 'false';
|
|
3389
|
+
scrollRow.dataset.populated = 'false';
|
|
3390
|
+
|
|
3391
|
+
// Row click handler - populate cells first if needed
|
|
3392
|
+
const handleRowClick = (e, targetRow) => {
|
|
3393
|
+
// Populate cells if not yet done
|
|
3394
|
+
if (fixedRow.dataset.populated !== 'true') {
|
|
3395
|
+
this.populateRowCellsWithFixedColumns(fixedRow, scrollRow, item);
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
if (e.target.closest('.checkbox-column')) return;
|
|
3399
|
+
|
|
3400
|
+
const selection = window.getSelection();
|
|
3401
|
+
if (selection.toString().length > 0) return;
|
|
3402
|
+
|
|
3403
|
+
const focusableElement = this.getFocusableElementForRow(fixedRow);
|
|
3404
|
+
if (focusableElement) {
|
|
3405
|
+
const focusableElements = this.getAllFocusableElements();
|
|
3406
|
+
const focusIndex = focusableElements.indexOf(focusableElement);
|
|
3407
|
+
if (focusIndex !== -1) {
|
|
3408
|
+
this.focusElementAtIndex(focusIndex);
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
};
|
|
3412
|
+
|
|
3413
|
+
fixedRow.addEventListener('click', (e) => handleRowClick(e, fixedRow));
|
|
3414
|
+
scrollRow.addEventListener('click', (e) => handleRowClick(e, scrollRow));
|
|
3415
|
+
|
|
3416
|
+
fixedRow.addEventListener('focus', (e) => {
|
|
3417
|
+
if (fixedRow.dataset.populated !== 'true') {
|
|
3418
|
+
this.populateRowCellsWithFixedColumns(fixedRow, scrollRow, item);
|
|
3419
|
+
}
|
|
3420
|
+
this.updateFocusStateForFixedRows(fixedRow, scrollRow);
|
|
3421
|
+
});
|
|
3422
|
+
|
|
3423
|
+
// Sync hover state between row parts
|
|
3424
|
+
fixedRow.addEventListener('mouseenter', () => scrollRow.classList.add('hover'));
|
|
3425
|
+
fixedRow.addEventListener('mouseleave', () => scrollRow.classList.remove('hover'));
|
|
3426
|
+
scrollRow.addEventListener('mouseenter', () => fixedRow.classList.add('hover'));
|
|
3427
|
+
scrollRow.addEventListener('mouseleave', () => fixedRow.classList.remove('hover'));
|
|
3428
|
+
|
|
3429
|
+
return { fixedRow, scrollRow };
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
// Non-lazy rendering: create cells immediately (original behavior)
|
|
3433
|
+
// Checkbox column (in fixed row only)
|
|
3434
|
+
if (this.showCheckboxes) {
|
|
3435
|
+
const checkboxCell = document.createElement('div');
|
|
3436
|
+
checkboxCell.className = 'div-table-cell checkbox-column';
|
|
3437
|
+
|
|
3438
|
+
const checkbox = document.createElement('input');
|
|
3439
|
+
checkbox.type = 'checkbox';
|
|
3440
|
+
checkbox.checked = this.selectedRows.has(rowId);
|
|
3441
|
+
|
|
3442
|
+
checkbox.addEventListener('change', (e) => {
|
|
3443
|
+
e.stopPropagation();
|
|
3444
|
+
|
|
3445
|
+
const rowData = this.findRowData(rowId);
|
|
3446
|
+
if (!rowData) {
|
|
3447
|
+
console.warn('DivTable: Could not find data for row ID:', rowId);
|
|
3448
|
+
return;
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
if (checkbox.checked) {
|
|
3452
|
+
if (!this.multiSelect) this.clearSelection();
|
|
3453
|
+
this.selectedRows.add(rowId);
|
|
3454
|
+
rowData.selected = true;
|
|
3455
|
+
fixedRow.classList.add('selected');
|
|
3456
|
+
scrollRow.classList.add('selected');
|
|
3457
|
+
} else {
|
|
3458
|
+
this.selectedRows.delete(rowId);
|
|
3459
|
+
rowData.selected = false;
|
|
3460
|
+
fixedRow.classList.remove('selected');
|
|
3461
|
+
scrollRow.classList.remove('selected');
|
|
3462
|
+
|
|
3463
|
+
if (this.showOnlySelected) {
|
|
3464
|
+
this.renderBody();
|
|
3465
|
+
this.updateInfoSection();
|
|
3466
|
+
|
|
3467
|
+
const selectedData = Array.from(this.selectedRows)
|
|
3468
|
+
.map(id => this.findRowData(id))
|
|
3469
|
+
.filter(Boolean);
|
|
3470
|
+
|
|
3471
|
+
if (typeof this.onSelectionChange === 'function') {
|
|
3472
|
+
this.onSelectionChange(selectedData);
|
|
3473
|
+
}
|
|
3474
|
+
return;
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
this.updateSelectionStates();
|
|
3479
|
+
this.updateInfoSection();
|
|
3480
|
+
|
|
3481
|
+
const selectedData = Array.from(this.selectedRows)
|
|
3482
|
+
.map(id => this.findRowData(id))
|
|
3483
|
+
.filter(Boolean);
|
|
3484
|
+
|
|
3485
|
+
if (typeof this.onSelectionChange === 'function') {
|
|
3486
|
+
this.onSelectionChange(selectedData);
|
|
3487
|
+
}
|
|
3488
|
+
});
|
|
3489
|
+
|
|
3490
|
+
checkbox.addEventListener('focus', (e) => {
|
|
3491
|
+
this.updateFocusStateForFixedRows(fixedRow, scrollRow);
|
|
3492
|
+
});
|
|
3493
|
+
|
|
3494
|
+
checkboxCell.appendChild(checkbox);
|
|
3495
|
+
|
|
3496
|
+
checkboxCell.addEventListener('click', (e) => {
|
|
3497
|
+
if (e.target === checkbox) return;
|
|
3498
|
+
e.stopPropagation();
|
|
3499
|
+
checkbox.click();
|
|
3500
|
+
});
|
|
3501
|
+
|
|
3502
|
+
fixedRow.appendChild(checkboxCell);
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
// Render fixed columns
|
|
3506
|
+
fixedColumns.forEach(composite => {
|
|
3507
|
+
const cell = this.createCellForComposite(composite, item);
|
|
3508
|
+
fixedRow.appendChild(cell);
|
|
3509
|
+
});
|
|
3510
|
+
|
|
3511
|
+
// Render scrollable columns
|
|
3512
|
+
scrollColumns.forEach(composite => {
|
|
3513
|
+
const cell = this.createCellForComposite(composite, item);
|
|
3514
|
+
scrollRow.appendChild(cell);
|
|
3515
|
+
});
|
|
3516
|
+
|
|
3517
|
+
// Row click handlers for both parts
|
|
3518
|
+
const handleRowClick = (e, targetRow) => {
|
|
3519
|
+
if (e.target.closest('.checkbox-column')) return;
|
|
3520
|
+
|
|
3521
|
+
const selection = window.getSelection();
|
|
3522
|
+
if (selection.toString().length > 0) return;
|
|
3523
|
+
|
|
3524
|
+
const focusableElement = this.getFocusableElementForRow(fixedRow);
|
|
3525
|
+
if (focusableElement) {
|
|
3526
|
+
const focusableElements = this.getAllFocusableElements();
|
|
3527
|
+
const focusIndex = focusableElements.indexOf(focusableElement);
|
|
3528
|
+
if (focusIndex !== -1) {
|
|
3529
|
+
this.focusElementAtIndex(focusIndex);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
2870
3532
|
};
|
|
2871
3533
|
|
|
2872
3534
|
fixedRow.addEventListener('click', (e) => handleRowClick(e, fixedRow));
|
|
@@ -2914,6 +3576,13 @@ class DivTable {
|
|
|
2914
3576
|
const cell = document.createElement('div');
|
|
2915
3577
|
cell.className = 'div-table-cell';
|
|
2916
3578
|
|
|
3579
|
+
// Apply text alignment from column config (single column only)
|
|
3580
|
+
if (!composite.compositeName && composite.columns[0]?.align) {
|
|
3581
|
+
cell.style.textAlign = composite.columns[0].align;
|
|
3582
|
+
cell.style.justifyContent = composite.columns[0].align === 'right' ? 'flex-end' :
|
|
3583
|
+
composite.columns[0].align === 'center' ? 'center' : 'flex-start';
|
|
3584
|
+
}
|
|
3585
|
+
|
|
2917
3586
|
if (composite.compositeName) {
|
|
2918
3587
|
// Composite cell with multiple columns stacked vertically
|
|
2919
3588
|
cell.classList.add('composite-cell');
|
|
@@ -3512,10 +4181,15 @@ class DivTable {
|
|
|
3512
4181
|
updateInfoSection() {
|
|
3513
4182
|
if (!this.infoSection) return;
|
|
3514
4183
|
|
|
3515
|
-
|
|
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;
|
|
3516
4189
|
const loaded = this.data.length;
|
|
3517
4190
|
const filtered = this.filteredData.length;
|
|
3518
|
-
|
|
4191
|
+
// Count only valid selected rows (detail records, not stale IDs)
|
|
4192
|
+
const selected = this.getValidSelectedCount();
|
|
3519
4193
|
|
|
3520
4194
|
// Clear existing content
|
|
3521
4195
|
this.infoSection.innerHTML = '';
|
|
@@ -3951,10 +4625,12 @@ class DivTable {
|
|
|
3951
4625
|
updateInfoSectionWithAnticipatedProgress() {
|
|
3952
4626
|
if (!this.infoSection || !this.virtualScrolling) return;
|
|
3953
4627
|
|
|
3954
|
-
|
|
4628
|
+
// Use max of totalRecords and actual loaded data to handle inconsistencies
|
|
4629
|
+
const total = Math.max(this.totalRecords, this.data.length);
|
|
3955
4630
|
const currentLoaded = this.data.length;
|
|
3956
4631
|
const filtered = this.filteredData.length;
|
|
3957
|
-
|
|
4632
|
+
// Count only valid selected rows (detail records, not stale IDs)
|
|
4633
|
+
const selected = this.getValidSelectedCount();
|
|
3958
4634
|
|
|
3959
4635
|
// Calculate anticipated progress (assume we'll get a full page of data)
|
|
3960
4636
|
const anticipatedLoaded = Math.min(currentLoaded + this.pageSize, total);
|
|
@@ -4232,6 +4908,15 @@ class DivTable {
|
|
|
4232
4908
|
return Array.from(this.selectedRows).map(id => this.findRowData(id)).filter(Boolean);
|
|
4233
4909
|
}
|
|
4234
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
|
+
|
|
4235
4920
|
/**
|
|
4236
4921
|
* Toggle the filter to show only selected rows or all rows
|
|
4237
4922
|
* @param {boolean} [showOnlySelected] - Optional: explicitly set the filter state (true = show only selected, false = show all). If omitted, toggles current state.
|
|
@@ -4853,145 +5538,705 @@ class DivTable {
|
|
|
4853
5538
|
}
|
|
4854
5539
|
}
|
|
4855
5540
|
|
|
4856
|
-
if (addedCount > 0 || updatedCount > 0) {
|
|
4857
|
-
this.isLoadingState = false;
|
|
4858
|
-
|
|
4859
|
-
// Update the query engine with new/updated data
|
|
4860
|
-
this.queryEngine.setObjects(this.data);
|
|
4861
|
-
|
|
4862
|
-
// Update query editor if field values changed (for completion suggestions)
|
|
4863
|
-
this.updateQueryEditorIfNeeded();
|
|
5541
|
+
if (addedCount > 0 || updatedCount > 0) {
|
|
5542
|
+
this.isLoadingState = false;
|
|
5543
|
+
|
|
5544
|
+
// Update the query engine with new/updated data
|
|
5545
|
+
this.queryEngine.setObjects(this.data);
|
|
5546
|
+
|
|
5547
|
+
// Update query editor if field values changed (for completion suggestions)
|
|
5548
|
+
this.updateQueryEditorIfNeeded();
|
|
5549
|
+
|
|
5550
|
+
// Update filtered data if no active query
|
|
5551
|
+
if (!this.currentQuery.trim()) {
|
|
5552
|
+
this.filteredData = [...this.data];
|
|
5553
|
+
} else {
|
|
5554
|
+
// Re-apply query to include new/updated data
|
|
5555
|
+
this.applyQuery(this.currentQuery);
|
|
5556
|
+
}
|
|
5557
|
+
|
|
5558
|
+
// Only update info section and re-render if not skipped
|
|
5559
|
+
// (loadNextPage will handle this after setting loading state correctly)
|
|
5560
|
+
if (!skipInfoUpdate) {
|
|
5561
|
+
this.updateInfoSection();
|
|
5562
|
+
this.render();
|
|
5563
|
+
} else {
|
|
5564
|
+
// Still need to render the data, just skip the info section update
|
|
5565
|
+
this.render();
|
|
5566
|
+
}
|
|
5567
|
+
}
|
|
5568
|
+
|
|
5569
|
+
return {
|
|
5570
|
+
added: addedCount,
|
|
5571
|
+
updated: updatedCount,
|
|
5572
|
+
skipped: invalid.length,
|
|
5573
|
+
invalid
|
|
5574
|
+
};
|
|
5575
|
+
}
|
|
5576
|
+
|
|
5577
|
+
replaceData(newData) {
|
|
5578
|
+
if (!newData || !Array.isArray(newData)) {
|
|
5579
|
+
console.warn('replaceData requires a valid array');
|
|
5580
|
+
return { success: false, message: 'Invalid data provided' };
|
|
5581
|
+
}
|
|
5582
|
+
|
|
5583
|
+
// Validate data integrity and check for duplicates within the new data
|
|
5584
|
+
const duplicates = [];
|
|
5585
|
+
const seenIds = new Set();
|
|
5586
|
+
const validRecords = [];
|
|
5587
|
+
|
|
5588
|
+
for (const record of newData) {
|
|
5589
|
+
if (!record || typeof record !== 'object') {
|
|
5590
|
+
console.warn('replaceData: Skipping invalid record', record);
|
|
5591
|
+
continue;
|
|
5592
|
+
}
|
|
5593
|
+
|
|
5594
|
+
// Ensure the record has a primary key
|
|
5595
|
+
if (!record[this.primaryKeyField]) {
|
|
5596
|
+
console.warn(`replaceData: Skipping record without ${this.primaryKeyField}`, record);
|
|
5597
|
+
continue;
|
|
5598
|
+
}
|
|
5599
|
+
|
|
5600
|
+
// Check for duplicate primary key within the new data
|
|
5601
|
+
const recordId = String(record[this.primaryKeyField]);
|
|
5602
|
+
if (seenIds.has(recordId)) {
|
|
5603
|
+
duplicates.push(recordId);
|
|
5604
|
+
console.warn(`replaceData: Skipping duplicate ${this.primaryKeyField} '${recordId}' within new data`);
|
|
5605
|
+
continue;
|
|
5606
|
+
}
|
|
5607
|
+
|
|
5608
|
+
seenIds.add(recordId);
|
|
5609
|
+
validRecords.push(record);
|
|
5610
|
+
}
|
|
5611
|
+
|
|
5612
|
+
this.data = validRecords;
|
|
5613
|
+
this.isLoadingState = false;
|
|
5614
|
+
this.clearRefreshButtonLoadingState();
|
|
5615
|
+
|
|
5616
|
+
this.queryEngine.setObjects(this.data);
|
|
5617
|
+
|
|
5618
|
+
this.updateQueryEditorIfNeeded();
|
|
5619
|
+
|
|
5620
|
+
if (this.currentQuery && this.currentQuery.trim()) {
|
|
5621
|
+
this.applyQuery(this.currentQuery);
|
|
5622
|
+
} else {
|
|
5623
|
+
this.filteredData = [...this.data];
|
|
5624
|
+
}
|
|
5625
|
+
|
|
5626
|
+
this.selectedRows.clear();
|
|
5627
|
+
|
|
5628
|
+
this.virtualScrollingState = {
|
|
5629
|
+
scrollTop: 0,
|
|
5630
|
+
displayStartIndex: 0,
|
|
5631
|
+
displayEndIndex: Math.min(this.pageSize, this.data.length),
|
|
5632
|
+
isLoading: false,
|
|
5633
|
+
};
|
|
5634
|
+
|
|
5635
|
+
// Reset pagination to first page (zero-indexed)
|
|
5636
|
+
this.currentPage = 0;
|
|
5637
|
+
this.startId = 1;
|
|
5638
|
+
|
|
5639
|
+
// Update hasMoreData flag based on whether we have less data than totalRecords
|
|
5640
|
+
if (this.virtualScrolling && this.totalRecords) {
|
|
5641
|
+
this.hasMoreData = validRecords.length < this.totalRecords;
|
|
5642
|
+
}
|
|
5643
|
+
|
|
5644
|
+
// Update info display and re-render
|
|
5645
|
+
this.updateInfoSection();
|
|
5646
|
+
this.render();
|
|
5647
|
+
|
|
5648
|
+
return {
|
|
5649
|
+
success: true,
|
|
5650
|
+
totalProvided: newData.length,
|
|
5651
|
+
validRecords: validRecords.length,
|
|
5652
|
+
skipped: newData.length - validRecords.length,
|
|
5653
|
+
duplicates
|
|
5654
|
+
};
|
|
5655
|
+
}
|
|
5656
|
+
|
|
5657
|
+
// Loading placeholder management
|
|
5658
|
+
resetToLoading() {
|
|
5659
|
+
this.isLoadingState = true;
|
|
5660
|
+
this.data = [];
|
|
5661
|
+
this.filteredData = [];
|
|
5662
|
+
this.selectedRows.clear();
|
|
5663
|
+
this.currentQuery = '';
|
|
5664
|
+
|
|
5665
|
+
// Update Monaco editor if it exists
|
|
5666
|
+
if (this.queryEditor?.editor) {
|
|
5667
|
+
this.queryEditor.editor.setValue('');
|
|
5668
|
+
}
|
|
5669
|
+
|
|
5670
|
+
// Update the query engine with empty data
|
|
5671
|
+
this.queryEngine.setObjects([]);
|
|
5672
|
+
|
|
5673
|
+
// Re-render to show loading placeholder
|
|
5674
|
+
this.render();
|
|
5675
|
+
}
|
|
5676
|
+
|
|
5677
|
+
setLoadingState(isLoading) {
|
|
5678
|
+
this.isLoadingState = Boolean(isLoading);
|
|
5679
|
+
this.render(); // Re-render to show/hide loading placeholder
|
|
5680
|
+
}
|
|
5681
|
+
|
|
5682
|
+
// =====================================
|
|
5683
|
+
// Aggregate Summary Row Methods
|
|
5684
|
+
// =====================================
|
|
5685
|
+
|
|
5686
|
+
/**
|
|
5687
|
+
* Check if any column has an aggregate defined
|
|
5688
|
+
* @returns {boolean} True if at least one column has aggregate property
|
|
5689
|
+
*/
|
|
5690
|
+
hasAggregateColumns() {
|
|
5691
|
+
return this.columns.some(col => col.aggregate);
|
|
5692
|
+
}
|
|
5693
|
+
|
|
5694
|
+
/**
|
|
5695
|
+
* Get columns that have aggregate functions defined
|
|
5696
|
+
* @returns {Array} Array of columns with aggregate property
|
|
5697
|
+
*/
|
|
5698
|
+
getAggregateColumns() {
|
|
5699
|
+
return this.columns.filter(col => col.aggregate);
|
|
5700
|
+
}
|
|
5701
|
+
|
|
5702
|
+
/**
|
|
5703
|
+
* Calculate aggregate value for a specific column and data set
|
|
5704
|
+
* @param {Object} column - Column definition with aggregate property
|
|
5705
|
+
* @param {Array} data - Array of data items to aggregate
|
|
5706
|
+
* @returns {number|null} Calculated aggregate value
|
|
5707
|
+
*/
|
|
5708
|
+
calculateAggregate(column, data) {
|
|
5709
|
+
if (!column.aggregate || !data || data.length === 0) {
|
|
5710
|
+
return null;
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5713
|
+
const field = column.field;
|
|
5714
|
+
const aggregateType = column.aggregate.toLowerCase();
|
|
5715
|
+
|
|
5716
|
+
// Extract numeric values from data
|
|
5717
|
+
const values = data
|
|
5718
|
+
.map(item => item[field])
|
|
5719
|
+
.filter(val => val !== null && val !== undefined && !isNaN(parseFloat(val)))
|
|
5720
|
+
.map(val => parseFloat(val));
|
|
5721
|
+
|
|
5722
|
+
if (values.length === 0 && aggregateType !== 'count') {
|
|
5723
|
+
return null;
|
|
5724
|
+
}
|
|
5725
|
+
|
|
5726
|
+
switch (aggregateType) {
|
|
5727
|
+
case 'sum':
|
|
5728
|
+
return values.reduce((sum, val) => sum + val, 0);
|
|
5729
|
+
|
|
5730
|
+
case 'avg':
|
|
5731
|
+
case 'average':
|
|
5732
|
+
return values.length > 0 ? values.reduce((sum, val) => sum + val, 0) / values.length : null;
|
|
5733
|
+
|
|
5734
|
+
case 'count':
|
|
5735
|
+
return data.length;
|
|
5736
|
+
|
|
5737
|
+
case 'min':
|
|
5738
|
+
return values.length > 0 ? Math.min(...values) : null;
|
|
5739
|
+
|
|
5740
|
+
case 'max':
|
|
5741
|
+
return values.length > 0 ? Math.max(...values) : null;
|
|
5742
|
+
|
|
5743
|
+
default:
|
|
5744
|
+
console.warn(`DivTable: Unknown aggregate type '${aggregateType}' for column '${field}'`);
|
|
5745
|
+
return null;
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5748
|
+
|
|
5749
|
+
/**
|
|
5750
|
+
* Get the data set to use for aggregation, considering selection state
|
|
5751
|
+
* For header summary: all filtered data, or selected rows if any are selected
|
|
5752
|
+
* @param {Array} dataToRender - The currently displayed data
|
|
5753
|
+
* @returns {Array} Data set to use for aggregation
|
|
5754
|
+
*/
|
|
5755
|
+
getAggregationDataSet(dataToRender) {
|
|
5756
|
+
// If there are selected rows, aggregate only selected rows
|
|
5757
|
+
if (this.selectedRows.size > 0) {
|
|
5758
|
+
return dataToRender.filter(item => {
|
|
5759
|
+
const itemId = String(item[this.primaryKeyField]);
|
|
5760
|
+
return this.selectedRows.has(itemId);
|
|
5761
|
+
});
|
|
5762
|
+
}
|
|
5763
|
+
// Otherwise aggregate all displayed data
|
|
5764
|
+
return dataToRender;
|
|
5765
|
+
}
|
|
5766
|
+
|
|
5767
|
+
/**
|
|
5768
|
+
* Format aggregate value for display
|
|
5769
|
+
* @param {number|null} value - The aggregate value
|
|
5770
|
+
* @param {Object} column - Column definition
|
|
5771
|
+
* @returns {string} Formatted value string
|
|
5772
|
+
*/
|
|
5773
|
+
formatAggregateValue(value, column) {
|
|
5774
|
+
if (value === null || value === undefined) {
|
|
5775
|
+
return '';
|
|
5776
|
+
}
|
|
5777
|
+
|
|
5778
|
+
// Use column's render function if available for formatting
|
|
5779
|
+
// But only pass the value, not row data
|
|
5780
|
+
if (typeof column.aggregateRender === 'function') {
|
|
5781
|
+
return column.aggregateRender(value);
|
|
5782
|
+
}
|
|
5783
|
+
|
|
5784
|
+
// Default formatting based on aggregate type
|
|
5785
|
+
const aggregateType = column.aggregate.toLowerCase();
|
|
5786
|
+
|
|
5787
|
+
if (aggregateType === 'count') {
|
|
5788
|
+
return String(value);
|
|
5789
|
+
}
|
|
5790
|
+
|
|
5791
|
+
// For numeric values, use locale formatting
|
|
5792
|
+
if (typeof value === 'number') {
|
|
5793
|
+
// Check if it's a decimal that needs precision
|
|
5794
|
+
if (aggregateType === 'avg' || aggregateType === 'average') {
|
|
5795
|
+
return value.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
|
5796
|
+
}
|
|
5797
|
+
return value.toLocaleString();
|
|
5798
|
+
}
|
|
5799
|
+
|
|
5800
|
+
return String(value);
|
|
5801
|
+
}
|
|
5802
|
+
|
|
5803
|
+
/**
|
|
5804
|
+
* Create the header summary row (grand total row below header)
|
|
5805
|
+
* @param {Array} dataToRender - Data to calculate aggregates from
|
|
5806
|
+
* @returns {HTMLElement} Summary row element
|
|
5807
|
+
*/
|
|
5808
|
+
createHeaderSummaryRow(dataToRender) {
|
|
5809
|
+
const summaryRow = document.createElement('div');
|
|
5810
|
+
summaryRow.className = 'div-table-row summary-row header-summary';
|
|
5811
|
+
|
|
5812
|
+
const compositeColumns = this.getCompositeColumns();
|
|
5813
|
+
const aggregationData = this.getAggregationDataSet(dataToRender);
|
|
5814
|
+
|
|
5815
|
+
// Build grid template matching header
|
|
5816
|
+
let gridTemplate = '';
|
|
5817
|
+
if (this.showCheckboxes) {
|
|
5818
|
+
gridTemplate = '40px '; // Checkbox column
|
|
5819
|
+
}
|
|
5820
|
+
|
|
5821
|
+
compositeColumns.forEach(composite => {
|
|
5822
|
+
const firstCol = composite.columns[0];
|
|
5823
|
+
const responsive = firstCol.responsive || {};
|
|
5824
|
+
switch (responsive.size) {
|
|
5825
|
+
case 'fixed-narrow': gridTemplate += '80px '; break;
|
|
5826
|
+
case 'fixed-medium': gridTemplate += '120px '; break;
|
|
5827
|
+
case 'flexible-small': gridTemplate += '1fr '; break;
|
|
5828
|
+
case 'flexible-medium': gridTemplate += '2fr '; break;
|
|
5829
|
+
case 'flexible-large': gridTemplate += '3fr '; break;
|
|
5830
|
+
default: gridTemplate += '1fr ';
|
|
5831
|
+
}
|
|
5832
|
+
});
|
|
5833
|
+
|
|
5834
|
+
summaryRow.style.gridTemplateColumns = gridTemplate.trim();
|
|
5835
|
+
|
|
5836
|
+
// Empty checkbox cell
|
|
5837
|
+
if (this.showCheckboxes) {
|
|
5838
|
+
const emptyCell = document.createElement('div');
|
|
5839
|
+
emptyCell.className = 'div-table-cell checkbox-column summary-cell';
|
|
5840
|
+
summaryRow.appendChild(emptyCell);
|
|
5841
|
+
}
|
|
5842
|
+
|
|
5843
|
+
// Create cells for each composite column
|
|
5844
|
+
compositeColumns.forEach(composite => {
|
|
5845
|
+
const cell = document.createElement('div');
|
|
5846
|
+
cell.className = 'div-table-cell summary-cell';
|
|
5847
|
+
|
|
5848
|
+
if (composite.compositeName) {
|
|
5849
|
+
// Composite column - check each sub-column for aggregates
|
|
5850
|
+
cell.classList.add('composite-cell');
|
|
5851
|
+
|
|
5852
|
+
composite.columns.forEach((col, idx) => {
|
|
5853
|
+
const subCell = document.createElement('div');
|
|
5854
|
+
subCell.className = 'composite-sub-cell';
|
|
5855
|
+
|
|
5856
|
+
if (col.aggregate) {
|
|
5857
|
+
const aggregateValue = this.calculateAggregate(col, aggregationData);
|
|
5858
|
+
const formattedValue = this.formatAggregateValue(aggregateValue, col);
|
|
5859
|
+
subCell.innerHTML = formattedValue;
|
|
5860
|
+
subCell.classList.add('aggregate-value');
|
|
5861
|
+
}
|
|
5862
|
+
|
|
5863
|
+
cell.appendChild(subCell);
|
|
5864
|
+
});
|
|
5865
|
+
} else {
|
|
5866
|
+
// Single column
|
|
5867
|
+
const col = composite.columns[0];
|
|
5868
|
+
|
|
5869
|
+
if (col.aggregate) {
|
|
5870
|
+
const aggregateValue = this.calculateAggregate(col, aggregationData);
|
|
5871
|
+
const formattedValue = this.formatAggregateValue(aggregateValue, col);
|
|
5872
|
+
cell.innerHTML = formattedValue;
|
|
5873
|
+
cell.classList.add('aggregate-value');
|
|
5874
|
+
}
|
|
5875
|
+
}
|
|
5876
|
+
|
|
5877
|
+
summaryRow.appendChild(cell);
|
|
5878
|
+
});
|
|
5879
|
+
|
|
5880
|
+
return summaryRow;
|
|
5881
|
+
}
|
|
5882
|
+
|
|
5883
|
+
/**
|
|
5884
|
+
* Create a group summary row (subtotal row after group items)
|
|
5885
|
+
* @param {Object} group - Group object with items array
|
|
5886
|
+
* @returns {HTMLElement} Group summary row element
|
|
5887
|
+
*/
|
|
5888
|
+
createGroupSummaryRow(group) {
|
|
5889
|
+
const summaryRow = document.createElement('div');
|
|
5890
|
+
summaryRow.className = 'div-table-row summary-row group-summary';
|
|
5891
|
+
summaryRow.dataset.groupKey = group.key;
|
|
5892
|
+
|
|
5893
|
+
const compositeColumns = this.getCompositeColumns();
|
|
5894
|
+
|
|
5895
|
+
// For group summary, aggregate only the items in this group
|
|
5896
|
+
// But also respect selection: if items are selected, only aggregate selected items in this group
|
|
5897
|
+
let groupData = group.items;
|
|
5898
|
+
if (this.selectedRows.size > 0) {
|
|
5899
|
+
groupData = group.items.filter(item => {
|
|
5900
|
+
const itemId = String(item[this.primaryKeyField]);
|
|
5901
|
+
return this.selectedRows.has(itemId);
|
|
5902
|
+
});
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5905
|
+
// Build grid template matching body rows
|
|
5906
|
+
let gridTemplate = '';
|
|
5907
|
+
if (this.showCheckboxes) {
|
|
5908
|
+
gridTemplate = '40px ';
|
|
5909
|
+
}
|
|
5910
|
+
|
|
5911
|
+
compositeColumns.forEach(composite => {
|
|
5912
|
+
const firstCol = composite.columns[0];
|
|
5913
|
+
const responsive = firstCol.responsive || {};
|
|
5914
|
+
switch (responsive.size) {
|
|
5915
|
+
case 'fixed-narrow': gridTemplate += '80px '; break;
|
|
5916
|
+
case 'fixed-medium': gridTemplate += '120px '; break;
|
|
5917
|
+
case 'flexible-small': gridTemplate += '1fr '; break;
|
|
5918
|
+
case 'flexible-medium': gridTemplate += '2fr '; break;
|
|
5919
|
+
case 'flexible-large': gridTemplate += '3fr '; break;
|
|
5920
|
+
default: gridTemplate += '1fr ';
|
|
5921
|
+
}
|
|
5922
|
+
});
|
|
5923
|
+
|
|
5924
|
+
summaryRow.style.gridTemplateColumns = gridTemplate.trim();
|
|
5925
|
+
|
|
5926
|
+
// Empty checkbox cell
|
|
5927
|
+
if (this.showCheckboxes) {
|
|
5928
|
+
const emptyCell = document.createElement('div');
|
|
5929
|
+
emptyCell.className = 'div-table-cell checkbox-column summary-cell';
|
|
5930
|
+
summaryRow.appendChild(emptyCell);
|
|
5931
|
+
}
|
|
5932
|
+
|
|
5933
|
+
// Create cells for each composite column
|
|
5934
|
+
compositeColumns.forEach(composite => {
|
|
5935
|
+
const cell = document.createElement('div');
|
|
5936
|
+
cell.className = 'div-table-cell summary-cell';
|
|
4864
5937
|
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
5938
|
+
if (composite.compositeName) {
|
|
5939
|
+
cell.classList.add('composite-cell');
|
|
5940
|
+
|
|
5941
|
+
composite.columns.forEach(col => {
|
|
5942
|
+
const subCell = document.createElement('div');
|
|
5943
|
+
subCell.className = 'composite-sub-cell';
|
|
5944
|
+
|
|
5945
|
+
if (col.aggregate) {
|
|
5946
|
+
const aggregateValue = this.calculateAggregate(col, groupData);
|
|
5947
|
+
const formattedValue = this.formatAggregateValue(aggregateValue, col);
|
|
5948
|
+
subCell.innerHTML = formattedValue;
|
|
5949
|
+
subCell.classList.add('aggregate-value');
|
|
5950
|
+
}
|
|
5951
|
+
|
|
5952
|
+
cell.appendChild(subCell);
|
|
5953
|
+
});
|
|
4868
5954
|
} else {
|
|
4869
|
-
|
|
4870
|
-
|
|
5955
|
+
const col = composite.columns[0];
|
|
5956
|
+
|
|
5957
|
+
if (col.aggregate) {
|
|
5958
|
+
const aggregateValue = this.calculateAggregate(col, groupData);
|
|
5959
|
+
const formattedValue = this.formatAggregateValue(aggregateValue, col);
|
|
5960
|
+
cell.innerHTML = formattedValue;
|
|
5961
|
+
cell.classList.add('aggregate-value');
|
|
5962
|
+
}
|
|
4871
5963
|
}
|
|
4872
5964
|
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
if (!skipInfoUpdate) {
|
|
4876
|
-
this.updateInfoSection();
|
|
4877
|
-
this.render();
|
|
4878
|
-
} else {
|
|
4879
|
-
// Still need to render the data, just skip the info section update
|
|
4880
|
-
this.render();
|
|
4881
|
-
}
|
|
4882
|
-
}
|
|
5965
|
+
summaryRow.appendChild(cell);
|
|
5966
|
+
});
|
|
4883
5967
|
|
|
4884
|
-
return
|
|
4885
|
-
added: addedCount,
|
|
4886
|
-
updated: updatedCount,
|
|
4887
|
-
skipped: invalid.length,
|
|
4888
|
-
invalid
|
|
4889
|
-
};
|
|
5968
|
+
return summaryRow;
|
|
4890
5969
|
}
|
|
4891
5970
|
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
5971
|
+
/**
|
|
5972
|
+
* Create header summary row pair for fixed columns layout
|
|
5973
|
+
* @param {Array} dataToRender - Data to calculate aggregates from
|
|
5974
|
+
* @returns {Object} Object with fixedSummary and scrollSummary elements
|
|
5975
|
+
*/
|
|
5976
|
+
createHeaderSummaryRowWithFixedColumns(dataToRender) {
|
|
5977
|
+
const { fixedColumns, scrollColumns } = this.splitColumnsForFixedLayout();
|
|
5978
|
+
const aggregationData = this.getAggregationDataSet(dataToRender);
|
|
5979
|
+
|
|
5980
|
+
// Fixed section summary row
|
|
5981
|
+
const fixedSummary = document.createElement('div');
|
|
5982
|
+
fixedSummary.className = 'div-table-row div-table-fixed-row summary-row header-summary';
|
|
5983
|
+
|
|
5984
|
+
let fixedGridTemplate = '';
|
|
5985
|
+
if (this.showCheckboxes) {
|
|
5986
|
+
fixedGridTemplate = '40px ';
|
|
4896
5987
|
}
|
|
4897
|
-
|
|
4898
|
-
// Validate data integrity and check for duplicates within the new data
|
|
4899
|
-
const duplicates = [];
|
|
4900
|
-
const seenIds = new Set();
|
|
4901
|
-
const validRecords = [];
|
|
4902
5988
|
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
// Check for duplicate primary key within the new data
|
|
4916
|
-
const recordId = String(record[this.primaryKeyField]);
|
|
4917
|
-
if (seenIds.has(recordId)) {
|
|
4918
|
-
duplicates.push(recordId);
|
|
4919
|
-
console.warn(`replaceData: Skipping duplicate ${this.primaryKeyField} '${recordId}' within new data`);
|
|
4920
|
-
continue;
|
|
4921
|
-
}
|
|
4922
|
-
|
|
4923
|
-
seenIds.add(recordId);
|
|
4924
|
-
validRecords.push(record);
|
|
5989
|
+
fixedColumns.forEach(composite => {
|
|
5990
|
+
fixedGridTemplate += this.getColumnGridSize(composite) + ' ';
|
|
5991
|
+
});
|
|
5992
|
+
|
|
5993
|
+
fixedSummary.style.gridTemplateColumns = fixedGridTemplate.trim();
|
|
5994
|
+
|
|
5995
|
+
// Empty checkbox cell in fixed section
|
|
5996
|
+
if (this.showCheckboxes) {
|
|
5997
|
+
const emptyCell = document.createElement('div');
|
|
5998
|
+
emptyCell.className = 'div-table-cell checkbox-column summary-cell';
|
|
5999
|
+
fixedSummary.appendChild(emptyCell);
|
|
4925
6000
|
}
|
|
4926
|
-
|
|
4927
|
-
this.data = validRecords;
|
|
4928
|
-
this.isLoadingState = false;
|
|
4929
|
-
this.clearRefreshButtonLoadingState();
|
|
4930
6001
|
|
|
4931
|
-
|
|
6002
|
+
// Fixed columns cells
|
|
6003
|
+
fixedColumns.forEach(composite => {
|
|
6004
|
+
const cell = this.createSummaryCell(composite, aggregationData);
|
|
6005
|
+
fixedSummary.appendChild(cell);
|
|
6006
|
+
});
|
|
4932
6007
|
|
|
4933
|
-
|
|
6008
|
+
// Scroll section summary row
|
|
6009
|
+
const scrollSummary = document.createElement('div');
|
|
6010
|
+
scrollSummary.className = 'div-table-row summary-row header-summary';
|
|
4934
6011
|
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
6012
|
+
let scrollGridTemplate = '';
|
|
6013
|
+
scrollColumns.forEach(composite => {
|
|
6014
|
+
scrollGridTemplate += this.getColumnGridSize(composite) + ' ';
|
|
6015
|
+
});
|
|
6016
|
+
|
|
6017
|
+
scrollSummary.style.gridTemplateColumns = scrollGridTemplate.trim();
|
|
6018
|
+
|
|
6019
|
+
// Scroll columns cells
|
|
6020
|
+
scrollColumns.forEach(composite => {
|
|
6021
|
+
const cell = this.createSummaryCell(composite, aggregationData);
|
|
6022
|
+
scrollSummary.appendChild(cell);
|
|
6023
|
+
});
|
|
6024
|
+
|
|
6025
|
+
return { fixedSummary, scrollSummary };
|
|
6026
|
+
}
|
|
6027
|
+
|
|
6028
|
+
/**
|
|
6029
|
+
* Create group summary row pair for fixed columns layout
|
|
6030
|
+
* @param {Object} group - Group object with items array
|
|
6031
|
+
* @returns {Object} Object with fixedSummary and scrollSummary elements
|
|
6032
|
+
*/
|
|
6033
|
+
createGroupSummaryRowWithFixedColumns(group) {
|
|
6034
|
+
const { fixedColumns, scrollColumns } = this.splitColumnsForFixedLayout();
|
|
6035
|
+
|
|
6036
|
+
// Get data for this group, considering selection
|
|
6037
|
+
let groupData = group.items;
|
|
6038
|
+
if (this.selectedRows.size > 0) {
|
|
6039
|
+
groupData = group.items.filter(item => {
|
|
6040
|
+
const itemId = String(item[this.primaryKeyField]);
|
|
6041
|
+
return this.selectedRows.has(itemId);
|
|
6042
|
+
});
|
|
4939
6043
|
}
|
|
4940
6044
|
|
|
4941
|
-
|
|
6045
|
+
// Fixed section summary row
|
|
6046
|
+
const fixedSummary = document.createElement('div');
|
|
6047
|
+
fixedSummary.className = 'div-table-row div-table-fixed-row summary-row group-summary';
|
|
6048
|
+
fixedSummary.dataset.groupKey = group.key;
|
|
4942
6049
|
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
isLoading: false,
|
|
4948
|
-
};
|
|
6050
|
+
let fixedGridTemplate = '';
|
|
6051
|
+
if (this.showCheckboxes) {
|
|
6052
|
+
fixedGridTemplate = '40px ';
|
|
6053
|
+
}
|
|
4949
6054
|
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
6055
|
+
fixedColumns.forEach(composite => {
|
|
6056
|
+
fixedGridTemplate += this.getColumnGridSize(composite) + ' ';
|
|
6057
|
+
});
|
|
4953
6058
|
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
6059
|
+
fixedSummary.style.gridTemplateColumns = fixedGridTemplate.trim();
|
|
6060
|
+
|
|
6061
|
+
// Empty checkbox cell
|
|
6062
|
+
if (this.showCheckboxes) {
|
|
6063
|
+
const emptyCell = document.createElement('div');
|
|
6064
|
+
emptyCell.className = 'div-table-cell checkbox-column summary-cell';
|
|
6065
|
+
fixedSummary.appendChild(emptyCell);
|
|
4957
6066
|
}
|
|
4958
6067
|
|
|
4959
|
-
//
|
|
4960
|
-
|
|
4961
|
-
|
|
6068
|
+
// Fixed columns cells
|
|
6069
|
+
fixedColumns.forEach(composite => {
|
|
6070
|
+
const cell = this.createSummaryCell(composite, groupData);
|
|
6071
|
+
fixedSummary.appendChild(cell);
|
|
6072
|
+
});
|
|
4962
6073
|
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
6074
|
+
// Scroll section summary row
|
|
6075
|
+
const scrollSummary = document.createElement('div');
|
|
6076
|
+
scrollSummary.className = 'div-table-row summary-row group-summary';
|
|
6077
|
+
scrollSummary.dataset.groupKey = group.key;
|
|
6078
|
+
|
|
6079
|
+
let scrollGridTemplate = '';
|
|
6080
|
+
scrollColumns.forEach(composite => {
|
|
6081
|
+
scrollGridTemplate += this.getColumnGridSize(composite) + ' ';
|
|
6082
|
+
});
|
|
6083
|
+
|
|
6084
|
+
scrollSummary.style.gridTemplateColumns = scrollGridTemplate.trim();
|
|
6085
|
+
|
|
6086
|
+
// Scroll columns cells
|
|
6087
|
+
scrollColumns.forEach(composite => {
|
|
6088
|
+
const cell = this.createSummaryCell(composite, groupData);
|
|
6089
|
+
scrollSummary.appendChild(cell);
|
|
6090
|
+
});
|
|
6091
|
+
|
|
6092
|
+
return { fixedSummary, scrollSummary };
|
|
4970
6093
|
}
|
|
4971
6094
|
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
6095
|
+
/**
|
|
6096
|
+
* Create a summary cell for a composite column
|
|
6097
|
+
* @param {Object} composite - Composite column definition
|
|
6098
|
+
* @param {Array} data - Data to aggregate
|
|
6099
|
+
* @returns {HTMLElement} Cell element
|
|
6100
|
+
*/
|
|
6101
|
+
createSummaryCell(composite, data) {
|
|
6102
|
+
const cell = document.createElement('div');
|
|
6103
|
+
cell.className = 'div-table-cell summary-cell';
|
|
4979
6104
|
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
6105
|
+
if (composite.compositeName) {
|
|
6106
|
+
cell.classList.add('composite-cell');
|
|
6107
|
+
|
|
6108
|
+
composite.columns.forEach(col => {
|
|
6109
|
+
const subCell = document.createElement('div');
|
|
6110
|
+
subCell.className = 'composite-sub-cell';
|
|
6111
|
+
|
|
6112
|
+
if (col.aggregate) {
|
|
6113
|
+
const aggregateValue = this.calculateAggregate(col, data);
|
|
6114
|
+
const formattedValue = this.formatAggregateValue(aggregateValue, col);
|
|
6115
|
+
subCell.innerHTML = formattedValue;
|
|
6116
|
+
subCell.classList.add('aggregate-value');
|
|
6117
|
+
// Apply column alignment
|
|
6118
|
+
if (col.align) {
|
|
6119
|
+
subCell.style.textAlign = col.align;
|
|
6120
|
+
}
|
|
6121
|
+
}
|
|
6122
|
+
|
|
6123
|
+
cell.appendChild(subCell);
|
|
6124
|
+
});
|
|
6125
|
+
} else {
|
|
6126
|
+
const col = composite.columns[0];
|
|
6127
|
+
|
|
6128
|
+
// Apply column alignment to summary cell
|
|
6129
|
+
if (col.align) {
|
|
6130
|
+
cell.style.textAlign = col.align;
|
|
6131
|
+
cell.style.justifyContent = col.align === 'right' ? 'flex-end' :
|
|
6132
|
+
col.align === 'center' ? 'center' : 'flex-start';
|
|
6133
|
+
}
|
|
6134
|
+
|
|
6135
|
+
if (col.aggregate) {
|
|
6136
|
+
const aggregateValue = this.calculateAggregate(col, data);
|
|
6137
|
+
const formattedValue = this.formatAggregateValue(aggregateValue, col);
|
|
6138
|
+
cell.innerHTML = formattedValue;
|
|
6139
|
+
cell.classList.add('aggregate-value');
|
|
6140
|
+
}
|
|
4983
6141
|
}
|
|
4984
6142
|
|
|
4985
|
-
|
|
4986
|
-
|
|
6143
|
+
return cell;
|
|
6144
|
+
}
|
|
6145
|
+
|
|
6146
|
+
/**
|
|
6147
|
+
* Update all summary rows (called when selection changes)
|
|
6148
|
+
* Re-calculates aggregates based on current selection state
|
|
6149
|
+
*/
|
|
6150
|
+
updateSummaryRows() {
|
|
6151
|
+
if (!this.hasAggregateColumns()) return;
|
|
6152
|
+
if (!this.showHeaderSummary && !this.showGroupSummary) return;
|
|
4987
6153
|
|
|
4988
|
-
//
|
|
4989
|
-
this.
|
|
6154
|
+
// Get current data to render
|
|
6155
|
+
let dataToRender = this.filteredData;
|
|
6156
|
+
if (this.showOnlySelected && this.selectedRows.size > 0) {
|
|
6157
|
+
dataToRender = this.filteredData.filter(item => {
|
|
6158
|
+
const itemId = String(item[this.primaryKeyField]);
|
|
6159
|
+
return this.selectedRows.has(itemId);
|
|
6160
|
+
});
|
|
6161
|
+
}
|
|
6162
|
+
|
|
6163
|
+
const aggregationData = this.getAggregationDataSet(dataToRender);
|
|
6164
|
+
|
|
6165
|
+
// Update header summary row
|
|
6166
|
+
if (this.showHeaderSummary) {
|
|
6167
|
+
this.updateHeaderSummaryValues(aggregationData);
|
|
6168
|
+
}
|
|
6169
|
+
|
|
6170
|
+
// Update group summary rows
|
|
6171
|
+
if (this.showGroupSummary && this.groupByField) {
|
|
6172
|
+
this.updateGroupSummaryValues(dataToRender);
|
|
6173
|
+
}
|
|
4990
6174
|
}
|
|
4991
6175
|
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
6176
|
+
/**
|
|
6177
|
+
* Update aggregate values in the header summary row
|
|
6178
|
+
* @param {Array} aggregationData - Data to aggregate
|
|
6179
|
+
*/
|
|
6180
|
+
updateHeaderSummaryValues(aggregationData) {
|
|
6181
|
+
const summaryRows = this.fixedColumns > 0
|
|
6182
|
+
? [
|
|
6183
|
+
this.fixedBodyContainer?.querySelector('.header-summary'),
|
|
6184
|
+
this.scrollBodyContainer?.querySelector('.header-summary')
|
|
6185
|
+
].filter(Boolean)
|
|
6186
|
+
: [this.bodyContainer?.querySelector('.header-summary')].filter(Boolean);
|
|
6187
|
+
|
|
6188
|
+
summaryRows.forEach(summaryRow => {
|
|
6189
|
+
const aggregateCells = summaryRow.querySelectorAll('.aggregate-value');
|
|
6190
|
+
const aggregateColumns = this.getAggregateColumns();
|
|
6191
|
+
|
|
6192
|
+
aggregateCells.forEach((cell, index) => {
|
|
6193
|
+
if (aggregateColumns[index]) {
|
|
6194
|
+
const col = aggregateColumns[index];
|
|
6195
|
+
const aggregateValue = this.calculateAggregate(col, aggregationData);
|
|
6196
|
+
const formattedValue = this.formatAggregateValue(aggregateValue, col);
|
|
6197
|
+
cell.innerHTML = formattedValue;
|
|
6198
|
+
}
|
|
6199
|
+
});
|
|
6200
|
+
});
|
|
6201
|
+
}
|
|
6202
|
+
|
|
6203
|
+
/**
|
|
6204
|
+
* Update aggregate values in all group summary rows
|
|
6205
|
+
* @param {Array} dataToRender - Current displayed data
|
|
6206
|
+
*/
|
|
6207
|
+
updateGroupSummaryValues(dataToRender) {
|
|
6208
|
+
const groups = this.groupData(dataToRender);
|
|
6209
|
+
|
|
6210
|
+
groups.forEach(group => {
|
|
6211
|
+
let groupData = group.items;
|
|
6212
|
+
if (this.selectedRows.size > 0) {
|
|
6213
|
+
groupData = group.items.filter(item => {
|
|
6214
|
+
const itemId = String(item[this.primaryKeyField]);
|
|
6215
|
+
return this.selectedRows.has(itemId);
|
|
6216
|
+
});
|
|
6217
|
+
}
|
|
6218
|
+
|
|
6219
|
+
const groupSummaries = this.fixedColumns > 0
|
|
6220
|
+
? [
|
|
6221
|
+
this.fixedBodyContainer?.querySelector(`.group-summary[data-group-key="${group.key}"]`),
|
|
6222
|
+
this.scrollBodyContainer?.querySelector(`.group-summary[data-group-key="${group.key}"]`)
|
|
6223
|
+
].filter(Boolean)
|
|
6224
|
+
: [this.bodyContainer?.querySelector(`.group-summary[data-group-key="${group.key}"]`)].filter(Boolean);
|
|
6225
|
+
|
|
6226
|
+
groupSummaries.forEach(summaryRow => {
|
|
6227
|
+
const aggregateCells = summaryRow.querySelectorAll('.aggregate-value');
|
|
6228
|
+
const aggregateColumns = this.getAggregateColumns();
|
|
6229
|
+
|
|
6230
|
+
aggregateCells.forEach((cell, index) => {
|
|
6231
|
+
if (aggregateColumns[index]) {
|
|
6232
|
+
const col = aggregateColumns[index];
|
|
6233
|
+
const aggregateValue = this.calculateAggregate(col, groupData);
|
|
6234
|
+
const formattedValue = this.formatAggregateValue(aggregateValue, col);
|
|
6235
|
+
cell.innerHTML = formattedValue;
|
|
6236
|
+
}
|
|
6237
|
+
});
|
|
6238
|
+
});
|
|
6239
|
+
});
|
|
4995
6240
|
}
|
|
4996
6241
|
}
|
|
4997
6242
|
|