flexdesk 0.4.6 → 0.5.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.
@@ -1,3 +1,8 @@
1
+ import {
2
+ isRowActivation,
3
+ isRowControl
4
+ } from "./chunk-6JSOVNID.js";
5
+
1
6
  // src/ui/utils/raf_resize_observer.js
2
7
  function createRafResizeObserver(callback) {
3
8
  let scheduled = false;
@@ -13,8 +18,438 @@ function createRafResizeObserver(callback) {
13
18
  });
14
19
  }
15
20
 
21
+ // src/ui/components/table_values.js
22
+ var MONTHS = [
23
+ "January",
24
+ "February",
25
+ "March",
26
+ "April",
27
+ "May",
28
+ "June",
29
+ "July",
30
+ "August",
31
+ "September",
32
+ "October",
33
+ "November",
34
+ "December"
35
+ ];
36
+ var WEEKDAYS = [
37
+ "Sunday",
38
+ "Monday",
39
+ "Tuesday",
40
+ "Wednesday",
41
+ "Thursday",
42
+ "Friday",
43
+ "Saturday"
44
+ ];
45
+ var ISO_DATE = "YYYY-MM-DD";
46
+ var ISO_DATETIME = "YYYY-MM-DD HH:mm";
47
+ var ISO_RE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?)?\s*(Z|[+-]\d{2}(?::?\d{2})?)?$/i;
48
+ var pad = (n, w = 2) => String(Math.trunc(Math.abs(n))).padStart(w, "0");
49
+ function daysInMonth(y, mo) {
50
+ if (mo !== 1) return [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][mo];
51
+ return y % 4 === 0 && y % 100 !== 0 || y % 400 === 0 ? 29 : 28;
52
+ }
53
+ function partsToTime(utc, y, mo, d = 1, h = 0, mi = 0, s = 0, ms = 0) {
54
+ const date = /* @__PURE__ */ new Date(0);
55
+ if (utc) {
56
+ date.setUTCFullYear(y, mo, d);
57
+ date.setUTCHours(h, mi, s, ms);
58
+ } else {
59
+ date.setFullYear(y, mo, d);
60
+ date.setHours(h, mi, s, ms);
61
+ }
62
+ return date.getTime();
63
+ }
64
+ function parseDateValue(value, { utc = false } = {}) {
65
+ if (value == null || value === "") return NaN;
66
+ if (value instanceof Date) return value.getTime();
67
+ if (typeof value === "number") return Number.isFinite(value) ? value : NaN;
68
+ const text = String(value).trim();
69
+ if (!text) return NaN;
70
+ const m = ISO_RE.exec(text);
71
+ if (!m) return NaN;
72
+ const y = Number(m[1]);
73
+ const mo = Number(m[2]);
74
+ const d = Number(m[3]);
75
+ if (mo < 1 || mo > 12 || d < 1 || d > daysInMonth(y, mo - 1)) return NaN;
76
+ const h = m[4] ? Number(m[4]) : 0;
77
+ const mi = m[5] ? Number(m[5]) : 0;
78
+ const s = m[6] ? Number(m[6]) : 0;
79
+ const ms = m[7] ? Math.round(Number(`0.${m[7]}`) * 1e3) : 0;
80
+ if (h > 24 || mi > 59 || s > 60 || h === 24 && (mi || s || ms)) return NaN;
81
+ const zone = m[8];
82
+ if (zone) {
83
+ let offset = 0;
84
+ if (zone.toUpperCase() !== "Z") {
85
+ const sign = zone[0] === "-" ? -1 : 1;
86
+ const digits = zone.slice(1).replace(":", "");
87
+ offset = sign * (Number(digits.slice(0, 2)) * 60 + Number(digits.slice(2, 4) || 0));
88
+ }
89
+ return partsToTime(true, y, mo - 1, d, h, mi, s, ms) - offset * 6e4;
90
+ }
91
+ return partsToTime(utc, y, mo - 1, d, h, mi, s, ms);
92
+ }
93
+ function formatDate(value, pattern = ISO_DATE, { utc = false } = {}) {
94
+ if (value == null || value === "") return "";
95
+ const t = parseDateValue(value, { utc });
96
+ if (!Number.isFinite(t)) return String(value);
97
+ const date = new Date(t);
98
+ if (typeof pattern === "function") return String(pattern(date) ?? "");
99
+ const get = utc ? {
100
+ y: date.getUTCFullYear(),
101
+ mo: date.getUTCMonth(),
102
+ d: date.getUTCDate(),
103
+ wd: date.getUTCDay(),
104
+ h: date.getUTCHours(),
105
+ mi: date.getUTCMinutes(),
106
+ s: date.getUTCSeconds(),
107
+ ms: date.getUTCMilliseconds()
108
+ } : {
109
+ y: date.getFullYear(),
110
+ mo: date.getMonth(),
111
+ d: date.getDate(),
112
+ wd: date.getDay(),
113
+ h: date.getHours(),
114
+ mi: date.getMinutes(),
115
+ s: date.getSeconds(),
116
+ ms: date.getMilliseconds()
117
+ };
118
+ const h12 = get.h % 12 === 0 ? 12 : get.h % 12;
119
+ return String(pattern || ISO_DATE).replace(
120
+ /\[([^\]]*)\]|YYYY|YY|MMMM|MMM|MM|M|dddd|ddd|DD|D|HH|H|hh|h|mm|m|ss|s|SSS|A|a/g,
121
+ (tok, literal) => {
122
+ if (literal !== void 0) return literal;
123
+ switch (tok) {
124
+ case "YYYY":
125
+ return pad(get.y, 4);
126
+ case "YY":
127
+ return pad(get.y % 100);
128
+ case "MMMM":
129
+ return MONTHS[get.mo];
130
+ case "MMM":
131
+ return MONTHS[get.mo].slice(0, 3);
132
+ case "MM":
133
+ return pad(get.mo + 1);
134
+ case "M":
135
+ return String(get.mo + 1);
136
+ case "dddd":
137
+ return WEEKDAYS[get.wd];
138
+ case "ddd":
139
+ return WEEKDAYS[get.wd].slice(0, 3);
140
+ case "DD":
141
+ return pad(get.d);
142
+ case "D":
143
+ return String(get.d);
144
+ case "HH":
145
+ return pad(get.h);
146
+ case "H":
147
+ return String(get.h);
148
+ case "hh":
149
+ return pad(h12);
150
+ case "h":
151
+ return String(h12);
152
+ case "mm":
153
+ return pad(get.mi);
154
+ case "m":
155
+ return String(get.mi);
156
+ case "ss":
157
+ return pad(get.s);
158
+ case "s":
159
+ return String(get.s);
160
+ case "SSS":
161
+ return pad(get.ms, 3);
162
+ case "A":
163
+ return get.h < 12 ? "AM" : "PM";
164
+ case "a":
165
+ return get.h < 12 ? "am" : "pm";
166
+ default:
167
+ return tok;
168
+ }
169
+ }
170
+ );
171
+ }
172
+ function parseDatePeriod(text, { utc = false } = {}) {
173
+ const t = String(text ?? "").trim();
174
+ const m = /^(\d{4})(?:-(\d{2})(?:-(\d{2})(?:[T ](\d{2})(?::(\d{2})(?::(\d{2}))?)?)?)?)?$/i.exec(t);
175
+ if (m) {
176
+ const parts = [
177
+ Number(m[1]),
178
+ m[2] ? Number(m[2]) - 1 : 0,
179
+ m[3] ? Number(m[3]) : 1,
180
+ m[4] ? Number(m[4]) : 0,
181
+ m[5] ? Number(m[5]) : 0,
182
+ m[6] ? Number(m[6]) : 0
183
+ ];
184
+ if (parts[1] < 0 || parts[1] > 11 || parts[2] < 1 || parts[2] > daysInMonth(parts[0], parts[1]) || parts[3] > 23 || parts[4] > 59 || parts[5] > 59) return null;
185
+ const unit = m[6] ? 5 : m[5] ? 4 : m[4] ? 3 : m[3] ? 2 : m[2] ? 1 : 0;
186
+ const next = parts.slice();
187
+ next[unit] += 1;
188
+ return { start: partsToTime(utc, ...parts), end: partsToTime(utc, ...next) };
189
+ }
190
+ const instant = parseDateValue(t, { utc });
191
+ return Number.isFinite(instant) ? { start: instant, end: instant + 1 } : null;
192
+ }
193
+ var DURATION_UNITS = {
194
+ ms: 1,
195
+ msec: 1,
196
+ msecs: 1,
197
+ millisecond: 1,
198
+ milliseconds: 1,
199
+ s: 1e3,
200
+ sec: 1e3,
201
+ secs: 1e3,
202
+ second: 1e3,
203
+ seconds: 1e3,
204
+ m: 6e4,
205
+ min: 6e4,
206
+ mins: 6e4,
207
+ minute: 6e4,
208
+ minutes: 6e4,
209
+ h: 36e5,
210
+ hr: 36e5,
211
+ hrs: 36e5,
212
+ hour: 36e5,
213
+ hours: 36e5,
214
+ d: 864e5,
215
+ day: 864e5,
216
+ days: 864e5
217
+ };
218
+ function parseDuration(value) {
219
+ if (value == null || value === "") return NaN;
220
+ if (typeof value === "number") return Number.isFinite(value) ? value : NaN;
221
+ let text = String(value).trim().toLowerCase();
222
+ if (!text) return NaN;
223
+ let sign = 1;
224
+ if (text[0] === "-") {
225
+ sign = -1;
226
+ text = text.slice(1).trim();
227
+ }
228
+ if (/^\d+(?:\.\d+)?$/.test(text)) return sign * Number(text);
229
+ const clock = /^(?:(\d+)\.)?(\d+):(\d{1,2})(?::(\d{1,2}(?:\.\d+)?))?$/.exec(text);
230
+ if (clock) {
231
+ const [, d, a, b, c] = clock;
232
+ const ms = (Number(d || 0) * 86400 + Number(a) * 3600 + Number(b) * 60 + Number(c || 0)) * 1e3;
233
+ return sign * ms;
234
+ }
235
+ const re = /(\d+(?:\.\d+)?)\s*([a-z]+)\s*/g;
236
+ let total = 0;
237
+ let consumed = 0;
238
+ let m;
239
+ while ((m = re.exec(text)) !== null) {
240
+ if (m.index !== consumed) return NaN;
241
+ const unit = DURATION_UNITS[m[2]];
242
+ if (unit === void 0) return NaN;
243
+ total += Number(m[1]) * unit;
244
+ consumed = re.lastIndex;
245
+ }
246
+ return consumed === text.length && consumed > 0 ? sign * total : NaN;
247
+ }
248
+ function formatDuration(value, pattern = "auto") {
249
+ if (value == null || value === "") return "";
250
+ const ms = parseDuration(value);
251
+ if (!Number.isFinite(ms)) return String(value);
252
+ if (typeof pattern === "function") return String(pattern(ms) ?? "");
253
+ const neg = ms < 0;
254
+ const a = Math.abs(ms);
255
+ let out;
256
+ if (pattern === "clock") {
257
+ const total = Math.round(a / 1e3);
258
+ const d = Math.floor(total / 86400);
259
+ const h = Math.floor(total % 86400 / 3600);
260
+ const mi = Math.floor(total % 3600 / 60);
261
+ const s = total % 60;
262
+ out = `${d ? `${d}.${pad(h)}` : h}:${pad(mi)}:${pad(s)}`;
263
+ } else if (a < 1e3) {
264
+ out = `${Math.round(a)} ms`;
265
+ } else if (a < 59950) {
266
+ out = `${(a / 1e3).toFixed(1)} s`;
267
+ } else if (a < 36e5) {
268
+ let mi = Math.floor(a / 6e4);
269
+ let s = Math.round(a % 6e4 / 1e3);
270
+ if (s === 60) {
271
+ mi += 1;
272
+ s = 0;
273
+ }
274
+ out = `${mi}m ${s}s`;
275
+ } else if (a < 864e5) {
276
+ let h = Math.floor(a / 36e5);
277
+ let mi = Math.round(a % 36e5 / 6e4);
278
+ if (mi === 60) {
279
+ h += 1;
280
+ mi = 0;
281
+ }
282
+ out = `${h}h ${mi}m`;
283
+ } else {
284
+ let d = Math.floor(a / 864e5);
285
+ let h = Math.round(a % 864e5 / 36e5);
286
+ if (h === 24) {
287
+ d += 1;
288
+ h = 0;
289
+ }
290
+ out = `${d}d ${h}h`;
291
+ }
292
+ return neg ? `-${out}` : out;
293
+ }
294
+ function matchDateFilter(t, filterText, shown, { utc = false } = {}) {
295
+ const text = String(filterText ?? "").trim();
296
+ if (!text) return true;
297
+ const range = /^(.+?)\s*\.\.\s*(.+)$/.exec(text);
298
+ if (range) {
299
+ const lo = parseDatePeriod(range[1], { utc });
300
+ const hi = parseDatePeriod(range[2], { utc });
301
+ if (!lo || !hi) return true;
302
+ return Number.isFinite(t) && t >= lo.start && t < hi.end;
303
+ }
304
+ const op = /^(>=|<=|!=|>|<|=)\s*(.+)$/.exec(text);
305
+ if (op) {
306
+ const p2 = parseDatePeriod(op[2], { utc });
307
+ if (!p2) return true;
308
+ if (!Number.isFinite(t)) return false;
309
+ switch (op[1]) {
310
+ case ">":
311
+ return t >= p2.end;
312
+ case ">=":
313
+ return t >= p2.start;
314
+ case "<":
315
+ return t < p2.start;
316
+ case "<=":
317
+ return t < p2.end;
318
+ case "=":
319
+ return t >= p2.start && t < p2.end;
320
+ case "!=":
321
+ return !(t >= p2.start && t < p2.end);
322
+ }
323
+ }
324
+ const p = parseDatePeriod(text, { utc });
325
+ if (p) return Number.isFinite(t) && t >= p.start && t < p.end;
326
+ return String(shown ?? "").toLowerCase().includes(text.toLowerCase());
327
+ }
328
+ function matchDurationFilter(ms, filterText, shown) {
329
+ const text = String(filterText ?? "").trim();
330
+ if (!text) return true;
331
+ const range = /^(.+?)\s*\.\.\s*(.+)$/.exec(text);
332
+ if (range) {
333
+ const lo = parseDuration(range[1]);
334
+ const hi = parseDuration(range[2]);
335
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) return true;
336
+ return Number.isFinite(ms) && ms >= lo && ms <= hi;
337
+ }
338
+ const op = /^(>=|<=|!=|>|<|=)\s*(.+)$/.exec(text);
339
+ if (op) {
340
+ const target = parseDuration(op[2]);
341
+ if (!Number.isFinite(target)) return true;
342
+ if (!Number.isFinite(ms)) return false;
343
+ switch (op[1]) {
344
+ case ">":
345
+ return ms > target;
346
+ case ">=":
347
+ return ms >= target;
348
+ case "<":
349
+ return ms < target;
350
+ case "<=":
351
+ return ms <= target;
352
+ case "=":
353
+ return ms === target;
354
+ case "!=":
355
+ return ms !== target;
356
+ }
357
+ }
358
+ return String(shown ?? "").toLowerCase().includes(text.toLowerCase());
359
+ }
360
+
16
361
  // src/ui/components/data_table.js
