flexdesk 0.4.4 → 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
  }
@@ -302,8 +309,13 @@ var DataTable = class {
302
309
  */
303
310
  render() {
304
311
  const activeFilterColIdx = this._getActiveFilterColIdx();
312
+ const doc = this.container.ownerDocument;
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;
305
316
  this._cleanup();
306
317
  this.container.innerHTML = "";
318
+ this._virt = null;
307
319
  const { rows, pagination, emptyMessage } = this.config;
308
320
  const colSig = this._colSig();
309
321
  if (this._colWidthsSig !== colSig) {
@@ -390,6 +402,18 @@ var DataTable = class {
390
402
  }
391
403
  this._tableWrapEl = tableWrap;
392
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
+ }
393
417
  if (this._headerTableEl && this._tableEl) {
394
418
  requestAnimationFrame(() => this._syncHeaderWidths());
395
419
  if (this._resizeObserver) {
@@ -399,9 +423,10 @@ var DataTable = class {
399
423
  }
400
424
  }
401
425
  if (typeof ResizeObserver !== "undefined") {
402
- this._resizeObserver = createRafResizeObserver(
403
- () => this._syncHeaderWidths()
404
- );
426
+ this._resizeObserver = createRafResizeObserver(() => {
427
+ this._syncHeaderWidths();
428
+ this._virtUpdate();
429
+ });
405
430
  this._resizeObserver.observe(this._wrapperEl);
406
431
  }
407
432
  this._installHeaderScrollSync();
@@ -412,6 +437,11 @@ var DataTable = class {
412
437
  }
413
438
  if (activeFilterColIdx !== null) {
414
439
  this._restoreFilterFocus(activeFilterColIdx);
440
+ } else if (hadTableFocus && this._tableEl) {
441
+ try {
442
+ this._tableEl.focus({ preventScroll: true });
443
+ } catch (_) {
444
+ }
415
445
  }
416
446
  }
417
447
  /**
@@ -527,6 +557,8 @@ var DataTable = class {
527
557
  this._tableWrapEl = null;
528
558
  this._processedRows = null;
529
559
  this._processedIndexMap = null;
560
+ this._virt = null;
561
+ this._colgroupEl = null;
530
562
  }
531
563
  // ─────────────────────────────────────────────────────────────────
532
564
  // Processed rows pipeline (filter → sort → cache)
@@ -1129,7 +1161,7 @@ var DataTable = class {
1129
1161
  }
1130
1162
  _createTable() {
1131
1163
  const { headers, rows, pageSize, sortable, filterable, showRowNumbers, readonly, getHeaderIcon, onPageChange, offset: configOffset } = this.config;
1132
- const { selected, sortColumn, sortAscending } = this._state;
1164
+ const { sortColumn, sortAscending } = this._state;
1133
1165
  const isServerSide = typeof onPageChange === "function";
1134
1166
  const processedRows = isServerSide ? rows : this._getProcessedRows();
1135
1167
  const offset = isServerSide ? configOffset || 0 : this._state.offset;
@@ -1185,33 +1217,455 @@ var DataTable = class {
1185
1217
  const filterRow = this._createFilterRow(headers);
1186
1218
  thead.appendChild(filterRow);
1187
1219
  }
1220
+ const virtual = this._isVirtual();
1221
+ if (virtual) {
1222
+ table.classList.add("twm-dt--virtual");
1223
+ table.appendChild(this._createColgroup());
1224
+ }
1188
1225
  table.appendChild(thead);
1189
- const tbody = document.createElement("tbody");
1190
- pageRows.forEach((row, localIdx) => {
1226
+ const globalOf = (localIdx) => {
1191
1227
  const processedIdx = offset + localIdx;
1192
- 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) => {
1193
1338
  const tr = document.createElement("tr");
1194
- tr.__rowIndex = globalIdx;
1195
- tr.className = "data-preview-row";
1196
- if (selected.has(globalIdx)) {
1197
- 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;
1198
1389
  }
1199
- this._fillRowCells(tr, row, globalIdx, showRowNumbers);
1200
- if (this.config.onRowClick) {
1201
- tr.addEventListener("click", (ev) => {
1202
- this.config.onRowClick(globalIdx, row, ev);
1203
- });
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;
1204
1429
  }
1205
- if (this.config.onRowContextMenu) {
1206
- tr.addEventListener("contextmenu", (ev) => {
1207
- this.config.onRowContextMenu(globalIdx, row, ev);
1208
- });
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);
1209
1473
  }
1210
- tbody.appendChild(tr);
1211
- });
1212
- table.appendChild(tbody);
1213
- this._tbodyEl = tbody;
1214
- 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;
1215
1669
  }
1216
1670
  /** Build (or rebuild) one row's cells in place.
1217
1671
  *
@@ -1291,10 +1745,14 @@ var DataTable = class {
1291
1745
  break;
1292
1746
  }
1293
1747
  }
1294
- if (!tr) return false;
1748
+ if (!tr) {
1749
+ if (this._virt) this._virtSetRow(index, row);
1750
+ return false;
1751
+ }
1295
1752
  if (Array.isArray(this.config.rows) && this.config.rows[index]) {
1296
1753
  this.config.rows[index] = row;
1297
1754
  }
1755
+ if (this._virt) this._virtSetRow(index, row);
1298
1756
  const active = document.activeElement;
1299
1757
  let focusedCol = -1;
1300
1758
  if (active && tr.contains(active)) {
@@ -1343,14 +1801,23 @@ var DataTable = class {
1343
1801
  * never visible. Clearing inline widths first stops the last sync's forced
1344
1802
  * widths from constraining the measure.
1345
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]
1346
1813
  * @returns {number[]|null} width per DOM column index, or null when there
1347
1814
  * is nothing laid out to measure.
1348
1815
  */
1349
- _measureNaturalWidths() {
1816
+ _measureNaturalWidths({ sample = false } = {}) {
1350
1817
  const headerTable = this._headerTableEl;
1351
1818
  const bodyTable = this._tableEl;
1352
1819
  if (!headerTable || !bodyTable) return null;
1353
- const firstRow = bodyTable.querySelector("tbody > tr");
1820
+ const firstRow = this._firstBodyRow();
1354
1821
  if (!firstRow) return null;
1355
1822
  const bodyCells = firstRow.children;
1356
1823
  if (!bodyCells.length) return null;
@@ -1358,10 +1825,13 @@ var DataTable = class {
1358
1825
  for (const th of tr.children) this._clearCellWidth(th);
1359
1826
  });
1360
1827
  for (const td of bodyCells) this._clearCellWidth(td);
1828
+ for (const col of this._cols() || []) col.style.width = "";
1361
1829
  headerTable.style.width = "";
1362
1830
  bodyTable.style.width = "";
1363
1831
  headerTable.classList.add("twm-dt-measuring");
1364
1832
  bodyTable.classList.add("twm-dt-measuring");
1833
+ const sampled = sample ? this._virtSampleRows() : [];
1834
+ for (const tr of sampled) this._virt.tbody.appendChild(tr);
1365
1835
  bodyTable.offsetWidth;
1366
1836
  const labelRow = headerTable.querySelector("thead > tr");
1367
1837
  const cols = bodyCells.length;
@@ -1372,12 +1842,61 @@ var DataTable = class {
1372
1842
  const head = labelRow && labelRow.children[i] ? labelRow.children[i].getBoundingClientRect().width / zoom : 0;
1373
1843
  natural[i] = Math.max(body, head);
1374
1844
  }
1845
+ for (const tr of sampled) tr.remove();
1375
1846
  headerTable.classList.remove("twm-dt-measuring");
1376
1847
  bodyTable.classList.remove("twm-dt-measuring");
1377
1848
  return natural;
1378
1849
  }
1379
1850
  /**
1380
- * 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.
1381
1900
  *
1382
1901
  * The gesture is a double-click on the column's resize grip, which is where
1383
1902
  * every spreadsheet has put it — and the grip is this component's, which is
@@ -1392,7 +1911,7 @@ var DataTable = class {
1392
1911
  * @returns {boolean} whether it had a layout to measure.
1393
1912
  */
1394
1913
  autoSizeColumn(domIdx, opts = {}) {
1395
- const natural = this._measureNaturalWidths();
1914
+ const natural = this._measureNaturalWidths({ sample: true });
1396
1915
  if (!natural || domIdx < 0 || domIdx >= natural.length) return false;
1397
1916
  this._colWidths[domIdx] = this._autoWidth(natural[domIdx], opts);
1398
1917
  this._colWidthsSig = this._colSig();
@@ -1416,7 +1935,7 @@ var DataTable = class {
1416
1935
  * already, so a button that did that would be a button that does nothing.
1417
1936
  */
1418
1937
  autoSizeColumns(opts = {}) {
1419
- const natural = this._measureNaturalWidths();
1938
+ const natural = this._measureNaturalWidths({ sample: true });
1420
1939
  if (!natural) return false;
1421
1940
  this._colWidths = {};
1422
1941
  for (let i = 0; i < natural.length; i++) {
@@ -1465,17 +1984,18 @@ var DataTable = class {
1465
1984
  const headerTable = this._headerTableEl;
1466
1985
  const bodyTable = this._tableEl;
1467
1986
  if (!headerTable || !bodyTable) return;
1468
- const firstRow = bodyTable.querySelector("tbody > tr");
1469
- if (!firstRow) return;
1470
- const bodyCells = firstRow.children;
1471
- 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;
1472
1992
  const headerRows = headerTable.querySelectorAll("thead > tr");
1473
- 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();
1474
1994
  if (!natural) return;
1475
1995
  const wrap = this._tableWrapEl;
1476
1996
  const headerWrap = this._headerWrapEl;
1477
1997
  const avail = wrap ? wrap.clientWidth : 0;
1478
- const firstIdx = this.config.showRowNumbers && bodyCells.length > 1 ? 1 : 0;
1998
+ const firstIdx = this.config.showRowNumbers && n > 1 ? 1 : 0;
1479
1999
  const widths = this._fitColumnWidths(natural, avail, {
1480
2000
  firstIdx,
1481
2001
  overrides: this._colWidths,
@@ -1487,8 +2007,14 @@ var DataTable = class {
1487
2007
  this._setCellWidth(tr.children[i], widths[i]);
1488
2008
  }
1489
2009
  });
1490
- for (let i = 0; i < bodyCells.length && i < widths.length; i++) {
1491
- 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
+ }
1492
2018
  }
1493
2019
  headerTable.style.tableLayout = "fixed";
1494
2020
  bodyTable.style.tableLayout = "fixed";
@@ -1717,28 +2243,48 @@ var DataTable = class {
1717
2243
  th.appendChild(grip);
1718
2244
  });
1719
2245
  }
2246
+ /**
2247
+ * CSS px per screen px. Content inside a zoomed tile reports rects and
2248
+ * pointer coordinates in screen pixels, while `style.width` is CSS pixels;
2249
+ * mixing them widened every column by the zoom factor on a mere click.
2250
+ */
2251
+ _zoomFactor() {
2252
+ const el = this._tableWrapEl || this._tableEl;
2253
+ const css = el?.offsetWidth || 0;
2254
+ const screen = el?.getBoundingClientRect?.().width || 0;
2255
+ return css > 0 && screen > 0 ? screen / css : 1;
2256
+ }
1720
2257
  _beginColResize(ev, domIdx) {
2258
+ if (ev.button != null && ev.button !== 0) return;
1721
2259
  ev.preventDefault();
1722
2260
  ev.stopPropagation();
1723
2261
  const headerTable = this._headerTableEl;
1724
2262
  const bodyTable = this._tableEl;
1725
2263
  const headRow = headerTable && headerTable.querySelector("thead > tr");
1726
2264
  if (!headRow) return;
1727
- const startWidths = [...headRow.children].map(
1728
- (c) => c.getBoundingClientRect().width
1729
- );
1730
- headerTable.style.tableLayout = "fixed";
1731
- if (bodyTable) bodyTable.style.tableLayout = "fixed";
1732
- startWidths.forEach((w, i) => this._pinColumnWidth(i, w));
1733
- this._applyTableWidth();
1734
2265
  const startX = ev.clientX;
2266
+ const THRESHOLD = 3;
1735
2267
  const MIN = 40;
1736
- document.body.classList.add("twm-dt-col-resizing");
2268
+ let startWidths = null;
2269
+ const begin = () => {
2270
+ const z = this._zoomFactor();
2271
+ startWidths = [...headRow.children].map((c) => {
2272
+ const set = parseFloat(c.style.width);
2273
+ return Number.isFinite(set) ? set : c.getBoundingClientRect().width / z;
2274
+ });
2275
+ headerTable.style.tableLayout = "fixed";
2276
+ if (bodyTable) bodyTable.style.tableLayout = "fixed";
2277
+ startWidths.forEach((w, i) => this._pinColumnWidth(i, w));
2278
+ this._applyTableWidth();
2279
+ document.body.classList.add("twm-dt-col-resizing");
2280
+ };
1737
2281
  const onMove = (mv) => {
1738
- const w = Math.max(
1739
- MIN,
1740
- Math.round(startWidths[domIdx] + (mv.clientX - startX))
1741
- );
2282
+ const dx = mv.clientX - startX;
2283
+ if (!startWidths) {
2284
+ if (Math.abs(dx) < THRESHOLD) return;
2285
+ begin();
2286
+ }
2287
+ const w = Math.max(MIN, Math.round(startWidths[domIdx] + dx / this._zoomFactor()));
1742
2288
  this._pinColumnWidth(domIdx, w);
1743
2289
  this._applyTableWidth(domIdx);
1744
2290
  this._syncHeaderScroll();
@@ -1746,6 +2292,7 @@ var DataTable = class {
1746
2292
  const onUp = () => {
1747
2293
  document.removeEventListener("mousemove", onMove);
1748
2294
  document.removeEventListener("mouseup", onUp);
2295
+ if (!startWidths) return;
1749
2296
  document.body.classList.remove("twm-dt-col-resizing");
1750
2297
  this._updateCellTooltips();
1751
2298
  this._savePersisted();
@@ -1765,7 +2312,12 @@ var DataTable = class {
1765
2312
  headerTable.querySelectorAll("thead > tr").forEach((tr) => {
1766
2313
  if (tr.children[i]) this._setCellWidth(tr.children[i], w);
1767
2314
  });
1768
- 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();
1769
2321
  if (bodyRow && bodyRow.children[i]) this._setCellWidth(bodyRow.children[i], w);
1770
2322
  }
1771
2323
  /** Size both tables so a widened column grows the table (→ horizontal
@@ -1814,10 +2366,12 @@ var DataTable = class {
1814
2366
  /** Add a native `title` tooltip to any body cell whose text is
1815
2367
  * clipped by its column width; remove ours once it fits again.
1816
2368
  * Leaves caller-supplied titles (e.g. from renderCell) untouched. */
1817
- _updateCellTooltips() {
2369
+ _updateCellTooltips(rows = null) {
1818
2370
  const bodyTable = this._tableEl;
1819
2371
  if (!bodyTable) return;
1820
- 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;
1821
2375
  const clipped = td.scrollWidth > td.clientWidth + 1;
1822
2376
  if (clipped) {
1823
2377
  if (!td.title) td.title = td.textContent;
@@ -2065,4 +2619,4 @@ export {
2065
2619
  createRafResizeObserver,
2066
2620
  DataTable
2067
2621
  };
2068
- //# sourceMappingURL=chunk-P4AALM3A.js.map
2622
+ //# sourceMappingURL=chunk-CYTWMP2Y.js.map