@xiee/utils 1.14.91 → 1.14.93

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.
@@ -5,8 +5,9 @@
5
5
  * table whose spec carries `interactive`, it adds the requested controls and
6
6
  * re-renders <tbody> through the core `spec._viewRows` seam. Every control is a
7
7
  * row of the table itself — the search box in <thead>, the pager in <tfoot> —
8
- * so it is exactly as wide as the table and scrolls with it. Flat rows only (no
9
- * row groups or indentation); such tables stay static, with a console warning.
8
+ * so it is exactly as wide as the table and scrolls with it. Row structure is
9
+ * honored: separator row groups, indentation, and rowspan row groups all
10
+ * sort/filter within each group or subtree, never across it (see computeView).
10
11
  */
11
12
  (root => {
12
13
  "use strict";
@@ -25,12 +26,25 @@
25
26
  // explicit `{col, dir}` object. Normalizes either to `{col, dir}`.
26
27
  const parseKey = k => typeof k !== "string" ? { ...k } :
27
28
  k[0] === "-" ? { col: k.slice(1), dir: "desc" } : { col: k, dir: "asc" };
29
+ // Compare two non-null values: numeric subtraction for a numeric column, else
30
+ // a locale-aware string compare (the collator built once, lazily reused).
31
+ const cmpVals = (x, y, num) => {
32
+ coll ||= new Intl.Collator();
33
+ return num ? x - y : coll.compare(String(x), String(y));
34
+ };
28
35
 
29
36
  // --- Small DOM helpers, so building the controls stays terse ---
30
37
  const $ = (el, sel) => el.querySelector(sel),
31
38
  $$ = (el, sel) => el.querySelectorAll(sel),
32
39
  on = (t, type, fn, opts) => t.addEventListener(type, fn, opts),
33
40
  off = (t, type, fn) => t.removeEventListener(type, fn);
41
+ // A row's data cells, in column order: its children minus the leading rowspan
42
+ // group-column cells (class `lt-row-group`), which are not in `spec._cols` and,
43
+ // in rowspan mode, appear on some rows but not others (a run's later rows have
44
+ // none). Selecting by class — not position — is what lets the positional code
45
+ // (hide, detail, display capture) stay correct whatever leads the row.
46
+ const dataCells = tr =>
47
+ [...tr.children].filter(c => !c.classList.contains("lt-row-group"));
34
48
  // Create a <tag> and assign `props`: a dashed key ("aria-label") sets an
35
49
  // attribute, any other key ("className", "type", "textContent") a DOM
36
50
  // property. Appends to `parent` when given, and returns the new element.
@@ -97,51 +111,138 @@
97
111
  // predicates are combined with AND: a row must pass all of them.
98
112
  function computeView(spec, disp, state) {
99
113
  const cols = spec._cols || [], data = spec.data || {},
114
+ nRow = (data[cols[0]] || []).length,
100
115
  cell = (c, r) => ({
101
116
  raw: data[c]?.[r - 1] ?? null, disp: disp[c]?.[r - 1] ?? ""
102
117
  });
103
- let idx = Array.from({ length: (data[cols[0]] || []).length }, (_, i) => i + 1);
104
118
 
105
- // filters first: each looks at one cell, so they are the cheapest
119
+ // The per-row keep test: the column filters, the table-wide search, then the
120
+ // externally registered predicates, combined with AND (a row must pass all).
121
+ // A filter looks at one cell, so the cheap tests come first.
122
+ const tests = [];
106
123
  for (const c in state.filters || {}) {
107
124
  const pred = matcher(state.filters[c]);
108
- if (pred) idx = idx.filter(r => pred([cell(c, r)]));
125
+ if (pred) tests.push(r => pred([cell(c, r)]));
109
126
  }
110
- const pred = matcher(state.term);
111
- if (pred) idx = idx.filter(r => pred(cols.map(c => cell(c, r))));
112
-
127
+ const sPred = matcher(state.term);
128
+ if (sPred) tests.push(r => sPred(cols.map(c => cell(c, r))));
113
129
  // predicates registered from outside (el._lt.filter) run last, each a
114
- // function of the row's raw values keyed by column; combined with AND. The
115
- // row carries *every* column (not just the visible `cols`), so a widget can
116
- // filter on a hidden helper column — e.g. forestly's incidence slider reads
117
- // the hidden `hide_prop`, its parameter dropdown the hidden `parameter`.
130
+ // function of the row's raw values keyed by column. The row carries *every*
131
+ // column (not just the visible `cols`), so a widget can filter on a hidden
132
+ // helper column — e.g. forestly's incidence slider reads the hidden
133
+ // `hide_prop`, its parameter dropdown the hidden `parameter`.
118
134
  const fns = Object.values(state.predicates || {});
119
135
  if (fns.length) {
120
- const all = Object.keys(data);
121
- const row = r => Object.fromEntries(all.map(c => [c, data[c]?.[r - 1] ?? null]));
122
- idx = idx.filter(r => { const o = row(r); return fns.every(f => f(o)); });
136
+ const all = Object.keys(data),
137
+ row = r => Object.fromEntries(all.map(c => [c, data[c]?.[r - 1] ?? null]));
138
+ tests.push(r => { const o = row(r); return fns.every(f => f(o)); });
123
139
  }
140
+ const keep = r => tests.every(t => t(r));
124
141
 
125
- // sort by each key in turn, falling through to the next on a tie; a key's
126
- // column (resolved once) carries its numeric test and direction. nulls sort
127
- // last regardless of direction, so the direction never applies to them.
128
- const keys = (state.sort || []).map(k => {
129
- const col = data[k.col];
130
- return col && { col, num: numCol(col), dir: k.dir === "desc" ? -1 : 1 };
131
- }).filter(Boolean);
132
- if (keys.length) {
133
- coll ||= new Intl.Collator();
134
- idx.sort((a, b) => {
142
+ // Build a comparator from a list of sort keys: each key in turn, falling
143
+ // through to the next on a tie; a key's column (resolved once) carries its
144
+ // numeric test and direction. nulls sort last regardless of direction, so the
145
+ // direction never applies to them. Null when the list sorts nothing.
146
+ const makeCmp = sortKeys => {
147
+ const keys = sortKeys.map(k => {
148
+ const col = data[k.col];
149
+ return col && { col, num: numCol(col), dir: k.dir === "desc" ? -1 : 1 };
150
+ }).filter(Boolean);
151
+ return keys.length ? (a, b) => {
135
152
  for (const { col, num, dir } of keys) {
136
153
  const x = col[a - 1], y = col[b - 1], xn = x == null, yn = y == null;
137
154
  if (xn || yn) { if (xn !== yn) return xn ? 1 : -1; continue; }
138
- const c = num ? x - y : coll.compare(String(x), String(y));
155
+ const c = cmpVals(x, y, num);
139
156
  if (c) return dir * c;
140
157
  }
141
158
  return 0;
142
- });
159
+ } : null;
160
+ };
161
+
162
+ // Reduce a list of rows (in document order) to the kept rows in view order,
163
+ // sorted by `cmp`. With no indentation this is a plain filter + sort. With
164
+ // indentation the rows form a tree (a row's parent is the nearest earlier row
165
+ // one level up): filtering keeps any row with a surviving descendant, so a
166
+ // match stays in context, and a sort reorders siblings while each subtree
167
+ // travels with its parent.
168
+ const indent = spec._indent;
169
+ const reduce = (rows, cmp) => {
170
+ if (!indent) {
171
+ const out = rows.filter(keep);
172
+ return cmp ? out.sort(cmp) : out;
173
+ }
174
+ const roots = [], stack = [];
175
+ for (const r of rows) {
176
+ const node = { r, kids: [] }, lv = indent[r - 1] || 0;
177
+ while (stack.length && stack[stack.length - 1].lv >= lv) stack.pop();
178
+ (stack.length ? stack[stack.length - 1].node.kids : roots).push(node);
179
+ stack.push({ node, lv });
180
+ }
181
+ const prune = ns => ns.map(n => {
182
+ const kids = prune(n.kids);
183
+ return keep(n.r) || kids.length ? { r: n.r, kids } : null;
184
+ }).filter(Boolean);
185
+ const sortNodes = ns => {
186
+ if (cmp) ns.sort((a, b) => cmp(a.r, b.r));
187
+ ns.forEach(n => sortNodes(n.kids));
188
+ return ns;
189
+ };
190
+ const flat = [], walk = ns => ns.forEach(n => { flat.push(n.r); walk(n.kids); });
191
+ walk(sortNodes(prune(roots)));
192
+ return flat;
193
+ };
194
+
195
+ const all = () => Array.from({ length: nRow }, (_, i) => i + 1);
196
+
197
+ // Rowspan row groups: the group columns (outermost first) nest, so the view
198
+ // must stay a hierarchy — sorting a data column may only reorder rows within
199
+ // the innermost group, never across a group boundary, or a spanning cell
200
+ // would have to split. So partition by group column 1, then recurse on
201
+ // column 2 within each block, …, and only at the leaves sort by the data
202
+ // keys. A group column the reader clicked orders its blocks by that column
203
+ // (its key is pulled out of the leaf sort); untouched, blocks keep their
204
+ // first-appearance order. Rendering recomputes the spans from this order.
205
+ const gcols = spec._rowspan;
206
+ if (gcols) {
207
+ const dir = {}, leaf = [];
208
+ for (const k of state.sort || [])
209
+ gcols.includes(k.col) ? (dir[k.col] = k.dir) : leaf.push(k);
210
+ const leafCmp = makeCmp(leaf);
211
+ const partition = (rows, depth) => {
212
+ if (depth === gcols.length) return reduce(rows, leafCmp);
213
+ const col = data[gcols[depth]], blocks = new Map();
214
+ for (const r of rows) {
215
+ const v = col[r - 1], key = v == null ? "\0" : String(v);
216
+ (blocks.get(key) || blocks.set(key, { v, rows: [] }).get(key)).rows.push(r);
217
+ }
218
+ let order = [...blocks.values()];
219
+ if (dir[gcols[depth]]) {
220
+ const num = numCol(col), d = dir[gcols[depth]] === "desc" ? -1 : 1;
221
+ order.sort((a, b) => {
222
+ const an = a.v == null, bn = b.v == null;
223
+ if (an || bn) return an === bn ? 0 : an ? 1 : -1;
224
+ return d * cmpVals(a.v, b.v, num);
225
+ });
226
+ }
227
+ return order.flatMap(b => partition(b.rows, depth + 1));
228
+ };
229
+ return partition(all(), 0);
143
230
  }
144
- return idx;
231
+
232
+ // Separator row groups keep their place (the group is the unit that does not
233
+ // move): each group's rows filter/sort within it, and the surviving rows of
234
+ // all groups concatenate in group order, then any ungrouped rows (manual
235
+ // groups need not cover every row). A group left with no row simply
236
+ // contributes nothing — its header is re-emitted at render only when a row
237
+ // remains.
238
+ const cmp = makeCmp(state.sort || []);
239
+ const groups = spec._groups;
240
+ if (!groups) return reduce(all(), cmp);
241
+ const grouped = new Set(), out = [];
242
+ for (const g of groups) { g.rows.forEach(r => grouped.add(r)); out.push(...reduce(g.rows, cmp)); }
243
+ const rest = [];
244
+ for (let r = 1; r <= nRow; r++) if (!grouped.has(r)) rest.push(r);
245
+ return [...out, ...reduce(rest, cmp)];
145
246
  }
146
247
 
147
248
  // The rows of `view` on the current page. `state.page` is clamped into range
@@ -156,33 +257,60 @@
156
257
  return view.slice(state.page * n, (state.page + 1) * n);
157
258
  }
158
259
 
159
- // Enhanceable only when the rows are flat: row groups and indentation give the
160
- // row order a meaning of its own (grouping, hierarchy) that reordering or
161
- // dropping rows would destroy. Column spanners, explicit or from `auto_span`,
162
- // are no obstacle: they are <thead> rows, and only <tbody> is ever re-rendered.
163
- // Row-keyed styles and footnotes are fine too — core keys them to the original
164
- // row indices, which is what the view carries.
165
- const isFlat = spec => !spec.row_group &&
166
- !(spec.ops || []).some(o => o.type === "row_group" || o.type === "indent");
167
-
168
260
  // Displayed text per column, keyed to the original row order. It does not
169
261
  // change with the view, so capture it once from the initial full render.
170
- function captureDisplay(el, cols) {
262
+ // `order[i]` is the original 1-based index of the i-th body row as rendered
263
+ // (for a grouped table whose groups do not follow file order); absent ⇒ body
264
+ // rows are in index order. Separator group-header rows are skipped — they are
265
+ // no column's cell. `dataCells` drops any leading rowspan group cell, so the
266
+ // index lines up with `cols` whatever precedes the data cells.
267
+ function captureDisplay(el, cols, order) {
171
268
  const disp = {};
172
269
  cols.forEach(c => disp[c] = []);
173
- $$(el, "tbody tr").forEach((tr, ri) => cols.forEach(
174
- (c, ci) => disp[c][ri] = tr.children[ci]?.textContent ?? ""
175
- ));
270
+ let i = 0;
271
+ $$(el, "tbody tr").forEach(tr => {
272
+ if (tr.classList.contains("lt-row-group")) return;
273
+ const r = order ? order[i] : i + 1, cells = dataCells(tr);
274
+ cols.forEach((c, ci) => disp[c][r - 1] = cells[ci]?.textContent ?? "");
275
+ i++;
276
+ });
176
277
  return disp;
177
278
  }
178
279
 
179
280
  function enhance(el, spec) {
180
- if (!isFlat(spec)) return console.warn(
181
- "lt: interactive tables need flat rows (no row groups or indentation); " +
182
- "rendering a static table."
183
- );
184
281
  const opts = spec.interactive, cols = spec._cols || [],
185
- disp = captureDisplay(el, cols),
282
+ data = spec.data || {}, nRow = (data[cols[0]] || []).length,
283
+ groups = spec._groups,
284
+ // each row's group (for re-emitting a surviving header), and the body
285
+ // render order: groups in order, then any ungrouped rows (so a manual
286
+ // group listed out of file order maps the display text correctly)
287
+ groupOf = {}, order = groups ? [] : null;
288
+ if (groups) {
289
+ const seen = new Set();
290
+ groups.forEach(g => g.rows.forEach(r => { groupOf[r] = g; order.push(r); seen.add(r); }));
291
+ for (let r = 1; r <= nRow; r++) if (!seen.has(r)) order.push(r);
292
+ }
293
+ // turn a page's data rows into the row list core renders: a group header
294
+ // (an object core draws as a full-width label row) is inserted whenever the
295
+ // group changes, so a group spanning a page boundary re-shows its header at
296
+ // the top of the next page; ungrouped rows get none.
297
+ const withGroups = rows => {
298
+ if (!groups) return rows;
299
+ const out = []; let prev;
300
+ for (const r of rows) {
301
+ const g = groupOf[r];
302
+ if (g !== prev) { if (g) out.push({ label: g.label, raw: g.raw }); prev = g; }
303
+ out.push(r);
304
+ }
305
+ return out;
306
+ };
307
+ // the rowspan group columns lead the header row (they are hidden from
308
+ // `cols`); the full header order drives sort, so clicking a group header
309
+ // reorders its blocks (computeView pulls a group-column key out of the leaf
310
+ // sort — see the `_rowspan` branch there)
311
+ const nGroup = spec._rowspan ? spec._rowspan.length : 0,
312
+ allCols = nGroup ? [...spec._rowspan, ...cols] : cols;
313
+ const disp = captureDisplay(el, cols, order),
186
314
  // the bottom header row: the one whose cells line up with `cols`
187
315
  hrow = [...$$(el, "thead tr")].pop(),
188
316
  // an array `sort` on the options is an initial sort (a list of key
@@ -204,13 +332,16 @@
204
332
  const refresh = (stale = true) => {
205
333
  if (stale) { view = computeView(spec, disp, state); state.page = 0; }
206
334
  const rows = pageSlice(view, state),
335
+ // _viewRows carries the page's data rows with group headers spliced
336
+ // back in; postSwap hooks get the same list, one entry per <tbody> row
337
+ vr = withGroups(rows),
207
338
  tmp = elem(el.ownerDocument, "template", {
208
- innerHTML: LT.buildHtml({ ...spec, _viewRows: rows })
339
+ innerHTML: LT.buildHtml({ ...spec, _viewRows: vr })
209
340
  });
210
341
  const body = $(tmp.content, "tbody");
211
342
  if (!rows.length) // no matches: a neutral symbol spanning all columns
212
343
  body.innerHTML = `<tr class="lti-empty"><td colspan="${cols.length}">—</td></tr>`;
213
- else postSwap.forEach(f => f(body, rows));
344
+ else postSwap.forEach(f => f(body, vr));
214
345
  $(el, "tbody").replaceWith(body);
215
346
  sync(view.length);
216
347
  };
@@ -242,26 +373,39 @@
242
373
  const flt = opts.filter,
243
374
  barCols = flt?.cols ?
244
375
  Object.keys(flt.cols).filter(c => flt.cols[c] !== true && !cols.includes(c)) : [];
245
- // the table-wide controls share one full-width head row: the column menu
246
- // (if any) at its start, the search box, then any head-bar filter chips
247
- const headBar = (opts.search !== false || opts.hide || barCols.length) ?
376
+ // the table-wide controls share one full-width head row, laid out left to
377
+ // right: an icon group (the column menu and the download button), then the
378
+ // head-bar filter chips, then the search box (which absorbs the free space)
379
+ const headBar = (opts.search !== false || opts.hide || barCols.length || opts.download) ?
248
380
  elem(el.ownerDocument, "div", { className: "lti-bar" },
249
381
  fullRow(el.tHead || el.createTHead(), "lti-head", cols.length, 0)) : null;
250
- // the header labels, read before sort/resize decorate the cells, so the
251
- // column menu can list the displayed labels rather than the raw names
252
- const labels = [...$$(hrow, "th")].map(th => th.textContent);
253
- // wire sort before adding the filter row, so it sees the header row only
254
- if (opts.sort !== false) addSort(hrow, cols, state, refresh);
382
+ // the data-column header labels (skipping any leading group cell), read
383
+ // before sort/resize decorate the cells, so the column menu can list the
384
+ // displayed labels rather than the raw names
385
+ const labels = dataCells(hrow).map(th => th.textContent);
386
+ // wire sort before adding the filter row, so it sees the header row only;
387
+ // `allCols` so every header cell binds (a group header is sortable too)
388
+ if (opts.sort !== false) addSort(hrow, allCols, state, refresh);
255
389
  // the under-header filter row: needed when a default applies to visible
256
390
  // columns or any named column is itself visible
257
391
  if (flt && (flt.default || (flt.cols && Object.keys(flt.cols).some(c => cols.includes(c)))))
258
392
  addFilter(hrow, cols, flt, spec.data, state, refresh);
259
- const layout = opts.resize ? fixedLayout(el, hrow, cols.length) : null;
393
+ const layout = opts.resize ? fixedLayout(el, hrow, cols.length, nGroup) : null;
260
394
  if (opts.resize) addResize(el, layout);
395
+ // the icon buttons sit together in a group that keeps its natural width;
396
+ // append them (menu first, download second) before the chips and search so
397
+ // the DOM order is the visual order
398
+ const icons = (opts.hide || opts.download) ?
399
+ elem(el.ownerDocument, "div", { className: "lti-icons" }, headBar) : null;
261
400
  if (opts.hide)
262
- addColumnToggle(headBar, el, hrow, cols, labels, opts.hide, layout, postSwap);
401
+ addColumnToggle(icons, el, hrow, cols, labels, opts.hide, layout, postSwap);
402
+ if (opts.download) addDownload(icons, el, spec, cols, labels, opts.download);
403
+ // the chips share a group so the bar has three parts (icons, chips, search)
404
+ // with a wider gap between them than within each
405
+ if (barCols.length) addControlFilters(
406
+ elem(el.ownerDocument, "div", { className: "lti-chips" }, headBar),
407
+ barCols, flt.cols, spec.data, state, refresh);
263
408
  if (opts.search !== false) addSearch(headBar, el, state, refresh);
264
- if (barCols.length) addControlFilters(headBar, barCols, flt.cols, spec.data, state, refresh);
265
409
  // row detail re-renders through the same seam: toggling a row only changes
266
410
  // which rows carry a detail block, so a plain re-render (no new view) is
267
411
  // enough
@@ -313,6 +457,40 @@
313
457
  onType(input, v => { state.term = v; refresh(); });
314
458
  }
315
459
 
460
+ // Escape one value for a CSV field: wrap in quotes (doubling any inside) only
461
+ // when it holds a comma, quote, or newline, so plain values stay bare.
462
+ function csvField(v) {
463
+ const s = v == null ? "" : String(v);
464
+ return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
465
+ }
466
+
467
+ // A button that downloads the table's current view as a CSV file. The header
468
+ // row is the column labels; each body row is a view row (every row the filters
469
+ // and search keep, in sort order, across all pages — `el._lt.view()` gives the
470
+ // 1-based original indices) rendered with the displayed cell text from
471
+ // `spec._display`. `name` is the file name (`true` = a default); group columns
472
+ // (null in `cols`) are skipped. No library, no network: a Blob and an <a>.
473
+ function addDownload(cell, el, spec, cols, labels, name) {
474
+ const doc = el.ownerDocument,
475
+ file = (typeof name === "string" && name ? name : "table")
476
+ .replace(/(\.csv)?$/i, ".csv"),
477
+ keep = cols.map((c, i) => i).filter(i => cols[i] != null),
478
+ btn = elem(doc, "button", {
479
+ type: "button", className: "lti-download", title: "Download CSV",
480
+ "aria-label": "Download table as CSV"
481
+ }, cell);
482
+ btn.onclick = () => {
483
+ const disp = spec._display || {},
484
+ lines = [keep.map(i => csvField(labels[i] ?? cols[i])).join(",")];
485
+ for (const r of el._lt.view())
486
+ lines.push(keep.map(i => csvField((disp[cols[i]] || [])[r - 1])).join(","));
487
+ const url = URL.createObjectURL(
488
+ new Blob([lines.join("\n")], { type: "text/csv" }));
489
+ elem(doc, "a", { href: url, download: file }).click();
490
+ URL.revokeObjectURL(url);
491
+ };
492
+ }
493
+
316
494
  // Advance one column through asc → desc → unsorted, updating `state.sort` (an
317
495
  // ordered list of `{col, dir}` keys). A plain click sorts by that column
318
496
  // alone; a shift-click adds it as a further tie-breaker (or re-cycles it where
@@ -426,7 +604,11 @@
426
604
  rerender();
427
605
  };
428
606
  return (body, rows) => [...body.rows].forEach((tr, i) => {
429
- const r = rows[i], td0 = tr.cells[0], open = state.expanded.has(r);
607
+ const r = rows[i];
608
+ if (typeof r !== "number") return; // a separator group-header row
609
+ // the first data cell (past any leading rowspan group cell), so the caret
610
+ // sits in the row's own first column, not a group label
611
+ const td0 = dataCells(tr)[0], open = state.expanded.has(r);
430
612
  if (!td0) return;
431
613
  const btn = elem(doc, "button", {
432
614
  type: "button", className: "lti-expand", textContent: open ? "▾" : "▸",
@@ -444,14 +626,16 @@
444
626
  });
445
627
  }
446
628
 
447
- // The table's <colgroup>, created (one <col> per column) when core emitted
448
- // none — it only does so for a table given explicit widths on the R side.
449
- function colGroup(el, nCol) {
629
+ // The table's <colgroup>, created when core emitted none (it only does so for
630
+ // a table given explicit widths on the R side). It gets `nGroup` leading <col>s
631
+ // for the rowspan group columns (which lead each header/body row) then one per
632
+ // data column, matching core's own layout so widths line up.
633
+ function colGroup(el, nCol, nGroup = 0) {
450
634
  let g = $(el, "colgroup");
451
635
  if (!g) {
452
636
  const doc = el.ownerDocument;
453
637
  g = elem(doc, "colgroup");
454
- for (let i = 0; i < nCol; i++) elem(doc, "col", {}, g);
638
+ for (let i = 0; i < nGroup + nCol; i++) elem(doc, "col", {}, g);
455
639
  // <colgroup> comes after <caption> (the title), before <thead>
456
640
  el.caption ? el.caption.after(g) : el.prepend(g);
457
641
  }
@@ -466,9 +650,9 @@
466
650
  // content width (an unconstrained reflow, with the widths put back after).
467
651
  // `setWidth(i, w, min)` sets column i to `w` px (clamped to `min`), widening or
468
652
  // narrowing the table by as much; the other columns keep their widths.
469
- function fixedLayout(el, hrow, nCol) {
653
+ function fixedLayout(el, hrow, nCol, nGroup = 0) {
470
654
  const ths = [...$$(hrow, "th")],
471
- cs = colGroup(el, nCol), wOf = e => e.getBoundingClientRect().width;
655
+ cs = colGroup(el, nCol, nGroup), wOf = e => e.getBoundingClientRect().width;
472
656
  const freeze = () => {
473
657
  if (el.classList.contains("lti-fixed")) return;
474
658
  const w = ths.map(wOf);
@@ -491,9 +675,11 @@
491
675
  const old = parseFloat(cs[i].style.width);
492
676
  cs[i].style.width = Math.max(w, min) + "px";
493
677
  el.style.width =
494
- parseFloat(el.style.width) + parseFloat(cs[i].style.width) - old + "px";
678
+ `${parseFloat(el.style.width) + parseFloat(cs[i].style.width) - old}px`;
495
679
  };
496
- return { ths, cs, freeze, natural, setWidth };
680
+ // `dataCs` drops the leading group <col>s, so a data-column consumer (the
681
+ // column-hide menu) indexes it by data-column position
682
+ return { ths, cs, dataCs: cs.slice(nGroup), freeze, natural, setWidth };
497
683
  }
498
684
 
499
685
  // Drag-to-resize column edges: a grip on the right edge of each header cell.
@@ -514,8 +700,8 @@
514
700
  freeze();
515
701
  const x0 = e.clientX, w0 = parseFloat(cs[i].style.width);
516
702
  el.classList.add("lti-resizing");
517
- // the floor is MIN_COL (setWidth's default): a frozen cell clips its
518
- // content, so shrinking just hides the overflow, grip at the edge
703
+ // no label floor: omit setWidth's min arg to take its default, and the
704
+ // cell clips its overflow (CSS) instead of spilling when shrunk
519
705
  drag(e, ev => setWidth(i, w0 + ev.clientX - x0),
520
706
  () => el.classList.remove("lti-resizing"));
521
707
  };
@@ -532,10 +718,11 @@
532
718
  function addColumnToggle(cell, el, hrow, cols, labels, opt, layout, postSwap) {
533
719
  const doc = el.ownerDocument,
534
720
  start = (opt && opt.hidden) || [], hidden = new Set();
535
- // show/hide column i's cell in one row, skipping the single colspan cell of
536
- // a detail or empty row (which spans every column and is no column's own)
721
+ // show/hide column i's cell in one row (indexing the row's data cells, past
722
+ // any leading rowspan group cell), skipping the single colspan cell of a
723
+ // detail or empty row (which spans every column and is no column's own)
537
724
  const setCell = (tr, i, on) => {
538
- const c = tr.children[i];
725
+ const c = dataCells(tr)[i];
539
726
  if (c && c.colSpan === 1) c.hidden = on;
540
727
  };
541
728
  // re-hide every hidden column's cells on each freshly-built <tbody>
@@ -551,7 +738,7 @@
551
738
  // keeps them in sync; only the fresh <tbody> is re-hidden (via postSwap).
552
739
  const apply = i => {
553
740
  const on = hidden.has(i);
554
- if (layout?.cs[i]) layout.cs[i].hidden = on;
741
+ if (layout?.dataCs[i]) layout.dataCs[i].hidden = on;
555
742
  for (const tr of el.rows)
556
743
  if (!tr.classList.contains("lt-spanner-row")) setCell(tr, i, on);
557
744
  };
@@ -671,7 +858,7 @@
671
858
  t.setAttribute("aria-valuenow", val[i]);
672
859
  });
673
860
  fill.style.left = pct(val[0]) + "%";
674
- fill.style.right = 100 - pct(val[1]) + "%";
861
+ fill.style.right = `${100 - pct(val[1])}%`;
675
862
  },
676
863
  setOne = (i, v) => {
677
864
  val[i] = snap(v);
@@ -1,2 +1,2 @@
1
- (t=>{"use strict";const e=t.LT;if(!e?.onMount||e.plugins.interactive)return;let n;const o=t=>"number"==typeof t.find(t=>null!=t),s=t=>"string"!=typeof t?{...t}:"-"===t[0]?{col:t.slice(1),dir:"desc"}:{col:t,dir:"asc"},i=(t,e)=>t.querySelector(e),l=(t,e)=>t.querySelectorAll(e),r=(t,e,n,o)=>t.addEventListener(e,n,o);function a(t,e,n={},o){const s=t.createElement(e);for(const t in n)t.includes("-")?s.setAttribute(t,n[t]):s[t]=n[t];return o?o.appendChild(s):s}function c(t,e,n){t.preventDefault();const o=t.target.ownerDocument||document,s=t=>e(t);r(o,"pointermove",s),r(o,"pointerup",()=>{var t,e;t="pointermove",e=s,o.removeEventListener(t,e),n&&n()},{once:!0})}function d(t){const e=String(t??"").trim();if(""===e)return null;if(/(^|[^\w$])x([^\w$]|$)/.test(e)){let t;try{t=new Function("x",`return (${e});`)}catch(t){}if(t){const n=/^\s*!|!=/.test(e),o=e=>{if(null==e)return n;try{const n=Number(e);return!!t(String(e))||""!==e&&isFinite(n)&&!!t(n)}catch(t){return!1}};return t=>t[n?"every":"some"](t=>o(t.raw))}}const n="!"===e[0],o=(n?e.slice(1).trim():e).toLowerCase();return""===o?null:t=>n!==t.some(t=>null!=t.disp&&String(t.disp).toLowerCase().includes(o))}function u(t,e,s){const i=t._cols||[],l=t.data||{},r=(t,n)=>({raw:l[t]?.[n-1]??null,disp:e[t]?.[n-1]??""});let a=Array.from({length:(l[i[0]]||[]).length},(t,e)=>e+1);for(const t in s.filters||{}){const e=d(s.filters[t]);e&&(a=a.filter(n=>e([r(t,n)])))}const c=d(s.term);c&&(a=a.filter(t=>c(i.map(e=>r(e,t)))));const u=Object.values(s.predicates||{});if(u.length){const t=Object.keys(l),e=e=>Object.fromEntries(t.map(t=>[t,l[t]?.[e-1]??null]));a=a.filter(t=>{const n=e(t);return u.every(t=>t(n))})}const p=(s.sort||[]).map(t=>{const e=l[t.col];return e&&{col:e,num:o(e),dir:"desc"===t.dir?-1:1}}).filter(Boolean);return p.length&&(n||=new Intl.Collator,a.sort((t,e)=>{for(const{col:o,num:s,dir:i}of p){const l=o[t-1],r=o[e-1],a=null==l,c=null==r;if(a||c){if(a!==c)return a?1:-1;continue}const d=s?l-r:n.compare(String(l),String(r));if(d)return i*d}return 0})),a}function p(t,e){const n=e.pageSize;if(!n)return e.page=0,t;const o=Math.max(0,Math.ceil(t.length/n)-1);return e.page=Math.min(Math.max(e.page||0,0),o),t.slice(e.page*n,(e.page+1)*n)}function f(n,o){if(!(t=>!t.row_group&&!(t.ops||[]).some(t=>"row_group"===t.type||"indent"===t.type))(o))return console.warn("lt: interactive tables need flat rows (no row groups or indentation); rendering a static table.");const d=o.interactive,f=o._cols||[],w=function(t,e){const n={};return e.forEach(t=>n[t]=[]),l(t,"tbody tr").forEach((t,o)=>e.forEach((e,s)=>n[e][o]=t.children[s]?.textContent??"")),n}(n,f),y=[...l(n,"thead tr")].pop(),b={filters:{},predicates:{},page:0,pageSize:0,sort:Array.isArray(d.sort)?d.sort.map(s):[]};let x,v=()=>{};const S=[],N=(t=!0)=>{t&&(x=u(o,w,b),b.page=0);const s=p(x,b),l=a(n.ownerDocument,"template",{innerHTML:e.buildHtml({...o,_viewRows:s})}),r=i(l.content,"tbody");s.length?S.forEach(t=>t(r,s)):r.innerHTML=`<tr class="lti-empty"><td colspan="${f.length}">—</td></tr>`,i(n,"tbody").replaceWith(r),v(x.length)};n._lt={spec:o,state:b,refresh:()=>N(),view:()=>x||u(o,w,b),filter(t,e){e?b.predicates[t]=e:delete b.predicates[t],N()}};const E=d.filter,k=E?.cols?Object.keys(E.cols).filter(t=>!0!==E.cols[t]&&!f.includes(t)):[],M=!1!==d.search||d.hide||k.length?a(n.ownerDocument,"div",{className:"lti-bar"},m(n.tHead||n.createTHead(),"lti-head",f.length,0)):null,$=[...l(y,"th")].map(t=>t.textContent);!1!==d.sort&&function(t,e,n,o){const s=t.ownerDocument,i={},r=()=>{for(const t in i)i[t].th.removeAttribute("aria-sort"),i[t].ind.textContent="";const t=n.sort||[];t.forEach(({col:e,dir:n},o)=>{const s=i[e];s&&(s.th.setAttribute("aria-sort","desc"===n?"descending":"ascending"),s.ind.textContent=("desc"===n?"▼":"▲")+(t.length>1?String(o+1).replace(/\d/g,t=>"₀₁₂₃₄₅₆₇₈₉"[t]):""))})};l(t,"th").forEach((t,l)=>{const c=e[l];if(null==c)return;t.classList.add("lti-sortable");const d=a(s,"span",{className:"lti-label"},t);for(;t.firstChild!==d;)d.append(t.firstChild);i[c]={th:t,ind:a(s,"span",{className:"lti-sort"},d)},d.onclick=t=>{!function(t,e,n){const o=(t.sort||[]).slice(),s=o.findIndex(t=>t.col===e),i=s<0?"asc":"asc"===o[s].dir?"desc":null;if(!n)return void(t.sort=i?[{col:e,dir:i}]:[]);s<0?o.push({col:e,dir:i}):i?o[s]={col:e,dir:i}:o.splice(s,1);t.sort=o}(n,c,t.shiftKey),r(),o()}}),r()}(y,f,b,N),E&&(E.default||E.cols&&Object.keys(E.cols).some(t=>f.includes(t)))&&function(t,e,n,o,s,i){const l=t.ownerDocument,r=a(l,"tr",{className:"lti-filters"}),c=n.default,d=n.cols||{};e.forEach(t=>{const e=a(l,"td",{},r),n=t in d?d[t]:c;if(n)if(!0===n){g(e.appendChild(h(l,`Filter ${t}`)),e=>{e?s.filters[t]=e:delete s.filters[t],i()})}else e.appendChild(C(l,t,n,o,s,i,null))}),t.parentNode.appendChild(r)}(y,f,E,o.data,b,N);const A=d.resize?function(t,e,n){const o=[...l(e,"th")],s=function(t,e){let n=i(t,"colgroup");if(!n){const o=t.ownerDocument;n=a(o,"colgroup");for(let t=0;t<e;t++)a(o,"col",{},n);t.caption?t.caption.after(n):t.prepend(n)}return[...n.children]}(t,n),r=t=>t.getBoundingClientRect().width,c=()=>{if(t.classList.contains("lti-fixed"))return;const e=o.map(r);t.style.width=r(t)+"px",s.forEach((t,n)=>t.style.width=e[n]+"px"),t.classList.add("lti-fixed")},d=e=>{const n=s.map(t=>t.style.width),i=t.style.width;s.forEach(t=>t.style.width=""),t.style.width="",t.classList.remove("lti-fixed");const l=r(o[e]);return s.forEach((t,e)=>t.style.width=n[e]),t.style.width=i,t.classList.add("lti-fixed"),l},u=(e,n,o=24)=>{const i=parseFloat(s[e].style.width);s[e].style.width=Math.max(n,o)+"px",t.style.width=parseFloat(t.style.width)+parseFloat(s[e].style.width)-i+"px"};return{ths:o,cs:s,freeze:c,natural:d,setWidth:u}}(n,y,f.length):null;if(d.resize&&function(t,e){const n=t.ownerDocument,{ths:o,cs:s,freeze:i,natural:l,setWidth:r}=e;o.forEach((e,o)=>{const d=a(n,"div",{className:"lti-resizer"},e);d.ondblclick=t=>{t.stopPropagation(),i(),r(o,l(o))},d.onpointerdown=e=>{e.stopPropagation(),i();const n=e.clientX,l=parseFloat(s[o].style.width);t.classList.add("lti-resizing"),c(e,t=>r(o,l+t.clientX-n),()=>t.classList.remove("lti-resizing"))}})}(n,A),d.hide&&function(t,e,n,o,s,i,l,c){const d=e.ownerDocument,u=i&&i.hidden||[],p=new Set,f=(t,e,n)=>{const o=t.children[e];o&&1===o.colSpan&&(o.hidden=n)};c.push(t=>p.forEach(e=>{for(const n of t.rows)f(n,e,!0)}));const h=t=>{const n=p.has(t);l?.cs[t]&&(l.cs[t].hidden=n);for(const o of e.rows)o.classList.contains("lt-spanner-row")||f(o,t,n)},m=a(d,"span",{className:"lti-cols"},t),g=a(d,"button",{type:"button",title:"Columns","aria-label":"Show or hide columns","aria-expanded":"false"},m),w=a(d,"div",{className:"lti-menu",hidden:!0},m);o.forEach((t,e)=>{if(null==t)return;const n=a(d,"label",{},w),o=a(d,"input",{type:"checkbox",checked:!0},n);n.append(s[e]??t),u.includes(t)&&(o.checked=!1,p.add(e)),o.onchange=()=>{o.checked?p.delete(e):p.add(e),h(e)}}),o.forEach((t,e)=>h(e));const y=t=>{w.hidden=!t,g.setAttribute("aria-expanded",String(t))};g.onclick=t=>{t.stopPropagation(),y(w.hidden)},r(d,"click",t=>{m.contains(t.target)||y(!1)}),r(d,"keydown",t=>{"Escape"===t.key&&y(!1)})}(M,n,0,f,$,d.hide,A,S),!1!==d.search&&function(t,e,n,o){const s=t.appendChild(h(e.ownerDocument,"Search"));g(s,t=>{n.term=t,o()})}(M,n,b,N),k.length&&function(t,e,n,o,s,i){const l=t.ownerDocument;for(const r of e)t.appendChild(C(l,r,n[r],o,s,i,n[r].label||r))}(M,k,E.cols,o.data,b,N),d.detail&&S.push(function(n,o,s,i,l,r){const c=n.ownerDocument,d=s.length,u={};i.expanded=new Set;const p=(t,e)=>{const n={};for(const o in t)n[o]=t[o]?.[e-1]??null;return n},f=e=>{if(e in u)return u[e];const n=p(o._display||{},e);let s=null;if(Array.isArray(l)){const t={};for(const e of l)t[e]=[n[e]??null];s={data:t}}else{const i="function"==typeof l?l:t[l];"function"==typeof i&&(s=i(p(o.data||{},e),e,n))}return u[e]=s},h=t=>{i.expanded.has(t)?i.expanded.delete(t):i.expanded.add(t),r()};return(t,n)=>[...t.rows].forEach((t,o)=>{const s=n[o],l=t.cells[0],r=i.expanded.has(s);if(!l)return;const u=a(c,"button",{type:"button",className:"lti-expand",textContent:r?"▾":"▸","aria-expanded":String(r),"aria-label":"Toggle detail"});u.onclick=()=>h(s),l.prepend(u);const p=r&&f(s);if(p){const n=a(c,"tr",{className:"lti-detail"});t.after(n);const o=a(c,"td",{colSpan:d},n);e.render(a(c,"div",{},o),p)}})}(n,o,f,b,d.detail,()=>N(!1))),d.pager){const t=Array.isArray(d.pager)?d.pager:[10,25,50,100];v=function(t,e,n,o,s){const i=t.ownerDocument;let l=t.tFoot;l||((l=t.createTFoot()).className="lt-footer");const r=a(i,"div",{className:"lti-pager"},m(l,"lti-pager-row",e.length,-1)),c=a(i,"span",{className:"lti-pos"});o.pageSize=n[0];const d=[()=>0,t=>t-1,t=>t+1,()=>1e9],u=["«","‹","›","»"].map((t,e)=>{const n=a(i,"button",{type:"button",textContent:t,"aria-label":["First","Previous","Next","Last"][e]},r);return n.onclick=()=>{o.page=d[e](o.page),s()},n});if(r.appendChild(c),n.length>1){const t=a(i,"select",{"aria-label":"Rows per page"},r);n.forEach(e=>a(i,"option",{value:e,textContent:e||"∞"},t)),t.onchange=()=>{o.pageSize=+t.value,o.page=0,s()}}return t=>{const e=o.pageSize||t||1,n=Math.max(0,Math.ceil(t/e)-1);c.textContent=t?`${o.page*e+1}–${Math.min(t,(o.page+1)*e)} / ${t}`:"0 / 0",u.forEach((t,e)=>t.disabled=e<2?!o.page:o.page===n)}}(n,f,t,b,()=>N(!1)),N()}else(b.sort.length||d.detail)&&N()}function h(t,e){return a(t,"input",{type:"search",className:"lti-search","aria-label":e})}function m(t,e,n,o){const s=t.insertRow(o);return s.className=e,a(t.ownerDocument,"td",{colSpan:n},s)}function g(t,e){let n;const o=()=>e(t.value);t.oninput=()=>{clearTimeout(n),n=setTimeout(o,150)},t.onchange=()=>{clearTimeout(n),o()}}const w=t=>""===t?"":`x == ${JSON.stringify(String(t))}`,y=t=>{const e=/^\s*x\s*==\s*(['"])([\s\S]*)\1\s*$/.exec(t||"");return e?e[2]:null},b=(t,e,n,o)=>t<=n&&e>=o?"":`x >= ${t} && x <= ${e}`,x=t=>{const e=/^\s*x\s*>=\s*(-?[\d.]+)\s*&&\s*x\s*<=\s*(-?[\d.]+)\s*$/.exec(t||"");return e?[parseFloat(e[1]),parseFloat(e[2])]:null};function v(t,e,n){const o=e.min,s=e.max,i=s-o||1,l=e.step||i/100,r=a(t,"div",{className:"lti-slider"}),d=a(t,"div",{className:"lti-slider-fill"},r),u=e=>a(t,"button",{type:"button",className:"lti-thumb",role:"slider","aria-label":e,"aria-valuemin":o,"aria-valuemax":s},r),p=[u("Minimum"),u("Maximum")];let f=[o,s];const h=t=>(t-o)/i*100,m=()=>{p.forEach((t,e)=>{t.style.left=h(f[e])+"%",t.setAttribute("aria-valuenow",f[e])}),d.style.left=h(f[0])+"%",d.style.right=100-h(f[1])+"%"},g=(t,e)=>{f[t]=(t=>{const e=Math.round((t-o)/l)*l+o;return Math.min(s,Math.max(o,Math.round(1e6*e)/1e6))})(e),f[0]>f[1]&&(f=[Math.min(...f),Math.max(...f)]),m(),n(f.slice())};return p.forEach((t,e)=>{t.onpointerdown=n=>{t.focus(),c(n,t=>g(e,(t=>{const e=r.getBoundingClientRect();return o+i*Math.min(1,Math.max(0,(t-e.left)/e.width))})(t.clientX)))},t.onkeydown=t=>{const n={ArrowLeft:-1,ArrowDown:-1,ArrowRight:1,ArrowUp:1}[t.key];if(n)g(e,f[e]+n*l);else if("Home"===t.key)g(e,o);else{if("End"!==t.key)return;g(e,s)}t.preventDefault()}}),m(),{el:r,set:t=>{f=t.slice(),m()}}}function S(t,e,n){const o=a(t,"span",{className:"lti-pop"}),s=a(t,"button",{type:"button",className:"lti-funnel",title:e,"aria-label":e,"aria-expanded":"false"},o),i=a(t,"div",{className:"lti-pop-panel",hidden:!0},o);n(i);const l=e=>{i.hidden=!e,s.setAttribute("aria-expanded",String(e)),e&&(()=>{i.style.left=i.style.right="";const e=i.getBoundingClientRect(),n=(t.defaultView||window).innerWidth;e.right>n-4&&(i.style.left="auto",i.style.right="0")})()};return s.onclick=t=>{t.stopPropagation(),l(i.hidden)},r(t,"click",t=>{o.contains(t.target)||l(!1)}),r(t,"keydown",t=>{"Escape"===t.key&&l(!1)}),o}const N={select:{describe:(t,e)=>{const n=y(t);if(null==n)return t?"⋯":"";const o=e.choices.find(t=>t.value===n);return o?o.label:n},init:t=>w(t.selected??(t.choices[0]||{}).value??""),build:(t,e,n)=>{const o=function(t,e,n){const o=a(t,"select",{className:"lti-filter-pick"});return e.forEach(e=>a(t,"option",{value:e.value,textContent:e.label},o)),o.onchange=()=>n(o.value),{el:o,set:t=>o.value=t}}(t,e.choices,t=>n(w(t),s)),s={el:o.el,reflect:t=>{const e=y(t);o.set(null==e?"":e)}};return s}},range:{describe:t=>{const e=x(t);return e?`${e[0]} – ${e[1]}`:t?"⋯":""},init:t=>t.value?b(t.value[0],t.value[1],t.min,t.max):"",build:(t,e,n)=>{const o=a(t,"span",{className:"lti-slider-out"}),s=t=>o.textContent=`${t[0]} – ${t[1]}`,i=v(t,e,t=>{s(t),n(b(t[0],t[1],e.min,e.max),l)}),l={el:[i.el,o],reflect:t=>{const n=x(t)||[e.min,e.max];i.set(n),s(n)}};return l}}};function C(t,e,n,o,s,i,l){const r=function(t,e){if("select"===t.type){let n=t.choices;if(!n){const t=new Set,o=[];for(const n of e)null==n||t.has(n)||(t.add(n),o.push(n));o.sort((t,e)=>"number"==typeof t&&"number"==typeof e?t-e:String(t).localeCompare(String(e))),n=o.map(t=>({value:String(t),label:String(t)}))}return{...t,choices:n}}let{min:n,max:o}=t;if(null==n||null==o){let t=1/0,s=-1/0;for(const n of e){const e=Number(n);isFinite(e)&&(e<t&&(t=e),e>s&&(s=e))}null==n&&(n=isFinite(t)?t:0),null==o&&(o=isFinite(s)?s:1)}return{...t,min:n,max:o}}(n,o[e]||[]),c=N[r.type],d=[];let u=()=>{};const p=(t,n)=>{t?s.filters[e]=t:delete s.filters[e],i();const o=s.filters[e]||"";d.forEach(t=>t!==n&&t.reflect(o)),u(o)},f=S(t,`Filter ${r.label||e}`,n=>{const o=a(t,"input",{type:"search",className:"lti-search","aria-label":`${r.label||e} filter expression`},n),s={reflect:e=>{t.activeElement!==o&&(o.value=e)}};d.push(s),g(o,t=>p(t.trim(),s));const i=c.build(t,r,p);d.push(i),n.append(...[].concat(i.el)),p(c.init(r),null)});let h,m=f;return null!=l&&(m=a(t,"span",{className:"lti-chip"}),a(t,"span",{className:"lti-chip-name",textContent:l},m),h=a(t,"span",{className:"lti-chip-cur"},m),m.appendChild(f)),u=t=>{m.classList.toggle("lti-on",!!t),h&&(h.textContent=c.describe(t,r))},u(s.filters[e]||""),m}const E=(t,e)=>{e?.interactive&&!t.dataset.ltiOn&&(t.dataset.ltiOn="1",f(t,e))};e.plugins.interactive={matcher:d,computeView:u,pageSlice:p,enhance:f},e.onMount.push(E),"undefined"!=typeof document&&l(document,".lt-table").forEach(t=>E(t,t._ltSpec))})("undefined"!=typeof window?window:globalThis);
1
+ (t=>{"use strict";const e=t.LT;if(!e?.onMount||e.plugins.interactive)return;let n;const o=t=>"number"==typeof t.find(t=>null!=t),s=t=>"string"!=typeof t?{...t}:"-"===t[0]?{col:t.slice(1),dir:"desc"}:{col:t,dir:"asc"},l=(t,e,o)=>(n||=new Intl.Collator,o?t-e:n.compare(String(t),String(e))),r=(t,e)=>t.querySelector(e),i=(t,e)=>t.querySelectorAll(e),a=(t,e,n,o)=>t.addEventListener(e,n,o),c=t=>[...t.children].filter(t=>!t.classList.contains("lt-row-group"));function u(t,e,n={},o){const s=t.createElement(e);for(const t in n)t.includes("-")?s.setAttribute(t,n[t]):s[t]=n[t];return o?o.appendChild(s):s}function d(t,e,n){t.preventDefault();const o=t.target.ownerDocument||document,s=t=>e(t);a(o,"pointermove",s),a(o,"pointerup",()=>{var t,e;t="pointermove",e=s,o.removeEventListener(t,e),n&&n()},{once:!0})}function p(t){const e=String(t??"").trim();if(""===e)return null;if(/(^|[^\w$])x([^\w$]|$)/.test(e)){let t;try{t=new Function("x",`return (${e});`)}catch(t){}if(t){const n=/^\s*!|!=/.test(e),o=e=>{if(null==e)return n;try{const n=Number(e);return!!t(String(e))||""!==e&&isFinite(n)&&!!t(n)}catch(t){return!1}};return t=>t[n?"every":"some"](t=>o(t.raw))}}const n="!"===e[0],o=(n?e.slice(1).trim():e).toLowerCase();return""===o?null:t=>n!==t.some(t=>null!=t.disp&&String(t.disp).toLowerCase().includes(o))}function f(t,e,n){const s=t._cols||[],r=t.data||{},i=(r[s[0]]||[]).length,a=(t,n)=>({raw:r[t]?.[n-1]??null,disp:e[t]?.[n-1]??""}),c=[];for(const t in n.filters||{}){const e=p(n.filters[t]);e&&c.push(n=>e([a(t,n)]))}const u=p(n.term);u&&c.push(t=>u(s.map(e=>a(e,t))));const d=Object.values(n.predicates||{});if(d.length){const t=Object.keys(r),e=e=>Object.fromEntries(t.map(t=>[t,r[t]?.[e-1]??null]));c.push(t=>{const n=e(t);return d.every(t=>t(n))})}const f=t=>c.every(e=>e(t)),h=t=>{const e=t.map(t=>{const e=r[t.col];return e&&{col:e,num:o(e),dir:"desc"===t.dir?-1:1}}).filter(Boolean);return e.length?(t,n)=>{for(const{col:o,num:s,dir:r}of e){const e=o[t-1],i=o[n-1],a=null==e,c=null==i;if(a||c){if(a!==c)return a?1:-1;continue}const u=l(e,i,s);if(u)return r*u}return 0}:null},m=t._indent,g=(t,e)=>{if(!m){const n=t.filter(f);return e?n.sort(e):n}const n=[],o=[];for(const e of t){const t={r:e,kids:[]},s=m[e-1]||0;for(;o.length&&o[o.length-1].lv>=s;)o.pop();(o.length?o[o.length-1].node.kids:n).push(t),o.push({node:t,lv:s})}const s=t=>t.map(t=>{const e=s(t.kids);return f(t.r)||e.length?{r:t.r,kids:e}:null}).filter(Boolean),l=t=>(e&&t.sort((t,n)=>e(t.r,n.r)),t.forEach(t=>l(t.kids)),t),r=[],i=t=>t.forEach(t=>{r.push(t.r),i(t.kids)});return i(l(s(n))),r},w=()=>Array.from({length:i},(t,e)=>e+1),y=t._rowspan;if(y){const t={},e=[];for(const o of n.sort||[])y.includes(o.col)?t[o.col]=o.dir:e.push(o);const s=h(e),i=(e,n)=>{if(n===y.length)return g(e,s);const a=r[y[n]],c=new Map;for(const t of e){const e=a[t-1],n=null==e?"\0":String(e);(c.get(n)||c.set(n,{v:e,rows:[]}).get(n)).rows.push(t)}let u=[...c.values()];if(t[y[n]]){const e=o(a),s="desc"===t[y[n]]?-1:1;u.sort((t,n)=>{const o=null==t.v,r=null==n.v;return o||r?o===r?0:o?1:-1:s*l(t.v,n.v,e)})}return u.flatMap(t=>i(t.rows,n+1))};return i(w(),0)}const b=h(n.sort||[]),v=t._groups;if(!v)return g(w(),b);const x=new Set,S=[];for(const t of v)t.rows.forEach(t=>x.add(t)),S.push(...g(t.rows,b));const C=[];for(let t=1;t<=i;t++)x.has(t)||C.push(t);return[...S,...g(C,b)]}function h(t,e){const n=e.pageSize;if(!n)return e.page=0,t;const o=Math.max(0,Math.ceil(t.length/n)-1);return e.page=Math.min(Math.max(e.page||0,0),o),t.slice(e.page*n,(e.page+1)*n)}function m(n,o){const l=o.interactive,p=o._cols||[],m=((o.data||{})[p[0]]||[]).length,v=o._groups,x={},S=v?[]:null;if(v){const t=new Set;v.forEach(e=>e.rows.forEach(n=>{x[n]=e,S.push(n),t.add(n)}));for(let e=1;e<=m;e++)t.has(e)||S.push(e)}const C=o._rowspan?o._rowspan.length:0,E=C?[...o._rowspan,...p]:p,N=function(t,e,n){const o={};e.forEach(t=>o[t]=[]);let s=0;return i(t,"tbody tr").forEach(t=>{if(t.classList.contains("lt-row-group"))return;const l=n?n[s]:s+1,r=c(t);e.forEach((t,e)=>o[t][l-1]=r[e]?.textContent??""),s++}),o}(n,p,S),k=[...i(n,"thead tr")].pop(),M={filters:{},predicates:{},page:0,pageSize:0,sort:Array.isArray(l.sort)?l.sort.map(s):[]};let D,$=()=>{};const A=[],F=(t=!0)=>{t&&(D=f(o,N,M),M.page=0);const s=h(D,M),l=(t=>{if(!v)return t;const e=[];let n;for(const o of t){const t=x[o];t!==n&&(t&&e.push({label:t.label,raw:t.raw}),n=t),e.push(o)}return e})(s),i=u(n.ownerDocument,"template",{innerHTML:e.buildHtml({...o,_viewRows:l})}),a=r(i.content,"tbody");s.length?A.forEach(t=>t(a,l)):a.innerHTML=`<tr class="lti-empty"><td colspan="${p.length}">—</td></tr>`,r(n,"tbody").replaceWith(a),$(D.length)};n._lt={spec:o,state:M,refresh:()=>F(),view:()=>D||f(o,N,M),filter(t,e){e?M.predicates[t]=e:delete M.predicates[t],F()}};const _=l.filter,z=_?.cols?Object.keys(_.cols).filter(t=>!0!==_.cols[t]&&!p.includes(t)):[],R=!1!==l.search||l.hide||z.length||l.download?u(n.ownerDocument,"div",{className:"lti-bar"},w(n.tHead||n.createTHead(),"lti-head",p.length,0)):null,j=c(k).map(t=>t.textContent);!1!==l.sort&&function(t,e,n,o){const s=t.ownerDocument,l={},r=()=>{for(const t in l)l[t].th.removeAttribute("aria-sort"),l[t].ind.textContent="";const t=n.sort||[];t.forEach(({col:e,dir:n},o)=>{const s=l[e];s&&(s.th.setAttribute("aria-sort","desc"===n?"descending":"ascending"),s.ind.textContent=("desc"===n?"▼":"▲")+(t.length>1?String(o+1).replace(/\d/g,t=>"₀₁₂₃₄₅₆₇₈₉"[t]):""))})};i(t,"th").forEach((t,i)=>{const a=e[i];if(null==a)return;t.classList.add("lti-sortable");const c=u(s,"span",{className:"lti-label"},t);for(;t.firstChild!==c;)c.append(t.firstChild);l[a]={th:t,ind:u(s,"span",{className:"lti-sort"},c)},c.onclick=t=>{!function(t,e,n){const o=(t.sort||[]).slice(),s=o.findIndex(t=>t.col===e),l=s<0?"asc":"asc"===o[s].dir?"desc":null;if(!n)return void(t.sort=l?[{col:e,dir:l}]:[]);s<0?o.push({col:e,dir:l}):l?o[s]={col:e,dir:l}:o.splice(s,1);t.sort=o}(n,a,t.shiftKey),r(),o()}}),r()}(k,E,M,F),_&&(_.default||_.cols&&Object.keys(_.cols).some(t=>p.includes(t)))&&function(t,e,n,o,s,l){const r=t.ownerDocument,i=u(r,"tr",{className:"lti-filters"}),a=n.default,c=n.cols||{};e.forEach(t=>{const e=u(r,"td",{},i),n=t in c?c[t]:a;if(n)if(!0===n){y(e.appendChild(g(r,`Filter ${t}`)),e=>{e?s.filters[t]=e:delete s.filters[t],l()})}else e.appendChild(L(r,t,n,o,s,l,null))}),t.parentNode.appendChild(i)}(k,p,_,o.data,M,F);const O=l.resize?function(t,e,n,o=0){const s=[...i(e,"th")],l=function(t,e,n=0){let o=r(t,"colgroup");if(!o){const s=t.ownerDocument;o=u(s,"colgroup");for(let t=0;t<n+e;t++)u(s,"col",{},o);t.caption?t.caption.after(o):t.prepend(o)}return[...o.children]}(t,n,o),a=t=>t.getBoundingClientRect().width,c=()=>{if(t.classList.contains("lti-fixed"))return;const e=s.map(a);t.style.width=a(t)+"px",l.forEach((t,n)=>t.style.width=e[n]+"px"),t.classList.add("lti-fixed")},d=e=>{const n=l.map(t=>t.style.width),o=t.style.width;l.forEach(t=>t.style.width=""),t.style.width="",t.classList.remove("lti-fixed");const r=a(s[e]);return l.forEach((t,e)=>t.style.width=n[e]),t.style.width=o,t.classList.add("lti-fixed"),r},p=(e,n,o=24)=>{const s=parseFloat(l[e].style.width);l[e].style.width=Math.max(n,o)+"px",t.style.width=parseFloat(t.style.width)+parseFloat(l[e].style.width)-s+"px"};return{ths:s,cs:l,dataCs:l.slice(o),freeze:c,natural:d,setWidth:p}}(n,k,p.length,C):null;l.resize&&function(t,e){const n=t.ownerDocument,{ths:o,cs:s,freeze:l,natural:r,setWidth:i}=e;o.forEach((e,o)=>{const a=u(n,"div",{className:"lti-resizer"},e);a.ondblclick=t=>{t.stopPropagation(),l(),i(o,r(o))},a.onpointerdown=e=>{e.stopPropagation(),l();const n=e.clientX,r=parseFloat(s[o].style.width);t.classList.add("lti-resizing"),d(e,t=>i(o,r+t.clientX-n),()=>t.classList.remove("lti-resizing"))}})}(n,O);const T=l.hide||l.download?u(n.ownerDocument,"div",{className:"lti-icons"},R):null;if(l.hide&&function(t,e,n,o,s,l,r,i){const d=e.ownerDocument,p=l&&l.hidden||[],f=new Set,h=(t,e,n)=>{const o=c(t)[e];o&&1===o.colSpan&&(o.hidden=n)};i.push(t=>f.forEach(e=>{for(const n of t.rows)h(n,e,!0)}));const m=t=>{const n=f.has(t);r?.dataCs[t]&&(r.dataCs[t].hidden=n);for(const o of e.rows)o.classList.contains("lt-spanner-row")||h(o,t,n)},g=u(d,"span",{className:"lti-cols"},t),w=u(d,"button",{type:"button",title:"Columns","aria-label":"Show or hide columns","aria-expanded":"false"},g),y=u(d,"div",{className:"lti-menu",hidden:!0},g);o.forEach((t,e)=>{if(null==t)return;const n=u(d,"label",{},y),o=u(d,"input",{type:"checkbox",checked:!0},n);n.append(s[e]??t),p.includes(t)&&(o.checked=!1,f.add(e)),o.onchange=()=>{o.checked?f.delete(e):f.add(e),m(e)}}),o.forEach((t,e)=>m(e));const b=t=>{y.hidden=!t,w.setAttribute("aria-expanded",String(t))};w.onclick=t=>{t.stopPropagation(),b(y.hidden)},a(d,"click",t=>{g.contains(t.target)||b(!1)}),a(d,"keydown",t=>{"Escape"===t.key&&b(!1)})}(T,n,0,p,j,l.hide,O,A),l.download&&function(t,e,n,o,s,l){const r=e.ownerDocument,i=("string"==typeof l&&l?l:"table").replace(/(\.csv)?$/i,".csv"),a=o.map((t,e)=>e).filter(t=>null!=o[t]);u(r,"button",{type:"button",className:"lti-download",title:"Download CSV","aria-label":"Download table as CSV"},t).onclick=()=>{const t=n._display||{},l=[a.map(t=>b(s[t]??o[t])).join(",")];for(const n of e._lt.view())l.push(a.map(e=>b((t[o[e]]||[])[n-1])).join(","));const c=URL.createObjectURL(new Blob([l.join("\n")],{type:"text/csv"}));u(r,"a",{href:c,download:i}).click(),URL.revokeObjectURL(c)}}(T,n,o,p,j,l.download),z.length&&function(t,e,n,o,s,l){const r=t.ownerDocument;for(const i of e)t.appendChild(L(r,i,n[i],o,s,l,n[i].label||i))}(u(n.ownerDocument,"div",{className:"lti-chips"},R),z,_.cols,o.data,M,F),!1!==l.search&&function(t,e,n,o){const s=t.appendChild(g(e.ownerDocument,"Search"));y(s,t=>{n.term=t,o()})}(R,n,M,F),l.detail&&A.push(function(n,o,s,l,r,i){const a=n.ownerDocument,d=s.length,p={};l.expanded=new Set;const f=(t,e)=>{const n={};for(const o in t)n[o]=t[o]?.[e-1]??null;return n},h=e=>{if(e in p)return p[e];const n=f(o._display||{},e);let s=null;if(Array.isArray(r)){const t={};for(const e of r)t[e]=[n[e]??null];s={data:t}}else{const l="function"==typeof r?r:t[r];"function"==typeof l&&(s=l(f(o.data||{},e),e,n))}return p[e]=s},m=t=>{l.expanded.has(t)?l.expanded.delete(t):l.expanded.add(t),i()};return(t,n)=>[...t.rows].forEach((t,o)=>{const s=n[o];if("number"!=typeof s)return;const r=c(t)[0],i=l.expanded.has(s);if(!r)return;const p=u(a,"button",{type:"button",className:"lti-expand",textContent:i?"▾":"▸","aria-expanded":String(i),"aria-label":"Toggle detail"});p.onclick=()=>m(s),r.prepend(p);const f=i&&h(s);if(f){const n=u(a,"tr",{className:"lti-detail"});t.after(n);const o=u(a,"td",{colSpan:d},n);e.render(u(a,"div",{},o),f)}})}(n,o,p,M,l.detail,()=>F(!1))),l.pager){const t=Array.isArray(l.pager)?l.pager:[10,25,50,100];$=function(t,e,n,o,s){const l=t.ownerDocument;let r=t.tFoot;r||((r=t.createTFoot()).className="lt-footer");const i=u(l,"div",{className:"lti-pager"},w(r,"lti-pager-row",e.length,-1)),a=u(l,"span",{className:"lti-pos"});o.pageSize=n[0];const c=[()=>0,t=>t-1,t=>t+1,()=>1e9],d=["«","‹","›","»"].map((t,e)=>{const n=u(l,"button",{type:"button",textContent:t,"aria-label":["First","Previous","Next","Last"][e]},i);return n.onclick=()=>{o.page=c[e](o.page),s()},n});if(i.appendChild(a),n.length>1){const t=u(l,"select",{"aria-label":"Rows per page"},i);n.forEach(e=>u(l,"option",{value:e,textContent:e||"∞"},t)),t.onchange=()=>{o.pageSize=+t.value,o.page=0,s()}}return t=>{const e=o.pageSize||t||1,n=Math.max(0,Math.ceil(t/e)-1);a.textContent=t?`${o.page*e+1}–${Math.min(t,(o.page+1)*e)} / ${t}`:"0 / 0",d.forEach((t,e)=>t.disabled=e<2?!o.page:o.page===n)}}(n,p,t,M,()=>F(!1)),F()}else(M.sort.length||l.detail)&&F()}function g(t,e){return u(t,"input",{type:"search",className:"lti-search","aria-label":e})}function w(t,e,n,o){const s=t.insertRow(o);return s.className=e,u(t.ownerDocument,"td",{colSpan:n},s)}function y(t,e){let n;const o=()=>e(t.value);t.oninput=()=>{clearTimeout(n),n=setTimeout(o,150)},t.onchange=()=>{clearTimeout(n),o()}}function b(t){const e=null==t?"":String(t);return/[",\n]/.test(e)?'"'+e.replace(/"/g,'""')+'"':e}const v=t=>""===t?"":`x == ${JSON.stringify(String(t))}`,x=t=>{const e=/^\s*x\s*==\s*(['"])([\s\S]*)\1\s*$/.exec(t||"");return e?e[2]:null},S=(t,e,n,o)=>t<=n&&e>=o?"":`x >= ${t} && x <= ${e}`,C=t=>{const e=/^\s*x\s*>=\s*(-?[\d.]+)\s*&&\s*x\s*<=\s*(-?[\d.]+)\s*$/.exec(t||"");return e?[parseFloat(e[1]),parseFloat(e[2])]:null};function E(t,e,n){const o=e.min,s=e.max,l=s-o||1,r=e.step||l/100,i=u(t,"div",{className:"lti-slider"}),a=u(t,"div",{className:"lti-slider-fill"},i),c=e=>u(t,"button",{type:"button",className:"lti-thumb",role:"slider","aria-label":e,"aria-valuemin":o,"aria-valuemax":s},i),p=[c("Minimum"),c("Maximum")];let f=[o,s];const h=t=>(t-o)/l*100,m=()=>{p.forEach((t,e)=>{t.style.left=h(f[e])+"%",t.setAttribute("aria-valuenow",f[e])}),a.style.left=h(f[0])+"%",a.style.right=100-h(f[1])+"%"},g=(t,e)=>{f[t]=(t=>{const e=Math.round((t-o)/r)*r+o;return Math.min(s,Math.max(o,Math.round(1e6*e)/1e6))})(e),f[0]>f[1]&&(f=[Math.min(...f),Math.max(...f)]),m(),n(f.slice())};return p.forEach((t,e)=>{t.onpointerdown=n=>{t.focus(),d(n,t=>g(e,(t=>{const e=i.getBoundingClientRect();return o+l*Math.min(1,Math.max(0,(t-e.left)/e.width))})(t.clientX)))},t.onkeydown=t=>{const n={ArrowLeft:-1,ArrowDown:-1,ArrowRight:1,ArrowUp:1}[t.key];if(n)g(e,f[e]+n*r);else if("Home"===t.key)g(e,o);else{if("End"!==t.key)return;g(e,s)}t.preventDefault()}}),m(),{el:i,set:t=>{f=t.slice(),m()}}}function N(t,e,n){const o=u(t,"span",{className:"lti-pop"}),s=u(t,"button",{type:"button",className:"lti-funnel",title:e,"aria-label":e,"aria-expanded":"false"},o),l=u(t,"div",{className:"lti-pop-panel",hidden:!0},o);n(l);const r=e=>{l.hidden=!e,s.setAttribute("aria-expanded",String(e)),e&&(()=>{l.style.left=l.style.right="";const e=l.getBoundingClientRect(),n=(t.defaultView||window).innerWidth;e.right>n-4&&(l.style.left="auto",l.style.right="0")})()};return s.onclick=t=>{t.stopPropagation(),r(l.hidden)},a(t,"click",t=>{o.contains(t.target)||r(!1)}),a(t,"keydown",t=>{"Escape"===t.key&&r(!1)}),o}const k={select:{describe:(t,e)=>{const n=x(t);if(null==n)return t?"⋯":"";const o=e.choices.find(t=>t.value===n);return o?o.label:n},init:t=>v(t.selected??(t.choices[0]||{}).value??""),build:(t,e,n)=>{const o=function(t,e,n){const o=u(t,"select",{className:"lti-filter-pick"});return e.forEach(e=>u(t,"option",{value:e.value,textContent:e.label},o)),o.onchange=()=>n(o.value),{el:o,set:t=>o.value=t}}(t,e.choices,t=>n(v(t),s)),s={el:o.el,reflect:t=>{const e=x(t);o.set(null==e?"":e)}};return s}},range:{describe:t=>{const e=C(t);return e?`${e[0]} – ${e[1]}`:t?"⋯":""},init:t=>t.value?S(t.value[0],t.value[1],t.min,t.max):"",build:(t,e,n)=>{const o=u(t,"span",{className:"lti-slider-out"}),s=t=>o.textContent=`${t[0]} – ${t[1]}`,l=E(t,e,t=>{s(t),n(S(t[0],t[1],e.min,e.max),r)}),r={el:[l.el,o],reflect:t=>{const n=C(t)||[e.min,e.max];l.set(n),s(n)}};return r}}};function L(t,e,n,o,s,l,r){const i=function(t,e){if("select"===t.type){let n=t.choices;if(!n){const t=new Set,o=[];for(const n of e)null==n||t.has(n)||(t.add(n),o.push(n));o.sort((t,e)=>"number"==typeof t&&"number"==typeof e?t-e:String(t).localeCompare(String(e))),n=o.map(t=>({value:String(t),label:String(t)}))}return{...t,choices:n}}let{min:n,max:o}=t;if(null==n||null==o){let t=1/0,s=-1/0;for(const n of e){const e=Number(n);isFinite(e)&&(e<t&&(t=e),e>s&&(s=e))}null==n&&(n=isFinite(t)?t:0),null==o&&(o=isFinite(s)?s:1)}return{...t,min:n,max:o}}(n,o[e]||[]),a=k[i.type],c=[];let d=()=>{};const p=(t,n)=>{t?s.filters[e]=t:delete s.filters[e],l();const o=s.filters[e]||"";c.forEach(t=>t!==n&&t.reflect(o)),d(o)},f=N(t,`Filter ${i.label||e}`,n=>{const o=u(t,"input",{type:"search",className:"lti-search","aria-label":`${i.label||e} filter expression`},n),s={reflect:e=>{t.activeElement!==o&&(o.value=e)}};c.push(s),y(o,t=>p(t.trim(),s));const l=a.build(t,i,p);c.push(l),n.append(...[].concat(l.el)),p(a.init(i),null)});let h,m=f;return null!=r&&(m=u(t,"span",{className:"lti-chip"}),u(t,"span",{className:"lti-chip-name",textContent:r},m),h=u(t,"span",{className:"lti-chip-cur"},m),m.appendChild(f)),d=t=>{m.classList.toggle("lti-on",!!t),h&&(h.textContent=a.describe(t,i))},d(s.filters[e]||""),m}const M=(t,e)=>{e?.interactive&&!t.dataset.ltiOn&&(t.dataset.ltiOn="1",m(t,e))};e.plugins.interactive={matcher:p,computeView:f,pageSlice:h,enhance:m},e.onMount.push(M),"undefined"!=typeof document&&i(document,".lt-table").forEach(t=>M(t,t._ltSpec))})("undefined"!=typeof window?window:globalThis);
2
2
  //# sourceMappingURL=lt-interactive.min.js.map