17
362
  var DEFAULT_PAGE_SIZE = 100;
363
+ var TYPED_COLUMNS = /* @__PURE__ */ new Set(["date", "datetime", "duration"]);
364
+ var DESELECTING_CLICKS = /* @__PURE__ */ new WeakSet();
365
+ var AUTO_DISPOSE = /* @__PURE__ */ new Map();
366
+ var AUTO_REFS = /* @__PURE__ */ new WeakMap();
367
+ var weakRef = (table) => typeof WeakRef === "function" ? new WeakRef(table) : { deref: () => table };
368
+ function autoDisposeSweep(entry) {
369
+ for (const ref of entry.tables) {
370
+ const table = ref.deref();
371
+ if (!table) {
372
+ entry.tables.delete(ref);
373
+ continue;
374
+ }
375
+ const el = table._wrapperEl;
376
+ if (!el) continue;
377
+ if (el.isConnected) {
378
+ table._autoSeen = true;
379
+ continue;
380
+ }
381
+ if (!table._autoSeen || table._autoPending) continue;
382
+ table._autoPending = true;
383
+ setTimeout(() => {
384
+ table._autoPending = false;
385
+ if (!entry.tables.has(ref)) return;
386
+ const now = table._wrapperEl;
387
+ if (now && !now.isConnected) table._disposeOwn();
388
+ }, 0);
389
+ }
390
+ if (entry.tables.size === 0) autoDisposeRelease(entry);
391
+ }
392
+ function autoDisposeRelease(entry) {
393
+ for (const [doc, e] of AUTO_DISPOSE) {
394
+ if (e !== entry) continue;
395
+ try {
396
+ e.observer.disconnect();
397
+ } catch (_) {
398
+ }
399
+ AUTO_DISPOSE.delete(doc);
400
+ }
401
+ }
402
+ function autoDisposeWatch(table) {
403
+ const doc = table.container?.ownerDocument;
404
+ const MO = doc?.defaultView?.MutationObserver || globalThis.MutationObserver;
405
+ if (!doc || typeof MO !== "function") return;
406
+ let entry = AUTO_DISPOSE.get(doc);
407
+ if (!entry) {
408
+ entry = { observer: null, tables: /* @__PURE__ */ new Set() };
409
+ entry.observer = new MO(() => autoDisposeSweep(entry));
410
+ entry.observer.observe(doc, { childList: true, subtree: true });
411
+ AUTO_DISPOSE.set(doc, entry);
412
+ }
413
+ let ref = AUTO_REFS.get(table);
414
+ if (!ref) {
415
+ ref = weakRef(table);
416
+ AUTO_REFS.set(table, ref);
417
+ }
418
+ entry.tables.add(ref);
419
+ if (table._wrapperEl?.isConnected) table._autoSeen = true;
420
+ }
421
+ function autoDisposeUnwatch(table) {
422
+ const ref = AUTO_REFS.get(table);
423
+ if (!ref) return;
424
+ for (const entry of [...AUTO_DISPOSE.values()]) {
425
+ if (entry.tables.delete(ref) && entry.tables.size === 0) autoDisposeRelease(entry);
426
+ }
427
+ }
428
+ function perColumn(option, colIdx) {
429
+ if (option == null) return null;
430
+ if (Array.isArray(option)) return option[colIdx] ?? null;
431
+ if (typeof option === "object") return option[colIdx] ?? null;
432
+ return option;
433
+ }
434
+ function normaliseSortKey(k) {
435
+ if (k == null) return null;
436
+ if (k instanceof Date) k = k.getTime();
437
+ if (typeof k === "boolean") return k ? 1 : 0;
438
+ if (typeof k === "number") return Number.isFinite(k) ? k : null;
439
+ if (typeof k === "bigint") return Number(k);
440
+ return String(k).toLowerCase();
441
+ }
442
+ function compareSortKeys(a, b, asc) {
443
+ if (a === null && b === null) return 0;
444
+ if (a === null) return 1;
445
+ if (b === null) return -1;
446
+ const na = typeof a === "number";
447
+ const nb = typeof b === "number";
448
+ let cmp;
449
+ if (na !== nb) cmp = na ? -1 : 1;
450
+ else cmp = a < b ? -1 : a > b ? 1 : 0;
451
+ return asc ? cmp : -cmp;
452
+ }
18
453
  var AUTOSIZE_MAX_PX = 520;
