@ukiahinsure/a2ui-react-adapter 0.1.12 → 0.1.14

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/dist/index.js CHANGED
@@ -13,9 +13,65 @@ import {
13
13
  import { basicCatalog } from "@a2ui/react/v0_9";
14
14
  import {
15
15
  A2uiError,
16
+ Catalog,
16
17
  A2uiMessageListSchema,
17
18
  MessageProcessor
18
19
  } from "@a2ui/web_core/v0_9";
20
+
21
+ // src/DateTimeInput.tsx
22
+ import { useId, useLayoutEffect, useRef } from "react";
23
+ import { createComponentImplementation } from "@a2ui/react/v0_9";
24
+ import { DateTimeInputApi } from "@a2ui/web_core/v0_9/basic_catalog";
25
+ import { jsx, jsxs } from "react/jsx-runtime";
26
+ var DateTimeInput = createComponentImplementation(
27
+ DateTimeInputApi,
28
+ ({ props }) => {
29
+ const id = useId();
30
+ const inputRef = useRef(null);
31
+ const initialValue = useRef(props.value || "");
32
+ const type = props.enableDate && !props.enableTime ? "date" : !props.enableDate && props.enableTime ? "time" : "datetime-local";
33
+ useLayoutEffect(() => {
34
+ const input = inputRef.current;
35
+ if (input && input.ownerDocument.activeElement !== input && input.value !== (props.value || "")) {
36
+ input.value = props.value || "";
37
+ }
38
+ }, [props.value]);
39
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "var(--a2ui-spacing-xs, 0.25rem)" }, children: [
40
+ props.label && /* @__PURE__ */ jsx("label", { htmlFor: id, style: { fontSize: "var(--a2ui-label-font-size, var(--a2ui-font-size-s))", fontWeight: "bold" }, children: props.label }),
41
+ /* @__PURE__ */ jsx(
42
+ "input",
43
+ {
44
+ ref: inputRef,
45
+ id,
46
+ type,
47
+ "data-a2ui-native-date-draft": "true",
48
+ defaultValue: initialValue.current,
49
+ onChange: (event) => props.setValue(event.currentTarget.value),
50
+ min: typeof props.min === "string" ? props.min : void 0,
51
+ max: typeof props.max === "string" ? props.max : void 0,
52
+ style: {
53
+ backgroundColor: "var(--a2ui-datetimeinput-background, var(--a2ui-color-input, #fff))",
54
+ color: "var(--a2ui-datetimeinput-color, var(--a2ui-color-on-input, #333))",
55
+ border: "var(--a2ui-datetimeinput-border, var(--a2ui-border))",
56
+ borderRadius: "var(--a2ui-datetimeinput-border-radius, var(--a2ui-border-radius))",
57
+ padding: "var(--a2ui-datetimeinput-padding, var(--a2ui-spacing-s))",
58
+ boxSizing: "border-box"
59
+ }
60
+ }
61
+ )
62
+ ] });
63
+ }
64
+ );
65
+
66
+ // src/adapter.ts
67
+ var editableDateCatalog = new Catalog(
68
+ basicCatalog.id,
69
+ [...basicCatalog.components.values()].map(
70
+ (component) => component.name === DateTimeInput.name ? DateTimeInput : component
71
+ ),
72
+ [...basicCatalog.functions.values()],
73
+ basicCatalog.themeSchema
74
+ );
19
75
  var RENDERER_NAME = "a2ui_react_adapter";
20
76
  var RENDERER_VERSION = "0.1.0";
