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.
- package/README.md +140 -0
- package/css/base.css +123 -2
- package/css/flexdesk.css +127 -4
- package/css/overrides.css +4 -2
- package/css/tokens.css +14 -0
- package/dist/charts.js +3 -3
- package/dist/{chunk-ELXVW542.js → chunk-24MS7W5Y.js} +2 -2
- package/dist/{chunk-LH5TSOZW.js → chunk-6JSOVNID.js} +42 -2
- package/dist/{chunk-LH5TSOZW.js.map → chunk-6JSOVNID.js.map} +3 -3
- package/dist/{chunk-HWCX5I4R.js → chunk-CNXP3SIN.js} +3 -3
- package/dist/{chunk-CYTWMP2Y.js → chunk-KH2GJTKR.js} +1112 -32
- package/dist/chunk-KH2GJTKR.js.map +7 -0
- package/dist/flexdesk.css +127 -4
- package/dist/tiles.js +3 -3
- package/dist/tokens.css +14 -0
- package/dist/widgets.js +31 -5
- package/dist/widgets.js.map +1 -1
- package/dist/wm.js +79 -19
- package/dist/wm.js.map +3 -3
- package/package.json +1 -1
- package/src/tiling/landing_table.js +13 -5
- package/src/tiling/tile_breadcrumb.js +21 -0
- package/src/tiling/tile_renderer.js +11 -0
- package/src/tiling/wm.js +62 -18
- package/src/ui/components/data_table.js +961 -37
- package/src/ui/components/row_activation.js +70 -0
- package/src/ui/components/table_values.js +423 -0
- package/dist/chunk-CYTWMP2Y.js.map +0 -7
- /package/dist/{chunk-ELXVW542.js.map → chunk-24MS7W5Y.js.map} +0 -0
- /package/dist/{chunk-HWCX5I4R.js.map → chunk-CNXP3SIN.js.map} +0 -0
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
374
|
-
|
|
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.
|
|
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
|
|
725
|
-
input.placeholder =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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,
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
2538
|
-
|
|
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-
|
|
3702
|
+
//# sourceMappingURL=chunk-KH2GJTKR.js.map
|