19
454
  var VIRTUAL_FALLBACK_VIEWPORT_PX = 1e3;
20
455
  var DataTable = class {
@@ -38,6 +473,11 @@ var DataTable = class {
38
473
  showRowNumbers: false,
39
474
  emptyMessage: "No data",
40
475
  onSort: null,
476
+ // OFF unless asked for, for `columnFit`'s reason below: a sort kept
477
+ // the client-side page before this key existed, so turning the
478
+ // reset on for everybody would change every paged table that never
479
+ // asked. See the typedef.
480
+ resetPageOnSort: false,
41
481
  onSelectionChange: null,
42
482
  formatValue: null,
43
483
  getHeaderIcon: null,
@@ -76,8 +516,34 @@ var DataTable = class {
76
516
  virtualize: false,
77
517
  virtualRowHeight: null,
78
518
  onRowsRendered: null,
519
+ // 0.5.0. Every key below is off — or exactly the 0.4 behaviour — until
520
+ // a consumer sets it (D6: additive and back-compatible). The typedef
521
+ // says what each does.
522
+ fitContent: false,
523
+ maxHeight: null,
524
+ autoDispose: false,
525
+ firstColumn: "time",
526
+ clickable: null,
527
+ onRowActivate: null,
528
+ activateOn: "click",
529
+ getRowKey: null,
530
+ activeRow: null,
531
+ rowClass: null,
532
+ rowAttrs: null,
533
+ cellClass: null,
534
+ rowIcon: null,
535
+ emptyState: void 0,
536
+ nullDisplay: null,
537
+ columnTypes: null,
538
+ sortValue: null,
539
+ dateFormat: null,
540
+ dateTimeFormat: null,
541
+ durationFormat: null,
542
+ dateTimeZone: "local",
79
543
  ...config
80
544
  };
545
+ this._activeKey = this.config.activeRow == null ? null : String(this.config.activeRow);
546
+ this._error = null;
81
547
  this._virt = null;
82
548
  this._virtBase = null;
83
549
  if (this.config.persistKey && !this.config.stateStore) {
@@ -106,6 +572,7 @@ var DataTable = class {
106
572
  this._colWidthsSig = null;
107
573
  this._disposers = [];
108
574
  this._contextMenuGlobals = [];
575
+ this._activationOffs = [];
109
576
  if (this.config.persistKey) {
110
577
  const store = this.config.stateStore;
111
578
  this._restorePersisted(store.get(this.config.persistKey));
@@ -118,6 +585,44 @@ var DataTable = class {
118
585
  });
119
586
  }
120
587
  }
588
+ /**
589
+ * 0.5.0. Construct AND DRAW, in one call. The constructor draws nothing
590
+ * until `render()`, so every consumer that forgot the second line got an
591
+ * empty box; this is the two lines, once.
592
+ *
593
+ * Called on a class made by `withDefaults`, it builds that class — so the
594
+ * house defaults apply.
595
+ *
596
+ * @param {HTMLElement} container
597
+ * @param {DataTableConfig} [config]
598
+ * @returns {DataTable}
599
+ */
600
+ static mount(container, config = {}) {
601
+ const table = new this(container, config);
602
+ table.render();
603
+ return table;
604
+ }
605
+ /**
606
+ * 0.5.0. A DataTable class with house defaults: `defaults` sit under every
607
+ * config it is constructed (or `mount`ed) with, and the config wins key by
608
+ * key. It is a real subclass — `instanceof DataTable` holds — and it can be
609
+ * narrowed again with its own `withDefaults`.
610
+ *
611
+ * const ListTable = DataTable.withDefaults({ mode: 'compact', sortable: true,
612
+ * pagination: false, fitContent: true });
613
+ * const table = ListTable.mount(host, { headers, rows });
614
+ *
615
+ * @param {DataTableConfig} defaults
616
+ * @returns {typeof DataTable}
617
+ */
618
+ static withDefaults(defaults = {}) {
619
+ const house = { ...defaults };
620
+ return class extends this {
621
+ constructor(container, config = {}) {
622
+ super(container, { ...house, ...config });
623
+ }
624
+ };
625
+ }
121
626
  /** Apply a persisted blob ({sort, asc, filters, widths}) onto this
122
627
  * table's live state. Returns true if anything was applied. */
123
628
  _restorePersisted(blob) {
@@ -178,6 +683,13 @@ var DataTable = class {
178
683
  this._state.offset = 0;
179
684
  }
180
685
  this._columnTypes = this._detectColumnTypes();
686
+ this._error = null;
687
+ }
688
+ if (Object.prototype.hasOwnProperty.call(updates, "activeRow")) {
689
+ this._activeKey = updates.activeRow == null ? null : String(updates.activeRow);
690
+ }
691
+ if (Object.prototype.hasOwnProperty.call(updates, "columnTypes") && !updates.rows) {
692
+ this._columnTypes = this._detectColumnTypes();
181
693
  }
182
694
  this._processedRows = null;
183
695
  this._processedIndexMap = null;
@@ -194,7 +706,12 @@ var DataTable = class {
194
706
  * @param {boolean} on
195
707
  */
196
708
  setLoading(on) {
709
+ this._loading = !!on;
197
710
  if (!this._wrapperEl) return;
711
+ this._wrapperEl.classList.toggle(
712
+ "twm-data-table-component--loading",
713
+ !!on && this.config.emptyState !== void 0
714
+ );
198
715
  let overlay = this._wrapperEl.querySelector(".twm-data-table__loading");
199
716
  if (on) {
200
717
  if (!overlay) {
@@ -225,12 +742,79 @@ var DataTable = class {
225
742
  this._state.selected.clear();
226
743
  for (const idx of indices) {
227
744
  if (idx >= 0 && idx < this.config.rows.length) {
745
+ if (this.config.selectable === "single") this._state.selected.clear();
228
746
  this._state.selected.add(idx);
229
747
  }
230
748
  }
749
+ if (this.config.selectable === "single") {
750
+ this._state.anchorIndex = this._state.selected.size ? [...this._state.selected][0] : null;
751
+ }
231
752
  this._updateRowSelection();
232
753
  this._notifySelectionChange();
233
754
  }
755
+ // ─────────────────────────────────────────────────────────────────
756
+ // 0.5.0 — the open row, the failure state, column widths
757
+ // ─────────────────────────────────────────────────────────────────
758
+ /**
759
+ * Mark the row that is OPEN — the master of a master-detail — by its key
760
+ * (`getRowKey`; without it, the row's original index). `null` clears it.
761
+ *
762
+ * The mark is NOT the selection: `setData` keeps it (the key is looked for
763
+ * in the new rows), a right-click does not move it, and a sort, a filter or
764
+ * a page change finds it where the row went. Drawn as `twm-dt-row--active`
765
+ * and `aria-current="true"` on the row, in place — no re-render.
766
+ *
767
+ * @param {*} key
768
+ */
769
+ setActiveRow(key) {
770
+ this._activeKey = key == null ? null : String(key);
771
+ this.config.activeRow = key ?? null;
772
+ const tbody = this._tbodyEl;
773
+ if (!tbody) return;
774
+ for (const tr of tbody.children) {
775
+ if (tr.__rowIndex === void 0) continue;
776
+ this._paintActive(tr);
777
+ }
778
+ }
779
+ /** The open row's key, as text, or null. */
780
+ getActiveRow() {
781
+ return this._activeKey;
782
+ }
783
+ /**
784
+ * Show a FAILURE inside the table's own box: text, a node, or an `Error`
785
+ * (its message). The rows already drawn STAY — a failed refresh does not
786
+ * blank a list a person was reading — under a banner saying what failed;
787
+ * with no rows, the failure takes the empty row's place. `setError(null)`
788
+ * clears it, and so does `setData({rows})`: new rows are an answer. A
789
+ * failure also ends `setLoading`.
790
+ *
791
+ * @param {string|Node|Error|null} error
792
+ */
793
+ setError(error) {
794
+ this._error = error == null || error === false ? null : error;
795
+ if (this._error) this.setLoading(false);
796
+ if (this._wrapperEl) this.render();
797
+ }
798
+ /** What `setError` was last given, or null. */
799
+ getError() {
800
+ return this._error;
801
+ }
802
+ /**
803
+ * Forget every column width a person dragged, a double-click fitted or the
804
+ * store restored, and measure the columns again from what is on screen —
805
+ * for an embedder whose content has just changed size under them (a zoom,
806
+ * a font). Persisted widths are cleared too.
807
+ *
808
+ * @returns {boolean} whether there was a table to measure
809
+ */
810
+ resetColumnWidths() {
811
+ this._colWidths = {};
812
+ this._colWidthsSig = this._colSig();
813
+ this._savePersisted();
814
+ if (!this._headerTableEl || !this._tableEl) return false;
815
+ this._syncHeaderWidths();
816
+ return true;
817
+ }
234
818
  /**
235
819
  * Clear selection
236
820
  */
@@ -274,6 +858,7 @@ var DataTable = class {
274
858
  }
275
859
  this._processedRows = null;
276
860
  this._processedIndexMap = null;
861
+ if (this.config.resetPageOnSort) this._state.offset = 0;
277
862
  const ret = this.config.onSort?.(colIndex, this._state.sortAscending);
278
863
  this._awaitWithSpinner(ret);
279
864
  this._savePersisted();
@@ -314,6 +899,7 @@ var DataTable = class {
314
899
  const prevWrap = this._tableWrapEl;
315
900
  const prevScroll = prevWrap?.isConnected && this._renderedOffset === this._state.offset ? { top: prevWrap.scrollTop, left: prevWrap.scrollLeft } : null;
316
901
  this._cleanup();
902
+ this._retireActivation();
317
903
  this.container.innerHTML = "";
318
904
  this._virt = null;
319
905
  const { rows, pagination, emptyMessage } = this.config;
@@ -327,21 +913,32 @@ var DataTable = class {
327
913
  }
328
914
  this._wrapperEl = document.createElement("div");
329
915
  this._wrapperEl.className = "twm-data-table-component" + (this.config.mode === "compact" ? " twm-data-table-component--compact" : "");
330
- this._wrapperEl.style.cssText = "display:flex; flex-direction:column; height:100%; min-height:0;";
916
+ const fit = this._fitsContent();
917
+ if (fit) {
918
+ this._wrapperEl.classList.add("twm-data-table-component--fit");
919
+ const max = this._maxHeightCss();
920
+ this._wrapperEl.style.cssText = "display:flex; flex-direction:column; height:auto; min-height:0;" + (max ? ` max-height:${max};` : "");
921
+ } else {
922
+ this._wrapperEl.style.cssText = "display:flex; flex-direction:column; height:100%; min-height:0;";
923
+ }
924
+ this._loading = false;
331
925
  if (pagination && rows.length > 0) {
332
926
  this._paginationEl = this._createPagination();
333
927
  if (this._paginationEl) this._wrapperEl.appendChild(this._paginationEl);
334
928
  }
335
929
  const tableWrap = document.createElement("div");
336
930
  tableWrap.className = this.config.readonly ? "twm-preview-table-wrap twm-preview-table-wrap--wizard" : "twm-preview-table-wrap";
337
- tableWrap.style.cssText = "flex:1 1 0; min-height:0; min-width:0; overflow:auto;";
931
+ tableWrap.style.cssText = fit ? "flex:0 1 auto; min-height:0; min-width:0; overflow:auto;" : "flex:1 1 0; min-height:0; min-width:0; overflow:auto;";
932
+ if (this._error && rows.length > 0) {
933
+ this._wrapperEl.appendChild(this._createErrorBanner());
934
+ }
338
935
  if (rows.length === 0) {
339
936
  const { headers, showRowNumbers } = this.config;
340
937
  const headerWrap = document.createElement("div");
341
938
  headerWrap.className = "twm-preview-table-header-wrap";
342
939
  headerWrap.style.cssText = "flex:0 0 auto; overflow:hidden; min-width:0;";
343
940
  const headerTable = document.createElement("table");
344
- headerTable.className = this.config.readonly ? "twm-preview-table twm-preview-table--readonly" : "twm-preview-table";
941
+ headerTable.className = this._tableClassName();
345
942
  const thead = document.createElement("thead");
346
943
  const tr = document.createElement("tr");
347
944
  if (showRowNumbers) {
@@ -370,8 +967,18 @@ var DataTable = class {
370
967
  const colCount = headers.length + (showRowNumbers ? 1 : 0);
371
968
  const emptyTd = document.createElement("td");
372
969
  emptyTd.colSpan = colCount;
373
- emptyTd.style.cssText = "text-align:center; font-style:italic; color:#888; padding:16px;";
374
- emptyTd.textContent = emptyMessage;
970
+ if (this._error) {
971
+ emptyTd.className = "twm-data-table__empty-cell twm-data-table__empty-cell--error";
972
+ emptyTd.setAttribute("role", "alert");
973
+ this._appendStateContent(emptyTd, this._errorContent());
974
+ } else if (this.config.emptyState !== void 0) {
975
+ emptyTd.className = "twm-data-table__empty-cell twm-data-table__empty-cell--state";
976
+ const state = typeof this.config.emptyState === "function" ? this.config.emptyState() : this.config.emptyState;
977
+ this._appendStateContent(emptyTd, state);
978
+ } else {
979
+ emptyTd.style.cssText = "text-align:center; font-style:italic; color:#888; padding:16px;";
980
+ emptyTd.textContent = emptyMessage;
981
+ }
375
982
  emptyTr.appendChild(emptyTd);
376
983
  tbody.appendChild(emptyTr);
377
984
  bodyTable.appendChild(tbody);
@@ -402,6 +1009,7 @@ var DataTable = class {
402
1009
  }
403
1010
  this._tableWrapEl = tableWrap;
404
1011
  this.container.appendChild(this._wrapperEl);
1012
+ if (this.config.autoDispose) autoDisposeWatch(this);
405
1013
  if (prevScroll && this._state.offset === this._renderedOffset) {
406
1014
  tableWrap.scrollTop = prevScroll.top;
407
1015
  tableWrap.scrollLeft = prevScroll.left;
@@ -432,7 +1040,7 @@ var DataTable = class {
432
1040
  this._installHeaderScrollSync();
433
1041
  if (rows.length > 0) this._installColumnResizers();
434
1042
  }
435
- if (rows.length > 0 && (this.config.selectable || this.config.copyable || !this.config.readonly)) {
1043
+ if (rows.length > 0 && (this.config.selectable || this.config.copyable || !this.config.readonly || typeof this.config.onRowActivate === "function")) {
436
1044
  this._installInteractions();
437
1045
  }
438
1046
  if (activeFilterColIdx !== null) {
@@ -526,9 +1134,26 @@ var DataTable = class {
526
1134
  this._notify("Download", "Download started", "info");
527
1135
  }
528
1136
  /**
529
- * Dispose and cleanup
1137
+ * Dispose and cleanup. Empties the host, as it always has.
530
1138
  */
531
1139
  dispose() {
1140
+ this._teardown(true);
1141
+ }
1142
+ /**
1143
+ * 0.5.0 `autoDispose`'s dispose: everything `dispose()` lets go of — but of
1144
+ * the host, only the table's OWN wrapper. A table that has been taken out
1145
+ * of the page no longer owns its host: in the task before this runs, the
1146
+ * consumer may have mounted the next list into it, or written its own
1147
+ * message there, and emptying the host would wipe that.
1148
+ */
1149
+ _disposeOwn() {
1150
+ this._teardown(false);
1151
+ }
1152
+ _teardown(emptyHost) {
1153
+ autoDisposeUnwatch(this);
1154
+ this._autoSeen = false;
1155
+ this._runOffs(this._activationOffs);
1156
+ this._activationOffs = [];
532
1157
  this._cleanup();
533
1158
  this._closeFilterDropdown();
534
1159
  this._teardownContextMenu();
@@ -547,7 +1172,8 @@ var DataTable = class {
547
1172
  this._onBodyScroll = null;
548
1173
  this._scrollSyncEl = null;
549
1174
  }
550
- this.container.innerHTML = "";
1175
+ if (emptyHost) this.container.innerHTML = "";
1176
+ else this._wrapperEl?.remove();
551
1177
  this._wrapperEl = null;
552
1178
  this._paginationEl = null;
553
1179
  this._tableEl = null;
@@ -592,12 +1218,32 @@ var DataTable = class {
592
1218
  for (const [colIdx, filterText] of filters) {
593
1219
  if (!filterText) continue;
594
1220
  const value = row[colIdx];
595
- const colType = this._columnTypes[colIdx];
1221
+ const colType = this._valueKind(colIdx);
596
1222
  if (colType === "num") {
597
1223
  if (!this._matchNumericFilter(value, filterText)) {
598
1224
  pass = false;
599
1225
  break;
600
1226
  }
1227
+ } else if (colType === "date" || colType === "datetime") {
1228
+ const utc = this._utc();
1229
+ if (!matchDateFilter(
1230
+ parseDateValue(value, { utc }),
1231
+ filterText,
1232
+ this._formatValue(value, colIdx),
1233
+ { utc }
1234
+ )) {
1235
+ pass = false;
1236
+ break;
1237
+ }
1238
+ } else if (colType === "duration") {
1239
+ if (!matchDurationFilter(
1240
+ parseDuration(value),
1241
+ filterText,
1242
+ this._formatValue(value, colIdx)
1243
+ )) {
1244
+ pass = false;
1245
+ break;
1246
+ }
601
1247
  } else {
602
1248
  if (!this._matchTextFilter(value, filterText)) {
603
1249
  pass = false;
@@ -615,6 +1261,32 @@ var DataTable = class {
615
1261
  _applySorting(rows, indexMap) {
616
1262
  const colIdx = this._state.sortColumn;
617
1263
  const asc = this._state.sortAscending;
1264
+ const hook = this._sortValueFor(colIdx);
1265
+ const kind = this._valueKind(colIdx);
1266
+ if (hook || TYPED_COLUMNS.has(kind)) {
1267
+ const utc = this._utc();
1268
+ const keyOf = (row) => {
1269
+ const value = row[colIdx];
1270
+ if (hook) {
1271
+ const k = hook(value, colIdx, row);
1272
+ if (k !== void 0) return normaliseSortKey(k);
1273
+ }
1274
+ if (kind === "date" || kind === "datetime") {
1275
+ return normaliseSortKey(parseDateValue(value, { utc }));
1276
+ }
1277
+ if (kind === "duration") return normaliseSortKey(parseDuration(value));
1278
+ if (kind === "num") {
1279
+ return normaliseSortKey(typeof value === "number" ? value : parseFloat(value));
1280
+ }
1281
+ return value == null ? null : String(value).toLowerCase();
1282
+ };
1283
+ const keyed = rows.map((row, i) => ({ row, origIdx: indexMap[i], key: keyOf(row) }));
1284
+ keyed.sort((a, b) => compareSortKeys(a.key, b.key, asc));
1285
+ return {
1286
+ rows: keyed.map((p) => p.row),
1287
+ indexMap: keyed.map((p) => p.origIdx)
1288
+ };
1289
+ }
618
1290
  const paired = rows.map((row, i) => ({ row, origIdx: indexMap[i] }));
619
1291
  const isNumeric = this._columnTypes[colIdx] === "num";
620
1292
  paired.sort((a, b) => {
@@ -721,8 +1393,8 @@ var DataTable = class {
721
1393
  const input = document.createElement("input");
722
1394
  input.type = "text";
723
1395
  input.className = "twm-data-table__filter-input";
724
- const isNumeric = this._columnTypes[colIdx] === "num";
725
- input.placeholder = isNumeric ? "e.g. >100" : "Filter...";
1396
+ const kind = this._valueKind(colIdx);
1397
+ input.placeholder = kind === "num" ? "e.g. >100" : kind === "date" || kind === "datetime" ? "e.g. >2026-01-01" : kind === "duration" ? "e.g. >1s" : "Filter...";
726
1398
  const existingFilter = this._state.filters.get(colIdx);
727
1399
  if (existingFilter) {
728
1400
  input.value = existingFilter;
@@ -765,17 +1437,28 @@ var DataTable = class {
765
1437
  // ─────────────────────────────────────────────────────────────────
766
1438
  _openFilterDropdown(colIdx, anchorEl, filterInput) {
767
1439
  this._closeFilterDropdown();
768
- const isNumeric = this._columnTypes[colIdx] === "num";
1440
+ const kind = this._valueKind(colIdx);
1441
+ const isNumeric = kind === "num";
1442
+ const isDate = kind === "date" || kind === "datetime";
1443
+ const isOrdered = isNumeric || isDate || kind === "duration";
769
1444
  const currentFilter = this._state.filters.get(colIdx) || "";
770
- const parsed = this._parseFilterForDropdown(currentFilter, isNumeric);
1445
+ const parsed = this._parseFilterForDropdown(currentFilter, isOrdered ? isNumeric ? true : kind : false);
771
1446
  const panel = document.createElement("div");
772
1447
  panel.className = "twm-data-table__filter-dropdown";
773
1448
  const selectLabel = document.createElement("label");
774
1449
  selectLabel.className = "twm-data-table__filter-dropdown-label";
775
- selectLabel.textContent = isNumeric ? "Operator" : "Mode";
1450
+ selectLabel.textContent = isOrdered ? "Operator" : "Mode";
776
1451
  const select = document.createElement("select");
777
1452
  select.className = "twm-data-table__filter-dropdown-select";
778
- const options = isNumeric ? [
1453
+ const options = isDate ? [
1454
+ { value: "=", label: "On / in" },
1455
+ { value: "!=", label: "Not on / in" },
1456
+ { value: ">", label: "After" },
1457
+ { value: ">=", label: "On or after" },
1458
+ { value: "<", label: "Before" },
1459
+ { value: "<=", label: "On or before" },
1460
+ { value: "..", label: "Between" }
1461
+ ] : isOrdered ? [
779
1462
  { value: "=", label: "Equals" },
780
1463
  { value: "!=", label: "Not equals" },
781
1464
  { value: ">", label: "Greater than" },
@@ -800,27 +1483,29 @@ var DataTable = class {
800
1483
  const valueLabel = document.createElement("label");
801
1484
  valueLabel.className = "twm-data-table__filter-dropdown-label";
802
1485
  valueLabel.textContent = "Value";
1486
+ const operandPlaceholder = isNumeric ? "Number..." : isDate ? "YYYY-MM-DD" : isOrdered ? "e.g. 1.5s" : "Text...";
803
1487
  const valueInput = document.createElement("input");
804
1488
  valueInput.type = isNumeric ? "number" : "text";
805
1489
  valueInput.className = "twm-data-table__filter-dropdown-input";
806
- valueInput.placeholder = isNumeric ? "Number..." : "Text...";
1490
+ valueInput.placeholder = operandPlaceholder;
807
1491
  valueInput.value = parsed.value;
808
1492
  const value2Label = document.createElement("label");
809
1493
  value2Label.className = "twm-data-table__filter-dropdown-label";
810
1494
  value2Label.textContent = "And";
811
1495
  const value2Input = document.createElement("input");
812
- value2Input.type = "number";
1496
+ value2Input.type = isOrdered && !isNumeric ? "text" : "number";
813
1497
  value2Input.className = "twm-data-table__filter-dropdown-input";
814
- value2Input.placeholder = "Number...";
1498
+ value2Input.placeholder = isOrdered && !isNumeric ? operandPlaceholder : "Number...";
815
1499
  value2Input.value = parsed.value2;
816
1500
  const value2Container = document.createElement("div");
817
1501
  value2Container.className = "twm-data-table__filter-dropdown-between";
818
- value2Container.style.display = isNumeric && parsed.operator === ".." ? "" : "none";
1502
+ value2Container.style.display = isOrdered && parsed.operator === ".." ? "" : "none";
819
1503
  value2Container.appendChild(value2Label);
820
1504
  value2Container.appendChild(value2Input);
821
1505
  select.addEventListener("change", () => {
822
- value2Container.style.display = isNumeric && select.value === ".." ? "" : "none";
1506
+ value2Container.style.display = isOrdered && select.value === ".." ? "" : "none";
823
1507
  });
1508
+ const composeMode = isNumeric ? true : isOrdered ? kind : false;
824
1509
  const actions = document.createElement("div");
825
1510
  actions.className = "twm-data-table__filter-dropdown-actions";
826
1511
  const clearBtn = document.createElement("button");
@@ -839,7 +1524,7 @@ var DataTable = class {
839
1524
  applyBtn.textContent = "Apply";
840
1525
  applyBtn.addEventListener("click", (e) => {
841
1526
  e.stopPropagation();
842
- const composed = this._composeFilterString(select.value, valueInput.value, value2Input.value, isNumeric);
1527
+ const composed = this._composeFilterString(select.value, valueInput.value, value2Input.value, composeMode);
843
1528
  filterInput.value = composed;
844
1529
  this._onFilterInput(colIdx, composed);
845
1530
  this._closeFilterDropdown();
@@ -879,7 +1564,7 @@ var DataTable = class {
879
1564
  const handleEnter = (e) => {
880
1565
  if (e.key === "Enter") {
881
1566
  e.preventDefault();
882
- const composed = this._composeFilterString(select.value, valueInput.value, value2Input.value, isNumeric);
1567
+ const composed = this._composeFilterString(select.value, valueInput.value, value2Input.value, composeMode);
883
1568
  filterInput.value = composed;
884
1569
  this._onFilterInput(colIdx, composed);
885
1570
  this._closeFilterDropdown();
@@ -909,6 +1594,13 @@ var DataTable = class {
909
1594
  if (!text) {
910
1595
  return { operator: isNumeric ? "=" : "contains", value: "", value2: "" };
911
1596
  }
1597
+ if (typeof isNumeric === "string") {
1598
+ const range = text.match(/^(.+?)\s*\.\.\s*(.+)$/);
1599
+ if (range) return { operator: "..", value: range[1], value2: range[2] };
1600
+ const op = text.match(/^(>=|<=|!=|>|<|=)\s*(.+)$/);
1601
+ if (op) return { operator: op[1], value: op[2], value2: "" };
1602
+ return { operator: "=", value: text, value2: "" };
1603
+ }
912
1604
  if (isNumeric) {
913
1605
  const rangeMatch = text.match(/^(-?[\d.]+)\.\.(-?[\d.]+)$/);
914
1606
  if (rangeMatch) {
@@ -936,6 +1628,10 @@ var DataTable = class {
936
1628
  }
937
1629
  _composeFilterString(operator, value, value2, isNumeric) {
938
1630
  if (!value && operator !== "..") return "";
1631
+ if (typeof isNumeric === "string") {
1632
+ if (operator === "..") return value && value2 ? `${value}..${value2}` : "";
1633
+ return `${operator}${value}`;
1634
+ }
939
1635
  if (isNumeric) {
940
1636
  if (operator === "..") {
941
1637
  return value && value2 ? `${value}..${value2}` : "";
@@ -1004,6 +1700,30 @@ var DataTable = class {
1004
1700
  // ─────────────────────────────────────────────────────────────────
1005
1701
  // Private methods
1006
1702
  // ─────────────────────────────────────────────────────────────────
1703
+ /** Run (and forget) a list of removers. */
1704
+ _runOffs(offs) {
1705
+ for (const off of offs || []) {
1706
+ try {
1707
+ off?.();
1708
+ } catch (_) {
1709
+ }
1710
+ }
1711
+ }
1712
+ /**
1713
+ * 0.5.0. A render takes `onRowActivate`'s listeners off the body it is
1714
+ * replacing ONE TASK LATER, not now. The render may be running INSIDE the
1715
+ * very click that is about to open a row — an `onSelectionChange` that calls
1716
+ * `setData`, an `onRowClick` that calls `render` — and a listener removed
1717
+ * during its own event's dispatch is skipped: the row silently did not
1718
+ * open. The event's path was fixed when it was dispatched, so it still
1719
+ * reaches the old body, whose listener opens the row that was pressed. The
1720
+ * old body is out of the page, so nothing else can reach it meanwhile.
1721
+ */
1722
+ _retireActivation() {
1723
+ const offs = this._activationOffs;
1724
+ this._activationOffs = [];
1725
+ if (offs.length) setTimeout(() => this._runOffs(offs), 0);
1726
+ }
1007
1727
  _cleanup() {
1008
1728
  this._disposers.forEach((dispose) => {
1009
1729
  try {
@@ -1019,9 +1739,14 @@ var DataTable = class {
1019
1739
  }
1020
1740
  }
1021
1741
  _detectColumnTypes() {
1022
- const { headers, rows, getColumnType } = this.config;
1742
+ const { headers, rows, getColumnType, columnTypes } = this.config;
1023
1743
  const types = [];
1024
1744
  for (let colIdx = 0; colIdx < headers.length; colIdx++) {
1745
+ const declared = Array.isArray(columnTypes) ? columnTypes[colIdx] : null;
1746
+ if (declared) {
1747
+ types.push(declared);
1748
+ continue;
1749
+ }
1025
1750
  if (getColumnType) {
1026
1751
  types.push(getColumnType(colIdx, rows));
1027
1752
  continue;
@@ -1167,7 +1892,7 @@ var DataTable = class {
1167
1892
  const offset = isServerSide ? configOffset || 0 : this._state.offset;
1168
1893
  const pageRows = isServerSide || !this.config.pagination ? processedRows : processedRows.slice(offset, offset + pageSize);
1169
1894
  const table = document.createElement("table");
1170
- table.className = readonly ? "twm-preview-table twm-preview-table--readonly" : "twm-preview-table";
1895
+ table.className = this._tableClassName();
1171
1896
  table.tabIndex = 0;
1172
1897
  const thead = document.createElement("thead");
1173
1898
  const headerRow = document.createElement("tr");
@@ -1227,6 +1952,7 @@ var DataTable = class {
1227
1952
  const processedIdx = offset + localIdx;
1228
1953
  return isServerSide ? (configOffset || 0) + localIdx : this._processedIndexMap?.[processedIdx] ?? processedIdx;
1229
1954
  };
1955
+ this._positionBase = isServerSide ? configOffset || 0 : this.config.pagination ? offset : 0;
1230
1956
  const tbody = document.createElement("tbody");
1231
1957
  table.appendChild(tbody);
1232
1958
  this._tbodyEl = tbody;
@@ -1250,10 +1976,12 @@ var DataTable = class {
1250
1976
  const { showRowNumbers } = this.config;
1251
1977
  const tr = document.createElement("tr");
1252
1978
  tr.__rowIndex = globalIdx;
1979
+ tr.__position = (this._positionBase || 0) + localIdx;
1253
1980
  tr.className = "data-preview-row";
1254
1981
  if (this._state.selected.has(globalIdx)) {
1255
1982
  tr.classList.add("selected");
1256
1983
  }
1984
+ this._decorateRow(tr, row, globalIdx);
1257
1985
  if (this._virt) tr.classList.toggle("twm-dt-row--alt", localIdx % 2 === 1);
1258
1986
  this._fillRowCells(tr, row, globalIdx, showRowNumbers);
1259
1987
  if (this.config.onRowClick) {
@@ -1678,21 +2406,33 @@ var DataTable = class {
1678
2406
  if (showRowNumbers) {
1679
2407
  const td = document.createElement("td");
1680
2408
  td.className = "num";
1681
- td.textContent = String(globalIdx + 1);
2409
+ const byPosition = this.config.showRowNumbers === "position" && Number.isInteger(tr.__position);
2410
+ td.textContent = String((byPosition ? tr.__position : globalIdx) + 1);
1682
2411
  tr.appendChild(td);
1683
2412
  }
2413
+ const { cellClass, nullDisplay } = this.config;
1684
2414
  for (let colIdx = 0; colIdx < row.length; colIdx++) {
1685
2415
  const td = document.createElement("td");
1686
2416
  td.className = this._columnTypes[colIdx] || "text";
1687
2417
  const value = row[colIdx];
2418
+ let handled = false;
1688
2419
  if (this.config.renderCell) {
1689
- const handled = this.config.renderCell(td, value, colIdx, globalIdx, row);
2420
+ handled = !!this.config.renderCell(td, value, colIdx, globalIdx, row);
1690
2421
  if (!handled) {
1691
2422
  td.textContent = this._formatValue(value, colIdx);
1692
2423
  }
1693
2424
  } else {
1694
2425
  td.textContent = this._formatValue(value, colIdx);
1695
2426
  }
2427
+ if (!handled && value == null && nullDisplay != null) {
2428
+ td.classList.add("twm-dt-cell--null");
2429
+ }
2430
+ if (typeof cellClass === "function") {
2431
+ this._addClasses(td, cellClass(value, colIdx, row, globalIdx));
2432
+ }
2433
+ if (colIdx === 0 && typeof this.config.rowIcon === "function") {
2434
+ this._prependRowIcon(td, this.config.rowIcon(row, globalIdx));
2435
+ }
1696
2436
  if (this.config.onCellContextMenu) {
1697
2437
  const cellColIdx = colIdx;
1698
2438
  td.addEventListener("contextmenu", (ev) => {
@@ -1776,11 +2516,302 @@ var DataTable = class {
1776
2516
  });
1777
2517
  }
1778
2518
  tr.classList.toggle("selected", this._state?.selected?.has(index) === true);
2519
+ this._decorateRow(tr, row, index);
1779
2520
  if (focusedCol >= 0 && tr.children[focusedCol]) {
1780
2521
  tr.children[focusedCol].focus?.();
1781
2522
  }
1782
2523
  return true;
1783
2524
  }
2525
+ // ─────────────────────────────────────────────────────────────────
2526
+ // 0.5.0 — decoration, states and the typed columns
2527
+ // ─────────────────────────────────────────────────────────────────
2528
+ /** The class list both halves of the table carry: the 0.4 pair, plus the
2529
+ * 0.5.0 modifiers a consumer asked for. */
2530
+ _tableClassName() {
2531
+ const names = this.config.readonly ? ["twm-preview-table", "twm-preview-table--readonly"] : ["twm-preview-table"];
2532
+ if (this.config.firstColumn === "plain") names.push("twm-dt--first-plain");
2533
+ if (this._isClickable()) names.push("twm-dt--clickable");
2534
+ return names.join(" ");
2535
+ }
2536
+ /** `clickable` when it is said; otherwise exactly when rows open. */
2537
+ _isClickable() {
2538
+ const c = this.config.clickable;
2539
+ if (c === true || c === false) return c;
2540
+ return typeof this.config.onRowActivate === "function";
2541
+ }
2542
+ _fitsContent() {
2543
+ return !!this.config.fitContent || this._maxHeightCss() !== null;
2544
+ }
2545
+ _maxHeightCss() {
2546
+ const m = this.config.maxHeight;
2547
+ if (m == null || m === "" || m === false) return null;
2548
+ if (typeof m === "number") return Number.isFinite(m) && m > 0 ? `${m}px` : null;
2549
+ return String(m);
2550
+ }
2551
+ _utc() {
2552
+ return String(this.config.dateTimeZone || "").toUpperCase() === "UTC";
2553
+ }
2554
+ /**
2555
+ * What a column is drawn, sorted and filtered BY: its type, except that a
2556
+ * 'date', 'datetime' or 'duration' counts only when it was DECLARED through
2557
+ * `columnTypes`. In 0.4, whatever `getColumnType` returned was the cells'
2558
+ * class name and nothing more (only 'num' changed the sort and the
2559
+ * filter), so a consumer that returned 'date' there to style its cells gets
2560
+ * exactly 0.4 — its `formatValue`, its text sort, its text filter — and
2561
+ * the class name it asked for.
2562
+ */
2563
+ _valueKind(colIdx) {
2564
+ const t = this._columnTypes?.[colIdx];
2565
+ if (!TYPED_COLUMNS.has(t)) return t;
2566
+ const declared = Array.isArray(this.config.columnTypes) ? this.config.columnTypes[colIdx] : null;
2567
+ return declared === t ? t : "text";
2568
+ }
2569
+ /** `classList.add` for whatever a hook returned: a string (space-separated
2570
+ * names allowed), an array of them, or nothing. */
2571
+ _addClasses(el, names) {
2572
+ if (!names) return [];
2573
+ const list = (Array.isArray(names) ? names : String(names).split(/\s+/)).map((n) => String(n || "").trim()).filter(Boolean);
2574
+ if (list.length) el.classList.add(...list);
2575
+ return list;
2576
+ }
2577
+ /** A row's key, as text, or null: `getRowKey`, else its original index. */
2578
+ _rowKey(row, rowIdx) {
2579
+ const fn = this.config.getRowKey;
2580
+ let key;
2581
+ if (typeof fn === "function") {
2582
+ try {
2583
+ key = fn(row, rowIdx);
2584
+ } catch (err) {
2585
+ console.error("[DataTable] getRowKey threw", err);
2586
+ key = null;
2587
+ }
2588
+ } else {
2589
+ key = rowIdx;
2590
+ }
2591
+ return key == null ? null : String(key);
2592
+ }
2593
+ /** The gestures `activateOn` names, as a set. */
2594
+ _activateOn() {
2595
+ const a = this.config.activateOn;
2596
+ const list = Array.isArray(a) ? a : [a || "click"];
2597
+ return new Set(list.map((g) => String(g).toLowerCase()));
2598
+ }
2599
+ /**
2600
+ * Open a row on the gestures `activateOn` names, by the one rule in
2601
+ * `row_activation.js` (a control is its own gesture, a drag that selects
2602
+ * text is not a click, the second click of a double-click opens nothing).
2603
+ * Delegated on the body, bound after the selection handler.
2604
+ *
2605
+ * The listeners go in `_activationOffs`, not `_disposers`, and a render
2606
+ * retires them a task late (`_retireActivation`): a press whose selection
2607
+ * callback re-draws the table still opens the row it pressed — the row
2608
+ * as it was drawn (`tr.__row`), with its original index.
2609
+ */
2610
+ _installActivation(tbody, table) {
2611
+ const on = this._activateOn();
2612
+ const scope = this._wrapperEl;
2613
+ const sel = this.config.selectable;
2614
+ const multi = !!sel && sel !== "single";
2615
+ const fire = (tr, ev) => {
2616
+ if (!this._wrapperEl || typeof this.config.onRowActivate !== "function") return;
2617
+ try {
2618
+ this.config.onRowActivate(tr.__rowIndex, tr.__row, ev);
2619
+ } catch (err) {
2620
+ console.error("[DataTable] onRowActivate threw", err);
2621
+ }
2622
+ };
2623
+ const rowFor = (ev) => {
2624
+ const tr = ev.target?.closest?.("tr");
2625
+ return tr && tr.__rowIndex !== void 0 && tbody.contains(tr) ? tr : null;
2626
+ };
2627
+ const listen = (el, type, fn) => {
2628
+ el.addEventListener(type, fn);
2629
+ this._activationOffs.push(() => el.removeEventListener(type, fn));
2630
+ };
2631
+ if (on.has("click")) {
2632
+ listen(tbody, "click", (ev) => {
2633
+ if (ev.button != null && ev.button !== 0) return;
2634
+ const tr = rowFor(ev);
2635
+ if (!tr || !isRowActivation(ev, "click", { scope })) return;
2636
+ if (multi && (ev.shiftKey || ev.ctrlKey || ev.metaKey)) return;
2637
+ if (DESELECTING_CLICKS.has(ev)) return;
2638
+ fire(tr, ev);
2639
+ });
2640
+ }
2641
+ if (on.has("dblclick")) {
2642
+ listen(tbody, "dblclick", (ev) => {
2643
+ const tr = rowFor(ev);
2644
+ if (!tr || !isRowActivation(ev, "dblclick", { scope })) return;
2645
+ fire(tr, ev);
2646
+ });
2647
+ }
2648
+ if (on.has("enter")) {
2649
+ listen(table, "keydown", (ev) => {
2650
+ if (ev.key !== "Enter" || ev.isComposing) return;
2651
+ if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.shiftKey) return;
2652
+ if (isRowControl(ev.target, table)) return;
2653
+ const tr = this._currentRowEl();
2654
+ if (!tr) return;
2655
+ ev.preventDefault();
2656
+ fire(tr, ev);
2657
+ });
2658
+ }
2659
+ }
2660
+ /**
2661
+ * The row Enter opens: the SELECTED one — the one row selected, or the
2662
+ * selection's anchor — because that is the row a person just chose (a
2663
+ * double-click opens the row it lands on, and Enter must agree with it);
2664
+ * the open row (`activeRow`) only when nothing is selected. A selected row
2665
+ * that is not drawn (another page) opens nothing rather than a different
2666
+ * row.
2667
+ */
2668
+ _currentRowEl() {
2669
+ const tbody = this._tbodyEl;
2670
+ if (!tbody) return null;
2671
+ const selected = this._state.selected;
2672
+ let idx = null;
2673
+ if (selected.size === 1) idx = [...selected][0];
2674
+ else if (this._state.anchorIndex != null && selected.has(this._state.anchorIndex)) {
2675
+ idx = this._state.anchorIndex;
2676
+ }
2677
+ if (idx != null) return this.getRowElement(idx);
2678
+ if (this._activeKey !== null) {
2679
+ for (const tr of tbody.children) {
2680
+ if (tr.__rowIndex !== void 0 && tr.classList.contains("twm-dt-row--active")) return tr;
2681
+ }
2682
+ }
2683
+ return null;
2684
+ }
2685
+ /** Everything `rowClass`, `rowAttrs` and the open mark put on a `<tr>`,
2686
+ * taken off again first — so `updateRow` cannot leave last state's class
2687
+ * behind. */
2688
+ _decorateRow(tr, row, rowIdx) {
2689
+ tr.__row = row;
2690
+ const { rowClass, rowAttrs } = this.config;
2691
+ if (tr.__twmClasses) {
2692
+ for (const c of tr.__twmClasses) tr.classList.remove(c);
2693
+ tr.__twmClasses = null;
2694
+ }
2695
+ if (typeof rowClass === "function") {
2696
+ const added = this._addClasses(tr, rowClass(row, rowIdx));
2697
+ tr.__twmClasses = added.filter((c) => c !== "data-preview-row" && c !== "selected");
2698
+ }
2699
+ if (tr.__twmAttrs) {
2700
+ for (const name of tr.__twmAttrs) tr.removeAttribute(name);
2701
+ tr.__twmAttrs = null;
2702
+ }
2703
+ if (typeof rowAttrs === "function") {
2704
+ const attrs = rowAttrs(row, rowIdx) || {};
2705
+ const set = [];
2706
+ for (const [name, value] of Object.entries(attrs)) {
2707
+ if (value == null || value === false) continue;
2708
+ try {
2709
+ tr.setAttribute(name, value === true ? "" : String(value));
2710
+ set.push(name);
2711
+ } catch (err) {
2712
+ console.error(`[DataTable] rowAttrs: bad attribute "${name}"`, err);
2713
+ }
2714
+ }
2715
+ tr.__twmAttrs = set;
2716
+ }
2717
+ if (typeof this.config.getRowKey === "function" || this._activeKey !== null) {
2718
+ tr.__rowKey = this._rowKey(row, rowIdx);
2719
+ }
2720
+ this._paintActive(tr);
2721
+ }
2722
+ /** The open mark on one row, from `_activeKey`. */
2723
+ _paintActive(tr) {
2724
+ if (tr.__rowKey === void 0 && this._activeKey !== null && tr.__rowIndex !== void 0) {
2725
+ const rows = this.config.rows;
2726
+ const local = this._virt ? this._virtLocalOf(tr.__rowIndex) : -1;
2727
+ const row = local >= 0 ? this._virt.rows[local] : rows?.[tr.__rowIndex];
2728
+ tr.__rowKey = this._rowKey(row, tr.__rowIndex);
2729
+ }
2730
+ const on = this._activeKey !== null && tr.__rowKey === this._activeKey;
2731
+ tr.classList.toggle("twm-dt-row--active", on);
2732
+ if (on) {
2733
+ if (tr.getAttribute("aria-current") !== "true") {
2734
+ tr.setAttribute("aria-current", "true");
2735
+ tr.__twmAriaCurrent = true;
2736
+ }
2737
+ } else if (tr.__twmAriaCurrent) {
2738
+ tr.__twmAriaCurrent = false;
2739
+ if (!(tr.__twmAttrs || []).includes("aria-current")) tr.removeAttribute("aria-current");
2740
+ }
2741
+ }
2742
+ /** The row icon, drawn by CSS from `data-twm-icon` so that its name is in
2743
+ * neither `textContent`, the clipped-cell tooltip nor a copy. */
2744
+ _prependRowIcon(td, spec) {
2745
+ if (!spec) return;
2746
+ const s = typeof spec === "string" ? { icon: spec } : spec;
2747
+ if (!s.icon) return;
2748
+ const icon = document.createElement("span");
2749
+ icon.className = "material-symbols-outlined twm-dt-row-icon" + (s.tone ? ` twm-dt-row-icon--${String(s.tone).replace(/[^\w-]/g, "")}` : "");
2750
+ icon.dataset.twmIcon = String(s.icon);
2751
+ if (s.title) {
2752
+ icon.title = String(s.title);
2753
+ icon.setAttribute("role", "img");
2754
+ icon.setAttribute("aria-label", String(s.title));
2755
+ } else {
2756
+ icon.setAttribute("aria-hidden", "true");
2757
+ }
2758
+ td.insertBefore(icon, td.firstChild);
2759
+ td.classList.add("twm-dt-cell--has-icon");
2760
+ }
2761
+ /** Text, a node, or nothing, into an empty-state cell. */
2762
+ _appendStateContent(td, content) {
2763
+ if (content == null || content === false) return;
2764
+ if (typeof Node !== "undefined" && content instanceof Node) td.appendChild(content);
2765
+ else td.textContent = String(content);
2766
+ }
2767
+ /** What `setError` holds, as something `_appendStateContent` can draw. */
2768
+ _errorContent() {
2769
+ const e = this._error;
2770
+ if (e == null) return null;
2771
+ if (typeof Node !== "undefined" && e instanceof Node) return e;
2772
+ if (e instanceof Error) return e.message || String(e);
2773
+ if (typeof e === "object" && e.message) return String(e.message);
2774
+ return String(e);
2775
+ }
2776
+ /** The failure banner over rows that are kept. */
2777
+ _createErrorBanner() {
2778
+ const banner = document.createElement("div");
2779
+ banner.className = "twm-data-table__error";
2780
+ banner.setAttribute("role", "alert");
2781
+ const icon = document.createElement("span");
2782
+ icon.className = "material-symbols-outlined twm-data-table__error-icon";
2783
+ icon.setAttribute("aria-hidden", "true");
2784
+ icon.textContent = "error";
2785
+ const text = document.createElement("span");
2786
+ text.className = "twm-data-table__error-text";
2787
+ const content = this._errorContent();
2788
+ if (typeof Node !== "undefined" && content instanceof Node) text.appendChild(content);
2789
+ else text.textContent = String(content ?? "");
2790
+ banner.append(icon, text);
2791
+ return banner;
2792
+ }
2793
+ /** The `sortValue` hook for one column, or null. */
2794
+ _sortValueFor(colIdx) {
2795
+ const sv = this.config.sortValue;
2796
+ if (typeof sv === "function") return sv;
2797
+ const fn = perColumn(sv, colIdx);
2798
+ return typeof fn === "function" ? fn : null;
2799
+ }
2800
+ /** The words for a value in a typed column, or `undefined` when the
2801
+ * column is not typed (or the value is empty). */
2802
+ _formatTyped(value, colIdx) {
2803
+ const kind = this._valueKind(colIdx);
2804
+ if (!TYPED_COLUMNS.has(kind) || value == null || value === "") return void 0;
2805
+ if (kind === "duration") {
2806
+ return formatDuration(value, perColumn(this.config.durationFormat, colIdx) || "auto");
2807
+ }
2808
+ const pattern = kind === "datetime" ? perColumn(this.config.dateTimeFormat, colIdx) || ISO_DATETIME : perColumn(this.config.dateFormat, colIdx) || ISO_DATE;
2809
+ if (typeof pattern === "function") {
2810
+ const t = parseDateValue(value, { utc: this._utc() });
2811
+ return Number.isFinite(t) ? String(pattern(new Date(t), colIdx, value) ?? "") : String(value);
2812
+ }
2813
+ return formatDate(value, pattern, { utc: this._utc() });
2814
+ }
1784
2815
  /** Sync the (separate) header table's column widths to the body
1785
2816
  * table's measured widths. Without this, the two tables compute
1786
2817
  * widths independently and the columns drift apart. Locks both
@@ -2391,7 +3422,13 @@ var DataTable = class {
2391
3422
  if (!rowEl || rowEl.__rowIndex === void 0) return;
2392
3423
  const idx = rowEl.__rowIndex;
2393
3424
  const selected = this._state.selected;
2394
- if (event.shiftKey && this._state.anchorIndex != null) {
3425
+ if (selectable === "single") {
3426
+ const was = selected.has(idx);
3427
+ selected.clear();
3428
+ if (!(was && (event.metaKey || event.ctrlKey))) selected.add(idx);
3429
+ else DESELECTING_CLICKS.add(event);
3430
+ this._state.anchorIndex = selected.size ? idx : null;
3431
+ } else if (event.shiftKey && this._state.anchorIndex != null) {
2395
3432
  const start = Math.min(this._state.anchorIndex, idx);
2396
3433
  const end = Math.max(this._state.anchorIndex, idx);
2397
3434
  selected.clear();
@@ -2415,6 +3452,9 @@ var DataTable = class {
2415
3452
  tbody.addEventListener("click", handleRowClick);
2416
3453
  this._disposers.push(() => tbody.removeEventListener("click", handleRowClick));
2417
3454
  }
3455
+ if (typeof this.config.onRowActivate === "function") {
3456
+ this._installActivation(tbody, table);
3457
+ }
2418
3458
  if (copyable) {
2419
3459
  const handleContextMenu = (event) => {
2420
3460
  const rowEl = event.target.closest("tr");
@@ -2444,7 +3484,9 @@ var DataTable = class {
2444
3484
  const ctrlLike = event.ctrlKey || event.metaKey;
2445
3485
  if (ctrlLike && !event.altKey) {
2446
3486
  const key = String(event.key || "").toLowerCase();
2447
- if (key === "a" && selectable) {
3487
+ if (key === "a" && selectable === "single") {
3488
+ event.preventDefault();
3489
+ } else if (key === "a" && selectable) {
2448
3490
  event.preventDefault();
2449
3491
  this._state.selected.clear();
2450
3492
  const indexMap = this._processedIndexMap;
@@ -2518,8 +3560,23 @@ var DataTable = class {
2518
3560
  e.preventDefault();
2519
3561
  e.stopPropagation();
2520
3562
  });
3563
+ this._contextMenuEl = menu;
3564
+ return menu;
3565
+ }
3566
+ /**
3567
+ * The document-level listeners that close the copy menu — a click
3568
+ * elsewhere, a scroll, Escape, a resize — held ONLY WHILE IT IS OPEN
3569
+ * (0.5.0). They were installed the first time the menu was built and kept
3570
+ * until `dispose()`, so every table anybody had ever right-clicked kept four
3571
+ * listeners on the document for as long as the instance lived, and an
3572
+ * instance whose host was thrown away without a `dispose()` kept them for
3573
+ * the life of the page. A closed menu has nothing for them to do.
3574
+ */
3575
+ _armContextMenuGlobals() {
3576
+ if (this._contextMenuGlobals.length) return;
3577
+ const menu = this._contextMenuEl;
2521
3578
  const hideOnGlobal = (event) => {
2522
- if (event?.target && menu.contains(event.target)) return;
3579
+ if (event?.target && menu?.contains(event.target)) return;
2523
3580
  this._hideContextMenu();
2524
3581
  };
2525
3582
  const hideOnEscape = (event) => {
@@ -2534,16 +3591,25 @@ var DataTable = class {
2534
3591
  this._contextMenuGlobals.push(() => document.removeEventListener("scroll", hideOnGlobal, true));
2535
3592
  this._contextMenuGlobals.push(() => document.removeEventListener("keydown", hideOnEscape));
2536
3593
  this._contextMenuGlobals.push(() => window.removeEventListener("resize", hideOnResize));
2537
- this._contextMenuEl = menu;
2538
- return menu;
3594
+ }
3595
+ _disarmContextMenuGlobals() {
3596
+ this._contextMenuGlobals.forEach((off) => {
3597
+ try {
3598
+ off?.();
3599
+ } catch (_) {
3600
+ }
3601
+ });
3602
+ this._contextMenuGlobals = [];
2539
3603
  }
2540
3604
  _hideContextMenu() {
2541
3605
  if (this._contextMenuEl) {
2542
3606
  this._contextMenuEl.style.display = "none";
2543
3607
  }
3608
+ this._disarmContextMenuGlobals();
2544
3609
  }
2545
3610
  _showContextMenu(clientX, clientY) {
2546
3611
  const menu = this._ensureContextMenu();
3612
+ this._armContextMenuGlobals();
2547
3613
  const hasSelection = this._state.selected.size > 0;
2548
3614
  menu.querySelectorAll(".twm-context-menu-item").forEach((item) => {
2549
3615
  item.classList.toggle("disabled", !hasSelection);
@@ -2576,6 +3642,11 @@ var DataTable = class {
2576
3642
  this._contextMenuEl = null;
2577
3643
  }
2578
3644
  _formatValue(value, colIndex) {
3645
+ if (value == null && this.config.nullDisplay != null) {
3646
+ return String(this.config.nullDisplay);
3647
+ }
3648
+ const typed = this._formatTyped(value, colIndex);
3649
+ if (typed !== void 0) return typed;
2579
3650
  if (this.config.formatValue) {
2580
3651
  return this.config.formatValue(value, colIndex);
2581
3652
  }
@@ -2617,6 +3688,15 @@ var DataTable = class {
2617
3688
 
2618
3689
  export {
2619
3690
  createRafResizeObserver,
3691
+ ISO_DATE,
3692
+ ISO_DATETIME,
3693
+ parseDateValue,
3694
+ formatDate,
3695
+ parseDatePeriod,
3696
+ parseDuration,
3697
+ formatDuration,
3698
+ matchDateFilter,
3699
+ matchDurationFilter,
2620
3700
  DataTable
2621
3701
  };
2622
- //# sourceMappingURL=chunk-CYTWMP2Y.js.map
3702
+ //# sourceMappingURL=chunk-KH2GJTKR.js.map