@ukiahinsure/a2ui-react-adapter 0.1.4 → 0.1.5
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 +176 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -73,6 +73,10 @@ var A2UIClientAdapter = class {
|
|
|
73
73
|
const values = {};
|
|
74
74
|
for (const field of fields) {
|
|
75
75
|
const value = surface.dataModel.get(fieldPointer(field));
|
|
76
|
+
if (Array.isArray(value) && value.length === 1 && isFieldValue(value[0])) {
|
|
77
|
+
values[field] = value[0];
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
76
80
|
if (!isFieldValue(value)) {
|
|
77
81
|
return null;
|
|
78
82
|
}
|
|
@@ -89,8 +93,9 @@ var A2UIClientAdapter = class {
|
|
|
89
93
|
let changed = false;
|
|
90
94
|
for (const [field, value] of Object.entries(values)) {
|
|
91
95
|
const path = fieldPointer(field);
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
const next = this.#dataModelValueForField(surfaceId, field, value);
|
|
97
|
+
if (!fieldValueEquivalent(surface.dataModel.get(path), next)) {
|
|
98
|
+
surface.dataModel.set(path, next);
|
|
94
99
|
changed = true;
|
|
95
100
|
}
|
|
96
101
|
}
|
|
@@ -99,6 +104,22 @@ var A2UIClientAdapter = class {
|
|
|
99
104
|
}
|
|
100
105
|
return true;
|
|
101
106
|
}
|
|
107
|
+
#dataModelValueForField(surfaceId, field, value) {
|
|
108
|
+
const descriptor = this.#fieldDescriptor(surfaceId, field);
|
|
109
|
+
if (descriptor?.control === "choice" && typeof value === "string" && value.length > 0) {
|
|
110
|
+
return [value];
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
#fieldDescriptor(surfaceId, field) {
|
|
115
|
+
const structure = this.#structureBySurface.get(surfaceId);
|
|
116
|
+
if (structure === void 0) {
|
|
117
|
+
return void 0;
|
|
118
|
+
}
|
|
119
|
+
return [...structure.fieldsByComponent.values()].find(
|
|
120
|
+
(descriptor) => descriptor.field === field
|
|
121
|
+
);
|
|
122
|
+
}
|
|
102
123
|
/**
|
|
103
124
|
* Apply one validated server envelope. Returns false for duplicates and
|
|
104
125
|
* out-of-order deliveries (the per-session `seq` guard) and for envelopes
|
|
@@ -189,11 +210,20 @@ var A2UIClientAdapter = class {
|
|
|
189
210
|
);
|
|
190
211
|
return false;
|
|
191
212
|
}
|
|
192
|
-
const
|
|
213
|
+
const rawMessages = envelope.kind === "snapshot" ? validated.data : mergePersistedListRowsIntoMessages(
|
|
193
214
|
validated.data,
|
|
194
215
|
envelope.surfaceId,
|
|
195
216
|
this.#persistedListRowsByPath
|
|
196
217
|
);
|
|
218
|
+
const nextStructure = updateSurfaceStructures(
|
|
219
|
+
rawMessages,
|
|
220
|
+
envelope.kind === "snapshot" ? /* @__PURE__ */ new Map() : cloneSurfaceStructures(this.#structureBySurface)
|
|
221
|
+
);
|
|
222
|
+
const messages = normalizeChoiceValuesInMessages(
|
|
223
|
+
rawMessages,
|
|
224
|
+
envelope.surfaceId,
|
|
225
|
+
nextStructure.get(envelope.surfaceId)
|
|
226
|
+
);
|
|
197
227
|
const candidate = this.#createProcessor();
|
|
198
228
|
try {
|
|
199
229
|
if (envelope.kind !== "snapshot") {
|
|
@@ -212,10 +242,6 @@ var A2UIClientAdapter = class {
|
|
|
212
242
|
return false;
|
|
213
243
|
}
|
|
214
244
|
const previous = this.#processor;
|
|
215
|
-
const nextStructure = updateSurfaceStructures(
|
|
216
|
-
messages,
|
|
217
|
-
envelope.kind === "snapshot" ? /* @__PURE__ */ new Map() : cloneSurfaceStructures(this.#structureBySurface)
|
|
218
|
-
);
|
|
219
245
|
pruneSurfaceStructures(nextStructure, candidate);
|
|
220
246
|
if (!fieldInteractionBindingsTrusted(
|
|
221
247
|
envelope.fieldInteractions ?? [],
|
|
@@ -435,6 +461,59 @@ function fieldDescriptor(component) {
|
|
|
435
461
|
...typeof component.max === "string" ? { max: component.max } : {}
|
|
436
462
|
};
|
|
437
463
|
}
|
|
464
|
+
function normalizeChoiceValuesInMessages(messages, surfaceId, structure) {
|
|
465
|
+
if (structure === void 0) {
|
|
466
|
+
return [...messages];
|
|
467
|
+
}
|
|
468
|
+
const choiceFields = new Set(
|
|
469
|
+
[...structure.fieldsByComponent.values()].filter((field) => field.control === "choice").map((field) => field.field)
|
|
470
|
+
);
|
|
471
|
+
if (choiceFields.size === 0) {
|
|
472
|
+
return [...messages];
|
|
473
|
+
}
|
|
474
|
+
return messages.map((message) => {
|
|
475
|
+
if (!isRecord(message) || !isRecord(message.updateDataModel)) {
|
|
476
|
+
return message;
|
|
477
|
+
}
|
|
478
|
+
const update = message.updateDataModel;
|
|
479
|
+
if (update.surfaceId !== surfaceId) {
|
|
480
|
+
return message;
|
|
481
|
+
}
|
|
482
|
+
const path = typeof update.path === "string" ? update.path : "/";
|
|
483
|
+
if (path === "/" && isRecord(update.value)) {
|
|
484
|
+
const value = cloneJsonRecord(update.value);
|
|
485
|
+
for (const field2 of choiceFields) {
|
|
486
|
+
if (Object.hasOwn(value, field2)) {
|
|
487
|
+
value[field2] = choiceDataModelValue(value[field2]);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
...message,
|
|
492
|
+
updateDataModel: { ...update, value }
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
const field = fieldFromPointer(path);
|
|
496
|
+
if (field !== void 0 && choiceFields.has(field)) {
|
|
497
|
+
return {
|
|
498
|
+
...message,
|
|
499
|
+
updateDataModel: {
|
|
500
|
+
...update,
|
|
501
|
+
value: choiceDataModelValue(update.value)
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
return message;
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
function choiceDataModelValue(value) {
|
|
509
|
+
if (Array.isArray(value)) {
|
|
510
|
+
return value;
|
|
511
|
+
}
|
|
512
|
+
if (typeof value === "string") {
|
|
513
|
+
return value.length === 0 ? [] : [value];
|
|
514
|
+
}
|
|
515
|
+
return value;
|
|
516
|
+
}
|
|
438
517
|
function hasRequiredCheck(value) {
|
|
439
518
|
return Array.isArray(value) && value.some(
|
|
440
519
|
(check) => isRecord(check) && isRecord(check.condition) && check.condition.call === "required"
|
|
@@ -602,6 +681,12 @@ function cloneJsonArray(value) {
|
|
|
602
681
|
function isFieldValue(value) {
|
|
603
682
|
return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
604
683
|
}
|
|
684
|
+
function fieldValueEquivalent(left, right) {
|
|
685
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
686
|
+
return left.length === right.length && left.every((value, index) => Object.is(value, right[index]));
|
|
687
|
+
}
|
|
688
|
+
return Object.is(left, right);
|
|
689
|
+
}
|
|
605
690
|
function tabDescriptors(component) {
|
|
606
691
|
if (component.component !== "Tabs" || !isNonEmptyString(component.id) || !Array.isArray(component.tabs)) {
|
|
607
692
|
return [];
|
|
@@ -711,6 +796,7 @@ function A2UISurfaceHost({
|
|
|
711
796
|
resolvedSurfaceMetadata?.fieldInteractions ?? []
|
|
712
797
|
),
|
|
713
798
|
annotateActionControls(root),
|
|
799
|
+
blockInvalidActionControls(root),
|
|
714
800
|
setPendingGroupsDisabled(
|
|
715
801
|
root,
|
|
716
802
|
new Set(editing.pendingGroupIds)
|
|
@@ -1104,6 +1190,10 @@ function annotateFields(root, fields, invalidFields, validationSummaryId) {
|
|
|
1104
1190
|
}
|
|
1105
1191
|
}
|
|
1106
1192
|
applyControlValidationAttributes(control, field);
|
|
1193
|
+
const cleanup = attachControlValidationBehavior(control, field);
|
|
1194
|
+
if (cleanup !== void 0) {
|
|
1195
|
+
matchCleanups.push(cleanup);
|
|
1196
|
+
}
|
|
1107
1197
|
}
|
|
1108
1198
|
}
|
|
1109
1199
|
return () => {
|
|
@@ -1133,13 +1223,90 @@ function applyControlValidationAttributes(control, field) {
|
|
|
1133
1223
|
if (field.max !== void 0) {
|
|
1134
1224
|
control.setAttribute("max", field.max);
|
|
1135
1225
|
}
|
|
1136
|
-
if (field.validationRegexp
|
|
1226
|
+
if (isPhoneValidationRegexp(field.validationRegexp)) {
|
|
1137
1227
|
control.setAttribute("inputmode", "numeric");
|
|
1138
1228
|
control.setAttribute("maxlength", "10");
|
|
1139
|
-
} else if (field.validationRegexp
|
|
1229
|
+
} else if (isZipValidationRegexp(field.validationRegexp)) {
|
|
1140
1230
|
control.setAttribute("inputmode", "numeric");
|
|
1141
1231
|
control.setAttribute("maxlength", "5");
|
|
1232
|
+
} else if (isIntegerValidationRegexp(field.validationRegexp)) {
|
|
1233
|
+
control.setAttribute("inputmode", "numeric");
|
|
1234
|
+
control.setAttribute("maxlength", "2");
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
function attachControlValidationBehavior(control, field) {
|
|
1238
|
+
const maxLength = numericMaxLengthForField(field);
|
|
1239
|
+
if (maxLength === void 0 || !isTextInput(control)) {
|
|
1240
|
+
return void 0;
|
|
1241
|
+
}
|
|
1242
|
+
const sanitize = () => {
|
|
1243
|
+
const next = control.value.replace(/\D/g, "").slice(0, maxLength);
|
|
1244
|
+
if (control.value !== next) {
|
|
1245
|
+
control.value = next;
|
|
1246
|
+
control.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
control.addEventListener("input", sanitize);
|
|
1250
|
+
control.addEventListener("change", sanitize);
|
|
1251
|
+
sanitize();
|
|
1252
|
+
return () => {
|
|
1253
|
+
control.removeEventListener("input", sanitize);
|
|
1254
|
+
control.removeEventListener("change", sanitize);
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
function numericMaxLengthForField(field) {
|
|
1258
|
+
if (isPhoneValidationRegexp(field.validationRegexp)) {
|
|
1259
|
+
return 10;
|
|
1260
|
+
}
|
|
1261
|
+
if (isZipValidationRegexp(field.validationRegexp)) {
|
|
1262
|
+
return 5;
|
|
1263
|
+
}
|
|
1264
|
+
if (isIntegerValidationRegexp(field.validationRegexp)) {
|
|
1265
|
+
return 2;
|
|
1142
1266
|
}
|
|
1267
|
+
return void 0;
|
|
1268
|
+
}
|
|
1269
|
+
function isPhoneValidationRegexp(pattern) {
|
|
1270
|
+
return pattern === "^[2-9]\\d{2}[2-9]\\d{6}$" || pattern?.startsWith("^(?:") === true && pattern.endsWith(")[2-9]\\d{6}$");
|
|
1271
|
+
}
|
|
1272
|
+
function isZipValidationRegexp(pattern) {
|
|
1273
|
+
return pattern === "^(?!00000$)\\d{5}$" || pattern === "^(?:00(?:50[1-9]|5[1-9][0-9]|[6-9][0-9]{2})|0[1-9][0-9]{3}|[1-8][0-9]{4}|9[0-8][0-9]{3}|99[0-8][0-9]{2}|999[0-4][0-9]|99950)$";
|
|
1274
|
+
}
|
|
1275
|
+
function isIntegerValidationRegexp(pattern) {
|
|
1276
|
+
return pattern === "^\\d{1,2}$";
|
|
1277
|
+
}
|
|
1278
|
+
function isTextInput(element) {
|
|
1279
|
+
return element instanceof HTMLInputElement && element.type !== "radio";
|
|
1280
|
+
}
|
|
1281
|
+
function blockInvalidActionControls(root) {
|
|
1282
|
+
const blockIfInvalid = (event) => {
|
|
1283
|
+
const target = event.target;
|
|
1284
|
+
if (!(target instanceof HTMLElement)) {
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const action = target.closest("[data-a2ui-action='true']");
|
|
1288
|
+
if (action === null || !root.contains(action)) {
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const invalid2 = firstInvalidInput(root);
|
|
1292
|
+
if (invalid2 === void 0) {
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
event.preventDefault();
|
|
1296
|
+
event.stopImmediatePropagation();
|
|
1297
|
+
invalid2.setAttribute("aria-invalid", "true");
|
|
1298
|
+
invalid2.reportValidity();
|
|
1299
|
+
invalid2.focus();
|
|
1300
|
+
};
|
|
1301
|
+
root.addEventListener("click", blockIfInvalid, true);
|
|
1302
|
+
root.addEventListener("keydown", blockIfInvalid, true);
|
|
1303
|
+
return () => {
|
|
1304
|
+
root.removeEventListener("click", blockIfInvalid, true);
|
|
1305
|
+
root.removeEventListener("keydown", blockIfInvalid, true);
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
function firstInvalidInput(root) {
|
|
1309
|
+
return [...root.querySelectorAll("input[data-a2ui-field]")].filter((input) => input.type !== "radio" && !isControlDisabled(input)).find((input) => !input.checkValidity());
|
|
1143
1310
|
}
|
|
1144
1311
|
function findFieldControls(root, field) {
|
|
1145
1312
|
const labels = [...root.querySelectorAll("label")];
|