flexdesk 0.4.5 → 0.4.6

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.
@@ -16,6 +16,7 @@ function createRafResizeObserver(callback) {
16
16
  // src/ui/components/data_table.js
17
17
  var DEFAULT_PAGE_SIZE = 100;
18
18
  var AUTOSIZE_MAX_PX = 520;
19
+ var VIRTUAL_FALLBACK_VIEWPORT_PX = 1e3;
19
20
  var DataTable = class {
20
21
  /**
21
22
  * Create a DataTable instance
@@ -71,8 +72,14 @@ var DataTable = class {
71
72
  // promise that adding the key changed no existing layout by a
72
73
  // pixel. Only a consumer that asks for 'content' sees anything new.
73
74
  columnFit: "container",
75
+ // C10. Off unless asked for — see the typedef.
76
+ virtualize: false,
77
+ virtualRowHeight: null,
78
+ onRowsRendered: null,
74
79
  ...config
75
80
  };
81
+ this._virt = null;
82
+ this._virtBase = null;
76
83
  if (this.config.persistKey && !this.config.stateStore) {
77
84
  throw new Error(`DataTable: persistKey "${this.config.persistKey}" requires { stateStore }`);
78
85
  }
@@ -304,8 +311,11 @@ var DataTable = class {
304
311
  const activeFilterColIdx = this._getActiveFilterColIdx();
305
312
  const doc = this.container.ownerDocument;
306
313
  const hadTableFocus = !!this._tableEl && doc?.activeElement === this._tableEl;
314
+ const prevWrap = this._tableWrapEl;
315
+ const prevScroll = prevWrap?.isConnected && this._renderedOffset === this._state.offset ? { top: prevWrap.scrollTop, left: prevWrap.scrollLeft } : null;
307
316
  this._cleanup();
308
317
  this.container.innerHTML = "";
318
+ this._virt = null;
309
319
  const { rows, pagination, emptyMessage } = this.config;
310
320
  const colSig = this._colSig();
311
321
  if (this._colWidthsSig !== colSig) {
@@ -392,6 +402,18 @@ var DataTable = class {
392
402
  }
393
403
  this._tableWrapEl = tableWrap;
394
404
  this.container.appendChild(this._wrapperEl);
405
+ if (prevScroll && this._state.offset === this._renderedOffset) {
406
+ tableWrap.scrollTop = prevScroll.top;
407
+ tableWrap.scrollLeft = prevScroll.left;
408
+ }
409
+ this._renderedOffset = this._state.offset;
410
+ if (this._virt) {
411
+ const onScroll = () => this._virtUpdate();
412
+ tableWrap.addEventListener("scroll", onScroll, { passive: true });
413
+ this._disposers.push(() => tableWrap.removeEventListener("scroll", onScroll));
414
+ this._virtMeasure();
415
+ this._virtUpdate({ force: true, notify: false });
416
+ }
395
417
  if (this._headerTableEl && this._tableEl) {
396
418
  requestAnimationFrame(() => this._syncHeaderWidths());
397
419
  if (this._resizeObserver) {
@@ -401,9 +423,10 @@ var DataTable = class {
401
423
  }
402
424
  }
403
425
  if (typeof ResizeObserver !== "undefined") {
404
- this._resizeObserver = createRafResizeObserver(
405
- () => this._syncHeaderWidths()
406
- );
426
+ this._resizeObserver = createRafResizeObserver(() => {
427
+ this._syncHeaderWidths();
428
+ this._virtUpdate();
429
+ });
407
430
  this._resizeObserver.observe(this._wrapperEl);
408
431
  }
409
432
  this._installHeaderScrollSync();
@@ -534,6 +557,8 @@ var DataTable = class {
534
557
  this._tableWrapEl = null;
535
558
  this._processedRows = null;
536
559
  this._processedIndexMap = null;
560
+ this._virt = null;
561
+ this._colgroupEl = null;
537
562
  }
538
563
  // ─────────────────────────────────────────────────────────────────
539
564
  // Processed rows pipeline (filter → sort → cache)
@@ -1136,7 +1161,7 @@ var DataTable = class {
1136
1161
  }
1137
1162
  _createTable() {
1138
1163
  const { headers, rows, pageSize, sortable, filterable, showRowNumbers, readonly, getHeaderIcon, onPageChange, offset: configOffset } = this.config;
1139
- const { selected, sortColumn, sortAscending } = this._state;
1164
+ const { sortColumn, sortAscending } = this._state;
1140
1165
  const isServerSide = typeof onPageChange === "function";
1141
1166
  const processedRows = isServerSide ? rows : this._getProcessedRows();
1142
1167
  const offset = isServerSide ? configOffset || 0 : this._state.offset;
@@ -1192,33 +1217,455 @@ var DataTable = class {
1192
1217
  const filterRow = this._createFilterRow(headers);
1193
1218
  thead.appendChild(filterRow);
1194
1219
  }
1220
+ const virtual = this._isVirtual();
1221
+ if (virtual) {
1222
+ table.classList.add("twm-dt--virtual");
1223
+ table.appendChild(this._createColgroup());
1224
+ }
1195
1225
  table.appendChild(thead);
1196
- const tbody = document.createElement("tbody");
1197
- pageRows.forEach((row, localIdx) => {
1226
+ const globalOf = (localIdx) => {
1198
1227
  const processedIdx = offset + localIdx;
1199
- const globalIdx = isServerSide ? (configOffset || 0) + localIdx : this._processedIndexMap?.[processedIdx] ?? processedIdx;
1228
+ return isServerSide ? (configOffset || 0) + localIdx : this._processedIndexMap?.[processedIdx] ?? processedIdx;
1229
+ };
1230
+ const tbody = document.createElement("tbody");
1231
+ table.appendChild(tbody);
1232
+ this._tbodyEl = tbody;
1233
+ if (virtual) {
1234
+ this._virtInit(tbody, pageRows, globalOf, {
1235
+ serverSide: isServerSide,
1236
+ offset,
1237
+ configOffset: configOffset || 0
1238
+ });
1239
+ } else {
1240
+ pageRows.forEach((row, localIdx) => {
1241
+ tbody.appendChild(this._buildBodyRow(row, globalOf(localIdx), localIdx));
1242
+ });
1243
+ }
1244
+ return table;
1245
+ }
1246
+ /** One body row, fully built. The ONE place a `<tr>` for a row is made, so
1247
+ * the full render and the virtual window cannot drift apart: a row drawn
1248
+ * on scroll is the same row a render would have drawn. */
1249
+ _buildBodyRow(row, globalIdx, localIdx) {
1250
+ const { showRowNumbers } = this.config;
1251
+ const tr = document.createElement("tr");
1252
+ tr.__rowIndex = globalIdx;
1253
+ tr.className = "data-preview-row";
1254
+ if (this._state.selected.has(globalIdx)) {
1255
+ tr.classList.add("selected");
1256
+ }
1257
+ if (this._virt) tr.classList.toggle("twm-dt-row--alt", localIdx % 2 === 1);
1258
+ this._fillRowCells(tr, row, globalIdx, showRowNumbers);
1259
+ if (this.config.onRowClick) {
1260
+ tr.addEventListener("click", (ev) => {
1261
+ this.config.onRowClick(globalIdx, row, ev);
1262
+ });
1263
+ }
1264
+ if (this.config.onRowContextMenu) {
1265
+ tr.addEventListener("contextmenu", (ev) => {
1266
+ this.config.onRowContextMenu(globalIdx, row, ev);
1267
+ });
1268
+ }
1269
+ return tr;
1270
+ }
1271
+ // ─────────────────────────────────────────────────────────────────
1272
+ // C10 — the virtual window
1273
+ // ─────────────────────────────────────────────────────────────────
1274
+ //
1275
+ // ══ WHAT IT IS ═══════════════════════════════════════════════════
1276
+ //
1277
+ // Only the rows near the viewport are in the DOM: a TOP SPACER row whose
1278
+ // height is every row above the window, the window's rows, and a BOTTOM
1279
+ // SPACER for everything below it. The scrollbar is therefore sized to the
1280
+ // whole table and every row is reachable, while the DOM stays bounded by
1281
+ // the viewport — a thousand-row page (or fifty of them) costs the same
1282
+ // few dozen `<tr>`s as one screenful.
1283
+ //
1284
+ // ══ WHAT IT PROMISES ═════════════════════════════════════════════
1285
+ //
1286
+ // - A row that stays in the window is NEVER MOVED OR REBUILT when the
1287
+ // window slides. Rows leave from the edges and arrive at the edges; the
1288
+ // ones in the middle are the same nodes. An open editor, a focused
1289
+ // cell, a hover — anything living in a row that is still on screen —
1290
+ // survives a scroll. (Moving a node that holds focus blurs it, which in
1291
+ // an editable grid is a commit nobody asked for.)
1292
+ // - Column widths come from a `<colgroup>`, so the first row changing
1293
+ // moves nothing (see `_createTable`).
1294
+ // - A spacer is in the DOM only while it has a height. A table that fits
1295
+ // in its window has exactly the DOM it had without `virtualize`.
1296
+ // - `onRowsRendered` hears about every row added after `render()`, and
1297
+ // every row taken out.
1298
+ //
1299
+ // ══ WHAT IT ASSUMES ══════════════════════════════════════════════
1300
+ //
1301
+ // That a row's height is knowable without drawing it: `baseHeight`,
1302
+ // measured from a real row, or what `virtualRowHeight` says. A row that
1303
+ // wraps to two lines is drawn correctly and is simply mis-counted in the
1304
+ // spacer arithmetic by the difference — the overscan absorbs that.
1305
+ _isVirtual() {
1306
+ return !!this.config.virtualize;
1307
+ }
1308
+ _virtOptions() {
1309
+ const v = this.config.virtualize;
1310
+ const o = v && typeof v === "object" ? v : {};
1311
+ return {
1312
+ overscan: Number.isFinite(o.overscan) && o.overscan >= 0 ? Math.floor(o.overscan) : 20,
1313
+ rowHeight: Number.isFinite(o.rowHeight) && o.rowHeight > 0 ? o.rowHeight : 24
1314
+ };
1315
+ }
1316
+ /** One `<col>` per DOM column (the row-number column included), carrying
1317
+ * any width already pinned for it, so a re-render starts at the widths it
1318
+ * had rather than at a content-sized frame that then snaps. */
1319
+ _createColgroup() {
1320
+ const n = (this.config.headers?.length || 0) + (this.config.showRowNumbers ? 1 : 0);
1321
+ const colgroup = document.createElement("colgroup");
1322
+ for (let i = 0; i < n; i++) {
1323
+ const col = document.createElement("col");
1324
+ const w = this._colWidths?.[i];
1325
+ if (w != null && this._colWidthsSig === this._colSig()) col.style.width = `${w}px`;
1326
+ colgroup.appendChild(col);
1327
+ }
1328
+ this._colgroupEl = colgroup;
1329
+ return colgroup;
1330
+ }
1331
+ /** The `<col>`s, when this table has them. */
1332
+ _cols() {
1333
+ return this._virt && this._colgroupEl ? this._colgroupEl.children : null;
1334
+ }
1335
+ _virtInit(tbody, rows, globalOf, { serverSide, offset, configOffset }) {
1336
+ const { overscan, rowHeight } = this._virtOptions();
1337
+ const spacer = (edge) => {
1200
1338
  const tr = document.createElement("tr");
1201
- tr.__rowIndex = globalIdx;
1202
- tr.className = "data-preview-row";
1203
- if (selected.has(globalIdx)) {
1204
- tr.classList.add("selected");
1339
+ tr.className = `twm-dt-spacer twm-dt-spacer--${edge}`;
1340
+ tr.setAttribute("aria-hidden", "true");
1341
+ const td = document.createElement("td");
1342
+ td.colSpan = Math.max(1, (this.config.headers?.length || 0) + (this.config.showRowNumbers ? 1 : 0));
1343
+ td.style.cssText = "padding:0;border:0;height:0;min-width:0;max-width:none;";
1344
+ tr.appendChild(td);
1345
+ return tr;
1346
+ };
1347
+ const filler = document.createElement("tr");
1348
+ filler.className = "twm-dt-spacer twm-dt-spacer--parity";
1349
+ filler.setAttribute("aria-hidden", "true");
1350
+ filler.hidden = true;
1351
+ this._virt = {
1352
+ tbody,
1353
+ rows,
1354
+ globalOf,
1355
+ serverSide,
1356
+ offset,
1357
+ configOffset,
1358
+ overscan,
1359
+ base: this._virtBase || rowHeight,
1360
+ heights: null,
1361
+ offsets: null,
1362
+ start: 0,
1363
+ end: 0,
1364
+ map: /* @__PURE__ */ new Map(),
1365
+ top: spacer("top"),
1366
+ bottom: spacer("bottom"),
1367
+ filler
1368
+ };
1369
+ this._virtComputeOffsets();
1370
+ const guess = this.container?.clientHeight || 0;
1371
+ this._virtUpdate({ force: true, notify: false, viewport: guess || void 0, top: 0 });
1372
+ }
1373
+ /** Heights and their running sum. `offsets[i]` is where row `i` starts;
1374
+ * `offsets[n]` is the whole table. O(n), and rerun only when a height can
1375
+ * have changed — a new measurement, or `refreshVirtualLayout()`. */
1376
+ _virtComputeOffsets() {
1377
+ const v = this._virt;
1378
+ if (!v) return;
1379
+ const n = v.rows.length;
1380
+ const heights = new Float64Array(n);
1381
+ const offsets = new Float64Array(n + 1);
1382
+ const fn = typeof this.config.virtualRowHeight === "function" ? this.config.virtualRowHeight : null;
1383
+ let y = 0;
1384
+ for (let i = 0; i < n; i++) {
1385
+ let h = v.base;
1386
+ if (fn) {
1387
+ const asked = fn(v.globalOf(i), v.base);
1388
+ h = Number.isFinite(asked) && asked > 0 ? asked : 0;
1205
1389
  }
1206
- this._fillRowCells(tr, row, globalIdx, showRowNumbers);
1207
- if (this.config.onRowClick) {
1208
- tr.addEventListener("click", (ev) => {
1209
- this.config.onRowClick(globalIdx, row, ev);
1210
- });
1390
+ heights[i] = h;
1391
+ offsets[i] = y;
1392
+ y += h;
1393
+ }
1394
+ offsets[n] = y;
1395
+ v.heights = heights;
1396
+ v.offsets = offsets;
1397
+ }
1398
+ /** The row whose span contains `y`: the LAST `i` with `offsets[i] <= y`,
1399
+ * which steps over rows of zero height at the same offset. */
1400
+ _virtIndexAt(y) {
1401
+ const v = this._virt;
1402
+ const n = v.rows.length;
1403
+ if (n === 0) return 0;
1404
+ let lo = 0;
1405
+ let hi = n - 1;
1406
+ while (lo < hi) {
1407
+ const mid = lo + hi + 1 >> 1;
1408
+ if (v.offsets[mid] <= y) lo = mid;
1409
+ else hi = mid - 1;
1410
+ }
1411
+ return lo;
1412
+ }
1413
+ /** Measure one real row. Client pixels, so the zoom is divided back out —
1414
+ * `_measureNaturalWidths` explains why — and NOT `offsetHeight`, which is
1415
+ * rounded: 21 for a 21.19px row is a 1,100px error over 6,000 rows. */
1416
+ _virtMeasure() {
1417
+ const v = this._virt;
1418
+ if (!v) return false;
1419
+ for (const tr of v.map.values()) {
1420
+ if (!tr.isConnected || tr.hidden) continue;
1421
+ const zoom = this._tableEl?.currentCSSZoom || 1;
1422
+ const h = tr.getBoundingClientRect().height / zoom;
1423
+ if (!(h > 0)) return false;
1424
+ if (Math.abs(h - v.base) > 0.01) {
1425
+ v.base = h;
1426
+ this._virtBase = h;
1427
+ this._virtComputeOffsets();
1428
+ return true;
1211
1429
  }
1212
- if (this.config.onRowContextMenu) {
1213
- tr.addEventListener("contextmenu", (ev) => {
1214
- this.config.onRowContextMenu(globalIdx, row, ev);
1215
- });
1430
+ return false;
1431
+ }
1432
+ return false;
1433
+ }
1434
+ /**
1435
+ * Slide the window to wherever the scroll box now is.
1436
+ *
1437
+ * HYSTERESIS, so a scroll of one pixel does not rebuild anything: the
1438
+ * window is redrawn only when the visible rows come within a quarter of the
1439
+ * overscan of either end of what is drawn, and it is then redrawn with the
1440
+ * full overscan on both sides.
1441
+ *
1442
+ * @param {object} [o]
1443
+ * @param {boolean} [o.force] redraw even inside the hysteresis band.
1444
+ * @param {boolean} [o.notify] report added and removed rows to `onRowsRendered`.
1445
+ * @param {number} [o.top] the scroll offset to draw for, instead of
1446
+ * reading one back — `scrollToRow` passes the offset it has just set,
1447
+ * which a DOM with no layout would read back as 0.
1448
+ * @param {number} [o.viewport] the viewport height, likewise.
1449
+ * @returns {HTMLElement[]} the rows added.
1450
+ */
1451
+ _virtUpdate({ force = false, notify = true, top = null, viewport = null } = {}) {
1452
+ const v = this._virt;
1453
+ if (!v || !v.offsets) return [];
1454
+ const n = v.rows.length;
1455
+ const wrap = this._tableWrapEl;
1456
+ const view = viewport || wrap?.clientHeight || VIRTUAL_FALLBACK_VIEWPORT_PX;
1457
+ const scrollTop = top ?? (wrap?.scrollTop || 0);
1458
+ const vs = this._virtIndexAt(scrollTop);
1459
+ const ve = Math.min(n, this._virtIndexAt(scrollTop + view) + 1);
1460
+ if (!force && v.end > v.start) {
1461
+ const margin = Math.max(1, Math.floor(v.overscan / 4));
1462
+ const lowOk = v.start === 0 || vs >= v.start + margin;
1463
+ const highOk = v.end === n || ve <= v.end - margin;
1464
+ if (lowOk && highOk) return [];
1465
+ }
1466
+ const start = Math.max(0, vs - v.overscan);
1467
+ const end = Math.min(n, ve + v.overscan);
1468
+ const removed = [];
1469
+ const added = this._virtRender(start, end, removed);
1470
+ if (added.length || removed.length) {
1471
+ if (added[0]?.isConnected && this._tableEl?.style.tableLayout === "fixed") {
1472
+ this._updateCellTooltips(added);
1216
1473
  }
1217
- tbody.appendChild(tr);
1218
- });
1219
- table.appendChild(tbody);
1220
- this._tbodyEl = tbody;
1221
- return table;
1474
+ if (notify) {
1475
+ try {
1476
+ this.config.onRowsRendered?.({ rows: added, removed });
1477
+ } catch (err) {
1478
+ console.error("[DataTable] onRowsRendered threw", err);
1479
+ }
1480
+ }
1481
+ }
1482
+ return added;
1483
+ }
1484
+ /** Make the DOM hold exactly the drawable rows of `[start, end)`, without
1485
+ * moving a row that is already there. See the promises above. */
1486
+ _virtRender(start, end, removed = null) {
1487
+ const v = this._virt;
1488
+ const tbody = v.tbody;
1489
+ const want = [];
1490
+ for (let i = start; i < end; i++) if (v.heights[i] > 0) want.push(i);
1491
+ const wanted = new Set(want);
1492
+ for (const [i, tr] of v.map) {
1493
+ if (!wanted.has(i)) {
1494
+ tr.remove();
1495
+ v.map.delete(i);
1496
+ removed?.push(tr);
1497
+ }
1498
+ }
1499
+ v.start = start;
1500
+ v.end = end;
1501
+ this._virtSpacers();
1502
+ const added = [];
1503
+ let ref = v.bottom.isConnected ? v.bottom : null;
1504
+ for (let k = want.length - 1; k >= 0; k--) {
1505
+ const i = want[k];
1506
+ const have = v.map.get(i);
1507
+ if (have) {
1508
+ ref = have;
1509
+ continue;
1510
+ }
1511
+ const tr = this._buildBodyRow(v.rows[i], v.globalOf(i), i);
1512
+ v.map.set(i, tr);
1513
+ tbody.insertBefore(tr, ref);
1514
+ ref = tr;
1515
+ added.push(tr);
1516
+ }
1517
+ added.reverse();
1518
+ return added;
1519
+ }
1520
+ /**
1521
+ * Size the two spacers, and put each in the DOM only while it has a height.
1522
+ *
1523
+ * AND KEEP THE STRIPE. `tr:nth-child(even)` stripes by POSITION, and in a
1524
+ * full render row `i` is child `i + 1`. Here the first drawn row is preceded
1525
+ * by the top spacer, so its position is off by one — and by a different one
1526
+ * each time the window's start changes parity, which strobes every stripe
1527
+ * on the screen as you scroll. A hidden filler row after the spacer, present
1528
+ * exactly when `start` is even, puts the parity back: a `display: none` row
1529
+ * is still a child, and so still counted by `nth-child`.
1530
+ */
1531
+ _virtSpacers() {
1532
+ const v = this._virt;
1533
+ const n = v.rows.length;
1534
+ const above = v.offsets[v.start];
1535
+ const below = v.offsets[n] - v.offsets[v.end];
1536
+ const tbody = v.tbody;
1537
+ if (above > 0) {
1538
+ v.top.firstChild.style.height = `${above}px`;
1539
+ if (tbody.firstChild !== v.top) tbody.insertBefore(v.top, tbody.firstChild);
1540
+ const needFiller = v.start % 2 === 0;
1541
+ if (needFiller && v.top.nextSibling !== v.filler) v.top.after(v.filler);
1542
+ else if (!needFiller) v.filler.remove();
1543
+ } else {
1544
+ v.top.remove();
1545
+ v.filler.remove();
1546
+ }
1547
+ if (below > 0) {
1548
+ v.bottom.firstChild.style.height = `${below}px`;
1549
+ if (tbody.lastChild !== v.bottom) tbody.appendChild(v.bottom);
1550
+ } else {
1551
+ v.bottom.remove();
1552
+ }
1553
+ }
1554
+ /**
1555
+ * Recompute every row's height and redraw the window. For an embedder whose
1556
+ * `virtualRowHeight` answer has changed — a group folded, a header added.
1557
+ * A no-op on a table that is not virtual.
1558
+ */
1559
+ refreshVirtualLayout() {
1560
+ if (!this._virt) return;
1561
+ this._virtMeasure();
1562
+ this._virtComputeOffsets();
1563
+ this._virtUpdate({ force: true });
1564
+ }
1565
+ /** The element that scrolls the body. Read-only; the table owns it. */
1566
+ get scrollElement() {
1567
+ return this._tableWrapEl || null;
1568
+ }
1569
+ /** The `<tr>` drawing row `rowIndex` (the `__rowIndex` space), or null when
1570
+ * it is not in the DOM — outside the window, on another page, or hidden. */
1571
+ getRowElement(rowIndex) {
1572
+ const v = this._virt;
1573
+ if (v) {
1574
+ const local = this._virtLocalOf(rowIndex);
1575
+ return local < 0 ? null : v.map.get(local) || null;
1576
+ }
1577
+ const tbody = this._tbodyEl;
1578
+ if (!tbody) return null;
1579
+ for (const tr of tbody.children) if (tr.__rowIndex === rowIndex) return tr;
1580
+ return null;
1581
+ }
1582
+ /** Keep the window's row list and `config.rows` in step for one row. They
1583
+ * are the same array in server-side mode; in client mode the window reads
1584
+ * the filtered-and-sorted copy, which holds its own reference. */
1585
+ _virtSetRow(rowIndex, row) {
1586
+ const v = this._virt;
1587
+ if (!v) return;
1588
+ if (Array.isArray(this.config.rows) && rowIndex >= 0 && rowIndex < this.config.rows.length) {
1589
+ this.config.rows[rowIndex] = row;
1590
+ }
1591
+ const local = this._virtLocalOf(rowIndex);
1592
+ if (local >= 0) v.rows[local] = row;
1593
+ }
1594
+ /** `__rowIndex` → position in the window's row list, or -1. */
1595
+ _virtLocalOf(rowIndex) {
1596
+ const v = this._virt;
1597
+ if (!v) return -1;
1598
+ let local;
1599
+ if (v.serverSide) local = rowIndex - v.configOffset;
1600
+ else {
1601
+ const map = this._processedIndexMap;
1602
+ const processed = map ? map.indexOf(rowIndex) : rowIndex;
1603
+ local = processed < 0 ? -1 : processed - v.offset;
1604
+ }
1605
+ return local >= 0 && local < v.rows.length ? local : -1;
1606
+ }
1607
+ /**
1608
+ * Scroll the body so row `rowIndex` is on screen, drawing it if it was not,
1609
+ * and return its `<tr>` (null when it cannot be shown: another page, or a
1610
+ * row the embedder hides). Works with or without `virtualize`.
1611
+ *
1612
+ * @param {number} rowIndex in the `__rowIndex` space.
1613
+ * @param {{block?: 'nearest'|'center'|'start'}} [opts]
1614
+ */
1615
+ scrollToRow(rowIndex, { block = "nearest" } = {}) {
1616
+ const wrap = this._tableWrapEl;
1617
+ const v = this._virt;
1618
+ if (!v) {
1619
+ const tr = this.getRowElement(rowIndex);
1620
+ if (!tr || !wrap) return tr;
1621
+ const zoom = this._tableEl?.currentCSSZoom || 1;
1622
+ const box = wrap.getBoundingClientRect();
1623
+ const r = tr.getBoundingClientRect();
1624
+ const rowTop = (r.top - box.top) / zoom + wrap.scrollTop;
1625
+ const rowH = r.height / zoom;
1626
+ const target2 = this._scrollTarget(
1627
+ rowTop,
1628
+ rowH,
1629
+ wrap.scrollTop,
1630
+ wrap.clientHeight,
1631
+ block
1632
+ );
1633
+ if (target2 !== wrap.scrollTop) wrap.scrollTop = target2;
1634
+ return tr;
1635
+ }
1636
+ const local = this._virtLocalOf(rowIndex);
1637
+ if (local < 0 || !(v.heights[local] > 0)) return null;
1638
+ const view = wrap?.clientHeight || VIRTUAL_FALLBACK_VIEWPORT_PX;
1639
+ const current = wrap?.scrollTop || 0;
1640
+ let target = this._scrollTarget(
1641
+ v.offsets[local],
1642
+ v.heights[local],
1643
+ current,
1644
+ view,
1645
+ block
1646
+ );
1647
+ target = Math.max(0, Math.min(target, Math.max(0, v.offsets[v.rows.length] - view)));
1648
+ if (wrap && Math.abs(target - current) >= 1) wrap.scrollTop = target;
1649
+ this._virtUpdate({ top: Math.abs(target - current) >= 1 ? target : current });
1650
+ return v.map.get(local) || null;
1651
+ }
1652
+ _scrollTarget(rowTop, rowH, current, view, block) {
1653
+ if (block === "start") return rowTop;
1654
+ if (block === "center") return rowTop - Math.max(0, (view - rowH) / 2);
1655
+ if (rowTop < current) return rowTop;
1656
+ if (rowTop + rowH > current + view) return rowTop + rowH - view;
1657
+ return current;
1658
+ }
1659
+ /** The first body row that draws a row of data — never a spacer, and never
1660
+ * a row an embedder injected (a group header spans every column with ONE
1661
+ * cell, so measuring it would report one column). */
1662
+ _firstBodyRow() {
1663
+ const tbody = this._tableEl?.querySelector?.("tbody");
1664
+ if (!tbody) return null;
1665
+ for (const tr of tbody.children) {
1666
+ if (tr.__rowIndex !== void 0) return tr;
1667
+ }
1668
+ return null;
1222
1669
  }
1223
1670
  /** Build (or rebuild) one row's cells in place.
1224
1671
  *
@@ -1298,10 +1745,14 @@ var DataTable = class {
1298
1745
  break;
1299
1746
  }
1300
1747
  }
1301
- if (!tr) return false;
1748
+ if (!tr) {
1749
+ if (this._virt) this._virtSetRow(index, row);
1750
+ return false;
1751
+ }
1302
1752
  if (Array.isArray(this.config.rows) && this.config.rows[index]) {
1303
1753
  this.config.rows[index] = row;
1304
1754
  }
1755
+ if (this._virt) this._virtSetRow(index, row);
1305
1756
  const active = document.activeElement;
1306
1757
  let focusedCol = -1;
1307
1758
  if (active && tr.contains(active)) {
@@ -1350,14 +1801,23 @@ var DataTable = class {
1350
1801
  * never visible. Clearing inline widths first stops the last sync's forced
1351
1802
  * widths from constraining the measure.
1352
1803
  *
1804
+ * C10 — UNDER `virtualize` ONLY THE WINDOW IS IN THE DOM, so `max-content`
1805
+ * spans the eighty-odd rows drawn rather than every row loaded. The fit
1806
+ * pass that runs on every render is content with that (it is fitting what
1807
+ * is on screen). An EXPLICIT auto-size is not: it is a promise to fit the
1808
+ * column's content, and the longest value in a 6,000-row table is usually
1809
+ * not in the window. So `sample` adds, for the length of this one reflow,
1810
+ * the row holding each column's longest value — see `_virtSampleRows`.
1811
+ *
1812
+ * @param {{sample?: boolean}} [o]
1353
1813
  * @returns {number[]|null} width per DOM column index, or null when there
1354
1814
  * is nothing laid out to measure.
1355
1815
  */
1356
- _measureNaturalWidths() {
1816
+ _measureNaturalWidths({ sample = false } = {}) {
1357
1817
  const headerTable = this._headerTableEl;
1358
1818
  const bodyTable = this._tableEl;
1359
1819
  if (!headerTable || !bodyTable) return null;
1360
- const firstRow = bodyTable.querySelector("tbody > tr");
1820
+ const firstRow = this._firstBodyRow();
1361
1821
  if (!firstRow) return null;
1362
1822
  const bodyCells = firstRow.children;
1363
1823
  if (!bodyCells.length) return null;
@@ -1365,10 +1825,13 @@ var DataTable = class {
1365
1825
  for (const th of tr.children) this._clearCellWidth(th);
1366
1826
  });
1367
1827
  for (const td of bodyCells) this._clearCellWidth(td);
1828
+ for (const col of this._cols() || []) col.style.width = "";
1368
1829
  headerTable.style.width = "";
1369
1830
  bodyTable.style.width = "";
1370
1831
  headerTable.classList.add("twm-dt-measuring");
1371
1832
  bodyTable.classList.add("twm-dt-measuring");
1833
+ const sampled = sample ? this._virtSampleRows() : [];
1834
+ for (const tr of sampled) this._virt.tbody.appendChild(tr);
1372
1835
  bodyTable.offsetWidth;
1373
1836
  const labelRow = headerTable.querySelector("thead > tr");
1374
1837
  const cols = bodyCells.length;
@@ -1379,12 +1842,61 @@ var DataTable = class {
1379
1842
  const head = labelRow && labelRow.children[i] ? labelRow.children[i].getBoundingClientRect().width / zoom : 0;
1380
1843
  natural[i] = Math.max(body, head);
1381
1844
  }
1845
+ for (const tr of sampled) tr.remove();
1382
1846
  headerTable.classList.remove("twm-dt-measuring");
1383
1847
  bodyTable.classList.remove("twm-dt-measuring");
1384
1848
  return natural;
1385
1849
  }
1386
1850
  /**
1387
- * Fit one column to its widest visible value and pin it there.
1851
+ * The rows an explicit auto-size must measure that the window has not
1852
+ * drawn: for each column, the drawable row whose value is LONGEST as a
1853
+ * string, plus the last row (the widest row number). Built with
1854
+ * `_buildBodyRow` — so a consumer's `renderCell` draws them exactly as it
1855
+ * would on screen — and handed back detached, for `_measureNaturalWidths`
1856
+ * to put in and take out around its one reflow.
1857
+ *
1858
+ * The string length is a proxy, not a measurement: in a proportional font
1859
+ * forty `i`s are narrower than thirty `W`s. It is the proxy a spreadsheet's
1860
+ * own autofit falls short by, and it is right about the case that matters
1861
+ * here — a value five times longer than anything in the window. One pass
1862
+ * over the loaded rows, on a click and never on a scroll or a resize.
1863
+ *
1864
+ * @returns {HTMLTableRowElement[]}
1865
+ */
1866
+ _virtSampleRows() {
1867
+ const v = this._virt;
1868
+ if (!v || !v.rows.length) return [];
1869
+ const longest = [];
1870
+ const at = [];
1871
+ let last = -1;
1872
+ for (let i = 0; i < v.rows.length; i++) {
1873
+ if (!(v.heights[i] > 0)) continue;
1874
+ last = i;
1875
+ const row = v.rows[i];
1876
+ if (!Array.isArray(row)) continue;
1877
+ for (let c = 0; c < row.length; c++) {
1878
+ const value = row[c];
1879
+ const len = value == null ? 0 : String(value).length;
1880
+ if (len > (longest[c] ?? -1)) {
1881
+ longest[c] = len;
1882
+ at[c] = i;
1883
+ }
1884
+ }
1885
+ }
1886
+ const want = new Set(at.filter((i) => i !== void 0));
1887
+ if (last >= 0) want.add(last);
1888
+ const rows = [];
1889
+ for (const i of want) {
1890
+ if (v.map.has(i)) continue;
1891
+ const tr = this._buildBodyRow(v.rows[i], v.globalOf(i), i);
1892
+ tr.setAttribute("aria-hidden", "true");
1893
+ rows.push(tr);
1894
+ }
1895
+ return rows;
1896
+ }
1897
+ /**
1898
+ * Fit one column to its widest value and pin it there — every loaded
1899
+ * row's, under `virtualize` (see `_virtSampleRows`), not only the window's.
1388
1900
  *
1389
1901
  * The gesture is a double-click on the column's resize grip, which is where
1390
1902
  * every spreadsheet has put it — and the grip is this component's, which is
@@ -1399,7 +1911,7 @@ var DataTable = class {
1399
1911
  * @returns {boolean} whether it had a layout to measure.
1400
1912
  */
1401
1913
  autoSizeColumn(domIdx, opts = {}) {
1402
- const natural = this._measureNaturalWidths();
1914
+ const natural = this._measureNaturalWidths({ sample: true });
1403
1915
  if (!natural || domIdx < 0 || domIdx >= natural.length) return false;
1404
1916
  this._colWidths[domIdx] = this._autoWidth(natural[domIdx], opts);
1405
1917
  this._colWidthsSig = this._colSig();
@@ -1423,7 +1935,7 @@ var DataTable = class {
1423
1935
  * already, so a button that did that would be a button that does nothing.
1424
1936
  */
1425
1937
  autoSizeColumns(opts = {}) {
1426
- const natural = this._measureNaturalWidths();
1938
+ const natural = this._measureNaturalWidths({ sample: true });
1427
1939
  if (!natural) return false;
1428
1940
  this._colWidths = {};
1429
1941
  for (let i = 0; i < natural.length; i++) {
@@ -1472,17 +1984,18 @@ var DataTable = class {
1472
1984
  const headerTable = this._headerTableEl;
1473
1985
  const bodyTable = this._tableEl;
1474
1986
  if (!headerTable || !bodyTable) return;
1475
- const firstRow = bodyTable.querySelector("tbody > tr");
1476
- if (!firstRow) return;
1477
- const bodyCells = firstRow.children;
1478
- if (!bodyCells.length) return;
1987
+ const cols = this._cols();
1988
+ const firstRow = this._firstBodyRow();
1989
+ const bodyCells = cols ? [] : firstRow?.children;
1990
+ const n = cols ? cols.length : bodyCells?.length || 0;
1991
+ if (!n) return;
1479
1992
  const headerRows = headerTable.querySelectorAll("thead > tr");
1480
- const natural = this._allColumnsPinned(bodyCells.length) ? new Array(bodyCells.length).fill(0) : this._measureNaturalWidths();
1993
+ const natural = this._allColumnsPinned(n) ? new Array(n).fill(0) : this._measureNaturalWidths();
1481
1994
  if (!natural) return;
1482
1995
  const wrap = this._tableWrapEl;
1483
1996
  const headerWrap = this._headerWrapEl;
1484
1997
  const avail = wrap ? wrap.clientWidth : 0;
1485
- const firstIdx = this.config.showRowNumbers && bodyCells.length > 1 ? 1 : 0;
1998
+ const firstIdx = this.config.showRowNumbers && n > 1 ? 1 : 0;
1486
1999
  const widths = this._fitColumnWidths(natural, avail, {
1487
2000
  firstIdx,
1488
2001
  overrides: this._colWidths,
@@ -1494,8 +2007,14 @@ var DataTable = class {
1494
2007
  this._setCellWidth(tr.children[i], widths[i]);
1495
2008
  }
1496
2009
  });
1497
- for (let i = 0; i < bodyCells.length && i < widths.length; i++) {
1498
- this._setCellWidth(bodyCells[i], widths[i]);
2010
+ if (cols) {
2011
+ for (let i = 0; i < cols.length && i < widths.length; i++) {
2012
+ cols[i].style.width = `${widths[i]}px`;
2013
+ }
2014
+ } else {
2015
+ for (let i = 0; i < bodyCells.length && i < widths.length; i++) {
2016
+ this._setCellWidth(bodyCells[i], widths[i]);
2017
+ }
1499
2018
  }
1500
2019
  headerTable.style.tableLayout = "fixed";
1501
2020
  bodyTable.style.tableLayout = "fixed";
@@ -1793,7 +2312,12 @@ var DataTable = class {
1793
2312
  headerTable.querySelectorAll("thead > tr").forEach((tr) => {
1794
2313
  if (tr.children[i]) this._setCellWidth(tr.children[i], w);
1795
2314
  });
1796
- const bodyRow = bodyTable && bodyTable.querySelector("tbody > tr");
2315
+ const cols = this._cols();
2316
+ if (cols) {
2317
+ if (cols[i]) cols[i].style.width = `${w}px`;
2318
+ return;
2319
+ }
2320
+ const bodyRow = bodyTable && this._firstBodyRow();
1797
2321
  if (bodyRow && bodyRow.children[i]) this._setCellWidth(bodyRow.children[i], w);
1798
2322
  }
1799
2323
  /** Size both tables so a widened column grows the table (→ horizontal
@@ -1842,10 +2366,12 @@ var DataTable = class {
1842
2366
  /** Add a native `title` tooltip to any body cell whose text is
1843
2367
  * clipped by its column width; remove ours once it fits again.
1844
2368
  * Leaves caller-supplied titles (e.g. from renderCell) untouched. */
1845
- _updateCellTooltips() {
2369
+ _updateCellTooltips(rows = null) {
1846
2370
  const bodyTable = this._tableEl;
1847
2371
  if (!bodyTable) return;
1848
- bodyTable.querySelectorAll("tbody td").forEach((td) => {
2372
+ const cells = rows ? rows.flatMap((tr) => [...tr.children]) : bodyTable.querySelectorAll("tbody td");
2373
+ cells.forEach((td) => {
2374
+ if (td.parentElement?.classList.contains("twm-dt-spacer")) return;
1849
2375
  const clipped = td.scrollWidth > td.clientWidth + 1;
1850
2376
  if (clipped) {
1851
2377
  if (!td.title) td.title = td.textContent;
@@ -2093,4 +2619,4 @@ export {
2093
2619
  createRafResizeObserver,
2094
2620
  DataTable
2095
2621
  };
2096
- //# sourceMappingURL=chunk-BIAOGX6K.js.map
2622
+ //# sourceMappingURL=chunk-CYTWMP2Y.js.map