21
77
  var A2UIClientAdapter = class {
@@ -30,6 +86,9 @@ var A2UIClientAdapter = class {
30
86
  #structureBySurface = /* @__PURE__ */ new Map();
31
87
  #persistedListRowsByPath = /* @__PURE__ */ new Map();
32
88
  #deletedListItemIdsByPath = /* @__PURE__ */ new Map();
89
+ #stagedListRowUpdatesBySurface = /* @__PURE__ */ new Map();
90
+ #activeListRowEditsBySurface = /* @__PURE__ */ new Map();
91
+ #optimisticListRowsByPath = /* @__PURE__ */ new Map();
33
92
  #version = 0;
34
93
  #lastSeq = 0;
35
94
  #disposed = false;
@@ -105,6 +164,38 @@ var A2UIClientAdapter = class {
105
164
  }
106
165
  return true;
107
166
  }
167
+ stageListRowUpdate(surfaceId, values) {
168
+ this.#stagedListRowUpdatesBySurface.set(surfaceId, { ...values });
169
+ }
170
+ beginListRowEdit(surfaceId, rowKey, values) {
171
+ let edits = this.#activeListRowEditsBySurface.get(surfaceId);
172
+ if (edits === void 0) {
173
+ edits = /* @__PURE__ */ new Map();
174
+ this.#activeListRowEditsBySurface.set(surfaceId, edits);
175
+ }
176
+ edits.set(rowKey, { ...values });
177
+ }
178
+ updateListRowEdit(surfaceId, rowKey, values) {
179
+ const edits = this.#activeListRowEditsBySurface.get(surfaceId);
180
+ if (edits?.has(rowKey) !== true) {
181
+ return;
182
+ }
183
+ edits.set(rowKey, { ...values });
184
+ }
185
+ getListRowEdit(surfaceId, rowKey) {
186
+ const values = this.#activeListRowEditsBySurface.get(surfaceId)?.get(rowKey);
187
+ return values === void 0 ? void 0 : { ...values };
188
+ }
189
+ clearListRowEdit(surfaceId, rowKey) {
190
+ const edits = this.#activeListRowEditsBySurface.get(surfaceId);
191
+ if (edits === void 0) {
192
+ return;
193
+ }
194
+ edits.delete(rowKey);
195
+ if (edits.size === 0) {
196
+ this.#activeListRowEditsBySurface.delete(surfaceId);
197
+ }
198
+ }
108
199
  #dataModelValueForField(surfaceId, field, value) {
109
200
  const descriptor = this.#fieldDescriptor(surfaceId, field);
110
201
  if (descriptor?.control === "choice" && typeof value === "string" && value.length > 0) {
@@ -159,6 +250,8 @@ var A2UIClientAdapter = class {
159
250
  this.#structureBySurface.clear();
160
251
  this.#persistedListRowsByPath.clear();
161
252
  this.#deletedListItemIdsByPath.clear();
253
+ this.#activeListRowEditsBySurface.clear();
254
+ this.#optimisticListRowsByPath.clear();
162
255
  previous.model.dispose();
163
256
  this.#notify();
164
257
  }
@@ -198,6 +291,10 @@ var A2UIClientAdapter = class {
198
291
  this.#disposed = true;
199
292
  this.#processor.model.dispose();
200
293
  this.#structureBySurface.clear();
294
+ this.#persistedListRowsByPath.clear();
295
+ this.#deletedListItemIdsByPath.clear();
296
+ this.#activeListRowEditsBySurface.clear();
297
+ this.#optimisticListRowsByPath.clear();
201
298
  this.#listeners.clear();
202
299
  this.#actionListeners.clear();
203
300
  this.#errorListeners.clear();
@@ -216,7 +313,8 @@ var A2UIClientAdapter = class {
216
313
  validated.data,
217
314
  envelope.surfaceId,
218
315
  this.#persistedListRowsByPath,
219
- this.#deletedListItemIdsByPath
316
+ this.#deletedListItemIdsByPath,
317
+ this.#optimisticListRowsByPath
220
318
  );
221
319
  const listAddFreeMessages = removeListAddActionMessages(
222
320
  rawMessages,
@@ -272,6 +370,8 @@ var A2UIClientAdapter = class {
272
370
  if (envelope.kind === "snapshot") {
273
371
  this.#persistedListRowsByPath.clear();
274
372
  this.#deletedListItemIdsByPath.clear();
373
+ this.#activeListRowEditsBySurface.clear();
374
+ this.#optimisticListRowsByPath.clear();
275
375
  }
276
376
  rememberPersistedListRows(
277
377
  this.#persistedListRowsByPath,
@@ -284,7 +384,7 @@ var A2UIClientAdapter = class {
284
384
  }
285
385
  #createProcessor() {
286
386
  return new MessageProcessor(
287
- [basicCatalog],
387
+ [editableDateCatalog],
288
388
  (action) => this.#emitAction(action)
289
389
  );
290
390
  }
@@ -297,11 +397,18 @@ var A2UIClientAdapter = class {
297
397
  );
298
398
  return;
299
399
  }
400
+ const stagedListRowUpdate = action.name === "flow.list.update" ? this.#stagedListRowUpdatesBySurface.get(action.surfaceId) : void 0;
401
+ if (stagedListRowUpdate !== void 0) {
402
+ this.#stagedListRowUpdatesBySurface.delete(action.surfaceId);
403
+ }
300
404
  const event = {
301
405
  actionName: action.name,
302
406
  surfaceId: action.surfaceId,
303
407
  sourceComponentId: action.sourceComponentId,
304
- context: { ...action.context }
408
+ context: {
409
+ ...action.context,
410
+ ...stagedListRowUpdate ?? {}
411
+ }
305
412
  };
306
413
  const submittedProviderRow = submittedProviderRowFromEvent(event);
307
414
  if (submittedProviderRow !== void 0) {
@@ -314,6 +421,9 @@ var A2UIClientAdapter = class {
314
421
  if (event.actionName === "flow.list.delete") {
315
422
  this.#removeDeletedListRow(event.surfaceId, event.context);
316
423
  }
424
+ if (event.actionName === "flow.list.update") {
425
+ this.#updateListRow(event.surfaceId, event.context);
426
+ }
317
427
  this.#options.onAction?.(event);
318
428
  for (const listener of this.#actionListeners) {
319
429
  listener(event);
@@ -348,6 +458,7 @@ var A2UIClientAdapter = class {
348
458
  const cachedRows = this.#persistedListRowsByPath.get(listPath) ?? [];
349
459
  const nextRows = removeListRowByItemId(cachedRows, itemId);
350
460
  this.#persistedListRowsByPath.set(listPath, nextRows);
461
+ this.#optimisticListRowsByPath.get(listPath)?.delete(itemId);
351
462
  rememberDeletedListItemId(this.#deletedListItemIdsByPath, listPath, itemId);
352
463
  const surface = this.#processor.model.getSurface(surfaceId);
353
464
  if (surface === void 0) {
@@ -360,6 +471,39 @@ var A2UIClientAdapter = class {
360
471
  );
361
472
  this.#notify();
362
473
  }
474
+ #updateListRow(surfaceId, context) {
475
+ const listPath = context.listPath;
476
+ const itemId = context.itemId;
477
+ if (typeof listPath !== "string" || listPath.length === 0 || typeof itemId !== "string" || itemId.length === 0) {
478
+ return;
479
+ }
480
+ const cachedRows = this.#persistedListRowsByPath.get(listPath) ?? [];
481
+ const existingRow = cachedRows.find(
482
+ (candidate) => isRecord(candidate) && candidate.itemId === itemId
483
+ );
484
+ const row = updateListRowFromContext(context, existingRow);
485
+ if (row === void 0) {
486
+ return;
487
+ }
488
+ const rows = normalizeListRows(listPath, upsertListRow(cachedRows, row));
489
+ this.#persistedListRowsByPath.set(listPath, rows);
490
+ let optimisticRows = this.#optimisticListRowsByPath.get(listPath);
491
+ if (optimisticRows === void 0) {
492
+ optimisticRows = /* @__PURE__ */ new Map();
493
+ this.#optimisticListRowsByPath.set(listPath, optimisticRows);
494
+ }
495
+ optimisticRows.set(itemId, cloneJsonRecord(row));
496
+ const surface = this.#processor.model.getSurface(surfaceId);
497
+ if (surface === void 0) {
498
+ return;
499
+ }
500
+ surface.dataModel.set(`/${listPath}`, cloneJsonArray(rows));
501
+ surface.dataModel.set(
502
+ `/${listPath.replaceAll(".", "/")}`,
503
+ cloneJsonArray(rows)
504
+ );
505
+ this.#notify();
506
+ }
363
507
  #snapshotMessages() {
364
508
  const messages = [];
365
509
  for (const surface of this.#processor.model.surfacesMap.values()) {
@@ -859,8 +1003,8 @@ function fieldFromPointer(path) {
859
1003
  function fieldPointer(field) {
860
1004
  return `/${field.replaceAll("~", "~0").replaceAll("/", "~1")}`;
861
1005
  }
862
- function mergePersistedListRowsIntoMessages(messages, surfaceId, persistedRowsByPath, deletedItemIdsByPath) {
863
- if (persistedRowsByPath.size === 0 && deletedItemIdsByPath.size === 0) {
1006
+ function mergePersistedListRowsIntoMessages(messages, surfaceId, persistedRowsByPath, deletedItemIdsByPath, optimisticRowsByPath) {
1007
+ if (persistedRowsByPath.size === 0 && deletedItemIdsByPath.size === 0 && optimisticRowsByPath.size === 0) {
864
1008
  return [...messages];
865
1009
  }
866
1010
  let mergedRoot = false;
@@ -876,7 +1020,8 @@ function mergePersistedListRowsIntoMessages(messages, surfaceId, persistedRowsBy
876
1020
  value: mergePersistedListRowsIntoRoot(
877
1021
  message.updateDataModel.value,
878
1022
  persistedRowsByPath,
879
- deletedItemIdsByPath
1023
+ deletedItemIdsByPath,
1024
+ optimisticRowsByPath
880
1025
  )
881
1026
  }
882
1027
  };
@@ -894,26 +1039,33 @@ function mergePersistedListRowsIntoMessages(messages, surfaceId, persistedRowsBy
894
1039
  value: mergePersistedListRowsIntoRoot(
895
1040
  {},
896
1041
  persistedRowsByPath,
897
- deletedItemIdsByPath
1042
+ deletedItemIdsByPath,
1043
+ optimisticRowsByPath
898
1044
  )
899
1045
  }
900
1046
  }
901
1047
  ];
902
1048
  }
903
- function mergePersistedListRowsIntoRoot(root, persistedRowsByPath, deletedItemIdsByPath) {
1049
+ function mergePersistedListRowsIntoRoot(root, persistedRowsByPath, deletedItemIdsByPath, optimisticRowsByPath) {
904
1050
  const merged = cloneJsonRecord(root);
905
1051
  for (const listPath of /* @__PURE__ */ new Set([
906
1052
  ...persistedRowsByPath.keys(),
907
- ...deletedItemIdsByPath.keys()
1053
+ ...deletedItemIdsByPath.keys(),
1054
+ ...optimisticRowsByPath.keys()
908
1055
  ])) {
909
1056
  const rows = persistedRowsByPath.get(listPath) ?? [];
1057
+ const optimisticRows = optimisticRowsByPath.get(listPath);
910
1058
  const dottedRows = merged[listPath];
911
1059
  const nestedRows = getNestedValue(merged, listPath);
912
1060
  const incomingRows = Array.isArray(dottedRows) ? dottedRows : Array.isArray(nestedRows) ? nestedRows : void 0;
913
1061
  const shouldPreserveCachedRows = rows.length > 0 && (incomingRows === void 0 || incomingRows.length === 0 && (!hasListDraft(merged, listPath) || hasBlankListDraft(merged, listPath)));
914
1062
  const effectiveRows = shouldPreserveCachedRows ? rows : incomingRows ?? rows;
915
- const visibleRows = filterDeletedListRows(
1063
+ const optimisticEffectiveRows = mergeOptimisticListRows(
916
1064
  effectiveRows,
1065
+ optimisticRows
1066
+ );
1067
+ const visibleRows = filterDeletedListRows(
1068
+ optimisticEffectiveRows,
917
1069
  deletedItemIdsByPath.get(listPath)
918
1070
  );
919
1071
  const normalizedRows = normalizeListRows(listPath, visibleRows, merged);
@@ -949,6 +1101,16 @@ function isBlankDraftValue(value) {
949
1101
  }
950
1102
  return false;
951
1103
  }
1104
+ function mergeOptimisticListRows(rows, optimisticRows) {
1105
+ if (optimisticRows === void 0 || optimisticRows.size === 0) {
1106
+ return cloneJsonArray(rows);
1107
+ }
1108
+ let merged = cloneJsonArray(rows);
1109
+ for (const row of optimisticRows.values()) {
1110
+ merged = upsertListRow(merged, cloneJsonRecord(row));
1111
+ }
1112
+ return merged;
1113
+ }
952
1114
  function rememberPersistedListRows(persistedRowsByPath, deletedItemIdsByPath, root) {
953
1115
  if (!isRecord(root)) {
954
1116
  return;
@@ -1192,6 +1354,35 @@ function isProviderSubmitLabel(label) {
1192
1354
  function normalizeFieldKey(key) {
1193
1355
  return key.trim().toLowerCase().replaceAll(/[^a-z0-9]+/g, "_").replaceAll(/^_+|_+$/g, "");
1194
1356
  }
1357
+ function updateListRowFromContext(context, existingRow) {
1358
+ const itemId = context.itemId;
1359
+ if (typeof itemId !== "string" || itemId.length === 0) {
1360
+ return void 0;
1361
+ }
1362
+ const row = isRecord(existingRow) ? cloneJsonRecord(existingRow) : {};
1363
+ row.itemId = itemId;
1364
+ let hasUpdate = false;
1365
+ for (const [key, value] of Object.entries(context)) {
1366
+ if (key === "listPath" || key === "itemId" || !isFieldValue(value)) {
1367
+ continue;
1368
+ }
1369
+ row[key] = value;
1370
+ hasUpdate = true;
1371
+ }
1372
+ if (!hasUpdate) {
1373
+ return void 0;
1374
+ }
1375
+ row.__displayText = listRowDisplayText(row);
1376
+ return row;
1377
+ }
1378
+ function listRowDisplayText(row) {
1379
+ return Object.entries(row).filter(([key, value]) => {
1380
+ if (key === "itemId" || key === "__displayText" || value === null) {
1381
+ return false;
1382
+ }
1383
+ return isFieldValue(value) && String(value).trim().length > 0;
1384
+ }).map(([key, value]) => `${key}: ${String(value).trim()}`).join(" | ");
1385
+ }
1195
1386
  function upsertListRow(rows, row) {
1196
1387
  const itemId = row.itemId;
1197
1388
  if (typeof itemId !== "string" || itemId.length === 0) {
@@ -1434,13 +1625,13 @@ function classifyProcessingError(error) {
1434
1625
  // src/SurfaceHost.tsx
1435
1626
  import {
1436
1627
  Component,
1437
- useId,
1438
- useLayoutEffect,
1439
- useRef,
1628
+ useId as useId2,
1629
+ useLayoutEffect as useLayoutEffect2,
1630
+ useRef as useRef2,
1440
1631
  useSyncExternalStore
1441
1632
  } from "react";
1442
1633
  import { A2uiSurface } from "@a2ui/react/v0_9";
1443
- import { jsx } from "react/jsx-runtime";
1634
+ import { jsx as jsx2 } from "react/jsx-runtime";
1444
1635
  function A2UISurfaceHost({
1445
1636
  adapter,
1446
1637
  surfaceId,
@@ -1467,18 +1658,18 @@ function A2UISurfaceHost({
1467
1658
  readSessionSnapshot,
1468
1659
  readSessionSnapshot
1469
1660
  );
1470
- const generatedId = useId().replaceAll(":", "");
1471
- const rootRef = useRef(null);
1472
- const lastFocusedState = useRef(void 0);
1473
- const lastViewedSection = useRef(void 0);
1474
- const lastValidationKey = useRef("");
1661
+ const generatedId = useId2().replaceAll(":", "");
1662
+ const rootRef = useRef2(null);
1663
+ const lastFocusedState = useRef2(void 0);
1664
+ const lastViewedSection = useRef2(void 0);
1665
+ const lastValidationKey = useRef2("");
1475
1666
  const resolvedSurfaceMetadata = surfaceMetadata ?? controllerSnapshot?.surface ?? null;
1476
1667
  const editing = controllerSnapshot?.editing ?? editingForSurface(resolvedSurfaceMetadata);
1477
1668
  const surface = surfaceId === void 0 ? adapter.currentSurface : adapter.getSurface(surfaceId);
1478
1669
  const structure = surface === void 0 ? EMPTY_STRUCTURE : adapter.getSurfaceStructure(surface.id);
1479
1670
  const focusKey = surface === void 0 ? void 0 : `${surface.id}:${resolvedSurfaceMetadata?.activeStateId ?? "surface"}`;
1480
1671
  const frameId = hostId ?? `a2ui-surface-${generatedId}`;
1481
- useLayoutEffect(() => {
1672
+ useLayoutEffect2(() => {
1482
1673
  const root = rootRef.current;
1483
1674
  if (root === null || surface === void 0) {
1484
1675
  return;
@@ -1500,7 +1691,7 @@ function A2UISurfaceHost({
1500
1691
  validationSummaryId
1501
1692
  ),
1502
1693
  formatFieldLabels(root),
1503
- annotateSavedLoopTables(root),
1694
+ annotateSavedLoopTables(root, adapter, surface.id),
1504
1695
  bindFieldInteractionGroups(
1505
1696
  root,
1506
1697
  resolvedSurfaceMetadata?.fieldInteractions ?? [],
@@ -1580,9 +1771,9 @@ function A2UISurfaceHost({
1580
1771
  version
1581
1772
  ]);
1582
1773
  if (surface === void 0) {
1583
- return noSurfaceFallback ?? /* @__PURE__ */ jsx("div", { "data-a2ui-host": "no-surface", role: "status", "aria-live": "polite", children: "Your conversation continues by voice." });
1774
+ return noSurfaceFallback ?? /* @__PURE__ */ jsx2("div", { "data-a2ui-host": "no-surface", role: "status", "aria-live": "polite", children: "Your conversation continues by voice." });
1584
1775
  }
1585
- return /* @__PURE__ */ jsx(
1776
+ return /* @__PURE__ */ jsx2(
1586
1777
  "div",
1587
1778
  {
1588
1779
  ref: rootRef,
@@ -1595,13 +1786,13 @@ function A2UISurfaceHost({
1595
1786
  "aria-busy": busy || void 0,
1596
1787
  "aria-disabled": disabled || busy || void 0,
1597
1788
  tabIndex: -1,
1598
- children: /* @__PURE__ */ jsx(
1789
+ children: /* @__PURE__ */ jsx2(
1599
1790
  SurfaceErrorBoundary,
1600
1791
  {
1601
1792
  adapter,
1602
1793
  surface,
1603
- fallback: errorFallback ?? /* @__PURE__ */ jsx("div", { "data-a2ui-host": "render-error", role: "alert", children: "This step can't be shown right now. Please continue by voice." }),
1604
- children: /* @__PURE__ */ jsx(A2uiSurface, { surface })
1794
+ fallback: errorFallback ?? /* @__PURE__ */ jsx2("div", { "data-a2ui-host": "render-error", role: "alert", children: "This step can't be shown right now. Please continue by voice." }),
1795
+ children: /* @__PURE__ */ jsx2(A2uiSurface, { surface })
1605
1796
  },
1606
1797
  surface.id
1607
1798
  )
@@ -1887,7 +2078,7 @@ var SurfaceErrorBoundary = class extends Component {
1887
2078
  }
1888
2079
  render() {
1889
2080
  if (this.state.failedSurfaceId !== void 0) {
1890
- return /* @__PURE__ */ jsx(
2081
+ return /* @__PURE__ */ jsx2(
1891
2082
  "div",
1892
2083
  {
1893
2084
  "data-a2ui-error-boundary": "true",
@@ -2188,9 +2379,10 @@ function generatedFieldLabelName(text) {
2188
2379
  const unwrapped = trimmed.startsWith("*") && trimmed.endsWith("*") ? trimmed.slice(1, -1).trim() : trimmed;
2189
2380
  return /^[A-Za-z][A-Za-z0-9_]*$/.test(unwrapped) && /[_A-Z]/.test(unwrapped) ? unwrapped : void 0;
2190
2381
  }
2191
- function annotateSavedLoopTables(root) {
2382
+ function annotateSavedLoopTables(root, adapter, surfaceId) {
2192
2383
  const originals = /* @__PURE__ */ new Map();
2193
2384
  const textOriginals = /* @__PURE__ */ new Map();
2385
+ const listeners = [];
2194
2386
  const remember = (element) => {
2195
2387
  if (originals.has(element)) {
2196
2388
  return;
@@ -2205,6 +2397,7 @@ function annotateSavedLoopTables(root) {
2205
2397
  loopColumnCount: element.dataset.a2uiLoopColumnCount,
2206
2398
  loopLabel: element.dataset.a2uiLoopLabel,
2207
2399
  generatedHeader: element.dataset.a2uiLoopGeneratedHeader,
2400
+ actionControl: element.dataset.a2uiLoopActionControl,
2208
2401
  hidden: element.getAttribute("hidden"),
2209
2402
  ariaHidden: element.getAttribute("aria-hidden")
2210
2403
  });
@@ -2215,6 +2408,10 @@ function annotateSavedLoopTables(root) {
2215
2408
  }
2216
2409
  element.textContent = text;
2217
2410
  };
2411
+ const commitText = (element, text) => {
2412
+ textOriginals.set(element, text);
2413
+ element.textContent = text;
2414
+ };
2218
2415
  const generatedElements = [];
2219
2416
  const rows = [...root.querySelectorAll("div, section, article, li")].filter(
2220
2417
  (row) => getSavedLoopRowFields(row).length > 0 && normalizedLoopSummaryFields(getSavedLoopSummaryFields(row)).length >= 2
@@ -2294,10 +2491,23 @@ function annotateSavedLoopTables(root) {
2294
2491
  child.dataset.a2uiLoopColumn = String(Math.floor(columnIndex / 2) + 1);
2295
2492
  columnIndex += 1;
2296
2493
  }
2297
- normalizeSavedLoopRow(row, rowIndex, remember, setText, generatedElements);
2494
+ normalizeSavedLoopRow(
2495
+ row,
2496
+ rowIndex,
2497
+ remember,
2498
+ setText,
2499
+ commitText,
2500
+ generatedElements,
2501
+ listeners,
2502
+ adapter,
2503
+ surfaceId
2504
+ );
2298
2505
  });
2299
2506
  }
2300
2507
  return () => {
2508
+ for (const remove of listeners.reverse()) {
2509
+ remove();
2510
+ }
2301
2511
  for (const [element, original] of originals) {
2302
2512
  restoreDataset(element, "a2uiLoopTable", original.loopTable);
2303
2513
  restoreDataset(element, "a2uiLoopRow", original.loopRow);
@@ -2312,6 +2522,7 @@ function annotateSavedLoopTables(root) {
2312
2522
  "a2uiLoopGeneratedHeader",
2313
2523
  original.generatedHeader
2314
2524
  );
2525
+ restoreDataset(element, "a2uiLoopActionControl", original.actionControl);
2315
2526
  restoreAttribute(element, "hidden", original.hidden);
2316
2527
  restoreAttribute(element, "aria-hidden", original.ariaHidden);
2317
2528
  }
@@ -2319,6 +2530,14 @@ function annotateSavedLoopTables(root) {
2319
2530
  element.textContent = text;
2320
2531
  }
2321
2532
  for (const element of generatedElements) {
2533
+ if (element.dataset.a2uiGeneratedLoopActionCell === "true") {
2534
+ const parent = element.parentElement;
2535
+ if (parent !== null) {
2536
+ while (element.firstChild !== null) {
2537
+ parent.insertBefore(element.firstChild, element);
2538
+ }
2539
+ }
2540
+ }
2322
2541
  element.remove();
2323
2542
  }
2324
2543
  };
@@ -2346,7 +2565,7 @@ function buildSavedLoopHeader(parent, firstRow) {
2346
2565
  header.appendChild(action);
2347
2566
  return header;
2348
2567
  }
2349
- function normalizeSavedLoopRow(row, rowIndex, remember, setText, generatedElements) {
2568
+ function normalizeSavedLoopRow(row, rowIndex, remember, setText, commitText, generatedElements, listeners, adapter, surfaceId) {
2350
2569
  const summaryFields = normalizedLoopSummaryFields(getSavedLoopSummaryFields(row));
2351
2570
  if (summaryFields.length < 2) {
2352
2571
  return;
@@ -2356,6 +2575,7 @@ function normalizeSavedLoopRow(row, rowIndex, remember, setText, generatedElemen
2356
2575
  const fields = [...row.querySelectorAll("[data-a2ui-loop-field='true']")];
2357
2576
  const summaryValues = getSavedLoopSummaryValues(row);
2358
2577
  const action = row.querySelector("[data-a2ui-loop-cell='action']");
2578
+ const save = row.querySelector("[data-a2ui-loop-cell='save']");
2359
2579
  const orderedFields = summaryFields.slice(0, columnCount).map((fieldName) => {
2360
2580
  const field = findSavedLoopField(fields, fieldName);
2361
2581
  if (field !== void 0) {
@@ -2394,9 +2614,27 @@ function normalizeSavedLoopRow(row, rowIndex, remember, setText, generatedElemen
2394
2614
  value.dataset.a2uiLoopCell = "value";
2395
2615
  }
2396
2616
  });
2617
+ const rowEditValues = savedLoopRowEditValues(orderedFields, summaryFields);
2618
+ const rowKey = savedLoopRowEditKey(rowIndex, summaryFields, rowEditValues);
2397
2619
  if (action !== null) {
2398
2620
  remember(action);
2399
2621
  action.dataset.a2uiLoopColumn = String(columnCount + 1);
2622
+ createSavedLoopActionCell(
2623
+ row,
2624
+ save,
2625
+ action,
2626
+ columnCount + 1,
2627
+ remember,
2628
+ setText,
2629
+ commitText,
2630
+ generatedElements,
2631
+ listeners,
2632
+ adapter,
2633
+ surfaceId,
2634
+ rowKey,
2635
+ orderedFields,
2636
+ summaryFields.slice(0, columnCount)
2637
+ );
2400
2638
  }
2401
2639
  for (const field of fields) {
2402
2640
  if (orderedFields.includes(field)) {
@@ -2408,6 +2646,202 @@ function normalizeSavedLoopRow(row, rowIndex, remember, setText, generatedElemen
2408
2646
  field.setAttribute("aria-hidden", "true");
2409
2647
  }
2410
2648
  }
2649
+ function createSavedLoopActionCell(row, save, action, column, remember, setText, commitText, generatedElements, listeners, adapter, surfaceId, rowKey, orderedFields, summaryFields) {
2650
+ if (save === null) {
2651
+ return void 0;
2652
+ }
2653
+ remember(save);
2654
+ remember(action);
2655
+ const actionCell = row.ownerDocument.createElement("div");
2656
+ const editButton = row.ownerDocument.createElement("button");
2657
+ editButton.type = "button";
2658
+ actionCell.dataset.a2uiLoopCell = "action";
2659
+ actionCell.dataset.a2uiLoopColumn = String(column);
2660
+ actionCell.dataset.a2uiLoopActionGroup = "true";
2661
+ actionCell.dataset.a2uiGeneratedLoopActionCell = "true";
2662
+ editButton.dataset.a2uiLoopCell = "edit";
2663
+ editButton.dataset.a2uiLoopColumn = String(column);
2664
+ editButton.dataset.a2uiGeneratedLoopEditButton = "true";
2665
+ editButton.textContent = "Edit";
2666
+ row.insertBefore(actionCell, action);
2667
+ actionCell.append(editButton, save, action);
2668
+ save.dataset.a2uiLoopCell = "save";
2669
+ save.dataset.a2uiLoopColumn = String(column);
2670
+ save.setAttribute("hidden", "");
2671
+ save.setAttribute("aria-hidden", "true");
2672
+ action.dataset.a2uiLoopColumn = String(column);
2673
+ listeners.push(
2674
+ bindSavedLoopEditAction(
2675
+ editButton,
2676
+ save,
2677
+ row,
2678
+ orderedFields,
2679
+ summaryFields,
2680
+ adapter,
2681
+ surfaceId,
2682
+ rowKey,
2683
+ setText,
2684
+ commitText,
2685
+ generatedElements,
2686
+ listeners
2687
+ )
2688
+ );
2689
+ const activeEditValues = adapter.getListRowEdit(surfaceId, rowKey);
2690
+ if (activeEditValues !== void 0) {
2691
+ renderSavedLoopEditInputs(
2692
+ editButton,
2693
+ row,
2694
+ orderedFields,
2695
+ summaryFields,
2696
+ adapter,
2697
+ surfaceId,
2698
+ rowKey,
2699
+ setText,
2700
+ generatedElements,
2701
+ listeners,
2702
+ activeEditValues,
2703
+ false
2704
+ );
2705
+ }
2706
+ generatedElements.push(editButton);
2707
+ generatedElements.push(actionCell);
2708
+ delete action.dataset.a2uiLoopCell;
2709
+ delete action.dataset.a2uiLoopColumn;
2710
+ action.dataset.a2uiLoopActionControl = "true";
2711
+ return actionCell;
2712
+ }
2713
+ function bindSavedLoopEditAction(editButton, submitButton, row, orderedFields, summaryFields, adapter, surfaceId, rowKey, setText, commitText, generatedElements, listeners) {
2714
+ const onClick = (event) => {
2715
+ if (adapter.getListRowEdit(surfaceId, rowKey) === void 0) {
2716
+ event.preventDefault();
2717
+ event.stopImmediatePropagation();
2718
+ const values2 = savedLoopRowEditValues(orderedFields, summaryFields);
2719
+ adapter.beginListRowEdit(surfaceId, rowKey, values2);
2720
+ renderSavedLoopEditInputs(
2721
+ editButton,
2722
+ row,
2723
+ orderedFields,
2724
+ summaryFields,
2725
+ adapter,
2726
+ surfaceId,
2727
+ rowKey,
2728
+ setText,
2729
+ generatedElements,
2730
+ listeners,
2731
+ values2,
2732
+ true
2733
+ );
2734
+ return;
2735
+ }
2736
+ event.preventDefault();
2737
+ event.stopImmediatePropagation();
2738
+ const values = savedLoopRowInputValues(orderedFields, summaryFields);
2739
+ adapter.clearListRowEdit(surfaceId, rowKey);
2740
+ delete row.dataset.a2uiLoopEditing;
2741
+ setText(editButton, "Edit");
2742
+ orderedFields.forEach((field, index) => {
2743
+ const fieldName = summaryFields[index];
2744
+ const value = field.querySelector(
2745
+ "[data-a2ui-loop-cell='value']"
2746
+ );
2747
+ if (fieldName !== void 0 && value !== null) {
2748
+ commitText(value, stringEditValue(values[fieldName]));
2749
+ }
2750
+ });
2751
+ adapter.stageListRowUpdate(surfaceId, values);
2752
+ window.setTimeout(() => {
2753
+ const clickable = submitButton instanceof HTMLButtonElement ? submitButton : submitButton.querySelector("button");
2754
+ (clickable ?? submitButton).click();
2755
+ }, 0);
2756
+ };
2757
+ editButton.addEventListener("click", onClick, { capture: true });
2758
+ return () => {
2759
+ editButton.removeEventListener("click", onClick, { capture: true });
2760
+ };
2761
+ }
2762
+ function renderSavedLoopEditInputs(editButton, row, orderedFields, summaryFields, adapter, surfaceId, rowKey, setText, generatedElements, listeners, values, focusFirstInput) {
2763
+ row.dataset.a2uiLoopEditing = "true";
2764
+ for (const [index, field] of orderedFields.entries()) {
2765
+ const fieldName = summaryFields[index];
2766
+ const value = field.querySelector(
2767
+ "[data-a2ui-loop-cell='value']"
2768
+ );
2769
+ if (fieldName === void 0 || value === null) {
2770
+ continue;
2771
+ }
2772
+ let input = value.querySelector(
2773
+ "input[data-a2ui-loop-edit-input='true']"
2774
+ );
2775
+ if (input === null) {
2776
+ input = row.ownerDocument.createElement("input");
2777
+ input.type = "text";
2778
+ input.name = fieldName;
2779
+ input.setAttribute("aria-label", displayFieldLabel(fieldName));
2780
+ input.dataset.a2uiLoopEditInput = "true";
2781
+ setText(value, "");
2782
+ value.appendChild(input);
2783
+ generatedElements.push(input);
2784
+ }
2785
+ input.value = stringEditValue(values[fieldName]);
2786
+ const syncEditValue = () => {
2787
+ adapter.updateListRowEdit(
2788
+ surfaceId,
2789
+ rowKey,
2790
+ savedLoopRowInputValues(orderedFields, summaryFields)
2791
+ );
2792
+ };
2793
+ input.addEventListener("input", syncEditValue);
2794
+ input.addEventListener("change", syncEditValue);
2795
+ listeners.push(() => {
2796
+ input.removeEventListener("input", syncEditValue);
2797
+ input.removeEventListener("change", syncEditValue);
2798
+ });
2799
+ }
2800
+ setText(editButton, "Save");
2801
+ if (focusFirstInput) {
2802
+ row.querySelector(
2803
+ "input[data-a2ui-loop-edit-input='true']"
2804
+ )?.focus();
2805
+ }
2806
+ }
2807
+ function savedLoopRowEditValues(orderedFields, summaryFields) {
2808
+ const values = {};
2809
+ orderedFields.forEach((field, index) => {
2810
+ const fieldName = summaryFields[index];
2811
+ const value = field.querySelector(
2812
+ "[data-a2ui-loop-cell='value']"
2813
+ );
2814
+ if (fieldName !== void 0) {
2815
+ values[fieldName] = value?.textContent?.trim() ?? "";
2816
+ }
2817
+ });
2818
+ return values;
2819
+ }
2820
+ function savedLoopRowInputValues(orderedFields, summaryFields) {
2821
+ const values = {};
2822
+ orderedFields.forEach((field, index) => {
2823
+ const fieldName = summaryFields[index];
2824
+ const input = field.querySelector(
2825
+ "input[data-a2ui-loop-edit-input='true']"
2826
+ );
2827
+ const value = field.querySelector(
2828
+ "[data-a2ui-loop-cell='value']"
2829
+ );
2830
+ if (fieldName !== void 0) {
2831
+ values[fieldName] = input?.value ?? value?.textContent?.trim() ?? "";
2832
+ }
2833
+ });
2834
+ return values;
2835
+ }
2836
+ function savedLoopRowEditKey(rowIndex, summaryFields, values) {
2837
+ return `${rowIndex}:${summaryFields.map((field) => `${field}=${stringEditValue(values[field])}`).join("|")}`;
2838
+ }
2839
+ function stringEditValue(value) {
2840
+ if (value === null || value === void 0) {
2841
+ return "";
2842
+ }
2843
+ return String(value);
2844
+ }
2411
2845
  function findSavedLoopField(fields, summaryField) {
2412
2846
  const expectedName = canonicalLoopFieldName(summaryField).toLowerCase();
2413
2847
  const expectedLabel = displayFieldLabel(expectedName).toLowerCase();