@dmitryvim/form-builder 0.5.4 → 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 +7 -1
- package/dist/browser/formbuilder.min.js +136 -154
- package/dist/browser/formbuilder.v0.6.4.min.js +1472 -0
- package/dist/cjs/index.cjs +423 -445
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +368 -394
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +136 -154
- package/dist/types/components/container.d.ts +0 -7
- package/dist/types/components/index.d.ts +1 -2
- package/dist/types/instance/FormBuilderInstance.d.ts +17 -4
- 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 +8 -0
- package/dist/types/utils/helpers.d.ts +18 -1
- package/dist/types/utils/styles.d.ts +13 -7
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.5.4.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
|
}
|
|
@@ -58,7 +58,7 @@ function formatFileSize(bytes) {
|
|
|
58
58
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
59
59
|
}
|
|
60
60
|
function serializeHiddenValue(value) {
|
|
61
|
-
if (value ===
|
|
61
|
+
if (value === void 0) return "";
|
|
62
62
|
return JSON.stringify(value);
|
|
63
63
|
}
|
|
64
64
|
function deserializeHiddenValue(raw) {
|
|
@@ -69,6 +69,23 @@ function deserializeHiddenValue(raw) {
|
|
|
69
69
|
return raw;
|
|
70
70
|
}
|
|
71
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
|
+
}
|
|
72
89
|
function createHiddenInput(name, value) {
|
|
73
90
|
const input = document.createElement("input");
|
|
74
91
|
input.type = "hidden";
|
|
@@ -306,6 +323,14 @@ function validateSchema(schema) {
|
|
|
306
323
|
}
|
|
307
324
|
}
|
|
308
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
|
+
});
|
|
309
334
|
elements.forEach((element, index) => {
|
|
310
335
|
const elementPath = `${path}[${index}]`;
|
|
311
336
|
if (!element.type) {
|
|
@@ -473,12 +498,55 @@ function deepEqual(a, b) {
|
|
|
473
498
|
}
|
|
474
499
|
|
|
475
500
|
// src/utils/styles.ts
|
|
476
|
-
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);
|
|
477
506
|
const name = input.getAttribute("name");
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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();
|
|
482
550
|
}
|
|
483
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>';
|
|
484
552
|
function ensureThemingHooks(doc) {
|
|
@@ -600,7 +668,7 @@ function ensureThemingHooks(doc) {
|
|
|
600
668
|
`;
|
|
601
669
|
doc.head.appendChild(style);
|
|
602
670
|
}
|
|
603
|
-
function applyAutoExpand(textarea, options
|
|
671
|
+
function applyAutoExpand(textarea, options) {
|
|
604
672
|
textarea.style.overflow = "hidden";
|
|
605
673
|
textarea.style.resize = "none";
|
|
606
674
|
const minRows = Math.max(1, options.minRows ?? 1);
|
|
@@ -630,6 +698,7 @@ function applyAutoExpand(textarea, options = {}) {
|
|
|
630
698
|
const ro = new ResizeObserver((entries) => {
|
|
631
699
|
if (!textarea.isConnected) {
|
|
632
700
|
ro.disconnect();
|
|
701
|
+
options.observers?.delete(ro);
|
|
633
702
|
return;
|
|
634
703
|
}
|
|
635
704
|
const entry = entries[0];
|
|
@@ -639,6 +708,7 @@ function applyAutoExpand(textarea, options = {}) {
|
|
|
639
708
|
resize();
|
|
640
709
|
});
|
|
641
710
|
ro.observe(textarea);
|
|
711
|
+
options.observers?.add(ro);
|
|
642
712
|
}
|
|
643
713
|
function applySingleLineMode(textarea) {
|
|
644
714
|
textarea.addEventListener("keydown", (e) => {
|
|
@@ -950,10 +1020,10 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
950
1020
|
`;
|
|
951
1021
|
textInput.name = pathKey;
|
|
952
1022
|
textInput.placeholder = element.placeholder ?? t("placeholderText", state);
|
|
953
|
-
textInput.value = ctx.prefill[element.key]
|
|
1023
|
+
textInput.value = ctx.prefill[element.key] ?? element.default ?? "";
|
|
954
1024
|
textInput.readOnly = readonly;
|
|
955
1025
|
applySingleLineMode(textInput);
|
|
956
|
-
applyAutoExpand(textInput);
|
|
1026
|
+
applyAutoExpand(textInput, { observers: state.autoExpandObservers });
|
|
957
1027
|
if (!readonly) {
|
|
958
1028
|
textInput.addEventListener("focus", () => {
|
|
959
1029
|
textInput.style.borderColor = "var(--fb-border-focus-color)";
|
|
@@ -1011,7 +1081,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1011
1081
|
const chip = input.closest(".fb-chip");
|
|
1012
1082
|
const sib = chip?.nextElementSibling;
|
|
1013
1083
|
if (sib && sib.classList.contains("error-message")) {
|
|
1014
|
-
sib.
|
|
1084
|
+
sib.setAttribute("data-error-for", input.name);
|
|
1015
1085
|
}
|
|
1016
1086
|
});
|
|
1017
1087
|
}
|
|
@@ -1026,7 +1096,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1026
1096
|
input.rows = 1;
|
|
1027
1097
|
input.className = "fb-chip-input";
|
|
1028
1098
|
input.value = value;
|
|
1029
|
-
input.placeholder = element.placeholder
|
|
1099
|
+
input.placeholder = element.placeholder ?? t("placeholderText", state);
|
|
1030
1100
|
input.readOnly = readonly;
|
|
1031
1101
|
chip.appendChild(input);
|
|
1032
1102
|
if (!readonly && ctx.instance) {
|
|
@@ -1040,7 +1110,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1040
1110
|
input.addEventListener("input", handleChange);
|
|
1041
1111
|
}
|
|
1042
1112
|
applySingleLineMode(input);
|
|
1043
|
-
applyAutoExpand(input);
|
|
1113
|
+
applyAutoExpand(input, { observers: state.autoExpandObservers });
|
|
1044
1114
|
if (!readonly) {
|
|
1045
1115
|
const rem = document.createElement("button");
|
|
1046
1116
|
rem.type = "button";
|
|
@@ -1104,40 +1174,6 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1104
1174
|
function validateTextElement(element, key, context) {
|
|
1105
1175
|
const errors = [];
|
|
1106
1176
|
const { scopeRoot, skipValidation } = context;
|
|
1107
|
-
const markValidity = (input, errorMessage) => {
|
|
1108
|
-
if (!input) return;
|
|
1109
|
-
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
1110
|
-
let errorElement = document.getElementById(errorId);
|
|
1111
|
-
if (errorMessage) {
|
|
1112
|
-
input.classList.add("invalid");
|
|
1113
|
-
input.title = errorMessage;
|
|
1114
|
-
if (!errorElement) {
|
|
1115
|
-
errorElement = document.createElement("div");
|
|
1116
|
-
errorElement.id = errorId;
|
|
1117
|
-
errorElement.className = "error-message";
|
|
1118
|
-
errorElement.style.cssText = `
|
|
1119
|
-
color: var(--fb-error-color);
|
|
1120
|
-
font-size: var(--fb-font-size-small);
|
|
1121
|
-
margin-top: 0.25rem;
|
|
1122
|
-
`;
|
|
1123
|
-
const chipAncestor = input.closest?.(".fb-chip");
|
|
1124
|
-
const anchor = chipAncestor || input;
|
|
1125
|
-
if (anchor.nextSibling) {
|
|
1126
|
-
anchor.parentNode?.insertBefore(errorElement, anchor.nextSibling);
|
|
1127
|
-
} else {
|
|
1128
|
-
anchor.parentNode?.appendChild(errorElement);
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
errorElement.textContent = errorMessage;
|
|
1132
|
-
errorElement.style.display = "block";
|
|
1133
|
-
} else {
|
|
1134
|
-
input.classList.remove("invalid");
|
|
1135
|
-
input.title = "";
|
|
1136
|
-
if (errorElement) {
|
|
1137
|
-
errorElement.remove();
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
};
|
|
1141
1177
|
const validateTextInput = (input, val, fieldKey) => {
|
|
1142
1178
|
let hasError = false;
|
|
1143
1179
|
const { state } = context;
|
|
@@ -1145,12 +1181,12 @@ function validateTextElement(element, key, context) {
|
|
|
1145
1181
|
if (element.minLength !== void 0 && element.minLength !== null && val.length < element.minLength) {
|
|
1146
1182
|
const msg = t("minLength", state, { min: element.minLength });
|
|
1147
1183
|
errors.push(`${fieldKey}: ${msg}`);
|
|
1148
|
-
|
|
1184
|
+
markFieldValidity(input, msg);
|
|
1149
1185
|
hasError = true;
|
|
1150
1186
|
} else if (element.maxLength !== void 0 && element.maxLength !== null && val.length > element.maxLength) {
|
|
1151
1187
|
const msg = t("maxLength", state, { max: element.maxLength });
|
|
1152
1188
|
errors.push(`${fieldKey}: ${msg}`);
|
|
1153
|
-
|
|
1189
|
+
markFieldValidity(input, msg);
|
|
1154
1190
|
hasError = true;
|
|
1155
1191
|
} else if (element.pattern) {
|
|
1156
1192
|
try {
|
|
@@ -1158,19 +1194,19 @@ function validateTextElement(element, key, context) {
|
|
|
1158
1194
|
if (!re.test(val)) {
|
|
1159
1195
|
const msg = t("patternMismatch", state);
|
|
1160
1196
|
errors.push(`${fieldKey}: ${msg}`);
|
|
1161
|
-
|
|
1197
|
+
markFieldValidity(input, msg);
|
|
1162
1198
|
hasError = true;
|
|
1163
1199
|
}
|
|
1164
1200
|
} catch {
|
|
1165
1201
|
const msg = t("invalidPattern", state);
|
|
1166
1202
|
errors.push(`${fieldKey}: ${msg}`);
|
|
1167
|
-
|
|
1203
|
+
markFieldValidity(input, msg);
|
|
1168
1204
|
hasError = true;
|
|
1169
1205
|
}
|
|
1170
1206
|
}
|
|
1171
1207
|
}
|
|
1172
1208
|
if (!hasError) {
|
|
1173
|
-
|
|
1209
|
+
markFieldValidity(input, null);
|
|
1174
1210
|
}
|
|
1175
1211
|
};
|
|
1176
1212
|
if (element.multiple) {
|
|
@@ -1205,7 +1241,7 @@ function validateTextElement(element, key, context) {
|
|
|
1205
1241
|
if (!skipValidation && element.required && val === "") {
|
|
1206
1242
|
const msg = t("required", context.state);
|
|
1207
1243
|
errors.push(`${key}: ${msg}`);
|
|
1208
|
-
|
|
1244
|
+
markFieldValidity(input, msg);
|
|
1209
1245
|
return { value: null, errors };
|
|
1210
1246
|
}
|
|
1211
1247
|
if (input) {
|
|
@@ -1267,8 +1303,8 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1267
1303
|
line-height: var(--fb-line-height, 1.5);
|
|
1268
1304
|
`;
|
|
1269
1305
|
textareaInput.name = pathKey;
|
|
1270
|
-
textareaInput.placeholder = element.placeholder ?? "
|
|
1271
|
-
textareaInput.value = ctx.prefill[element.key]
|
|
1306
|
+
textareaInput.placeholder = element.placeholder ?? t("placeholderText", state);
|
|
1307
|
+
textareaInput.value = ctx.prefill[element.key] ?? element.default ?? "";
|
|
1272
1308
|
textareaInput.readOnly = readonly;
|
|
1273
1309
|
if (!readonly && ctx.instance) {
|
|
1274
1310
|
const handleChange = () => {
|
|
@@ -1278,7 +1314,10 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1278
1314
|
textareaInput.addEventListener("blur", handleChange);
|
|
1279
1315
|
textareaInput.addEventListener("input", handleChange);
|
|
1280
1316
|
}
|
|
1281
|
-
applyAutoExpand(textareaInput, {
|
|
1317
|
+
applyAutoExpand(textareaInput, {
|
|
1318
|
+
minRows: element.rows ?? 1,
|
|
1319
|
+
observers: state.autoExpandObservers
|
|
1320
|
+
});
|
|
1282
1321
|
textareaWrapper.appendChild(textareaInput);
|
|
1283
1322
|
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
1284
1323
|
const counter = createCharCounter(element, textareaInput);
|
|
@@ -1321,7 +1360,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1321
1360
|
font-family: var(--fb-font-family);
|
|
1322
1361
|
line-height: var(--fb-line-height, 1.5);
|
|
1323
1362
|
`;
|
|
1324
|
-
textareaInput.placeholder = element.placeholder
|
|
1363
|
+
textareaInput.placeholder = element.placeholder ?? t("placeholderText", state);
|
|
1325
1364
|
textareaInput.value = value;
|
|
1326
1365
|
textareaInput.readOnly = readonly;
|
|
1327
1366
|
if (!readonly && ctx.instance) {
|
|
@@ -1332,7 +1371,10 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1332
1371
|
textareaInput.addEventListener("blur", handleChange);
|
|
1333
1372
|
textareaInput.addEventListener("input", handleChange);
|
|
1334
1373
|
}
|
|
1335
|
-
applyAutoExpand(textareaInput, {
|
|
1374
|
+
applyAutoExpand(textareaInput, {
|
|
1375
|
+
minRows: element.rows ?? 1,
|
|
1376
|
+
observers: state.autoExpandObservers
|
|
1377
|
+
});
|
|
1336
1378
|
textareaContainer.appendChild(textareaInput);
|
|
1337
1379
|
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
1338
1380
|
const counter = createCharCounter(element, textareaInput);
|
|
@@ -1551,6 +1593,17 @@ function createNumberRangeHint(element, input) {
|
|
|
1551
1593
|
updateColor();
|
|
1552
1594
|
return hint;
|
|
1553
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
|
+
}
|
|
1554
1607
|
function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
1555
1608
|
const state = ctx.state;
|
|
1556
1609
|
const readonly = isElementReadonly(element, state, ctx);
|
|
@@ -1559,11 +1612,12 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1559
1612
|
const numberInput = document.createElement("input");
|
|
1560
1613
|
numberInput.type = "number";
|
|
1561
1614
|
numberInput.name = pathKey;
|
|
1562
|
-
numberInput.placeholder = element.placeholder
|
|
1615
|
+
numberInput.placeholder = element.placeholder ?? "0";
|
|
1563
1616
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
1564
1617
|
if (element.max !== void 0) numberInput.max = element.max.toString();
|
|
1565
|
-
|
|
1566
|
-
numberInput
|
|
1618
|
+
numberInput.step = numberStepAttr(element);
|
|
1619
|
+
applyDecimalsMarker(numberInput, element);
|
|
1620
|
+
numberInput.value = ctx.prefill[element.key] ?? element.default ?? "";
|
|
1567
1621
|
numberInput.readOnly = readonly;
|
|
1568
1622
|
if (!element.stepper) {
|
|
1569
1623
|
numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
@@ -1602,7 +1656,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1602
1656
|
const minCount = element.minCount ?? (element.required ? 1 : 0);
|
|
1603
1657
|
const maxCount = element.maxCount ?? Infinity;
|
|
1604
1658
|
while (values.length < minCount) {
|
|
1605
|
-
values.push(element.default
|
|
1659
|
+
values.push(element.default ?? "");
|
|
1606
1660
|
}
|
|
1607
1661
|
const container = document.createElement("div");
|
|
1608
1662
|
container.className = "fb-row";
|
|
@@ -1631,10 +1685,11 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1631
1685
|
width: 100%;
|
|
1632
1686
|
box-sizing: border-box;
|
|
1633
1687
|
`;
|
|
1634
|
-
numberInput.placeholder = element.placeholder
|
|
1688
|
+
numberInput.placeholder = element.placeholder ?? "0";
|
|
1635
1689
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
1636
1690
|
if (element.max !== void 0) numberInput.max = element.max.toString();
|
|
1637
|
-
|
|
1691
|
+
numberInput.step = numberStepAttr(element);
|
|
1692
|
+
applyDecimalsMarker(numberInput, element);
|
|
1638
1693
|
numberInput.value = value.toString();
|
|
1639
1694
|
numberInput.readOnly = readonly;
|
|
1640
1695
|
if (!readonly && ctx.instance) {
|
|
@@ -1698,8 +1753,8 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1698
1753
|
const handle = createAddItemRow(
|
|
1699
1754
|
"number",
|
|
1700
1755
|
() => {
|
|
1701
|
-
values.push(element.default
|
|
1702
|
-
addNumberItem(element.default
|
|
1756
|
+
values.push(element.default ?? "");
|
|
1757
|
+
addNumberItem(element.default ?? "");
|
|
1703
1758
|
updateAddButton();
|
|
1704
1759
|
updateRemoveButtons();
|
|
1705
1760
|
ctx.instance?.triggerOnChange(pathKey);
|
|
@@ -1720,54 +1775,22 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1720
1775
|
function validateNumberElement(element, key, context) {
|
|
1721
1776
|
const errors = [];
|
|
1722
1777
|
const { scopeRoot, skipValidation } = context;
|
|
1723
|
-
const markValidity = (input, errorMessage) => {
|
|
1724
|
-
if (!input) return;
|
|
1725
|
-
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
1726
|
-
let errorElement = document.getElementById(errorId);
|
|
1727
|
-
if (errorMessage) {
|
|
1728
|
-
input.classList.add("invalid");
|
|
1729
|
-
input.title = errorMessage;
|
|
1730
|
-
if (!errorElement) {
|
|
1731
|
-
errorElement = document.createElement("div");
|
|
1732
|
-
errorElement.id = errorId;
|
|
1733
|
-
errorElement.className = "error-message";
|
|
1734
|
-
errorElement.style.cssText = `
|
|
1735
|
-
color: var(--fb-error-color);
|
|
1736
|
-
font-size: var(--fb-font-size-small);
|
|
1737
|
-
margin-top: 0.25rem;
|
|
1738
|
-
`;
|
|
1739
|
-
if (input.nextSibling) {
|
|
1740
|
-
input.parentNode?.insertBefore(errorElement, input.nextSibling);
|
|
1741
|
-
} else {
|
|
1742
|
-
input.parentNode?.appendChild(errorElement);
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
errorElement.textContent = errorMessage;
|
|
1746
|
-
errorElement.style.display = "block";
|
|
1747
|
-
} else {
|
|
1748
|
-
input.classList.remove("invalid");
|
|
1749
|
-
input.title = "";
|
|
1750
|
-
if (errorElement) {
|
|
1751
|
-
errorElement.remove();
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
};
|
|
1755
1778
|
const validateNumberInput = (input, v, fieldKey) => {
|
|
1756
1779
|
let hasError = false;
|
|
1757
1780
|
const { state } = context;
|
|
1758
1781
|
if (!skipValidation && element.min !== void 0 && element.min !== null && v < element.min) {
|
|
1759
1782
|
const msg = t("minValue", state, { min: element.min });
|
|
1760
1783
|
errors.push(`${fieldKey}: ${msg}`);
|
|
1761
|
-
|
|
1784
|
+
markFieldValidity(input, msg);
|
|
1762
1785
|
hasError = true;
|
|
1763
1786
|
} else if (!skipValidation && element.max !== void 0 && element.max !== null && v > element.max) {
|
|
1764
1787
|
const msg = t("maxValue", state, { max: element.max });
|
|
1765
1788
|
errors.push(`${fieldKey}: ${msg}`);
|
|
1766
|
-
|
|
1789
|
+
markFieldValidity(input, msg);
|
|
1767
1790
|
hasError = true;
|
|
1768
1791
|
}
|
|
1769
1792
|
if (!hasError) {
|
|
1770
|
-
|
|
1793
|
+
markFieldValidity(input, null);
|
|
1771
1794
|
}
|
|
1772
1795
|
};
|
|
1773
1796
|
if (element.multiple) {
|
|
@@ -1779,14 +1802,14 @@ function validateNumberElement(element, key, context) {
|
|
|
1779
1802
|
const raw = input?.value ?? "";
|
|
1780
1803
|
if (raw === "") {
|
|
1781
1804
|
values.push(null);
|
|
1782
|
-
|
|
1805
|
+
markFieldValidity(input, null);
|
|
1783
1806
|
return;
|
|
1784
1807
|
}
|
|
1785
1808
|
const v = parseFloat(raw);
|
|
1786
1809
|
if (!skipValidation && !Number.isFinite(v)) {
|
|
1787
1810
|
const msg = t("notANumber", context.state);
|
|
1788
1811
|
errors.push(`${key}[${index}]: ${msg}`);
|
|
1789
|
-
|
|
1812
|
+
markFieldValidity(input, msg);
|
|
1790
1813
|
values.push(null);
|
|
1791
1814
|
return;
|
|
1792
1815
|
}
|
|
@@ -1816,18 +1839,18 @@ function validateNumberElement(element, key, context) {
|
|
|
1816
1839
|
if (!skipValidation && element.required && raw === "") {
|
|
1817
1840
|
const msg = t("required", state);
|
|
1818
1841
|
errors.push(`${key}: ${msg}`);
|
|
1819
|
-
|
|
1842
|
+
markFieldValidity(input, msg);
|
|
1820
1843
|
return { value: null, errors };
|
|
1821
1844
|
}
|
|
1822
1845
|
if (raw === "") {
|
|
1823
|
-
|
|
1846
|
+
markFieldValidity(input, null);
|
|
1824
1847
|
return { value: null, errors };
|
|
1825
1848
|
}
|
|
1826
1849
|
const v = parseFloat(raw);
|
|
1827
1850
|
if (!skipValidation && !Number.isFinite(v)) {
|
|
1828
1851
|
const msg = t("notANumber", state);
|
|
1829
1852
|
errors.push(`${key}: ${msg}`);
|
|
1830
|
-
|
|
1853
|
+
markFieldValidity(input, msg);
|
|
1831
1854
|
return { value: null, errors };
|
|
1832
1855
|
}
|
|
1833
1856
|
validateNumberInput(input, v, key);
|
|
@@ -1877,6 +1900,38 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1877
1900
|
}
|
|
1878
1901
|
|
|
1879
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
|
+
}
|
|
1880
1935
|
function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
1881
1936
|
const state = ctx.state;
|
|
1882
1937
|
const readonly = isElementReadonly(element, state, ctx);
|
|
@@ -1889,18 +1944,18 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1889
1944
|
`;
|
|
1890
1945
|
selectInput.name = pathKey;
|
|
1891
1946
|
selectInput.disabled = readonly;
|
|
1892
|
-
(
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
}
|
|
1899
|
-
selectInput.appendChild(optionEl);
|
|
1900
|
-
});
|
|
1947
|
+
appendSelectOptions(
|
|
1948
|
+
selectInput,
|
|
1949
|
+
element,
|
|
1950
|
+
ctx.prefill[element.key] ?? element.default,
|
|
1951
|
+
state
|
|
1952
|
+
);
|
|
1901
1953
|
if (!readonly && ctx.instance) {
|
|
1902
1954
|
const handleChange = () => {
|
|
1903
|
-
ctx.instance.triggerOnChange(
|
|
1955
|
+
ctx.instance.triggerOnChange(
|
|
1956
|
+
pathKey,
|
|
1957
|
+
selectInput.value === "" ? null : selectInput.value
|
|
1958
|
+
);
|
|
1904
1959
|
};
|
|
1905
1960
|
selectInput.addEventListener("change", handleChange);
|
|
1906
1961
|
}
|
|
@@ -1920,7 +1975,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1920
1975
|
const minCount = element.minCount ?? (element.required ? 1 : 0);
|
|
1921
1976
|
const maxCount = element.maxCount ?? Infinity;
|
|
1922
1977
|
while (values.length < minCount) {
|
|
1923
|
-
values.push(element.default
|
|
1978
|
+
values.push(element.default ?? "");
|
|
1924
1979
|
}
|
|
1925
1980
|
const container = document.createElement("div");
|
|
1926
1981
|
container.className = "fb-row";
|
|
@@ -1945,15 +2000,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1945
2000
|
font-family: var(--fb-font-family);
|
|
1946
2001
|
`;
|
|
1947
2002
|
selectInput.disabled = readonly;
|
|
1948
|
-
(element
|
|
1949
|
-
const optionElement = document.createElement("option");
|
|
1950
|
-
optionElement.value = option.value;
|
|
1951
|
-
optionElement.textContent = option.label;
|
|
1952
|
-
if (value === option.value) {
|
|
1953
|
-
optionElement.selected = true;
|
|
1954
|
-
}
|
|
1955
|
-
selectInput.appendChild(optionElement);
|
|
1956
|
-
});
|
|
2003
|
+
appendSelectOptions(selectInput, element, value, state);
|
|
1957
2004
|
if (!readonly && ctx.instance) {
|
|
1958
2005
|
const handleChange = () => {
|
|
1959
2006
|
ctx.instance.triggerOnChange(selectInput.name, selectInput.value);
|
|
@@ -2006,7 +2053,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
2006
2053
|
const handle = createAddItemRow(
|
|
2007
2054
|
"select",
|
|
2008
2055
|
() => {
|
|
2009
|
-
const defaultValue = element.default
|
|
2056
|
+
const defaultValue = element.default ?? "";
|
|
2010
2057
|
values.push(defaultValue);
|
|
2011
2058
|
addSelectItem(defaultValue);
|
|
2012
2059
|
updateAddButton();
|
|
@@ -2035,38 +2082,6 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
2035
2082
|
function validateSelectElement(element, key, context) {
|
|
2036
2083
|
const errors = [];
|
|
2037
2084
|
const { scopeRoot, skipValidation } = context;
|
|
2038
|
-
const markValidity = (input, errorMessage) => {
|
|
2039
|
-
if (!input) return;
|
|
2040
|
-
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
2041
|
-
let errorElement = document.getElementById(errorId);
|
|
2042
|
-
if (errorMessage) {
|
|
2043
|
-
input.classList.add("invalid");
|
|
2044
|
-
input.title = errorMessage;
|
|
2045
|
-
if (!errorElement) {
|
|
2046
|
-
errorElement = document.createElement("div");
|
|
2047
|
-
errorElement.id = errorId;
|
|
2048
|
-
errorElement.className = "error-message";
|
|
2049
|
-
errorElement.style.cssText = `
|
|
2050
|
-
color: var(--fb-error-color);
|
|
2051
|
-
font-size: var(--fb-font-size-small);
|
|
2052
|
-
margin-top: 0.25rem;
|
|
2053
|
-
`;
|
|
2054
|
-
if (input.nextSibling) {
|
|
2055
|
-
input.parentNode?.insertBefore(errorElement, input.nextSibling);
|
|
2056
|
-
} else {
|
|
2057
|
-
input.parentNode?.appendChild(errorElement);
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
errorElement.textContent = errorMessage;
|
|
2061
|
-
errorElement.style.display = "block";
|
|
2062
|
-
} else {
|
|
2063
|
-
input.classList.remove("invalid");
|
|
2064
|
-
input.title = "";
|
|
2065
|
-
if (errorElement) {
|
|
2066
|
-
errorElement.remove();
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
};
|
|
2070
2085
|
const validateMultipleCount = (key2, values, element2, filterFn) => {
|
|
2071
2086
|
if (skipValidation) return;
|
|
2072
2087
|
const { state } = context;
|
|
@@ -2090,27 +2105,36 @@ function validateSelectElement(element, key, context) {
|
|
|
2090
2105
|
const values = [];
|
|
2091
2106
|
inputs.forEach((input) => {
|
|
2092
2107
|
const val = input?.value ?? "";
|
|
2093
|
-
values.push(val);
|
|
2094
|
-
|
|
2108
|
+
values.push(val === "" ? null : val);
|
|
2109
|
+
markFieldValidity(input, null);
|
|
2095
2110
|
});
|
|
2096
|
-
validateMultipleCount(key, values, element, (v) => v
|
|
2111
|
+
validateMultipleCount(key, values, element, (v) => v != null);
|
|
2097
2112
|
return { value: values, errors };
|
|
2098
2113
|
} else {
|
|
2099
|
-
const input = scopeRoot.querySelector(
|
|
2100
|
-
`[name="${key}"]`
|
|
2101
|
-
);
|
|
2114
|
+
const input = scopeRoot.querySelector(`[name="${key}"]`);
|
|
2102
2115
|
const val = input?.value ?? "";
|
|
2103
2116
|
if (!skipValidation && element.required && val === "") {
|
|
2104
2117
|
const msg = t("required", context.state);
|
|
2105
2118
|
errors.push(`${key}: ${msg}`);
|
|
2106
|
-
|
|
2119
|
+
markFieldValidity(input, msg);
|
|
2107
2120
|
return { value: null, errors };
|
|
2108
2121
|
} else {
|
|
2109
|
-
|
|
2122
|
+
markFieldValidity(input, null);
|
|
2110
2123
|
}
|
|
2111
2124
|
return { value: val === "" ? null : val, errors };
|
|
2112
2125
|
}
|
|
2113
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
|
+
}
|
|
2114
2138
|
function updateSelectField(element, fieldPath, value, context) {
|
|
2115
2139
|
const { scopeRoot } = context;
|
|
2116
2140
|
if ("multiple" in element && element.multiple) {
|
|
@@ -2125,10 +2149,17 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
2125
2149
|
);
|
|
2126
2150
|
selects.forEach((select, index) => {
|
|
2127
2151
|
if (index < value.length) {
|
|
2128
|
-
|
|
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;
|
|
2129
2160
|
const options = select.querySelectorAll("option");
|
|
2130
2161
|
options.forEach((option) => {
|
|
2131
|
-
option.selected = option.value ===
|
|
2162
|
+
option.selected = option.value === strValue;
|
|
2132
2163
|
});
|
|
2133
2164
|
select.classList.remove("invalid");
|
|
2134
2165
|
select.title = "";
|
|
@@ -2145,10 +2176,12 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
2145
2176
|
`[name="${fieldPath}"]`
|
|
2146
2177
|
);
|
|
2147
2178
|
if (select) {
|
|
2148
|
-
|
|
2179
|
+
const strValue = value != null ? String(value) : "";
|
|
2180
|
+
assertValueInOptions(select, strValue, fieldPath);
|
|
2181
|
+
select.value = strValue;
|
|
2149
2182
|
const options = select.querySelectorAll("option");
|
|
2150
2183
|
options.forEach((option) => {
|
|
2151
|
-
option.selected = option.value ===
|
|
2184
|
+
option.selected = option.value === strValue;
|
|
2152
2185
|
});
|
|
2153
2186
|
select.classList.remove("invalid");
|
|
2154
2187
|
select.title = "";
|
|
@@ -2351,7 +2384,7 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
|
2351
2384
|
const minCount = element.minCount ?? (element.required ? 1 : 0);
|
|
2352
2385
|
const maxCount = element.maxCount ?? Infinity;
|
|
2353
2386
|
while (values.length < minCount) {
|
|
2354
|
-
values.push(element.default
|
|
2387
|
+
values.push(element.default ?? "");
|
|
2355
2388
|
}
|
|
2356
2389
|
const readonly = isElementReadonly(element, state, ctx);
|
|
2357
2390
|
const container = document.createElement("div");
|
|
@@ -2445,7 +2478,7 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
|
2445
2478
|
const handle = createAddItemRow(
|
|
2446
2479
|
"switcher",
|
|
2447
2480
|
() => {
|
|
2448
|
-
const defaultValue = element.default
|
|
2481
|
+
const defaultValue = element.default ?? "";
|
|
2449
2482
|
values.push(defaultValue);
|
|
2450
2483
|
addSwitcherItem(defaultValue);
|
|
2451
2484
|
updateAddButton();
|
|
@@ -2474,38 +2507,6 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
|
2474
2507
|
function validateSwitcherElement(element, key, context) {
|
|
2475
2508
|
const errors = [];
|
|
2476
2509
|
const { scopeRoot, skipValidation } = context;
|
|
2477
|
-
const markValidity = (input, errorMessage) => {
|
|
2478
|
-
if (!input) return;
|
|
2479
|
-
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
2480
|
-
let errorElement = document.getElementById(errorId);
|
|
2481
|
-
if (errorMessage) {
|
|
2482
|
-
input.classList.add("invalid");
|
|
2483
|
-
input.title = errorMessage;
|
|
2484
|
-
if (!errorElement) {
|
|
2485
|
-
errorElement = document.createElement("div");
|
|
2486
|
-
errorElement.id = errorId;
|
|
2487
|
-
errorElement.className = "error-message";
|
|
2488
|
-
errorElement.style.cssText = `
|
|
2489
|
-
color: var(--fb-error-color);
|
|
2490
|
-
font-size: var(--fb-font-size-small);
|
|
2491
|
-
margin-top: 0.25rem;
|
|
2492
|
-
`;
|
|
2493
|
-
if (input.nextSibling) {
|
|
2494
|
-
input.parentNode?.insertBefore(errorElement, input.nextSibling);
|
|
2495
|
-
} else {
|
|
2496
|
-
input.parentNode?.appendChild(errorElement);
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
errorElement.textContent = errorMessage;
|
|
2500
|
-
errorElement.style.display = "block";
|
|
2501
|
-
} else {
|
|
2502
|
-
input.classList.remove("invalid");
|
|
2503
|
-
input.title = "";
|
|
2504
|
-
if (errorElement) {
|
|
2505
|
-
errorElement.remove();
|
|
2506
|
-
}
|
|
2507
|
-
}
|
|
2508
|
-
};
|
|
2509
2510
|
const validateMultipleCount = (fieldKey, values, el, filterFn) => {
|
|
2510
2511
|
if (skipValidation) return;
|
|
2511
2512
|
const { state } = context;
|
|
@@ -2532,16 +2533,16 @@ function validateSwitcherElement(element, key, context) {
|
|
|
2532
2533
|
const values = [];
|
|
2533
2534
|
inputs.forEach((input) => {
|
|
2534
2535
|
const val = input?.value ?? "";
|
|
2535
|
-
values.push(val);
|
|
2536
|
+
values.push(val === "" ? null : val);
|
|
2536
2537
|
if (!skipValidation && val !== "" && !validOptionValues.has(val)) {
|
|
2537
2538
|
const msg = t("invalidOption", context.state);
|
|
2538
|
-
|
|
2539
|
+
markFieldValidity(input, msg);
|
|
2539
2540
|
errors.push(`${key}: ${msg}`);
|
|
2540
2541
|
} else {
|
|
2541
|
-
|
|
2542
|
+
markFieldValidity(input, null);
|
|
2542
2543
|
}
|
|
2543
2544
|
});
|
|
2544
|
-
validateMultipleCount(key, values, element, (v) => v
|
|
2545
|
+
validateMultipleCount(key, values, element, (v) => v != null);
|
|
2545
2546
|
return { value: values, errors };
|
|
2546
2547
|
} else {
|
|
2547
2548
|
const input = scopeRoot.querySelector(
|
|
@@ -2551,16 +2552,16 @@ function validateSwitcherElement(element, key, context) {
|
|
|
2551
2552
|
if (!skipValidation && element.required && val === "") {
|
|
2552
2553
|
const msg = t("required", context.state);
|
|
2553
2554
|
errors.push(`${key}: ${msg}`);
|
|
2554
|
-
|
|
2555
|
+
markFieldValidity(input, msg);
|
|
2555
2556
|
return { value: null, errors };
|
|
2556
2557
|
}
|
|
2557
2558
|
if (!skipValidation && val !== "" && !validOptionValues.has(val)) {
|
|
2558
2559
|
const msg = t("invalidOption", context.state);
|
|
2559
2560
|
errors.push(`${key}: ${msg}`);
|
|
2560
|
-
|
|
2561
|
+
markFieldValidity(input, msg);
|
|
2561
2562
|
return { value: null, errors };
|
|
2562
2563
|
}
|
|
2563
|
-
|
|
2564
|
+
markFieldValidity(input, null);
|
|
2564
2565
|
return { value: val === "" ? null : val, errors };
|
|
2565
2566
|
}
|
|
2566
2567
|
}
|
|
@@ -2732,6 +2733,7 @@ function renderBooleanElement(element, ctx, wrapper, pathKey) {
|
|
|
2732
2733
|
const hiddenInput = document.createElement("input");
|
|
2733
2734
|
hiddenInput.type = "hidden";
|
|
2734
2735
|
hiddenInput.name = pathKey;
|
|
2736
|
+
hiddenInput.setAttribute("data-boolean-field", "true");
|
|
2735
2737
|
hiddenInput.value = initial ? "true" : "false";
|
|
2736
2738
|
const row = document.createElement("div");
|
|
2737
2739
|
row.className = "fb-toggle-row";
|
|
@@ -5807,7 +5809,7 @@ function createReadonlyColourUI(value) {
|
|
|
5807
5809
|
container.appendChild(hexText);
|
|
5808
5810
|
return container;
|
|
5809
5811
|
}
|
|
5810
|
-
function createEditColourUI(value, pathKey, ctx) {
|
|
5812
|
+
function createEditColourUI(value, pathKey, ctx, placeholder) {
|
|
5811
5813
|
const normalizedValue = normalizeColourValue(value);
|
|
5812
5814
|
const pickerWrapper = document.createElement("div");
|
|
5813
5815
|
pickerWrapper.className = "colour-picker-wrapper";
|
|
@@ -5831,9 +5833,10 @@ function createEditColourUI(value, pathKey, ctx) {
|
|
|
5831
5833
|
const hexInput = document.createElement("input");
|
|
5832
5834
|
hexInput.type = "text";
|
|
5833
5835
|
hexInput.className = "colour-hex-input";
|
|
5836
|
+
hexInput.setAttribute("data-colour-field", "true");
|
|
5834
5837
|
hexInput.name = pathKey;
|
|
5835
5838
|
hexInput.value = normalizedValue;
|
|
5836
|
-
hexInput.placeholder = "#000000";
|
|
5839
|
+
hexInput.placeholder = placeholder ?? "#000000";
|
|
5837
5840
|
hexInput.style.cssText = `
|
|
5838
5841
|
width: 100px;
|
|
5839
5842
|
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
@@ -5922,12 +5925,17 @@ function createEditColourUI(value, pathKey, ctx) {
|
|
|
5922
5925
|
function renderColourElement(element, ctx, wrapper, pathKey) {
|
|
5923
5926
|
const state = ctx.state;
|
|
5924
5927
|
const readonly = isElementReadonly(element, state, ctx);
|
|
5925
|
-
const initialValue = ctx.prefill[element.key]
|
|
5928
|
+
const initialValue = ctx.prefill[element.key] ?? element.default ?? "#000000";
|
|
5926
5929
|
if (readonly) {
|
|
5927
5930
|
const readonlyUI = createReadonlyColourUI(initialValue);
|
|
5928
5931
|
wrapper.appendChild(readonlyUI);
|
|
5929
5932
|
} else {
|
|
5930
|
-
const editUI = createEditColourUI(
|
|
5933
|
+
const editUI = createEditColourUI(
|
|
5934
|
+
initialValue,
|
|
5935
|
+
pathKey,
|
|
5936
|
+
ctx,
|
|
5937
|
+
element.placeholder
|
|
5938
|
+
);
|
|
5931
5939
|
wrapper.appendChild(editUI);
|
|
5932
5940
|
}
|
|
5933
5941
|
if (!readonly) {
|
|
@@ -5949,7 +5957,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
5949
5957
|
const minCount = element.minCount ?? (element.required ? 1 : 0);
|
|
5950
5958
|
const maxCount = element.maxCount ?? Infinity;
|
|
5951
5959
|
while (values.length < minCount) {
|
|
5952
|
-
values.push(element.default
|
|
5960
|
+
values.push(element.default ?? "#000000");
|
|
5953
5961
|
}
|
|
5954
5962
|
const container = document.createElement("div");
|
|
5955
5963
|
container.className = "fb-row";
|
|
@@ -5973,7 +5981,12 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
5973
5981
|
}
|
|
5974
5982
|
} else {
|
|
5975
5983
|
const tempPathKey = `${pathKey}[${container.children.length}]`;
|
|
5976
|
-
const editUI = createEditColourUI(
|
|
5984
|
+
const editUI = createEditColourUI(
|
|
5985
|
+
value,
|
|
5986
|
+
tempPathKey,
|
|
5987
|
+
ctx,
|
|
5988
|
+
element.placeholder
|
|
5989
|
+
);
|
|
5977
5990
|
editUI.style.flex = "1";
|
|
5978
5991
|
itemWrapper.appendChild(editUI);
|
|
5979
5992
|
}
|
|
@@ -6035,7 +6048,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
6035
6048
|
const handle = createAddItemRow(
|
|
6036
6049
|
"colour",
|
|
6037
6050
|
() => {
|
|
6038
|
-
const defaultColour = element.default
|
|
6051
|
+
const defaultColour = element.default ?? "#000000";
|
|
6039
6052
|
values.push(defaultColour);
|
|
6040
6053
|
addColourItem(defaultColour);
|
|
6041
6054
|
updateAddButton();
|
|
@@ -6068,58 +6081,26 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
6068
6081
|
function validateColourElement(element, key, context) {
|
|
6069
6082
|
const errors = [];
|
|
6070
6083
|
const { scopeRoot, skipValidation } = context;
|
|
6071
|
-
const markValidity = (input, errorMessage) => {
|
|
6072
|
-
if (!input) return;
|
|
6073
|
-
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
6074
|
-
let errorElement = document.getElementById(errorId);
|
|
6075
|
-
if (errorMessage) {
|
|
6076
|
-
input.classList.add("invalid");
|
|
6077
|
-
input.title = errorMessage;
|
|
6078
|
-
if (!errorElement) {
|
|
6079
|
-
errorElement = document.createElement("div");
|
|
6080
|
-
errorElement.id = errorId;
|
|
6081
|
-
errorElement.className = "error-message";
|
|
6082
|
-
errorElement.style.cssText = `
|
|
6083
|
-
color: var(--fb-error-color);
|
|
6084
|
-
font-size: var(--fb-font-size-small);
|
|
6085
|
-
margin-top: 0.25rem;
|
|
6086
|
-
`;
|
|
6087
|
-
if (input.nextSibling) {
|
|
6088
|
-
input.parentNode?.insertBefore(errorElement, input.nextSibling);
|
|
6089
|
-
} else {
|
|
6090
|
-
input.parentNode?.appendChild(errorElement);
|
|
6091
|
-
}
|
|
6092
|
-
}
|
|
6093
|
-
errorElement.textContent = errorMessage;
|
|
6094
|
-
errorElement.style.display = "block";
|
|
6095
|
-
} else {
|
|
6096
|
-
input.classList.remove("invalid");
|
|
6097
|
-
input.title = "";
|
|
6098
|
-
if (errorElement) {
|
|
6099
|
-
errorElement.remove();
|
|
6100
|
-
}
|
|
6101
|
-
}
|
|
6102
|
-
};
|
|
6103
6084
|
const validateColourValue = (input, val, fieldKey) => {
|
|
6104
6085
|
const { state } = context;
|
|
6105
6086
|
if (!val) {
|
|
6106
6087
|
if (!skipValidation && element.required) {
|
|
6107
6088
|
const msg = t("required", state);
|
|
6108
6089
|
errors.push(`${fieldKey}: ${msg}`);
|
|
6109
|
-
|
|
6090
|
+
markFieldValidity(input, msg);
|
|
6110
6091
|
return "";
|
|
6111
6092
|
}
|
|
6112
|
-
|
|
6093
|
+
markFieldValidity(input, null);
|
|
6113
6094
|
return "";
|
|
6114
6095
|
}
|
|
6115
6096
|
const normalized = normalizeColourValue(val);
|
|
6116
6097
|
if (!skipValidation && !isValidHexColour(normalized)) {
|
|
6117
6098
|
const msg = t("invalidHexColour", state);
|
|
6118
6099
|
errors.push(`${fieldKey}: ${msg}`);
|
|
6119
|
-
|
|
6100
|
+
markFieldValidity(input, msg);
|
|
6120
6101
|
return val;
|
|
6121
6102
|
}
|
|
6122
|
-
|
|
6103
|
+
markFieldValidity(input, null);
|
|
6123
6104
|
return normalized;
|
|
6124
6105
|
};
|
|
6125
6106
|
if (element.multiple) {
|
|
@@ -6156,7 +6137,7 @@ function validateColourElement(element, key, context) {
|
|
|
6156
6137
|
if (!skipValidation && element.required && val === "") {
|
|
6157
6138
|
const msg = t("required", context.state);
|
|
6158
6139
|
errors.push(`${key}: ${msg}`);
|
|
6159
|
-
|
|
6140
|
+
markFieldValidity(hexInput, msg);
|
|
6160
6141
|
return { value: "", errors };
|
|
6161
6142
|
}
|
|
6162
6143
|
const validated = validateColourValue(hexInput, val, key);
|
|
@@ -6539,42 +6520,6 @@ function validateSliderElement(element, key, context) {
|
|
|
6539
6520
|
const max = element.max;
|
|
6540
6521
|
const step = element.step ?? 1;
|
|
6541
6522
|
const scale = element.scale || "linear";
|
|
6542
|
-
const markValidity = (input, errorMessage) => {
|
|
6543
|
-
if (!input) return;
|
|
6544
|
-
const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
|
|
6545
|
-
let errorElement = document.getElementById(errorId);
|
|
6546
|
-
if (errorMessage) {
|
|
6547
|
-
input.classList.add("invalid");
|
|
6548
|
-
input.title = errorMessage;
|
|
6549
|
-
if (!errorElement) {
|
|
6550
|
-
errorElement = document.createElement("div");
|
|
6551
|
-
errorElement.id = errorId;
|
|
6552
|
-
errorElement.className = "error-message";
|
|
6553
|
-
errorElement.style.cssText = `
|
|
6554
|
-
color: var(--fb-error-color);
|
|
6555
|
-
font-size: var(--fb-font-size-small);
|
|
6556
|
-
margin-top: 0.25rem;
|
|
6557
|
-
`;
|
|
6558
|
-
const sliderContainer = input.closest(".slider-container");
|
|
6559
|
-
if (sliderContainer && sliderContainer.nextSibling) {
|
|
6560
|
-
sliderContainer.parentNode?.insertBefore(
|
|
6561
|
-
errorElement,
|
|
6562
|
-
sliderContainer.nextSibling
|
|
6563
|
-
);
|
|
6564
|
-
} else if (sliderContainer) {
|
|
6565
|
-
sliderContainer.parentNode?.appendChild(errorElement);
|
|
6566
|
-
}
|
|
6567
|
-
}
|
|
6568
|
-
errorElement.textContent = errorMessage;
|
|
6569
|
-
errorElement.style.display = "block";
|
|
6570
|
-
} else {
|
|
6571
|
-
input.classList.remove("invalid");
|
|
6572
|
-
input.title = "";
|
|
6573
|
-
if (errorElement) {
|
|
6574
|
-
errorElement.remove();
|
|
6575
|
-
}
|
|
6576
|
-
}
|
|
6577
|
-
};
|
|
6578
6523
|
const validateSliderValue = (slider, fieldKey) => {
|
|
6579
6524
|
const { state } = context;
|
|
6580
6525
|
const rawValue = slider.value;
|
|
@@ -6582,10 +6527,10 @@ function validateSliderElement(element, key, context) {
|
|
|
6582
6527
|
if (!skipValidation && element.required) {
|
|
6583
6528
|
const msg = t("required", state);
|
|
6584
6529
|
errors.push(`${fieldKey}: ${msg}`);
|
|
6585
|
-
|
|
6530
|
+
markFieldValidity(slider, msg);
|
|
6586
6531
|
return null;
|
|
6587
6532
|
}
|
|
6588
|
-
|
|
6533
|
+
markFieldValidity(slider, null);
|
|
6589
6534
|
return null;
|
|
6590
6535
|
}
|
|
6591
6536
|
let value;
|
|
@@ -6601,17 +6546,17 @@ function validateSliderElement(element, key, context) {
|
|
|
6601
6546
|
if (value < min) {
|
|
6602
6547
|
const msg = t("minValue", state, { min });
|
|
6603
6548
|
errors.push(`${fieldKey}: ${msg}`);
|
|
6604
|
-
|
|
6549
|
+
markFieldValidity(slider, msg);
|
|
6605
6550
|
return value;
|
|
6606
6551
|
}
|
|
6607
6552
|
if (value > max) {
|
|
6608
6553
|
const msg = t("maxValue", state, { max });
|
|
6609
6554
|
errors.push(`${fieldKey}: ${msg}`);
|
|
6610
|
-
|
|
6555
|
+
markFieldValidity(slider, msg);
|
|
6611
6556
|
return value;
|
|
6612
6557
|
}
|
|
6613
6558
|
}
|
|
6614
|
-
|
|
6559
|
+
markFieldValidity(slider, null);
|
|
6615
6560
|
return value;
|
|
6616
6561
|
};
|
|
6617
6562
|
if (element.multiple) {
|
|
@@ -6756,22 +6701,12 @@ function extractRootFormData(formRoot) {
|
|
|
6756
6701
|
inputs.forEach((input) => {
|
|
6757
6702
|
const fieldName = input.getAttribute("name");
|
|
6758
6703
|
if (fieldName && !fieldName.includes("[") && !fieldName.includes(".")) {
|
|
6759
|
-
if (input instanceof
|
|
6760
|
-
|
|
6761
|
-
} else if (input instanceof HTMLInputElement) {
|
|
6762
|
-
if (input.type === "checkbox") {
|
|
6763
|
-
data[fieldName] = input.checked;
|
|
6764
|
-
} else if (input.type === "radio") {
|
|
6765
|
-
if (input.checked) {
|
|
6766
|
-
data[fieldName] = input.value;
|
|
6767
|
-
}
|
|
6768
|
-
} else if (input.dataset.hiddenField) {
|
|
6769
|
-
data[fieldName] = deserializeHiddenValue(input.value);
|
|
6770
|
-
} else {
|
|
6704
|
+
if (input instanceof HTMLInputElement && input.type === "radio") {
|
|
6705
|
+
if (input.checked) {
|
|
6771
6706
|
data[fieldName] = input.value;
|
|
6772
6707
|
}
|
|
6773
|
-
} else
|
|
6774
|
-
data[fieldName] = input
|
|
6708
|
+
} else {
|
|
6709
|
+
data[fieldName] = readTypedInputValue(input);
|
|
6775
6710
|
}
|
|
6776
6711
|
}
|
|
6777
6712
|
});
|
|
@@ -6840,7 +6775,7 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
6840
6775
|
};
|
|
6841
6776
|
element.elements.forEach((child) => {
|
|
6842
6777
|
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
6843
|
-
const prefillVal = containerPrefill[child.key]
|
|
6778
|
+
const prefillVal = child.key in containerPrefill ? containerPrefill[child.key] : ("default" in child ? child.default : null) ?? null;
|
|
6844
6779
|
itemsWrap.appendChild(
|
|
6845
6780
|
createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
|
|
6846
6781
|
);
|
|
@@ -6941,7 +6876,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
6941
6876
|
);
|
|
6942
6877
|
element.elements.forEach((child) => {
|
|
6943
6878
|
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
6944
|
-
const hiddenValue = rowPrefill
|
|
6879
|
+
const hiddenValue = rowPrefill && child.key in rowPrefill ? rowPrefill[child.key] : ("default" in child ? child.default : null) ?? null;
|
|
6945
6880
|
childWrapper.appendChild(
|
|
6946
6881
|
createHiddenInput(pathJoin(subCtx.path, child.key), hiddenValue)
|
|
6947
6882
|
);
|
|
@@ -7050,19 +6985,16 @@ function renderMultipleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
7050
6985
|
}
|
|
7051
6986
|
}
|
|
7052
6987
|
}
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
validateElementFunc = fn;
|
|
7056
|
-
}
|
|
7057
|
-
function validateElement(element, ctx, customScopeRoot) {
|
|
7058
|
-
if (!validateElementFunc) {
|
|
6988
|
+
function requireValidateElement(context) {
|
|
6989
|
+
if (!context.validateElement) {
|
|
7059
6990
|
throw new Error(
|
|
7060
|
-
"validateElement
|
|
6991
|
+
"validateContainerElement: context.validateElement missing \u2014 container validation requires the instance validator"
|
|
7061
6992
|
);
|
|
7062
6993
|
}
|
|
7063
|
-
return
|
|
6994
|
+
return context.validateElement;
|
|
7064
6995
|
}
|
|
7065
6996
|
function validateContainerElement(element, key, context) {
|
|
6997
|
+
const validateChild = requireValidateElement(context);
|
|
7066
6998
|
const errors = [];
|
|
7067
6999
|
const { scopeRoot, skipValidation, path } = context;
|
|
7068
7000
|
if (!("elements" in element)) {
|
|
@@ -7113,7 +7045,7 @@ function validateContainerElement(element, key, context) {
|
|
|
7113
7045
|
}
|
|
7114
7046
|
}
|
|
7115
7047
|
const childKey = `${key}[${domIndex}].${child.key}`;
|
|
7116
|
-
const childResult =
|
|
7048
|
+
const childResult = validateChild(
|
|
7117
7049
|
{ ...child, key: childKey },
|
|
7118
7050
|
{ path },
|
|
7119
7051
|
itemContainer
|
|
@@ -7154,7 +7086,7 @@ function validateContainerElement(element, key, context) {
|
|
|
7154
7086
|
}
|
|
7155
7087
|
{
|
|
7156
7088
|
const childKey = `${key}.${child.key}`;
|
|
7157
|
-
const childResult =
|
|
7089
|
+
const childResult = validateChild(
|
|
7158
7090
|
{ ...child, key: childKey },
|
|
7159
7091
|
{ path },
|
|
7160
7092
|
containerContainer
|
|
@@ -7645,7 +7577,7 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
7645
7577
|
rebuild();
|
|
7646
7578
|
} catch (e) {
|
|
7647
7579
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
7648
|
-
console.error(t("tableImportError", state
|
|
7580
|
+
console.error(t("tableImportError", state, { error: errMsg }));
|
|
7649
7581
|
} finally {
|
|
7650
7582
|
overlay.remove();
|
|
7651
7583
|
}
|
|
@@ -8587,7 +8519,7 @@ function updateTableField(element, fieldPath, value, context) {
|
|
|
8587
8519
|
}
|
|
8588
8520
|
|
|
8589
8521
|
// src/components/richinput.ts
|
|
8590
|
-
function applyAutoExpand2(textarea, backdrop) {
|
|
8522
|
+
function applyAutoExpand2(textarea, backdrop, observers) {
|
|
8591
8523
|
textarea.style.overflow = "hidden";
|
|
8592
8524
|
textarea.style.resize = "none";
|
|
8593
8525
|
const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
|
|
@@ -8609,6 +8541,7 @@ function applyAutoExpand2(textarea, backdrop) {
|
|
|
8609
8541
|
const ro = new ResizeObserver((entries) => {
|
|
8610
8542
|
if (!textarea.isConnected) {
|
|
8611
8543
|
ro.disconnect();
|
|
8544
|
+
observers.delete(ro);
|
|
8612
8545
|
return;
|
|
8613
8546
|
}
|
|
8614
8547
|
const entry = entries[0];
|
|
@@ -8618,6 +8551,7 @@ function applyAutoExpand2(textarea, backdrop) {
|
|
|
8618
8551
|
resize();
|
|
8619
8552
|
});
|
|
8620
8553
|
ro.observe(textarea);
|
|
8554
|
+
observers.add(ro);
|
|
8621
8555
|
}
|
|
8622
8556
|
function buildFileLabels(files, state) {
|
|
8623
8557
|
const labels = /* @__PURE__ */ new Map();
|
|
@@ -9187,7 +9121,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
9187
9121
|
`;
|
|
9188
9122
|
const textarea = document.createElement("textarea");
|
|
9189
9123
|
textarea.name = `${pathKey}__text`;
|
|
9190
|
-
textarea.placeholder = element.placeholder
|
|
9124
|
+
textarea.placeholder = element.placeholder ?? t("richinputPlaceholder", state);
|
|
9191
9125
|
const rawInitialText = initialValue.text ?? "";
|
|
9192
9126
|
textarea.value = rawInitialText ? replaceRidsWithFilenames(rawInitialText, files, state) : "";
|
|
9193
9127
|
textarea.style.cssText = `
|
|
@@ -9204,7 +9138,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
9204
9138
|
z-index: 1;
|
|
9205
9139
|
caret-color: var(--fb-text-color, #111827);
|
|
9206
9140
|
`;
|
|
9207
|
-
applyAutoExpand2(textarea, backdrop);
|
|
9141
|
+
applyAutoExpand2(textarea, backdrop, ctx.state.autoExpandObservers);
|
|
9208
9142
|
textarea.addEventListener("scroll", () => {
|
|
9209
9143
|
backdrop.scrollTop = textarea.scrollTop;
|
|
9210
9144
|
});
|
|
@@ -10177,12 +10111,7 @@ function validateHiddenElement(element, key, context) {
|
|
|
10177
10111
|
const input = scopeRoot.querySelector(
|
|
10178
10112
|
`input[type="hidden"][data-hidden-field="true"][name="${key}"]`
|
|
10179
10113
|
);
|
|
10180
|
-
|
|
10181
|
-
if (raw === "") {
|
|
10182
|
-
const defaultVal = "default" in element ? element.default : null;
|
|
10183
|
-
return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
|
|
10184
|
-
}
|
|
10185
|
-
return { value: deserializeHiddenValue(raw), errors: [] };
|
|
10114
|
+
return { value: deserializeHiddenValue(input?.value ?? ""), errors: [] };
|
|
10186
10115
|
}
|
|
10187
10116
|
function updateHiddenField(_element, fieldPath, value, context) {
|
|
10188
10117
|
const { scopeRoot } = context;
|
|
@@ -10373,25 +10302,13 @@ function extractDOMValue(fieldPath, formRoot) {
|
|
|
10373
10302
|
if (!input) {
|
|
10374
10303
|
return void 0;
|
|
10375
10304
|
}
|
|
10376
|
-
if (input instanceof
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
} else if (input.type === "radio") {
|
|
10382
|
-
const checked = formRoot.querySelector(
|
|
10383
|
-
`[name="${fieldPath}"]:checked`
|
|
10384
|
-
);
|
|
10385
|
-
return checked ? checked.value : void 0;
|
|
10386
|
-
} else if (input.dataset.hiddenField) {
|
|
10387
|
-
return deserializeHiddenValue(input.value);
|
|
10388
|
-
} else {
|
|
10389
|
-
return input.value;
|
|
10390
|
-
}
|
|
10391
|
-
} else if (input instanceof HTMLTextAreaElement) {
|
|
10392
|
-
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;
|
|
10393
10310
|
}
|
|
10394
|
-
return
|
|
10311
|
+
return readTypedInputValue(input);
|
|
10395
10312
|
}
|
|
10396
10313
|
function buildScopedDataAtPath(path, value) {
|
|
10397
10314
|
const segments = path.match(/[^.[\]]+|\[\d+\]/g);
|
|
@@ -10520,11 +10437,19 @@ function createFieldLabel(element) {
|
|
|
10520
10437
|
}
|
|
10521
10438
|
return title;
|
|
10522
10439
|
}
|
|
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
|
+
}
|
|
10523
10447
|
function createInfoButton(element, state) {
|
|
10524
10448
|
const infoBtn = document.createElement("button");
|
|
10525
10449
|
infoBtn.type = "button";
|
|
10526
10450
|
infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
|
|
10527
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);
|
|
10528
10453
|
const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
|
|
10529
10454
|
const tooltip = document.createElement("div");
|
|
10530
10455
|
tooltip.id = tooltipId;
|
|
@@ -10705,6 +10630,7 @@ var defaultConfig = {
|
|
|
10705
10630
|
onDownloadError: null,
|
|
10706
10631
|
debounceMs: 300,
|
|
10707
10632
|
verboseErrors: false,
|
|
10633
|
+
postMessageTarget: null,
|
|
10708
10634
|
enableFilePreview: true,
|
|
10709
10635
|
maxPreviewSize: "200px",
|
|
10710
10636
|
readonly: false,
|
|
@@ -10726,6 +10652,7 @@ var defaultConfig = {
|
|
|
10726
10652
|
openInNewTab: "Open in new tab",
|
|
10727
10653
|
changeButton: "Change",
|
|
10728
10654
|
placeholderText: "Enter text",
|
|
10655
|
+
selectPlaceholder: "Select\u2026",
|
|
10729
10656
|
previewAlt: "Preview",
|
|
10730
10657
|
previewUnavailable: "Preview unavailable",
|
|
10731
10658
|
previewError: "Preview error",
|
|
@@ -10801,6 +10728,7 @@ var defaultConfig = {
|
|
|
10801
10728
|
openInNewTab: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0432 \u043D\u043E\u0432\u043E\u0439 \u0432\u043A\u043B\u0430\u0434\u043A\u0435",
|
|
10802
10729
|
changeButton: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C",
|
|
10803
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",
|
|
10804
10732
|
previewAlt: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",
|
|
10805
10733
|
previewUnavailable: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
|
|
10806
10734
|
previewError: "\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430",
|
|
@@ -10865,20 +10793,23 @@ var defaultConfig = {
|
|
|
10865
10793
|
},
|
|
10866
10794
|
theme: {}
|
|
10867
10795
|
};
|
|
10868
|
-
function
|
|
10869
|
-
const
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
config.translations
|
|
10875
|
-
)) {
|
|
10876
|
-
mergedTranslations[locale] = {
|
|
10877
|
-
...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] || {},
|
|
10878
10802
|
...userTranslations
|
|
10879
10803
|
};
|
|
10880
10804
|
}
|
|
10881
10805
|
}
|
|
10806
|
+
return merged;
|
|
10807
|
+
}
|
|
10808
|
+
function createInstanceState(config) {
|
|
10809
|
+
const mergedTranslations = mergeTranslations(
|
|
10810
|
+
defaultConfig.translations,
|
|
10811
|
+
config?.translations
|
|
10812
|
+
);
|
|
10882
10813
|
return {
|
|
10883
10814
|
schema: null,
|
|
10884
10815
|
formRoot: null,
|
|
@@ -10895,6 +10826,7 @@ function createInstanceState(config) {
|
|
|
10895
10826
|
syntheticElementIds: /* @__PURE__ */ new WeakMap(),
|
|
10896
10827
|
syntheticElementIdCounter: 0,
|
|
10897
10828
|
enableIfObservers: /* @__PURE__ */ new Set(),
|
|
10829
|
+
autoExpandObservers: /* @__PURE__ */ new Set(),
|
|
10898
10830
|
tooltipElements: /* @__PURE__ */ new Set()
|
|
10899
10831
|
};
|
|
10900
10832
|
}
|
|
@@ -11196,6 +11128,11 @@ function findOwnField(scope, lookupKey, ownBoundary) {
|
|
|
11196
11128
|
}
|
|
11197
11129
|
var FormBuilderInstance = class {
|
|
11198
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;
|
|
11199
11136
|
this.instanceId = generateInstanceId();
|
|
11200
11137
|
this.state = createInstanceState(config);
|
|
11201
11138
|
if (this.state.config.verboseErrors) {
|
|
@@ -11229,10 +11166,21 @@ var FormBuilderInstance = class {
|
|
|
11229
11166
|
this.state.formRoot = element;
|
|
11230
11167
|
}
|
|
11231
11168
|
/**
|
|
11232
|
-
* 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.
|
|
11233
11172
|
*/
|
|
11234
11173
|
configure(config) {
|
|
11235
|
-
|
|
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 });
|
|
11236
11184
|
}
|
|
11237
11185
|
/**
|
|
11238
11186
|
* Set file upload handler
|
|
@@ -11265,12 +11213,16 @@ var FormBuilderInstance = class {
|
|
|
11265
11213
|
this.state.config.readonly = mode === "readonly";
|
|
11266
11214
|
}
|
|
11267
11215
|
/**
|
|
11268
|
-
* Set locale
|
|
11216
|
+
* Set locale. Custom locales are allowed — their translations must have
|
|
11217
|
+
* been provided via the constructor or configure() first.
|
|
11269
11218
|
*/
|
|
11270
11219
|
setLocale(locale) {
|
|
11271
|
-
if (this.state.config.translations[locale]) {
|
|
11272
|
-
|
|
11220
|
+
if (!this.state.config.translations[locale]) {
|
|
11221
|
+
throw new Error(
|
|
11222
|
+
`setLocale: no translations configured for locale "${locale}"`
|
|
11223
|
+
);
|
|
11273
11224
|
}
|
|
11225
|
+
this.state.config.locale = locale;
|
|
11274
11226
|
}
|
|
11275
11227
|
/**
|
|
11276
11228
|
* Trigger onChange callbacks with debouncing
|
|
@@ -11623,11 +11575,13 @@ var FormBuilderInstance = class {
|
|
|
11623
11575
|
renderForm(root, schema, prefill, actions) {
|
|
11624
11576
|
const errors = validateSchema(schema);
|
|
11625
11577
|
if (errors.length > 0) {
|
|
11626
|
-
|
|
11627
|
-
|
|
11578
|
+
throw new Error(`renderForm: invalid schema:
|
|
11579
|
+
- ${errors.join("\n- ")}`);
|
|
11628
11580
|
}
|
|
11629
11581
|
this.disconnectEnableIfObservers();
|
|
11582
|
+
this.disconnectAutoExpandObservers();
|
|
11630
11583
|
this.removeTooltipElements();
|
|
11584
|
+
this.removePrefillHintListener();
|
|
11631
11585
|
this.state.formRoot = root;
|
|
11632
11586
|
this.state.schema = schema;
|
|
11633
11587
|
this.state.externalActions = actions || null;
|
|
@@ -11650,7 +11604,7 @@ var FormBuilderInstance = class {
|
|
|
11650
11604
|
}
|
|
11651
11605
|
schema.elements.forEach((element) => {
|
|
11652
11606
|
if (element.type !== "markdown" && (element.hidden || element.type === "hidden")) {
|
|
11653
|
-
const val = prefill
|
|
11607
|
+
const val = prefill && element.key in prefill ? prefill[element.key] : element.default ?? null;
|
|
11654
11608
|
fieldsWrapper.appendChild(createHiddenInput(element.key, val));
|
|
11655
11609
|
return;
|
|
11656
11610
|
}
|
|
@@ -11667,7 +11621,10 @@ var FormBuilderInstance = class {
|
|
|
11667
11621
|
rootContainer.appendChild(fieldsWrapper);
|
|
11668
11622
|
root.appendChild(rootContainer);
|
|
11669
11623
|
if (!this.state.config.readonly) {
|
|
11670
|
-
|
|
11624
|
+
this.prefillHintHandler = this.handlePrefillHintClick.bind(
|
|
11625
|
+
this
|
|
11626
|
+
);
|
|
11627
|
+
root.addEventListener("click", this.prefillHintHandler);
|
|
11671
11628
|
}
|
|
11672
11629
|
if (this.state.config.readonly && this.state.externalActions && Array.isArray(this.state.externalActions)) {
|
|
11673
11630
|
this.renderExternalActions();
|
|
@@ -11683,7 +11640,7 @@ var FormBuilderInstance = class {
|
|
|
11683
11640
|
return { valid: true, errors: [], data: {} };
|
|
11684
11641
|
const errors = [];
|
|
11685
11642
|
const data = {};
|
|
11686
|
-
const
|
|
11643
|
+
const validateElement = (element, ctx, customScopeRoot = null) => {
|
|
11687
11644
|
const key = element.key ?? "";
|
|
11688
11645
|
const scopeRoot = customScopeRoot || this.state.formRoot;
|
|
11689
11646
|
const componentContext = {
|
|
@@ -11691,7 +11648,10 @@ var FormBuilderInstance = class {
|
|
|
11691
11648
|
state: this.state,
|
|
11692
11649
|
instance: this,
|
|
11693
11650
|
path: ctx.path,
|
|
11694
|
-
skipValidation
|
|
11651
|
+
skipValidation,
|
|
11652
|
+
// Containers recurse into their children through this — threaded per
|
|
11653
|
+
// pass, never module state (see ComponentContext.validateElement).
|
|
11654
|
+
validateElement
|
|
11695
11655
|
};
|
|
11696
11656
|
const componentResult = validateElementWithComponent(
|
|
11697
11657
|
element,
|
|
@@ -11709,7 +11669,6 @@ var FormBuilderInstance = class {
|
|
|
11709
11669
|
console.warn(`Unknown field type "${element.type}" for key "${key}"`);
|
|
11710
11670
|
return { value: null, spread: false };
|
|
11711
11671
|
};
|
|
11712
|
-
setValidateElement(validateElement2);
|
|
11713
11672
|
this.state.schema.elements.forEach((element) => {
|
|
11714
11673
|
if (element.enableIf) {
|
|
11715
11674
|
try {
|
|
@@ -11727,7 +11686,7 @@ var FormBuilderInstance = class {
|
|
|
11727
11686
|
if (element.type === "markdown") {
|
|
11728
11687
|
return;
|
|
11729
11688
|
}
|
|
11730
|
-
const result =
|
|
11689
|
+
const result = validateElement(element, { path: "" });
|
|
11731
11690
|
if (result.skip) return;
|
|
11732
11691
|
if (result.spread && result.value !== null && typeof result.value === "object") {
|
|
11733
11692
|
Object.assign(data, result.value);
|
|
@@ -11753,16 +11712,7 @@ var FormBuilderInstance = class {
|
|
|
11753
11712
|
submitForm() {
|
|
11754
11713
|
const result = this.validateForm(false);
|
|
11755
11714
|
if (result.valid) {
|
|
11756
|
-
|
|
11757
|
-
window.parent.postMessage(
|
|
11758
|
-
{
|
|
11759
|
-
type: "formSubmit",
|
|
11760
|
-
data: result.data,
|
|
11761
|
-
schema: this.state.schema
|
|
11762
|
-
},
|
|
11763
|
-
"*"
|
|
11764
|
-
);
|
|
11765
|
-
}
|
|
11715
|
+
this.postToParent("formSubmit", result.data);
|
|
11766
11716
|
}
|
|
11767
11717
|
return result;
|
|
11768
11718
|
}
|
|
@@ -11771,17 +11721,27 @@ var FormBuilderInstance = class {
|
|
|
11771
11721
|
*/
|
|
11772
11722
|
saveDraft() {
|
|
11773
11723
|
const result = this.validateForm(true);
|
|
11774
|
-
|
|
11775
|
-
|
|
11776
|
-
|
|
11777
|
-
|
|
11778
|
-
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
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'
|
|
11782
11738
|
);
|
|
11783
11739
|
}
|
|
11784
|
-
return
|
|
11740
|
+
if (!target || typeof window === "undefined" || !window.parent) return;
|
|
11741
|
+
window.parent.postMessage(
|
|
11742
|
+
{ type, data, schema: this.state.schema },
|
|
11743
|
+
target
|
|
11744
|
+
);
|
|
11785
11745
|
}
|
|
11786
11746
|
/**
|
|
11787
11747
|
* Clear the form - reset all field values to empty while preserving form structure
|
|
@@ -12051,7 +12011,9 @@ var FormBuilderInstance = class {
|
|
|
12051
12011
|
this.state.debounceTimer = null;
|
|
12052
12012
|
}
|
|
12053
12013
|
this.disconnectEnableIfObservers();
|
|
12014
|
+
this.disconnectAutoExpandObservers();
|
|
12054
12015
|
this.removeTooltipElements();
|
|
12016
|
+
this.removePrefillHintListener();
|
|
12055
12017
|
this.state.resourceIndex.clear();
|
|
12056
12018
|
if (this.state.formRoot) {
|
|
12057
12019
|
clear(this.state.formRoot);
|
|
@@ -12069,6 +12031,18 @@ var FormBuilderInstance = class {
|
|
|
12069
12031
|
}
|
|
12070
12032
|
this.state.enableIfObservers.clear();
|
|
12071
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
|
+
}
|
|
12072
12046
|
removeTooltipElements() {
|
|
12073
12047
|
for (const tooltip of this.state.tooltipElements) {
|
|
12074
12048
|
tooltip.remove();
|