flexdesk 0.6.2 → 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.
- package/README.md +326 -0
- package/css/base.css +333 -0
- package/css/flexdesk.css +333 -0
- package/dist/charts.js +2 -2
- package/dist/{chunk-CPYBQN3Q.js → chunk-JLCJWCRS.js} +1617 -58
- package/dist/chunk-JLCJWCRS.js.map +7 -0
- package/dist/{chunk-XYUZF5CD.js → chunk-WKTYKXWF.js} +2 -2
- package/dist/flexdesk.css +333 -0
- package/dist/flow.js +24 -8
- package/dist/flow.js.map +2 -2
- package/dist/tiles.js +2 -2
- package/dist/widgets.js +672 -1
- package/dist/widgets.js.map +4 -4
- package/package.json +1 -1
- package/src/flow/outline/editor.js +46 -8
- package/src/ui/components/data_table.js +1695 -70
- package/src/ui/components/fact_table.js +342 -0
- package/src/ui/components/record_list.js +594 -0
- package/src/ui/components/table_filter_chips.js +153 -0
- package/dist/chunk-CPYBQN3Q.js.map +0 -7
- /package/dist/{chunk-XYUZF5CD.js.map → chunk-WKTYKXWF.js.map} +0 -0
|
@@ -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
|
-
},
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
|
@@ -1073,7 +1284,10 @@ var DataTable = class {
|
|
|
1073
1284
|
});
|
|
1074
1285
|
}
|
|
1075
1286
|
if (this._headerTableEl && this._tableEl) {
|
|
1076
|
-
requestAnimationFrame(() =>
|
|
1287
|
+
requestAnimationFrame(() => {
|
|
1288
|
+
this._syncHeaderWidths();
|
|
1289
|
+
this._virtRemeasure();
|
|
1290
|
+
});
|
|
1077
1291
|
if (this._resizeObserver) {
|
|
1078
1292
|
try {
|
|
1079
1293
|
this._resizeObserver.disconnect();
|
|
@@ -1090,7 +1304,7 @@ var DataTable = class {
|
|
|
1090
1304
|
this._installHeaderScrollSync();
|
|
1091
1305
|
if (rows.length > 0) this._installColumnResizers();
|
|
1092
1306
|
}
|
|
1093
|
-
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")) {
|
|
1094
1308
|
this._installInteractions();
|
|
1095
1309
|
}
|
|
1096
1310
|
if (activeFilterColIdx !== null) {
|
|
@@ -1100,6 +1314,18 @@ var DataTable = class {
|
|
|
1100
1314
|
this._tableEl.focus({ preventScroll: true });
|
|
1101
1315
|
} catch (_) {
|
|
1102
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();
|
|
1103
1329
|
}
|
|
1104
1330
|
}
|
|
1105
1331
|
/**
|
|
@@ -1202,6 +1428,22 @@ var DataTable = class {
|
|
|
1202
1428
|
_teardown(emptyHost) {
|
|
1203
1429
|
autoDisposeUnwatch(this);
|
|
1204
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();
|
|
1205
1447
|
this._runOffs(this._activationOffs);
|
|
1206
1448
|
this._activationOffs = [];
|
|
1207
1449
|
this._cleanup();
|
|
@@ -1241,7 +1483,7 @@ var DataTable = class {
|
|
|
1241
1483
|
// ─────────────────────────────────────────────────────────────────
|
|
1242
1484
|
_getProcessedRows() {
|
|
1243
1485
|
if (this._processedRows !== null) return this._processedRows;
|
|
1244
|
-
const isServerSide =
|
|
1486
|
+
const isServerSide = this._isServerSide();
|
|
1245
1487
|
let rows = this.config.rows;
|
|
1246
1488
|
let indexMap = rows.map((_, i) => i);
|
|
1247
1489
|
if (!isServerSide && this._state.filters.size > 0) {
|
|
@@ -1262,12 +1504,23 @@ var DataTable = class {
|
|
|
1262
1504
|
const filters = this._state.filters;
|
|
1263
1505
|
const filteredRows = [];
|
|
1264
1506
|
const filteredMap = [];
|
|
1507
|
+
const matchers = /* @__PURE__ */ new Map();
|
|
1508
|
+
for (const colIdx of filters.keys()) matchers.set(colIdx, this._filterMatcher(colIdx));
|
|
1265
1509
|
for (let i = 0; i < rows.length; i++) {
|
|
1266
1510
|
const row = rows[i];
|
|
1267
1511
|
let pass = true;
|
|
1268
1512
|
for (const [colIdx, filterText] of filters) {
|
|
1269
1513
|
if (!filterText) continue;
|
|
1270
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
|
+
}
|
|
1271
1524
|
const colType = this._valueKind(colIdx);
|
|
1272
1525
|
if (colType === "num") {
|
|
1273
1526
|
if (!this._matchNumericFilter(value, filterText)) {
|
|
@@ -1438,6 +1691,15 @@ var DataTable = class {
|
|
|
1438
1691
|
headers.forEach((header, colIdx) => {
|
|
1439
1692
|
const th = document.createElement("th");
|
|
1440
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
|
+
}
|
|
1441
1703
|
const wrapper = document.createElement("div");
|
|
1442
1704
|
wrapper.className = "twm-data-table__filter-wrapper";
|
|
1443
1705
|
const input = document.createElement("input");
|
|
@@ -1708,23 +1970,55 @@ var DataTable = class {
|
|
|
1708
1970
|
if (this._filterDebounceTimer) {
|
|
1709
1971
|
clearTimeout(this._filterDebounceTimer);
|
|
1710
1972
|
}
|
|
1973
|
+
this._pendingFilter = { colIdx, value };
|
|
1711
1974
|
this._filterDebounceTimer = setTimeout(() => {
|
|
1975
|
+
this._pendingFilter = null;
|
|
1712
1976
|
this._onFilterInput(colIdx, value);
|
|
1713
1977
|
}, 200);
|
|
1714
1978
|
}
|
|
1715
1979
|
_onFilterInput(colIdx, value) {
|
|
1716
|
-
|
|
1980
|
+
const empty = typeof value === "string" ? !value : this._isEmptyFilterValue(value);
|
|
1981
|
+
if (!empty) {
|
|
1717
1982
|
this._state.filters.set(colIdx, value);
|
|
1718
1983
|
} else {
|
|
1719
1984
|
this._state.filters.delete(colIdx);
|
|
1720
1985
|
}
|
|
1986
|
+
this._pendingFilter = null;
|
|
1721
1987
|
this._invalidateProcessedCache();
|
|
1722
1988
|
this._state.offset = 0;
|
|
1723
1989
|
this._state.selected.clear();
|
|
1724
1990
|
this._state.anchorIndex = null;
|
|
1725
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
|
+
}
|
|
1726
2005
|
this.render();
|
|
1727
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
|
+
}
|
|
1728
2022
|
_getActiveFilterColIdx() {
|
|
1729
2023
|
const active = document.activeElement;
|
|
1730
2024
|
if (!active || !active.classList.contains("twm-data-table__filter-input")) return null;
|
|
@@ -1818,6 +2112,7 @@ var DataTable = class {
|
|
|
1818
2112
|
return types;
|
|
1819
2113
|
}
|
|
1820
2114
|
_createPagination() {
|
|
2115
|
+
if (this._usesPager070()) return this._createPager070();
|
|
1821
2116
|
const { rows, pageSize, totalCount: configTotalCount, offset: configOffset, onPageChange, onJumpToRow } = this.config;
|
|
1822
2117
|
const isServerSide = typeof onPageChange === "function";
|
|
1823
2118
|
const offset = isServerSide ? configOffset || 0 : this._state.offset;
|
|
@@ -1940,6 +2235,25 @@ var DataTable = class {
|
|
|
1940
2235
|
const p = this.config.paging;
|
|
1941
2236
|
if (!p || typeof p !== "object") return null;
|
|
1942
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
|
+
}
|
|
1943
2257
|
return {
|
|
1944
2258
|
...p,
|
|
1945
2259
|
pageSize: size,
|
|
@@ -1975,7 +2289,12 @@ var DataTable = class {
|
|
|
1975
2289
|
*/
|
|
1976
2290
|
setPaging(patch = {}) {
|
|
1977
2291
|
if (!this.config.paging) return;
|
|
2292
|
+
const before = this.config.dataSource ? this.pagingState?.mode : null;
|
|
1978
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
|
+
}
|
|
1979
2298
|
this._refreshPagingStrip();
|
|
1980
2299
|
this._pagingMaybeLoadMore();
|
|
1981
2300
|
}
|
|
@@ -2029,6 +2348,7 @@ var DataTable = class {
|
|
|
2029
2348
|
_pagingMaybeLoadMore() {
|
|
2030
2349
|
const p = this._pagingConfig();
|
|
2031
2350
|
if (!p || !p.hasMore || this._pagingBusy) return;
|
|
2351
|
+
if (this.config.dataSource && this._source?.inFlight) return;
|
|
2032
2352
|
const state = this.pagingState;
|
|
2033
2353
|
if (state.mode !== "scroll" || p.held >= state.hardLimit) return;
|
|
2034
2354
|
const wrap = this._tableWrapEl;
|
|
@@ -2065,19 +2385,22 @@ var DataTable = class {
|
|
|
2065
2385
|
if (exact) of = ` of ${n(total)}`;
|
|
2066
2386
|
else if (known > to) of = ` of more than ${n(known - 1)}`;
|
|
2067
2387
|
info.textContent = held ? `Rows ${n(from)}\u2013${n(to)}${of}` : "No rows on this page";
|
|
2068
|
-
const
|
|
2069
|
-
const
|
|
2070
|
-
const lastOffset = exact && total > 0 ?
|
|
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;
|
|
2071
2391
|
const go = (offset) => (ev) => {
|
|
2072
2392
|
if (!ev?.currentTarget?.disabled) this._pagingCall("page", p.onPage, offset, pageSize);
|
|
2073
2393
|
};
|
|
2074
2394
|
const nav = document.createElement("span");
|
|
2075
2395
|
nav.className = "twm-dt-paging__nav";
|
|
2076
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);
|
|
2077
2399
|
nav.append(
|
|
2078
|
-
|
|
2079
|
-
|
|
2400
|
+
button("first_page", "first", "First page", off || !hasPrev, go(0)),
|
|
2401
|
+
button(
|
|
2080
2402
|
"chevron_left",
|
|
2403
|
+
"prev",
|
|
2081
2404
|
"Previous page",
|
|
2082
2405
|
off || !hasPrev,
|
|
2083
2406
|
go(Math.max(0, p.offset - pageSize))
|
|
@@ -2085,20 +2408,28 @@ var DataTable = class {
|
|
|
2085
2408
|
);
|
|
2086
2409
|
const page = document.createElement("span");
|
|
2087
2410
|
page.className = "twm-dt-paging__page";
|
|
2088
|
-
|
|
2089
|
-
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)}`;
|
|
2090
2412
|
nav.append(
|
|
2091
2413
|
page,
|
|
2092
|
-
|
|
2414
|
+
button(
|
|
2093
2415
|
"chevron_right",
|
|
2416
|
+
"next",
|
|
2094
2417
|
"Next page",
|
|
2095
2418
|
off || !hasNext,
|
|
2096
2419
|
go(p.offset + pageSize)
|
|
2097
2420
|
),
|
|
2098
2421
|
// The last page needs the exact count: without it there is no
|
|
2099
2422
|
// offset to go to, and the button says so by being off.
|
|
2100
|
-
|
|
2423
|
+
words ? button(
|
|
2101
2424
|
"last_page",
|
|
2425
|
+
"last",
|
|
2426
|
+
"Last page",
|
|
2427
|
+
off || !hasNext || lastOffset === null,
|
|
2428
|
+
go(lastOffset),
|
|
2429
|
+
pn.lastRefusal
|
|
2430
|
+
) : button(
|
|
2431
|
+
"last_page",
|
|
2432
|
+
"last",
|
|
2102
2433
|
lastOffset === null ? "Last page (the total is not known)" : "Last page",
|
|
2103
2434
|
off || !hasNext || lastOffset === null,
|
|
2104
2435
|
go(lastOffset)
|
|
@@ -2156,9 +2487,10 @@ var DataTable = class {
|
|
|
2156
2487
|
return el;
|
|
2157
2488
|
}
|
|
2158
2489
|
_createTable() {
|
|
2159
|
-
const { headers, rows, pageSize, sortable, filterable, showRowNumbers, readonly, getHeaderIcon
|
|
2490
|
+
const { headers, rows, pageSize, sortable, filterable, showRowNumbers, readonly, getHeaderIcon } = this.config;
|
|
2160
2491
|
const { sortColumn, sortAscending } = this._state;
|
|
2161
|
-
const
|
|
2492
|
+
const configOffset = this.config.dataSource ? 0 : this.config.offset;
|
|
2493
|
+
const isServerSide = this._isServerSide();
|
|
2162
2494
|
const processedRows = isServerSide ? rows : this._getProcessedRows();
|
|
2163
2495
|
const offset = isServerSide ? configOffset || 0 : this._state.offset;
|
|
2164
2496
|
const pageRows = isServerSide || !this.config.pagination || this._pagingConfig() ? processedRows : processedRows.slice(offset, offset + pageSize);
|
|
@@ -2223,8 +2555,8 @@ var DataTable = class {
|
|
|
2223
2555
|
const processedIdx = offset + localIdx;
|
|
2224
2556
|
return isServerSide ? (configOffset || 0) + localIdx : this._processedIndexMap?.[processedIdx] ?? processedIdx;
|
|
2225
2557
|
};
|
|
2226
|
-
this._positionBase = isServerSide ? configOffset || 0 : this.config.pagination ? offset : 0;
|
|
2227
|
-
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();
|
|
2228
2560
|
if (paged && Number.isFinite(paged.offset)) this._positionBase += paged.offset;
|
|
2229
2561
|
const tbody = document.createElement("tbody");
|
|
2230
2562
|
table.appendChild(tbody);
|
|
@@ -2237,7 +2569,9 @@ var DataTable = class {
|
|
|
2237
2569
|
});
|
|
2238
2570
|
} else {
|
|
2239
2571
|
pageRows.forEach((row, localIdx) => {
|
|
2240
|
-
|
|
2572
|
+
const tr = this._buildBodyRow(row, globalOf(localIdx), localIdx);
|
|
2573
|
+
tbody.appendChild(tr);
|
|
2574
|
+
if (this._detailOn()) this._appendDetailIfOpen(tr, null);
|
|
2241
2575
|
});
|
|
2242
2576
|
}
|
|
2243
2577
|
return table;
|
|
@@ -2381,6 +2715,7 @@ var DataTable = class {
|
|
|
2381
2715
|
const heights = new Float64Array(n);
|
|
2382
2716
|
const offsets = new Float64Array(n + 1);
|
|
2383
2717
|
const fn = typeof this.config.virtualRowHeight === "function" ? this.config.virtualRowHeight : null;
|
|
2718
|
+
const details = this._detailOn() && this._expanded.size > 0;
|
|
2384
2719
|
let y = 0;
|
|
2385
2720
|
for (let i = 0; i < n; i++) {
|
|
2386
2721
|
let h = v.base;
|
|
@@ -2388,6 +2723,7 @@ var DataTable = class {
|
|
|
2388
2723
|
const asked = fn(v.globalOf(i), v.base);
|
|
2389
2724
|
h = Number.isFinite(asked) && asked > 0 ? asked : 0;
|
|
2390
2725
|
}
|
|
2726
|
+
if (details && h > 0) h += this._detailHeightOf(v.rows[i], v.globalOf(i), v.base);
|
|
2391
2727
|
heights[i] = h;
|
|
2392
2728
|
offsets[i] = y;
|
|
2393
2729
|
y += h;
|
|
@@ -2411,26 +2747,65 @@ var DataTable = class {
|
|
|
2411
2747
|
}
|
|
2412
2748
|
return lo;
|
|
2413
2749
|
}
|
|
2414
|
-
/**
|
|
2415
|
-
*
|
|
2416
|
-
*
|
|
2750
|
+
/**
|
|
2751
|
+
* Measure the rows that are DRAWN — the median of up to 64 of them. Client
|
|
2752
|
+
* pixels, so the zoom is divided back out (`_measureNaturalWidths` explains
|
|
2753
|
+
* why), and NOT `offsetHeight`, which is rounded: 21 for a 21.19px row is a
|
|
2754
|
+
* 1,100px error over 6,000 rows.
|
|
2755
|
+
*
|
|
2756
|
+
* 0.6.3: IT USED TO MEASURE ONE ROW, ONCE, AT RENDER, and trust it for the
|
|
2757
|
+
* life of the table. A row measured at a bad moment — a box not yet laid
|
|
2758
|
+
* out at its width, a font not yet loaded — set every row's height: a
|
|
2759
|
+
* Tables grid drawing 22.8px rows was found holding a 134px estimate, so the
|
|
2760
|
+
* window took a 650px viewport for five rows, drew rows 1–35 and left the
|
|
2761
|
+
* rest of the box blank spacer while the reader scrolled. Now it is asked
|
|
2762
|
+
* again on every window update (`_virtUpdate`), on a resize and once the
|
|
2763
|
+
* column widths are fixed, and corrects itself the first time real rows
|
|
2764
|
+
* are on screen. The first measure is exact; after that a change has to be
|
|
2765
|
+
* larger than half a pixel, so rows of mixed heights cannot make the
|
|
2766
|
+
* offsets move under the reader on every scroll.
|
|
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
|
+
*
|
|
2776
|
+
* @returns {boolean} whether the heights changed (the offsets are recomputed).
|
|
2777
|
+
*/
|
|
2417
2778
|
_virtMeasure() {
|
|
2418
2779
|
const v = this._virt;
|
|
2419
2780
|
if (!v) return false;
|
|
2781
|
+
const zoom = this._tableEl?.currentCSSZoom || 1;
|
|
2782
|
+
const heights = [];
|
|
2420
2783
|
for (const tr of v.map.values()) {
|
|
2421
2784
|
if (!tr.isConnected || tr.hidden) continue;
|
|
2422
|
-
const zoom = this._tableEl?.currentCSSZoom || 1;
|
|
2423
2785
|
const h = tr.getBoundingClientRect().height / zoom;
|
|
2424
|
-
if (
|
|
2425
|
-
if (
|
|
2786
|
+
if (h > 0) heights.push(h);
|
|
2787
|
+
if (heights.length >= 64) break;
|
|
2788
|
+
}
|
|
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) {
|
|
2426
2797
|
v.base = h;
|
|
2427
2798
|
this._virtBase = h;
|
|
2428
|
-
|
|
2429
|
-
return true;
|
|
2799
|
+
base = true;
|
|
2430
2800
|
}
|
|
2431
|
-
return false;
|
|
2432
2801
|
}
|
|
2433
|
-
return false;
|
|
2802
|
+
if (!base && !details) return false;
|
|
2803
|
+
this._virtComputeOffsets();
|
|
2804
|
+
return true;
|
|
2805
|
+
}
|
|
2806
|
+
/** Measure again, and redraw the window when the heights moved. */
|
|
2807
|
+
_virtRemeasure() {
|
|
2808
|
+
if (this._virtMeasure()) this._virtUpdate({ force: true });
|
|
2434
2809
|
}
|
|
2435
2810
|
/**
|
|
2436
2811
|
* Slide the window to wherever the scroll box now is.
|
|
@@ -2449,9 +2824,10 @@ var DataTable = class {
|
|
|
2449
2824
|
* @param {number} [o.viewport] the viewport height, likewise.
|
|
2450
2825
|
* @returns {HTMLElement[]} the rows added.
|
|
2451
2826
|
*/
|
|
2452
|
-
_virtUpdate({ force = false, notify = true, top = null, viewport = null } = {}) {
|
|
2827
|
+
_virtUpdate({ force = false, notify = true, top = null, viewport = null, measure = true } = {}) {
|
|
2453
2828
|
const v = this._virt;
|
|
2454
2829
|
if (!v || !v.offsets) return [];
|
|
2830
|
+
if (measure && this._virtMeasure()) force = true;
|
|
2455
2831
|
const n = v.rows.length;
|
|
2456
2832
|
const wrap = this._tableWrapEl;
|
|
2457
2833
|
const view = viewport || wrap?.clientHeight || VIRTUAL_FALLBACK_VIEWPORT_PX;
|
|
@@ -2479,6 +2855,15 @@ var DataTable = class {
|
|
|
2479
2855
|
console.error("[DataTable] onRowsRendered threw", err);
|
|
2480
2856
|
}
|
|
2481
2857
|
}
|
|
2858
|
+
if (measure && added.length && this._virtMeasure()) {
|
|
2859
|
+
return added.concat(this._virtUpdate({
|
|
2860
|
+
force: true,
|
|
2861
|
+
notify,
|
|
2862
|
+
top,
|
|
2863
|
+
viewport,
|
|
2864
|
+
measure: false
|
|
2865
|
+
}));
|
|
2866
|
+
}
|
|
2482
2867
|
}
|
|
2483
2868
|
return added;
|
|
2484
2869
|
}
|
|
@@ -2493,6 +2878,7 @@ var DataTable = class {
|
|
|
2493
2878
|
for (const [i, tr] of v.map) {
|
|
2494
2879
|
if (!wanted.has(i)) {
|
|
2495
2880
|
tr.remove();
|
|
2881
|
+
this._removeDetailPieces(tr);
|
|
2496
2882
|
v.map.delete(i);
|
|
2497
2883
|
removed?.push(tr);
|
|
2498
2884
|
}
|
|
@@ -2512,10 +2898,12 @@ var DataTable = class {
|
|
|
2512
2898
|
const tr = this._buildBodyRow(v.rows[i], v.globalOf(i), i);
|
|
2513
2899
|
v.map.set(i, tr);
|
|
2514
2900
|
tbody.insertBefore(tr, ref);
|
|
2901
|
+
if (this._detailOn()) this._appendDetailIfOpen(tr, ref);
|
|
2515
2902
|
ref = tr;
|
|
2516
2903
|
added.push(tr);
|
|
2517
2904
|
}
|
|
2518
2905
|
added.reverse();
|
|
2906
|
+
if (this._detailOn() && added.length) this._virtMeasureDetails(added);
|
|
2519
2907
|
return added;
|
|
2520
2908
|
}
|
|
2521
2909
|
/**
|
|
@@ -2715,6 +3103,7 @@ var DataTable = class {
|
|
|
2715
3103
|
if (colIdx === 0 && typeof this.config.rowIcon === "function") {
|
|
2716
3104
|
this._prependRowIcon(td, this.config.rowIcon(row, globalIdx));
|
|
2717
3105
|
}
|
|
3106
|
+
if (colIdx === 0 && this._detailOn()) this._prependDetailToggle(td, tr, row, globalIdx);
|
|
2718
3107
|
if (this.config.onCellContextMenu) {
|
|
2719
3108
|
const cellColIdx = colIdx;
|
|
2720
3109
|
td.addEventListener("contextmenu", (ev) => {
|
|
@@ -2799,12 +3188,1141 @@ var DataTable = class {
|
|
|
2799
3188
|
}
|
|
2800
3189
|
tr.classList.toggle("selected", this._state?.selected?.has(index) === true);
|
|
2801
3190
|
this._decorateRow(tr, row, index);
|
|
3191
|
+
if (this._detailOn()) this._redrawDetail(tr);
|
|
2802
3192
|
if (focusedCol >= 0 && tr.children[focusedCol]) {
|
|
2803
3193
|
tr.children[focusedCol].focus?.();
|
|
2804
3194
|
}
|
|
2805
3195
|
return true;
|
|
2806
3196
|
}
|
|
2807
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
|
+
// ─────────────────────────────────────────────────────────────────
|
|
2808
4326
|
// 0.5.0 — decoration, states and the typed columns
|
|
2809
4327
|
// ─────────────────────────────────────────────────────────────────
|
|
2810
4328
|
/** The class list both halves of the table carry: the 0.4 pair, plus the
|
|
@@ -3013,6 +4531,9 @@ var DataTable = class {
|
|
|
3013
4531
|
tr.__rowKey = this._rowKey(row, rowIdx);
|
|
3014
4532
|
}
|
|
3015
4533
|
this._paintActive(tr);
|
|
4534
|
+
if (this._newKeys) {
|
|
4535
|
+
tr.classList.toggle("twm-dt-row--new", this._newKeys.has(this._rowKey(row, rowIdx)));
|
|
4536
|
+
}
|
|
3016
4537
|
}
|
|
3017
4538
|
/** The open mark on one row, from `_activeKey`. */
|
|
3018
4539
|
_paintActive(tr) {
|
|
@@ -3168,6 +4689,21 @@ var DataTable = class {
|
|
|
3168
4689
|
const head = labelRow && labelRow.children[i] ? labelRow.children[i].getBoundingClientRect().width / zoom : 0;
|
|
3169
4690
|
natural[i] = Math.max(body, head);
|
|
3170
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;
|
|
3171
4707
|
for (const tr of sampled) tr.remove();
|
|
3172
4708
|
headerTable.classList.remove("twm-dt-measuring");
|
|
3173
4709
|
bodyTable.classList.remove("twm-dt-measuring");
|
|
@@ -3325,7 +4861,8 @@ var DataTable = class {
|
|
|
3325
4861
|
const widths = this._fitColumnWidths(natural, avail, {
|
|
3326
4862
|
firstIdx,
|
|
3327
4863
|
overrides: this._colWidths,
|
|
3328
|
-
fit: this.config.columnFit
|
|
4864
|
+
fit: this.config.columnFit,
|
|
4865
|
+
floors: this._filterFloors
|
|
3329
4866
|
});
|
|
3330
4867
|
const total = widths.reduce((a, b) => a + b, 0);
|
|
3331
4868
|
headerRows.forEach((tr) => {
|
|
@@ -3395,6 +4932,8 @@ var DataTable = class {
|
|
|
3395
4932
|
const n = natural.length;
|
|
3396
4933
|
const firstIdx = opts.firstIdx ?? 0;
|
|
3397
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));
|
|
3398
4937
|
const toContent = opts.fit === "content";
|
|
3399
4938
|
const desired = new Array(n);
|
|
3400
4939
|
const pinned = new Array(n).fill(false);
|
|
@@ -3435,7 +4974,7 @@ var DataTable = class {
|
|
|
3435
4974
|
}
|
|
3436
4975
|
const budget = avail - protectedSum;
|
|
3437
4976
|
if (budget < shrinkable.length * FLOOR) {
|
|
3438
|
-
for (const i of shrinkable) widths[i] =
|
|
4977
|
+
for (const i of shrinkable) widths[i] = floorOf(i);
|
|
3439
4978
|
return widths;
|
|
3440
4979
|
}
|
|
3441
4980
|
shrinkable.sort((a, b) => desired[a] - desired[b]);
|
|
@@ -3450,7 +4989,7 @@ var DataTable = class {
|
|
|
3450
4989
|
} else {
|
|
3451
4990
|
const level = Math.max(FLOOR, fair);
|
|
3452
4991
|
for (let j = k; j < shrinkable.length; j++) {
|
|
3453
|
-
widths[shrinkable[j]] = level;
|
|
4992
|
+
widths[shrinkable[j]] = Math.max(level, floorOf(shrinkable[j]));
|
|
3454
4993
|
}
|
|
3455
4994
|
const leftover = remaining - colsLeft * level;
|
|
3456
4995
|
if (leftover > 0) widths[shrinkable[shrinkable.length - 1]] += leftover;
|
|
@@ -3698,6 +5237,7 @@ var DataTable = class {
|
|
|
3698
5237
|
const cells = rows ? rows.flatMap((tr) => [...tr.children]) : bodyTable.querySelectorAll("tbody td");
|
|
3699
5238
|
cells.forEach((td) => {
|
|
3700
5239
|
if (td.parentElement?.classList.contains("twm-dt-spacer")) return;
|
|
5240
|
+
if (td.closest?.(".twm-dt-detail-row")) return;
|
|
3701
5241
|
const clipped = td.scrollWidth > td.clientWidth + 1;
|
|
3702
5242
|
if (clipped) {
|
|
3703
5243
|
if (!td.title) td.title = td.textContent;
|
|
@@ -3750,6 +5290,20 @@ var DataTable = class {
|
|
|
3750
5290
|
if (typeof this.config.onRowActivate === "function") {
|
|
3751
5291
|
this._installActivation(tbody, table);
|
|
3752
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
|
+
}
|
|
3753
5307
|
if (copyable) {
|
|
3754
5308
|
const handleContextMenu = (event) => {
|
|
3755
5309
|
const rowEl = event.target.closest("tr");
|
|
@@ -3757,6 +5311,10 @@ var DataTable = class {
|
|
|
3757
5311
|
this._hideContextMenu();
|
|
3758
5312
|
return;
|
|
3759
5313
|
}
|
|
5314
|
+
if (rowEl.closest(".twm-dt-detail-row")) {
|
|
5315
|
+
this._hideContextMenu();
|
|
5316
|
+
return;
|
|
5317
|
+
}
|
|
3760
5318
|
event.preventDefault();
|
|
3761
5319
|
event.stopPropagation();
|
|
3762
5320
|
const idx = rowEl.__rowIndex;
|
|
@@ -3992,9 +5550,10 @@ export {
|
|
|
3992
5550
|
formatDuration,
|
|
3993
5551
|
matchDateFilter,
|
|
3994
5552
|
matchDurationFilter,
|
|
5553
|
+
createChipsFilter,
|
|
3995
5554
|
PAGING_HARD_LIMIT,
|
|
3996
5555
|
PAGING_SOFT_LIMIT,
|
|
3997
5556
|
resolvePaging,
|
|
3998
5557
|
DataTable
|
|
3999
5558
|
};
|
|
4000
|
-
//# sourceMappingURL=chunk-
|
|
5559
|
+
//# sourceMappingURL=chunk-JLCJWCRS.js.map
|