flexdesk 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -358,6 +358,114 @@ function matchDurationFilter(ms, filterText, shown) {
358
358
  return String(shown ?? "").toLowerCase().includes(text.toLowerCase());
359
359
  }
360
360
 
361
+ // src/ui/components/table_filter_chips.js
362
+ var isAll = (v) => v === "" || v == null;
363
+ function createChipsFilter(ctx = {}, spec = {}) {
364
+ const options = Array.isArray(spec.options) ? spec.options : [];
365
+ const multiple = !!spec.multiple;
366
+ const showCounts = spec.counts !== false;
367
+ const el = document.createElement("div");
368
+ el.className = "twm-dt-filter-chips" + (multiple ? " twm-dt-filter-chips--multiple" : "");
369
+ el.setAttribute("role", "group");
370
+ const name = spec.label || ctx.header;
371
+ if (name) el.setAttribute("aria-label", String(name));
372
+ const valueOf = /* @__PURE__ */ new Map();
373
+ const chips = /* @__PURE__ */ new Map();
374
+ let value = null;
375
+ const keyOf = (v) => String(v ?? "");
376
+ const normalise = (v) => {
377
+ if (multiple) {
378
+ const list = Array.isArray(v) ? v : isAll(v) ? [] : [v];
379
+ return list.filter((x) => !isAll(x)).map(keyOf);
380
+ }
381
+ if (Array.isArray(v)) v = v[0];
382
+ return isAll(v) ? null : keyOf(v);
383
+ };
384
+ const chosen = () => multiple ? value.length === 0 : value === null;
385
+ for (const opt of options) {
386
+ const key = keyOf(opt.value);
387
+ valueOf.set(key, opt.value);
388
+ const btn = document.createElement("button");
389
+ btn.type = "button";
390
+ btn.className = "twm-dt-filter-chip";
391
+ btn.dataset.value = key;
392
+ if (opt.title) btn.title = String(opt.title);
393
+ if (opt.icon) {
394
+ const icon = document.createElement("span");
395
+ icon.className = "material-symbols-outlined twm-dt-filter-chip__icon";
396
+ icon.setAttribute("aria-hidden", "true");
397
+ icon.textContent = String(opt.icon);
398
+ btn.appendChild(icon);
399
+ }
400
+ const label = document.createElement("span");
401
+ label.className = "twm-dt-filter-chip__label";
402
+ label.textContent = String(opt.label ?? key);
403
+ btn.appendChild(label);
404
+ let count = null;
405
+ if (showCounts) {
406
+ count = document.createElement("span");
407
+ count.className = "twm-dt-filter-chip__count";
408
+ count.textContent = "\u2013";
409
+ btn.appendChild(count);
410
+ }
411
+ btn.addEventListener("click", (ev) => {
412
+ ev.stopPropagation();
413
+ pick(key);
414
+ });
415
+ chips.set(key, { btn, count });
416
+ el.appendChild(btn);
417
+ }
418
+ function outward() {
419
+ if (multiple) return value.length ? value.map((k) => valueOf.get(k)) : null;
420
+ return value === null ? null : valueOf.get(value);
421
+ }
422
+ function pick(key) {
423
+ if (isAll(key)) value = multiple ? [] : null;
424
+ else if (multiple) {
425
+ value = value.includes(key) ? value.filter((k) => k !== key) : [...value, key];
426
+ } else {
427
+ value = value === key ? null : key;
428
+ }
429
+ paint();
430
+ const out = outward();
431
+ if (typeof ctx.setValue === "function") ctx.setValue(out);
432
+ try {
433
+ spec.onChange?.(out);
434
+ } catch (err) {
435
+ console.error("[chips filter] onChange threw", err);
436
+ }
437
+ }
438
+ function paint() {
439
+ for (const [key, { btn }] of chips) {
440
+ const on = isAll(key) ? chosen() : multiple ? value.includes(key) : value === key;
441
+ btn.classList.toggle("twm-dt-filter-chip--on", on);
442
+ btn.setAttribute("aria-pressed", on ? "true" : "false");
443
+ }
444
+ }
445
+ function setValue(v) {
446
+ value = normalise(v);
447
+ paint();
448
+ }
449
+ function setCounts(counts) {
450
+ for (const [key, { btn, count }] of chips) {
451
+ const n = counts == null ? void 0 : counts[key];
452
+ const known = typeof n === "number" && Number.isFinite(n);
453
+ if (count) count.textContent = known ? n.toLocaleString() : "\u2013";
454
+ btn.classList.toggle("twm-dt-filter-chip--empty", known && n === 0);
455
+ }
456
+ }
457
+ setValue(ctx.value);
458
+ if (ctx.counts) setCounts(ctx.counts);
459
+ return {
460
+ el,
461
+ setValue,
462
+ setCounts,
463
+ getValue: outward,
464
+ destroy() {
465
+ }
466
+ };
467
+ }
468
+
361
469
  // src/ui/components/data_table.js
362
470
  var DEFAULT_PAGE_SIZE = 100;
363
471
  var DIFF_STATUSES = /* @__PURE__ */ new Set(["added", "removed", "changed", "same", "gap"]);
@@ -405,16 +513,24 @@ function autoDisposeSweep(entry) {
405
513
  if (!el) continue;
406
514
  if (el.isConnected) {
407
515
  table._autoSeen = true;
516
+ if (table._autoPending) {
517
+ clearTimeout(table._autoTimer);
518
+ table._autoTimer = null;
519
+ table._autoPending = false;
520
+ }
408
521
  continue;
409
522
  }
410
523
  if (!table._autoSeen || table._autoPending) continue;
524
+ const grace = table._detachGrace();
525
+ if (grace === Infinity) continue;
411
526
  table._autoPending = true;
412
- setTimeout(() => {
527
+ table._autoTimer = setTimeout(() => {
413
528
  table._autoPending = false;
529
+ table._autoTimer = null;
414
530
  if (!entry.tables.has(ref)) return;
415
531
  const now = table._wrapperEl;
416
532
  if (now && !now.isConnected) table._disposeOwn();
417
- }, 0);
533
+ }, grace);
418
534
  }
419
535
  if (entry.tables.size === 0) autoDisposeRelease(entry);
420
536
  }
@@ -460,6 +576,38 @@ function perColumn(option, colIdx) {
460
576
  if (typeof option === "object") return option[colIdx] ?? null;
461
577
  return option;
462
578
  }
579
+ var DETAIL_SEQ = { n: 0 };
580
+ function defaultControlMatch(cell, value) {
581
+ if (Array.isArray(value)) return value.some((v) => String(v ?? "") === String(cell ?? ""));
582
+ if (value && typeof value === "object" && !(value instanceof Date) && ("from" in value || "to" in value)) {
583
+ const asNumber = (x) => {
584
+ if (x == null || x === "") return null;
585
+ if (x instanceof Date) return x.getTime();
586
+ if (typeof x === "number") return x;
587
+ const t = parseDateValue(x);
588
+ if (Number.isFinite(t)) return t;
589
+ const n = parseFloat(x);
590
+ return Number.isFinite(n) ? n : null;
591
+ };
592
+ const c = asNumber(cell);
593
+ if (c === null) return false;
594
+ const lo = asNumber(value.from);
595
+ const hi = asNumber(value.to);
596
+ if (lo !== null && c < lo) return false;
597
+ if (hi !== null && c > hi) return false;
598
+ return true;
599
+ }
600
+ if (typeof value === "string") {
601
+ return String(cell ?? "").toLowerCase().includes(value.toLowerCase());
602
+ }
603
+ return String(value) === String(cell ?? "");
604
+ }
605
+ function chipsMatch(spec) {
606
+ return (cell, value) => {
607
+ const want = Array.isArray(value) ? value : [value];
608
+ return want.some((v) => String(v ?? "") === String(cell ?? ""));
609
+ };
610
+ }
463
611
  function normaliseSortKey(k) {
464
612
  if (k == null) return null;
465
613
  if (k instanceof Date) k = k.getTime();
@@ -574,8 +722,36 @@ var DataTable = class {
574
722
  dateTimeFormat: null,
575
723
  durationFormat: null,
576
724
  dateTimeZone: "local",
725
+ // 0.7.0. Off — or exactly the 0.6 behaviour — until set. The
726
+ // typedef says what each does.
727
+ rowDetail: null,
728
+ hasRowDetail: null,
729
+ expandedRows: null,
730
+ onRowDetailToggle: null,
731
+ rowDetailLabel: "Details",
732
+ dataSource: null,
733
+ onLoad: null,
734
+ onLoadError: null,
735
+ hasMore: void 0,
736
+ pagerLabels: null,
737
+ pagerPosition: "top",
738
+ maxJumpOffset: null,
739
+ follow: false,
740
+ onFollowChange: null,
741
+ keepWhileDetached: false,
742
+ filterKinds: null,
743
+ onFilterChange: null,
577
744
  ...config
578
745
  };
746
+ this._expanded = new Set((this.config.expandedRows || []).map((k) => String(k)));
747
+ this._detailHeights = /* @__PURE__ */ new Map();
748
+ this._source = null;
749
+ this._following = !!this.config.follow && !!this.config.dataSource;
750
+ this._followTimer = null;
751
+ this._newKeys = null;
752
+ this._filterControls = /* @__PURE__ */ new Map();
753
+ this._filterCounts = /* @__PURE__ */ new Map();
754
+ this._pendingFilter = null;
579
755
  this._activeKey = this.config.activeRow == null ? null : String(this.config.activeRow);
580
756
  this._error = null;
581
757
  this._virt = null;
@@ -709,6 +885,17 @@ var DataTable = class {
709
885
  this._state.filters.clear();
710
886
  }
711
887
  }
888
+ if (updates.headers && (this._filterControls.size || this._filterCounts.size)) {
889
+ const oldHeaders = this.config.headers;
890
+ const newHeaders = updates.headers;
891
+ if (oldHeaders.length !== newHeaders.length || oldHeaders.some((h, i) => h !== newHeaders[i])) {
892
+ this._destroyFilterControls();
893
+ this._filterCounts.clear();
894
+ }
895
+ }
896
+ if (Object.prototype.hasOwnProperty.call(updates, "filterKinds")) {
897
+ this._destroyFilterControls();
898
+ }
712
899
  Object.assign(this.config, updates);
