@dmitryvim/form-builder 0.5.3 → 0.6.4
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 +8 -2
- package/dist/browser/formbuilder.min.js +129 -147
- package/dist/browser/formbuilder.v0.6.4.min.js +1472 -0
- package/dist/cjs/index.cjs +544 -489
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +483 -445
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +129 -147
- package/dist/types/components/container.d.ts +1 -8
- package/dist/types/components/index.d.ts +1 -2
- package/dist/types/instance/FormBuilderInstance.d.ts +36 -5
- package/dist/types/instance/state.d.ts +8 -0
- package/dist/types/types/component-operations.d.ts +15 -0
- package/dist/types/types/config.d.ts +2 -0
- package/dist/types/types/state.d.ts +15 -0
- package/dist/types/utils/helpers.d.ts +33 -4
- package/dist/types/utils/styles.d.ts +13 -7
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.5.3.min.js +0 -1490
package/dist/esm/index.js
CHANGED
|
@@ -3,12 +3,12 @@ function t(key, state, params) {
|
|
|
3
3
|
const locale = state.config.locale || "en";
|
|
4
4
|
const localeTranslations = state.config.translations[locale];
|
|
5
5
|
const fallbackTranslations = state.config.translations.en;
|
|
6
|
-
let text = localeTranslations?.[key]
|
|
6
|
+
let text = localeTranslations?.[key] ?? fallbackTranslations?.[key] ?? key;
|
|
7
7
|
if (params) {
|
|
8
8
|
for (const [paramKey, paramValue] of Object.entries(params)) {
|
|
9
9
|
text = text.replace(
|
|
10
10
|
new RegExp(`\\{${paramKey}\\}`, "g"),
|
|
11
|
-
String(paramValue)
|
|
11
|
+
() => String(paramValue)
|
|
12
12
|
);
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -23,9 +23,7 @@ function isPlainObject(obj) {
|
|
|
23
23
|
return obj && typeof obj === "object" && obj.constructor === Object;
|
|
24
24
|
}
|
|
25
25
|
function escapeHtml(text) {
|
|
26
|
-
|
|
27
|
-
div.textContent = text;
|
|
28
|
-
return div.innerHTML;
|
|
26
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
29
27
|
}
|
|
30
28
|
function getElementLookupKey(element, state) {
|
|
31
29
|
if (element.key) {
|
|
@@ -60,8 +58,8 @@ function formatFileSize(bytes) {
|
|
|
60
58
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
61
59
|
}
|
|
62
60
|
function serializeHiddenValue(value) {
|
|
63
|
-
if (value ===
|
|
64
|
-
return
|
|
61
|
+
if (value === void 0) return "";
|
|
62
|
+
return JSON.stringify(value);
|
|
65
63
|
}
|
|
66
64
|
function deserializeHiddenValue(raw) {
|
|
67
65
|
if (raw === "") return null;
|
|
@@ -71,6 +69,23 @@ function deserializeHiddenValue(raw) {
|
|
|
71
69
|
return raw;
|
|
72
70
|
}
|
|
73
71
|
}
|
|
72
|
+
function readTypedInputValue(input) {
|
|
73
|
+
if (input instanceof HTMLInputElement) {
|
|
74
|
+
if (input.type === "checkbox") return input.checked;
|
|
75
|
+
if (input.dataset.hiddenField) return deserializeHiddenValue(input.value);
|
|
76
|
+
if (input.dataset.booleanField) return input.value === "true";
|
|
77
|
+
if (input.type === "number" || input.type === "range") {
|
|
78
|
+
if (input.value === "") return null;
|
|
79
|
+
const parsed = parseFloat(input.value);
|
|
80
|
+
const decimals = input.dataset.decimals;
|
|
81
|
+
return decimals !== void 0 ? Number(parsed.toFixed(parseInt(decimals, 10))) : parsed;
|
|
82
|
+
}
|
|
83
|
+
if (input.dataset.colourField) {
|
|
84
|
+
return input.value.toUpperCase();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return input.value === "" ? null : input.value;
|
|
88
|
+
}
|
|
74
89
|
function createHiddenInput(name, value) {
|
|
75
90
|
const input = document.createElement("input");
|
|
76
91
|
input.type = "hidden";
|
|
@@ -308,6 +323,14 @@ function validateSchema(schema) {
|
|
|
308
323
|
}
|
|
309
324
|
}
|
|
310
325
|
function validateElements(elements, path) {
|
|
326
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
327
|
+
elements.forEach((element, index) => {
|
|
328
|
+
if (!element.key) return;
|
|
329
|
+
if (seenKeys.has(element.key)) {
|
|
330
|
+
errors.push(`${path}[${index}]: duplicate key "${element.key}"`);
|
|
331
|
+
}
|
|
332
|
+
seenKeys.add(element.key);
|
|
333
|
+
});
|
|
311
334
|
elements.forEach((element, index) => {
|
|
312
335
|
const elementPath = `${path}[${index}]`;
|
|
313
336
|
if (!element.type) {
|
|
@@ -317,6 +340,14 @@ function validateSchema(schema) {
|
|
|
317
340
|
errors.push(`${elementPath}: missing key`);
|
|
318
341
|
}
|
|
319
342
|
validateCountBounds(element, elementPath, errors);
|
|
343
|
+
if (element.type === "number" && "decimals" in element) {
|
|
344
|
+
const decimals = element.decimals;
|
|
345
|
+
if (decimals !== void 0 && (!Number.isInteger(decimals) || decimals < 0)) {
|
|
346
|
+
errors.push(
|
|
347
|
+
`${elementPath}: decimals must be a non-negative integer (got ${JSON.stringify(decimals)})`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
320
351
|
if (element.type === "markdown") {
|
|
321
352
|
const content = element.content;
|
|
322
353
|
if (typeof content !== "string") {
|
|
@@ -467,12 +498,55 @@ function deepEqual(a, b) {
|
|
|
467
498
|
}
|
|
468
499
|
|
|
469
500
|
// src/utils/styles.ts
|
|
470
|
-
function
|
|
501
|
+
function findErrorAnchor(input) {
|
|
502
|
+
return input.closest?.(".fb-chip") ?? input.closest?.(".slider-container") ?? input;
|
|
503
|
+
}
|
|
504
|
+
function findErrorNode(input) {
|
|
505
|
+
const anchor = findErrorAnchor(input);
|
|
471
506
|
const name = input.getAttribute("name");
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
507
|
+
const parent = anchor.parentElement;
|
|
508
|
+
if (name && parent) {
|
|
509
|
+
for (const child of Array.from(parent.children)) {
|
|
510
|
+
if (child.classList.contains("error-message") && child.getAttribute("data-error-for") === name) {
|
|
511
|
+
return child;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const sibling = anchor.nextElementSibling;
|
|
516
|
+
return sibling && sibling.classList.contains("error-message") ? sibling : null;
|
|
517
|
+
}
|
|
518
|
+
function markFieldValidity(input, errorMessage) {
|
|
519
|
+
if (!input) return;
|
|
520
|
+
if (errorMessage == null) {
|
|
521
|
+
input.classList.remove("invalid");
|
|
522
|
+
input.title = "";
|
|
523
|
+
findErrorNode(input)?.remove();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
input.classList.add("invalid");
|
|
527
|
+
input.title = errorMessage;
|
|
528
|
+
if (errorMessage === "") {
|
|
529
|
+
findErrorNode(input)?.remove();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
let errorElement = findErrorNode(input);
|
|
533
|
+
if (!errorElement) {
|
|
534
|
+
const anchor = findErrorAnchor(input);
|
|
535
|
+
errorElement = document.createElement("div");
|
|
536
|
+
errorElement.className = "error-message";
|
|
537
|
+
errorElement.style.cssText = `
|
|
538
|
+
color: var(--fb-error-color);
|
|
539
|
+
font-size: var(--fb-font-size-small);
|
|
540
|
+
margin-top: 0.25rem;
|
|
541
|
+
`;
|
|
542
|
+
anchor.parentNode?.insertBefore(errorElement, anchor.nextSibling);
|
|
543
|
+
}
|
|
544
|
+
errorElement.setAttribute("data-error-for", input.getAttribute("name") ?? "");
|
|
545
|
+
errorElement.textContent = errorMessage;
|
|
546
|
+
errorElement.style.display = "block";
|
|
547
|
+
}
|
|
548
|
+
function clearFieldError(input) {
|
|
549
|
+
findErrorNode(input)?.remove();
|
|
476
550
|
}
|
|
477
551
|
var BIN_ICON_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
|
|
478
552
|
function ensureThemingHooks(doc) {
|
|
@@ -594,7 +668,7 @@ function ensureThemingHooks(doc) {
|
|
|
594
668
|
`;
|
|
595
669
|
doc.head.appendChild(style);
|
|
596
670
|
}
|
|
597
|
-
function applyAutoExpand(textarea, options
|
|
671
|
+
function applyAutoExpand(textarea, options) {
|
|
598
672
|
textarea.style.overflow = "hidden";
|
|
599
673
|
textarea.style.resize = "none";
|
|
600
674
|
const minRows = Math.max(1, options.minRows ?? 1);
|
|
@@ -624,6 +698,7 @@ function applyAutoExpand(textarea, options = {}) {
|
|
|
624
698
|
const ro = new ResizeObserver((entries) => {
|
|
625
699
|
if (!textarea.isConnected) {
|
|
626
700
|
ro.disconnect();
|
|
701
|
+
options.observers?.delete(ro);
|
|
627
702
|
return;
|
|
628
703
|
}
|
|
629
704
|
const entry = entries[0];
|
|
@@ -633,6 +708,7 @@ function applyAutoExpand(textarea, options = {}) {
|
|
|
633
708
|
resize();
|
|
634
709
|
});
|
|
635
710
|
ro.observe(textarea);
|
|
711
|
+
options.observers?.add(ro);
|
|
636
712
|
}
|
|
637
713
|
function applySingleLineMode(textarea) {
|
|
638
714
|
textarea.addEventListener("keydown", (e) => {
|
|
@@ -943,11 +1019,11 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
943
1019
|
overflow-wrap: anywhere;
|
|
944
1020
|
`;
|
|
945
1021
|
textInput.name = pathKey;
|
|
946
|
-
textInput.placeholder = element.placeholder ?? "
|
|
947
|
-
textInput.value = ctx.prefill[element.key]
|
|
1022
|
+
textInput.placeholder = element.placeholder ?? t("placeholderText", state);
|
|
1023
|
+
textInput.value = ctx.prefill[element.key] ?? element.default ?? "";
|
|
948
1024
|
textInput.readOnly = readonly;
|
|
949
1025
|
applySingleLineMode(textInput);
|
|
950
|
-
applyAutoExpand(textInput);
|
|
1026
|
+
applyAutoExpand(textInput, { observers: state.autoExpandObservers });
|
|
951
1027
|
if (!readonly) {
|
|
952
1028
|
textInput.addEventListener("focus", () => {
|
|
953
1029
|
textInput.style.borderColor = "var(--fb-border-focus-color)";
|
|
@@ -1005,7 +1081,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1005
1081
|
const chip = input.closest(".fb-chip");
|
|
1006
1082
|
const sib = chip?.nextElementSibling;
|
|
1007
1083
|
if (sib && sib.classList.contains("error-message")) {
|
|
1008
|
-
sib.
|
|
1084
|
+
sib.setAttribute("data-error-for", input.name);
|
|
1009
1085
|
}
|
|
1010
1086
|
});
|
|
1011
1087
|
}
|
|
@@ -1020,7 +1096,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1020
1096
|
input.rows = 1;
|
|
1021
1097
|
input.className = "fb-chip-input";
|
|
1022
1098
|
input.value = value;
|
|
1023
|
-
input.placeholder = element.placeholder
|
|
1099
|
+
input.placeholder = element.placeholder ?? t("placeholderText", state);
|
|
1024
1100
|
input.readOnly = readonly;
|
|
1025
1101
|
chip.appendChild(input);
|
|
1026
1102
|
if (!readonly && ctx.instance) {
|
|
@@ -1034,7 +1110,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1034
1110
|
input.addEventListener("input", handleChange);
|
|
1035
1111
|
}
|
|
1036
1112
|
applySingleLineMode(input);
|
|
1037
|
-
applyAutoExpand(input);
|
|
1113
|
+
applyAutoExpand(input, { observers: state.autoExpandObservers });
|
|
1038
1114
|
if (!readonly) {
|
|
1039
1115
|
const rem = document.createElement("button");
|
|
1040
1116
|
rem.type = "button";
|
|
@@ -1055,6 +1131,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1055
1131
|
updateIndices();
|
|
1056
1132
|
updateAddButton();
|
|
1057
1133
|
updateRemoveButtons();
|
|
1134
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
1058
1135
|
};
|
|
1059
1136
|
chip.appendChild(rem);
|
|
1060
1137
|
}
|
|
@@ -1079,6 +1156,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1079
1156
|
addChip(element.default || "");
|
|
1080
1157
|
updateAddButton();
|
|
1081
1158
|
updateRemoveButtons();
|
|
1159
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
1082
1160
|
},
|
|
1083
1161
|
{ label: element.addLabel }
|
|
1084
1162
|
);
|
|
@@ -1096,40 +1174,6 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1096
1174
|
function validateTextElement(element, key, context) {
|
|
1097
1175
|
const errors = [];
|
|
1098
1176
|
const { scopeRoot, skipValidation } = context;
|
|
1099
|
-
const markValidity = (input, errorMessage) => {
|
|
1100
|
-
if (!input) return;
|
|
1101
|
-
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
1102
|
-
let errorElement = document.getElementById(errorId);
|
|
1103
|
-
if (errorMessage) {
|
|
1104
|
-
input.classList.add("invalid");
|
|
1105
|
-
input.title = errorMessage;
|
|
1106
|
-
if (!errorElement) {
|
|
1107
|
-
errorElement = document.createElement("div");
|
|
1108
|
-
errorElement.id = errorId;
|
|
1109
|
-
errorElement.className = "error-message";
|
|
1110
|
-
errorElement.style.cssText = `
|
|
1111
|
-
color: var(--fb-error-color);
|
|
1112
|
-
font-size: var(--fb-font-size-small);
|
|
1113
|
-
margin-top: 0.25rem;
|
|
1114
|
-
`;
|
|
1115
|
-
const chipAncestor = input.closest?.(".fb-chip");
|
|
1116
|
-
const anchor = chipAncestor || input;
|
|
1117
|
-
if (anchor.nextSibling) {
|
|
1118
|
-
anchor.parentNode?.insertBefore(errorElement, anchor.nextSibling);
|
|
1119
|
-
} else {
|
|
1120
|
-
anchor.parentNode?.appendChild(errorElement);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
errorElement.textContent = errorMessage;
|
|
1124
|
-
errorElement.style.display = "block";
|
|
1125
|
-
} else {
|
|
1126
|
-
input.classList.remove("invalid");
|
|
1127
|
-
input.title = "";
|
|
1128
|
-
if (errorElement) {
|
|
1129
|
-
errorElement.remove();
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
};
|
|
1133
1177
|
const validateTextInput = (input, val, fieldKey) => {
|
|
1134
1178
|
let hasError = false;
|
|
1135
1179
|
const { state } = context;
|
|
@@ -1137,12 +1181,12 @@ function validateTextElement(element, key, context) {
|
|
|
1137
1181
|
if (element.minLength !== void 0 && element.minLength !== null && val.length < element.minLength) {
|
|
1138
1182
|
const msg = t("minLength", state, { min: element.minLength });
|
|
1139
1183
|
errors.push(`${fieldKey}: ${msg}`);
|
|
1140
|
-
|
|
1184
|
+
markFieldValidity(input, msg);
|
|
1141
1185
|
hasError = true;
|
|
1142
1186
|
} else if (element.maxLength !== void 0 && element.maxLength !== null && val.length > element.maxLength) {
|
|
1143
1187
|
const msg = t("maxLength", state, { max: element.maxLength });
|
|
1144
1188
|
errors.push(`${fieldKey}: ${msg}`);
|
|
1145
|
-
|
|
1189
|
+
markFieldValidity(input, msg);
|
|
1146
1190
|
hasError = true;
|
|
1147
1191
|
} else if (element.pattern) {
|
|
1148
1192
|
try {
|
|
@@ -1150,19 +1194,19 @@ function validateTextElement(element, key, context) {
|
|
|
1150
1194
|
if (!re.test(val)) {
|
|
1151
1195
|
const msg = t("patternMismatch", state);
|
|
1152
1196
|
errors.push(`${fieldKey}: ${msg}`);
|
|
1153
|
-
|
|
1197
|
+
markFieldValidity(input, msg);
|
|
1154
1198
|
hasError = true;
|
|
1155
1199
|
}
|
|
1156
1200
|
} catch {
|
|
1157
1201
|
const msg = t("invalidPattern", state);
|
|
1158
1202
|
errors.push(`${fieldKey}: ${msg}`);
|
|
1159
|
-
|
|
1203
|
+
markFieldValidity(input, msg);
|
|
1160
1204
|
hasError = true;
|
|
1161
1205
|
}
|
|
1162
1206
|
}
|
|
1163
1207
|
}
|
|
1164
1208
|
if (!hasError) {
|
|
1165
|
-
|
|
1209
|
+
markFieldValidity(input, null);
|
|
1166
1210
|
}
|
|
1167
1211
|
};
|
|
1168
1212
|
if (element.multiple) {
|
|
@@ -1192,12 +1236,12 @@ function validateTextElement(element, key, context) {
|
|
|
1192
1236
|
}
|
|
1193
1237
|
return { value: values, errors };
|
|
1194
1238
|
} else {
|
|
1195
|
-
const input = scopeRoot.querySelector(`[name
|
|
1239
|
+
const input = scopeRoot.querySelector(`[name="${key}"]`);
|
|
1196
1240
|
const val = input?.value ?? "";
|
|
1197
1241
|
if (!skipValidation && element.required && val === "") {
|
|
1198
1242
|
const msg = t("required", context.state);
|
|
1199
1243
|
errors.push(`${key}: ${msg}`);
|
|
1200
|
-
|
|
1244
|
+
markFieldValidity(input, msg);
|
|
1201
1245
|
return { value: null, errors };
|
|
1202
1246
|
}
|
|
1203
1247
|
if (input) {
|
|
@@ -1259,8 +1303,8 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1259
1303
|
line-height: var(--fb-line-height, 1.5);
|
|
1260
1304
|
`;
|
|
1261
1305
|
textareaInput.name = pathKey;
|
|
1262
|
-
textareaInput.placeholder = element.placeholder ?? "
|
|
1263
|
-
textareaInput.value = ctx.prefill[element.key]
|
|
1306
|
+
textareaInput.placeholder = element.placeholder ?? t("placeholderText", state);
|
|
1307
|
+
textareaInput.value = ctx.prefill[element.key] ?? element.default ?? "";
|
|
1264
1308
|
textareaInput.readOnly = readonly;
|
|
1265
1309
|
if (!readonly && ctx.instance) {
|
|
1266
1310
|
const handleChange = () => {
|
|
@@ -1270,7 +1314,10 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1270
1314
|
textareaInput.addEventListener("blur", handleChange);
|
|
1271
1315
|
textareaInput.addEventListener("input", handleChange);
|
|
1272
1316
|
}
|
|
1273
|
-
applyAutoExpand(textareaInput, {
|
|
1317
|
+
applyAutoExpand(textareaInput, {
|
|
1318
|
+
minRows: element.rows ?? 1,
|
|
1319
|
+
observers: state.autoExpandObservers
|
|
1320
|
+
});
|
|
1274
1321
|
textareaWrapper.appendChild(textareaInput);
|
|
1275
1322
|
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
1276
1323
|
const counter = createCharCounter(element, textareaInput);
|
|
@@ -1313,7 +1360,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1313
1360
|
font-family: var(--fb-font-family);
|
|
1314
1361
|
line-height: var(--fb-line-height, 1.5);
|
|
1315
1362
|
`;
|
|
1316
|
-
textareaInput.placeholder = element.placeholder
|
|
1363
|
+
textareaInput.placeholder = element.placeholder ?? t("placeholderText", state);
|
|
1317
1364
|
textareaInput.value = value;
|
|
1318
1365
|
textareaInput.readOnly = readonly;
|
|
1319
1366
|
if (!readonly && ctx.instance) {
|
|
@@ -1324,7 +1371,10 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1324
1371
|
textareaInput.addEventListener("blur", handleChange);
|
|
1325
1372
|
textareaInput.addEventListener("input", handleChange);
|
|
1326
1373
|
}
|
|
1327
|
-
applyAutoExpand(textareaInput, {
|
|
1374
|
+
applyAutoExpand(textareaInput, {
|
|
1375
|
+
minRows: element.rows ?? 1,
|
|
1376
|
+
observers: state.autoExpandObservers
|
|
1377
|
+
});
|
|
1328
1378
|
textareaContainer.appendChild(textareaInput);
|
|
1329
1379
|
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
1330
1380
|
const counter = createCharCounter(element, textareaInput);
|
|
@@ -1362,6 +1412,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1362
1412
|
updateIndices();
|
|
1363
1413
|
updateAddButton();
|
|
1364
1414
|
updateRemoveButtons();
|
|
1415
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
1365
1416
|
}
|
|
1366
1417
|
};
|
|
1367
1418
|
item.appendChild(removeBtn);
|
|
@@ -1381,6 +1432,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1381
1432
|
addTextareaItem(element.default || "");
|
|
1382
1433
|
updateAddButton();
|
|
1383
1434
|
updateRemoveButtons();
|
|
1435
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
1384
1436
|
},
|
|
1385
1437
|
{ label: element.addLabel }
|
|
1386
1438
|
);
|
|
@@ -1541,6 +1593,17 @@ function createNumberRangeHint(element, input) {
|
|
|
1541
1593
|
updateColor();
|
|
1542
1594
|
return hint;
|
|
1543
1595
|
}
|
|
1596
|
+
function numberStepAttr(element) {
|
|
1597
|
+
if (element.step !== void 0) return element.step.toString();
|
|
1598
|
+
if (element.decimals !== void 0)
|
|
1599
|
+
return (10 ** -element.decimals).toString();
|
|
1600
|
+
return "any";
|
|
1601
|
+
}
|
|
1602
|
+
function applyDecimalsMarker(input, element) {
|
|
1603
|
+
if (element.decimals !== void 0) {
|
|
1604
|
+
input.setAttribute("data-decimals", String(element.decimals));
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1544
1607
|
function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
1545
1608
|
const state = ctx.state;
|
|
1546
1609
|
const readonly = isElementReadonly(element, state, ctx);
|
|
@@ -1549,11 +1612,12 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1549
1612
|
const numberInput = document.createElement("input");
|
|
1550
1613
|
numberInput.type = "number";
|
|
1551
1614
|
numberInput.name = pathKey;
|
|
1552
|
-
numberInput.placeholder = element.placeholder
|
|
1615
|
+
numberInput.placeholder = element.placeholder ?? "0";
|
|
1553
1616
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
1554
1617
|
if (element.max !== void 0) numberInput.max = element.max.toString();
|
|
1555
|
-
|
|
1556
|
-
numberInput
|
|
1618
|
+
numberInput.step = numberStepAttr(element);
|
|
1619
|
+
applyDecimalsMarker(numberInput, element);
|
|
1620
|
+
numberInput.value = ctx.prefill[element.key] ?? element.default ?? "";
|
|
1557
1621
|
numberInput.readOnly = readonly;
|
|
1558
1622
|
if (!element.stepper) {
|
|
1559
1623
|
numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
@@ -1592,7 +1656,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1592
1656
|
const minCount = element.minCount ?? (element.required ? 1 : 0);
|
|
1593
1657
|
const maxCount = element.maxCount ?? Infinity;
|
|
1594
1658
|
while (values.length < minCount) {
|
|
1595
|
-
values.push(element.default
|
|
1659
|
+
values.push(element.default ?? "");
|
|
1596
1660
|
}
|
|
1597
1661
|
const container = document.createElement("div");
|
|
1598
1662
|
container.className = "fb-row";
|
|
@@ -1621,10 +1685,11 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1621
1685
|
width: 100%;
|
|
1622
1686
|
box-sizing: border-box;
|
|
1623
1687
|
`;
|
|
1624
|
-
numberInput.placeholder = element.placeholder
|
|
1688
|
+
numberInput.placeholder = element.placeholder ?? "0";
|
|
1625
1689
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
1626
1690
|
if (element.max !== void 0) numberInput.max = element.max.toString();
|
|
1627
|
-
|
|
1691
|
+
numberInput.step = numberStepAttr(element);
|
|
1692
|
+
applyDecimalsMarker(numberInput, element);
|
|
1628
1693
|
numberInput.value = value.toString();
|
|
1629
1694
|
numberInput.readOnly = readonly;
|
|
1630
1695
|
if (!readonly && ctx.instance) {
|
|
@@ -1672,6 +1737,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1672
1737
|
updateIndices();
|
|
1673
1738
|
updateAddButton();
|
|
1674
1739
|
updateRemoveButtons();
|
|
1740
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
1675
1741
|
}
|
|
1676
1742
|
};
|
|
1677
1743
|
item.appendChild(removeBtn);
|
|
@@ -1687,10 +1753,11 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1687
1753
|
const handle = createAddItemRow(
|
|
1688
1754
|
"number",
|
|
1689
1755
|
() => {
|
|
1690
|
-
values.push(element.default
|
|
1691
|
-
addNumberItem(element.default
|
|
1756
|
+
values.push(element.default ?? "");
|
|
1757
|
+
addNumberItem(element.default ?? "");
|
|
1692
1758
|
updateAddButton();
|
|
1693
1759
|
updateRemoveButtons();
|
|
1760
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
1694
1761
|
},
|
|
1695
1762
|
{ label: element.addLabel }
|
|
1696
1763
|
);
|
|
@@ -1708,54 +1775,22 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1708
1775
|
function validateNumberElement(element, key, context) {
|
|
1709
1776
|
const errors = [];
|
|
1710
1777
|
const { scopeRoot, skipValidation } = context;
|
|
1711
|
-
const markValidity = (input, errorMessage) => {
|
|
1712
|
-
if (!input) return;
|
|
1713
|
-
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
1714
|
-
let errorElement = document.getElementById(errorId);
|
|
1715
|
-
if (errorMessage) {
|
|
1716
|
-
input.classList.add("invalid");
|
|
1717
|
-
input.title = errorMessage;
|
|
1718
|
-
if (!errorElement) {
|
|
1719
|
-
errorElement = document.createElement("div");
|
|
1720
|
-
errorElement.id = errorId;
|
|
1721
|
-
errorElement.className = "error-message";
|
|
1722
|
-
errorElement.style.cssText = `
|
|
1723
|
-
color: var(--fb-error-color);
|
|
1724
|
-
font-size: var(--fb-font-size-small);
|
|
1725
|
-
margin-top: 0.25rem;
|
|
1726
|
-
`;
|
|
1727
|
-
if (input.nextSibling) {
|
|
1728
|
-
input.parentNode?.insertBefore(errorElement, input.nextSibling);
|
|
1729
|
-
} else {
|
|
1730
|
-
input.parentNode?.appendChild(errorElement);
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
errorElement.textContent = errorMessage;
|
|
1734
|
-
errorElement.style.display = "block";
|
|
1735
|
-
} else {
|
|
1736
|
-
input.classList.remove("invalid");
|
|
1737
|
-
input.title = "";
|
|
1738
|
-
if (errorElement) {
|
|
1739
|
-
errorElement.remove();
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
};
|
|
1743
1778
|
const validateNumberInput = (input, v, fieldKey) => {
|
|
1744
1779
|
let hasError = false;
|
|
1745
1780
|
const { state } = context;
|
|
1746
1781
|
if (!skipValidation && element.min !== void 0 && element.min !== null && v < element.min) {
|
|
1747
1782
|
const msg = t("minValue", state, { min: element.min });
|
|
1748
1783
|
errors.push(`${fieldKey}: ${msg}`);
|
|
1749
|
-
|
|
1784
|
+
markFieldValidity(input, msg);
|
|
1750
1785
|
hasError = true;
|
|
1751
1786
|
} else if (!skipValidation && element.max !== void 0 && element.max !== null && v > element.max) {
|
|
1752
1787
|
const msg = t("maxValue", state, { max: element.max });
|
|
1753
1788
|
errors.push(`${fieldKey}: ${msg}`);
|
|
1754
|
-
|
|
1789
|
+
markFieldValidity(input, msg);
|
|
1755
1790
|
hasError = true;
|
|
1756
1791
|
}
|
|
1757
1792
|
if (!hasError) {
|
|
1758
|
-
|
|
1793
|
+
markFieldValidity(input, null);
|
|
1759
1794
|
}
|
|
1760
1795
|
};
|
|
1761
1796
|
if (element.multiple) {
|
|
@@ -1767,20 +1802,19 @@ function validateNumberElement(element, key, context) {
|
|
|
1767
1802
|
const raw = input?.value ?? "";
|
|
1768
1803
|
if (raw === "") {
|
|
1769
1804
|
values.push(null);
|
|
1770
|
-
|
|
1805
|
+
markFieldValidity(input, null);
|
|
1771
1806
|
return;
|
|
1772
1807
|
}
|
|
1773
1808
|
const v = parseFloat(raw);
|
|
1774
1809
|
if (!skipValidation && !Number.isFinite(v)) {
|
|
1775
1810
|
const msg = t("notANumber", context.state);
|
|
1776
1811
|
errors.push(`${key}[${index}]: ${msg}`);
|
|
1777
|
-
|
|
1812
|
+
markFieldValidity(input, msg);
|
|
1778
1813
|
values.push(null);
|
|
1779
1814
|
return;
|
|
1780
1815
|
}
|
|
1781
1816
|
validateNumberInput(input, v, `${key}[${index}]`);
|
|
1782
|
-
|
|
1783
|
-
values.push(Number(v.toFixed(d)));
|
|
1817
|
+
values.push(applyDecimals(v, element.decimals));
|
|
1784
1818
|
});
|
|
1785
1819
|
if (!skipValidation) {
|
|
1786
1820
|
const { state } = context;
|
|
@@ -1799,31 +1833,34 @@ function validateNumberElement(element, key, context) {
|
|
|
1799
1833
|
}
|
|
1800
1834
|
return { value: values, errors };
|
|
1801
1835
|
} else {
|
|
1802
|
-
const input = scopeRoot.querySelector(`[name
|
|
1836
|
+
const input = scopeRoot.querySelector(`[name="${key}"]`);
|
|
1803
1837
|
const raw = input?.value ?? "";
|
|
1804
1838
|
const { state } = context;
|
|
1805
1839
|
if (!skipValidation && element.required && raw === "") {
|
|
1806
1840
|
const msg = t("required", state);
|
|
1807
1841
|
errors.push(`${key}: ${msg}`);
|
|
1808
|
-
|
|
1842
|
+
markFieldValidity(input, msg);
|
|
1809
1843
|
return { value: null, errors };
|
|
1810
1844
|
}
|
|
1811
1845
|
if (raw === "") {
|
|
1812
|
-
|
|
1846
|
+
markFieldValidity(input, null);
|
|
1813
1847
|
return { value: null, errors };
|
|
1814
1848
|
}
|
|
1815
1849
|
const v = parseFloat(raw);
|
|
1816
1850
|
if (!skipValidation && !Number.isFinite(v)) {
|
|
1817
1851
|
const msg = t("notANumber", state);
|
|
1818
1852
|
errors.push(`${key}: ${msg}`);
|
|
1819
|
-
|
|
1853
|
+
markFieldValidity(input, msg);
|
|
1820
1854
|
return { value: null, errors };
|
|
1821
1855
|
}
|
|
1822
1856
|
validateNumberInput(input, v, key);
|
|
1823
|
-
|
|
1824
|
-
return { value: Number(v.toFixed(d)), errors };
|
|
1857
|
+
return { value: applyDecimals(v, element.decimals), errors };
|
|
1825
1858
|
}
|
|
1826
1859
|
}
|
|
1860
|
+
function applyDecimals(v, decimals) {
|
|
1861
|
+
if (!Number.isInteger(decimals) || decimals < 0) return v;
|
|
1862
|
+
return Number(v.toFixed(decimals));
|
|
1863
|
+
}
|
|
1827
1864
|
function updateNumberField(element, fieldPath, value, context) {
|
|
1828
1865
|
const { scopeRoot } = context;
|
|
1829
1866
|
if (element.multiple) {
|
|
@@ -1863,6 +1900,38 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1863
1900
|
}
|
|
1864
1901
|
|
|
1865
1902
|
// src/components/select.ts
|
|
1903
|
+
function appendSelectOptions(select, element, selectedValue, state) {
|
|
1904
|
+
const options = element.options || [];
|
|
1905
|
+
if (!options.some((option) => option.value === "")) {
|
|
1906
|
+
const emptyOption = document.createElement("option");
|
|
1907
|
+
emptyOption.value = "";
|
|
1908
|
+
emptyOption.textContent = element.placeholder ?? t("selectPlaceholder", state);
|
|
1909
|
+
select.appendChild(emptyOption);
|
|
1910
|
+
}
|
|
1911
|
+
const strSelected = selectedValue == null ? null : String(selectedValue);
|
|
1912
|
+
let anySelected = false;
|
|
1913
|
+
options.forEach((option) => {
|
|
1914
|
+
const optionEl = document.createElement("option");
|
|
1915
|
+
optionEl.value = option.value;
|
|
1916
|
+
optionEl.textContent = option.label;
|
|
1917
|
+
if (strSelected === option.value) {
|
|
1918
|
+
optionEl.selected = true;
|
|
1919
|
+
anySelected = true;
|
|
1920
|
+
}
|
|
1921
|
+
select.appendChild(optionEl);
|
|
1922
|
+
});
|
|
1923
|
+
if (!anySelected && strSelected !== null && strSelected !== "") {
|
|
1924
|
+
console.warn(
|
|
1925
|
+
`select "${element.key}": prefill value "${strSelected}" is not among the options; leaving the field unselected`
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
if (!anySelected) {
|
|
1929
|
+
const empty = Array.from(select.options).find(
|
|
1930
|
+
(option) => option.value === ""
|
|
1931
|
+
);
|
|
1932
|
+
if (empty) empty.selected = true;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1866
1935
|
function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
1867
1936
|
const state = ctx.state;
|
|
1868
1937
|
const readonly = isElementReadonly(element, state, ctx);
|
|
@@ -1875,18 +1944,18 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1875
1944
|
`;
|
|
1876
1945
|
selectInput.name = pathKey;
|
|
1877
1946
|
selectInput.disabled = readonly;
|
|
1878
|
-
(
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
}
|
|
1885
|
-
selectInput.appendChild(optionEl);
|
|
1886
|
-
});
|
|
1947
|
+
appendSelectOptions(
|
|
1948
|
+
selectInput,
|
|
1949
|
+
element,
|
|
1950
|
+
ctx.prefill[element.key] ?? element.default,
|
|
1951
|
+
state
|
|
1952
|
+
);
|
|
1887
1953
|
if (!readonly && ctx.instance) {
|
|
1888
1954
|
const handleChange = () => {
|
|
1889
|
-
ctx.instance.triggerOnChange(
|
|
1955
|
+
ctx.instance.triggerOnChange(
|
|
1956
|
+
pathKey,
|
|
1957
|
+
selectInput.value === "" ? null : selectInput.value
|
|
1958
|
+
);
|
|
1890
1959
|
};
|
|
1891
1960
|
selectInput.addEventListener("change", handleChange);
|
|
1892
1961
|
}
|
|
@@ -1906,7 +1975,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1906
1975
|
const minCount = element.minCount ?? (element.required ? 1 : 0);
|
|
1907
1976
|
const maxCount = element.maxCount ?? Infinity;
|
|
1908
1977
|
while (values.length < minCount) {
|
|
1909
|
-
values.push(element.default
|
|
1978
|
+
values.push(element.default ?? "");
|
|
1910
1979
|
}
|
|
1911
1980
|
const container = document.createElement("div");
|
|
1912
1981
|
container.className = "fb-row";
|
|
@@ -1931,15 +2000,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1931
2000
|
font-family: var(--fb-font-family);
|
|
1932
2001
|
`;
|
|
1933
2002
|
selectInput.disabled = readonly;
|
|
1934
|
-
(element
|
|
1935
|
-
const optionElement = document.createElement("option");
|
|
1936
|
-
optionElement.value = option.value;
|
|
1937
|
-
optionElement.textContent = option.label;
|
|
1938
|
-
if (value === option.value) {
|
|
1939
|
-
optionElement.selected = true;
|
|
1940
|
-
}
|
|
1941
|
-
selectInput.appendChild(optionElement);
|
|
1942
|
-
});
|
|
2003
|
+
appendSelectOptions(selectInput, element, value, state);
|
|
1943
2004
|
if (!readonly && ctx.instance) {
|
|
1944
2005
|
const handleChange = () => {
|
|
1945
2006
|
ctx.instance.triggerOnChange(selectInput.name, selectInput.value);
|
|
@@ -1976,6 +2037,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1976
2037
|
updateIndices();
|
|
1977
2038
|
updateAddButton();
|
|
1978
2039
|
updateRemoveButtons();
|
|
2040
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
1979
2041
|
}
|
|
1980
2042
|
};
|
|
1981
2043
|
item.appendChild(removeBtn);
|
|
@@ -1991,11 +2053,12 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1991
2053
|
const handle = createAddItemRow(
|
|
1992
2054
|
"select",
|
|
1993
2055
|
() => {
|
|
1994
|
-
const defaultValue = element.default
|
|
2056
|
+
const defaultValue = element.default ?? "";
|
|
1995
2057
|
values.push(defaultValue);
|
|
1996
2058
|
addSelectItem(defaultValue);
|
|
1997
2059
|
updateAddButton();
|
|
1998
2060
|
updateRemoveButtons();
|
|
2061
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
1999
2062
|
},
|
|
2000
2063
|
{ label: element.addLabel }
|
|
2001
2064
|
);
|
|
@@ -2019,38 +2082,6 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
2019
2082
|
function validateSelectElement(element, key, context) {
|
|
2020
2083
|
const errors = [];
|
|
2021
2084
|
const { scopeRoot, skipValidation } = context;
|
|
2022
|
-
const markValidity = (input, errorMessage) => {
|
|
2023
|
-
if (!input) return;
|
|
2024
|
-
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
2025
|
-
let errorElement = document.getElementById(errorId);
|
|
2026
|
-
if (errorMessage) {
|
|
2027
|
-
input.classList.add("invalid");
|
|
2028
|
-
input.title = errorMessage;
|
|
2029
|
-
if (!errorElement) {
|
|
2030
|
-
errorElement = document.createElement("div");
|
|
2031
|
-
errorElement.id = errorId;
|
|
2032
|
-
errorElement.className = "error-message";
|
|
2033
|
-
errorElement.style.cssText = `
|
|
2034
|
-
color: var(--fb-error-color);
|
|
2035
|
-
font-size: var(--fb-font-size-small);
|
|
2036
|
-
margin-top: 0.25rem;
|
|
2037
|
-
`;
|
|
2038
|
-
if (input.nextSibling) {
|
|
2039
|
-
input.parentNode?.insertBefore(errorElement, input.nextSibling);
|
|
2040
|
-
} else {
|
|
2041
|
-
input.parentNode?.appendChild(errorElement);
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
errorElement.textContent = errorMessage;
|
|
2045
|
-
errorElement.style.display = "block";
|
|
2046
|
-
} else {
|
|
2047
|
-
input.classList.remove("invalid");
|
|
2048
|
-
input.title = "";
|
|
2049
|
-
if (errorElement) {
|
|
2050
|
-
errorElement.remove();
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
};
|
|
2054
2085
|
const validateMultipleCount = (key2, values, element2, filterFn) => {
|
|
2055
2086
|
if (skipValidation) return;
|
|
2056
2087
|
const { state } = context;
|
|
@@ -2074,27 +2105,36 @@ function validateSelectElement(element, key, context) {
|
|
|
2074
2105
|
const values = [];
|
|
2075
2106
|
inputs.forEach((input) => {
|
|
2076
2107
|
const val = input?.value ?? "";
|
|
2077
|
-
values.push(val);
|
|
2078
|
-
|
|
2108
|
+
values.push(val === "" ? null : val);
|
|
2109
|
+
markFieldValidity(input, null);
|
|
2079
2110
|
});
|
|
2080
|
-
validateMultipleCount(key, values, element, (v) => v
|
|
2111
|
+
validateMultipleCount(key, values, element, (v) => v != null);
|
|
2081
2112
|
return { value: values, errors };
|
|
2082
2113
|
} else {
|
|
2083
|
-
const input = scopeRoot.querySelector(
|
|
2084
|
-
`[name$="${key}"]`
|
|
2085
|
-
);
|
|
2114
|
+
const input = scopeRoot.querySelector(`[name="${key}"]`);
|
|
2086
2115
|
const val = input?.value ?? "";
|
|
2087
2116
|
if (!skipValidation && element.required && val === "") {
|
|
2088
2117
|
const msg = t("required", context.state);
|
|
2089
2118
|
errors.push(`${key}: ${msg}`);
|
|
2090
|
-
|
|
2119
|
+
markFieldValidity(input, msg);
|
|
2091
2120
|
return { value: null, errors };
|
|
2092
2121
|
} else {
|
|
2093
|
-
|
|
2122
|
+
markFieldValidity(input, null);
|
|
2094
2123
|
}
|
|
2095
2124
|
return { value: val === "" ? null : val, errors };
|
|
2096
2125
|
}
|
|
2097
2126
|
}
|
|
2127
|
+
function assertValueInOptions(select, strValue, fieldPath) {
|
|
2128
|
+
if (strValue === "") return;
|
|
2129
|
+
const match = Array.from(select.options).some(
|
|
2130
|
+
(option) => option.value === strValue
|
|
2131
|
+
);
|
|
2132
|
+
if (!match) {
|
|
2133
|
+
throw new Error(
|
|
2134
|
+
`updateSelectField: value "${strValue}" is not among the options of "${fieldPath}"`
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2098
2138
|
function updateSelectField(element, fieldPath, value, context) {
|
|
2099
2139
|
const { scopeRoot } = context;
|
|
2100
2140
|
if ("multiple" in element && element.multiple) {
|
|
@@ -2109,10 +2149,17 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
2109
2149
|
);
|
|
2110
2150
|
selects.forEach((select, index) => {
|
|
2111
2151
|
if (index < value.length) {
|
|
2112
|
-
|
|
2152
|
+
const strValue = value[index] != null ? String(value[index]) : "";
|
|
2153
|
+
assertValueInOptions(select, strValue, `${fieldPath}[${index}]`);
|
|
2154
|
+
}
|
|
2155
|
+
});
|
|
2156
|
+
selects.forEach((select, index) => {
|
|
2157
|
+
if (index < value.length) {
|
|
2158
|
+
const strValue = value[index] != null ? String(value[index]) : "";
|
|
2159
|
+
select.value = strValue;
|
|
2113
2160
|
const options = select.querySelectorAll("option");
|
|
2114
2161
|
options.forEach((option) => {
|
|
2115
|
-
option.selected = option.value ===
|
|
2162
|
+
option.selected = option.value === strValue;
|
|
2116
2163
|
});
|
|
2117
2164
|
select.classList.remove("invalid");
|
|
2118
2165
|
select.title = "";
|
|
@@ -2129,10 +2176,12 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
2129
2176
|
`[name="${fieldPath}"]`
|
|
2130
2177
|
);
|
|
2131
2178
|
if (select) {
|
|
2132
|
-
|
|
2179
|
+
const strValue = value != null ? String(value) : "";
|
|
2180
|
+
assertValueInOptions(select, strValue, fieldPath);
|
|
2181
|
+
select.value = strValue;
|
|
2133
2182
|
const options = select.querySelectorAll("option");
|
|
2134
2183
|
options.forEach((option) => {
|
|
2135
|
-
option.selected = option.value ===
|
|
2184
|
+
option.selected = option.value === strValue;
|
|
2136
2185
|
});
|
|
2137
2186
|
select.classList.remove("invalid");
|
|
2138
2187
|
select.title = "";
|
|
@@ -2335,7 +2384,7 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
|
2335
2384
|
const minCount = element.minCount ?? (element.required ? 1 : 0);
|
|
2336
2385
|
const maxCount = element.maxCount ?? Infinity;
|
|
2337
2386
|
while (values.length < minCount) {
|
|
2338
|
-
values.push(element.default
|
|
2387
|
+
values.push(element.default ?? "");
|
|
2339
2388
|
}
|
|
2340
2389
|
const readonly = isElementReadonly(element, state, ctx);
|
|
2341
2390
|
const container = document.createElement("div");
|
|
@@ -2413,6 +2462,7 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
|
2413
2462
|
updateIndices();
|
|
2414
2463
|
updateAddButton();
|
|
2415
2464
|
updateRemoveButtons();
|
|
2465
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
2416
2466
|
}
|
|
2417
2467
|
};
|
|
2418
2468
|
item.appendChild(removeBtn);
|
|
@@ -2428,11 +2478,12 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
|
2428
2478
|
const handle = createAddItemRow(
|
|
2429
2479
|
"switcher",
|
|
2430
2480
|
() => {
|
|
2431
|
-
const defaultValue = element.default
|
|
2481
|
+
const defaultValue = element.default ?? "";
|
|
2432
2482
|
values.push(defaultValue);
|
|
2433
2483
|
addSwitcherItem(defaultValue);
|
|
2434
2484
|
updateAddButton();
|
|
2435
2485
|
updateRemoveButtons();
|
|
2486
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
2436
2487
|
},
|
|
2437
2488
|
{ label: element.addLabel }
|
|
2438
2489
|
);
|
|
@@ -2456,38 +2507,6 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
|
2456
2507
|
function validateSwitcherElement(element, key, context) {
|
|
2457
2508
|
const errors = [];
|
|
2458
2509
|
const { scopeRoot, skipValidation } = context;
|
|
2459
|
-
const markValidity = (input, errorMessage) => {
|
|
2460
|
-
if (!input) return;
|
|
2461
|
-
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
2462
|
-
let errorElement = document.getElementById(errorId);
|
|
2463
|
-
if (errorMessage) {
|
|
2464
|
-
input.classList.add("invalid");
|
|
2465
|
-
input.title = errorMessage;
|
|
2466
|
-
if (!errorElement) {
|
|
2467
|
-
errorElement = document.createElement("div");
|
|
2468
|
-
errorElement.id = errorId;
|
|
2469
|
-
errorElement.className = "error-message";
|
|
2470
|
-
errorElement.style.cssText = `
|
|
2471
|
-
color: var(--fb-error-color);
|
|
2472
|
-
font-size: var(--fb-font-size-small);
|
|
2473
|
-
margin-top: 0.25rem;
|
|
2474
|
-
`;
|
|
2475
|
-
if (input.nextSibling) {
|
|
2476
|
-
input.parentNode?.insertBefore(errorElement, input.nextSibling);
|
|
2477
|
-
} else {
|
|
2478
|
-
input.parentNode?.appendChild(errorElement);
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
|
-
errorElement.textContent = errorMessage;
|
|
2482
|
-
errorElement.style.display = "block";
|
|
2483
|
-
} else {
|
|
2484
|
-
input.classList.remove("invalid");
|
|
2485
|
-
input.title = "";
|
|
2486
|
-
if (errorElement) {
|
|
2487
|
-
errorElement.remove();
|
|
2488
|
-
}
|
|
2489
|
-
}
|
|
2490
|
-
};
|
|
2491
2510
|
const validateMultipleCount = (fieldKey, values, el, filterFn) => {
|
|
2492
2511
|
if (skipValidation) return;
|
|
2493
2512
|
const { state } = context;
|
|
@@ -2514,35 +2533,35 @@ function validateSwitcherElement(element, key, context) {
|
|
|
2514
2533
|
const values = [];
|
|
2515
2534
|
inputs.forEach((input) => {
|
|
2516
2535
|
const val = input?.value ?? "";
|
|
2517
|
-
values.push(val);
|
|
2536
|
+
values.push(val === "" ? null : val);
|
|
2518
2537
|
if (!skipValidation && val !== "" && !validOptionValues.has(val)) {
|
|
2519
2538
|
const msg = t("invalidOption", context.state);
|
|
2520
|
-
|
|
2539
|
+
markFieldValidity(input, msg);
|
|
2521
2540
|
errors.push(`${key}: ${msg}`);
|
|
2522
2541
|
} else {
|
|
2523
|
-
|
|
2542
|
+
markFieldValidity(input, null);
|
|
2524
2543
|
}
|
|
2525
2544
|
});
|
|
2526
|
-
validateMultipleCount(key, values, element, (v) => v
|
|
2545
|
+
validateMultipleCount(key, values, element, (v) => v != null);
|
|
2527
2546
|
return { value: values, errors };
|
|
2528
2547
|
} else {
|
|
2529
2548
|
const input = scopeRoot.querySelector(
|
|
2530
|
-
`input[type="hidden"][name
|
|
2549
|
+
`input[type="hidden"][name="${key}"]`
|
|
2531
2550
|
);
|
|
2532
2551
|
const val = input?.value ?? "";
|
|
2533
2552
|
if (!skipValidation && element.required && val === "") {
|
|
2534
2553
|
const msg = t("required", context.state);
|
|
2535
2554
|
errors.push(`${key}: ${msg}`);
|
|
2536
|
-
|
|
2555
|
+
markFieldValidity(input, msg);
|
|
2537
2556
|
return { value: null, errors };
|
|
2538
2557
|
}
|
|
2539
2558
|
if (!skipValidation && val !== "" && !validOptionValues.has(val)) {
|
|
2540
2559
|
const msg = t("invalidOption", context.state);
|
|
2541
2560
|
errors.push(`${key}: ${msg}`);
|
|
2542
|
-
|
|
2561
|
+
markFieldValidity(input, msg);
|
|
2543
2562
|
return { value: null, errors };
|
|
2544
2563
|
}
|
|
2545
|
-
|
|
2564
|
+
markFieldValidity(input, null);
|
|
2546
2565
|
return { value: val === "" ? null : val, errors };
|
|
2547
2566
|
}
|
|
2548
2567
|
}
|
|
@@ -2714,6 +2733,7 @@ function renderBooleanElement(element, ctx, wrapper, pathKey) {
|
|
|
2714
2733
|
const hiddenInput = document.createElement("input");
|
|
2715
2734
|
hiddenInput.type = "hidden";
|
|
2716
2735
|
hiddenInput.name = pathKey;
|
|
2736
|
+
hiddenInput.setAttribute("data-boolean-field", "true");
|
|
2717
2737
|
hiddenInput.value = initial ? "true" : "false";
|
|
2718
2738
|
const row = document.createElement("div");
|
|
2719
2739
|
row.className = "fb-toggle-row";
|
|
@@ -5569,7 +5589,7 @@ function validateSingleFile(element, key, context) {
|
|
|
5569
5589
|
const { scopeRoot, skipValidation, state } = context;
|
|
5570
5590
|
const errors = [];
|
|
5571
5591
|
const input = scopeRoot.querySelector(
|
|
5572
|
-
`input[name
|
|
5592
|
+
`input[name="${key}"][type="hidden"]`
|
|
5573
5593
|
);
|
|
5574
5594
|
const rid = input?.value ?? "";
|
|
5575
5595
|
if (!skipValidation && element.required && rid === "") {
|
|
@@ -5789,7 +5809,7 @@ function createReadonlyColourUI(value) {
|
|
|
5789
5809
|
container.appendChild(hexText);
|
|
5790
5810
|
return container;
|
|
5791
5811
|
}
|
|
5792
|
-
function createEditColourUI(value, pathKey, ctx) {
|
|
5812
|
+
function createEditColourUI(value, pathKey, ctx, placeholder) {
|
|
5793
5813
|
const normalizedValue = normalizeColourValue(value);
|
|
5794
5814
|
const pickerWrapper = document.createElement("div");
|
|
5795
5815
|
pickerWrapper.className = "colour-picker-wrapper";
|
|
@@ -5813,9 +5833,10 @@ function createEditColourUI(value, pathKey, ctx) {
|
|
|
5813
5833
|
const hexInput = document.createElement("input");
|
|
5814
5834
|
hexInput.type = "text";
|
|
5815
5835
|
hexInput.className = "colour-hex-input";
|
|
5836
|
+
hexInput.setAttribute("data-colour-field", "true");
|
|
5816
5837
|
hexInput.name = pathKey;
|
|
5817
5838
|
hexInput.value = normalizedValue;
|
|
5818
|
-
hexInput.placeholder = "#000000";
|
|
5839
|
+
hexInput.placeholder = placeholder ?? "#000000";
|
|
5819
5840
|
hexInput.style.cssText = `
|
|
5820
5841
|
width: 100px;
|
|
5821
5842
|
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
@@ -5904,12 +5925,17 @@ function createEditColourUI(value, pathKey, ctx) {
|
|
|
5904
5925
|
function renderColourElement(element, ctx, wrapper, pathKey) {
|
|
5905
5926
|
const state = ctx.state;
|
|
5906
5927
|
const readonly = isElementReadonly(element, state, ctx);
|
|
5907
|
-
const initialValue = ctx.prefill[element.key]
|
|
5928
|
+
const initialValue = ctx.prefill[element.key] ?? element.default ?? "#000000";
|
|
5908
5929
|
if (readonly) {
|
|
5909
5930
|
const readonlyUI = createReadonlyColourUI(initialValue);
|
|
5910
5931
|
wrapper.appendChild(readonlyUI);
|
|
5911
5932
|
} else {
|
|
5912
|
-
const editUI = createEditColourUI(
|
|
5933
|
+
const editUI = createEditColourUI(
|
|
5934
|
+
initialValue,
|
|
5935
|
+
pathKey,
|
|
5936
|
+
ctx,
|
|
5937
|
+
element.placeholder
|
|
5938
|
+
);
|
|
5913
5939
|
wrapper.appendChild(editUI);
|
|
5914
5940
|
}
|
|
5915
5941
|
if (!readonly) {
|
|
@@ -5931,7 +5957,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
5931
5957
|
const minCount = element.minCount ?? (element.required ? 1 : 0);
|
|
5932
5958
|
const maxCount = element.maxCount ?? Infinity;
|
|
5933
5959
|
while (values.length < minCount) {
|
|
5934
|
-
values.push(element.default
|
|
5960
|
+
values.push(element.default ?? "#000000");
|
|
5935
5961
|
}
|
|
5936
5962
|
const container = document.createElement("div");
|
|
5937
5963
|
container.className = "fb-row";
|
|
@@ -5955,7 +5981,12 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
5955
5981
|
}
|
|
5956
5982
|
} else {
|
|
5957
5983
|
const tempPathKey = `${pathKey}[${container.children.length}]`;
|
|
5958
|
-
const editUI = createEditColourUI(
|
|
5984
|
+
const editUI = createEditColourUI(
|
|
5985
|
+
value,
|
|
5986
|
+
tempPathKey,
|
|
5987
|
+
ctx,
|
|
5988
|
+
element.placeholder
|
|
5989
|
+
);
|
|
5959
5990
|
editUI.style.flex = "1";
|
|
5960
5991
|
itemWrapper.appendChild(editUI);
|
|
5961
5992
|
}
|
|
@@ -6001,6 +6032,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
6001
6032
|
updateIndices();
|
|
6002
6033
|
updateAddButton();
|
|
6003
6034
|
updateRemoveButtons();
|
|
6035
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
6004
6036
|
}
|
|
6005
6037
|
};
|
|
6006
6038
|
item.appendChild(removeBtn);
|
|
@@ -6016,11 +6048,12 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
6016
6048
|
const handle = createAddItemRow(
|
|
6017
6049
|
"colour",
|
|
6018
6050
|
() => {
|
|
6019
|
-
const defaultColour = element.default
|
|
6051
|
+
const defaultColour = element.default ?? "#000000";
|
|
6020
6052
|
values.push(defaultColour);
|
|
6021
6053
|
addColourItem(defaultColour);
|
|
6022
6054
|
updateAddButton();
|
|
6023
6055
|
updateRemoveButtons();
|
|
6056
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
6024
6057
|
},
|
|
6025
6058
|
{ label: element.addLabel }
|
|
6026
6059
|
);
|
|
@@ -6048,58 +6081,26 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
6048
6081
|
function validateColourElement(element, key, context) {
|
|
6049
6082
|
const errors = [];
|
|
6050
6083
|
const { scopeRoot, skipValidation } = context;
|
|
6051
|
-
const markValidity = (input, errorMessage) => {
|
|
6052
|
-
if (!input) return;
|
|
6053
|
-
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
6054
|
-
let errorElement = document.getElementById(errorId);
|
|
6055
|
-
if (errorMessage) {
|
|
6056
|
-
input.classList.add("invalid");
|
|
6057
|
-
input.title = errorMessage;
|
|
6058
|
-
if (!errorElement) {
|
|
6059
|
-
errorElement = document.createElement("div");
|
|
6060
|
-
errorElement.id = errorId;
|
|
6061
|
-
errorElement.className = "error-message";
|
|
6062
|
-
errorElement.style.cssText = `
|
|
6063
|
-
color: var(--fb-error-color);
|
|
6064
|
-
font-size: var(--fb-font-size-small);
|
|
6065
|
-
margin-top: 0.25rem;
|
|
6066
|
-
`;
|
|
6067
|
-
if (input.nextSibling) {
|
|
6068
|
-
input.parentNode?.insertBefore(errorElement, input.nextSibling);
|
|
6069
|
-
} else {
|
|
6070
|
-
input.parentNode?.appendChild(errorElement);
|
|
6071
|
-
}
|
|
6072
|
-
}
|
|
6073
|
-
errorElement.textContent = errorMessage;
|
|
6074
|
-
errorElement.style.display = "block";
|
|
6075
|
-
} else {
|
|
6076
|
-
input.classList.remove("invalid");
|
|
6077
|
-
input.title = "";
|
|
6078
|
-
if (errorElement) {
|
|
6079
|
-
errorElement.remove();
|
|
6080
|
-
}
|
|
6081
|
-
}
|
|
6082
|
-
};
|
|
6083
6084
|
const validateColourValue = (input, val, fieldKey) => {
|
|
6084
6085
|
const { state } = context;
|
|
6085
6086
|
if (!val) {
|
|
6086
6087
|
if (!skipValidation && element.required) {
|
|
6087
6088
|
const msg = t("required", state);
|
|
6088
6089
|
errors.push(`${fieldKey}: ${msg}`);
|
|
6089
|
-
|
|
6090
|
+
markFieldValidity(input, msg);
|
|
6090
6091
|
return "";
|
|
6091
6092
|
}
|
|
6092
|
-
|
|
6093
|
+
markFieldValidity(input, null);
|
|
6093
6094
|
return "";
|
|
6094
6095
|
}
|
|
6095
6096
|
const normalized = normalizeColourValue(val);
|
|
6096
6097
|
if (!skipValidation && !isValidHexColour(normalized)) {
|
|
6097
6098
|
const msg = t("invalidHexColour", state);
|
|
6098
6099
|
errors.push(`${fieldKey}: ${msg}`);
|
|
6099
|
-
|
|
6100
|
+
markFieldValidity(input, msg);
|
|
6100
6101
|
return val;
|
|
6101
6102
|
}
|
|
6102
|
-
|
|
6103
|
+
markFieldValidity(input, null);
|
|
6103
6104
|
return normalized;
|
|
6104
6105
|
};
|
|
6105
6106
|
if (element.multiple) {
|
|
@@ -6136,7 +6137,7 @@ function validateColourElement(element, key, context) {
|
|
|
6136
6137
|
if (!skipValidation && element.required && val === "") {
|
|
6137
6138
|
const msg = t("required", context.state);
|
|
6138
6139
|
errors.push(`${key}: ${msg}`);
|
|
6139
|
-
|
|
6140
|
+
markFieldValidity(hexInput, msg);
|
|
6140
6141
|
return { value: "", errors };
|
|
6141
6142
|
}
|
|
6142
6143
|
const validated = validateColourValue(hexInput, val, key);
|
|
@@ -6457,6 +6458,7 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
6457
6458
|
updateIndices();
|
|
6458
6459
|
updateAddButton();
|
|
6459
6460
|
updateRemoveButtons();
|
|
6461
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
6460
6462
|
}
|
|
6461
6463
|
};
|
|
6462
6464
|
item.appendChild(removeBtn);
|
|
@@ -6476,6 +6478,7 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
6476
6478
|
addSliderItem(defaultValue);
|
|
6477
6479
|
updateAddButton();
|
|
6478
6480
|
updateRemoveButtons();
|
|
6481
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
6479
6482
|
},
|
|
6480
6483
|
{ label: element.addLabel }
|
|
6481
6484
|
);
|
|
@@ -6517,42 +6520,6 @@ function validateSliderElement(element, key, context) {
|
|
|
6517
6520
|
const max = element.max;
|
|
6518
6521
|
const step = element.step ?? 1;
|
|
6519
6522
|
const scale = element.scale || "linear";
|
|
6520
|
-
const markValidity = (input, errorMessage) => {
|
|
6521
|
-
if (!input) return;
|
|
6522
|
-
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
6523
|
-
let errorElement = document.getElementById(errorId);
|
|
6524
|
-
if (errorMessage) {
|
|
6525
|
-
input.classList.add("invalid");
|
|
6526
|
-
input.title = errorMessage;
|
|
6527
|
-
if (!errorElement) {
|
|
6528
|
-
errorElement = document.createElement("div");
|
|
6529
|
-
errorElement.id = errorId;
|
|
6530
|
-
errorElement.className = "error-message";
|
|
6531
|
-
errorElement.style.cssText = `
|
|
6532
|
-
color: var(--fb-error-color);
|
|
6533
|
-
font-size: var(--fb-font-size-small);
|
|
6534
|
-
margin-top: 0.25rem;
|
|
6535
|
-
`;
|
|
6536
|
-
const sliderContainer = input.closest(".slider-container");
|
|
6537
|
-
if (sliderContainer && sliderContainer.nextSibling) {
|
|
6538
|
-
sliderContainer.parentNode?.insertBefore(
|
|
6539
|
-
errorElement,
|
|
6540
|
-
sliderContainer.nextSibling
|
|
6541
|
-
);
|
|
6542
|
-
} else if (sliderContainer) {
|
|
6543
|
-
sliderContainer.parentNode?.appendChild(errorElement);
|
|
6544
|
-
}
|
|
6545
|
-
}
|
|
6546
|
-
errorElement.textContent = errorMessage;
|
|
6547
|
-
errorElement.style.display = "block";
|
|
6548
|
-
} else {
|
|
6549
|
-
input.classList.remove("invalid");
|
|
6550
|
-
input.title = "";
|
|
6551
|
-
if (errorElement) {
|
|
6552
|
-
errorElement.remove();
|
|
6553
|
-
}
|
|
6554
|
-
}
|
|
6555
|
-
};
|
|
6556
6523
|
const validateSliderValue = (slider, fieldKey) => {
|
|
6557
6524
|
const { state } = context;
|
|
6558
6525
|
const rawValue = slider.value;
|
|
@@ -6560,10 +6527,10 @@ function validateSliderElement(element, key, context) {
|
|
|
6560
6527
|
if (!skipValidation && element.required) {
|
|
6561
6528
|
const msg = t("required", state);
|
|
6562
6529
|
errors.push(`${fieldKey}: ${msg}`);
|
|
6563
|
-
|
|
6530
|
+
markFieldValidity(slider, msg);
|
|
6564
6531
|
return null;
|
|
6565
6532
|
}
|
|
6566
|
-
|
|
6533
|
+
markFieldValidity(slider, null);
|
|
6567
6534
|
return null;
|
|
6568
6535
|
}
|
|
6569
6536
|
let value;
|
|
@@ -6579,17 +6546,17 @@ function validateSliderElement(element, key, context) {
|
|
|
6579
6546
|
if (value < min) {
|
|
6580
6547
|
const msg = t("minValue", state, { min });
|
|
6581
6548
|
errors.push(`${fieldKey}: ${msg}`);
|
|
6582
|
-
|
|
6549
|
+
markFieldValidity(slider, msg);
|
|
6583
6550
|
return value;
|
|
6584
6551
|
}
|
|
6585
6552
|
if (value > max) {
|
|
6586
6553
|
const msg = t("maxValue", state, { max });
|
|
6587
6554
|
errors.push(`${fieldKey}: ${msg}`);
|
|
6588
|
-
|
|
6555
|
+
markFieldValidity(slider, msg);
|
|
6589
6556
|
return value;
|
|
6590
6557
|
}
|
|
6591
6558
|
}
|
|
6592
|
-
|
|
6559
|
+
markFieldValidity(slider, null);
|
|
6593
6560
|
return value;
|
|
6594
6561
|
};
|
|
6595
6562
|
if (element.multiple) {
|
|
@@ -6734,20 +6701,12 @@ function extractRootFormData(formRoot) {
|
|
|
6734
6701
|
inputs.forEach((input) => {
|
|
6735
6702
|
const fieldName = input.getAttribute("name");
|
|
6736
6703
|
if (fieldName && !fieldName.includes("[") && !fieldName.includes(".")) {
|
|
6737
|
-
if (input instanceof
|
|
6738
|
-
|
|
6739
|
-
} else if (input instanceof HTMLInputElement) {
|
|
6740
|
-
if (input.type === "checkbox") {
|
|
6741
|
-
data[fieldName] = input.checked;
|
|
6742
|
-
} else if (input.type === "radio") {
|
|
6743
|
-
if (input.checked) {
|
|
6744
|
-
data[fieldName] = input.value;
|
|
6745
|
-
}
|
|
6746
|
-
} else {
|
|
6704
|
+
if (input instanceof HTMLInputElement && input.type === "radio") {
|
|
6705
|
+
if (input.checked) {
|
|
6747
6706
|
data[fieldName] = input.value;
|
|
6748
6707
|
}
|
|
6749
|
-
} else
|
|
6750
|
-
data[fieldName] = input
|
|
6708
|
+
} else {
|
|
6709
|
+
data[fieldName] = readTypedInputValue(input);
|
|
6751
6710
|
}
|
|
6752
6711
|
}
|
|
6753
6712
|
});
|
|
@@ -6816,7 +6775,7 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
6816
6775
|
};
|
|
6817
6776
|
element.elements.forEach((child) => {
|
|
6818
6777
|
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
6819
|
-
const prefillVal = containerPrefill[child.key]
|
|
6778
|
+
const prefillVal = child.key in containerPrefill ? containerPrefill[child.key] : ("default" in child ? child.default : null) ?? null;
|
|
6820
6779
|
itemsWrap.appendChild(
|
|
6821
6780
|
createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
|
|
6822
6781
|
);
|
|
@@ -6861,7 +6820,7 @@ function mountRemoveButton(item, onRemove, state, containerLabel) {
|
|
|
6861
6820
|
item.classList.add("fb-row-removable");
|
|
6862
6821
|
item.insertBefore(rem, item.firstChild);
|
|
6863
6822
|
}
|
|
6864
|
-
function renderMultipleContainerElement(element, ctx, wrapper,
|
|
6823
|
+
function renderMultipleContainerElement(element, ctx, wrapper, pathKey) {
|
|
6865
6824
|
const state = ctx.state;
|
|
6866
6825
|
const containerIsReadonly = isElementReadonly(element, state, ctx);
|
|
6867
6826
|
const childInheritedReadonly = containerIsReadonly || ctx.inheritedReadonly;
|
|
@@ -6894,6 +6853,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6894
6853
|
if (countItems() <= min) return;
|
|
6895
6854
|
item.remove();
|
|
6896
6855
|
updateAddButton();
|
|
6856
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
6897
6857
|
};
|
|
6898
6858
|
const createContainerItem = (idx, rowPrefill, formData) => {
|
|
6899
6859
|
const subCtx = {
|
|
@@ -6916,7 +6876,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6916
6876
|
);
|
|
6917
6877
|
element.elements.forEach((child) => {
|
|
6918
6878
|
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
6919
|
-
const hiddenValue = rowPrefill
|
|
6879
|
+
const hiddenValue = rowPrefill && child.key in rowPrefill ? rowPrefill[child.key] : ("default" in child ? child.default : null) ?? null;
|
|
6920
6880
|
childWrapper.appendChild(
|
|
6921
6881
|
createHiddenInput(pathJoin(subCtx.path, child.key), hiddenValue)
|
|
6922
6882
|
);
|
|
@@ -6950,6 +6910,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
6950
6910
|
itemsWrap.appendChild(item);
|
|
6951
6911
|
}
|
|
6952
6912
|
updateAddButton();
|
|
6913
|
+
ctx.instance?.triggerOnChange(pathKey);
|
|
6953
6914
|
};
|
|
6954
6915
|
let slideAddTile = null;
|
|
6955
6916
|
let slideAddUpdate = null;
|
|
@@ -7024,19 +6985,16 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
7024
6985
|
}
|
|
7025
6986
|
}
|
|
7026
6987
|
}
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
validateElementFunc = fn;
|
|
7030
|
-
}
|
|
7031
|
-
function validateElement(element, ctx, customScopeRoot) {
|
|
7032
|
-
if (!validateElementFunc) {
|
|
6988
|
+
function requireValidateElement(context) {
|
|
6989
|
+
if (!context.validateElement) {
|
|
7033
6990
|
throw new Error(
|
|
7034
|
-
"validateElement
|
|
6991
|
+
"validateContainerElement: context.validateElement missing \u2014 container validation requires the instance validator"
|
|
7035
6992
|
);
|
|
7036
6993
|
}
|
|
7037
|
-
return
|
|
6994
|
+
return context.validateElement;
|
|
7038
6995
|
}
|
|
7039
6996
|
function validateContainerElement(element, key, context) {
|
|
6997
|
+
const validateChild = requireValidateElement(context);
|
|
7040
6998
|
const errors = [];
|
|
7041
6999
|
const { scopeRoot, skipValidation, path } = context;
|
|
7042
7000
|
if (!("elements" in element)) {
|
|
@@ -7087,7 +7045,7 @@ function validateContainerElement(element, key, context) {
|
|
|
7087
7045
|
}
|
|
7088
7046
|
}
|
|
7089
7047
|
const childKey = `${key}[${domIndex}].${child.key}`;
|
|
7090
|
-
const childResult =
|
|
7048
|
+
const childResult = validateChild(
|
|
7091
7049
|
{ ...child, key: childKey },
|
|
7092
7050
|
{ path },
|
|
7093
7051
|
itemContainer
|
|
@@ -7128,7 +7086,7 @@ function validateContainerElement(element, key, context) {
|
|
|
7128
7086
|
}
|
|
7129
7087
|
{
|
|
7130
7088
|
const childKey = `${key}.${child.key}`;
|
|
7131
|
-
const childResult =
|
|
7089
|
+
const childResult = validateChild(
|
|
7132
7090
|
{ ...child, key: childKey },
|
|
7133
7091
|
{ path },
|
|
7134
7092
|
containerContainer
|
|
@@ -7243,7 +7201,7 @@ function renderGroupElement(element, ctx, wrapper, pathKey) {
|
|
|
7243
7201
|
maxCount: element.repeat?.max
|
|
7244
7202
|
};
|
|
7245
7203
|
if (containerElement.multiple) {
|
|
7246
|
-
renderMultipleContainerElement(containerElement, ctx, wrapper);
|
|
7204
|
+
renderMultipleContainerElement(containerElement, ctx, wrapper, pathKey);
|
|
7247
7205
|
} else {
|
|
7248
7206
|
renderSingleContainerElement(containerElement, ctx, wrapper, pathKey);
|
|
7249
7207
|
}
|
|
@@ -7619,7 +7577,7 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
7619
7577
|
rebuild();
|
|
7620
7578
|
} catch (e) {
|
|
7621
7579
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
7622
|
-
console.error(t("tableImportError", state
|
|
7580
|
+
console.error(t("tableImportError", state, { error: errMsg }));
|
|
7623
7581
|
} finally {
|
|
7624
7582
|
overlay.remove();
|
|
7625
7583
|
}
|
|
@@ -8561,7 +8519,7 @@ function updateTableField(element, fieldPath, value, context) {
|
|
|
8561
8519
|
}
|
|
8562
8520
|
|
|
8563
8521
|
// src/components/richinput.ts
|
|
8564
|
-
function applyAutoExpand2(textarea, backdrop) {
|
|
8522
|
+
function applyAutoExpand2(textarea, backdrop, observers) {
|
|
8565
8523
|
textarea.style.overflow = "hidden";
|
|
8566
8524
|
textarea.style.resize = "none";
|
|
8567
8525
|
const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
|
|
@@ -8583,6 +8541,7 @@ function applyAutoExpand2(textarea, backdrop) {
|
|
|
8583
8541
|
const ro = new ResizeObserver((entries) => {
|
|
8584
8542
|
if (!textarea.isConnected) {
|
|
8585
8543
|
ro.disconnect();
|
|
8544
|
+
observers.delete(ro);
|
|
8586
8545
|
return;
|
|
8587
8546
|
}
|
|
8588
8547
|
const entry = entries[0];
|
|
@@ -8592,6 +8551,7 @@ function applyAutoExpand2(textarea, backdrop) {
|
|
|
8592
8551
|
resize();
|
|
8593
8552
|
});
|
|
8594
8553
|
ro.observe(textarea);
|
|
8554
|
+
observers.add(ro);
|
|
8595
8555
|
}
|
|
8596
8556
|
function buildFileLabels(files, state) {
|
|
8597
8557
|
const labels = /* @__PURE__ */ new Map();
|
|
@@ -9161,7 +9121,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
9161
9121
|
`;
|
|
9162
9122
|
const textarea = document.createElement("textarea");
|
|
9163
9123
|
textarea.name = `${pathKey}__text`;
|
|
9164
|
-
textarea.placeholder = element.placeholder
|
|
9124
|
+
textarea.placeholder = element.placeholder ?? t("richinputPlaceholder", state);
|
|
9165
9125
|
const rawInitialText = initialValue.text ?? "";
|
|
9166
9126
|
textarea.value = rawInitialText ? replaceRidsWithFilenames(rawInitialText, files, state) : "";
|
|
9167
9127
|
textarea.style.cssText = `
|
|
@@ -9178,7 +9138,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
9178
9138
|
z-index: 1;
|
|
9179
9139
|
caret-color: var(--fb-text-color, #111827);
|
|
9180
9140
|
`;
|
|
9181
|
-
applyAutoExpand2(textarea, backdrop);
|
|
9141
|
+
applyAutoExpand2(textarea, backdrop, ctx.state.autoExpandObservers);
|
|
9182
9142
|
textarea.addEventListener("scroll", () => {
|
|
9183
9143
|
backdrop.scrollTop = textarea.scrollTop;
|
|
9184
9144
|
});
|
|
@@ -10151,12 +10111,7 @@ function validateHiddenElement(element, key, context) {
|
|
|
10151
10111
|
const input = scopeRoot.querySelector(
|
|
10152
10112
|
`input[type="hidden"][data-hidden-field="true"][name="${key}"]`
|
|
10153
10113
|
);
|
|
10154
|
-
|
|
10155
|
-
if (raw === "") {
|
|
10156
|
-
const defaultVal = "default" in element ? element.default : null;
|
|
10157
|
-
return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
|
|
10158
|
-
}
|
|
10159
|
-
return { value: deserializeHiddenValue(raw), errors: [] };
|
|
10114
|
+
return { value: deserializeHiddenValue(input?.value ?? ""), errors: [] };
|
|
10160
10115
|
}
|
|
10161
10116
|
function updateHiddenField(_element, fieldPath, value, context) {
|
|
10162
10117
|
const { scopeRoot } = context;
|
|
@@ -10240,15 +10195,19 @@ var componentRegistry = {
|
|
|
10240
10195
|
function getComponentOperations(elementType) {
|
|
10241
10196
|
return componentRegistry[elementType] || null;
|
|
10242
10197
|
}
|
|
10198
|
+
function resolveOperations(element) {
|
|
10199
|
+
const isHiddenField = element.type !== "markdown" && (element.type === "hidden" || Boolean(element.hidden));
|
|
10200
|
+
return isHiddenField ? componentRegistry.hidden : getComponentOperations(element.type);
|
|
10201
|
+
}
|
|
10243
10202
|
function validateElementWithComponent(element, key, context) {
|
|
10244
|
-
const ops =
|
|
10203
|
+
const ops = resolveOperations(element);
|
|
10245
10204
|
if (ops && ops.validate) {
|
|
10246
10205
|
return ops.validate(element, key, context);
|
|
10247
10206
|
}
|
|
10248
10207
|
return null;
|
|
10249
10208
|
}
|
|
10250
10209
|
function updateElementWithComponent(element, fieldPath, value, context) {
|
|
10251
|
-
const ops =
|
|
10210
|
+
const ops = resolveOperations(element);
|
|
10252
10211
|
if (ops && ops.update) {
|
|
10253
10212
|
ops.update(element, fieldPath, value, context);
|
|
10254
10213
|
return true;
|
|
@@ -10260,6 +10219,10 @@ function updateElementWithComponent(element, fieldPath, value, context) {
|
|
|
10260
10219
|
function showTooltip(tooltipId, button) {
|
|
10261
10220
|
const tooltip = document.getElementById(tooltipId);
|
|
10262
10221
|
if (!tooltip) return;
|
|
10222
|
+
if (!button.isConnected) {
|
|
10223
|
+
tooltip.remove();
|
|
10224
|
+
return;
|
|
10225
|
+
}
|
|
10263
10226
|
const isCurrentlyVisible = !tooltip.classList.contains("hidden");
|
|
10264
10227
|
document.querySelectorAll('[id^="tooltip-"]').forEach((t2) => {
|
|
10265
10228
|
t2.classList.add("hidden");
|
|
@@ -10339,23 +10302,13 @@ function extractDOMValue(fieldPath, formRoot) {
|
|
|
10339
10302
|
if (!input) {
|
|
10340
10303
|
return void 0;
|
|
10341
10304
|
}
|
|
10342
|
-
if (input instanceof
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
} else if (input.type === "radio") {
|
|
10348
|
-
const checked = formRoot.querySelector(
|
|
10349
|
-
`[name="${fieldPath}"]:checked`
|
|
10350
|
-
);
|
|
10351
|
-
return checked ? checked.value : void 0;
|
|
10352
|
-
} else {
|
|
10353
|
-
return input.value;
|
|
10354
|
-
}
|
|
10355
|
-
} else if (input instanceof HTMLTextAreaElement) {
|
|
10356
|
-
return input.value;
|
|
10305
|
+
if (input instanceof HTMLInputElement && input.type === "radio") {
|
|
10306
|
+
const checked = formRoot.querySelector(
|
|
10307
|
+
`[name="${fieldPath}"]:checked`
|
|
10308
|
+
);
|
|
10309
|
+
return checked ? checked.value : void 0;
|
|
10357
10310
|
}
|
|
10358
|
-
return
|
|
10311
|
+
return readTypedInputValue(input);
|
|
10359
10312
|
}
|
|
10360
10313
|
function buildScopedDataAtPath(path, value) {
|
|
10361
10314
|
const segments = path.match(/[^.[\]]+|\[\d+\]/g);
|
|
@@ -10484,11 +10437,19 @@ function createFieldLabel(element) {
|
|
|
10484
10437
|
}
|
|
10485
10438
|
return title;
|
|
10486
10439
|
}
|
|
10487
|
-
function
|
|
10440
|
+
function ensureTooltipStyles(doc) {
|
|
10441
|
+
if (doc.head.querySelector("[data-fb-tooltip-styles]")) return;
|
|
10442
|
+
const style = doc.createElement("style");
|
|
10443
|
+
style.setAttribute("data-fb-tooltip-styles", "");
|
|
10444
|
+
style.textContent = `[id^="tooltip-"].hidden { display: none; }`;
|
|
10445
|
+
doc.head.appendChild(style);
|
|
10446
|
+
}
|
|
10447
|
+
function createInfoButton(element, state) {
|
|
10488
10448
|
const infoBtn = document.createElement("button");
|
|
10489
10449
|
infoBtn.type = "button";
|
|
10490
10450
|
infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
|
|
10491
10451
|
infoBtn.innerHTML = '<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>';
|
|
10452
|
+
ensureTooltipStyles(document);
|
|
10492
10453
|
const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
|
|
10493
10454
|
const tooltip = document.createElement("div");
|
|
10494
10455
|
tooltip.id = tooltipId;
|
|
@@ -10496,6 +10457,7 @@ function createInfoButton(element) {
|
|
|
10496
10457
|
tooltip.style.position = "fixed";
|
|
10497
10458
|
tooltip.textContent = element.description || element.hint || "Field information";
|
|
10498
10459
|
document.body.appendChild(tooltip);
|
|
10460
|
+
state.tooltipElements.add(tooltip);
|
|
10499
10461
|
infoBtn.onclick = (e) => {
|
|
10500
10462
|
e.preventDefault();
|
|
10501
10463
|
e.stopPropagation();
|
|
@@ -10503,7 +10465,7 @@ function createInfoButton(element) {
|
|
|
10503
10465
|
};
|
|
10504
10466
|
return infoBtn;
|
|
10505
10467
|
}
|
|
10506
|
-
function createLabelContainer(element) {
|
|
10468
|
+
function createLabelContainer(element, state) {
|
|
10507
10469
|
const label = document.createElement("div");
|
|
10508
10470
|
label.className = "flex items-center";
|
|
10509
10471
|
label.style.marginBottom = "var(--fb-label-margin-bottom, 2px)";
|
|
@@ -10511,7 +10473,7 @@ function createLabelContainer(element) {
|
|
|
10511
10473
|
const title = createFieldLabel(element);
|
|
10512
10474
|
label.appendChild(title);
|
|
10513
10475
|
if (element.description || element.hint) {
|
|
10514
|
-
const infoBtn = createInfoButton(element);
|
|
10476
|
+
const infoBtn = createInfoButton(element, state);
|
|
10515
10477
|
label.appendChild(infoBtn);
|
|
10516
10478
|
}
|
|
10517
10479
|
return label;
|
|
@@ -10586,7 +10548,7 @@ function dispatchToRenderer(element, ctx, wrapper, pathKey) {
|
|
|
10586
10548
|
break;
|
|
10587
10549
|
case "container":
|
|
10588
10550
|
if (isMultiple) {
|
|
10589
|
-
renderMultipleContainerElement(element, ctx, wrapper);
|
|
10551
|
+
renderMultipleContainerElement(element, ctx, wrapper, pathKey);
|
|
10590
10552
|
} else {
|
|
10591
10553
|
renderSingleContainerElement(element, ctx, wrapper, pathKey);
|
|
10592
10554
|
}
|
|
@@ -10639,7 +10601,7 @@ function renderElement2(element, ctx) {
|
|
|
10639
10601
|
wrapper.setAttribute("data-fb-width", element.width || "full");
|
|
10640
10602
|
const ops = getComponentOperations(element.type);
|
|
10641
10603
|
if (!ops?.ownsLabel) {
|
|
10642
|
-
const label = createLabelContainer(element);
|
|
10604
|
+
const label = createLabelContainer(element, ctx.state);
|
|
10643
10605
|
wrapper.appendChild(label);
|
|
10644
10606
|
}
|
|
10645
10607
|
const pathKey = pathJoin(ctx.path, element.key);
|
|
@@ -10668,6 +10630,7 @@ var defaultConfig = {
|
|
|
10668
10630
|
onDownloadError: null,
|
|
10669
10631
|
debounceMs: 300,
|
|
10670
10632
|
verboseErrors: false,
|
|
10633
|
+
postMessageTarget: null,
|
|
10671
10634
|
enableFilePreview: true,
|
|
10672
10635
|
maxPreviewSize: "200px",
|
|
10673
10636
|
readonly: false,
|
|
@@ -10689,6 +10652,7 @@ var defaultConfig = {
|
|
|
10689
10652
|
openInNewTab: "Open in new tab",
|
|
10690
10653
|
changeButton: "Change",
|
|
10691
10654
|
placeholderText: "Enter text",
|
|
10655
|
+
selectPlaceholder: "Select\u2026",
|
|
10692
10656
|
previewAlt: "Preview",
|
|
10693
10657
|
previewUnavailable: "Preview unavailable",
|
|
10694
10658
|
previewError: "Preview error",
|
|
@@ -10764,6 +10728,7 @@ var defaultConfig = {
|
|
|
10764
10728
|
openInNewTab: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0432 \u043D\u043E\u0432\u043E\u0439 \u0432\u043A\u043B\u0430\u0434\u043A\u0435",
|
|
10765
10729
|
changeButton: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C",
|
|
10766
10730
|
placeholderText: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442",
|
|
10731
|
+
selectPlaceholder: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435\u2026",
|
|
10767
10732
|
previewAlt: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",
|
|
10768
10733
|
previewUnavailable: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
|
|
10769
10734
|
previewError: "\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430",
|
|
@@ -10828,20 +10793,23 @@ var defaultConfig = {
|
|
|
10828
10793
|
},
|
|
10829
10794
|
theme: {}
|
|
10830
10795
|
};
|
|
10831
|
-
function
|
|
10832
|
-
const
|
|
10833
|
-
|
|
10834
|
-
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
config.translations
|
|
10838
|
-
)) {
|
|
10839
|
-
mergedTranslations[locale] = {
|
|
10840
|
-
...defaultConfig.translations[locale] || {},
|
|
10796
|
+
function mergeTranslations(base, overrides) {
|
|
10797
|
+
const merged = { ...base };
|
|
10798
|
+
if (overrides) {
|
|
10799
|
+
for (const [locale, userTranslations] of Object.entries(overrides)) {
|
|
10800
|
+
merged[locale] = {
|
|
10801
|
+
...base[locale] || {},
|
|
10841
10802
|
...userTranslations
|
|
10842
10803
|
};
|
|
10843
10804
|
}
|
|
10844
10805
|
}
|
|
10806
|
+
return merged;
|
|
10807
|
+
}
|
|
10808
|
+
function createInstanceState(config) {
|
|
10809
|
+
const mergedTranslations = mergeTranslations(
|
|
10810
|
+
defaultConfig.translations,
|
|
10811
|
+
config?.translations
|
|
10812
|
+
);
|
|
10845
10813
|
return {
|
|
10846
10814
|
schema: null,
|
|
10847
10815
|
formRoot: null,
|
|
@@ -10857,7 +10825,9 @@ function createInstanceState(config) {
|
|
|
10857
10825
|
prefill: {},
|
|
10858
10826
|
syntheticElementIds: /* @__PURE__ */ new WeakMap(),
|
|
10859
10827
|
syntheticElementIdCounter: 0,
|
|
10860
|
-
enableIfObservers: /* @__PURE__ */ new Set()
|
|
10828
|
+
enableIfObservers: /* @__PURE__ */ new Set(),
|
|
10829
|
+
autoExpandObservers: /* @__PURE__ */ new Set(),
|
|
10830
|
+
tooltipElements: /* @__PURE__ */ new Set()
|
|
10861
10831
|
};
|
|
10862
10832
|
}
|
|
10863
10833
|
function generateInstanceId() {
|
|
@@ -11158,6 +11128,11 @@ function findOwnField(scope, lookupKey, ownBoundary) {
|
|
|
11158
11128
|
}
|
|
11159
11129
|
var FormBuilderInstance = class {
|
|
11160
11130
|
constructor(config) {
|
|
11131
|
+
// The bound prefill-hint click handler currently attached to the form root.
|
|
11132
|
+
// Kept so renderForm()/destroy() can remove it — re-binding on every render
|
|
11133
|
+
// stacked listeners (hint clicks applied values N times) and destroy()
|
|
11134
|
+
// left the last one on the host-owned root, retaining the instance.
|
|
11135
|
+
this.prefillHintHandler = null;
|
|
11161
11136
|
this.instanceId = generateInstanceId();
|
|
11162
11137
|
this.state = createInstanceState(config);
|
|
11163
11138
|
if (this.state.config.verboseErrors) {
|
|
@@ -11191,10 +11166,21 @@ var FormBuilderInstance = class {
|
|
|
11191
11166
|
this.state.formRoot = element;
|
|
11192
11167
|
}
|
|
11193
11168
|
/**
|
|
11194
|
-
* Configure the form builder
|
|
11169
|
+
* Configure the form builder. Translations deep-merge per locale (same as
|
|
11170
|
+
* the constructor); a locale without translations — configured or default —
|
|
11171
|
+
* is rejected, matching setLocale.
|
|
11195
11172
|
*/
|
|
11196
11173
|
configure(config) {
|
|
11197
|
-
|
|
11174
|
+
const translations = mergeTranslations(
|
|
11175
|
+
this.state.config.translations,
|
|
11176
|
+
config.translations
|
|
11177
|
+
);
|
|
11178
|
+
if (config.locale !== void 0 && !translations[config.locale]) {
|
|
11179
|
+
throw new Error(
|
|
11180
|
+
`configure: no translations configured for locale "${config.locale}"`
|
|
11181
|
+
);
|
|
11182
|
+
}
|
|
11183
|
+
Object.assign(this.state.config, config, { translations });
|
|
11198
11184
|
}
|
|
11199
11185
|
/**
|
|
11200
11186
|
* Set file upload handler
|
|
@@ -11227,17 +11213,25 @@ var FormBuilderInstance = class {
|
|
|
11227
11213
|
this.state.config.readonly = mode === "readonly";
|
|
11228
11214
|
}
|
|
11229
11215
|
/**
|
|
11230
|
-
* Set locale
|
|
11216
|
+
* Set locale. Custom locales are allowed — their translations must have
|
|
11217
|
+
* been provided via the constructor or configure() first.
|
|
11231
11218
|
*/
|
|
11232
11219
|
setLocale(locale) {
|
|
11233
|
-
if (this.state.config.translations[locale]) {
|
|
11234
|
-
|
|
11220
|
+
if (!this.state.config.translations[locale]) {
|
|
11221
|
+
throw new Error(
|
|
11222
|
+
`setLocale: no translations configured for locale "${locale}"`
|
|
11223
|
+
);
|
|
11235
11224
|
}
|
|
11225
|
+
this.state.config.locale = locale;
|
|
11236
11226
|
}
|
|
11237
11227
|
/**
|
|
11238
11228
|
* Trigger onChange callbacks with debouncing
|
|
11239
11229
|
* @param fieldPath - Optional field path for field-specific change events
|
|
11240
|
-
* @param fieldValue - Optional field value for field-specific change events
|
|
11230
|
+
* @param fieldValue - Optional field value for field-specific change events.
|
|
11231
|
+
* When omitted while fieldPath is given, the value is read from the
|
|
11232
|
+
* freshly extracted form data at debounce time — used by structural
|
|
11233
|
+
* changes (multi-item add/remove), where the handler has no cheap
|
|
11234
|
+
* current value but the array is trivially derivable after the fact.
|
|
11241
11235
|
*/
|
|
11242
11236
|
triggerOnChange(fieldPath, fieldValue) {
|
|
11243
11237
|
if (this.state.config.readonly) return;
|
|
@@ -11250,12 +11244,49 @@ var FormBuilderInstance = class {
|
|
|
11250
11244
|
if (this.state.config.onChange) {
|
|
11251
11245
|
this.state.config.onChange(formData);
|
|
11252
11246
|
}
|
|
11253
|
-
if (this.state.config.onFieldChange && fieldPath !== void 0
|
|
11254
|
-
this.
|
|
11247
|
+
if (this.state.config.onFieldChange && fieldPath !== void 0) {
|
|
11248
|
+
const resolvedValue = fieldValue !== void 0 ? fieldValue : this.resolveDomPathValue(formData.data, fieldPath);
|
|
11249
|
+
this.state.config.onFieldChange(fieldPath, resolvedValue, formData);
|
|
11255
11250
|
}
|
|
11256
11251
|
this.state.debounceTimer = null;
|
|
11257
11252
|
}, this.state.config.debounceMs);
|
|
11258
11253
|
}
|
|
11254
|
+
/**
|
|
11255
|
+
* Resolve a DOM field path against the extracted form data.
|
|
11256
|
+
*
|
|
11257
|
+
* A plain getValueByPath is wrong for paths inside a multiple container:
|
|
11258
|
+
* row markers keep gaps after a deletion (`s[2]` may be the first surviving
|
|
11259
|
+
* row) while the extracted array is re-packed contiguously — the naive
|
|
11260
|
+
* lookup would read a different row, or nothing. Each `[N]` segment is
|
|
11261
|
+
* mapped from its marker to the row's position among the container's
|
|
11262
|
+
* rendered rows, the same DOM order extraction used to build the array.
|
|
11263
|
+
* A bracketed segment that is not a container marker (a multi-value leaf
|
|
11264
|
+
* like `tags[1]`, whose indices are contiguous) falls back to the index.
|
|
11265
|
+
*/
|
|
11266
|
+
resolveDomPathValue(data, domPath) {
|
|
11267
|
+
let cur = data;
|
|
11268
|
+
let domPrefix = "";
|
|
11269
|
+
for (const seg of domPath.split(".")) {
|
|
11270
|
+
if (cur === null || cur === void 0) return void 0;
|
|
11271
|
+
const marker = seg.match(/^(.+)\[(\d+)\]$/);
|
|
11272
|
+
if (!marker) {
|
|
11273
|
+
domPrefix = domPrefix ? `${domPrefix}.${seg}` : seg;
|
|
11274
|
+
cur = cur[seg];
|
|
11275
|
+
continue;
|
|
11276
|
+
}
|
|
11277
|
+
const key = marker[1];
|
|
11278
|
+
domPrefix = domPrefix ? `${domPrefix}.${key}` : key;
|
|
11279
|
+
const arr = cur[key];
|
|
11280
|
+
if (!Array.isArray(arr)) return void 0;
|
|
11281
|
+
const rows = this.state.formRoot ? findDirectContainerRows(this.state.formRoot, domPrefix) : [];
|
|
11282
|
+
domPrefix = `${domPrefix}[${marker[2]}]`;
|
|
11283
|
+
const pos = rows.findIndex(
|
|
11284
|
+
(row) => row.getAttribute("data-container-item") === domPrefix
|
|
11285
|
+
);
|
|
11286
|
+
cur = pos >= 0 ? arr[pos] : arr[parseInt(marker[2], 10)];
|
|
11287
|
+
}
|
|
11288
|
+
return cur;
|
|
11289
|
+
}
|
|
11259
11290
|
/**
|
|
11260
11291
|
* Register an external action that will be displayed as a button
|
|
11261
11292
|
* External actions can be form-level (no related_field) or field-level (with related_field)
|
|
@@ -11295,21 +11326,10 @@ var FormBuilderInstance = class {
|
|
|
11295
11326
|
*/
|
|
11296
11327
|
findFormElementByFieldPath(fieldPath) {
|
|
11297
11328
|
if (!this.state.formRoot) return null;
|
|
11298
|
-
|
|
11329
|
+
const element = this.state.formRoot.querySelector(
|
|
11299
11330
|
`[name="${fieldPath}"]`
|
|
11300
11331
|
);
|
|
11301
11332
|
if (element) return element;
|
|
11302
|
-
const variations = [
|
|
11303
|
-
fieldPath,
|
|
11304
|
-
fieldPath.replace(/\[(\d+)\]/g, "[$1]"),
|
|
11305
|
-
fieldPath.replace(/\./g, "[") + "]".repeat((fieldPath.match(/\./g) || []).length)
|
|
11306
|
-
];
|
|
11307
|
-
for (const variation of variations) {
|
|
11308
|
-
element = this.state.formRoot.querySelector(
|
|
11309
|
-
`[name="${variation}"]`
|
|
11310
|
-
);
|
|
11311
|
-
if (element) return element;
|
|
11312
|
-
}
|
|
11313
11333
|
const schemaElement = this.findSchemaElement(fieldPath);
|
|
11314
11334
|
if (!schemaElement) return null;
|
|
11315
11335
|
const fieldWrappers = this.state.formRoot.querySelectorAll(".fb-field-wrapper");
|
|
@@ -11555,10 +11575,13 @@ var FormBuilderInstance = class {
|
|
|
11555
11575
|
renderForm(root, schema, prefill, actions) {
|
|
11556
11576
|
const errors = validateSchema(schema);
|
|
11557
11577
|
if (errors.length > 0) {
|
|
11558
|
-
|
|
11559
|
-
|
|
11578
|
+
throw new Error(`renderForm: invalid schema:
|
|
11579
|
+
- ${errors.join("\n- ")}`);
|
|
11560
11580
|
}
|
|
11561
11581
|
this.disconnectEnableIfObservers();
|
|
11582
|
+
this.disconnectAutoExpandObservers();
|
|
11583
|
+
this.removeTooltipElements();
|
|
11584
|
+
this.removePrefillHintListener();
|
|
11562
11585
|
this.state.formRoot = root;
|
|
11563
11586
|
this.state.schema = schema;
|
|
11564
11587
|
this.state.externalActions = actions || null;
|
|
@@ -11581,7 +11604,7 @@ var FormBuilderInstance = class {
|
|
|
11581
11604
|
}
|
|
11582
11605
|
schema.elements.forEach((element) => {
|
|
11583
11606
|
if (element.type !== "markdown" && (element.hidden || element.type === "hidden")) {
|
|
11584
|
-
const val = prefill
|
|
11607
|
+
const val = prefill && element.key in prefill ? prefill[element.key] : element.default ?? null;
|
|
11585
11608
|
fieldsWrapper.appendChild(createHiddenInput(element.key, val));
|
|
11586
11609
|
return;
|
|
11587
11610
|
}
|
|
@@ -11598,7 +11621,10 @@ var FormBuilderInstance = class {
|
|
|
11598
11621
|
rootContainer.appendChild(fieldsWrapper);
|
|
11599
11622
|
root.appendChild(rootContainer);
|
|
11600
11623
|
if (!this.state.config.readonly) {
|
|
11601
|
-
|
|
11624
|
+
this.prefillHintHandler = this.handlePrefillHintClick.bind(
|
|
11625
|
+
this
|
|
11626
|
+
);
|
|
11627
|
+
root.addEventListener("click", this.prefillHintHandler);
|
|
11602
11628
|
}
|
|
11603
11629
|
if (this.state.config.readonly && this.state.externalActions && Array.isArray(this.state.externalActions)) {
|
|
11604
11630
|
this.renderExternalActions();
|
|
@@ -11614,7 +11640,7 @@ var FormBuilderInstance = class {
|
|
|
11614
11640
|
return { valid: true, errors: [], data: {} };
|
|
11615
11641
|
const errors = [];
|
|
11616
11642
|
const data = {};
|
|
11617
|
-
const
|
|
11643
|
+
const validateElement = (element, ctx, customScopeRoot = null) => {
|
|
11618
11644
|
const key = element.key ?? "";
|
|
11619
11645
|
const scopeRoot = customScopeRoot || this.state.formRoot;
|
|
11620
11646
|
const componentContext = {
|
|
@@ -11622,7 +11648,10 @@ var FormBuilderInstance = class {
|
|
|
11622
11648
|
state: this.state,
|
|
11623
11649
|
instance: this,
|
|
11624
11650
|
path: ctx.path,
|
|
11625
|
-
skipValidation
|
|
11651
|
+
skipValidation,
|
|
11652
|
+
// Containers recurse into their children through this — threaded per
|
|
11653
|
+
// pass, never module state (see ComponentContext.validateElement).
|
|
11654
|
+
validateElement
|
|
11626
11655
|
};
|
|
11627
11656
|
const componentResult = validateElementWithComponent(
|
|
11628
11657
|
element,
|
|
@@ -11640,7 +11669,6 @@ var FormBuilderInstance = class {
|
|
|
11640
11669
|
console.warn(`Unknown field type "${element.type}" for key "${key}"`);
|
|
11641
11670
|
return { value: null, spread: false };
|
|
11642
11671
|
};
|
|
11643
|
-
setValidateElement(validateElement2);
|
|
11644
11672
|
this.state.schema.elements.forEach((element) => {
|
|
11645
11673
|
if (element.enableIf) {
|
|
11646
11674
|
try {
|
|
@@ -11658,24 +11686,12 @@ var FormBuilderInstance = class {
|
|
|
11658
11686
|
if (element.type === "markdown") {
|
|
11659
11687
|
return;
|
|
11660
11688
|
}
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
);
|
|
11665
|
-
|
|
11666
|
-
|
|
11667
|
-
data[element.key] = deserializeHiddenValue(raw);
|
|
11668
|
-
} else {
|
|
11669
|
-
data[element.key] = element.default !== void 0 ? element.default : null;
|
|
11670
|
-
}
|
|
11671
|
-
} else {
|
|
11672
|
-
const result = validateElement2(element, { path: "" });
|
|
11673
|
-
if (result.skip) return;
|
|
11674
|
-
if (result.spread && result.value !== null && typeof result.value === "object") {
|
|
11675
|
-
Object.assign(data, result.value);
|
|
11676
|
-
} else if (element.key) {
|
|
11677
|
-
data[element.key] = result.value;
|
|
11678
|
-
}
|
|
11689
|
+
const result = validateElement(element, { path: "" });
|
|
11690
|
+
if (result.skip) return;
|
|
11691
|
+
if (result.spread && result.value !== null && typeof result.value === "object") {
|
|
11692
|
+
Object.assign(data, result.value);
|
|
11693
|
+
} else if (element.key) {
|
|
11694
|
+
data[element.key] = result.value;
|
|
11679
11695
|
}
|
|
11680
11696
|
});
|
|
11681
11697
|
return {
|
|
@@ -11696,16 +11712,7 @@ var FormBuilderInstance = class {
|
|
|
11696
11712
|
submitForm() {
|
|
11697
11713
|
const result = this.validateForm(false);
|
|
11698
11714
|
if (result.valid) {
|
|
11699
|
-
|
|
11700
|
-
window.parent.postMessage(
|
|
11701
|
-
{
|
|
11702
|
-
type: "formSubmit",
|
|
11703
|
-
data: result.data,
|
|
11704
|
-
schema: this.state.schema
|
|
11705
|
-
},
|
|
11706
|
-
"*"
|
|
11707
|
-
);
|
|
11708
|
-
}
|
|
11715
|
+
this.postToParent("formSubmit", result.data);
|
|
11709
11716
|
}
|
|
11710
11717
|
return result;
|
|
11711
11718
|
}
|
|
@@ -11714,17 +11721,27 @@ var FormBuilderInstance = class {
|
|
|
11714
11721
|
*/
|
|
11715
11722
|
saveDraft() {
|
|
11716
11723
|
const result = this.validateForm(true);
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11724
|
+
this.postToParent("formDraft", result.data);
|
|
11725
|
+
return result;
|
|
11726
|
+
}
|
|
11727
|
+
/**
|
|
11728
|
+
* Post form data to the parent frame — only when the host opted in via
|
|
11729
|
+
* `postMessageTarget`. Outside an iframe `window.parent === window`, so an
|
|
11730
|
+
* unconditional post broadcast form data and the full schema to any
|
|
11731
|
+
* embedding page (targetOrigin "*") on every submit. See CHANGELOG 0.6.0.
|
|
11732
|
+
*/
|
|
11733
|
+
postToParent(type, data) {
|
|
11734
|
+
const target = this.state.config.postMessageTarget;
|
|
11735
|
+
if (target === "") {
|
|
11736
|
+
throw new Error(
|
|
11737
|
+
'postMessageTarget: "" is not a valid target origin \u2014 use null to disable posting or "*" to knowingly broadcast'
|
|
11725
11738
|
);
|
|
11726
11739
|
}
|
|
11727
|
-
return
|
|
11740
|
+
if (!target || typeof window === "undefined" || !window.parent) return;
|
|
11741
|
+
window.parent.postMessage(
|
|
11742
|
+
{ type, data, schema: this.state.schema },
|
|
11743
|
+
target
|
|
11744
|
+
);
|
|
11728
11745
|
}
|
|
11729
11746
|
/**
|
|
11730
11747
|
* Clear the form - reset all field values to empty while preserving form structure
|
|
@@ -11994,6 +12011,9 @@ var FormBuilderInstance = class {
|
|
|
11994
12011
|
this.state.debounceTimer = null;
|
|
11995
12012
|
}
|
|
11996
12013
|
this.disconnectEnableIfObservers();
|
|
12014
|
+
this.disconnectAutoExpandObservers();
|
|
12015
|
+
this.removeTooltipElements();
|
|
12016
|
+
this.removePrefillHintListener();
|
|
11997
12017
|
this.state.resourceIndex.clear();
|
|
11998
12018
|
if (this.state.formRoot) {
|
|
11999
12019
|
clear(this.state.formRoot);
|
|
@@ -12011,6 +12031,24 @@ var FormBuilderInstance = class {
|
|
|
12011
12031
|
}
|
|
12012
12032
|
this.state.enableIfObservers.clear();
|
|
12013
12033
|
}
|
|
12034
|
+
disconnectAutoExpandObservers() {
|
|
12035
|
+
for (const observer of this.state.autoExpandObservers) {
|
|
12036
|
+
observer.disconnect();
|
|
12037
|
+
}
|
|
12038
|
+
this.state.autoExpandObservers.clear();
|
|
12039
|
+
}
|
|
12040
|
+
removePrefillHintListener() {
|
|
12041
|
+
if (this.prefillHintHandler && this.state.formRoot) {
|
|
12042
|
+
this.state.formRoot.removeEventListener("click", this.prefillHintHandler);
|
|
12043
|
+
}
|
|
12044
|
+
this.prefillHintHandler = null;
|
|
12045
|
+
}
|
|
12046
|
+
removeTooltipElements() {
|
|
12047
|
+
for (const tooltip of this.state.tooltipElements) {
|
|
12048
|
+
tooltip.remove();
|
|
12049
|
+
}
|
|
12050
|
+
this.state.tooltipElements.clear();
|
|
12051
|
+
}
|
|
12014
12052
|
};
|
|
12015
12053
|
|
|
12016
12054
|
// src/index.ts
|