@appius-fr/apx 2.7.0 → 2.7.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 +19 -0
- package/dist/APX.dev.mjs +427 -41
- package/dist/APX.mjs +1 -1
- package/dist/APX.prod.mjs +1 -1
- package/dist/APX.standalone.js +488 -46
- package/dist/APX.standalone.js.map +1 -1
- package/modules/scrollableTable/CHANGELOG.md +37 -0
- package/modules/scrollableTable/README.md +59 -3
- package/modules/scrollableTable/css/scrollableTable.css +23 -16
- package/modules/scrollableTable/scrollableTable.mjs +402 -23
- package/package.json +1 -1
|
@@ -5,32 +5,112 @@ const CLASS_TABLE = 'apx-scrollable-table';
|
|
|
5
5
|
|
|
6
6
|
const DEFAULT_MAX_HEIGHT = '200px';
|
|
7
7
|
|
|
8
|
+
const THROTTLE_MS = 16;
|
|
9
|
+
|
|
10
|
+
/** @type {Set<HTMLTableElement>} */
|
|
11
|
+
const dynamicTables = new Set();
|
|
12
|
+
/** @type {Map<Element, Set<HTMLTableElement>>} */
|
|
13
|
+
const scrollSourceToTables = new Map();
|
|
14
|
+
/** @type {Map<Element, Set<HTMLTableElement>>} */
|
|
15
|
+
const resizeSourceToTables = new Map();
|
|
16
|
+
/** @type {boolean} */
|
|
17
|
+
let resizeWindowAttached = false;
|
|
18
|
+
/** @type {ResizeObserver|null} */
|
|
19
|
+
let resizeObserver = null;
|
|
20
|
+
/** @type {number} */
|
|
21
|
+
let throttleLast = 0;
|
|
22
|
+
/** @type {number|null} */
|
|
23
|
+
let throttleRaf = null;
|
|
24
|
+
|
|
8
25
|
/**
|
|
9
|
-
*
|
|
10
|
-
* @param {
|
|
26
|
+
* Sum of colspans for one row (direct th/td children of tr).
|
|
27
|
+
* @param {HTMLTableRowElement} tr
|
|
11
28
|
* @returns {number}
|
|
12
29
|
*/
|
|
13
|
-
function
|
|
14
|
-
const firstRow = section?.querySelector(':scope > tr');
|
|
15
|
-
if (!firstRow) return 0;
|
|
30
|
+
function getRowColumnCount(tr) {
|
|
16
31
|
let cols = 0;
|
|
17
|
-
|
|
32
|
+
tr.querySelectorAll(':scope > th, :scope > td').forEach((cell) => {
|
|
18
33
|
cols += parseInt(cell.getAttribute('colspan'), 10) || 1;
|
|
19
34
|
});
|
|
20
35
|
return cols;
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
/**
|
|
24
|
-
* Get
|
|
39
|
+
* Get maximum column count in a section (max sum of colspans across all rows).
|
|
40
|
+
* @param {HTMLTableSectionElement} section - thead, tbody, or tfoot
|
|
41
|
+
* @returns {number}
|
|
42
|
+
*/
|
|
43
|
+
function getColumnCountFromSection(section) {
|
|
44
|
+
if (!section) return 0;
|
|
45
|
+
const rows = section.querySelectorAll(':scope > tr');
|
|
46
|
+
let maxCols = 0;
|
|
47
|
+
rows.forEach((tr) => {
|
|
48
|
+
const sum = getRowColumnCount(tr);
|
|
49
|
+
if (sum > maxCols) maxCols = sum;
|
|
50
|
+
});
|
|
51
|
+
return maxCols;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get total column count for the table (max across thead, tbody, tfoot).
|
|
25
56
|
* @param {HTMLTableElement} table
|
|
26
57
|
* @returns {number}
|
|
27
58
|
*/
|
|
28
59
|
function getTableColumnCount(table) {
|
|
29
60
|
const thead = table.querySelector('thead');
|
|
30
61
|
const tbody = table.querySelector('tbody');
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
62
|
+
const tfoot = table.querySelector('tfoot');
|
|
63
|
+
const countThead = thead ? getColumnCountFromSection(thead) : 0;
|
|
64
|
+
const countTbody = tbody ? getColumnCountFromSection(tbody) : 0;
|
|
65
|
+
const countTfoot = tfoot ? getColumnCountFromSection(tfoot) : 0;
|
|
66
|
+
return Math.max(countThead, countTbody, countTfoot, 1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Measure current column widths from the table in its natural layout (before applying scrollable class).
|
|
71
|
+
* Prefers a row where each cell has colspan 1 so we get one width per column; otherwise splits cell widths by colspan.
|
|
72
|
+
* @param {HTMLTableElement} table - table not yet with scrollable class
|
|
73
|
+
* @param {number} numCols
|
|
74
|
+
* @returns {number[]} pixel widths per column
|
|
75
|
+
*/
|
|
76
|
+
function measureColumnWidths(table, numCols) {
|
|
77
|
+
const thead = table.querySelector('thead');
|
|
78
|
+
const tbody = table.querySelector('tbody');
|
|
79
|
+
const sections = [thead, tbody].filter(Boolean);
|
|
80
|
+
/** @type {HTMLTableRowElement|null} */
|
|
81
|
+
let bestRow = null;
|
|
82
|
+
for (const section of sections) {
|
|
83
|
+
const rows = section.querySelectorAll(':scope > tr');
|
|
84
|
+
for (const tr of rows) {
|
|
85
|
+
const cells = tr.querySelectorAll(':scope > th, :scope > td');
|
|
86
|
+
if (getRowColumnCount(tr) !== numCols) continue;
|
|
87
|
+
const allSingle = Array.from(cells).every((c) => (parseInt(c.getAttribute('colspan'), 10) || 1) === 1);
|
|
88
|
+
if (allSingle && cells.length === numCols) {
|
|
89
|
+
bestRow = tr;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
if (!bestRow) bestRow = tr;
|
|
93
|
+
}
|
|
94
|
+
if (bestRow && Array.from(bestRow.querySelectorAll(':scope > th, :scope > td')).every((c) => (parseInt(c.getAttribute('colspan'), 10) || 1) === 1))
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
if (!bestRow) return [];
|
|
98
|
+
|
|
99
|
+
const widths = new Array(numCols).fill(0);
|
|
100
|
+
const cells = bestRow.querySelectorAll(':scope > th, :scope > td');
|
|
101
|
+
let col = 0;
|
|
102
|
+
for (const cell of cells) {
|
|
103
|
+
const span = Math.min(parseInt(cell.getAttribute('colspan'), 10) || 1, numCols - col);
|
|
104
|
+
if (span <= 0) break;
|
|
105
|
+
const w = cell.getBoundingClientRect().width;
|
|
106
|
+
const perCol = w / span;
|
|
107
|
+
for (let i = 0; i < span; i++) widths[col + i] = perCol;
|
|
108
|
+
col += span;
|
|
109
|
+
}
|
|
110
|
+
if (col === 0) return [];
|
|
111
|
+
const fallback = widths.some((w) => w > 0) ? Math.max(80, ...widths.filter((w) => w > 0)) / 2 : 80;
|
|
112
|
+
for (let i = 0; i < numCols; i++) if (widths[i] <= 0) widths[i] = fallback;
|
|
113
|
+
return widths.slice(0, numCols);
|
|
34
114
|
}
|
|
35
115
|
|
|
36
116
|
/**
|
|
@@ -108,22 +188,254 @@ function clearPlacements(table) {
|
|
|
108
188
|
}
|
|
109
189
|
|
|
110
190
|
/**
|
|
111
|
-
* Normalize maxHeight option to a CSS length string.
|
|
191
|
+
* Normalize height/maxHeight option to a CSS length string.
|
|
112
192
|
* @param {number|string} value
|
|
113
193
|
* @returns {string}
|
|
114
194
|
*/
|
|
115
|
-
function
|
|
195
|
+
function normalizeHeight(value) {
|
|
116
196
|
if (value == null) return DEFAULT_MAX_HEIGHT;
|
|
117
197
|
if (typeof value === 'number') return `${value}px`;
|
|
118
198
|
return String(value);
|
|
119
199
|
}
|
|
120
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Resolve current body height from options: either bodyHeightDynamic.get(table) or static height/maxHeight.
|
|
203
|
+
* @param {Object} options - scrollableTable options (may include bodyHeightDynamic, height, maxHeight)
|
|
204
|
+
* @param {HTMLTableElement} table
|
|
205
|
+
* @returns {{ bodySize: string, useFixedHeight: boolean }}
|
|
206
|
+
*/
|
|
207
|
+
function resolveBodyHeight(options, table) {
|
|
208
|
+
const dyn = options.bodyHeightDynamic;
|
|
209
|
+
if (dyn && typeof dyn.get === 'function') {
|
|
210
|
+
const value = dyn.get(table);
|
|
211
|
+
const bodySize = normalizeHeight(value);
|
|
212
|
+
const useFixedHeight = dyn.useAs === 'height';
|
|
213
|
+
return { bodySize, useFixedHeight };
|
|
214
|
+
}
|
|
215
|
+
const useFixedHeight = options.height != null;
|
|
216
|
+
const raw = useFixedHeight ? options.height : options.maxHeight;
|
|
217
|
+
const bodySize = normalizeHeight(raw);
|
|
218
|
+
return { bodySize, useFixedHeight };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Update only the tbody height CSS variable and class (used on scroll/resize for dynamic height).
|
|
223
|
+
* @param {HTMLTableElement} table
|
|
224
|
+
* @param {Object} options - full scrollableTable options (with ref.options when called from throttle)
|
|
225
|
+
*/
|
|
226
|
+
function updateTableBodyHeight(table, options) {
|
|
227
|
+
const { bodySize, useFixedHeight } = resolveBodyHeight(options, table);
|
|
228
|
+
table.style.setProperty('--apx-scrollable-body-max-height', bodySize);
|
|
229
|
+
table.classList.toggle('apx-scrollable-table--body-height', useFixedHeight);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Run lazy cleanup then updateTableBodyHeight for all tables still in dynamicTables. (Called by throttled entry point or RAF.)
|
|
234
|
+
*/
|
|
235
|
+
function flushDynamicHeightUpdate() {
|
|
236
|
+
const toRemove = [];
|
|
237
|
+
dynamicTables.forEach((table) => {
|
|
238
|
+
if (!table.isConnected) toRemove.push(table);
|
|
239
|
+
});
|
|
240
|
+
toRemove.forEach((table) => removeTableFromDynamicSources(table));
|
|
241
|
+
dynamicTables.forEach((table) => {
|
|
242
|
+
const ref = table[DATA_KEY];
|
|
243
|
+
if (ref?.options) updateTableBodyHeight(table, ref.options);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Throttled entry: run flushDynamicHeightUpdate now or schedule with RAF.
|
|
249
|
+
*/
|
|
250
|
+
function runDynamicHeightUpdate() {
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
if (now - throttleLast < THROTTLE_MS) {
|
|
253
|
+
if (!throttleRaf) {
|
|
254
|
+
throttleRaf = requestAnimationFrame(() => {
|
|
255
|
+
throttleRaf = null;
|
|
256
|
+
throttleLast = Date.now();
|
|
257
|
+
flushDynamicHeightUpdate();
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
throttleLast = now;
|
|
263
|
+
flushDynamicHeightUpdate();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Resolve updateOn into scroll targets (Element[]) and resize: { window: boolean, elements: Element[] }.
|
|
268
|
+
* @param {Object} options - options.bodyHeightDynamic.updateOn
|
|
269
|
+
* @returns {{ scrollTargets: Element[], resizeWindow: boolean, resizeElements: Element[] }}
|
|
270
|
+
*/
|
|
271
|
+
function resolveUpdateOn(options) {
|
|
272
|
+
const u = options?.bodyHeightDynamic?.updateOn;
|
|
273
|
+
const scrollOn = u?.scrollOn;
|
|
274
|
+
const resizeOn = u?.resizeOn;
|
|
275
|
+
const scrollTargets = [];
|
|
276
|
+
if (scrollOn != null && Array.isArray(scrollOn) && scrollOn.length > 0) {
|
|
277
|
+
scrollOn.forEach((x) => {
|
|
278
|
+
if (x === 'document') scrollTargets.push(document.documentElement);
|
|
279
|
+
else if (x && typeof x.addEventListener === 'function') scrollTargets.push(x);
|
|
280
|
+
});
|
|
281
|
+
} else if (u?.scroll === true) {
|
|
282
|
+
scrollTargets.push(document.documentElement);
|
|
283
|
+
scrollTargets.push(typeof window !== 'undefined' ? window : document.documentElement);
|
|
284
|
+
}
|
|
285
|
+
let resizeWindow = false;
|
|
286
|
+
const resizeElements = [];
|
|
287
|
+
if (resizeOn != null && Array.isArray(resizeOn) && resizeOn.length > 0) {
|
|
288
|
+
resizeOn.forEach((x) => {
|
|
289
|
+
if (x === 'window') resizeWindow = true;
|
|
290
|
+
else if (x && typeof x.addEventListener === 'function') resizeElements.push(x);
|
|
291
|
+
});
|
|
292
|
+
} else if (u?.resize !== false) {
|
|
293
|
+
resizeWindow = true;
|
|
294
|
+
resizeElements.push(document.documentElement);
|
|
295
|
+
}
|
|
296
|
+
return { scrollTargets, resizeWindow, resizeElements };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Remove table from dynamicTables and from all scroll/resize Maps; detach listeners if Set becomes empty.
|
|
301
|
+
* @param {HTMLTableElement} table
|
|
302
|
+
*/
|
|
303
|
+
function removeTableFromDynamicSources(table) {
|
|
304
|
+
dynamicTables.delete(table);
|
|
305
|
+
scrollSourceToTables.forEach((set, el) => {
|
|
306
|
+
set.delete(table);
|
|
307
|
+
if (set.size === 0) {
|
|
308
|
+
scrollSourceToTables.delete(el);
|
|
309
|
+
el.removeEventListener('scroll', onScrollThrottled);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
resizeSourceToTables.forEach((set, el) => {
|
|
313
|
+
set.delete(table);
|
|
314
|
+
if (set.size === 0) {
|
|
315
|
+
resizeSourceToTables.delete(el);
|
|
316
|
+
if (resizeObserver) resizeObserver.unobserve(el);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
if (resizeWindowAttached && dynamicTables.size === 0) {
|
|
320
|
+
window.removeEventListener('resize', onResizeThrottled);
|
|
321
|
+
resizeWindowAttached = false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function onScrollThrottled() {
|
|
326
|
+
runDynamicHeightUpdate();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function onResizeThrottled() {
|
|
330
|
+
runDynamicHeightUpdate();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Register a table with bodyHeightDynamic: add to Set and attach scroll/resize listeners per updateOn.
|
|
335
|
+
* @param {HTMLTableElement} table
|
|
336
|
+
* @param {Object} options - full options (ref.options)
|
|
337
|
+
*/
|
|
338
|
+
function registerDynamicTable(table, options) {
|
|
339
|
+
const { scrollTargets, resizeWindow, resizeElements } = resolveUpdateOn(options);
|
|
340
|
+
dynamicTables.add(table);
|
|
341
|
+
scrollTargets.forEach((el) => {
|
|
342
|
+
let set = scrollSourceToTables.get(el);
|
|
343
|
+
if (!set) {
|
|
344
|
+
set = new Set();
|
|
345
|
+
scrollSourceToTables.set(el, set);
|
|
346
|
+
el.addEventListener('scroll', onScrollThrottled, { passive: true });
|
|
347
|
+
}
|
|
348
|
+
set.add(table);
|
|
349
|
+
});
|
|
350
|
+
if (resizeWindow) {
|
|
351
|
+
if (!resizeWindowAttached) {
|
|
352
|
+
resizeWindowAttached = true;
|
|
353
|
+
window.addEventListener('resize', onResizeThrottled, { passive: true });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
resizeElements.forEach((el) => {
|
|
357
|
+
let set = resizeSourceToTables.get(el);
|
|
358
|
+
if (!set) {
|
|
359
|
+
set = new Set();
|
|
360
|
+
resizeSourceToTables.set(el, set);
|
|
361
|
+
if (!resizeObserver) resizeObserver = new ResizeObserver(onResizeThrottled);
|
|
362
|
+
resizeObserver.observe(el);
|
|
363
|
+
}
|
|
364
|
+
set.add(table);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Build grid-template-columns from measured widths and optional overrides per column.
|
|
370
|
+
* @param {number} numCols
|
|
371
|
+
* @param {number[]} columnWidths
|
|
372
|
+
* @param {Object<number, string>|(string|null)[]} columnOverrides - map column index → CSS value (e.g. '2fr'), or array; null/empty = use measured
|
|
373
|
+
* @returns {string}
|
|
374
|
+
*/
|
|
375
|
+
function buildTemplateColumns(numCols, columnWidths, columnOverrides) {
|
|
376
|
+
if (!columnWidths || columnWidths.length !== numCols) return '';
|
|
377
|
+
const get = (i) =>
|
|
378
|
+
Array.isArray(columnOverrides) ? columnOverrides[i] : columnOverrides[i];
|
|
379
|
+
const parts = [];
|
|
380
|
+
for (let i = 0; i < numCols; i++) {
|
|
381
|
+
const ov = get(i);
|
|
382
|
+
if (ov != null && typeof ov === 'string' && ov.trim() !== '') {
|
|
383
|
+
parts.push(ov.trim());
|
|
384
|
+
} else {
|
|
385
|
+
parts.push(`${Math.round(columnWidths[i])}fr`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return parts.join(' ');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Build grid-template-rows from measured heights and optional overrides per row.
|
|
393
|
+
* @param {number} numRows
|
|
394
|
+
* @param {number[]} rowHeights
|
|
395
|
+
* @param {Object<number, string>|(string|null)[]} rowOverrides - map row index → CSS value (e.g. '48px', '2fr'), or array; null/empty = use measured
|
|
396
|
+
* @returns {string}
|
|
397
|
+
*/
|
|
398
|
+
function buildTemplateRows(numRows, rowHeights, rowOverrides) {
|
|
399
|
+
if (!rowHeights || rowHeights.length < numRows) return '';
|
|
400
|
+
const get = (i) =>
|
|
401
|
+
Array.isArray(rowOverrides) ? rowOverrides[i] : rowOverrides?.[i];
|
|
402
|
+
const parts = [];
|
|
403
|
+
for (let i = 0; i < numRows; i++) {
|
|
404
|
+
const ov = get(i);
|
|
405
|
+
if (ov != null && typeof ov === 'string' && ov.trim() !== '') {
|
|
406
|
+
parts.push(ov.trim());
|
|
407
|
+
} else {
|
|
408
|
+
parts.push(`${Math.round(rowHeights[i] ?? 0)}px`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return parts.join(' ');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Measure each tr height in a section (table must not yet have scrollable class).
|
|
416
|
+
* @param {HTMLTableSectionElement|null} section
|
|
417
|
+
* @returns {number[]}
|
|
418
|
+
*/
|
|
419
|
+
function measureRowHeights(section) {
|
|
420
|
+
if (!section) return [];
|
|
421
|
+
const rows = section.querySelectorAll(':scope > tr');
|
|
422
|
+
return Array.from(rows).map((tr) => tr.getBoundingClientRect().height);
|
|
423
|
+
}
|
|
424
|
+
|
|
121
425
|
/**
|
|
122
426
|
* Apply scrollable table layout to a single table.
|
|
123
427
|
* @param {HTMLTableElement} table
|
|
124
|
-
* @param {
|
|
428
|
+
* @param {Object} options - scrollableTable options
|
|
429
|
+
* @param {number|string} [options.maxHeight] - Max height of tbody (default '200px'). Ignored when height or bodyHeightDynamic is set.
|
|
430
|
+
* @param {number|string} [options.height] - Fixed height of tbody. Ignored when bodyHeightDynamic is set.
|
|
431
|
+
* @param {{ get: (function(HTMLTableElement): number|string), useAs: 'height'|'maxHeight', updateOn?: { scroll?: boolean, resize?: boolean, scrollOn?: ('document'|Element)[], resizeOn?: ('window'|Element)[] } }} [options.bodyHeightDynamic] - When set, body size is computed by get(table) and re-applied when scroll/resize sources fire. updateOn: scrollOn/resizeOn list elements (sentinels 'document'/'window'); or use scroll (default false) / resize (default true) booleans.
|
|
432
|
+
* @param {string} [options.gridTemplateColumns]
|
|
433
|
+
* @param {{ thead?: string, tbody?: string, tfoot?: string }} [options.gridTemplateRows]
|
|
434
|
+
* @param {{ thead?: Object<number, string>|(string|null)[], tbody?: Object<number, string>|(string|null)[], tfoot?: Object<number, string>|(string|null)[] }} [options.rowOverrides]
|
|
435
|
+
* @param {Object<number, string>|(string|null)[]} [options.columnOverrides]
|
|
436
|
+
* @param {{ columnWidths?: number[], rowHeights?: { thead?: number[], tbody?: number[], tfoot?: number[] } } | undefined} ref - existing ref when refreshing
|
|
125
437
|
*/
|
|
126
|
-
function applyScrollableTable(table, options) {
|
|
438
|
+
function applyScrollableTable(table, options, ref) {
|
|
127
439
|
const thead = table.querySelector('thead');
|
|
128
440
|
const tbody = table.querySelector('tbody');
|
|
129
441
|
const tfoot = table.querySelector('tfoot');
|
|
@@ -131,32 +443,88 @@ function applyScrollableTable(table, options) {
|
|
|
131
443
|
const numCols = getTableColumnCount(table);
|
|
132
444
|
if (numCols === 0) return;
|
|
133
445
|
|
|
446
|
+
const alreadyScrollable = table.classList.contains(CLASS_TABLE);
|
|
447
|
+
const customTemplate = typeof options.gridTemplateColumns === 'string' && options.gridTemplateColumns.trim().length > 0;
|
|
448
|
+
const columnWidths = customTemplate
|
|
449
|
+
? null
|
|
450
|
+
: (ref?.columnWidths ?? (alreadyScrollable ? null : measureColumnWidths(table, numCols)));
|
|
451
|
+
|
|
134
452
|
const theadRows = getRowCount(thead);
|
|
135
453
|
const tbodyRows = getRowCount(tbody);
|
|
136
454
|
const tfootRows = getRowCount(tfoot);
|
|
137
455
|
|
|
456
|
+
const rowHeights =
|
|
457
|
+
ref?.rowHeights ??
|
|
458
|
+
(alreadyScrollable
|
|
459
|
+
? null
|
|
460
|
+
: {
|
|
461
|
+
thead: measureRowHeights(thead),
|
|
462
|
+
tbody: measureRowHeights(tbody),
|
|
463
|
+
tfoot: measureRowHeights(tfoot)
|
|
464
|
+
});
|
|
465
|
+
|
|
138
466
|
table.style.setProperty('--apx-scrollable-cols', String(numCols));
|
|
139
467
|
table.style.setProperty('--apx-scrollable-thead-rows', String(Math.max(1, theadRows)));
|
|
140
468
|
table.style.setProperty('--apx-scrollable-tbody-rows', String(Math.max(1, tbodyRows)));
|
|
141
469
|
table.style.setProperty('--apx-scrollable-tfoot-rows', String(Math.max(1, tfootRows)));
|
|
142
|
-
|
|
470
|
+
const { bodySize, useFixedHeight } = resolveBodyHeight(options, table);
|
|
471
|
+
table.style.setProperty('--apx-scrollable-body-max-height', bodySize);
|
|
472
|
+
table.classList.toggle('apx-scrollable-table--body-height', useFixedHeight);
|
|
143
473
|
|
|
144
474
|
table.classList.add(CLASS_TABLE);
|
|
145
475
|
table.classList.toggle('apx-scrollable-table--has-tfoot', !!(tfoot && tfootRows > 0));
|
|
146
476
|
table.classList.toggle('apx-scrollable-table--no-thead', !(thead && theadRows > 0));
|
|
147
477
|
|
|
478
|
+
// Force reflow so the grid layout is established before setting measured template-columns.
|
|
479
|
+
// Without this, the browser batches class + template into one pass and the tbody overflows horizontally.
|
|
480
|
+
table.offsetHeight; // eslint-disable-line no-unused-expressions
|
|
481
|
+
|
|
482
|
+
const gutterSuffix = ' minmax(var(--apx-scrollable-gutter-width, 17px), var(--apx-scrollable-gutter-width, 17px))';
|
|
483
|
+
if (customTemplate) {
|
|
484
|
+
table.style.setProperty('--apx-scrollable-template-columns', options.gridTemplateColumns.trim() + gutterSuffix);
|
|
485
|
+
} else if (columnWidths && columnWidths.length === numCols) {
|
|
486
|
+
const template =
|
|
487
|
+
options.columnOverrides != null
|
|
488
|
+
? buildTemplateColumns(numCols, columnWidths, options.columnOverrides)
|
|
489
|
+
: columnWidths.map((w) => `${Math.round(w)}fr`).join(' ');
|
|
490
|
+
table.style.setProperty('--apx-scrollable-template-columns', template + gutterSuffix);
|
|
491
|
+
} else {
|
|
492
|
+
table.style.removeProperty('--apx-scrollable-template-columns');
|
|
493
|
+
}
|
|
494
|
+
|
|
148
495
|
clearPlacements(table);
|
|
149
496
|
|
|
150
497
|
const sections = [
|
|
151
|
-
{ section: thead, rows: Math.max(1, theadRows) },
|
|
152
|
-
{ section: tbody, rows: Math.max(1, tbodyRows) },
|
|
153
|
-
{ section: tfoot, rows: Math.max(1, tfootRows) }
|
|
498
|
+
{ section: thead, rows: Math.max(1, theadRows), heights: rowHeights?.thead, key: 'thead' },
|
|
499
|
+
{ section: tbody, rows: Math.max(1, tbodyRows), heights: rowHeights?.tbody, key: 'tbody' },
|
|
500
|
+
{ section: tfoot, rows: Math.max(1, tfootRows), heights: rowHeights?.tfoot, key: 'tfoot' }
|
|
154
501
|
];
|
|
155
|
-
|
|
502
|
+
const customRows = options.gridTemplateRows && typeof options.gridTemplateRows === 'object' ? options.gridTemplateRows : null;
|
|
503
|
+
const rowOverrides = options.rowOverrides && typeof options.rowOverrides === 'object' ? options.rowOverrides : null;
|
|
504
|
+
sections.forEach(({ section, rows, heights, key }) => {
|
|
156
505
|
if (!section) return;
|
|
506
|
+
const varName = `--apx-scrollable-${key}-template-rows`;
|
|
507
|
+
const custom = customRows?.[key];
|
|
508
|
+
const overrides = rowOverrides?.[key];
|
|
509
|
+
if (typeof custom === 'string' && custom.trim().length > 0) {
|
|
510
|
+
section.style.setProperty(varName, custom.trim());
|
|
511
|
+
} else if (heights && heights.length >= rows) {
|
|
512
|
+
const template =
|
|
513
|
+
overrides != null
|
|
514
|
+
? buildTemplateRows(rows, heights.slice(0, rows), overrides)
|
|
515
|
+
: heights
|
|
516
|
+
.slice(0, rows)
|
|
517
|
+
.map((h) => `${Math.round(h)}px`)
|
|
518
|
+
.join(' ');
|
|
519
|
+
section.style.setProperty(varName, template);
|
|
520
|
+
} else {
|
|
521
|
+
section.style.removeProperty(varName);
|
|
522
|
+
}
|
|
157
523
|
const placements = computeCellPlacements(section, rows, numCols);
|
|
158
524
|
applyPlacements(placements);
|
|
159
525
|
});
|
|
526
|
+
|
|
527
|
+
return { columnWidths, rowHeights };
|
|
160
528
|
}
|
|
161
529
|
|
|
162
530
|
/**
|
|
@@ -180,17 +548,28 @@ export default function augmentWithScrollableTable(apx) {
|
|
|
180
548
|
const ref = table[DATA_KEY];
|
|
181
549
|
if (ref) {
|
|
182
550
|
if (isRefresh) {
|
|
183
|
-
applyScrollableTable(table, ref.options);
|
|
551
|
+
applyScrollableTable(table, ref.options, ref);
|
|
184
552
|
} else if (options && Object.keys(options).length > 0) {
|
|
185
553
|
ref.options = { ...ref.options, ...options };
|
|
186
|
-
applyScrollableTable(table, ref.options);
|
|
554
|
+
const result = applyScrollableTable(table, ref.options, ref);
|
|
555
|
+
if (result?.columnWidths) ref.columnWidths = result.columnWidths;
|
|
556
|
+
if (result?.rowHeights) ref.rowHeights = result.rowHeights;
|
|
187
557
|
}
|
|
558
|
+
const currentOptions = ref.options;
|
|
559
|
+
if (currentOptions?.bodyHeightDynamic) registerDynamicTable(table, currentOptions);
|
|
560
|
+
else removeTableFromDynamicSources(table);
|
|
188
561
|
return;
|
|
189
562
|
}
|
|
190
563
|
if (isRefresh) return;
|
|
191
564
|
|
|
192
|
-
|
|
193
|
-
|
|
565
|
+
console.log('[APX scrollableTable] create', table);
|
|
566
|
+
const result = applyScrollableTable(table, options, undefined);
|
|
567
|
+
table[DATA_KEY] = {
|
|
568
|
+
options: { ...options },
|
|
569
|
+
columnWidths: result?.columnWidths || undefined,
|
|
570
|
+
rowHeights: result?.rowHeights || undefined
|
|
571
|
+
};
|
|
572
|
+
if (options?.bodyHeightDynamic) registerDynamicTable(table, options);
|
|
194
573
|
});
|
|
195
574
|
|
|
196
575
|
return apx;
|