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