@veil-runtime/ui 0.4.1 → 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/CHANGELOG.md +29 -0
- package/README.md +35 -17
- package/dist/build-manifest.json +10 -10
- package/dist/dom.js +280 -24
- package/dist/interface.d.ts +207 -22
- package/dist/interface.js +151 -32
- package/dist/validation-safety.d.ts +10 -0
- package/dist/validation-safety.js +3 -1
- package/docs/API.md +15 -3
- package/docs/ASYNC-PRESENTATION-0.5.0.md +109 -0
- package/docs/INTERACTIVE-COLLECTIONS-0.5.0.md +139 -0
- package/docs/INTERFACE-CAPABILITIES-0.5.0.md +105 -0
- package/docs/RELEASE-0.5.0.md +47 -0
- package/docs/RELEASE-QUALIFICATION-0.5.0.md +157 -0
- package/docs/RICHER-FORMS-0.5.0.md +69 -0
- package/docs/UNIVERSAL-INTERFACE.md +19 -11
- package/docs/V0.5.0-HANDOVER.md +144 -0
- package/examples/universal/async-presentation.mjs +32 -0
- package/examples/universal/collections.mjs +22 -0
- package/examples/universal/forms.mjs +21 -0
- package/package.json +5 -2
package/dist/dom.js
CHANGED
|
@@ -209,6 +209,16 @@ export function createMountedInterface(target, definition, options = {}) {
|
|
|
209
209
|
return; disposed = true; unmount(); disposeController(); },
|
|
210
210
|
});
|
|
211
211
|
}
|
|
212
|
+
const gridCounts = [1, 2, 3, 4];
|
|
213
|
+
const gridTracks = (count) => `repeat(${count}, minmax(0, 1fr))`;
|
|
214
|
+
const gridStyles = [
|
|
215
|
+
`.veil-interface-grid > *{min-width:0}`,
|
|
216
|
+
`.veil-interface-badge{display:inline-block;border:1px solid currentColor;border-radius:0.25rem;padding:0.1rem 0.4rem}`,
|
|
217
|
+
...gridCounts.map((count) => `.veil-interface-grid-cols-${count}{grid-template-columns:${gridTracks(count)}}`),
|
|
218
|
+
`@media (min-width:600px){${gridCounts.map((count) => `.veil-interface-grid-sm-${count}{grid-template-columns:${gridTracks(count)}}`).join("")}}`,
|
|
219
|
+
`@media (min-width:1024px){${gridCounts.map((count) => `.veil-interface-grid-lg-${count}{grid-template-columns:${gridTracks(count)}}`).join("")}}`,
|
|
220
|
+
].join("");
|
|
221
|
+
const gridGaps = { none: "0", sm: "0.5rem", md: "0.75rem", lg: "1.5rem" };
|
|
212
222
|
/** Mount a framework-neutral interface. The caller owns and disposes the controller separately. */
|
|
213
223
|
export function mountInterface(target, controller, components = {}, onError) {
|
|
214
224
|
let active = true;
|
|
@@ -227,10 +237,29 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
227
237
|
catch { /* Reporting cannot interrupt a transaction. */ }
|
|
228
238
|
};
|
|
229
239
|
const domId = `veil-interface-${++instance}`;
|
|
240
|
+
const style = document.createElement("style");
|
|
241
|
+
style.textContent = gridStyles;
|
|
230
242
|
const make = (tag, className) => { const el = document.createElement(tag); el.className = className; return el; };
|
|
231
243
|
const display = (value) => value === undefined || value === null ? "" : typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
232
244
|
const resolveText = (node, snapshot) => typeof node.text === "string" ? node.text : node.text ? display(resolveInterfaceBinding(snapshot, node.text)) : "";
|
|
233
245
|
const visible = (node, snapshot) => !node.visible || evaluateInterfaceCondition(node.visible, snapshot);
|
|
246
|
+
// Resolve eligibility from accepted state, including ancestors, rather than a
|
|
247
|
+
// creation-time node captured by a retained control's event handler.
|
|
248
|
+
function currentVisibleNode(key, snapshot) {
|
|
249
|
+
const find = (node) => {
|
|
250
|
+
if (!visible(node, snapshot))
|
|
251
|
+
return undefined;
|
|
252
|
+
if (node.key === key)
|
|
253
|
+
return node;
|
|
254
|
+
for (const child of node.children ?? []) {
|
|
255
|
+
const found = find(child);
|
|
256
|
+
if (found)
|
|
257
|
+
return found;
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
};
|
|
261
|
+
return find(snapshot.definition.screen);
|
|
262
|
+
}
|
|
234
263
|
function create(node, snapshot, candidate = false) {
|
|
235
264
|
const children = [];
|
|
236
265
|
let element;
|
|
@@ -238,7 +267,7 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
238
267
|
let updateCustom;
|
|
239
268
|
let live = true;
|
|
240
269
|
const lifetime = new AbortController();
|
|
241
|
-
if (node.component === "Stack" || node.component === "Row")
|
|
270
|
+
if (node.component === "Stack" || node.component === "Row" || node.component === "Grid")
|
|
242
271
|
element = make("div", `veil-interface-${node.component.toLowerCase()}`);
|
|
243
272
|
else if (node.component === "Text")
|
|
244
273
|
element = make("p", "veil-interface-text");
|
|
@@ -253,14 +282,27 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
253
282
|
const button = document.createElement("button");
|
|
254
283
|
button.type = "button";
|
|
255
284
|
button.className = "veil-interface-button";
|
|
256
|
-
const click = () => {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
285
|
+
const click = () => {
|
|
286
|
+
if (active && live) {
|
|
287
|
+
try {
|
|
288
|
+
controller.dispatch(node.action, node.key);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
const snapshot = controller.snapshot();
|
|
292
|
+
const current = currentVisibleNode(node.key, snapshot);
|
|
293
|
+
if (current?.validate && !(current.disabled && evaluateInterfaceCondition(current.disabled, snapshot))) {
|
|
294
|
+
const errors = getInterfaceValidation(snapshot);
|
|
295
|
+
for (const control of root?.element.querySelectorAll("input,select,textarea") ?? []) {
|
|
296
|
+
if (control.dataset["veilField"] && Object.hasOwn(errors, control.dataset["veilField"]) && !control.disabled) {
|
|
297
|
+
control.focus();
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
report(error);
|
|
303
|
+
}
|
|
262
304
|
}
|
|
263
|
-
}
|
|
305
|
+
};
|
|
264
306
|
button.addEventListener("click", click);
|
|
265
307
|
dispose = () => button.removeEventListener("click", click);
|
|
266
308
|
element = button;
|
|
@@ -270,15 +312,29 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
270
312
|
const label = document.createElement("label");
|
|
271
313
|
const input = document.createElement(node.component === "Textarea" ? "textarea" : node.component === "Select" ? "select" : "input");
|
|
272
314
|
input.id = `${domId}-input-${node.key}`;
|
|
315
|
+
input.dataset["veilField"] = node.key;
|
|
273
316
|
label.htmlFor = input.id;
|
|
274
|
-
if (input instanceof HTMLInputElement)
|
|
317
|
+
if (input instanceof HTMLInputElement) {
|
|
275
318
|
input.type = node.inputType ?? "text";
|
|
319
|
+
if (input.type === "number")
|
|
320
|
+
input.step = "any";
|
|
321
|
+
}
|
|
276
322
|
input.style.boxSizing = "border-box";
|
|
277
323
|
input.style.maxWidth = "100%";
|
|
278
324
|
input.style.minWidth = "0";
|
|
279
325
|
const change = () => {
|
|
280
326
|
if (!active || !live)
|
|
281
327
|
return;
|
|
328
|
+
const snapshot = controller.snapshot();
|
|
329
|
+
const current = currentVisibleNode(node.key, snapshot);
|
|
330
|
+
if (!current || current.readOnly || (current.disabled && evaluateInterfaceCondition(current.disabled, snapshot))) {
|
|
331
|
+
const accepted = resolveInterfaceBinding(snapshot, node.binding);
|
|
332
|
+
if (input instanceof HTMLInputElement && input.type === "checkbox")
|
|
333
|
+
input.checked = accepted === true;
|
|
334
|
+
else
|
|
335
|
+
input.value = display(accepted);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
282
338
|
const value = input instanceof HTMLInputElement && input.type === "checkbox" ? input.checked : input.type === "number" && input.value !== "" ? Number(input.value) : input.value;
|
|
283
339
|
try {
|
|
284
340
|
controller.setState(node.binding.path, value);
|
|
@@ -299,7 +355,10 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
299
355
|
feedback.id = `${domId}-feedback-${node.key}`;
|
|
300
356
|
feedback.setAttribute("aria-live", "polite");
|
|
301
357
|
input.setAttribute("aria-describedby", feedback.id);
|
|
302
|
-
|
|
358
|
+
const description = document.createElement("p");
|
|
359
|
+
description.id = `${domId}-description-${node.key}`;
|
|
360
|
+
description.className = "veil-interface-description";
|
|
361
|
+
wrapper.append(label, description, input, feedback);
|
|
303
362
|
element = wrapper;
|
|
304
363
|
}
|
|
305
364
|
else if (node.component === "Table") {
|
|
@@ -350,9 +409,9 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
350
409
|
}
|
|
351
410
|
}
|
|
352
411
|
const result = { node, element, children, dispose: () => { live = false; lifetime.abort(); dispose(); }, updateCustom, visibleDisplay: element.style.display, live: true };
|
|
353
|
-
if (node.component === "Stack" || node.component === "Row") {
|
|
354
|
-
element.style.display = node.component === "
|
|
355
|
-
element.style.gap = "0.75rem";
|
|
412
|
+
if (node.component === "Stack" || node.component === "Row" || node.component === "Grid") {
|
|
413
|
+
element.style.display = node.component === "Row" ? "flex" : "grid";
|
|
414
|
+
element.style.gap = node.component === "Grid" ? gridGaps[node.gap ?? "md"] : "0.75rem";
|
|
356
415
|
element.style.minWidth = "0";
|
|
357
416
|
if (node.component === "Stack")
|
|
358
417
|
element.style.gridTemplateColumns = "minmax(0, 1fr)";
|
|
@@ -382,8 +441,157 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
382
441
|
}
|
|
383
442
|
return result;
|
|
384
443
|
}
|
|
444
|
+
function clearCollection(rendered) {
|
|
445
|
+
for (const row of rendered.collection?.rows.values() ?? [])
|
|
446
|
+
for (const button of row.buttons.values())
|
|
447
|
+
button.dispose();
|
|
448
|
+
delete rendered.collection;
|
|
449
|
+
}
|
|
450
|
+
function cellText(cell, value, badge) {
|
|
451
|
+
const text = display(value);
|
|
452
|
+
if (badge) {
|
|
453
|
+
let span = cell.firstElementChild;
|
|
454
|
+
if (!span || !span.classList.contains("veil-interface-badge")) {
|
|
455
|
+
span = make("span", "veil-interface-badge");
|
|
456
|
+
cell.replaceChildren(span);
|
|
457
|
+
}
|
|
458
|
+
if (span.textContent !== text)
|
|
459
|
+
span.textContent = text;
|
|
460
|
+
}
|
|
461
|
+
else if (cell.firstElementChild || cell.textContent !== text)
|
|
462
|
+
cell.textContent = text;
|
|
463
|
+
}
|
|
464
|
+
function updateCollection(rendered, snapshot) {
|
|
465
|
+
const { node, element } = rendered;
|
|
466
|
+
const scope = JSON.stringify([snapshot.definition.id, node.key, node.binding.source, node.binding.path, node.rowKey]);
|
|
467
|
+
if (rendered.collection?.scope !== scope) {
|
|
468
|
+
clearCollection(rendered);
|
|
469
|
+
const head = document.createElement("thead"), body = document.createElement("tbody");
|
|
470
|
+
rendered.collection = { scope, head, body, rows: new Map() };
|
|
471
|
+
element.replaceChildren(head, body);
|
|
472
|
+
}
|
|
473
|
+
const collection = rendered.collection;
|
|
474
|
+
const columns = Array.isArray(node.columns) ? node.columns : [];
|
|
475
|
+
const header = document.createElement("tr");
|
|
476
|
+
for (const label of [...columns.map(col => col.label), ...(node.rowActions ? ["Actions"] : [])]) {
|
|
477
|
+
const th = document.createElement("th");
|
|
478
|
+
th.scope = "col";
|
|
479
|
+
th.textContent = label;
|
|
480
|
+
header.append(th);
|
|
481
|
+
}
|
|
482
|
+
collection.head.replaceChildren(header);
|
|
483
|
+
const rows = (resolveInterfaceBinding(snapshot, node.binding) ?? []);
|
|
484
|
+
const retained = new Set();
|
|
485
|
+
for (const [index, data] of rows.entries()) {
|
|
486
|
+
const id = data[node.rowKey];
|
|
487
|
+
retained.add(id);
|
|
488
|
+
let row = collection.rows.get(id);
|
|
489
|
+
if (!row) {
|
|
490
|
+
row = { element: document.createElement("tr"), cells: [], actions: document.createElement("td"), buttons: new Map() };
|
|
491
|
+
collection.rows.set(id, row);
|
|
492
|
+
}
|
|
493
|
+
while (row.cells.length > columns.length)
|
|
494
|
+
row.cells.pop().remove();
|
|
495
|
+
for (const [i, col] of columns.entries()) {
|
|
496
|
+
if (!row.cells[i])
|
|
497
|
+
row.cells.push(document.createElement("td"));
|
|
498
|
+
cellText(row.cells[i], Object.hasOwn(data, col.key) ? data[col.key] : undefined, col.presentation === "badge");
|
|
499
|
+
if (row.element.children[i] !== row.cells[i])
|
|
500
|
+
row.element.insertBefore(row.cells[i], row.element.children[i] ?? null);
|
|
501
|
+
}
|
|
502
|
+
const desired = new Set(node.rowActions?.map(item => item.action));
|
|
503
|
+
for (const [action, button] of row.buttons)
|
|
504
|
+
if (!desired.has(action)) {
|
|
505
|
+
button.dispose();
|
|
506
|
+
button.element.remove();
|
|
507
|
+
row.buttons.delete(action);
|
|
508
|
+
}
|
|
509
|
+
for (const [i, action] of (node.rowActions ?? []).entries()) {
|
|
510
|
+
let button = row.buttons.get(action.action);
|
|
511
|
+
if (!button) {
|
|
512
|
+
const control = document.createElement("button");
|
|
513
|
+
control.type = "button";
|
|
514
|
+
let live = true;
|
|
515
|
+
let gesture;
|
|
516
|
+
let renderedRevision = snapshot.revision;
|
|
517
|
+
const clicks = new WeakMap();
|
|
518
|
+
const consumed = new WeakSet();
|
|
519
|
+
const start = () => { gesture = renderedRevision; };
|
|
520
|
+
const key = (event) => {
|
|
521
|
+
if (event.key !== "Enter" && event.key !== " ")
|
|
522
|
+
return;
|
|
523
|
+
if (event.repeat) {
|
|
524
|
+
event.preventDefault();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
start();
|
|
528
|
+
};
|
|
529
|
+
const captureClick = (event) => { clicks.set(event, gesture ?? renderedRevision); gesture = undefined; };
|
|
530
|
+
const click = (event) => {
|
|
531
|
+
if (event.target !== control || !active || !live || !rendered.live || consumed.has(event))
|
|
532
|
+
return;
|
|
533
|
+
consumed.add(event);
|
|
534
|
+
try {
|
|
535
|
+
controller.dispatchRow(action.action, node.key, id, clicks.get(event) ?? renderedRevision);
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
report(error);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
// Capture before target/bubble handlers can commit a replacement dataset.
|
|
542
|
+
const capture = (event) => { if (event.target === control)
|
|
543
|
+
captureClick(event); };
|
|
544
|
+
const pointer = (event) => { if (event.target === control)
|
|
545
|
+
start(); };
|
|
546
|
+
const keyboard = (event) => { if (event.target === control)
|
|
547
|
+
key(event); };
|
|
548
|
+
element.addEventListener("pointerdown", pointer, true);
|
|
549
|
+
element.addEventListener("keydown", keyboard, true);
|
|
550
|
+
element.addEventListener("click", capture, true);
|
|
551
|
+
element.addEventListener("click", click);
|
|
552
|
+
// Store revision separately from gesture so commits cannot refresh an active press.
|
|
553
|
+
button = { element: control, updateRevision(value) { renderedRevision = value; }, dispose() {
|
|
554
|
+
live = false;
|
|
555
|
+
element.removeEventListener("pointerdown", pointer, true);
|
|
556
|
+
element.removeEventListener("keydown", keyboard, true);
|
|
557
|
+
element.removeEventListener("click", capture, true);
|
|
558
|
+
element.removeEventListener("click", click);
|
|
559
|
+
} };
|
|
560
|
+
row.buttons.set(action.action, button);
|
|
561
|
+
}
|
|
562
|
+
button.updateRevision(snapshot.revision);
|
|
563
|
+
// Preserve the text node through data commits during a press.
|
|
564
|
+
if (button.element.textContent !== action.label) {
|
|
565
|
+
if (button.element.firstChild?.nodeType === 3)
|
|
566
|
+
button.element.firstChild.nodeValue = action.label;
|
|
567
|
+
else
|
|
568
|
+
button.element.textContent = action.label;
|
|
569
|
+
}
|
|
570
|
+
button.element.setAttribute("aria-label", `${action.label}, ${id}`);
|
|
571
|
+
if (row.actions.children[i] !== button.element)
|
|
572
|
+
row.actions.insertBefore(button.element, row.actions.children[i] ?? null);
|
|
573
|
+
}
|
|
574
|
+
if (node.rowActions)
|
|
575
|
+
row.element.append(row.actions);
|
|
576
|
+
else
|
|
577
|
+
row.actions.remove();
|
|
578
|
+
if (collection.body.children[index] !== row.element)
|
|
579
|
+
collection.body.insertBefore(row.element, collection.body.children[index] ?? null);
|
|
580
|
+
}
|
|
581
|
+
for (const [id, row] of collection.rows)
|
|
582
|
+
if (!retained.has(id)) {
|
|
583
|
+
for (const button of row.buttons.values())
|
|
584
|
+
button.dispose();
|
|
585
|
+
row.element.remove();
|
|
586
|
+
collection.rows.delete(id);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
385
589
|
function update(rendered, snapshot) {
|
|
386
590
|
const { node, element } = rendered;
|
|
591
|
+
if (node.component === "Grid") {
|
|
592
|
+
element.className = `veil-interface-grid veil-interface-grid-cols-${node.columns ?? 1}${node.columnsAt?.sm ? ` veil-interface-grid-sm-${node.columnsAt.sm}` : ""}${node.columnsAt?.lg ? ` veil-interface-grid-lg-${node.columnsAt.lg}` : ""}`;
|
|
593
|
+
element.style.gap = gridGaps[node.gap ?? "md"];
|
|
594
|
+
}
|
|
387
595
|
const isVisible = visible(node, snapshot);
|
|
388
596
|
element.hidden = !isVisible;
|
|
389
597
|
if (node.component === "Button")
|
|
@@ -393,8 +601,30 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
393
601
|
element.setAttribute("aria-label", node.ariaLabel);
|
|
394
602
|
else
|
|
395
603
|
element.removeAttribute("aria-label");
|
|
396
|
-
|
|
397
|
-
|
|
604
|
+
const phase = node.component === "Status" && node.phase !== undefined ? (typeof node.phase === "string" ? node.phase : resolveInterfaceBinding(snapshot, node.phase)) : undefined;
|
|
605
|
+
if (node.component === "Status") {
|
|
606
|
+
if (phase) {
|
|
607
|
+
element.dataset["phase"] = phase;
|
|
608
|
+
element.tabIndex = -1;
|
|
609
|
+
element.setAttribute("aria-atomic", "true");
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
delete element.dataset["phase"];
|
|
613
|
+
element.removeAttribute("tabindex");
|
|
614
|
+
element.removeAttribute("aria-atomic");
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (["Text", "Heading", "Status", "Button"].includes(node.component)) {
|
|
618
|
+
const text = phase && node.text === undefined ? { loading: "Loading…", ready: "Ready.", empty: "No results.", error: "Unable to complete the request." }[phase] : resolveText(node, snapshot);
|
|
619
|
+
// Retain the pressed button's text node across blur-driven state commits.
|
|
620
|
+
if (element.textContent !== text) {
|
|
621
|
+
const child = element.firstChild;
|
|
622
|
+
if (child?.nodeType === 3 && child === element.lastChild)
|
|
623
|
+
child.nodeValue = text;
|
|
624
|
+
else
|
|
625
|
+
element.textContent = text;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
398
628
|
if (node.component === "List") {
|
|
399
629
|
const items = resolveInterfaceBinding(snapshot, node.binding);
|
|
400
630
|
if (items !== undefined && (!Array.isArray(items) || items.length > 100))
|
|
@@ -410,7 +640,21 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
410
640
|
}
|
|
411
641
|
const errors = getInterfaceValidation(snapshot);
|
|
412
642
|
const error = Object.hasOwn(errors, node.key) ? errors[node.key] : "";
|
|
413
|
-
element.querySelector("span")
|
|
643
|
+
const feedback = element.querySelector("span");
|
|
644
|
+
if (feedback.textContent !== error)
|
|
645
|
+
feedback.textContent = error;
|
|
646
|
+
const description = element.querySelector("p");
|
|
647
|
+
if (description.textContent !== (node.description ?? ""))
|
|
648
|
+
description.textContent = node.description ?? "";
|
|
649
|
+
description.hidden = !node.description;
|
|
650
|
+
input.setAttribute("aria-describedby", [node.description ? description.id : "", feedback.id].filter(Boolean).join(" "));
|
|
651
|
+
if (error)
|
|
652
|
+
input.setAttribute("aria-errormessage", feedback.id);
|
|
653
|
+
else
|
|
654
|
+
input.removeAttribute("aria-errormessage");
|
|
655
|
+
input.disabled = !!node.disabled && evaluateInterfaceCondition(node.disabled, snapshot);
|
|
656
|
+
if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)
|
|
657
|
+
input.readOnly = node.readOnly === true;
|
|
414
658
|
input.setAttribute("aria-invalid", String(!!error));
|
|
415
659
|
input.required = node.validation?.required === true;
|
|
416
660
|
label.textContent = node.label ?? "";
|
|
@@ -428,13 +672,16 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
428
672
|
else
|
|
429
673
|
input.removeAttribute("aria-label");
|
|
430
674
|
}
|
|
431
|
-
if (node.component === "Table")
|
|
675
|
+
if (node.component === "Table" && node.rowKey)
|
|
676
|
+
updateCollection(rendered, snapshot);
|
|
677
|
+
if (node.component === "Table" && !node.rowKey) {
|
|
678
|
+
clearCollection(rendered);
|
|
432
679
|
const rows = resolveInterfaceBinding(snapshot, node.binding);
|
|
433
680
|
if (rows !== undefined && (!Array.isArray(rows) || rows.length > 100))
|
|
434
681
|
throw new Error("Table row limit exceeded");
|
|
435
682
|
const head = document.createElement("thead");
|
|
436
683
|
const header = document.createElement("tr");
|
|
437
|
-
for (const col of node.columns
|
|
684
|
+
for (const col of Array.isArray(node.columns) ? node.columns : []) {
|
|
438
685
|
const cell = document.createElement("th");
|
|
439
686
|
cell.scope = "col";
|
|
440
687
|
cell.textContent = col.label;
|
|
@@ -446,9 +693,9 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
446
693
|
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
447
694
|
throw new Error("Invalid table row");
|
|
448
695
|
const tr = document.createElement("tr");
|
|
449
|
-
for (const col of node.columns
|
|
696
|
+
for (const col of Array.isArray(node.columns) ? node.columns : []) {
|
|
450
697
|
const cell = document.createElement("td");
|
|
451
|
-
cell
|
|
698
|
+
cellText(cell, Object.hasOwn(row, col.key) ? row[col.key] : undefined, col.presentation === "badge");
|
|
452
699
|
tr.append(cell);
|
|
453
700
|
}
|
|
454
701
|
body.append(tr);
|
|
@@ -460,6 +707,7 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
460
707
|
if (!rendered.live)
|
|
461
708
|
return;
|
|
462
709
|
rendered.live = false;
|
|
710
|
+
clearCollection(rendered);
|
|
463
711
|
try {
|
|
464
712
|
rendered.dispose();
|
|
465
713
|
}
|
|
@@ -474,13 +722,13 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
474
722
|
const same = previous.node.key === node.key && previous.node.component === node.component &&
|
|
475
723
|
(node.component !== "Button" || previous.node.action === node.action) &&
|
|
476
724
|
(!isInterfaceField(node) || (previous.node.binding?.path === node.binding?.path && previous.node.inputType === node.inputType)) &&
|
|
477
|
-
["Stack", "Row", "Text", "Heading", "Status", "Input", "Textarea", "Select", "Button", "Table", "List"].includes(node.component);
|
|
725
|
+
["Stack", "Row", "Grid", "Text", "Heading", "Status", "Input", "Textarea", "Select", "Button", "Table", "List"].includes(node.component);
|
|
478
726
|
if (!same) {
|
|
479
727
|
const next = create(node, snapshot, true);
|
|
480
728
|
staged.push(next);
|
|
481
729
|
return () => { previous.element.replaceWith(next.element); destroy(previous); return next; };
|
|
482
730
|
}
|
|
483
|
-
const prior = node.component === "Stack" || node.component === "Row" ? new Map(previous.children.map((child) => [child.node.key, child])) : undefined;
|
|
731
|
+
const prior = node.component === "Stack" || node.component === "Row" || node.component === "Grid" ? new Map(previous.children.map((child) => [child.node.key, child])) : undefined;
|
|
484
732
|
const childPlans = (node.children ?? []).map((child) => {
|
|
485
733
|
const old = prior?.get(child.key);
|
|
486
734
|
if (old) {
|
|
@@ -494,7 +742,7 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
494
742
|
return () => {
|
|
495
743
|
previous.node = node;
|
|
496
744
|
update(previous, snapshot);
|
|
497
|
-
if (node.component === "Stack" || node.component === "Row") {
|
|
745
|
+
if (node.component === "Stack" || node.component === "Row" || node.component === "Grid") {
|
|
498
746
|
const children = childPlans.map((plan) => plan());
|
|
499
747
|
previous.children = children;
|
|
500
748
|
for (let index = 0; index < children.length; index++) {
|
|
@@ -526,7 +774,12 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
526
774
|
const focused = root?.element.contains(document.activeElement) ? document.activeElement : undefined;
|
|
527
775
|
const selection = focused instanceof HTMLInputElement || focused instanceof HTMLTextAreaElement ? { start: focused.selectionStart, end: focused.selectionEnd, direction: focused.selectionDirection } : undefined;
|
|
528
776
|
root = commit();
|
|
529
|
-
|
|
777
|
+
const eligible = (element) => element.isConnected && !element.closest('[hidden]') && !element.matches(':disabled');
|
|
778
|
+
if (focused && !eligible(focused)) {
|
|
779
|
+
const destination = [root.element, ...root.element.querySelectorAll('[data-phase]')].find(element => element.hasAttribute('data-phase') && eligible(element));
|
|
780
|
+
destination?.focus({ preventScroll: true });
|
|
781
|
+
}
|
|
782
|
+
else if (focused?.isConnected && document.activeElement !== focused) {
|
|
530
783
|
focused.focus({ preventScroll: true });
|
|
531
784
|
if (selection && selection.start !== null && selection.end !== null && (focused instanceof HTMLInputElement || focused instanceof HTMLTextAreaElement))
|
|
532
785
|
focused.setSelectionRange(selection.start, selection.end, selection.direction ?? undefined);
|
|
@@ -542,9 +795,11 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
542
795
|
}
|
|
543
796
|
});
|
|
544
797
|
mounted.get(target)?.();
|
|
798
|
+
document.head.append(style);
|
|
545
799
|
target.replaceChildren(root.element);
|
|
546
800
|
}
|
|
547
801
|
catch (error) {
|
|
802
|
+
style.remove();
|
|
548
803
|
unregister?.();
|
|
549
804
|
if (root)
|
|
550
805
|
destroy(root);
|
|
@@ -556,6 +811,7 @@ export function mountInterface(target, controller, components = {}, onError) {
|
|
|
556
811
|
return;
|
|
557
812
|
active = false;
|
|
558
813
|
unregister?.();
|
|
814
|
+
style.remove();
|
|
559
815
|
if (root) {
|
|
560
816
|
destroy(root);
|
|
561
817
|
if (root.element.parentNode === target)
|