713
900
  if (updates.rows) {
714
901
  this._state.selected.clear();
@@ -744,7 +931,7 @@ var DataTable = class {
744
931
  if (!this._wrapperEl) return;
745
932
  this._wrapperEl.classList.toggle(
746
933
  "twm-data-table-component--loading",
747
- !!on && this.config.emptyState !== void 0
934
+ !!on && (this.config.emptyState !== void 0 || !!this.config.dataSource)
748
935
  );
749
936
  let overlay = this._wrapperEl.querySelector(".twm-data-table__loading");
750
937
  if (on) {
@@ -863,6 +1050,13 @@ var DataTable = class {
863
1050
  * @param {number} page - Page number (0-indexed)
864
1051
  */
865
1052
  goToPage(page) {
1053
+ if (this.config.dataSource) {
1054
+ const size = this._sourcePageSize();
1055
+ let p = Math.max(0, Math.floor(Number(page)) || 0);
1056
+ const total = this._source?.total;
1057
+ if (typeof total === "number") p = Math.min(p, Math.max(0, Math.ceil(total / size) - 1));
1058
+ return this._sourceGoTo(p * size);
1059
+ }
866
1060
  const rowCount = this._getProcessedRows().length;
867
1061
  const maxPage = Math.ceil(rowCount / this.config.pageSize) - 1;
868
1062
  this._state.offset = Math.max(0, Math.min(page, maxPage)) * this.config.pageSize;
@@ -873,6 +1067,10 @@ var DataTable = class {
873
1067
  * @param {number} rowIndex - Row index
874
1068
  */
875
1069
  goToRow(rowIndex) {
1070
+ if (this.config.dataSource) {
1071
+ const size = this._sourcePageSize();
1072
+ return this.goToPage(Math.floor(Math.max(0, Math.floor(Number(rowIndex)) || 0) / size));
1073
+ }
876
1074
  const rowCount = this._getProcessedRows().length;
877
1075
  const idx = Math.max(0, Math.min(rowIndex, rowCount - 1));
878
1076
  this._state.offset = Math.floor(idx / this.config.pageSize) * this.config.pageSize;
@@ -897,6 +1095,7 @@ var DataTable = class {
897
1095
  this._awaitWithSpinner(ret);
898
1096
  this._savePersisted();
899
1097
  this.render();
1098
+ if (this.config.dataSource) this._sourceLoad({ offset: 0, reason: "sort" });
900
1099
  }
901
1100
  /** Internal: if `maybePromise` is a Promise (or thenable),
902
1101
  * toggle the loading overlay around it. No-op otherwise. */
@@ -913,7 +1112,21 @@ var DataTable = class {
913
1112
  this._processedRows = null;
914
1113
  this._processedIndexMap = null;
915
1114
  this._state.offset = 0;
1115
+ this._pendingFilter = null;
916
1116
  this._savePersisted();
1117
+ for (const { control } of this._filterControls.values()) {
1118
+ try {
1119
+ control.setValue?.(null);
1120
+ } catch (err) {
1121
+ console.error("[DataTable] filter control setValue threw", err);
1122
+ }
1123
+ }
1124
+ this._notifyFilterChange();
1125
+ if (this.config.dataSource) {
1126
+ if (this._wrapperEl) this.render();
1127
+ this._sourceLoad({ offset: 0, reason: "filter" });
1128
+ return;
1129
+ }
917
1130
  this.render();
918
1131
  }
919
1132
  /**
@@ -930,6 +1143,8 @@ var DataTable = class {
930
1143
  const activeFilterColIdx = this._getActiveFilterColIdx();
931
1144
  const doc = this.container.ownerDocument;
932
1145
  const hadTableFocus = !!this._tableEl && doc?.activeElement === this._tableEl;
1146
+ const focus070 = this._captureFocus070(doc);
1147
+ const pendingFilter = this.config.dataSource ? this._pendingFilter : null;
933
1148
  const prevWrap = this._tableWrapEl;
934
1149
  const prevScroll = prevWrap?.isConnected && this._renderedOffset === this._state.offset ? { top: prevWrap.scrollTop, left: prevWrap.scrollLeft } : null;
935
1150
  this._cleanup();
@@ -956,9 +1171,12 @@ var DataTable = class {
956
1171
  this._wrapperEl.style.cssText = "display:flex; flex-direction:column; height:100%; min-height:0;";
957
1172
  }
958
1173
  this._loading = false;
959
- if (pagination && rows.length > 0 && !this._pagingConfig()) {
1174
+ this._paginationEl = null;
1175
+ if (pagination && (rows.length > 0 || (this._source?.offset || 0) > 0) && !this._pagingConfig()) {
960
1176
  this._paginationEl = this._createPagination();
961
- if (this._paginationEl) this._wrapperEl.appendChild(this._paginationEl);
1177
+ if (this._paginationEl && this.config.pagerPosition !== "bottom") {
1178
+ this._wrapperEl.appendChild(this._paginationEl);
1179
+ }
962
1180
  }
963
1181
  const tableWrap = document.createElement("div");
964
1182
  tableWrap.className = this.config.readonly ? "twm-preview-table-wrap twm-preview-table-wrap--wizard" : "twm-preview-table-wrap";
@@ -966,7 +1184,8 @@ var DataTable = class {
966
1184
  if (this._error && rows.length > 0) {
967
1185
  this._wrapperEl.appendChild(this._createErrorBanner());
968
1186
  }
969
- if (rows.length === 0) {
1187
+ const keepFilterRow = rows.length === 0 && !!this.config.dataSource && this.config.filterable;
1188
+ if (rows.length === 0 && !keepFilterRow) {
970
1189
  const { headers, showRowNumbers } = this.config;
971
1190
  const headerWrap = document.createElement("div");
972
1191
  headerWrap.className = "twm-preview-table-header-wrap";
@@ -996,25 +1215,7 @@ var DataTable = class {
996
1215
  const bodyTable = document.createElement("table");
997
1216
  bodyTable.className = headerTable.className;
998
1217
  const tbody = document.createElement("tbody");
999
- const emptyTr = document.createElement("tr");
1000
- emptyTr.className = "data-preview-row data-table__empty-row";
1001
- const colCount = headers.length + (showRowNumbers ? 1 : 0);
1002
- const emptyTd = document.createElement("td");
1003
- emptyTd.colSpan = colCount;
1004
- if (this._error) {
1005
- emptyTd.className = "twm-data-table__empty-cell twm-data-table__empty-cell--error";
1006
- emptyTd.setAttribute("role", "alert");
1007
- this._appendStateContent(emptyTd, this._errorContent());
1008
- } else if (this.config.emptyState !== void 0) {
1009
- emptyTd.className = "twm-data-table__empty-cell twm-data-table__empty-cell--state";
1010
- const state = typeof this.config.emptyState === "function" ? this.config.emptyState() : this.config.emptyState;
1011
- this._appendStateContent(emptyTd, state);
1012
- } else {
1013
- emptyTd.style.cssText = "text-align:center; font-style:italic; color:#888; padding:16px;";
1014
- emptyTd.textContent = emptyMessage;
1015
- }
1016
- emptyTr.appendChild(emptyTd);
1017
- tbody.appendChild(emptyTr);
1218
+ tbody.appendChild(this._buildEmptyRow(headers.length + (showRowNumbers ? 1 : 0), emptyMessage));
1018
1219
  bodyTable.appendChild(tbody);
1019
1220
  tableWrap.appendChild(bodyTable);
1020
1221
  this._wrapperEl.appendChild(tableWrap);
@@ -1038,10 +1239,20 @@ var DataTable = class {
1038
1239
  } else {
1039
1240
  this._headerTableEl = null;
1040
1241
  }
1242
+ if (keepFilterRow && this._tbodyEl) {
1243
+ const n = this.config.headers.length + (this.config.showRowNumbers ? 1 : 0);
1244
+ this._tbodyEl.appendChild(this._buildEmptyRow(n, emptyMessage));
1245
+ }
1041
1246
  tableWrap.appendChild(fullTable);
1042
1247
  this._wrapperEl.appendChild(tableWrap);
1043
1248
  }
1044
1249
  this._tableWrapEl = tableWrap;
1250
+ if (this._paginationEl && this.config.pagerPosition === "bottom") {
1251
+ this._paginationEl.classList.add("twm-pagination-controls--bottom");
1252
+ this._paginationEl.style.borderBottom = "0";
1253
+ this._paginationEl.style.borderTop = "1px solid #2a2a2a";
1254
+ this._wrapperEl.appendChild(this._paginationEl);
1255
+ }
1045
1256
  this._pagingEl = null;
1046
1257
  if (this._pagingConfig()) {
1047
1258
  this._pagingEl = this._createPagingStrip();
@@ -1093,7 +1304,7 @@ var DataTable = class {
1093
1304
  this._installHeaderScrollSync();
1094
1305
  if (rows.length > 0) this._installColumnResizers();
1095
1306
  }
1096
- if (rows.length > 0 && (this.config.selectable || this.config.copyable || !this.config.readonly || typeof this.config.onRowActivate === "function")) {
1307
+ if (rows.length > 0 && (this.config.selectable || this.config.copyable || !this.config.readonly || typeof this.config.onRowActivate === "function" || typeof this.config.rowDetail === "function")) {
1097
1308
  this._installInteractions();
1098
1309
  }
1099
1310
  if (activeFilterColIdx !== null) {
@@ -1103,6 +1314,18 @@ var DataTable = class {
1103
1314
  this._tableEl.focus({ preventScroll: true });
1104
1315
  } catch (_) {
1105
1316
  }
1317
+ } else if (focus070) {
1318
+ this._restoreFocus070(focus070);
1319
+ }
1320
+ if (pendingFilter) this._resumePendingFilter(pendingFilter);
1321
+ if (this.config.dataSource) {
1322
+ if (!this._source?.started) {
1323
+ this._sourceState().started = true;
1324
+ Promise.resolve().then(() => this._sourceLoad({ offset: 0, reason: "initial" }));
1325
+ } else if (this._source.inFlight && !this._source.pendingAppend) {
1326
+ this.setLoading(true);
1327
+ }
1328
+ if (this._following && !this._followTimer) this._armFollow();
1106
1329
  }
1107
1330
  }
1108
1331
  /**
@@ -1205,6 +1428,22 @@ var DataTable = class {
1205
1428
  _teardown(emptyHost) {
1206
1429
  autoDisposeUnwatch(this);
1207
1430
  this._autoSeen = false;
1431
+ if (this._autoTimer) {
1432
+ clearTimeout(this._autoTimer);
1433
+ this._autoTimer = null;
1434
+ }
1435
+ this._autoPending = false;
1436
+ this._disarmFollow();
1437
+ if (this._source) {
1438
+ this._source.ticket++;
1439
+ try {
1440
+ this._source.controller?.abort();
1441
+ } catch (_) {
1442
+ }
1443
+ this._source.controller = null;
1444
+ this._source.inFlight = false;
1445
+ }
1446
+ this._destroyFilterControls();
1208
1447
  this._runOffs(this._activationOffs);
1209
1448
  this._activationOffs = [];
1210
1449
  this._cleanup();
@@ -1244,7 +1483,7 @@ var DataTable = class {
1244
1483
  // ─────────────────────────────────────────────────────────────────
1245
1484
  _getProcessedRows() {
1246
1485
  if (this._processedRows !== null) return this._processedRows;
1247
- const isServerSide = typeof this.config.onPageChange === "function";
1486
+ const isServerSide = this._isServerSide();
1248
1487
  let rows = this.config.rows;
1249
1488
  let indexMap = rows.map((_, i) => i);
1250
1489
  if (!isServerSide && this._state.filters.size > 0) {
@@ -1265,12 +1504,23 @@ var DataTable = class {
1265
1504
  const filters = this._state.filters;
1266
1505
  const filteredRows = [];
1267
1506
  const filteredMap = [];
1507
+ const matchers = /* @__PURE__ */ new Map();
1508
+ for (const colIdx of filters.keys()) matchers.set(colIdx, this._filterMatcher(colIdx));
1268
1509
  for (let i = 0; i < rows.length; i++) {
1269
1510
  const row = rows[i];
1270
1511
  let pass = true;
1271
1512
  for (const [colIdx, filterText] of filters) {
1272
1513
  if (!filterText) continue;
1273
1514
  const value = row[colIdx];
1515
+ const kindMatch = matchers.get(colIdx);
1516
+ if (kindMatch) {
1517
+ if (this._isEmptyFilterValue(filterText)) continue;
1518
+ if (!kindMatch(value, filterText, row)) {
1519
+ pass = false;
1520
+ break;
1521
+ }
1522
+ continue;
1523
+ }
1274
1524
  const colType = this._valueKind(colIdx);
1275
1525
  if (colType === "num") {
1276
1526
  if (!this._matchNumericFilter(value, filterText)) {
@@ -1441,6 +1691,15 @@ var DataTable = class {
1441
1691
  headers.forEach((header, colIdx) => {
1442
1692
  const th = document.createElement("th");
1443
1693
  th.className = "data-table__filter-cell";
1694
+ const control = this._filterControlFor(colIdx, header);
1695
+ if (control) {
1696
+ th.classList.add("data-table__filter-cell--control");
1697
+ tr.classList.add("twm-data-table__filter-row--controls");
1698
+ th.addEventListener("click", (e) => e.stopPropagation());
1699
+ if (control.el) th.appendChild(control.el);
1700
+ tr.appendChild(th);
1701
+ return;
1702
+ }
1444
1703
  const wrapper = document.createElement("div");
1445
1704
  wrapper.className = "twm-data-table__filter-wrapper";
1446
1705
  const input = document.createElement("input");
@@ -1711,23 +1970,55 @@ var DataTable = class {
1711
1970
  if (this._filterDebounceTimer) {
1712
1971
  clearTimeout(this._filterDebounceTimer);
1713
1972
  }
1973
+ this._pendingFilter = { colIdx, value };
1714
1974
  this._filterDebounceTimer = setTimeout(() => {
1975
+ this._pendingFilter = null;
1715
1976
  this._onFilterInput(colIdx, value);
1716
1977
  }, 200);
1717
1978
  }
1718
1979
  _onFilterInput(colIdx, value) {
1719
- if (value) {
1980
+ const empty = typeof value === "string" ? !value : this._isEmptyFilterValue(value);
1981
+ if (!empty) {
1720
1982
  this._state.filters.set(colIdx, value);
1721
1983
  } else {
1722
1984
  this._state.filters.delete(colIdx);
1723
1985
  }
1986
+ this._pendingFilter = null;
1724
1987
  this._invalidateProcessedCache();
1725
1988
  this._state.offset = 0;
1726
1989
  this._state.selected.clear();
1727
1990
  this._state.anchorIndex = null;
1728
1991
  this._savePersisted();
1992
+ const entry = this._filterControls.get(colIdx);
1993
+ if (entry) {
1994
+ try {
1995
+ entry.control.setValue?.(empty ? null : value);
1996
+ } catch (err) {
1997
+ console.error("[DataTable] filter control setValue threw", err);
1998
+ }
1999
+ }
2000
+ this._notifyFilterChange();
2001
+ if (this.config.dataSource) {
2002
+ this._sourceLoad({ offset: 0, reason: "filter" });
2003
+ return;
2004
+ }
1729
2005
  this.render();
1730
2006
  }
2007
+ /** 0.7.0. Put back a filter that was still being typed when a render
2008
+ * rebuilt its box: the words, and the debounce that will apply them. */
2009
+ _resumePendingFilter({ colIdx, value }) {
2010
+ const filterRow = this._wrapperEl?.querySelector(".twm-data-table__filter-row");
2011
+ const cell = filterRow?.children[this.config.showRowNumbers ? colIdx + 1 : colIdx];
2012
+ const input = cell?.querySelector(".twm-data-table__filter-input");
2013
+ if (input && input.value !== value) {
2014
+ const focused = input.ownerDocument?.activeElement === input;
2015
+ input.value = value;
2016
+ const clear = cell.querySelector(".twm-data-table__filter-clear");
2017
+ if (clear) clear.style.display = value ? "" : "none";
2018
+ if (focused) input.setSelectionRange(value.length, value.length);
2019
+ }
2020
+ this._onFilterInputDebounced(colIdx, value);
2021
+ }
1731
2022
  _getActiveFilterColIdx() {
1732
2023
  const active = document.activeElement;
1733
2024
  if (!active || !active.classList.contains("twm-data-table__filter-input")) return null;
@@ -1821,6 +2112,7 @@ var DataTable = class {
1821
2112
  return types;
1822
2113
  }
1823
2114
  _createPagination() {
2115
+ if (this._usesPager070()) return this._createPager070();
1824
2116
  const { rows, pageSize, totalCount: configTotalCount, offset: configOffset, onPageChange, onJumpToRow } = this.config;
1825
2117
  const isServerSide = typeof onPageChange === "function";
1826
2118
  const offset = isServerSide ? configOffset || 0 : this._state.offset;
@@ -1943,6 +2235,25 @@ var DataTable = class {
1943
2235
  const p = this.config.paging;
1944
2236
  if (!p || typeof p !== "object") return null;
1945
2237
  const size = Number.isFinite(p.pageSize) && p.pageSize >= 1 ? Math.floor(p.pageSize) : this.config.pageSize;
2238
+ if (this.config.dataSource) {
2239
+ const src = this._source || { offset: 0, total: null, atLeast: 0, hasMore: false };
2240
+ return {
2241
+ ...p,
2242
+ pageSize: size,
2243
+ offset: src.offset || 0,
2244
+ held: this.config.rows?.length || 0,
2245
+ total: src.total,
2246
+ atLeast: src.atLeast || 0,
2247
+ hasMore: !!src.hasMore,
2248
+ loadMorePx: Number.isFinite(p.loadMorePx) ? p.loadMorePx : PAGING_LOAD_MORE_PX,
2249
+ onPage: (offset) => this._sourceGoTo(offset),
2250
+ onLoadMore: () => this._sourceLoadMore(),
2251
+ onLoadAll: () => this._sourceLoadAll(),
2252
+ // The switch is drawn, as in 0.6.2, only for a consumer that
2253
+ // listens; the table switches and then tells it.
2254
+ onModeChange: typeof p.onModeChange === "function" ? (mode) => this._sourceSetMode(mode) : void 0
2255
+ };
2256
+ }
1946
2257
  return {
1947
2258
  ...p,
1948
2259
  pageSize: size,
@@ -1978,7 +2289,12 @@ var DataTable = class {
1978
2289
  */
1979
2290
  setPaging(patch = {}) {
1980
2291
  if (!this.config.paging) return;
2292
+ const before = this.config.dataSource ? this.pagingState?.mode : null;
1981
2293
  this.config.paging = { ...this.config.paging, ...patch };
2294
+ if (before && this.pagingState?.mode !== before) {
2295
+ this._sourceLoad({ offset: 0, reason: "mode" });
2296
+ return;
2297
+ }
1982
2298
  this._refreshPagingStrip();
1983
2299
  this._pagingMaybeLoadMore();
1984
2300
  }
@@ -2032,6 +2348,7 @@ var DataTable = class {
2032
2348
  _pagingMaybeLoadMore() {
2033
2349
  const p = this._pagingConfig();
2034
2350
  if (!p || !p.hasMore || this._pagingBusy) return;
2351
+ if (this.config.dataSource && this._source?.inFlight) return;
2035
2352
  const state = this.pagingState;
2036
2353
  if (state.mode !== "scroll" || p.held >= state.hardLimit) return;
2037
2354
  const wrap = this._tableWrapEl;
@@ -2068,19 +2385,22 @@ var DataTable = class {
2068
2385
  if (exact) of = ` of ${n(total)}`;
2069
2386
  else if (known > to) of = ` of more than ${n(known - 1)}`;
2070
2387
  info.textContent = held ? `Rows ${n(from)}\u2013${n(to)}${of}` : "No rows on this page";
2071
- const hasPrev = p.offset > 0;
2072
- const hasNext = exact ? p.offset + held < total : p.hasMore;
2073
- const lastOffset = exact && total > 0 ? Math.floor((total - 1) / pageSize) * pageSize : null;
2388
+ const pn = this._pageNav({ offset: p.offset, held, total, hasMore: p.hasMore, pageSize });
2389
+ const { hasPrev, hasNext } = pn;
2390
+ const lastOffset = exact && total > 0 ? pn.lastOffset : null;
2074
2391
  const go = (offset) => (ev) => {
2075
2392
  if (!ev?.currentTarget?.disabled) this._pagingCall("page", p.onPage, offset, pageSize);
2076
2393
  };
2077
2394
  const nav = document.createElement("span");
2078
2395
  nav.className = "twm-dt-paging__nav";
2079
2396
  const off = !!busy || typeof p.onPage !== "function";
2397
+ const words = this._usesPager070();
2398
+ const button = (icon, name, tip, disabled, onClick, refusal = null) => words ? this._pagerButton(icon, name, tip, disabled, onClick, refusal) : this._createPaginationBtn(icon, tip, disabled, onClick);
2080
2399
  nav.append(
2081
- this._createPaginationBtn("first_page", "First page", off || !hasPrev, go(0)),
2082
- this._createPaginationBtn(
2400
+ button("first_page", "first", "First page", off || !hasPrev, go(0)),
2401
+ button(
2083
2402
  "chevron_left",
2403
+ "prev",
2084
2404
  "Previous page",
2085
2405
  off || !hasPrev,
2086
2406
  go(Math.max(0, p.offset - pageSize))
@@ -2088,20 +2408,28 @@ var DataTable = class {
2088
2408
  );
2089
2409
  const page = document.createElement("span");
2090
2410
  page.className = "twm-dt-paging__page";
2091
- const current = Math.floor(p.offset / pageSize) + 1;
2092
- page.textContent = exact ? `Page ${n(current)} of ${n(Math.max(1, Math.ceil(total / pageSize)))}` : `Page ${n(current)}`;
2411
+ page.textContent = exact ? `Page ${n(pn.page)} of ${n(pn.pages)}` : `Page ${n(pn.page)}`;
2093
2412
  nav.append(
2094
2413
  page,
2095
- this._createPaginationBtn(
2414
+ button(
2096
2415
  "chevron_right",
2416
+ "next",
2097
2417
  "Next page",
2098
2418
  off || !hasNext,
2099
2419
  go(p.offset + pageSize)
2100
2420
  ),
2101
2421
  // The last page needs the exact count: without it there is no
2102
2422
  // offset to go to, and the button says so by being off.
2103
- this._createPaginationBtn(
2423
+ words ? button(
2424
+ "last_page",
2425
+ "last",
2426
+ "Last page",
2427
+ off || !hasNext || lastOffset === null,
2428
+ go(lastOffset),
2429
+ pn.lastRefusal
2430
+ ) : button(
2104
2431
  "last_page",
2432
+ "last",
2105
2433
  lastOffset === null ? "Last page (the total is not known)" : "Last page",
2106
2434
  off || !hasNext || lastOffset === null,
2107
2435
  go(lastOffset)
@@ -2159,9 +2487,10 @@ var DataTable = class {
2159
2487
  return el;
2160
2488
  }
2161
2489
  _createTable() {
2162
- const { headers, rows, pageSize, sortable, filterable, showRowNumbers, readonly, getHeaderIcon, onPageChange, offset: configOffset } = this.config;
2490
+ const { headers, rows, pageSize, sortable, filterable, showRowNumbers, readonly, getHeaderIcon } = this.config;
2163
2491
  const { sortColumn, sortAscending } = this._state;
2164
- const isServerSide = typeof onPageChange === "function";
2492
+ const configOffset = this.config.dataSource ? 0 : this.config.offset;
2493
+ const isServerSide = this._isServerSide();
2165
2494
  const processedRows = isServerSide ? rows : this._getProcessedRows();
2166
2495
  const offset = isServerSide ? configOffset || 0 : this._state.offset;
2167
2496
  const pageRows = isServerSide || !this.config.pagination || this._pagingConfig() ? processedRows : processedRows.slice(offset, offset + pageSize);
@@ -2226,8 +2555,8 @@ var DataTable = class {
2226
2555
  const processedIdx = offset + localIdx;
2227
2556
  return isServerSide ? (configOffset || 0) + localIdx : this._processedIndexMap?.[processedIdx] ?? processedIdx;
2228
2557
  };
2229
- this._positionBase = isServerSide ? configOffset || 0 : this.config.pagination ? offset : 0;
2230
- const paged = this._pagingConfig();
2558
+ this._positionBase = this.config.dataSource ? this._source?.offset || 0 : isServerSide ? configOffset || 0 : this.config.pagination ? offset : 0;
2559
+ const paged = this.config.dataSource ? null : this._pagingConfig();
2231
2560
  if (paged && Number.isFinite(paged.offset)) this._positionBase += paged.offset;
2232
2561
  const tbody = document.createElement("tbody");
2233
2562
  table.appendChild(tbody);
@@ -2240,7 +2569,9 @@ var DataTable = class {
2240
2569
  });
2241
2570
  } else {
2242
2571
  pageRows.forEach((row, localIdx) => {
2243
- tbody.appendChild(this._buildBodyRow(row, globalOf(localIdx), localIdx));
2572
+ const tr = this._buildBodyRow(row, globalOf(localIdx), localIdx);
2573
+ tbody.appendChild(tr);
2574
+ if (this._detailOn()) this._appendDetailIfOpen(tr, null);
2244
2575
  });
2245
2576
  }
2246
2577
  return table;
@@ -2384,6 +2715,7 @@ var DataTable = class {
2384
2715
  const heights = new Float64Array(n);
2385
2716
  const offsets = new Float64Array(n + 1);
2386
2717
  const fn = typeof this.config.virtualRowHeight === "function" ? this.config.virtualRowHeight : null;
2718
+ const details = this._detailOn() && this._expanded.size > 0;
2387
2719
  let y = 0;
2388
2720
  for (let i = 0; i < n; i++) {
2389
2721
  let h = v.base;
@@ -2391,6 +2723,7 @@ var DataTable = class {
2391
2723
  const asked = fn(v.globalOf(i), v.base);
2392
2724
  h = Number.isFinite(asked) && asked > 0 ? asked : 0;
2393
2725
  }
2726
+ if (details && h > 0) h += this._detailHeightOf(v.rows[i], v.globalOf(i), v.base);
2394
2727
  heights[i] = h;
2395
2728
  offsets[i] = y;
2396
2729
  y += h;
@@ -2432,6 +2765,14 @@ var DataTable = class {
2432
2765
  * larger than half a pixel, so rows of mixed heights cannot make the
2433
2766
  * offsets move under the reader on every scroll.
2434
2767
  *
2768
+ * 0.7.0 `rowDetail`: AN OPEN DETAIL IS PART OF ITS ROW'S HEIGHT, and is
2769
+ * measured at the same moments. It is never one of the rows the median is
2770
+ * taken from (the window's map holds the rows, not their details), so a
2771
+ * tall detail cannot pass for a row; it is read on its own, into the
2772
+ * row's height — and read again here, because a detail's height moves for
2773
+ * the same reasons a row's does (the column widths fixed, a font), and
2774
+ * more: it wraps across the whole table's width.
2775
+ *
2435
2776
  * @returns {boolean} whether the heights changed (the offsets are recomputed).
2436
2777
  */
2437
2778
  _virtMeasure() {
@@ -2441,18 +2782,24 @@ var DataTable = class {
2441
2782
  const heights = [];
2442
2783
  for (const tr of v.map.values()) {
2443
2784
  if (!tr.isConnected || tr.hidden) continue;
2444
- const h2 = tr.getBoundingClientRect().height / zoom;
2445
- if (h2 > 0) heights.push(h2);
2785
+ const h = tr.getBoundingClientRect().height / zoom;
2786
+ if (h > 0) heights.push(h);
2446
2787
  if (heights.length >= 64) break;
2447
2788
  }
2448
- if (!heights.length) return false;
2449
- heights.sort((a, b) => a - b);
2450
- const h = heights[heights.length >> 1];
2451
- const tolerance = v.measured ? 0.5 : 0.01;
2452
- v.measured = true;
2453
- if (Math.abs(h - v.base) <= tolerance) return false;
2454
- v.base = h;
2455
- this._virtBase = h;
2789
+ const details = this._detailOn() && this._expanded.size > 0 && this._virtReadDetailHeights(v.map.values());
2790
+ let base = false;
2791
+ if (heights.length) {
2792
+ heights.sort((a, b) => a - b);
2793
+ const h = heights[heights.length >> 1];
2794
+ const tolerance = v.measured ? 0.5 : 0.01;
2795
+ v.measured = true;
2796
+ if (Math.abs(h - v.base) > tolerance) {
2797
+ v.base = h;
2798
+ this._virtBase = h;
2799
+ base = true;
2800
+ }
2801
+ }
2802
+ if (!base && !details) return false;
2456
2803
  this._virtComputeOffsets();
2457
2804
  return true;
2458
2805
  }
@@ -2531,6 +2878,7 @@ var DataTable = class {
2531
2878
  for (const [i, tr] of v.map) {
2532
2879
  if (!wanted.has(i)) {
2533
2880
  tr.remove();
2881
+ this._removeDetailPieces(tr);
2534
2882
  v.map.delete(i);
2535
2883
  removed?.push(tr);
2536
2884
  }
@@ -2550,10 +2898,12 @@ var DataTable = class {
2550
2898
  const tr = this._buildBodyRow(v.rows[i], v.globalOf(i), i);
2551
2899
  v.map.set(i, tr);
2552
2900
  tbody.insertBefore(tr, ref);
2901
+ if (this._detailOn()) this._appendDetailIfOpen(tr, ref);
2553
2902
  ref = tr;
2554
2903
  added.push(tr);
2555
2904
  }
2556
2905
  added.reverse();
2906
+ if (this._detailOn() && added.length) this._virtMeasureDetails(added);
2557
2907
  return added;
2558
2908
  }
2559
2909
  /**
@@ -2753,6 +3103,7 @@ var DataTable = class {
2753
3103
  if (colIdx === 0 && typeof this.config.rowIcon === "function") {
2754
3104
  this._prependRowIcon(td, this.config.rowIcon(row, globalIdx));
2755
3105
  }
3106
+ if (colIdx === 0 && this._detailOn()) this._prependDetailToggle(td, tr, row, globalIdx);
2756
3107
  if (this.config.onCellContextMenu) {
2757
3108
  const cellColIdx = colIdx;
2758
3109
  td.addEventListener("contextmenu", (ev) => {
@@ -2837,12 +3188,1141 @@ var DataTable = class {
2837
3188
  }
2838
3189
  tr.classList.toggle("selected", this._state?.selected?.has(index) === true);
2839
3190
  this._decorateRow(tr, row, index);
3191
+ if (this._detailOn()) this._redrawDetail(tr);
2840
3192
  if (focusedCol >= 0 && tr.children[focusedCol]) {
2841
3193
  tr.children[focusedCol].focus?.();
2842
3194
  }
2843
3195
  return true;
2844
3196
  }
2845
3197
  // ─────────────────────────────────────────────────────────────────
3198
+ // 0.7.0 — a row's detail (#26)
3199
+ // ─────────────────────────────────────────────────────────────────
3200
+ //
3201
+ // A detail is TWO rows after its row: the detail itself (one cell across
3202
+ // every column) and a hidden filler. The filler is the stripe: rows are
3203
+ // striped by `tr:nth-child(even)`, a question about POSITION, and one extra
3204
+ // row would flip the stripe of every row below an open detail. Two keep
3205
+ // the parity — the trick `_virtSpacers` already plays for the window.
3206
+ //
3207
+ // Neither row has a `__rowIndex`, so everything that walks the body for
3208
+ // rows of data (selection, the open mark, `_firstBodyRow`, `getRowElement`,
3209
+ // the activation and the copy menu) steps over them as it steps over a
3210
+ // spacer.
3211
+ _detailOn() {
3212
+ return typeof this.config.rowDetail === "function";
3213
+ }
3214
+ _rowHasDetail(row, rowIdx) {
3215
+ const fn = this.config.hasRowDetail;
3216
+ if (typeof fn !== "function") return true;
3217
+ try {
3218
+ return !!fn(row, rowIdx);
3219
+ } catch (err) {
3220
+ console.error("[DataTable] hasRowDetail threw", err);
3221
+ return false;
3222
+ }
3223
+ }
3224
+ /** The toggle: a real `<button>` (so Enter and Space work on it, and it is
3225
+ * a control `onRowActivate` leaves alone), its glyph drawn by CSS so the
3226
+ * cell's text, its tooltip and a copy stay the cell's own. */
3227
+ _prependDetailToggle(td, tr, row, rowIdx) {
3228
+ if (!this._rowHasDetail(row, rowIdx)) {
3229
+ const gap = document.createElement("span");
3230
+ gap.className = "twm-dt-detail-toggle-gap";
3231
+ gap.setAttribute("aria-hidden", "true");
3232
+ td.insertBefore(gap, td.firstChild);
3233
+ return;
3234
+ }
3235
+ const key = this._rowKey(row, rowIdx);
3236
+ const open = key !== null && this._expanded.has(key);
3237
+ const btn = document.createElement("button");
3238
+ btn.type = "button";
3239
+ btn.className = "material-symbols-outlined twm-dt-detail-toggle";
3240
+ btn.setAttribute("aria-expanded", open ? "true" : "false");
3241
+ btn.setAttribute("aria-label", String(this.config.rowDetailLabel || "Details"));
3242
+ btn.title = String(this.config.rowDetailLabel || "Details");
3243
+ btn.addEventListener("click", (ev) => {
3244
+ ev.preventDefault();
3245
+ ev.stopPropagation();
3246
+ this._setRowElExpanded(tr, btn.getAttribute("aria-expanded") !== "true", ev);
3247
+ });
3248
+ td.insertBefore(btn, td.firstChild);
3249
+ td.classList.add("twm-dt-cell--has-detail-toggle");
3250
+ tr.classList.add("twm-dt-row--has-detail");
3251
+ }
3252
+ /** After `tr` has been placed: its open detail, in front of `ref` (the row
3253
+ * after it, or null for the end of the body). */
3254
+ _appendDetailIfOpen(tr, ref) {
3255
+ const row = tr.__row;
3256
+ const idx = tr.__rowIndex;
3257
+ if (!this._rowHasDetail(row, idx)) return;
3258
+ const key = this._rowKey(row, idx);
3259
+ if (key === null || !this._expanded.has(key)) return;
3260
+ const pieces = this._buildDetailPieces(tr, key);
3261
+ const parent = tr.parentNode;
3262
+ if (!parent) return;
3263
+ for (const p of pieces) parent.insertBefore(p, ref);
3264
+ }
3265
+ _buildDetailPieces(tr, key) {
3266
+ const n = (this.config.headers?.length || 0) + (this.config.showRowNumbers ? 1 : 0);
3267
+ const detailTr = document.createElement("tr");
3268
+ detailTr.className = "twm-dt-detail-row";
3269
+ const td = document.createElement("td");
3270
+ td.className = "twm-dt-detail-cell";
3271
+ td.colSpan = Math.max(1, n);
3272
+ const host = document.createElement("div");
3273
+ host.className = "twm-dt-detail";
3274
+ DETAIL_SEQ.n += 1;
3275
+ host.id = `twm-dt-detail-${DETAIL_SEQ.n}`;
3276
+ td.appendChild(host);
3277
+ detailTr.appendChild(td);
3278
+ const filler = document.createElement("tr");
3279
+ filler.className = "twm-dt-detail-filler";
3280
+ filler.hidden = true;
3281
+ filler.setAttribute("aria-hidden", "true");
3282
+ detailTr.__detailOf = tr;
3283
+ tr.__detail = [detailTr, filler];
3284
+ this._drawDetail(tr, host, key);
3285
+ const btn = tr.querySelector(":scope > td > .twm-dt-detail-toggle");
3286
+ if (btn) btn.setAttribute("aria-controls", host.id);
3287
+ return tr.__detail;
3288
+ }
3289
+ _drawDetail(tr, host, key) {
3290
+ host.replaceChildren();
3291
+ let out;
3292
+ try {
3293
+ out = this.config.rowDetail(tr.__row, tr.__rowIndex, { host, key, table: this });
3294
+ } catch (err) {
3295
+ console.error("[DataTable] rowDetail threw", err);
3296
+ out = err?.message || String(err);
3297
+ }
3298
+ if (out == null || out === false || out === host) return;
3299
+ if (typeof Node !== "undefined" && out instanceof Node) host.appendChild(out);
3300
+ else host.textContent = String(out);
3301
+ }
3302
+ _removeDetailPieces(tr) {
3303
+ const pieces = tr.__detail;
3304
+ if (!pieces) return;
3305
+ for (const p of pieces) p.remove();
3306
+ tr.__detail = null;
3307
+ const btn = tr.querySelector(":scope > td > .twm-dt-detail-toggle");
3308
+ if (btn) btn.removeAttribute("aria-controls");
3309
+ }
3310
+ /** `updateRow`'s half: the detail follows the row's new data. */
3311
+ _redrawDetail(tr) {
3312
+ const key = this._rowKey(tr.__row, tr.__rowIndex);
3313
+ const want = this._rowHasDetail(tr.__row, tr.__rowIndex) && key !== null && this._expanded.has(key);
3314
+ const pieces = tr.__detail;
3315
+ if (!want) {
3316
+ if (pieces) this._removeDetailPieces(tr);
3317
+ return;
3318
+ }
3319
+ if (pieces) {
3320
+ const host = pieces[0].querySelector(".twm-dt-detail");
3321
+ if (host) {
3322
+ this._drawDetail(tr, host, key);
3323
+ const btn = tr.querySelector(":scope > td > .twm-dt-detail-toggle");
3324
+ if (btn) btn.setAttribute("aria-controls", host.id);
3325
+ }
3326
+ return;
3327
+ }
3328
+ if (tr.parentNode) this._appendDetailIfOpen(tr, tr.nextSibling);
3329
+ }
3330
+ /** Open or close the detail of the row `tr` draws. Returns whether
3331
+ * anything changed. */
3332
+ _setRowElExpanded(tr, open, ev = null) {
3333
+ if (!this._detailOn() || tr.__rowIndex === void 0) return false;
3334
+ if (!this._rowHasDetail(tr.__row, tr.__rowIndex)) return false;
3335
+ const key = this._rowKey(tr.__row, tr.__rowIndex);
3336
+ if (key === null || this._expanded.has(key) === !!open) return false;
3337
+ if (open) this._expanded.add(key);
3338
+ else this._expanded.delete(key);
3339
+ this._syncDetailOnScreen(tr, key);
3340
+ this._afterDetailChange();
3341
+ try {
3342
+ this.config.onRowDetailToggle?.(tr.__rowIndex, tr.__row, !!open, key, ev);
3343
+ } catch (err) {
3344
+ console.error("[DataTable] onRowDetailToggle threw", err);
3345
+ }
3346
+ return true;
3347
+ }
3348
+ /** Make the row on screen agree with `_expanded` for its key. */
3349
+ _syncDetailOnScreen(tr, key) {
3350
+ const open = this._expanded.has(key);
3351
+ const btn = tr.querySelector(":scope > td > .twm-dt-detail-toggle");
3352
+ if (btn) btn.setAttribute("aria-expanded", open ? "true" : "false");
3353
+ if (open && !tr.__detail && tr.parentNode) {
3354
+ const ref = tr.nextSibling;
3355
+ for (const p of this._buildDetailPieces(tr, key)) tr.parentNode.insertBefore(p, ref);
3356
+ } else if (!open && tr.__detail) {
3357
+ this._removeDetailPieces(tr);
3358
+ }
3359
+ }
3360
+ /** Under `virtualize` an open detail is part of its row's height. */
3361
+ _afterDetailChange() {
3362
+ if (!this._virt) return;
3363
+ this._virtMeasureDetails([...this._virt.map.values()], { force: true });
3364
+ }
3365
+ /** The detail's height for row `i` of the window's list: 0 closed, the
3366
+ * measured height open, three rows until it has been measured. */
3367
+ _detailHeightOf(row, rowIdx, base) {
3368
+ if (!this._rowHasDetail(row, rowIdx)) return 0;
3369
+ const key = this._rowKey(row, rowIdx);
3370
+ if (key === null || !this._expanded.has(key)) return 0;
3371
+ const h = this._detailHeights.get(key);
3372
+ return h > 0 ? h : base * 3;
3373
+ }
3374
+ /** Measure the open details among `trs` (client pixels, the zoom divided
3375
+ * out, as `_virtMeasure` does), and re-lay the window if one changed. */
3376
+ _virtMeasureDetails(trs, { force = false } = {}) {
3377
+ const v = this._virt;
3378
+ if (!v) return;
3379
+ const changed = this._virtReadDetailHeights(trs) || force;
3380
+ if (!changed) return;
3381
+ this._virtComputeOffsets();
3382
+ this._virtSpacers();
3383
+ }
3384
+ /** Read the open details drawn under `trs` into `_detailHeights`; true
3385
+ * when one moved by more than half a pixel. Recomputes nothing. */
3386
+ _virtReadDetailHeights(trs) {
3387
+ let changed = false;
3388
+ const zoom = this._tableEl?.currentCSSZoom || 1;
3389
+ for (const tr of trs) {
3390
+ const d = tr?.__detail?.[0];
3391
+ if (!d || !d.isConnected) continue;
3392
+ const h = d.getBoundingClientRect().height / zoom;
3393
+ if (!(h > 0)) continue;
3394
+ const key = this._rowKey(tr.__row, tr.__rowIndex);
3395
+ if (Math.abs((this._detailHeights.get(key) || 0) - h) > 0.5) {
3396
+ this._detailHeights.set(key, h);
3397
+ changed = true;
3398
+ }
3399
+ }
3400
+ return changed;
3401
+ }
3402
+ /**
3403
+ * Open (or close) the detail of the row whose key is `key` (`getRowKey`,
3404
+ * else the row's index). A row not on screen — another page, outside the
3405
+ * window, filtered out — opens when it is drawn.
3406
+ *
3407
+ * @param {*} key
3408
+ * @param {boolean} [open=true]
3409
+ * @returns {boolean} whether anything changed
3410
+ */
3411
+ setRowExpanded(key, open = true) {
3412
+ if (key == null) return false;
3413
+ const k = String(key);
3414
+ if (this._expanded.has(k) === !!open) return false;
3415
+ if (open) this._expanded.add(k);
3416
+ else this._expanded.delete(k);
3417
+ const tr = this._rowElByKey(k);
3418
+ if (tr) {
3419
+ this._syncDetailOnScreen(tr, k);
3420
+ try {
3421
+ this.config.onRowDetailToggle?.(tr.__rowIndex, tr.__row, !!open, k, null);
3422
+ } catch (err) {
3423
+ console.error("[DataTable] onRowDetailToggle threw", err);
3424
+ }
3425
+ }
3426
+ if (this._virt) {
3427
+ this._virtComputeOffsets();
3428
+ this._virtSpacers();
3429
+ this._afterDetailChange();
3430
+ }
3431
+ return true;
3432
+ }
3433
+ /** Whether the row whose key is `key` has its detail open. */
3434
+ isRowExpanded(key) {
3435
+ return key != null && this._expanded.has(String(key));
3436
+ }
3437
+ /** The keys of the rows whose detail is open, as text. */
3438
+ getExpandedRows() {
3439
+ return [...this._expanded];
3440
+ }
3441
+ /** Close every detail. */
3442
+ collapseAllRows() {
3443
+ for (const k of [...this._expanded]) this.setRowExpanded(k, false);
3444
+ }
3445
+ /** The `<tr>` on screen for a key, or null. */
3446
+ _rowElByKey(key) {
3447
+ const tbody = this._tbodyEl;
3448
+ if (!tbody) return null;
3449
+ for (const tr of tbody.children) {
3450
+ if (tr.__rowIndex === void 0) continue;
3451
+ if (this._rowKey(tr.__row, tr.__rowIndex) === key) return tr;
3452
+ }
3453
+ return null;
3454
+ }
3455
+ // ─────────────────────────────────────────────────────────────────
3456
+ // 0.7.0 — rows from a server (#33), the pager for an unknown total
3457
+ // (#35), refresh and follow (#36)
3458
+ // ─────────────────────────────────────────────────────────────────
3459
+ //
3460
+ // ONE QUESTION AT A TIME, AND ONLY THE NEWEST ANSWER. Every request takes
3461
+ // a ticket; an answer whose ticket is not the latest is dropped, and the
3462
+ // request it answered is aborted the moment a newer one is sent. Two
3463
+ // requests in flight from a fast typist otherwise race, and the loser
3464
+ // painting last is a list that does not match the box above it.
3465
+ /** Rows that are the current page of something larger (`onPageChange`,
3466
+ * or 0.7.0's `dataSource`): drawn as given, never filtered, sorted or
3467
+ * sliced here. */
3468
+ _isServerSide() {
3469
+ return typeof this.config.onPageChange === "function" || !!this.config.dataSource;
3470
+ }
3471
+ /** `autoDispose`'s grace in ms: one task, `keepWhileDetached`'s number,
3472
+ * or Infinity for `true`. */
3473
+ _detachGrace() {
3474
+ const k = this.config.keepWhileDetached;
3475
+ if (k === true) return Infinity;
3476
+ if (typeof k === "number" && Number.isFinite(k) && k > 0) return k;
3477
+ if (k === Infinity) return Infinity;
3478
+ return 0;
3479
+ }
3480
+ _sourceState() {
3481
+ if (!this._source) {
3482
+ this._source = {
3483
+ started: false,
3484
+ ticket: 0,
3485
+ controller: null,
3486
+ inFlight: false,
3487
+ offset: 0,
3488
+ pendingOffset: 0,
3489
+ total: null,
3490
+ atLeast: 0,
3491
+ hasMore: false,
3492
+ query: null,
3493
+ loaded: false,
3494
+ pendingAppend: false
3495
+ };
3496
+ }
3497
+ return this._source;
3498
+ }
3499
+ /** The column filters, as a query and `onFilterChange` carry them. */
3500
+ _filterList() {
3501
+ const headers = this.config.headers || [];
3502
+ const out = [];
3503
+ for (const [column, value] of this._state.filters) {
3504
+ if (typeof value === "string" ? !value : this._isEmptyFilterValue(value)) continue;
3505
+ out.push({ column, header: headers[column], value });
3506
+ }
3507
+ return out.sort((a, b) => a.column - b.column);
3508
+ }
3509
+ /** Rows per question: `paging.pageSize` when the source is paged, else
3510
+ * `pageSize` — one number for the query, `goToPage` and the pager. */
3511
+ _sourcePageSize() {
3512
+ const size = this._pagingConfig()?.pageSize ?? this.config.pageSize;
3513
+ return Math.max(1, size | 0);
3514
+ }
3515
+ _buildQuery(offset, reason, limit = null) {
3516
+ const col = this._state.sortColumn;
3517
+ return {
3518
+ offset,
3519
+ limit: Number.isFinite(limit) && limit >= 1 ? Math.floor(limit) : this._sourcePageSize(),
3520
+ sort: col == null ? null : {
3521
+ column: col,
3522
+ header: (this.config.headers || [])[col],
3523
+ ascending: this._state.sortAscending
3524
+ },
3525
+ filters: this._filterList(),
3526
+ reason
3527
+ };
3528
+ }
3529
+ /**
3530
+ * Ask the source for the page at `offset`, and draw the answer if it is
3531
+ * still the newest by then. Resolves with the answer drawn, or undefined
3532
+ * when it was dropped (superseded, failed, or the table was disposed).
3533
+ */
3534
+ async _sourceLoad({ offset = 0, reason = "reload", keep = false, append = false, limit = null } = {}) {
3535
+ const fetch = this.config.dataSource;
3536
+ if (typeof fetch !== "function") return void 0;
3537
+ const src = this._sourceState();
3538
+ src.started = true;
3539
+ const ticket = ++src.ticket;
3540
+ try {
3541
+ src.controller?.abort();
3542
+ } catch (_) {
3543
+ }
3544
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
3545
+ src.controller = controller;
3546
+ const query = this._buildQuery(Math.max(0, offset | 0), reason, limit);
3547
+ src.query = query;
3548
+ src.inFlight = true;
3549
+ src.pendingOffset = query.offset;
3550
+ src.pendingAppend = append;
3551
+ if (!append) this.setLoading(true);
3552
+ let result;
3553
+ try {
3554
+ result = await fetch(query, { signal: controller?.signal });
3555
+ } catch (err) {
3556
+ if (ticket !== src.ticket) return void 0;
3557
+ src.inFlight = false;
3558
+ src.controller = null;
3559
+ src.pendingAppend = false;
3560
+ if (err?.name === "AbortError" || controller?.signal?.aborted) {
3561
+ this.setLoading(false);
3562
+ return void 0;
3563
+ }
3564
+ this.setError(err);
3565
+ try {
3566
+ this.config.onLoadError?.(err, query);
3567
+ } catch (e) {
3568
+ console.error("[DataTable] onLoadError threw", e);
3569
+ }
3570
+ return void 0;
3571
+ }
3572
+ if (ticket !== src.ticket) return void 0;
3573
+ src.inFlight = false;
3574
+ src.controller = null;
3575
+ src.pendingAppend = false;
3576
+ this._applySourceResult(result, query, { keep, append });
3577
+ try {
3578
+ this.config.onLoad?.(result, query);
3579
+ } catch (e) {
3580
+ console.error("[DataTable] onLoad threw", e);
3581
+ }
3582
+ return result;
3583
+ }
3584
+ _applySourceResult(result, query, { keep = false, append = false } = {}) {
3585
+ const src = this._sourceState();
3586
+ const r = Array.isArray(result) ? { rows: result } : result || {};
3587
+ const answered = Array.isArray(r.rows) ? r.rows : [];
3588
+ const totalRaw = r.total ?? r.totalCount;
3589
+ const total = typeof totalRaw === "number" && Number.isFinite(totalRaw) ? totalRaw : null;
3590
+ const hasMore = typeof r.hasMore === "boolean" ? r.hasMore : total !== null ? query.offset + answered.length < total : answered.length >= query.limit;
3591
+ let rows = append ? (this.config.rows || []).concat(answered) : answered;
3592
+ let rowsOffset = append ? src.offset || 0 : query.offset;
3593
+ const pc = append ? this._pagingConfig() : null;
3594
+ if (pc && resolvePaging({
3595
+ mode: pc.mode,
3596
+ total,
3597
+ atLeast: r.atLeast,
3598
+ held: rowsOffset + rows.length,
3599
+ hasMore,
3600
+ softLimit: pc.softLimit,
3601
+ hardLimit: pc.hardLimit
3602
+ }).mode === "pages") {
3603
+ rows = answered;
3604
+ rowsOffset = query.offset;
3605
+ append = false;
3606
+ }
3607
+ if (src.loaded && this._detailOn() && this._expanded.size && typeof this.config.getRowKey !== "function") {
3608
+ const before = this.config.rows || [];
3609
+ const same = (a, b) => {
3610
+ if (a === b) return true;
3611
+ try {
3612
+ return JSON.stringify(a) === JSON.stringify(b);
3613
+ } catch (_) {
3614
+ return false;
3615
+ }
3616
+ };
3617
+ for (const k of [...this._expanded]) {
3618
+ const i = Number(k);
3619
+ if (!Number.isInteger(i) || i < 0 || i >= rows.length || !same(before[i], rows[i])) {
3620
+ this._expanded.delete(k);
3621
+ this._detailHeights.delete(k);
3622
+ }
3623
+ }
3624
+ }
3625
+ src.offset = rowsOffset;
3626
+ src.total = total;
3627
+ src.atLeast = Number.isFinite(r.atLeast) ? r.atLeast : 0;
3628
+ src.hasMore = hasMore;
3629
+ src.loaded = true;
3630
+ if (r.filterCounts && typeof r.filterCounts === "object") this.setFilterCounts(r.filterCounts);
3631
+ const snap = keep ? this._snapshotView() : null;
3632
+ const kept = append ? this._tableWrapEl?.scrollTop || 0 : null;
3633
+ if (snap && snap.keys && snap.keys.size && typeof this.config.getRowKey === "function") {
3634
+ const fresh = /* @__PURE__ */ new Set();
3635
+ rows.forEach((row, i) => {
3636
+ const k = this._rowKey(row, i);
3637
+ if (k !== null && !snap.keys.has(k)) fresh.add(k);
3638
+ });
3639
+ this._newKeys = fresh.size ? fresh : null;
3640
+ } else {
3641
+ this._newKeys = null;
3642
+ }
3643
+ const chosen = append && this._state.selected.size ? { sel: [...this._state.selected], anchor: this._state.anchorIndex } : null;
3644
+ this.setData({ rows });
3645
+ this._newKeys = null;
3646
+ if (chosen) {
3647
+ for (const i of chosen.sel) this._state.selected.add(i);
3648
+ this._state.anchorIndex = chosen.anchor;
3649
+ this._updateRowSelection();
3650
+ }
3651
+ if (snap) this._restoreView(snap);
3652
+ else if (kept !== null) this._scrollBodyTo(kept);
3653
+ else this._scrollBodyTo(0);
3654
+ }
3655
+ /** What a refresh must put back: the keys on screen, the selection by
3656
+ * key, and the first row a reader can see with where it stood. */
3657
+ _snapshotView() {
3658
+ const rows = this.config.rows || [];
3659
+ const keyed = typeof this.config.getRowKey === "function";
3660
+ const keys = keyed ? new Set(rows.map((row, i) => this._rowKey(row, i))) : null;
3661
+ const selectedKeys = keyed ? this.getSelection().map((i) => this._rowKey(rows[i], i)).filter((k) => k !== null) : null;
3662
+ const wrap = this._tableWrapEl;
3663
+ let anchor = null;
3664
+ const scrollTop = wrap?.scrollTop || 0;
3665
+ if (wrap && scrollTop > 0 && this._tbodyEl) {
3666
+ const zoom = this._tableEl?.currentCSSZoom || 1;
3667
+ const top = wrap.getBoundingClientRect().top;
3668
+ for (const tr of this._tbodyEl.children) {
3669
+ if (tr.__rowIndex === void 0) continue;
3670
+ const box = tr.getBoundingClientRect();
3671
+ if (box.bottom > top + 0.5) {
3672
+ anchor = { key: this._rowKey(tr.__row, tr.__rowIndex), delta: (box.top - top) / zoom };
3673
+ break;
3674
+ }
3675
+ }
3676
+ }
3677
+ return { keys, selectedKeys, anchor, scrollTop };
3678
+ }
3679
+ _restoreView(snap) {
3680
+ if (snap.selectedKeys && snap.selectedKeys.length) {
3681
+ const want = new Set(snap.selectedKeys);
3682
+ const sel = this._state.selected;
3683
+ (this.config.rows || []).forEach((row, i) => {
3684
+ if (want.has(this._rowKey(row, i))) sel.add(i);
3685
+ });
3686
+ if (sel.size) {
3687
+ this._state.anchorIndex = [...sel][0];
3688
+ this._updateRowSelection();
3689
+ this._notifySelectionChange();
3690
+ }
3691
+ }
3692
+ const wrap = this._tableWrapEl;
3693
+ if (!wrap || !snap.anchor || snap.anchor.key === null) return;
3694
+ const rows = this.config.rows || [];
3695
+ const idx = rows.findIndex((row, i) => this._rowKey(row, i) === snap.anchor.key);
3696
+ if (idx < 0) return;
3697
+ const zoom = this._tableEl?.currentCSSZoom || 1;
3698
+ if (this._virt) {
3699
+ const local = this._virtLocalOf(idx);
3700
+ if (local < 0) return;
3701
+ this._scrollBodyTo(Math.max(0, this._virt.offsets[local] - snap.anchor.delta));
3702
+ return;
3703
+ }
3704
+ const tr = this.getRowElement(idx);
3705
+ if (!tr) return;
3706
+ const now = (tr.getBoundingClientRect().top - wrap.getBoundingClientRect().top) / zoom;
3707
+ const shift = now - snap.anchor.delta;
3708
+ if (Math.abs(shift) >= 0.5) this._scrollBodyTo(wrap.scrollTop + shift);
3709
+ }
3710
+ _scrollBodyTo(top) {
3711
+ const wrap = this._tableWrapEl;
3712
+ if (!wrap) return;
3713
+ if (Math.abs((wrap.scrollTop || 0) - top) >= 0.5) wrap.scrollTop = top;
3714
+ if (this._virt) this._virtUpdate({ force: true, top });
3715
+ }
3716
+ /** Go to the page at `offset` (paging back stops following). */
3717
+ _sourceGoTo(offset) {
3718
+ const next = Math.max(0, offset | 0);
3719
+ if (this._following && next > 0) this.setFollow(false);
3720
+ return this._sourceLoad({ offset: next, reason: "page" });
3721
+ }
3722
+ /**
3723
+ * `paging` over a `dataSource`, scrolling: the rows after those held, under
3724
+ * them — never past the hard limit. Scrolling down to ask for them is
3725
+ * paging away, so it turns following off as a page turn does; rows asked
3726
+ * for because the ones held do not fill the box are not (the reader is
3727
+ * still at the top).
3728
+ */
3729
+ _sourceLoadMore() {
3730
+ const held = this.config.rows?.length || 0;
3731
+ const hard = this.pagingState?.hardLimit ?? PAGING_HARD_LIMIT;
3732
+ const limit = Math.min(this._sourcePageSize(), hard - held);
3733
+ if (limit < 1) return Promise.resolve(void 0);
3734
+ if (this._following && (this._tableWrapEl?.scrollTop || 0) > 0) this.setFollow(false);
3735
+ const offset = (this._source?.offset || 0) + held;
3736
+ return this._sourceLoad({ offset, limit, reason: "more", append: true });
3737
+ }
3738
+ /** The strip's *Load all rows*: ONE question for every row left under the
3739
+ * hard limit (`reason: 'all'`); the source answers as many as it will, and
3740
+ * says with `hasMore` whether there are still more. */
3741
+ _sourceLoadAll() {
3742
+ const held = this.config.rows?.length || 0;
3743
+ const hard = this.pagingState?.hardLimit ?? PAGING_HARD_LIMIT;
3744
+ if (hard - held < 1) return Promise.resolve(void 0);
3745
+ if (this._following) this.setFollow(false);
3746
+ const offset = (this._source?.offset || 0) + held;
3747
+ return this._sourceLoad({ offset, limit: hard - held, reason: "all", append: true });
3748
+ }
3749
+ /** The strip's Pages / Scroll switch: the table switches, tells the
3750
+ * consumer (`paging.onModeChange`), and asks again from the start. */
3751
+ _sourceSetMode(mode) {
3752
+ const next = mode === "pages" ? "pages" : "scroll";
3753
+ const p = this.config.paging;
3754
+ this.config.paging = { ...p, mode: next };
3755
+ try {
3756
+ p.onModeChange?.(next);
3757
+ } catch (err) {
3758
+ console.error("[DataTable] onModeChange threw", err);
3759
+ }
3760
+ return this._sourceLoad({ offset: 0, reason: "mode" });
3761
+ }
3762
+ /**
3763
+ * Ask the source again from its FIRST page — for a consumer whose own
3764
+ * filters (outside the table) changed. Under `dataSource` only; resolves
3765
+ * with the answer drawn (undefined when it was superseded or failed).
3766
+ */
3767
+ reload() {
3768
+ if (!this.config.dataSource) return Promise.resolve(void 0);
3769
+ return this._sourceLoad({ offset: 0, reason: "reload" });
3770
+ }
3771
+ /**
3772
+ * Read the CURRENT page again, in place: the scroll position (a reader at
3773
+ * the top sees what arrived; one scrolled down keeps the rows they were
3774
+ * reading where they were), the selection by key, the open row and the
3775
+ * open details all stay; rows the last answer did not hold are drawn
3776
+ * `twm-dt-row--new` (needs `getRowKey`). What `follow` calls on its tick.
3777
+ * Without `dataSource`, it only draws again.
3778
+ */
3779
+ refresh() {
3780
+ if (!this.config.dataSource) {
3781
+ if (this._wrapperEl) this.render();
3782
+ return Promise.resolve(void 0);
3783
+ }
3784
+ const src = this._sourceState();
3785
+ if (this.pagingState?.mode === "scroll") {
3786
+ const held = this.config.rows?.length || 0;
3787
+ return this._sourceLoad({
3788
+ offset: src.offset || 0,
3789
+ limit: Math.max(this._sourcePageSize(), held),
3790
+ reason: "refresh",
3791
+ keep: true
3792
+ });
3793
+ }
3794
+ const offset = src.inFlight && !src.pendingAppend ? src.pendingOffset : src.offset;
3795
+ return this._sourceLoad({ offset, reason: "refresh", keep: true });
3796
+ }
3797
+ /** The last query sent to `dataSource`, or null. */
3798
+ getQuery() {
3799
+ return this._source?.query || null;
3800
+ }
3801
+ /** What the last answer said about the whole: `{offset, total, hasMore}`. */
3802
+ getPageInfo() {
3803
+ const s = this._source;
3804
+ return s ? { offset: s.offset, total: s.total, hasMore: s.hasMore } : null;
3805
+ }
3806
+ /**
3807
+ * Turn following on or off (`follow`). On, the first page is read again
3808
+ * every `follow.interval` ms while the table is on it and in the page.
3809
+ */
3810
+ setFollow(on) {
3811
+ const next = !!on;
3812
+ if (next === this._following) return;
3813
+ this._following = next;
3814
+ if (next) this._armFollow();
3815
+ else this._disarmFollow();
3816
+ try {
3817
+ this.config.onFollowChange?.(next);
3818
+ } catch (err) {
3819
+ console.error("[DataTable] onFollowChange threw", err);
3820
+ }
3821
+ }
3822
+ isFollowing() {
3823
+ return this._following;
3824
+ }
3825
+ _followInterval() {
3826
+ const f = this.config.follow;
3827
+ const ms = f && typeof f === "object" ? Number(f.interval) : NaN;
3828
+ return Number.isFinite(ms) && ms > 0 ? ms : 1e4;
3829
+ }
3830
+ /** The tick holds the table WEAKLY: a follow timer must never be what keeps
3831
+ * a table nobody holds alive (`keepWhileDetached: true`). */
3832
+ _armFollow() {
3833
+ this._disarmFollow();
3834
+ if (!this._following || !this.config.dataSource || !this._wrapperEl) return;
3835
+ const ref = weakRef(this);
3836
+ const id = setInterval(() => {
3837
+ const t = ref.deref();
3838
+ if (!t || t._followTimer !== id) {
3839
+ clearInterval(id);
3840
+ return;
3841
+ }
3842
+ const src = t._source;
3843
+ if (!t._following || !src || src.inFlight) return;
3844
+ if ((src.offset || 0) !== 0) return;
3845
+ if (!t._wrapperEl?.isConnected) return;
3846
+ if (t._readerIsBusy()) return;
3847
+ t.refresh();
3848
+ }, this._followInterval());
3849
+ this._followTimer = id;
3850
+ }
3851
+ _disarmFollow() {
3852
+ if (this._followTimer) clearInterval(this._followTimer);
3853
+ this._followTimer = null;
3854
+ }
3855
+ /** A TICK WAITS FOR A READER. A refresh redraws every row and every open
3856
+ * detail, so a tick that came while somebody was selecting text in the
3857
+ * table (a stack trace to copy) or working inside an open detail took the
3858
+ * text out from under the selection and the focus out of the detail —
3859
+ * every interval, for as long as they stayed. The tick is skipped
3860
+ * instead, and following resumes on the next one after (the release
3861
+ * review). `refresh()` called by the consumer is not a tick and does not
3862
+ * wait. */
3863
+ _readerIsBusy() {
3864
+ const wrap = this._tableWrapEl;
3865
+ const doc = wrap?.ownerDocument;
3866
+ if (!wrap || !doc) return false;
3867
+ const active = doc.activeElement;
3868
+ if (active && active !== wrap && wrap.contains(active) && active.closest?.(".twm-dt-detail")) return true;
3869
+ try {
3870
+ const sel = doc.defaultView?.getSelection?.();
3871
+ if (sel && !sel.isCollapsed && sel.rangeCount > 0) {
3872
+ for (let i = 0; i < sel.rangeCount; i++) {
3873
+ const r = sel.getRangeAt(i);
3874
+ if (wrap.contains(r.startContainer) || wrap.contains(r.endContainer)) return true;
3875
+ }
3876
+ }
3877
+ } catch (_) {
3878
+ }
3879
+ return false;
3880
+ }
3881
+ // ── the pager (#35) ────────────────────────────────────────────────
3882
+ _usesPager070() {
3883
+ const c = this.config;
3884
+ return !!c.dataSource || !!c.pagerLabels || c.maxJumpOffset != null || typeof c.onPageChange === "function" && c.totalCount == null && typeof c.hasMore === "boolean";
3885
+ }
3886
+ _pagerLabel(name) {
3887
+ const labels = this.config.pagerLabels || {};
3888
+ return labels[name];
3889
+ }
3890
+ _pagerRefusalTooDeep() {
3891
+ const max = this.config.maxJumpOffset;
3892
+ const fn = this._pagerLabel("tooDeep");
3893
+ if (typeof fn === "function") return String(fn(max));
3894
+ if (typeof fn === "string") return fn;
3895
+ return `Past ${Number(max).toLocaleString()} rows. Narrow the filters first.`;
3896
+ }
3897
+ _pagerButton(icon, name, tip, disabled, onClick, refusal = null) {
3898
+ const label = this._pagerLabel(name);
3899
+ const btn = this._createPaginationBtn(
3900
+ icon,
3901
+ typeof label === "string" ? label : tip,
3902
+ disabled || !!refusal,
3903
+ onClick
3904
+ );
3905
+ btn.dataset.pager = name;
3906
+ if (typeof label === "string" && label) {
3907
+ const span = document.createElement("span");
3908
+ span.className = "twm-pagination-btn__label";
3909
+ span.textContent = label;
3910
+ btn.appendChild(span);
3911
+ btn.classList.add("twm-pagination-btn--labelled");
3912
+ btn.style.padding = "1px 6px 1px 3px";
3913
+ btn.style.setProperty("min-width", "max-content", "important");
3914
+ btn.style.flexShrink = "0";
3915
+ }
3916
+ if (refusal) {
3917
+ btn.dataset.refusal = refusal;
3918
+ btn.setAttribute("data-tooltip", refusal);
3919
+ btn.title = refusal;
3920
+ }
3921
+ return btn;
3922
+ }
3923
+ /**
3924
+ * THE PAGE ARITHMETIC, ONCE — read by the pager above the rows (0.7.0) and
3925
+ * by the `paging` strip under them (0.6.2), which each worked it out for
3926
+ * themselves. `held` is the rows drawn on this page; `total` a number or
3927
+ * null (unknown, when `hasMore` says whether there is a next page).
3928
+ *
3929
+ * @returns {{known: boolean, hasPrev: boolean, hasNext: boolean, page: number,
3930
+ * pages: number|null, lastOffset: number|null, lastRefusal: string|null}}
3931
+ */
3932
+ _pageNav({ offset = 0, held = 0, total = null, hasMore = false, pageSize }) {
3933
+ const size = Math.max(1, pageSize | 0);
3934
+ const known = typeof total === "number" && Number.isFinite(total);
3935
+ const lastOffset = known ? Math.floor(Math.max(0, total - 1) / size) * size : null;
3936
+ const maxJump = Number.isFinite(this.config.maxJumpOffset) ? this.config.maxJumpOffset : null;
3937
+ let lastRefusal = null;
3938
+ if (!known) {
3939
+ const words = this._pagerLabel("noLast");
3940
+ lastRefusal = typeof words === "string" ? words : "The total is not known, so there is no last page.";
3941
+ } else if (maxJump !== null && lastOffset > maxJump) {
3942
+ lastRefusal = this._pagerRefusalTooDeep();
3943
+ }
3944
+ return {
3945
+ known,
3946
+ hasPrev: offset > 0,
3947
+ hasNext: known ? offset + held < total : !!hasMore,
3948
+ page: Math.floor(offset / size) + 1,
3949
+ pages: known ? Math.max(1, Math.ceil(total / size)) : null,
3950
+ lastOffset,
3951
+ lastRefusal
3952
+ };
3953
+ }
3954
+ /**
3955
+ * The pager for any mode, with the two things the 0.6 strip could not do:
3956
+ * an UNKNOWN total (no "of N", no last page, Next until a short page) and
3957
+ * a REFUSED deep jump (`maxJumpOffset`). For a known total its words are
3958
+ * the 0.6 strip's.
3959
+ */
3960
+ _createPager070() {
3961
+ const c = this.config;
3962
+ const { rows, pageSize } = c;
3963
+ const source = !!c.dataSource;
3964
+ const server = !source && typeof c.onPageChange === "function";
3965
+ const src = this._source || { offset: 0, total: null, hasMore: false };
3966
+ const offset = source ? src.offset : server ? c.offset || 0 : this._state.offset;
3967
+ let total;
3968
+ let hasMoreKnown;
3969
+ if (source) {
3970
+ total = src.total;
3971
+ hasMoreKnown = src.hasMore;
3972
+ } else if (server) {
3973
+ const unknown = c.totalCount == null && typeof c.hasMore === "boolean";
3974
+ total = unknown ? null : c.totalCount || rows.length;
3975
+ hasMoreKnown = unknown ? c.hasMore : null;
3976
+ } else {
3977
+ total = this._getProcessedRows().length;
3978
+ hasMoreKnown = null;
3979
+ }
3980
+ const known = typeof total === "number" && Number.isFinite(total);
3981
+ const isFiltered = !source && !server && this._state.filters.size > 0;
3982
+ const unfilteredTotal = !source && !server ? rows.length : total;
3983
+ const drawn = source || server ? rows.length : Math.max(0, Math.min(pageSize, total - offset));
3984
+ const pn = this._pageNav({ offset, held: drawn, total, hasMore: hasMoreKnown, pageSize });
3985
+ const { hasPrev, hasNext, page } = pn;
3986
+ const totalPages = pn.pages;
3987
+ if (known && totalPages <= 1 && !isFiltered && !hasPrev) return null;
3988
+ if (!known && !hasPrev && !hasNext) return null;
3989
+ const maxJump = Number.isFinite(c.maxJumpOffset) ? c.maxJumpOffset : null;
3990
+ const go = (newOffset) => {
3991
+ if (source) {
3992
+ this._sourceGoTo(newOffset);
3993
+ return;
3994
+ }
3995
+ if (server) {
3996
+ this._awaitWithSpinner(c.onPageChange(newOffset, pageSize));
3997
+ return;
3998
+ }
3999
+ this._state.offset = newOffset;
4000
+ this.render();
4001
+ };
4002
+ const el = document.createElement("div");
4003
+ el.className = "twm-pagination-controls";
4004
+ if (!known) el.classList.add("twm-pagination-controls--unknown-total");
4005
+ el.style.cssText = "display:flex; align-items:center; gap:6px; padding:2px 8px; border-bottom:1px solid #2a2a2a; font-size:11px;";
4006
+ const info = document.createElement("span");
4007
+ info.className = "twm-pagination-info";
4008
+ info.style.cssText = "color:#aaa; white-space:nowrap;";
4009
+ const from = (offset + 1).toLocaleString();
4010
+ const to = (offset + drawn).toLocaleString();
4011
+ if (drawn === 0) {
4012
+ info.textContent = hasPrev ? `Page ${page} \xB7 no rows` : isFiltered ? `0 of ${unfilteredTotal.toLocaleString()} rows match` : "No rows";
4013
+ } else if (!known) {
4014
+ info.textContent = `Rows ${from}\u2013${to}`;
4015
+ } else if (isFiltered) {
4016
+ info.textContent = `Rows ${from}\u2013${to} of ${total.toLocaleString()} (${unfilteredTotal.toLocaleString()} total)`;
4017
+ } else {
4018
+ info.textContent = `Rows ${from}\u2013${to} of ${total.toLocaleString()}`;
4019
+ }
4020
+ const spacer = document.createElement("div");
4021
+ spacer.style.flex = "1";
4022
+ const firstBtn = this._pagerButton("first_page", "first", "First page", !hasPrev, () => go(0));
4023
+ const prevBtn = this._pagerButton(
4024
+ "chevron_left",
4025
+ "prev",
4026
+ "Previous page",
4027
+ !hasPrev,
4028
+ () => go(Math.max(0, offset - pageSize))
4029
+ );
4030
+ const pageInfo = document.createElement("span");
4031
+ pageInfo.className = "twm-pagination-page";
4032
+ pageInfo.style.cssText = "color:#aaa; font-size:10px; min-width:72px; text-align:center;";
4033
+ pageInfo.textContent = known ? `Page ${page} of ${totalPages.toLocaleString()}` : `Page ${page}`;
4034
+ const nextBtn = this._pagerButton(
4035
+ "chevron_right",
4036
+ "next",
4037
+ "Next page",
4038
+ !hasNext,
4039
+ () => go(offset + pageSize)
4040
+ );
4041
+ const { lastOffset, lastRefusal } = pn;
4042
+ const lastBtn = this._pagerButton(
4043
+ "last_page",
4044
+ "last",
4045
+ "Last page",
4046
+ !hasNext || lastOffset === offset,
4047
+ () => {
4048
+ if (lastOffset !== null) go(lastOffset);
4049
+ },
4050
+ lastRefusal
4051
+ );
4052
+ const jumpInput = document.createElement("input");
4053
+ jumpInput.type = "number";
4054
+ jumpInput.className = "twm-pagination-input";
4055
+ jumpInput.style.cssText = "width:54px; padding:1px 4px; border:1px solid #444; background:#2a2a2a; color:#ccc; font-size:10px;";
4056
+ jumpInput.min = "1";
4057
+ if (known) jumpInput.max = String(total);
4058
+ jumpInput.placeholder = "Row #";
4059
+ const jumpBtn = this._createPaginationBtn(null, "Go to row", false, () => {
4060
+ const target = Math.floor(Number(jumpInput.value));
4061
+ if (!Number.isFinite(target) || target < 1) return;
4062
+ const row = known ? Math.min(target, Math.max(1, total)) : target;
4063
+ const newOffset = Math.floor((row - 1) / pageSize) * pageSize;
4064
+ if (maxJump !== null && newOffset > maxJump) {
4065
+ const why = this._pagerRefusalTooDeep();
4066
+ jumpInput.setAttribute("aria-invalid", "true");
4067
+ jumpInput.title = why;
4068
+ jumpInput.dataset.refusal = why;
4069
+ return;
4070
+ }
4071
+ jumpInput.removeAttribute("aria-invalid");
4072
+ delete jumpInput.dataset.refusal;
4073
+ if (server && typeof c.onJumpToRow === "function") {
4074
+ c.onJumpToRow(row - 1);
4075
+ return;
4076
+ }
4077
+ go(newOffset);
4078
+ }, "Go");
4079
+ jumpBtn.dataset.pager = "go";
4080
+ el.append(info, spacer, firstBtn, prevBtn, pageInfo, nextBtn, lastBtn, jumpInput, jumpBtn);
4081
+ if (c.showExportButton) {
4082
+ el.appendChild(this._createPaginationBtn(
4083
+ "download",
4084
+ "Download visible rows as CSV",
4085
+ false,
4086
+ () => this.downloadCSV("data.csv")
4087
+ ));
4088
+ }
4089
+ return el;
4090
+ }
4091
+ // ─────────────────────────────────────────────────────────────────
4092
+ // 0.7.0 — filter controls that are not text boxes (#37)
4093
+ // ─────────────────────────────────────────────────────────────────
4094
+ /** The spec for a column, normalised: null for the 0.4 text box. */
4095
+ _filterKindFor(colIdx, header) {
4096
+ const fk = this.config.filterKinds;
4097
+ if (fk == null) return null;
4098
+ let spec;
4099
+ if (typeof fk === "function") {
4100
+ try {
4101
+ spec = fk(colIdx, header);
4102
+ } catch (err) {
4103
+ console.error("[DataTable] filterKinds threw", err);
4104
+ spec = null;
4105
+ }
4106
+ } else {
4107
+ spec = perColumn(fk, colIdx);
4108
+ }
4109
+ if (spec == null || spec === "text" || spec === false) return null;
4110
+ if (typeof spec === "function") return { kind: "custom", create: spec };
4111
+ if (typeof spec === "object") {
4112
+ if (spec.kind === "text") return null;
4113
+ if (spec.kind === "chips") return spec;
4114
+ if (typeof spec.create === "function") return { ...spec, kind: spec.kind || "custom" };
4115
+ }
4116
+ if (spec === "chips") return null;
4117
+ return null;
4118
+ }
4119
+ /** The column's control — built once, then the same element every render. */
4120
+ _filterControlFor(colIdx, header) {
4121
+ const spec = this._filterKindFor(colIdx, header);
4122
+ if (!spec) return null;
4123
+ const have = this._filterControls.get(colIdx);
4124
+ if (have && have.kind === spec.kind && have.specKey === (spec.options || spec.create)) {
4125
+ return have.control;
4126
+ }
4127
+ if (have) this._destroyOneControl(have.control);
4128
+ const value = this._state.filters.get(colIdx) ?? null;
4129
+ const ctx = {
4130
+ column: colIdx,
4131
+ header,
4132
+ value,
4133
+ counts: this._filterCounts.get(colIdx) ?? null,
4134
+ table: this,
4135
+ setValue: (v) => this.setFilter(colIdx, v)
4136
+ };
4137
+ let control = null;
4138
+ try {
4139
+ control = spec.kind === "chips" ? createChipsFilter(ctx, spec) : spec.create(ctx);
4140
+ } catch (err) {
4141
+ console.error("[DataTable] filter control threw while being built", err);
4142
+ control = null;
4143
+ }
4144
+ if (typeof Node !== "undefined" && control instanceof Node) control = { el: control };
4145
+ if (!control || !control.el) return null;
4146
+ control.el.classList?.add("twm-dt-filter-control");
4147
+ this._filterControls.set(colIdx, {
4148
+ kind: spec.kind,
4149
+ specKey: spec.options || spec.create,
4150
+ control,
4151
+ match: typeof control.match === "function" ? control.match : spec.kind === "chips" ? chipsMatch(spec) : null
4152
+ });
4153
+ return control;
4154
+ }
4155
+ /** The client-side rule for a column with a control, or null. */
4156
+ _filterMatcher(colIdx) {
4157
+ if (!this._filterControls.size && this.config.filterKinds == null) return null;
4158
+ const entry = this._filterControls.get(colIdx);
4159
+ if (entry) return entry.match || defaultControlMatch;
4160
+ const spec = this._filterKindFor(colIdx, (this.config.headers || [])[colIdx]);
4161
+ if (!spec) return null;
4162
+ return spec.kind === "chips" ? chipsMatch(spec) : spec.match || defaultControlMatch;
4163
+ }
4164
+ _isEmptyFilterValue(v) {
4165
+ if (v == null || v === "" || v === false) return true;
4166
+ if (Array.isArray(v)) return v.length === 0;
4167
+ if (typeof v === "object" && !(v instanceof Date)) {
4168
+ return Object.values(v).every((x) => x == null || x === "");
4169
+ }
4170
+ return false;
4171
+ }
4172
+ _destroyOneControl(control) {
4173
+ try {
4174
+ control?.destroy?.();
4175
+ } catch (err) {
4176
+ console.error("[DataTable] filter control destroy threw", err);
4177
+ }
4178
+ control?.el?.remove?.();
4179
+ }
4180
+ _destroyFilterControls() {
4181
+ for (const { control } of this._filterControls.values()) this._destroyOneControl(control);
4182
+ this._filterControls.clear();
4183
+ }
4184
+ _notifyFilterChange() {
4185
+ if (typeof this.config.onFilterChange !== "function") return;
4186
+ try {
4187
+ this.config.onFilterChange(this._filterList());
4188
+ } catch (err) {
4189
+ console.error("[DataTable] onFilterChange threw", err);
4190
+ }
4191
+ }
4192
+ /** The column filters in force: `[{column, header, value}]`. */
4193
+ getFilters() {
4194
+ return this._filterList();
4195
+ }
4196
+ /**
4197
+ * Set one column's filter (`null`, `''` or an empty array clears it). The
4198
+ * value is whatever the column's control works in — text for the 0.4 box.
4199
+ */
4200
+ setFilter(colIdx, value) {
4201
+ const textBox = !this._filterKindFor(colIdx, (this.config.headers || [])[colIdx]);
4202
+ const v = textBox && value != null && typeof value !== "string" ? String(value) : value;
4203
+ if (textBox) {
4204
+ const filterRow = this._wrapperEl?.querySelector(".twm-data-table__filter-row");
4205
+ const cell = filterRow?.children[this.config.showRowNumbers ? colIdx + 1 : colIdx];
4206
+ const input = cell?.querySelector(".twm-data-table__filter-input");
4207
+ if (input) input.value = v || "";
4208
+ }
4209
+ this._onFilterInput(colIdx, v ?? "");
4210
+ }
4211
+ /**
4212
+ * Counts for a column's filter control — chips draw them beside each
4213
+ * option (keyed by the option's value; `''` is "no filter"), a dimmed
4214
+ * chip at zero. `setFilterCounts(colIdx, counts)`, or
4215
+ * `setFilterCounts({[colIdx]: counts})` for several. They are kept, so a
4216
+ * control built later starts with them.
4217
+ */
4218
+ setFilterCounts(colIdx, counts) {
4219
+ if (colIdx && typeof colIdx === "object" && counts === void 0) {
4220
+ for (const [k, c] of Object.entries(colIdx)) this.setFilterCounts(Number(k), c);
4221
+ return;
4222
+ }
4223
+ const col = Number(colIdx);
4224
+ if (!Number.isInteger(col)) return;
4225
+ if (counts == null) this._filterCounts.delete(col);
4226
+ else this._filterCounts.set(col, counts);
4227
+ const entry = this._filterControls.get(col);
4228
+ if (entry) {
4229
+ try {
4230
+ entry.control.setCounts?.(counts ?? null);
4231
+ } catch (err) {
4232
+ console.error("[DataTable] filter control setCounts threw", err);
4233
+ }
4234
+ }
4235
+ }
4236
+ /** What `setFilterCounts` last gave a column, or null. */
4237
+ getFilterCounts(colIdx) {
4238
+ return this._filterCounts.get(Number(colIdx)) ?? null;
4239
+ }
4240
+ // ── focus across a render ──────────────────────────────────────────
4241
+ _captureFocus070(doc) {
4242
+ const active = doc?.activeElement;
4243
+ if (!active || active === doc.body) return null;
4244
+ for (const [colIdx, { control }] of this._filterControls) {
4245
+ if (control.el?.contains?.(active)) {
4246
+ let sel = null;
4247
+ try {
4248
+ if (typeof active.selectionStart === "number") sel = [active.selectionStart, active.selectionEnd];
4249
+ } catch (_) {
4250
+ }
4251
+ return { kind: "control", colIdx, el: active, sel };
4252
+ }
4253
+ }
4254
+ const detailHost = active.closest?.(".twm-dt-detail");
4255
+ if (detailHost && this._tbodyEl?.contains(detailHost)) {
4256
+ const tr = detailHost.closest("tr")?.__detailOf;
4257
+ if (tr && tr.__rowIndex !== void 0) {
4258
+ const path = [];
4259
+ for (let n = active; n && n !== detailHost; n = n.parentElement) {
4260
+ path.unshift(Array.prototype.indexOf.call(n.parentElement.children, n));
4261
+ }
4262
+ let sel = null;
4263
+ try {
4264
+ if (typeof active.selectionStart === "number") sel = [active.selectionStart, active.selectionEnd];
4265
+ } catch (_) {
4266
+ }
4267
+ return { kind: "detail", key: this._rowKey(tr.__row, tr.__rowIndex), path, tag: active.tagName, sel };
4268
+ }
4269
+ }
4270
+ const toggle = active.closest?.(".twm-dt-detail-toggle");
4271
+ if (toggle && this._tbodyEl?.contains(toggle)) {
4272
+ const tr = toggle.closest("tr");
4273
+ if (tr && tr.__rowIndex !== void 0) {
4274
+ return { kind: "toggle", key: this._rowKey(tr.__row, tr.__rowIndex) };
4275
+ }
4276
+ }
4277
+ return null;
4278
+ }
4279
+ _restoreFocus070(f) {
4280
+ try {
4281
+ if (f.kind === "control") {
4282
+ if (!f.el.isConnected) return;
4283
+ f.el.focus({ preventScroll: true });
4284
+ if (f.sel) f.el.setSelectionRange?.(f.sel[0], f.sel[1]);
4285
+ } else if (f.kind === "detail" && f.key !== null) {
4286
+ let n = this._rowElByKey(f.key)?.__detail?.[0]?.querySelector(".twm-dt-detail");
4287
+ for (const i of f.path) n = n?.children[i];
4288
+ if (n && n.tagName === f.tag) {
4289
+ n.focus({ preventScroll: true });
4290
+ if (f.sel) n.setSelectionRange?.(f.sel[0], f.sel[1]);
4291
+ }
4292
+ } else if (f.kind === "toggle" && f.key !== null) {
4293
+ const tr = this._rowElByKey(f.key);
4294
+ tr?.querySelector(":scope > td > .twm-dt-detail-toggle")?.focus({ preventScroll: true });
4295
+ }
4296
+ } catch (_) {
4297
+ }
4298
+ }
4299
+ /** The one row spanning every column that says there are no rows (or why
4300
+ * they could not be read). Under `dataSource` the plain message is drawn
4301
+ * by class, so it is quiet while the first answer is on its way. */
4302
+ _buildEmptyRow(colCount, emptyMessage) {
4303
+ const emptyTr = document.createElement("tr");
4304
+ emptyTr.className = "data-preview-row data-table__empty-row";
4305
+ const emptyTd = document.createElement("td");
4306
+ emptyTd.colSpan = colCount;
4307
+ if (this._error) {
4308
+ emptyTd.className = "twm-data-table__empty-cell twm-data-table__empty-cell--error";
4309
+ emptyTd.setAttribute("role", "alert");
4310
+ this._appendStateContent(emptyTd, this._errorContent());
4311
+ } else if (this.config.emptyState !== void 0) {
4312
+ emptyTd.className = "twm-data-table__empty-cell twm-data-table__empty-cell--state";
4313
+ const state = typeof this.config.emptyState === "function" ? this.config.emptyState() : this.config.emptyState;
4314
+ this._appendStateContent(emptyTd, state);
4315
+ } else if (this.config.dataSource) {
4316
+ emptyTd.className = "twm-data-table__empty-cell twm-data-table__empty-cell--state";
4317
+ emptyTd.textContent = emptyMessage;
4318
+ } else {
4319
+ emptyTd.style.cssText = "text-align:center; font-style:italic; color:#888; padding:16px;";
4320
+ emptyTd.textContent = emptyMessage;
4321
+ }
4322
+ emptyTr.appendChild(emptyTd);
4323
+ return emptyTr;
4324
+ }
4325
+ // ─────────────────────────────────────────────────────────────────
2846
4326
  // 0.5.0 — decoration, states and the typed columns
2847
4327
  // ─────────────────────────────────────────────────────────────────
2848
4328
  /** The class list both halves of the table carry: the 0.4 pair, plus the
@@ -3051,6 +4531,9 @@ var DataTable = class {
3051
4531
  tr.__rowKey = this._rowKey(row, rowIdx);
3052
4532
  }
3053
4533
  this._paintActive(tr);
4534
+ if (this._newKeys) {
4535
+ tr.classList.toggle("twm-dt-row--new", this._newKeys.has(this._rowKey(row, rowIdx)));
4536
+ }
3054
4537
  }
3055
4538
  /** The open mark on one row, from `_activeKey`. */
3056
4539
  _paintActive(tr) {
@@ -3206,6 +4689,21 @@ var DataTable = class {
3206
4689
  const head = labelRow && labelRow.children[i] ? labelRow.children[i].getBoundingClientRect().width / zoom : 0;
3207
4690
  natural[i] = Math.max(body, head);
3208
4691
  }
4692
+ const floors = new Array(cols).fill(0);
4693
+ const filterRow = headerTable.querySelector("thead > tr.twm-data-table__filter-row--controls");
4694
+ if (filterRow) {
4695
+ for (let i = 0; i < cols && i < filterRow.children.length; i++) {
4696
+ const th = filterRow.children[i];
4697
+ if (!th.classList.contains("data-table__filter-cell--control")) continue;
4698
+ const el = th.firstElementChild;
4699
+ if (!el) continue;
4700
+ const cs = getComputedStyle(th);
4701
+ const pad2 = (parseFloat(cs.paddingLeft) || 0) + (parseFloat(cs.paddingRight) || 0) + (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0);
4702
+ floors[i] = Math.ceil(el.getBoundingClientRect().width / zoom + pad2);
4703
+ natural[i] = Math.max(natural[i], floors[i]);
4704
+ }
4705
+ }
4706
+ this._filterFloors = floors;
3209
4707
  for (const tr of sampled) tr.remove();
3210
4708
  headerTable.classList.remove("twm-dt-measuring");
3211
4709
  bodyTable.classList.remove("twm-dt-measuring");
@@ -3363,7 +4861,8 @@ var DataTable = class {
3363
4861
  const widths = this._fitColumnWidths(natural, avail, {
3364
4862
  firstIdx,
3365
4863
  overrides: this._colWidths,
3366
- fit: this.config.columnFit
4864
+ fit: this.config.columnFit,
4865
+ floors: this._filterFloors
3367
4866
  });
3368
4867
  const total = widths.reduce((a, b) => a + b, 0);
3369
4868
  headerRows.forEach((tr) => {
@@ -3433,6 +4932,8 @@ var DataTable = class {
3433
4932
  const n = natural.length;
3434
4933
  const firstIdx = opts.firstIdx ?? 0;
3435
4934
  const overrides = opts.overrides || {};
4935
+ const floors = Array.isArray(opts.floors) ? opts.floors : [];
4936
+ const floorOf = (i) => Math.max(FLOOR, Math.ceil(floors[i] || 0));
3436
4937
  const toContent = opts.fit === "content";
3437
4938
  const desired = new Array(n);
3438
4939
  const pinned = new Array(n).fill(false);
@@ -3473,7 +4974,7 @@ var DataTable = class {
3473
4974
  }
3474
4975
  const budget = avail - protectedSum;
3475
4976
  if (budget < shrinkable.length * FLOOR) {
3476
- for (const i of shrinkable) widths[i] = FLOOR;
4977
+ for (const i of shrinkable) widths[i] = floorOf(i);
3477
4978
  return widths;
3478
4979
  }
3479
4980
  shrinkable.sort((a, b) => desired[a] - desired[b]);
@@ -3488,7 +4989,7 @@ var DataTable = class {
3488
4989
  } else {
3489
4990
  const level = Math.max(FLOOR, fair);
3490
4991
  for (let j = k; j < shrinkable.length; j++) {
3491
- widths[shrinkable[j]] = level;
4992
+ widths[shrinkable[j]] = Math.max(level, floorOf(shrinkable[j]));
3492
4993
  }
3493
4994
  const leftover = remaining - colsLeft * level;
3494
4995
  if (leftover > 0) widths[shrinkable[shrinkable.length - 1]] += leftover;
@@ -3736,6 +5237,7 @@ var DataTable = class {
3736
5237
  const cells = rows ? rows.flatMap((tr) => [...tr.children]) : bodyTable.querySelectorAll("tbody td");
3737
5238
  cells.forEach((td) => {
3738
5239
  if (td.parentElement?.classList.contains("twm-dt-spacer")) return;
5240
+ if (td.closest?.(".twm-dt-detail-row")) return;
3739
5241
  const clipped = td.scrollWidth > td.clientWidth + 1;
3740
5242
  if (clipped) {
3741
5243
  if (!td.title) td.title = td.textContent;
@@ -3788,6 +5290,20 @@ var DataTable = class {
3788
5290
  if (typeof this.config.onRowActivate === "function") {
3789
5291
  this._installActivation(tbody, table);
3790
5292
  }
5293
+ if (this._detailOn()) {
5294
+ const onDetailKey = (ev) => {
5295
+ if (ev.key !== "ArrowRight" && ev.key !== "ArrowLeft") return;
5296
+ if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.shiftKey) return;
5297
+ const toggle = ev.target?.closest?.(".twm-dt-detail-toggle");
5298
+ let tr = null;
5299
+ if (toggle && table.contains(toggle)) tr = toggle.closest("tr");
5300
+ else if (ev.target === table) tr = this._currentRowEl();
5301
+ if (!tr || tr.__rowIndex === void 0) return;
5302
+ if (this._setRowElExpanded(tr, ev.key === "ArrowRight", ev)) ev.preventDefault();
5303
+ };
5304
+ table.addEventListener("keydown", onDetailKey);
5305
+ this._disposers.push(() => table.removeEventListener("keydown", onDetailKey));
5306
+ }
3791
5307
  if (copyable) {
3792
5308
  const handleContextMenu = (event) => {
3793
5309
  const rowEl = event.target.closest("tr");
@@ -3795,6 +5311,10 @@ var DataTable = class {
3795
5311
  this._hideContextMenu();
3796
5312
  return;
3797
5313
  }
5314
+ if (rowEl.closest(".twm-dt-detail-row")) {
5315
+ this._hideContextMenu();
5316
+ return;
5317
+ }
3798
5318
  event.preventDefault();
3799
5319
  event.stopPropagation();
3800
5320
  const idx = rowEl.__rowIndex;
@@ -4030,9 +5550,10 @@ export {
4030
5550
  formatDuration,
4031
5551
  matchDateFilter,
4032
5552
  matchDurationFilter,
5553
+ createChipsFilter,
4033
5554
  PAGING_HARD_LIMIT,
4034
5555
  PAGING_SOFT_LIMIT,
4035
5556
  resolvePaging,
4036
5557
  DataTable
4037
5558
  };
4038
- //# sourceMappingURL=chunk-WLASPRGP.js.map
5559
+ //# sourceMappingURL=chunk-JLCJWCRS.js.map