flexdesk 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +326 -0
- package/css/base.css +333 -0
- package/css/flexdesk.css +333 -0
- package/dist/charts.js +2 -2
- package/dist/{chunk-CPYBQN3Q.js → chunk-JLCJWCRS.js} +1617 -58
- package/dist/chunk-JLCJWCRS.js.map +7 -0
- package/dist/{chunk-XYUZF5CD.js → chunk-WKTYKXWF.js} +2 -2
- package/dist/flexdesk.css +333 -0
- package/dist/flow.js +24 -8
- package/dist/flow.js.map +2 -2
- package/dist/tiles.js +2 -2
- package/dist/widgets.js +672 -1
- package/dist/widgets.js.map +4 -4
- package/package.json +1 -1
- package/src/flow/outline/editor.js +46 -8
- package/src/ui/components/data_table.js +1695 -70
- package/src/ui/components/fact_table.js +342 -0
- package/src/ui/components/record_list.js +594 -0
- package/src/ui/components/table_filter_chips.js +153 -0
- package/dist/chunk-CPYBQN3Q.js.map +0 -7
- /package/dist/{chunk-XYUZF5CD.js.map → chunk-WKTYKXWF.js.map} +0 -0
package/dist/widgets.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
ISO_DATETIME,
|
|
27
27
|
PAGING_HARD_LIMIT,
|
|
28
28
|
PAGING_SOFT_LIMIT,
|
|
29
|
+
createChipsFilter,
|
|
29
30
|
createRafResizeObserver,
|
|
30
31
|
formatDate,
|
|
31
32
|
formatDuration,
|
|
@@ -35,7 +36,7 @@ import {
|
|
|
35
36
|
parseDateValue,
|
|
36
37
|
parseDuration,
|
|
37
38
|
resolvePaging
|
|
38
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-JLCJWCRS.js";
|
|
39
40
|
import {
|
|
40
41
|
ManagedWindow,
|
|
41
42
|
ROW_CONTROL_SELECTOR,
|
|
@@ -2238,6 +2239,237 @@ function _populate(header, config) {
|
|
|
2238
2239
|
}
|
|
2239
2240
|
}
|
|
2240
2241
|
|
|
2242
|
+
// src/ui/components/fact_table.js
|
|
2243
|
+
var SEQ = 0;
|
|
2244
|
+
var isNode = (v) => typeof Node !== "undefined" && v instanceof Node;
|
|
2245
|
+
var FactTable = class {
|
|
2246
|
+
/**
|
|
2247
|
+
* @param {HTMLElement|null} host appended to, if given
|
|
2248
|
+
* @param {FactTableConfig} [config]
|
|
2249
|
+
*/
|
|
2250
|
+
constructor(host, config = {}) {
|
|
2251
|
+
this.config = {
|
|
2252
|
+
rows: [],
|
|
2253
|
+
headers: null,
|
|
2254
|
+
compare: false,
|
|
2255
|
+
labelColumn: true,
|
|
2256
|
+
wrapLabels: false,
|
|
2257
|
+
wrap: "anywhere",
|
|
2258
|
+
mono: false,
|
|
2259
|
+
numeric: [],
|
|
2260
|
+
nullDisplay: "",
|
|
2261
|
+
missing: "\u2014",
|
|
2262
|
+
rowClass: null,
|
|
2263
|
+
rowAttrs: null,
|
|
2264
|
+
emptyState: null,
|
|
2265
|
+
label: null,
|
|
2266
|
+
className: null,
|
|
2267
|
+
...config
|
|
2268
|
+
};
|
|
2269
|
+
this._rows = Array.isArray(this.config.rows) ? this.config.rows.slice() : [];
|
|
2270
|
+
this._id = ++SEQ;
|
|
2271
|
+
const table = document.createElement("table");
|
|
2272
|
+
table.className = "twm-facts" + (this.config.compare ? " twm-facts--compare" : "") + (this.config.labelColumn ? "" : " twm-facts--no-label") + (this.config.wrapLabels ? " twm-facts--wrap-labels" : "") + (this.config.wrap === "words" ? " twm-facts--wrap-words" : this.config.wrap === "none" ? " twm-facts--wrap-none" : "") + (this.config.mono ? " twm-facts--mono" : "") + (this.config.className ? ` ${this.config.className}` : "");
|
|
2273
|
+
if (this.config.label) table.setAttribute("aria-label", this.config.label);
|
|
2274
|
+
this.element = table;
|
|
2275
|
+
this._draw();
|
|
2276
|
+
if (host) host.appendChild(table);
|
|
2277
|
+
}
|
|
2278
|
+
/** Replace the rows and draw them. */
|
|
2279
|
+
setRows(rows) {
|
|
2280
|
+
this._rows = Array.isArray(rows) ? rows.slice() : [];
|
|
2281
|
+
this._draw();
|
|
2282
|
+
}
|
|
2283
|
+
getRows() {
|
|
2284
|
+
return this._rows.slice();
|
|
2285
|
+
}
|
|
2286
|
+
/** The `<tr>` drawn for row `idx`, or null. */
|
|
2287
|
+
getRow(idx) {
|
|
2288
|
+
return this.element.tBodies[0]?.rows[idx]?.__factIndex === idx ? this.element.tBodies[0].rows[idx] : null;
|
|
2289
|
+
}
|
|
2290
|
+
dispose() {
|
|
2291
|
+
this.element.remove();
|
|
2292
|
+
}
|
|
2293
|
+
// ── drawing ──────────────────────────────────────────────────────────
|
|
2294
|
+
/** How many columns the sheet has. */
|
|
2295
|
+
_width() {
|
|
2296
|
+
const { compare, headers } = this.config;
|
|
2297
|
+
if (compare) return 4;
|
|
2298
|
+
let n = 2;
|
|
2299
|
+
if (Array.isArray(headers)) n = Math.max(n, headers.length);
|
|
2300
|
+
for (const r of this._rows) if (Array.isArray(r)) n = Math.max(n, r.length);
|
|
2301
|
+
return n;
|
|
2302
|
+
}
|
|
2303
|
+
_draw() {
|
|
2304
|
+
const table = this.element;
|
|
2305
|
+
table.replaceChildren();
|
|
2306
|
+
const width = this._width();
|
|
2307
|
+
const { headers, compare } = this.config;
|
|
2308
|
+
if (Array.isArray(headers) && headers.length) {
|
|
2309
|
+
const thead = document.createElement("thead");
|
|
2310
|
+
const tr = document.createElement("tr");
|
|
2311
|
+
const names = compare && headers.length === 3 ? [headers[0], headers[1], "", headers[2]] : headers;
|
|
2312
|
+
for (let i = 0; i < width; i++) {
|
|
2313
|
+
const th = document.createElement("th");
|
|
2314
|
+
th.scope = "col";
|
|
2315
|
+
appendContent(th, names[i] ?? "");
|
|
2316
|
+
if (this._numeric(i)) th.classList.add("twm-facts__num");
|
|
2317
|
+
tr.appendChild(th);
|
|
2318
|
+
}
|
|
2319
|
+
thead.appendChild(tr);
|
|
2320
|
+
table.appendChild(thead);
|
|
2321
|
+
}
|
|
2322
|
+
const tbody = document.createElement("tbody");
|
|
2323
|
+
this._rows.forEach((row, idx) => tbody.appendChild(this._row(row, idx, width)));
|
|
2324
|
+
if (!this._rows.length) {
|
|
2325
|
+
let content = this.config.emptyState;
|
|
2326
|
+
if (typeof content === "function") content = content();
|
|
2327
|
+
if (content != null && content !== false && content !== "") {
|
|
2328
|
+
const tr = document.createElement("tr");
|
|
2329
|
+
tr.className = "twm-facts__empty-row";
|
|
2330
|
+
const td = document.createElement("td");
|
|
2331
|
+
td.className = "twm-facts__empty";
|
|
2332
|
+
td.colSpan = width;
|
|
2333
|
+
appendContent(td, content);
|
|
2334
|
+
tr.appendChild(td);
|
|
2335
|
+
tbody.appendChild(tr);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
table.classList.toggle("twm-facts--empty", this._rows.length === 0);
|
|
2339
|
+
table.appendChild(tbody);
|
|
2340
|
+
}
|
|
2341
|
+
_numeric(col) {
|
|
2342
|
+
const n = this.config.numeric;
|
|
2343
|
+
return Array.isArray(n) && n.includes(col);
|
|
2344
|
+
}
|
|
2345
|
+
_row(row, idx, width) {
|
|
2346
|
+
const tr = document.createElement("tr");
|
|
2347
|
+
tr.className = "twm-facts__row";
|
|
2348
|
+
tr.__factIndex = idx;
|
|
2349
|
+
tr.__fact = row;
|
|
2350
|
+
if (Array.isArray(row)) {
|
|
2351
|
+
for (let i = 0; i < width; i++) {
|
|
2352
|
+
const td = this._cell(i === 0 && this.config.labelColumn ? "label" : "value", row[i]);
|
|
2353
|
+
if (this._numeric(i)) td.classList.add("twm-facts__num");
|
|
2354
|
+
if (i === row.length - 1 && row.length < width) {
|
|
2355
|
+
td.colSpan = width - i;
|
|
2356
|
+
tr.appendChild(td);
|
|
2357
|
+
break;
|
|
2358
|
+
}
|
|
2359
|
+
tr.appendChild(td);
|
|
2360
|
+
}
|
|
2361
|
+
} else {
|
|
2362
|
+
const r = row && typeof row === "object" ? row : { label: row };
|
|
2363
|
+
if (r.key !== void 0 && r.key !== null) tr.dataset.key = String(r.key);
|
|
2364
|
+
const label = this._cell(this.config.labelColumn ? "label" : "value", r.label);
|
|
2365
|
+
if (r.labelTitle) label.title = String(r.labelTitle);
|
|
2366
|
+
tr.appendChild(label);
|
|
2367
|
+
const tune = (td) => {
|
|
2368
|
+
if (r.mono) td.classList.add("twm-facts__mono");
|
|
2369
|
+
if (r.numeric) td.classList.add("twm-facts__num");
|
|
2370
|
+
return td;
|
|
2371
|
+
};
|
|
2372
|
+
const hasBefore = Object.prototype.hasOwnProperty.call(r, "before");
|
|
2373
|
+
const hasAfter = Object.prototype.hasOwnProperty.call(r, "after");
|
|
2374
|
+
if (this.config.compare && (hasBefore || hasAfter)) {
|
|
2375
|
+
const status = r.status || statusOf(r, hasBefore, hasAfter);
|
|
2376
|
+
tr.classList.add(`twm-facts__row--${String(status).replace(/[^\w-]/g, "")}`);
|
|
2377
|
+
tr.dataset.status = String(status);
|
|
2378
|
+
const before = tune(hasBefore ? this._cell("value", r.before) : this._missing());
|
|
2379
|
+
before.classList.add("twm-facts__before");
|
|
2380
|
+
const arrow = document.createElement("td");
|
|
2381
|
+
arrow.className = "twm-facts__arrow";
|
|
2382
|
+
arrow.setAttribute("aria-hidden", "true");
|
|
2383
|
+
arrow.textContent = "\u2192";
|
|
2384
|
+
const after = tune(hasAfter ? this._cell("value", r.after) : this._missing());
|
|
2385
|
+
after.classList.add("twm-facts__after");
|
|
2386
|
+
tr.append(before, arrow, after);
|
|
2387
|
+
} else {
|
|
2388
|
+
const value = tune(this._cell("value", r.value));
|
|
2389
|
+
if (width > 2) value.colSpan = width - 1;
|
|
2390
|
+
tr.appendChild(value);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
this._decorate(tr, row, idx);
|
|
2394
|
+
return tr;
|
|
2395
|
+
}
|
|
2396
|
+
_cell(kind, content) {
|
|
2397
|
+
const isLabel = kind === "label";
|
|
2398
|
+
const td = document.createElement(isLabel ? "th" : "td");
|
|
2399
|
+
td.className = isLabel ? "twm-facts__label" : "twm-facts__value";
|
|
2400
|
+
if (isLabel) td.scope = "row";
|
|
2401
|
+
if (content === null || content === void 0) {
|
|
2402
|
+
const said = isLabel ? "" : this.config.nullDisplay;
|
|
2403
|
+
if (said) {
|
|
2404
|
+
td.textContent = String(said);
|
|
2405
|
+
td.classList.add("twm-facts__null");
|
|
2406
|
+
}
|
|
2407
|
+
} else {
|
|
2408
|
+
appendContent(td, content);
|
|
2409
|
+
}
|
|
2410
|
+
return td;
|
|
2411
|
+
}
|
|
2412
|
+
_missing() {
|
|
2413
|
+
const td = document.createElement("td");
|
|
2414
|
+
td.className = "twm-facts__value twm-facts__missing";
|
|
2415
|
+
td.textContent = String(this.config.missing ?? "");
|
|
2416
|
+
return td;
|
|
2417
|
+
}
|
|
2418
|
+
_decorate(tr, row, idx) {
|
|
2419
|
+
const { rowClass, rowAttrs } = this.config;
|
|
2420
|
+
if (typeof rowClass === "function") {
|
|
2421
|
+
const c = rowClass(row, idx);
|
|
2422
|
+
const list = Array.isArray(c) ? c : String(c ?? "").split(/\s+/);
|
|
2423
|
+
for (const name of list) if (name) tr.classList.add(String(name));
|
|
2424
|
+
}
|
|
2425
|
+
if (typeof rowAttrs === "function") {
|
|
2426
|
+
for (const [name, value] of Object.entries(rowAttrs(row, idx) || {})) {
|
|
2427
|
+
if (value == null || value === false) continue;
|
|
2428
|
+
try {
|
|
2429
|
+
tr.setAttribute(name, value === true ? "" : String(value));
|
|
2430
|
+
} catch (err) {
|
|
2431
|
+
console.error(`[FactTable] rowAttrs: bad attribute "${name}"`, err);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2437
|
+
function createFactTable(host, config = {}) {
|
|
2438
|
+
return new FactTable(host, config);
|
|
2439
|
+
}
|
|
2440
|
+
function statusOf(r, hasBefore, hasAfter) {
|
|
2441
|
+
if (!hasBefore) return "added";
|
|
2442
|
+
if (!hasAfter) return "removed";
|
|
2443
|
+
if (isNode(r.before) || isNode(r.after)) return "changed";
|
|
2444
|
+
if (Object.is(r.before, r.after)) return "same";
|
|
2445
|
+
if (r.before && r.after && typeof r.before === "object" && typeof r.after === "object") {
|
|
2446
|
+
try {
|
|
2447
|
+
return JSON.stringify(r.before) === JSON.stringify(r.after) ? "same" : "changed";
|
|
2448
|
+
} catch {
|
|
2449
|
+
return "changed";
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
return "changed";
|
|
2453
|
+
}
|
|
2454
|
+
function appendContent(el, content) {
|
|
2455
|
+
if (content === null || content === void 0) return;
|
|
2456
|
+
if (isNode(content)) {
|
|
2457
|
+
el.appendChild(content);
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
let text;
|
|
2461
|
+
if (typeof content === "object") {
|
|
2462
|
+
try {
|
|
2463
|
+
text = JSON.stringify(content, null, 2);
|
|
2464
|
+
} catch {
|
|
2465
|
+
text = String(content);
|
|
2466
|
+
}
|
|
2467
|
+
} else {
|
|
2468
|
+
text = String(content);
|
|
2469
|
+
}
|
|
2470
|
+
el.appendChild(document.createTextNode(text));
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2241
2473
|
// src/ui/components/gallery_picker.js
|
|
2242
2474
|
function esc(s) {
|
|
2243
2475
|
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
|
|
@@ -2529,6 +2761,440 @@ var NotificationHistory = class {
|
|
|
2529
2761
|
}
|
|
2530
2762
|
};
|
|
2531
2763
|
|
|
2764
|
+
// src/ui/components/record_list.js
|
|
2765
|
+
var OWN_ARROWS = 'input, select, textarea, [contenteditable=""], [contenteditable="true"]';
|
|
2766
|
+
var safeWord = (s) => String(s).replace(/[^\w-]/g, "");
|
|
2767
|
+
var SEQ2 = 0;
|
|
2768
|
+
var RecordList = class {
|
|
2769
|
+
/**
|
|
2770
|
+
* @param {HTMLElement|null} host appended to, if given
|
|
2771
|
+
* @param {RecordListConfig} [config]
|
|
2772
|
+
*/
|
|
2773
|
+
constructor(host, config = {}) {
|
|
2774
|
+
this.config = {
|
|
2775
|
+
records: [],
|
|
2776
|
+
describe: null,
|
|
2777
|
+
getKey: null,
|
|
2778
|
+
onActivate: null,
|
|
2779
|
+
activateOn: ["click", "enter"],
|
|
2780
|
+
keyboard: void 0,
|
|
2781
|
+
activeKey: null,
|
|
2782
|
+
rowClass: null,
|
|
2783
|
+
rowAttrs: null,
|
|
2784
|
+
emptyState: null,
|
|
2785
|
+
factSeparator: " \u2014 ",
|
|
2786
|
+
facts: "wrap",
|
|
2787
|
+
verbClass: null,
|
|
2788
|
+
label: null,
|
|
2789
|
+
className: null,
|
|
2790
|
+
...config
|
|
2791
|
+
};
|
|
2792
|
+
this._records = Array.isArray(this.config.records) ? this.config.records.slice() : [];
|
|
2793
|
+
this._activeKey = this.config.activeKey ?? null;
|
|
2794
|
+
this._tabKey = null;
|
|
2795
|
+
this._items = /* @__PURE__ */ new Map();
|
|
2796
|
+
this._disposed = false;
|
|
2797
|
+
this._id = `twm-record-list-${++SEQ2}`;
|
|
2798
|
+
const root = document.createElement("div");
|
|
2799
|
+
root.className = "twm-record-list" + (this.config.facts === "ellipsis" ? " twm-record-list--clip" : "") + (this._activates() ? " twm-record-list--clickable" : "") + (this.config.className ? ` ${this.config.className}` : "");
|
|
2800
|
+
const list = document.createElement("ul");
|
|
2801
|
+
list.className = "twm-record-list__items";
|
|
2802
|
+
if (this.config.label) list.setAttribute("aria-label", this.config.label);
|
|
2803
|
+
root.appendChild(list);
|
|
2804
|
+
this.element = root;
|
|
2805
|
+
this.listElement = list;
|
|
2806
|
+
this._onClick = (ev) => this._gesture(ev, "click");
|
|
2807
|
+
this._onDblClick = (ev) => this._gesture(ev, "dblclick");
|
|
2808
|
+
this._onKeyDown = (ev) => this._key(ev);
|
|
2809
|
+
this._onFocusIn = (ev) => {
|
|
2810
|
+
const li = this._itemOf(ev.target);
|
|
2811
|
+
if (li && ev.target === li) this._setTabStop(li.__recordKey);
|
|
2812
|
+
};
|
|
2813
|
+
list.addEventListener("click", this._onClick);
|
|
2814
|
+
list.addEventListener("dblclick", this._onDblClick);
|
|
2815
|
+
list.addEventListener("keydown", this._onKeyDown);
|
|
2816
|
+
list.addEventListener("focusin", this._onFocusIn);
|
|
2817
|
+
this._draw();
|
|
2818
|
+
if (host) host.appendChild(root);
|
|
2819
|
+
}
|
|
2820
|
+
// ── public ───────────────────────────────────────────────────────────
|
|
2821
|
+
/** Replace the records and draw them. Focus inside the list is kept on
|
|
2822
|
+
* the same record, and on the same verb of it, when it is still there. */
|
|
2823
|
+
setRecords(records) {
|
|
2824
|
+
if (this._disposed) return;
|
|
2825
|
+
this._records = Array.isArray(records) ? records.slice() : [];
|
|
2826
|
+
this._draw();
|
|
2827
|
+
}
|
|
2828
|
+
/** The records, as last given. */
|
|
2829
|
+
getRecords() {
|
|
2830
|
+
return this._records.slice();
|
|
2831
|
+
}
|
|
2832
|
+
/** Draw ONE record again (`describe` asked afresh), or every one with no key. */
|
|
2833
|
+
refresh(key) {
|
|
2834
|
+
if (this._disposed) return;
|
|
2835
|
+
if (key === void 0) {
|
|
2836
|
+
this._draw();
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
const old = this._items.get(this._norm(key));
|
|
2840
|
+
if (!old) return;
|
|
2841
|
+
const idx = old.__recordIndex;
|
|
2842
|
+
const focus = this._focusMemo();
|
|
2843
|
+
const fresh = this._item(this._records[idx], idx);
|
|
2844
|
+
old.replaceWith(fresh);
|
|
2845
|
+
this._items.set(fresh.__recordKey, fresh);
|
|
2846
|
+
this._paintTabStop();
|
|
2847
|
+
this._restoreFocus(focus);
|
|
2848
|
+
}
|
|
2849
|
+
/** Mark the record that is open (`null` for none). */
|
|
2850
|
+
setActiveKey(key) {
|
|
2851
|
+
this._activeKey = key ?? null;
|
|
2852
|
+
for (const li of this._items.values()) this._paintActive(li);
|
|
2853
|
+
if (this._activeKey !== null && this._items.has(this._norm(this._activeKey))) {
|
|
2854
|
+
this._setTabStop(this._norm(this._activeKey));
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
getActiveKey() {
|
|
2858
|
+
return this._activeKey;
|
|
2859
|
+
}
|
|
2860
|
+
/** The `<li>` drawn for a key, or null. */
|
|
2861
|
+
getItem(key) {
|
|
2862
|
+
return this._items.get(this._norm(key)) || null;
|
|
2863
|
+
}
|
|
2864
|
+
/** Move the keyboard focus to a record. */
|
|
2865
|
+
focusRecord(key) {
|
|
2866
|
+
const li = this.getItem(key);
|
|
2867
|
+
if (!li) return false;
|
|
2868
|
+
this._setTabStop(li.__recordKey);
|
|
2869
|
+
li.focus();
|
|
2870
|
+
return true;
|
|
2871
|
+
}
|
|
2872
|
+
dispose() {
|
|
2873
|
+
if (this._disposed) return;
|
|
2874
|
+
this._disposed = true;
|
|
2875
|
+
const list = this.listElement;
|
|
2876
|
+
list.removeEventListener("click", this._onClick);
|
|
2877
|
+
list.removeEventListener("dblclick", this._onDblClick);
|
|
2878
|
+
list.removeEventListener("keydown", this._onKeyDown);
|
|
2879
|
+
list.removeEventListener("focusin", this._onFocusIn);
|
|
2880
|
+
this._items.clear();
|
|
2881
|
+
this.element.remove();
|
|
2882
|
+
}
|
|
2883
|
+
// ── drawing ──────────────────────────────────────────────────────────
|
|
2884
|
+
_activates() {
|
|
2885
|
+
return typeof this.config.onActivate === "function";
|
|
2886
|
+
}
|
|
2887
|
+
_keyboard() {
|
|
2888
|
+
return this.config.keyboard ?? this._activates();
|
|
2889
|
+
}
|
|
2890
|
+
_gestures() {
|
|
2891
|
+
const g = this.config.activateOn;
|
|
2892
|
+
return new Set(Array.isArray(g) ? g : [g]);
|
|
2893
|
+
}
|
|
2894
|
+
/** Keys compare as strings: a record keyed `7` and an `activeKey` of `'7'` are one. */
|
|
2895
|
+
_norm(key) {
|
|
2896
|
+
return key === null || key === void 0 ? key : String(key);
|
|
2897
|
+
}
|
|
2898
|
+
_spec(record, idx) {
|
|
2899
|
+
const { describe } = this.config;
|
|
2900
|
+
const spec = typeof describe === "function" ? describe(record, idx) : record;
|
|
2901
|
+
return spec && typeof spec === "object" ? spec : { name: spec };
|
|
2902
|
+
}
|
|
2903
|
+
_keyOf(record, idx, spec) {
|
|
2904
|
+
const { getKey } = this.config;
|
|
2905
|
+
let key;
|
|
2906
|
+
if (spec.key !== void 0 && spec.key !== null) key = spec.key;
|
|
2907
|
+
else if (typeof getKey === "function") key = getKey(record, idx);
|
|
2908
|
+
else if (record && typeof record === "object" && (record.key ?? record.id) != null) key = record.key ?? record.id;
|
|
2909
|
+
else key = idx;
|
|
2910
|
+
return String(key);
|
|
2911
|
+
}
|
|
2912
|
+
_draw() {
|
|
2913
|
+
const focus = this._focusMemo();
|
|
2914
|
+
const list = this.listElement;
|
|
2915
|
+
list.replaceChildren();
|
|
2916
|
+
this._items.clear();
|
|
2917
|
+
this.element.querySelector(":scope > .twm-record-list__empty")?.remove();
|
|
2918
|
+
this._records.forEach((record, idx) => {
|
|
2919
|
+
const li = this._item(record, idx);
|
|
2920
|
+
if (this._items.has(li.__recordKey)) {
|
|
2921
|
+
console.error(`[RecordList] two records share the key "${li.__recordKey}"`);
|
|
2922
|
+
}
|
|
2923
|
+
this._items.set(li.__recordKey, li);
|
|
2924
|
+
list.appendChild(li);
|
|
2925
|
+
});
|
|
2926
|
+
this.element.classList.toggle("twm-record-list--empty", this._records.length === 0);
|
|
2927
|
+
if (!this._records.length) this._drawEmpty();
|
|
2928
|
+
this._paintTabStop();
|
|
2929
|
+
this._restoreFocus(focus);
|
|
2930
|
+
}
|
|
2931
|
+
_drawEmpty() {
|
|
2932
|
+
let content = this.config.emptyState;
|
|
2933
|
+
if (typeof content === "function") content = content();
|
|
2934
|
+
if (content == null || content === false || content === "") return;
|
|
2935
|
+
const p = document.createElement("p");
|
|
2936
|
+
p.className = "twm-record-list__empty";
|
|
2937
|
+
if (typeof Node !== "undefined" && content instanceof Node) p.appendChild(content);
|
|
2938
|
+
else p.textContent = String(content);
|
|
2939
|
+
this.element.appendChild(p);
|
|
2940
|
+
}
|
|
2941
|
+
_item(record, idx) {
|
|
2942
|
+
const spec = this._spec(record, idx);
|
|
2943
|
+
const key = this._keyOf(record, idx, spec);
|
|
2944
|
+
const li = document.createElement("li");
|
|
2945
|
+
li.className = "twm-record";
|
|
2946
|
+
li.__record = record;
|
|
2947
|
+
li.__recordIndex = idx;
|
|
2948
|
+
li.__recordKey = key;
|
|
2949
|
+
li.dataset.recordKey = key;
|
|
2950
|
+
if (this._keyboard()) li.tabIndex = -1;
|
|
2951
|
+
if (spec.state) {
|
|
2952
|
+
li.dataset.state = safeWord(spec.state);
|
|
2953
|
+
li.classList.add(`twm-record--state-${safeWord(spec.state)}`);
|
|
2954
|
+
}
|
|
2955
|
+
const main = document.createElement("div");
|
|
2956
|
+
main.className = "twm-record__main";
|
|
2957
|
+
const name = document.createElement("div");
|
|
2958
|
+
name.className = "twm-record__name" + (spec.mono ? " twm-record__name--mono" : "");
|
|
2959
|
+
appendContent2(name, spec.name);
|
|
2960
|
+
main.appendChild(name);
|
|
2961
|
+
const factsText = this._facts(spec.facts);
|
|
2962
|
+
if (factsText !== null) {
|
|
2963
|
+
const facts = document.createElement("div");
|
|
2964
|
+
facts.className = "twm-record__facts";
|
|
2965
|
+
appendContent2(facts, factsText);
|
|
2966
|
+
if (this.config.facts === "ellipsis") facts.title = facts.textContent;
|
|
2967
|
+
main.appendChild(facts);
|
|
2968
|
+
}
|
|
2969
|
+
if (spec.error != null && spec.error !== false && spec.error !== "") {
|
|
2970
|
+
const err = document.createElement("div");
|
|
2971
|
+
err.className = "twm-record__error";
|
|
2972
|
+
appendContent2(err, spec.error);
|
|
2973
|
+
main.appendChild(err);
|
|
2974
|
+
}
|
|
2975
|
+
li.appendChild(main);
|
|
2976
|
+
const pills = (spec.pills || []).filter((p) => p != null && p !== false && p !== "");
|
|
2977
|
+
if (pills.length) {
|
|
2978
|
+
const box = document.createElement("div");
|
|
2979
|
+
box.className = "twm-record__pills";
|
|
2980
|
+
for (const p of pills) box.appendChild(pill(p));
|
|
2981
|
+
li.appendChild(box);
|
|
2982
|
+
}
|
|
2983
|
+
const verbs = (spec.verbs || []).filter(Boolean);
|
|
2984
|
+
if (verbs.length) {
|
|
2985
|
+
const box = document.createElement("div");
|
|
2986
|
+
box.className = "twm-record__verbs";
|
|
2987
|
+
const reasons = /* @__PURE__ */ new Map();
|
|
2988
|
+
for (const v of verbs) {
|
|
2989
|
+
const b = this._verb(v, record);
|
|
2990
|
+
box.appendChild(b);
|
|
2991
|
+
if (v.refusal) {
|
|
2992
|
+
const said = String(v.refusal);
|
|
2993
|
+
if (!reasons.has(said)) reasons.set(said, { labels: [], buttons: [] });
|
|
2994
|
+
reasons.get(said).labels.push(v.label);
|
|
2995
|
+
reasons.get(said).buttons.push(b);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
li.appendChild(box);
|
|
2999
|
+
if (reasons.size) {
|
|
3000
|
+
const lines = document.createElement("div");
|
|
3001
|
+
lines.className = "twm-record__refusals";
|
|
3002
|
+
let n = 0;
|
|
3003
|
+
for (const [said, { labels, buttons }] of reasons) {
|
|
3004
|
+
const line = document.createElement("div");
|
|
3005
|
+
line.className = "twm-record__refusal";
|
|
3006
|
+
line.id = `${this._id}-r${idx}-${n++}`;
|
|
3007
|
+
line.dataset.refusalFor = labels.join(", ");
|
|
3008
|
+
line.textContent = labels.length === verbs.length && verbs.length > 1 ? said : `${labels.join(", ")}: ${said}`;
|
|
3009
|
+
for (const b of buttons) b.setAttribute("aria-describedby", line.id);
|
|
3010
|
+
lines.appendChild(line);
|
|
3011
|
+
}
|
|
3012
|
+
main.appendChild(lines);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
this._decorate(li, record, idx);
|
|
3016
|
+
return li;
|
|
3017
|
+
}
|
|
3018
|
+
_facts(facts) {
|
|
3019
|
+
if (facts == null || facts === false) return null;
|
|
3020
|
+
if (typeof Node !== "undefined" && facts instanceof Node) return facts;
|
|
3021
|
+
if (!Array.isArray(facts)) {
|
|
3022
|
+
const s = String(facts);
|
|
3023
|
+
return s === "" ? null : s;
|
|
3024
|
+
}
|
|
3025
|
+
const parts = facts.filter((f) => f != null && f !== false && f !== "");
|
|
3026
|
+
if (!parts.length) return null;
|
|
3027
|
+
if (parts.every((p) => !(typeof Node !== "undefined" && p instanceof Node))) {
|
|
3028
|
+
return parts.map(String).join(this.config.factSeparator);
|
|
3029
|
+
}
|
|
3030
|
+
const frag = document.createDocumentFragment();
|
|
3031
|
+
parts.forEach((p, i) => {
|
|
3032
|
+
if (i) frag.appendChild(document.createTextNode(this.config.factSeparator));
|
|
3033
|
+
appendContent2(frag, p);
|
|
3034
|
+
});
|
|
3035
|
+
return frag;
|
|
3036
|
+
}
|
|
3037
|
+
_verb(v, record) {
|
|
3038
|
+
const b = document.createElement("button");
|
|
3039
|
+
b.type = "button";
|
|
3040
|
+
const { verbClass } = this.config;
|
|
3041
|
+
b.className = typeof verbClass === "function" ? String(verbClass(v) || "") : typeof verbClass === "string" ? verbClass : "twm-btn" + (v.primary ? " twm-btn--primary" : "") + (v.danger ? " twm-btn--danger" : "");
|
|
3042
|
+
b.classList.add("twm-record__verb");
|
|
3043
|
+
b.dataset.verb = String(v.name ?? v.label);
|
|
3044
|
+
if (v.icon) {
|
|
3045
|
+
const glyph = document.createElement("span");
|
|
3046
|
+
glyph.className = "material-symbols-outlined twm-record__verb-icon";
|
|
3047
|
+
glyph.setAttribute("aria-hidden", "true");
|
|
3048
|
+
glyph.textContent = String(v.icon);
|
|
3049
|
+
b.appendChild(glyph);
|
|
3050
|
+
}
|
|
3051
|
+
const label = document.createElement("span");
|
|
3052
|
+
label.className = "twm-record__verb-label";
|
|
3053
|
+
label.textContent = String(v.label ?? "");
|
|
3054
|
+
b.appendChild(label);
|
|
3055
|
+
if (v.refusal) {
|
|
3056
|
+
b.disabled = true;
|
|
3057
|
+
b.title = String(v.refusal);
|
|
3058
|
+
b.dataset.refusal = String(v.refusal);
|
|
3059
|
+
} else {
|
|
3060
|
+
if (v.title) b.title = String(v.title);
|
|
3061
|
+
if (typeof v.onClick === "function") b.addEventListener("click", (ev) => v.onClick(record, ev));
|
|
3062
|
+
}
|
|
3063
|
+
return b;
|
|
3064
|
+
}
|
|
3065
|
+
_decorate(li, record, idx) {
|
|
3066
|
+
const { rowClass, rowAttrs } = this.config;
|
|
3067
|
+
if (typeof rowClass === "function") {
|
|
3068
|
+
const c = rowClass(record, idx);
|
|
3069
|
+
const list = Array.isArray(c) ? c : String(c ?? "").split(/\s+/);
|
|
3070
|
+
for (const name of list) if (name) li.classList.add(String(name));
|
|
3071
|
+
}
|
|
3072
|
+
if (typeof rowAttrs === "function") {
|
|
3073
|
+
for (const [name, value] of Object.entries(rowAttrs(record, idx) || {})) {
|
|
3074
|
+
if (value == null || value === false) continue;
|
|
3075
|
+
try {
|
|
3076
|
+
li.setAttribute(name, value === true ? "" : String(value));
|
|
3077
|
+
} catch (err) {
|
|
3078
|
+
console.error(`[RecordList] rowAttrs: bad attribute "${name}"`, err);
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
this._paintActive(li);
|
|
3083
|
+
}
|
|
3084
|
+
_paintActive(li) {
|
|
3085
|
+
const on = this._activeKey !== null && li.__recordKey === this._norm(this._activeKey);
|
|
3086
|
+
li.classList.toggle("twm-record--active", on);
|
|
3087
|
+
if (on) {
|
|
3088
|
+
if (li.getAttribute("aria-current") !== "true") {
|
|
3089
|
+
li.setAttribute("aria-current", "true");
|
|
3090
|
+
li.__twmAriaCurrent = true;
|
|
3091
|
+
}
|
|
3092
|
+
} else if (li.__twmAriaCurrent) {
|
|
3093
|
+
li.__twmAriaCurrent = false;
|
|
3094
|
+
li.removeAttribute("aria-current");
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
// ── focus ────────────────────────────────────────────────────────────
|
|
3098
|
+
/** The ONE record that takes Tab: the one last focused, else the open
|
|
3099
|
+
* one, else the first. */
|
|
3100
|
+
_paintTabStop() {
|
|
3101
|
+
if (!this._keyboard()) return;
|
|
3102
|
+
let key = this._tabKey;
|
|
3103
|
+
if (key === null || !this._items.has(key)) {
|
|
3104
|
+
const active = this._norm(this._activeKey);
|
|
3105
|
+
key = active !== null && this._items.has(active) ? active : this._items.keys().next().value ?? null;
|
|
3106
|
+
}
|
|
3107
|
+
this._setTabStop(key);
|
|
3108
|
+
}
|
|
3109
|
+
_setTabStop(key) {
|
|
3110
|
+
if (!this._keyboard()) return;
|
|
3111
|
+
this._tabKey = key ?? null;
|
|
3112
|
+
for (const [k, li] of this._items) li.tabIndex = k === this._tabKey ? 0 : -1;
|
|
3113
|
+
}
|
|
3114
|
+
_focusMemo() {
|
|
3115
|
+
const doc = this.element.ownerDocument;
|
|
3116
|
+
const active = doc?.activeElement;
|
|
3117
|
+
if (!active || !this.listElement.contains(active)) return null;
|
|
3118
|
+
const li = this._itemOf(active);
|
|
3119
|
+
if (!li) return null;
|
|
3120
|
+
return { key: li.__recordKey, verb: active.closest(".twm-record__verb")?.dataset.verb ?? null };
|
|
3121
|
+
}
|
|
3122
|
+
_restoreFocus(memo) {
|
|
3123
|
+
if (!memo) return;
|
|
3124
|
+
const li = this._items.get(memo.key);
|
|
3125
|
+
if (!li) return;
|
|
3126
|
+
if (memo.verb !== null) {
|
|
3127
|
+
const b = [...li.querySelectorAll(".twm-record__verb")].find((x) => x.dataset.verb === memo.verb);
|
|
3128
|
+
if (b && !b.disabled) {
|
|
3129
|
+
b.focus();
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
if (this._keyboard()) {
|
|
3134
|
+
this._setTabStop(li.__recordKey);
|
|
3135
|
+
li.focus();
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
_itemOf(target) {
|
|
3139
|
+
const li = target?.closest?.(".twm-record");
|
|
3140
|
+
return li && this.listElement.contains(li) && li.parentNode === this.listElement ? li : null;
|
|
3141
|
+
}
|
|
3142
|
+
// ── gestures ─────────────────────────────────────────────────────────
|
|
3143
|
+
_gesture(ev, gesture) {
|
|
3144
|
+
if (!this._activates() || !this._gestures().has(gesture)) return;
|
|
3145
|
+
const li = this._itemOf(ev.target);
|
|
3146
|
+
if (!li) return;
|
|
3147
|
+
if (!isRowActivation(ev, gesture, { scope: this.listElement })) return;
|
|
3148
|
+
this.config.onActivate(li.__record, li.__recordIndex, ev);
|
|
3149
|
+
}
|
|
3150
|
+
_key(ev) {
|
|
3151
|
+
if (ev.defaultPrevented) return;
|
|
3152
|
+
const li = this._itemOf(ev.target);
|
|
3153
|
+
if (!li) return;
|
|
3154
|
+
if (ev.key === "Enter") {
|
|
3155
|
+
if (ev.target !== li || !this._activates() || !this._gestures().has("enter")) return;
|
|
3156
|
+
if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey) return;
|
|
3157
|
+
ev.preventDefault();
|
|
3158
|
+
this.config.onActivate(li.__record, li.__recordIndex, ev);
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
if (!this._keyboard()) return;
|
|
3162
|
+
if (ev.target !== li && (ev.target.closest(OWN_ARROWS) || !isRowControl(ev.target, li))) return;
|
|
3163
|
+
const items = [...this._items.values()];
|
|
3164
|
+
const at = items.indexOf(li);
|
|
3165
|
+
let next = null;
|
|
3166
|
+
if (ev.key === "ArrowDown") next = items[Math.min(items.length - 1, at + 1)];
|
|
3167
|
+
else if (ev.key === "ArrowUp") next = items[Math.max(0, at - 1)];
|
|
3168
|
+
else if (ev.key === "Home") next = items[0];
|
|
3169
|
+
else if (ev.key === "End") next = items[items.length - 1];
|
|
3170
|
+
else return;
|
|
3171
|
+
ev.preventDefault();
|
|
3172
|
+
if (next) {
|
|
3173
|
+
this._setTabStop(next.__recordKey);
|
|
3174
|
+
next.focus();
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
};
|
|
3178
|
+
function createRecordList(host, config = {}) {
|
|
3179
|
+
return new RecordList(host, config);
|
|
3180
|
+
}
|
|
3181
|
+
function pill(p) {
|
|
3182
|
+
const s = typeof p === "object" && !(typeof Node !== "undefined" && p instanceof Node) ? p : { text: p };
|
|
3183
|
+
const span = document.createElement("span");
|
|
3184
|
+
span.className = "twm-record__pill" + (s.tone ? ` twm-record__pill--${safeWord(s.tone)}` : "");
|
|
3185
|
+
if (typeof Node !== "undefined" && s.text instanceof Node) span.appendChild(s.text);
|
|
3186
|
+
else if (typeof Node !== "undefined" && p instanceof Node) span.appendChild(p);
|
|
3187
|
+
else span.textContent = String(s.text ?? "");
|
|
3188
|
+
if (s.title) span.title = String(s.title);
|
|
3189
|
+
if (s.tone) span.dataset.tone = safeWord(s.tone);
|
|
3190
|
+
return span;
|
|
3191
|
+
}
|
|
3192
|
+
function appendContent2(el, content) {
|
|
3193
|
+
if (content == null || content === false) return;
|
|
3194
|
+
if (typeof Node !== "undefined" && content instanceof Node) el.appendChild(content);
|
|
3195
|
+
else el.appendChild(document.createTextNode(String(content)));
|
|
3196
|
+
}
|
|
3197
|
+
|
|
2532
3198
|
// src/ui/components/slide_out_panel.js
|
|
2533
3199
|
var SlideOutPanel = class extends ComponentBase {
|
|
2534
3200
|
/** @type {HTMLElement} */
|
|
@@ -5827,6 +6493,7 @@ export {
|
|
|
5827
6493
|
DEFAULT_TYPES,
|
|
5828
6494
|
DataTable,
|
|
5829
6495
|
DragReorder,
|
|
6496
|
+
FactTable,
|
|
5830
6497
|
HelpModal,
|
|
5831
6498
|
ISO_DATE,
|
|
5832
6499
|
ISO_DATETIME,
|
|
@@ -5840,6 +6507,7 @@ export {
|
|
|
5840
6507
|
PageBase,
|
|
5841
6508
|
PanelStateMachine,
|
|
5842
6509
|
ROW_CONTROL_SELECTOR,
|
|
6510
|
+
RecordList,
|
|
5843
6511
|
SCALAR_TYPES,
|
|
5844
6512
|
SlideOutPanel,
|
|
5845
6513
|
SliderField,
|
|
@@ -5849,8 +6517,11 @@ export {
|
|
|
5849
6517
|
WindowChromeController,
|
|
5850
6518
|
WindowTaskbar,
|
|
5851
6519
|
attachInlineRenamer,
|
|
6520
|
+
createChipsFilter,
|
|
5852
6521
|
createDetailHeader,
|
|
6522
|
+
createFactTable,
|
|
5853
6523
|
createRafResizeObserver,
|
|
6524
|
+
createRecordList,
|
|
5854
6525
|
createScrollLock,
|
|
5855
6526
|
createTableStateStore,
|
|
5856
6527
|
createTreeCategory,
|