@dmitryvim/form-builder 0.5.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/browser/formbuilder.min.js +145 -153
- package/dist/browser/formbuilder.v0.7.0.min.js +1482 -0
- package/dist/cjs/index.cjs +1416 -1296
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +1377 -1260
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +145 -153
- package/dist/types/components/container.d.ts +0 -7
- package/dist/types/components/file/dom.d.ts +16 -4
- package/dist/types/components/file/render-edit.d.ts +6 -5
- package/dist/types/components/index.d.ts +1 -2
- package/dist/types/instance/FormBuilderInstance.d.ts +49 -8
- package/dist/types/instance/state.d.ts +8 -0
- package/dist/types/types/component-operations.d.ts +28 -2
- package/dist/types/types/config.d.ts +2 -0
- package/dist/types/types/state.d.ts +25 -0
- package/dist/types/utils/helpers.d.ts +18 -1
- package/dist/types/utils/styles.d.ts +75 -7
- package/dist/types/utils/validation.d.ts +14 -0
- 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";
|
|
@@ -78,408 +95,195 @@ function createHiddenInput(name, value) {
|
|
|
78
95
|
return input;
|
|
79
96
|
}
|
|
80
97
|
|
|
81
|
-
// src/utils/
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
if (element.minLength != null && element.maxLength != null) {
|
|
85
|
-
parts.push(
|
|
86
|
-
t("hintLengthRange", state, {
|
|
87
|
-
min: element.minLength,
|
|
88
|
-
max: element.maxLength
|
|
89
|
-
})
|
|
90
|
-
);
|
|
91
|
-
} else if (element.maxLength != null) {
|
|
92
|
-
parts.push(t("hintMaxLength", state, { max: element.maxLength }));
|
|
93
|
-
} else if (element.minLength != null) {
|
|
94
|
-
parts.push(t("hintMinLength", state, { min: element.minLength }));
|
|
95
|
-
}
|
|
96
|
-
}
|
|
98
|
+
// src/utils/styles.ts
|
|
99
|
+
function findErrorAnchor(input) {
|
|
100
|
+
return input.closest?.(".fb-chip") ?? input.closest?.(".slider-container") ?? input;
|
|
97
101
|
}
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
parts.push(t("hintMinValue", state, { min: element.min }));
|
|
102
|
+
function findErrorNode(input) {
|
|
103
|
+
const anchor = findErrorAnchor(input);
|
|
104
|
+
const name = input.getAttribute("name");
|
|
105
|
+
const parent = anchor.parentElement;
|
|
106
|
+
if (name && parent) {
|
|
107
|
+
for (const child of Array.from(parent.children)) {
|
|
108
|
+
if (isInputErrorNode(child) && child.getAttribute("data-error-for") === name) {
|
|
109
|
+
return child;
|
|
110
|
+
}
|
|
108
111
|
}
|
|
109
112
|
}
|
|
113
|
+
const sibling = anchor.nextElementSibling;
|
|
114
|
+
return sibling && isInputErrorNode(sibling) ? sibling : null;
|
|
110
115
|
}
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
if (sizeMB && sizeMB !== Infinity) {
|
|
114
|
-
parts.push(t("hintMaxSize", state, { size: sizeMB }));
|
|
115
|
-
}
|
|
116
|
+
function isInputErrorNode(node) {
|
|
117
|
+
return node.classList.contains("error-message") && !node.classList.contains("fb-field-error");
|
|
116
118
|
}
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
})
|
|
123
|
-
);
|
|
119
|
+
function resolveMark(target, message, scope) {
|
|
120
|
+
const reported = scope.state.reportedInvalid;
|
|
121
|
+
if (message === null || scope.readonly) {
|
|
122
|
+
reported.delete(target);
|
|
123
|
+
return null;
|
|
124
124
|
}
|
|
125
|
+
if (scope.draftMarks && !reported.has(target)) return void 0;
|
|
126
|
+
reported.add(target);
|
|
127
|
+
return message;
|
|
125
128
|
}
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
parts.push(t("hintPattern", state, { pattern: element.pattern }));
|
|
129
|
-
}
|
|
129
|
+
function joinErrorMessages(messages) {
|
|
130
|
+
return messages.length > 0 ? messages.join(" \u2022 ") : null;
|
|
130
131
|
}
|
|
131
|
-
function
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
function createErrorNode(state, className) {
|
|
133
|
+
const node = document.createElement("div");
|
|
134
|
+
node.className = className;
|
|
135
|
+
node.id = nextDomId(state, "error");
|
|
136
|
+
node.style.cssText = `
|
|
137
|
+
display: block;
|
|
138
|
+
color: var(--fb-error-color);
|
|
139
|
+
font-size: var(--fb-font-size-small);
|
|
140
|
+
margin-top: 0.25rem;
|
|
141
|
+
`;
|
|
142
|
+
return node;
|
|
143
|
+
}
|
|
144
|
+
function setAttr(el, name, value) {
|
|
145
|
+
if (el.getAttribute(name) !== value) el.setAttribute(name, value);
|
|
146
|
+
}
|
|
147
|
+
function nextDomId(state, kind) {
|
|
148
|
+
return `${state.instanceId}-${kind}-${++state.domIdCounter}`;
|
|
149
|
+
}
|
|
150
|
+
function describedByTokens(target) {
|
|
151
|
+
return (target.getAttribute("aria-describedby") ?? "").split(/\s+/).filter(Boolean);
|
|
152
|
+
}
|
|
153
|
+
var FORM_CONTROL = "input, select, textarea, button";
|
|
154
|
+
var ADDED_ATTRS = "data-fb-mark-added";
|
|
155
|
+
function fieldLabelOf(target) {
|
|
156
|
+
const field = target.closest(".fb-field-wrapper");
|
|
157
|
+
const labelRow = field ? Array.from(field.children).find(
|
|
158
|
+
(child) => child.hasAttribute("data-fb-label-row")
|
|
159
|
+
) : void 0;
|
|
160
|
+
return labelRow?.querySelector("label") ?? null;
|
|
161
|
+
}
|
|
162
|
+
function exposeAsGroup(target, state) {
|
|
163
|
+
if (target.matches(FORM_CONTROL) || target.hasAttribute(ADDED_ATTRS)) return;
|
|
164
|
+
const added = [];
|
|
165
|
+
if (!target.hasAttribute("tabindex")) {
|
|
166
|
+
target.tabIndex = -1;
|
|
167
|
+
added.push("tabindex");
|
|
168
|
+
}
|
|
169
|
+
if (!target.hasAttribute("role")) {
|
|
170
|
+
target.setAttribute("role", "group");
|
|
171
|
+
added.push("role");
|
|
172
|
+
}
|
|
173
|
+
const label = fieldLabelOf(target);
|
|
174
|
+
if (label && !target.hasAttribute("aria-labelledby")) {
|
|
175
|
+
if (!label.id) label.id = nextDomId(state, "label");
|
|
176
|
+
target.setAttribute("aria-labelledby", label.id);
|
|
177
|
+
added.push("aria-labelledby");
|
|
178
|
+
}
|
|
179
|
+
target.setAttribute(ADDED_ATTRS, added.join(" "));
|
|
180
|
+
}
|
|
181
|
+
function linkDescription(target, node) {
|
|
182
|
+
if (!describedByTokens(target).includes(node.id)) {
|
|
183
|
+
target.setAttribute(
|
|
184
|
+
"aria-describedby",
|
|
185
|
+
[...describedByTokens(target), node.id].join(" ")
|
|
186
|
+
);
|
|
136
187
|
}
|
|
137
|
-
addFileSizeHint(element, parts, state);
|
|
138
|
-
addFormatHint(element, parts, state);
|
|
139
|
-
addPatternHint(element, parts, state);
|
|
140
|
-
return parts.join(" \u2022 ");
|
|
141
188
|
}
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const columns = schema.columns;
|
|
154
|
-
const validColumns = [1, 2, 3, 4];
|
|
155
|
-
if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
|
|
156
|
-
errors.push(`schema.columns must be 1, 2, 3, or 4 (got ${columns})`);
|
|
189
|
+
function setInvalidMark(target, node, state) {
|
|
190
|
+
setAttr(target, "aria-invalid", "true");
|
|
191
|
+
exposeAsGroup(target, state);
|
|
192
|
+
if (node) linkDescription(target, node);
|
|
193
|
+
}
|
|
194
|
+
function unsetInvalidState(target) {
|
|
195
|
+
target.removeAttribute("aria-invalid");
|
|
196
|
+
const added = target.getAttribute(ADDED_ATTRS);
|
|
197
|
+
if (added !== null) {
|
|
198
|
+
for (const attr of added.split(" ").filter(Boolean)) {
|
|
199
|
+
target.removeAttribute(attr);
|
|
157
200
|
}
|
|
201
|
+
target.removeAttribute(ADDED_ATTRS);
|
|
158
202
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
if (!hint.values || typeof hint.values !== "object") {
|
|
169
|
-
errors.push(
|
|
170
|
-
`schema.prefillHints[${hintIndex}] must have a 'values' property of type object`
|
|
171
|
-
);
|
|
172
|
-
} else {
|
|
173
|
-
for (const fieldKey in hint.values) {
|
|
174
|
-
const fieldExists = schema.elements.some(
|
|
175
|
-
(element) => element.key === fieldKey
|
|
176
|
-
);
|
|
177
|
-
if (!fieldExists) {
|
|
178
|
-
errors.push(
|
|
179
|
-
`schema.prefillHints[${hintIndex}] references non-existent field "${fieldKey}"`
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
}
|
|
203
|
+
}
|
|
204
|
+
function clearInvalidMark(target, node) {
|
|
205
|
+
unsetInvalidState(target);
|
|
206
|
+
if (node) {
|
|
207
|
+
const rest = describedByTokens(target).filter((id) => id !== node.id);
|
|
208
|
+
if (rest.length > 0) setAttr(target, "aria-describedby", rest.join(" "));
|
|
209
|
+
else target.removeAttribute("aria-describedby");
|
|
210
|
+
node.remove();
|
|
186
211
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
`${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
if ("displayMode" in element && element.displayMode !== void 0) {
|
|
198
|
-
const displayMode = element.displayMode;
|
|
199
|
-
if (displayMode !== "stack" && displayMode !== "slides") {
|
|
200
|
-
errors2.push(
|
|
201
|
-
`${elementPath}: displayMode must be "stack" or "slides" (got ${JSON.stringify(displayMode)})`
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
212
|
+
}
|
|
213
|
+
function drawMark(target, message, existing, createNode, state) {
|
|
214
|
+
if (message === "") {
|
|
215
|
+
if (existing) clearInvalidMark(target, existing);
|
|
216
|
+
setInvalidMark(target, null, state);
|
|
217
|
+
return;
|
|
205
218
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with element key "${otherEl.key}"`
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
if (otherEl.key === filesKey) {
|
|
221
|
-
errors.push(
|
|
222
|
-
`${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with element key "${otherEl.key}"`
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
if (allOutputKeys.has(textKey)) {
|
|
227
|
-
errors.push(
|
|
228
|
-
`${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with another flatOutput key`
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
if (allOutputKeys.has(filesKey)) {
|
|
232
|
-
errors.push(
|
|
233
|
-
`${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with another flatOutput key`
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
allOutputKeys.add(textKey);
|
|
237
|
-
allOutputKeys.add(filesKey);
|
|
238
|
-
} else {
|
|
239
|
-
if (el.key) {
|
|
240
|
-
if (allOutputKeys.has(el.key)) {
|
|
241
|
-
errors.push(
|
|
242
|
-
`${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
allOutputKeys.add(el.key);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
219
|
+
const node = existing ?? createNode();
|
|
220
|
+
if (node.textContent !== message) node.textContent = message;
|
|
221
|
+
setInvalidMark(target, node, state);
|
|
222
|
+
}
|
|
223
|
+
function markFieldValidity(input, errorMessage, scope) {
|
|
224
|
+
if (!input) return;
|
|
225
|
+
const mark = resolveMark(input, errorMessage, scope);
|
|
226
|
+
if (mark === void 0) return;
|
|
227
|
+
if (mark === null) {
|
|
228
|
+
clearFieldError(input);
|
|
229
|
+
return;
|
|
249
230
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
231
|
+
if (!input.classList.contains("invalid")) input.classList.add("invalid");
|
|
232
|
+
if (input.title !== mark) input.title = mark;
|
|
233
|
+
const errorFor = input.getAttribute("name") ?? "";
|
|
234
|
+
const existing = findErrorNode(input);
|
|
235
|
+
if (existing) setAttr(existing, "data-error-for", errorFor);
|
|
236
|
+
drawMark(
|
|
237
|
+
input,
|
|
238
|
+
mark,
|
|
239
|
+
existing,
|
|
240
|
+
() => {
|
|
241
|
+
const node = createErrorNode(scope.state, "error-message");
|
|
242
|
+
node.setAttribute("data-error-for", errorFor);
|
|
243
|
+
const anchor = findErrorAnchor(input);
|
|
244
|
+
anchor.parentNode?.insertBefore(node, anchor.nextSibling);
|
|
245
|
+
return node;
|
|
246
|
+
},
|
|
247
|
+
scope.state
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
function clearFieldError(input) {
|
|
251
|
+
if (input.classList.contains("invalid")) input.classList.remove("invalid");
|
|
252
|
+
if (input.title !== "") input.title = "";
|
|
253
|
+
clearInvalidMark(input, findErrorNode(input));
|
|
254
|
+
}
|
|
255
|
+
function markFieldGroupValidity(scopeRoot, fieldPath, errorMessage, scope) {
|
|
256
|
+
const wrapper = scopeRoot.querySelector(
|
|
257
|
+
`[data-field-path="${fieldPath}"]`
|
|
258
|
+
);
|
|
259
|
+
if (!wrapper) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
`markFieldGroupValidity: no [data-field-path="${fieldPath}"] in scope`
|
|
275
262
|
);
|
|
276
263
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
const min = typeof minCount === "number" ? minCount : void 0;
|
|
289
|
-
const max = typeof maxCount === "number" ? maxCount : void 0;
|
|
290
|
-
if (max !== void 0 && (max < 0 || Number.isNaN(max))) {
|
|
291
|
-
errors2.push(
|
|
292
|
-
`${elementPath}: ${maxName} must be a non-negative number or Infinity (got ${max})`
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
if (min !== void 0 && (min < 0 || !Number.isFinite(min))) {
|
|
296
|
-
errors2.push(
|
|
297
|
-
`${elementPath}: ${minName} must be a finite non-negative number (got ${min})`
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
const effectiveMin = min ?? (requiredImpliesFloor ? 1 : void 0);
|
|
301
|
-
if (effectiveMin !== void 0 && max !== void 0 && effectiveMin > max) {
|
|
302
|
-
const shown = min !== void 0 ? `${minName} (${min})` : `required: true (implies ${minName} 1)`;
|
|
303
|
-
errors2.push(
|
|
304
|
-
`${elementPath}: ${shown} cannot be greater than ${maxName} (${max})`
|
|
305
|
-
);
|
|
306
|
-
}
|
|
264
|
+
if (wrapper.getAttribute("data-conditionally-disabled") === "true") return;
|
|
265
|
+
const mark = resolveMark(wrapper, errorMessage, scope);
|
|
266
|
+
if (mark === void 0) return;
|
|
267
|
+
const existing = Array.from(wrapper.children).find(
|
|
268
|
+
(child) => child instanceof HTMLElement && child.classList.contains("fb-field-error")
|
|
269
|
+
) ?? null;
|
|
270
|
+
if (mark === null) {
|
|
271
|
+
clearInvalidMark(wrapper, existing);
|
|
272
|
+
return;
|
|
307
273
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const decimals = element.decimals;
|
|
320
|
-
if (decimals !== void 0 && (!Number.isInteger(decimals) || decimals < 0)) {
|
|
321
|
-
errors.push(
|
|
322
|
-
`${elementPath}: decimals must be a non-negative integer (got ${JSON.stringify(decimals)})`
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
if (element.type === "markdown") {
|
|
327
|
-
const content = element.content;
|
|
328
|
-
if (typeof content !== "string") {
|
|
329
|
-
errors.push(
|
|
330
|
-
`${elementPath}: markdown element requires "content" to be a string (got ${content === null ? "null" : typeof content})`
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
if (element.enableIf) {
|
|
335
|
-
const enableIf = element.enableIf;
|
|
336
|
-
if (!enableIf.key || typeof enableIf.key !== "string") {
|
|
337
|
-
errors.push(
|
|
338
|
-
`${elementPath}: enableIf must have a 'key' property of type string`
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
const hasOperator = "equals" in enableIf;
|
|
342
|
-
if (!hasOperator) {
|
|
343
|
-
errors.push(
|
|
344
|
-
`${elementPath}: enableIf must have at least one operator (equals, etc.)`
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
if (element.type === "group" && "elements" in element && element.elements) {
|
|
349
|
-
validateElements(element.elements, `${elementPath}.elements`);
|
|
350
|
-
}
|
|
351
|
-
if (element.type === "container" && element.elements) {
|
|
352
|
-
validateContainerProps(element, elementPath, errors);
|
|
353
|
-
if ("prefillHints" in element && element.prefillHints) {
|
|
354
|
-
const prefillHints = element.prefillHints;
|
|
355
|
-
if (Array.isArray(prefillHints)) {
|
|
356
|
-
prefillHints.forEach((hint, hintIndex) => {
|
|
357
|
-
if (!hint.label || typeof hint.label !== "string") {
|
|
358
|
-
errors.push(
|
|
359
|
-
`${elementPath}: prefillHints[${hintIndex}] must have a 'label' property of type string`
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
if (!hint.values || typeof hint.values !== "object") {
|
|
363
|
-
errors.push(
|
|
364
|
-
`${elementPath}: prefillHints[${hintIndex}] must have a 'values' property of type object`
|
|
365
|
-
);
|
|
366
|
-
} else {
|
|
367
|
-
for (const fieldKey in hint.values) {
|
|
368
|
-
const fieldExists = element.elements.some(
|
|
369
|
-
(childElement) => childElement.key === fieldKey
|
|
370
|
-
);
|
|
371
|
-
if (!fieldExists) {
|
|
372
|
-
errors.push(
|
|
373
|
-
`container "${element.key}": prefillHints[${hintIndex}] references non-existent field "${fieldKey}"`
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
validateElements(element.elements, `${elementPath}.elements`);
|
|
382
|
-
checkFlatOutputCollisions(element.elements, `${elementPath}.elements`);
|
|
383
|
-
}
|
|
384
|
-
if (element.type === "select" && element.options) {
|
|
385
|
-
const defaultValue = element.default;
|
|
386
|
-
if (defaultValue !== void 0 && defaultValue !== null && defaultValue !== "") {
|
|
387
|
-
const hasMatchingOption = element.options.some(
|
|
388
|
-
(opt) => opt.value === defaultValue
|
|
389
|
-
);
|
|
390
|
-
if (!hasMatchingOption) {
|
|
391
|
-
errors.push(
|
|
392
|
-
`${elementPath}: default "${defaultValue}" not in options`
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
if (Array.isArray(schema.elements)) {
|
|
400
|
-
validateElements(schema.elements, "elements");
|
|
401
|
-
checkFlatOutputCollisions(schema.elements, "elements");
|
|
402
|
-
}
|
|
403
|
-
return errors;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// src/utils/enable-conditions.ts
|
|
407
|
-
function getValueByPath(data, path) {
|
|
408
|
-
if (!data || typeof data !== "object") {
|
|
409
|
-
return void 0;
|
|
410
|
-
}
|
|
411
|
-
const segments = path.match(/[^.[\]]+|\[\d+\]/g);
|
|
412
|
-
if (!segments || segments.length === 0) {
|
|
413
|
-
return void 0;
|
|
414
|
-
}
|
|
415
|
-
let current = data;
|
|
416
|
-
for (const segment of segments) {
|
|
417
|
-
if (current === void 0 || current === null) {
|
|
418
|
-
return void 0;
|
|
419
|
-
}
|
|
420
|
-
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
421
|
-
const index = parseInt(segment.slice(1, -1), 10);
|
|
422
|
-
if (!Array.isArray(current) || isNaN(index)) {
|
|
423
|
-
return void 0;
|
|
424
|
-
}
|
|
425
|
-
current = current[index];
|
|
426
|
-
} else {
|
|
427
|
-
current = current[segment];
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
return current;
|
|
431
|
-
}
|
|
432
|
-
function evaluateEnableCondition(condition, formData, containerData) {
|
|
433
|
-
if (!condition || !condition.key) {
|
|
434
|
-
throw new Error("Invalid enableIf condition: must have a 'key' property");
|
|
435
|
-
}
|
|
436
|
-
const scope = condition.scope ?? "relative";
|
|
437
|
-
let dataSource;
|
|
438
|
-
if (scope === "relative") {
|
|
439
|
-
dataSource = containerData ?? formData;
|
|
440
|
-
} else if (scope === "absolute") {
|
|
441
|
-
dataSource = formData;
|
|
442
|
-
} else {
|
|
443
|
-
throw new Error(
|
|
444
|
-
`Invalid enableIf scope: must be "relative" or "absolute" (got "${scope}")`
|
|
445
|
-
);
|
|
446
|
-
}
|
|
447
|
-
const actualValue = getValueByPath(dataSource, condition.key);
|
|
448
|
-
if ("equals" in condition) {
|
|
449
|
-
return deepEqual(actualValue, condition.equals);
|
|
450
|
-
}
|
|
451
|
-
throw new Error(
|
|
452
|
-
`Invalid enableIf condition: no recognized operator (equals, etc.)`
|
|
274
|
+
drawMark(
|
|
275
|
+
wrapper,
|
|
276
|
+
mark,
|
|
277
|
+
existing,
|
|
278
|
+
() => {
|
|
279
|
+
const node = createErrorNode(scope.state, "error-message fb-field-error");
|
|
280
|
+
node.setAttribute("data-error-for", fieldPath);
|
|
281
|
+
wrapper.appendChild(node);
|
|
282
|
+
return node;
|
|
283
|
+
},
|
|
284
|
+
scope.state
|
|
453
285
|
);
|
|
454
286
|
}
|
|
455
|
-
function deepEqual(a, b) {
|
|
456
|
-
if (a === b) return true;
|
|
457
|
-
if (a == null || b == null) return a === b;
|
|
458
|
-
if (typeof a !== typeof b) return false;
|
|
459
|
-
if (typeof a === "object" && typeof b === "object") {
|
|
460
|
-
try {
|
|
461
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
462
|
-
} catch (e) {
|
|
463
|
-
if (e instanceof TypeError && (e.message.includes("circular") || e.message.includes("cyclic"))) {
|
|
464
|
-
console.warn(
|
|
465
|
-
"deepEqual: Circular reference detected in enableIf comparison, using reference equality"
|
|
466
|
-
);
|
|
467
|
-
return a === b;
|
|
468
|
-
}
|
|
469
|
-
throw e;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
return a === b;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// src/utils/styles.ts
|
|
476
|
-
function clearFieldError(input) {
|
|
477
|
-
const name = input.getAttribute("name");
|
|
478
|
-
if (!name) return;
|
|
479
|
-
const doc = input.ownerDocument || document;
|
|
480
|
-
const errorNode = doc.getElementById(`error-${name}`);
|
|
481
|
-
if (errorNode) errorNode.remove();
|
|
482
|
-
}
|
|
483
287
|
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
288
|
function ensureThemingHooks(doc) {
|
|
485
289
|
if (doc.head.querySelector("[data-fb-theming-hooks]")) return;
|
|
@@ -597,10 +401,17 @@ function ensureThemingHooks(doc) {
|
|
|
597
401
|
/* .fb-size-md uses defaults \u2014 no override needed */
|
|
598
402
|
.fb-size-lg { --fb-input-padding-y: 2px; --fb-input-padding-x: 8px; --fb-font-size: 16px; --fb-font-size-small: 13px; --fb-file-wide-min: 128px; --fb-file-zone-min: 96px; --fb-file-zone-padding: 12px; }
|
|
599
403
|
.fb-size-xl { --fb-input-padding-y: 3px; --fb-input-padding-x: 8px; --fb-font-size: 18px; --fb-font-size-small: 14px; --fb-file-wide-min: 160px; --fb-file-zone-min: 120px; --fb-file-zone-padding: 14px; }
|
|
404
|
+
/* !important: controls carry their border inline and JS focus/hover
|
|
405
|
+
handlers rewrite it, so no weaker rule would ever show the mark. */
|
|
406
|
+
[data-fb-root] input[aria-invalid="true"],
|
|
407
|
+
[data-fb-root] select[aria-invalid="true"],
|
|
408
|
+
[data-fb-root] textarea[aria-invalid="true"] {
|
|
409
|
+
border-color: var(--fb-error-color) !important;
|
|
410
|
+
}
|
|
600
411
|
`;
|
|
601
412
|
doc.head.appendChild(style);
|
|
602
413
|
}
|
|
603
|
-
function applyAutoExpand(textarea, options
|
|
414
|
+
function applyAutoExpand(textarea, options) {
|
|
604
415
|
textarea.style.overflow = "hidden";
|
|
605
416
|
textarea.style.resize = "none";
|
|
606
417
|
const minRows = Math.max(1, options.minRows ?? 1);
|
|
@@ -630,6 +441,7 @@ function applyAutoExpand(textarea, options = {}) {
|
|
|
630
441
|
const ro = new ResizeObserver((entries) => {
|
|
631
442
|
if (!textarea.isConnected) {
|
|
632
443
|
ro.disconnect();
|
|
444
|
+
options.observers?.delete(ro);
|
|
633
445
|
return;
|
|
634
446
|
}
|
|
635
447
|
const entry = entries[0];
|
|
@@ -639,6 +451,7 @@ function applyAutoExpand(textarea, options = {}) {
|
|
|
639
451
|
resize();
|
|
640
452
|
});
|
|
641
453
|
ro.observe(textarea);
|
|
454
|
+
options.observers?.add(ro);
|
|
642
455
|
}
|
|
643
456
|
function applySingleLineMode(textarea) {
|
|
644
457
|
textarea.addEventListener("keydown", (e) => {
|
|
@@ -773,52 +586,473 @@ function createSlideAddTile(onClick, options = {}) {
|
|
|
773
586
|
text.textContent = label;
|
|
774
587
|
tile.appendChild(text);
|
|
775
588
|
}
|
|
776
|
-
tile.addEventListener("mouseenter", () => {
|
|
777
|
-
if (tile.disabled) return;
|
|
778
|
-
tile.style.borderStyle = "solid";
|
|
779
|
-
tile.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
780
|
-
});
|
|
781
|
-
tile.addEventListener("mouseleave", () => {
|
|
782
|
-
tile.style.borderStyle = "dashed";
|
|
783
|
-
tile.style.backgroundColor = "transparent";
|
|
784
|
-
});
|
|
785
|
-
tile.onclick = onClick;
|
|
786
|
-
const counter = document.createElement("span");
|
|
787
|
-
counter.className = "fb-add-counter";
|
|
788
|
-
counter.style.cssText = `
|
|
789
|
-
margin-left: auto;
|
|
790
|
-
font-size: var(--fb-font-size-small, 0.875rem);
|
|
791
|
-
color: var(--fb-text-secondary-color);
|
|
792
|
-
font-weight: 400;
|
|
793
|
-
`;
|
|
794
|
-
const update = (current, max) => {
|
|
795
|
-
const reached = current >= max;
|
|
796
|
-
tile.style.display = reached ? "none" : "flex";
|
|
797
|
-
tile.disabled = reached;
|
|
798
|
-
counter.textContent = `${current}/${max === Infinity ? "\u221E" : max}`;
|
|
799
|
-
};
|
|
800
|
-
return { tile, counter, update };
|
|
589
|
+
tile.addEventListener("mouseenter", () => {
|
|
590
|
+
if (tile.disabled) return;
|
|
591
|
+
tile.style.borderStyle = "solid";
|
|
592
|
+
tile.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
593
|
+
});
|
|
594
|
+
tile.addEventListener("mouseleave", () => {
|
|
595
|
+
tile.style.borderStyle = "dashed";
|
|
596
|
+
tile.style.backgroundColor = "transparent";
|
|
597
|
+
});
|
|
598
|
+
tile.onclick = onClick;
|
|
599
|
+
const counter = document.createElement("span");
|
|
600
|
+
counter.className = "fb-add-counter";
|
|
601
|
+
counter.style.cssText = `
|
|
602
|
+
margin-left: auto;
|
|
603
|
+
font-size: var(--fb-font-size-small, 0.875rem);
|
|
604
|
+
color: var(--fb-text-secondary-color);
|
|
605
|
+
font-weight: 400;
|
|
606
|
+
`;
|
|
607
|
+
const update = (current, max) => {
|
|
608
|
+
const reached = current >= max;
|
|
609
|
+
tile.style.display = reached ? "none" : "flex";
|
|
610
|
+
tile.disabled = reached;
|
|
611
|
+
counter.textContent = `${current}/${max === Infinity ? "\u221E" : max}`;
|
|
612
|
+
};
|
|
613
|
+
return { tile, counter, update };
|
|
614
|
+
}
|
|
615
|
+
function applyActionButtonStyles(button, isFormLevel = false) {
|
|
616
|
+
button.style.cssText = `
|
|
617
|
+
background-color: var(--fb-action-bg-color);
|
|
618
|
+
color: var(--fb-action-text-color);
|
|
619
|
+
border: var(--fb-border-width) solid var(--fb-action-border-color);
|
|
620
|
+
padding: ${isFormLevel ? "0.5rem 1rem" : "0.5rem 0.75rem"};
|
|
621
|
+
font-size: var(--fb-font-size);
|
|
622
|
+
font-weight: var(--fb-font-weight-medium);
|
|
623
|
+
border-radius: var(--fb-border-radius);
|
|
624
|
+
transition: all var(--fb-transition-duration);
|
|
625
|
+
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
626
|
+
`;
|
|
627
|
+
button.addEventListener("mouseenter", () => {
|
|
628
|
+
button.style.backgroundColor = "var(--fb-action-hover-bg-color)";
|
|
629
|
+
button.style.borderColor = "var(--fb-action-hover-border-color)";
|
|
630
|
+
});
|
|
631
|
+
button.addEventListener("mouseleave", () => {
|
|
632
|
+
button.style.backgroundColor = "var(--fb-action-bg-color)";
|
|
633
|
+
button.style.borderColor = "var(--fb-action-border-color)";
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/utils/validation.ts
|
|
638
|
+
function countRuleMessages(element, count, state, keys = { min: "minItems", max: "maxItems" }) {
|
|
639
|
+
const minCount = "minCount" in element ? element.minCount ?? 0 : 0;
|
|
640
|
+
const maxCount = "maxCount" in element ? element.maxCount ?? Infinity : Infinity;
|
|
641
|
+
const messages = [];
|
|
642
|
+
if (element.required && count === 0) messages.push(t("required", state));
|
|
643
|
+
if (count < minCount) messages.push(t(keys.min, state, { min: minCount }));
|
|
644
|
+
if (count > maxCount) messages.push(t(keys.max, state, { max: maxCount }));
|
|
645
|
+
return messages;
|
|
646
|
+
}
|
|
647
|
+
function validateItemCount(element, key, filledCount, context, errors) {
|
|
648
|
+
const messages = countRuleMessages(element, filledCount, context.state);
|
|
649
|
+
errors.push(...messages.map((message) => `${key}: ${message}`));
|
|
650
|
+
markFieldGroupValidity(
|
|
651
|
+
context.scopeRoot,
|
|
652
|
+
key,
|
|
653
|
+
joinErrorMessages(messages),
|
|
654
|
+
context
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
function addLengthHint(element, parts, state) {
|
|
658
|
+
if (element.minLength != null || element.maxLength != null) {
|
|
659
|
+
if (element.minLength != null && element.maxLength != null) {
|
|
660
|
+
parts.push(
|
|
661
|
+
t("hintLengthRange", state, {
|
|
662
|
+
min: element.minLength,
|
|
663
|
+
max: element.maxLength
|
|
664
|
+
})
|
|
665
|
+
);
|
|
666
|
+
} else if (element.maxLength != null) {
|
|
667
|
+
parts.push(t("hintMaxLength", state, { max: element.maxLength }));
|
|
668
|
+
} else if (element.minLength != null) {
|
|
669
|
+
parts.push(t("hintMinLength", state, { min: element.minLength }));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function addRangeHint(element, parts, state) {
|
|
674
|
+
if (element.min != null || element.max != null) {
|
|
675
|
+
if (element.min != null && element.max != null) {
|
|
676
|
+
parts.push(
|
|
677
|
+
t("hintValueRange", state, { min: element.min, max: element.max })
|
|
678
|
+
);
|
|
679
|
+
} else if (element.max != null) {
|
|
680
|
+
parts.push(t("hintMaxValue", state, { max: element.max }));
|
|
681
|
+
} else if (element.min != null) {
|
|
682
|
+
parts.push(t("hintMinValue", state, { min: element.min }));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function addFileSizeHint(element, parts, state) {
|
|
687
|
+
const sizeMB = element.maxSize ?? element.maxSizeMB;
|
|
688
|
+
if (sizeMB && sizeMB !== Infinity) {
|
|
689
|
+
parts.push(t("hintMaxSize", state, { size: sizeMB }));
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function addFormatHint(element, parts, state) {
|
|
693
|
+
if (element.accept?.extensions) {
|
|
694
|
+
parts.push(
|
|
695
|
+
t("hintFormats", state, {
|
|
696
|
+
formats: element.accept.extensions.map((ext) => ext.toUpperCase()).join(",")
|
|
697
|
+
})
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function addPatternHint(element, parts, state) {
|
|
702
|
+
if (element.pattern) {
|
|
703
|
+
parts.push(t("hintPattern", state, { pattern: element.pattern }));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function makeFieldHint(element, state) {
|
|
707
|
+
const parts = [];
|
|
708
|
+
addLengthHint(element, parts, state);
|
|
709
|
+
if (element.type !== "slider") {
|
|
710
|
+
addRangeHint(element, parts, state);
|
|
711
|
+
}
|
|
712
|
+
addFileSizeHint(element, parts, state);
|
|
713
|
+
addFormatHint(element, parts, state);
|
|
714
|
+
addPatternHint(element, parts, state);
|
|
715
|
+
return parts.join(" \u2022 ");
|
|
716
|
+
}
|
|
717
|
+
function validateSchema(schema) {
|
|
718
|
+
const errors = [];
|
|
719
|
+
if (!schema || typeof schema !== "object") {
|
|
720
|
+
errors.push("Schema must be an object");
|
|
721
|
+
return errors;
|
|
722
|
+
}
|
|
723
|
+
if (!Array.isArray(schema.elements)) {
|
|
724
|
+
errors.push("Schema missing elements array");
|
|
725
|
+
return errors;
|
|
726
|
+
}
|
|
727
|
+
if ("columns" in schema && schema.columns !== void 0) {
|
|
728
|
+
const columns = schema.columns;
|
|
729
|
+
const validColumns = [1, 2, 3, 4];
|
|
730
|
+
if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
|
|
731
|
+
errors.push(`schema.columns must be 1, 2, 3, or 4 (got ${columns})`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if ("prefillHints" in schema && schema.prefillHints) {
|
|
735
|
+
const prefillHints = schema.prefillHints;
|
|
736
|
+
if (Array.isArray(prefillHints)) {
|
|
737
|
+
prefillHints.forEach((hint, hintIndex) => {
|
|
738
|
+
if (!hint.label || typeof hint.label !== "string") {
|
|
739
|
+
errors.push(
|
|
740
|
+
`schema.prefillHints[${hintIndex}] must have a 'label' property of type string`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
if (!hint.values || typeof hint.values !== "object") {
|
|
744
|
+
errors.push(
|
|
745
|
+
`schema.prefillHints[${hintIndex}] must have a 'values' property of type object`
|
|
746
|
+
);
|
|
747
|
+
} else {
|
|
748
|
+
for (const fieldKey in hint.values) {
|
|
749
|
+
const fieldExists = schema.elements.some(
|
|
750
|
+
(element) => element.key === fieldKey
|
|
751
|
+
);
|
|
752
|
+
if (!fieldExists) {
|
|
753
|
+
errors.push(
|
|
754
|
+
`schema.prefillHints[${hintIndex}] references non-existent field "${fieldKey}"`
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function validateContainerProps(element, elementPath, errors2) {
|
|
763
|
+
if ("columns" in element && element.columns !== void 0) {
|
|
764
|
+
const columns = element.columns;
|
|
765
|
+
const validColumns = [1, 2, 3, 4];
|
|
766
|
+
if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
|
|
767
|
+
errors2.push(
|
|
768
|
+
`${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if ("displayMode" in element && element.displayMode !== void 0) {
|
|
773
|
+
const displayMode = element.displayMode;
|
|
774
|
+
if (displayMode !== "stack" && displayMode !== "slides") {
|
|
775
|
+
errors2.push(
|
|
776
|
+
`${elementPath}: displayMode must be "stack" or "slides" (got ${JSON.stringify(displayMode)})`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function checkFlatOutputCollisions(elements, scopePath) {
|
|
782
|
+
const allOutputKeys = /* @__PURE__ */ new Set();
|
|
783
|
+
for (const el of elements) {
|
|
784
|
+
if (el.type === "richinput" && el.flatOutput) {
|
|
785
|
+
const richEl = el;
|
|
786
|
+
const textKey = richEl.textKey ?? "text";
|
|
787
|
+
const filesKey = richEl.filesKey ?? "files";
|
|
788
|
+
for (const otherEl of elements) {
|
|
789
|
+
if (otherEl === el) continue;
|
|
790
|
+
if (otherEl.key === textKey) {
|
|
791
|
+
errors.push(
|
|
792
|
+
`${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with element key "${otherEl.key}"`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
if (otherEl.key === filesKey) {
|
|
796
|
+
errors.push(
|
|
797
|
+
`${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with element key "${otherEl.key}"`
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (allOutputKeys.has(textKey)) {
|
|
802
|
+
errors.push(
|
|
803
|
+
`${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with another flatOutput key`
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
if (allOutputKeys.has(filesKey)) {
|
|
807
|
+
errors.push(
|
|
808
|
+
`${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with another flatOutput key`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
allOutputKeys.add(textKey);
|
|
812
|
+
allOutputKeys.add(filesKey);
|
|
813
|
+
} else {
|
|
814
|
+
if (el.key) {
|
|
815
|
+
if (allOutputKeys.has(el.key)) {
|
|
816
|
+
errors.push(
|
|
817
|
+
`${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
allOutputKeys.add(el.key);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
function validateCountBounds(element, elementPath, errors2) {
|
|
826
|
+
const el = element;
|
|
827
|
+
if (el.type === "group") {
|
|
828
|
+
if (!isPlainObject(el.repeat)) return;
|
|
829
|
+
checkBounds(
|
|
830
|
+
elementPath,
|
|
831
|
+
el.repeat?.min,
|
|
832
|
+
el.repeat?.max,
|
|
833
|
+
"repeat.min",
|
|
834
|
+
"repeat.max",
|
|
835
|
+
el.required === true,
|
|
836
|
+
errors2
|
|
837
|
+
);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const isMultiple = el.multiple === true || el.type === "files";
|
|
841
|
+
if (!isMultiple) return;
|
|
842
|
+
checkBounds(
|
|
843
|
+
elementPath,
|
|
844
|
+
el.minCount,
|
|
845
|
+
el.maxCount,
|
|
846
|
+
"minCount",
|
|
847
|
+
"maxCount",
|
|
848
|
+
el.required === true,
|
|
849
|
+
errors2
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
function checkBounds(elementPath, minCount, maxCount, minName, maxName, requiredImpliesFloor, errors2) {
|
|
853
|
+
for (const [name, bound] of [
|
|
854
|
+
[minName, minCount],
|
|
855
|
+
[maxName, maxCount]
|
|
856
|
+
]) {
|
|
857
|
+
if (bound !== void 0 && typeof bound !== "number") {
|
|
858
|
+
errors2.push(
|
|
859
|
+
`${elementPath}: ${name} must be a number (got ${typeof bound})`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const min = typeof minCount === "number" ? minCount : void 0;
|
|
864
|
+
const max = typeof maxCount === "number" ? maxCount : void 0;
|
|
865
|
+
if (max !== void 0 && (max < 0 || Number.isNaN(max))) {
|
|
866
|
+
errors2.push(
|
|
867
|
+
`${elementPath}: ${maxName} must be a non-negative number or Infinity (got ${max})`
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
if (min !== void 0 && (min < 0 || !Number.isFinite(min))) {
|
|
871
|
+
errors2.push(
|
|
872
|
+
`${elementPath}: ${minName} must be a finite non-negative number (got ${min})`
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
const effectiveMin = min ?? (requiredImpliesFloor ? 1 : void 0);
|
|
876
|
+
if (effectiveMin !== void 0 && max !== void 0 && effectiveMin > max) {
|
|
877
|
+
const shown = min !== void 0 ? `${minName} (${min})` : `required: true (implies ${minName} 1)`;
|
|
878
|
+
errors2.push(
|
|
879
|
+
`${elementPath}: ${shown} cannot be greater than ${maxName} (${max})`
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
function validateElements(elements, path) {
|
|
884
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
885
|
+
elements.forEach((element, index) => {
|
|
886
|
+
if (!element.key) return;
|
|
887
|
+
if (seenKeys.has(element.key)) {
|
|
888
|
+
errors.push(`${path}[${index}]: duplicate key "${element.key}"`);
|
|
889
|
+
}
|
|
890
|
+
seenKeys.add(element.key);
|
|
891
|
+
});
|
|
892
|
+
elements.forEach((element, index) => {
|
|
893
|
+
const elementPath = `${path}[${index}]`;
|
|
894
|
+
if (!element.type) {
|
|
895
|
+
errors.push(`${elementPath}: missing type`);
|
|
896
|
+
}
|
|
897
|
+
if (!element.key && element.type !== "markdown") {
|
|
898
|
+
errors.push(`${elementPath}: missing key`);
|
|
899
|
+
}
|
|
900
|
+
validateCountBounds(element, elementPath, errors);
|
|
901
|
+
if (element.type === "number" && "decimals" in element) {
|
|
902
|
+
const decimals = element.decimals;
|
|
903
|
+
if (decimals !== void 0 && (!Number.isInteger(decimals) || decimals < 0)) {
|
|
904
|
+
errors.push(
|
|
905
|
+
`${elementPath}: decimals must be a non-negative integer (got ${JSON.stringify(decimals)})`
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (element.type === "markdown") {
|
|
910
|
+
const content = element.content;
|
|
911
|
+
if (typeof content !== "string") {
|
|
912
|
+
errors.push(
|
|
913
|
+
`${elementPath}: markdown element requires "content" to be a string (got ${content === null ? "null" : typeof content})`
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (element.enableIf) {
|
|
918
|
+
const enableIf = element.enableIf;
|
|
919
|
+
if (!enableIf.key || typeof enableIf.key !== "string") {
|
|
920
|
+
errors.push(
|
|
921
|
+
`${elementPath}: enableIf must have a 'key' property of type string`
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
const hasOperator = "equals" in enableIf;
|
|
925
|
+
if (!hasOperator) {
|
|
926
|
+
errors.push(
|
|
927
|
+
`${elementPath}: enableIf must have at least one operator (equals, etc.)`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (element.type === "group" && "elements" in element && element.elements) {
|
|
932
|
+
validateElements(element.elements, `${elementPath}.elements`);
|
|
933
|
+
}
|
|
934
|
+
if (element.type === "container" && element.elements) {
|
|
935
|
+
validateContainerProps(element, elementPath, errors);
|
|
936
|
+
if ("prefillHints" in element && element.prefillHints) {
|
|
937
|
+
const prefillHints = element.prefillHints;
|
|
938
|
+
if (Array.isArray(prefillHints)) {
|
|
939
|
+
prefillHints.forEach((hint, hintIndex) => {
|
|
940
|
+
if (!hint.label || typeof hint.label !== "string") {
|
|
941
|
+
errors.push(
|
|
942
|
+
`${elementPath}: prefillHints[${hintIndex}] must have a 'label' property of type string`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
if (!hint.values || typeof hint.values !== "object") {
|
|
946
|
+
errors.push(
|
|
947
|
+
`${elementPath}: prefillHints[${hintIndex}] must have a 'values' property of type object`
|
|
948
|
+
);
|
|
949
|
+
} else {
|
|
950
|
+
for (const fieldKey in hint.values) {
|
|
951
|
+
const fieldExists = element.elements.some(
|
|
952
|
+
(childElement) => childElement.key === fieldKey
|
|
953
|
+
);
|
|
954
|
+
if (!fieldExists) {
|
|
955
|
+
errors.push(
|
|
956
|
+
`container "${element.key}": prefillHints[${hintIndex}] references non-existent field "${fieldKey}"`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
validateElements(element.elements, `${elementPath}.elements`);
|
|
965
|
+
checkFlatOutputCollisions(element.elements, `${elementPath}.elements`);
|
|
966
|
+
}
|
|
967
|
+
if (element.type === "select" && element.options) {
|
|
968
|
+
const defaultValue = element.default;
|
|
969
|
+
if (defaultValue !== void 0 && defaultValue !== null && defaultValue !== "") {
|
|
970
|
+
const hasMatchingOption = element.options.some(
|
|
971
|
+
(opt) => opt.value === defaultValue
|
|
972
|
+
);
|
|
973
|
+
if (!hasMatchingOption) {
|
|
974
|
+
errors.push(
|
|
975
|
+
`${elementPath}: default "${defaultValue}" not in options`
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
if (Array.isArray(schema.elements)) {
|
|
983
|
+
validateElements(schema.elements, "elements");
|
|
984
|
+
checkFlatOutputCollisions(schema.elements, "elements");
|
|
985
|
+
}
|
|
986
|
+
return errors;
|
|
801
987
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
988
|
+
|
|
989
|
+
// src/utils/enable-conditions.ts
|
|
990
|
+
function getValueByPath(data, path) {
|
|
991
|
+
if (!data || typeof data !== "object") {
|
|
992
|
+
return void 0;
|
|
993
|
+
}
|
|
994
|
+
const segments = path.match(/[^.[\]]+|\[\d+\]/g);
|
|
995
|
+
if (!segments || segments.length === 0) {
|
|
996
|
+
return void 0;
|
|
997
|
+
}
|
|
998
|
+
let current = data;
|
|
999
|
+
for (const segment of segments) {
|
|
1000
|
+
if (current === void 0 || current === null) {
|
|
1001
|
+
return void 0;
|
|
1002
|
+
}
|
|
1003
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
1004
|
+
const index = parseInt(segment.slice(1, -1), 10);
|
|
1005
|
+
if (!Array.isArray(current) || isNaN(index)) {
|
|
1006
|
+
return void 0;
|
|
1007
|
+
}
|
|
1008
|
+
current = current[index];
|
|
1009
|
+
} else {
|
|
1010
|
+
current = current[segment];
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return current;
|
|
1014
|
+
}
|
|
1015
|
+
function evaluateEnableCondition(condition, formData, containerData) {
|
|
1016
|
+
if (!condition || !condition.key) {
|
|
1017
|
+
throw new Error("Invalid enableIf condition: must have a 'key' property");
|
|
1018
|
+
}
|
|
1019
|
+
const scope = condition.scope ?? "relative";
|
|
1020
|
+
let dataSource;
|
|
1021
|
+
if (scope === "relative") {
|
|
1022
|
+
dataSource = containerData ?? formData;
|
|
1023
|
+
} else if (scope === "absolute") {
|
|
1024
|
+
dataSource = formData;
|
|
1025
|
+
} else {
|
|
1026
|
+
throw new Error(
|
|
1027
|
+
`Invalid enableIf scope: must be "relative" or "absolute" (got "${scope}")`
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
const actualValue = getValueByPath(dataSource, condition.key);
|
|
1031
|
+
if ("equals" in condition) {
|
|
1032
|
+
return deepEqual(actualValue, condition.equals);
|
|
1033
|
+
}
|
|
1034
|
+
throw new Error(
|
|
1035
|
+
`Invalid enableIf condition: no recognized operator (equals, etc.)`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
function deepEqual(a, b) {
|
|
1039
|
+
if (a === b) return true;
|
|
1040
|
+
if (a == null || b == null) return a === b;
|
|
1041
|
+
if (typeof a !== typeof b) return false;
|
|
1042
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
1043
|
+
try {
|
|
1044
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1045
|
+
} catch (e) {
|
|
1046
|
+
if (e instanceof TypeError && (e.message.includes("circular") || e.message.includes("cyclic"))) {
|
|
1047
|
+
console.warn(
|
|
1048
|
+
"deepEqual: Circular reference detected in enableIf comparison, using reference equality"
|
|
1049
|
+
);
|
|
1050
|
+
return a === b;
|
|
1051
|
+
}
|
|
1052
|
+
throw e;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return a === b;
|
|
822
1056
|
}
|
|
823
1057
|
|
|
824
1058
|
// src/components/text.ts
|
|
@@ -950,10 +1184,10 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
950
1184
|
`;
|
|
951
1185
|
textInput.name = pathKey;
|
|
952
1186
|
textInput.placeholder = element.placeholder ?? t("placeholderText", state);
|
|
953
|
-
textInput.value = ctx.prefill[element.key]
|
|
1187
|
+
textInput.value = ctx.prefill[element.key] ?? element.default ?? "";
|
|
954
1188
|
textInput.readOnly = readonly;
|
|
955
1189
|
applySingleLineMode(textInput);
|
|
956
|
-
applyAutoExpand(textInput);
|
|
1190
|
+
applyAutoExpand(textInput, { observers: state.autoExpandObservers });
|
|
957
1191
|
if (!readonly) {
|
|
958
1192
|
textInput.addEventListener("focus", () => {
|
|
959
1193
|
textInput.style.borderColor = "var(--fb-border-focus-color)";
|
|
@@ -1011,7 +1245,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1011
1245
|
const chip = input.closest(".fb-chip");
|
|
1012
1246
|
const sib = chip?.nextElementSibling;
|
|
1013
1247
|
if (sib && sib.classList.contains("error-message")) {
|
|
1014
|
-
sib.
|
|
1248
|
+
sib.setAttribute("data-error-for", input.name);
|
|
1015
1249
|
}
|
|
1016
1250
|
});
|
|
1017
1251
|
}
|
|
@@ -1026,7 +1260,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1026
1260
|
input.rows = 1;
|
|
1027
1261
|
input.className = "fb-chip-input";
|
|
1028
1262
|
input.value = value;
|
|
1029
|
-
input.placeholder = element.placeholder
|
|
1263
|
+
input.placeholder = element.placeholder ?? t("placeholderText", state);
|
|
1030
1264
|
input.readOnly = readonly;
|
|
1031
1265
|
chip.appendChild(input);
|
|
1032
1266
|
if (!readonly && ctx.instance) {
|
|
@@ -1040,7 +1274,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1040
1274
|
input.addEventListener("input", handleChange);
|
|
1041
1275
|
}
|
|
1042
1276
|
applySingleLineMode(input);
|
|
1043
|
-
applyAutoExpand(input);
|
|
1277
|
+
applyAutoExpand(input, { observers: state.autoExpandObservers });
|
|
1044
1278
|
if (!readonly) {
|
|
1045
1279
|
const rem = document.createElement("button");
|
|
1046
1280
|
rem.type = "button";
|
|
@@ -1103,109 +1337,50 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1103
1337
|
}
|
|
1104
1338
|
function validateTextElement(element, key, context) {
|
|
1105
1339
|
const errors = [];
|
|
1106
|
-
const { scopeRoot,
|
|
1107
|
-
const
|
|
1108
|
-
if (!
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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);
|
|
1340
|
+
const { scopeRoot, state } = context;
|
|
1341
|
+
const lengthOrPatternError = (val) => {
|
|
1342
|
+
if (!val) return null;
|
|
1343
|
+
if (element.minLength != null && val.length < element.minLength) {
|
|
1344
|
+
return t("minLength", state, { min: element.minLength });
|
|
1345
|
+
}
|
|
1346
|
+
if (element.maxLength != null && val.length > element.maxLength) {
|
|
1347
|
+
return t("maxLength", state, { max: element.maxLength });
|
|
1348
|
+
}
|
|
1349
|
+
if (element.pattern) {
|
|
1350
|
+
try {
|
|
1351
|
+
if (!new RegExp(element.pattern).test(val)) {
|
|
1352
|
+
return t("patternMismatch", state);
|
|
1129
1353
|
}
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
errorElement.style.display = "block";
|
|
1133
|
-
} else {
|
|
1134
|
-
input.classList.remove("invalid");
|
|
1135
|
-
input.title = "";
|
|
1136
|
-
if (errorElement) {
|
|
1137
|
-
errorElement.remove();
|
|
1354
|
+
} catch {
|
|
1355
|
+
return t("invalidPattern", state);
|
|
1138
1356
|
}
|
|
1139
1357
|
}
|
|
1358
|
+
return null;
|
|
1140
1359
|
};
|
|
1141
1360
|
const validateTextInput = (input, val, fieldKey) => {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
if (element.minLength !== void 0 && element.minLength !== null && val.length < element.minLength) {
|
|
1146
|
-
const msg = t("minLength", state, { min: element.minLength });
|
|
1147
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
1148
|
-
markValidity(input, msg);
|
|
1149
|
-
hasError = true;
|
|
1150
|
-
} else if (element.maxLength !== void 0 && element.maxLength !== null && val.length > element.maxLength) {
|
|
1151
|
-
const msg = t("maxLength", state, { max: element.maxLength });
|
|
1152
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
1153
|
-
markValidity(input, msg);
|
|
1154
|
-
hasError = true;
|
|
1155
|
-
} else if (element.pattern) {
|
|
1156
|
-
try {
|
|
1157
|
-
const re = new RegExp(element.pattern);
|
|
1158
|
-
if (!re.test(val)) {
|
|
1159
|
-
const msg = t("patternMismatch", state);
|
|
1160
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
1161
|
-
markValidity(input, msg);
|
|
1162
|
-
hasError = true;
|
|
1163
|
-
}
|
|
1164
|
-
} catch {
|
|
1165
|
-
const msg = t("invalidPattern", state);
|
|
1166
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
1167
|
-
markValidity(input, msg);
|
|
1168
|
-
hasError = true;
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
if (!hasError) {
|
|
1173
|
-
markValidity(input, null);
|
|
1174
|
-
}
|
|
1361
|
+
const msg = lengthOrPatternError(val);
|
|
1362
|
+
if (msg !== null) errors.push(`${fieldKey}: ${msg}`);
|
|
1363
|
+
markFieldValidity(input, msg, context);
|
|
1175
1364
|
};
|
|
1176
1365
|
if (element.multiple) {
|
|
1177
1366
|
const inputs = scopeRoot.querySelectorAll(`[name^="${key}\\["]`);
|
|
1178
1367
|
const values = [];
|
|
1179
|
-
|
|
1368
|
+
let filledCount = 0;
|
|
1180
1369
|
inputs.forEach((input, index) => {
|
|
1181
1370
|
const val = input?.value ?? "";
|
|
1182
|
-
rawValues.push(val);
|
|
1183
1371
|
values.push(val === "" ? null : val);
|
|
1372
|
+
if (val.trim() !== "") filledCount++;
|
|
1184
1373
|
validateTextInput(input, val, `${key}[${index}]`);
|
|
1185
1374
|
});
|
|
1186
|
-
|
|
1187
|
-
const { state } = context;
|
|
1188
|
-
const minCount = element.minCount ?? 0;
|
|
1189
|
-
const maxCount = element.maxCount ?? Infinity;
|
|
1190
|
-
const filteredValues = rawValues.filter((v) => v.trim() !== "");
|
|
1191
|
-
if (element.required && filteredValues.length === 0) {
|
|
1192
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
1193
|
-
}
|
|
1194
|
-
if (filteredValues.length < minCount) {
|
|
1195
|
-
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
1196
|
-
}
|
|
1197
|
-
if (filteredValues.length > maxCount) {
|
|
1198
|
-
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1375
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
1201
1376
|
return { value: values, errors };
|
|
1202
1377
|
} else {
|
|
1203
1378
|
const input = scopeRoot.querySelector(`[name="${key}"]`);
|
|
1204
1379
|
const val = input?.value ?? "";
|
|
1205
|
-
if (
|
|
1206
|
-
const msg = t("required",
|
|
1380
|
+
if (element.required && val === "") {
|
|
1381
|
+
const msg = t("required", state);
|
|
1207
1382
|
errors.push(`${key}: ${msg}`);
|
|
1208
|
-
|
|
1383
|
+
markFieldValidity(input, msg, context);
|
|
1209
1384
|
return { value: null, errors };
|
|
1210
1385
|
}
|
|
1211
1386
|
if (input) {
|
|
@@ -1227,8 +1402,6 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
1227
1402
|
inputs.forEach((input, index) => {
|
|
1228
1403
|
if (index < value.length) {
|
|
1229
1404
|
input.value = value[index] != null ? String(value[index]) : "";
|
|
1230
|
-
input.classList.remove("invalid");
|
|
1231
|
-
input.title = "";
|
|
1232
1405
|
clearFieldError(input);
|
|
1233
1406
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1234
1407
|
}
|
|
@@ -1242,8 +1415,6 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
1242
1415
|
const input = scopeRoot.querySelector(`[name="${fieldPath}"]`);
|
|
1243
1416
|
if (input) {
|
|
1244
1417
|
input.value = value != null ? String(value) : "";
|
|
1245
|
-
input.classList.remove("invalid");
|
|
1246
|
-
input.title = "";
|
|
1247
1418
|
clearFieldError(input);
|
|
1248
1419
|
if (input instanceof HTMLTextAreaElement) {
|
|
1249
1420
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
@@ -1267,8 +1438,8 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1267
1438
|
line-height: var(--fb-line-height, 1.5);
|
|
1268
1439
|
`;
|
|
1269
1440
|
textareaInput.name = pathKey;
|
|
1270
|
-
textareaInput.placeholder = element.placeholder ?? "
|
|
1271
|
-
textareaInput.value = ctx.prefill[element.key]
|
|
1441
|
+
textareaInput.placeholder = element.placeholder ?? t("placeholderText", state);
|
|
1442
|
+
textareaInput.value = ctx.prefill[element.key] ?? element.default ?? "";
|
|
1272
1443
|
textareaInput.readOnly = readonly;
|
|
1273
1444
|
if (!readonly && ctx.instance) {
|
|
1274
1445
|
const handleChange = () => {
|
|
@@ -1278,7 +1449,10 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1278
1449
|
textareaInput.addEventListener("blur", handleChange);
|
|
1279
1450
|
textareaInput.addEventListener("input", handleChange);
|
|
1280
1451
|
}
|
|
1281
|
-
applyAutoExpand(textareaInput, {
|
|
1452
|
+
applyAutoExpand(textareaInput, {
|
|
1453
|
+
minRows: element.rows ?? 1,
|
|
1454
|
+
observers: state.autoExpandObservers
|
|
1455
|
+
});
|
|
1282
1456
|
textareaWrapper.appendChild(textareaInput);
|
|
1283
1457
|
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
1284
1458
|
const counter = createCharCounter(element, textareaInput);
|
|
@@ -1321,7 +1495,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1321
1495
|
font-family: var(--fb-font-family);
|
|
1322
1496
|
line-height: var(--fb-line-height, 1.5);
|
|
1323
1497
|
`;
|
|
1324
|
-
textareaInput.placeholder = element.placeholder
|
|
1498
|
+
textareaInput.placeholder = element.placeholder ?? t("placeholderText", state);
|
|
1325
1499
|
textareaInput.value = value;
|
|
1326
1500
|
textareaInput.readOnly = readonly;
|
|
1327
1501
|
if (!readonly && ctx.instance) {
|
|
@@ -1332,7 +1506,10 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1332
1506
|
textareaInput.addEventListener("blur", handleChange);
|
|
1333
1507
|
textareaInput.addEventListener("input", handleChange);
|
|
1334
1508
|
}
|
|
1335
|
-
applyAutoExpand(textareaInput, {
|
|
1509
|
+
applyAutoExpand(textareaInput, {
|
|
1510
|
+
minRows: element.rows ?? 1,
|
|
1511
|
+
observers: state.autoExpandObservers
|
|
1512
|
+
});
|
|
1336
1513
|
textareaContainer.appendChild(textareaInput);
|
|
1337
1514
|
if (!readonly && (element.minLength != null || element.maxLength != null)) {
|
|
1338
1515
|
const counter = createCharCounter(element, textareaInput);
|
|
@@ -1551,6 +1728,17 @@ function createNumberRangeHint(element, input) {
|
|
|
1551
1728
|
updateColor();
|
|
1552
1729
|
return hint;
|
|
1553
1730
|
}
|
|
1731
|
+
function numberStepAttr(element) {
|
|
1732
|
+
if (element.step !== void 0) return element.step.toString();
|
|
1733
|
+
if (element.decimals !== void 0)
|
|
1734
|
+
return (10 ** -element.decimals).toString();
|
|
1735
|
+
return "any";
|
|
1736
|
+
}
|
|
1737
|
+
function applyDecimalsMarker(input, element) {
|
|
1738
|
+
if (element.decimals !== void 0) {
|
|
1739
|
+
input.setAttribute("data-decimals", String(element.decimals));
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1554
1742
|
function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
1555
1743
|
const state = ctx.state;
|
|
1556
1744
|
const readonly = isElementReadonly(element, state, ctx);
|
|
@@ -1559,11 +1747,12 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1559
1747
|
const numberInput = document.createElement("input");
|
|
1560
1748
|
numberInput.type = "number";
|
|
1561
1749
|
numberInput.name = pathKey;
|
|
1562
|
-
numberInput.placeholder = element.placeholder
|
|
1750
|
+
numberInput.placeholder = element.placeholder ?? "0";
|
|
1563
1751
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
1564
1752
|
if (element.max !== void 0) numberInput.max = element.max.toString();
|
|
1565
|
-
|
|
1566
|
-
numberInput
|
|
1753
|
+
numberInput.step = numberStepAttr(element);
|
|
1754
|
+
applyDecimalsMarker(numberInput, element);
|
|
1755
|
+
numberInput.value = ctx.prefill[element.key] ?? element.default ?? "";
|
|
1567
1756
|
numberInput.readOnly = readonly;
|
|
1568
1757
|
if (!element.stepper) {
|
|
1569
1758
|
numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
@@ -1602,7 +1791,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1602
1791
|
const minCount = element.minCount ?? (element.required ? 1 : 0);
|
|
1603
1792
|
const maxCount = element.maxCount ?? Infinity;
|
|
1604
1793
|
while (values.length < minCount) {
|
|
1605
|
-
values.push(element.default
|
|
1794
|
+
values.push(element.default ?? "");
|
|
1606
1795
|
}
|
|
1607
1796
|
const container = document.createElement("div");
|
|
1608
1797
|
container.className = "fb-row";
|
|
@@ -1631,10 +1820,11 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1631
1820
|
width: 100%;
|
|
1632
1821
|
box-sizing: border-box;
|
|
1633
1822
|
`;
|
|
1634
|
-
numberInput.placeholder = element.placeholder
|
|
1823
|
+
numberInput.placeholder = element.placeholder ?? "0";
|
|
1635
1824
|
if (element.min !== void 0) numberInput.min = element.min.toString();
|
|
1636
1825
|
if (element.max !== void 0) numberInput.max = element.max.toString();
|
|
1637
|
-
|
|
1826
|
+
numberInput.step = numberStepAttr(element);
|
|
1827
|
+
applyDecimalsMarker(numberInput, element);
|
|
1638
1828
|
numberInput.value = value.toString();
|
|
1639
1829
|
numberInput.readOnly = readonly;
|
|
1640
1830
|
if (!readonly && ctx.instance) {
|
|
@@ -1698,8 +1888,8 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1698
1888
|
const handle = createAddItemRow(
|
|
1699
1889
|
"number",
|
|
1700
1890
|
() => {
|
|
1701
|
-
values.push(element.default
|
|
1702
|
-
addNumberItem(element.default
|
|
1891
|
+
values.push(element.default ?? "");
|
|
1892
|
+
addNumberItem(element.default ?? "");
|
|
1703
1893
|
updateAddButton();
|
|
1704
1894
|
updateRemoveButtons();
|
|
1705
1895
|
ctx.instance?.triggerOnChange(pathKey);
|
|
@@ -1719,56 +1909,27 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1719
1909
|
}
|
|
1720
1910
|
function validateNumberElement(element, key, context) {
|
|
1721
1911
|
const errors = [];
|
|
1722
|
-
const { scopeRoot,
|
|
1723
|
-
const
|
|
1724
|
-
if (
|
|
1725
|
-
|
|
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
|
-
}
|
|
1912
|
+
const { scopeRoot, state } = context;
|
|
1913
|
+
const rangeError = (v) => {
|
|
1914
|
+
if (element.min != null && v < element.min) {
|
|
1915
|
+
return t("minValue", state, { min: element.min });
|
|
1753
1916
|
}
|
|
1917
|
+
if (element.max != null && v > element.max) {
|
|
1918
|
+
return t("maxValue", state, { max: element.max });
|
|
1919
|
+
}
|
|
1920
|
+
return null;
|
|
1754
1921
|
};
|
|
1755
|
-
const validateNumberInput = (input,
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
1761
|
-
markValidity(input, msg);
|
|
1762
|
-
hasError = true;
|
|
1763
|
-
} else if (!skipValidation && element.max !== void 0 && element.max !== null && v > element.max) {
|
|
1764
|
-
const msg = t("maxValue", state, { max: element.max });
|
|
1765
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
1766
|
-
markValidity(input, msg);
|
|
1767
|
-
hasError = true;
|
|
1768
|
-
}
|
|
1769
|
-
if (!hasError) {
|
|
1770
|
-
markValidity(input, null);
|
|
1922
|
+
const validateNumberInput = (input, fieldKey) => {
|
|
1923
|
+
const raw = input.value;
|
|
1924
|
+
if (raw === "") {
|
|
1925
|
+
markFieldValidity(input, null, context);
|
|
1926
|
+
return null;
|
|
1771
1927
|
}
|
|
1928
|
+
const v = parseFloat(raw);
|
|
1929
|
+
const msg = Number.isFinite(v) ? rangeError(v) : t("notANumber", state);
|
|
1930
|
+
if (msg !== null) errors.push(`${fieldKey}: ${msg}`);
|
|
1931
|
+
markFieldValidity(input, msg, context);
|
|
1932
|
+
return Number.isFinite(v) ? applyDecimals(v, element.decimals) : null;
|
|
1772
1933
|
};
|
|
1773
1934
|
if (element.multiple) {
|
|
1774
1935
|
const inputs = scopeRoot.querySelectorAll(
|
|
@@ -1776,62 +1937,21 @@ function validateNumberElement(element, key, context) {
|
|
|
1776
1937
|
);
|
|
1777
1938
|
const values = [];
|
|
1778
1939
|
inputs.forEach((input, index) => {
|
|
1779
|
-
|
|
1780
|
-
if (raw === "") {
|
|
1781
|
-
values.push(null);
|
|
1782
|
-
markValidity(input, null);
|
|
1783
|
-
return;
|
|
1784
|
-
}
|
|
1785
|
-
const v = parseFloat(raw);
|
|
1786
|
-
if (!skipValidation && !Number.isFinite(v)) {
|
|
1787
|
-
const msg = t("notANumber", context.state);
|
|
1788
|
-
errors.push(`${key}[${index}]: ${msg}`);
|
|
1789
|
-
markValidity(input, msg);
|
|
1790
|
-
values.push(null);
|
|
1791
|
-
return;
|
|
1792
|
-
}
|
|
1793
|
-
validateNumberInput(input, v, `${key}[${index}]`);
|
|
1794
|
-
values.push(applyDecimals(v, element.decimals));
|
|
1940
|
+
values.push(validateNumberInput(input, `${key}[${index}]`));
|
|
1795
1941
|
});
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
const minCount = element.minCount ?? 0;
|
|
1799
|
-
const maxCount = element.maxCount ?? Infinity;
|
|
1800
|
-
const filteredValues = values.filter((v) => v !== null);
|
|
1801
|
-
if (element.required && filteredValues.length === 0) {
|
|
1802
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
1803
|
-
}
|
|
1804
|
-
if (filteredValues.length < minCount) {
|
|
1805
|
-
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
1806
|
-
}
|
|
1807
|
-
if (filteredValues.length > maxCount) {
|
|
1808
|
-
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1942
|
+
const filledCount = values.filter((v) => v !== null).length;
|
|
1943
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
1811
1944
|
return { value: values, errors };
|
|
1812
1945
|
} else {
|
|
1813
1946
|
const input = scopeRoot.querySelector(`[name="${key}"]`);
|
|
1814
|
-
|
|
1815
|
-
const { state } = context;
|
|
1816
|
-
if (!skipValidation && element.required && raw === "") {
|
|
1947
|
+
if (element.required && (input?.value ?? "") === "") {
|
|
1817
1948
|
const msg = t("required", state);
|
|
1818
1949
|
errors.push(`${key}: ${msg}`);
|
|
1819
|
-
|
|
1820
|
-
return { value: null, errors };
|
|
1821
|
-
}
|
|
1822
|
-
if (raw === "") {
|
|
1823
|
-
markValidity(input, null);
|
|
1824
|
-
return { value: null, errors };
|
|
1825
|
-
}
|
|
1826
|
-
const v = parseFloat(raw);
|
|
1827
|
-
if (!skipValidation && !Number.isFinite(v)) {
|
|
1828
|
-
const msg = t("notANumber", state);
|
|
1829
|
-
errors.push(`${key}: ${msg}`);
|
|
1830
|
-
markValidity(input, msg);
|
|
1950
|
+
markFieldValidity(input, msg, context);
|
|
1831
1951
|
return { value: null, errors };
|
|
1832
1952
|
}
|
|
1833
|
-
|
|
1834
|
-
return { value:
|
|
1953
|
+
if (!input) return { value: null, errors };
|
|
1954
|
+
return { value: validateNumberInput(input, key), errors };
|
|
1835
1955
|
}
|
|
1836
1956
|
}
|
|
1837
1957
|
function applyDecimals(v, decimals) {
|
|
@@ -1853,8 +1973,6 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1853
1973
|
inputs.forEach((input, index) => {
|
|
1854
1974
|
if (index < value.length) {
|
|
1855
1975
|
input.value = value[index] != null ? String(value[index]) : "";
|
|
1856
|
-
input.classList.remove("invalid");
|
|
1857
|
-
input.title = "";
|
|
1858
1976
|
clearFieldError(input);
|
|
1859
1977
|
}
|
|
1860
1978
|
});
|
|
@@ -1869,14 +1987,44 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1869
1987
|
);
|
|
1870
1988
|
if (input) {
|
|
1871
1989
|
input.value = value != null ? String(value) : "";
|
|
1872
|
-
input.classList.remove("invalid");
|
|
1873
|
-
input.title = "";
|
|
1874
1990
|
clearFieldError(input);
|
|
1875
1991
|
}
|
|
1876
1992
|
}
|
|
1877
1993
|
}
|
|
1878
1994
|
|
|
1879
1995
|
// src/components/select.ts
|
|
1996
|
+
function appendSelectOptions(select, element, selectedValue, state) {
|
|
1997
|
+
const options = element.options || [];
|
|
1998
|
+
if (!options.some((option) => option.value === "")) {
|
|
1999
|
+
const emptyOption = document.createElement("option");
|
|
2000
|
+
emptyOption.value = "";
|
|
2001
|
+
emptyOption.textContent = element.placeholder ?? t("selectPlaceholder", state);
|
|
2002
|
+
select.appendChild(emptyOption);
|
|
2003
|
+
}
|
|
2004
|
+
const strSelected = selectedValue == null ? null : String(selectedValue);
|
|
2005
|
+
let anySelected = false;
|
|
2006
|
+
options.forEach((option) => {
|
|
2007
|
+
const optionEl = document.createElement("option");
|
|
2008
|
+
optionEl.value = option.value;
|
|
2009
|
+
optionEl.textContent = option.label;
|
|
2010
|
+
if (strSelected === option.value) {
|
|
2011
|
+
optionEl.selected = true;
|
|
2012
|
+
anySelected = true;
|
|
2013
|
+
}
|
|
2014
|
+
select.appendChild(optionEl);
|
|
2015
|
+
});
|
|
2016
|
+
if (!anySelected && strSelected !== null && strSelected !== "") {
|
|
2017
|
+
console.warn(
|
|
2018
|
+
`select "${element.key}": prefill value "${strSelected}" is not among the options; leaving the field unselected`
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
if (!anySelected) {
|
|
2022
|
+
const empty = Array.from(select.options).find(
|
|
2023
|
+
(option) => option.value === ""
|
|
2024
|
+
);
|
|
2025
|
+
if (empty) empty.selected = true;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
1880
2028
|
function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
1881
2029
|
const state = ctx.state;
|
|
1882
2030
|
const readonly = isElementReadonly(element, state, ctx);
|
|
@@ -1889,18 +2037,18 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1889
2037
|
`;
|
|
1890
2038
|
selectInput.name = pathKey;
|
|
1891
2039
|
selectInput.disabled = readonly;
|
|
1892
|
-
(
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
}
|
|
1899
|
-
selectInput.appendChild(optionEl);
|
|
1900
|
-
});
|
|
2040
|
+
appendSelectOptions(
|
|
2041
|
+
selectInput,
|
|
2042
|
+
element,
|
|
2043
|
+
ctx.prefill[element.key] ?? element.default,
|
|
2044
|
+
state
|
|
2045
|
+
);
|
|
1901
2046
|
if (!readonly && ctx.instance) {
|
|
1902
2047
|
const handleChange = () => {
|
|
1903
|
-
ctx.instance.triggerOnChange(
|
|
2048
|
+
ctx.instance.triggerOnChange(
|
|
2049
|
+
pathKey,
|
|
2050
|
+
selectInput.value === "" ? null : selectInput.value
|
|
2051
|
+
);
|
|
1904
2052
|
};
|
|
1905
2053
|
selectInput.addEventListener("change", handleChange);
|
|
1906
2054
|
}
|
|
@@ -1920,7 +2068,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1920
2068
|
const minCount = element.minCount ?? (element.required ? 1 : 0);
|
|
1921
2069
|
const maxCount = element.maxCount ?? Infinity;
|
|
1922
2070
|
while (values.length < minCount) {
|
|
1923
|
-
values.push(element.default
|
|
2071
|
+
values.push(element.default ?? "");
|
|
1924
2072
|
}
|
|
1925
2073
|
const container = document.createElement("div");
|
|
1926
2074
|
container.className = "fb-row";
|
|
@@ -1945,15 +2093,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1945
2093
|
font-family: var(--fb-font-family);
|
|
1946
2094
|
`;
|
|
1947
2095
|
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
|
-
});
|
|
2096
|
+
appendSelectOptions(selectInput, element, value, state);
|
|
1957
2097
|
if (!readonly && ctx.instance) {
|
|
1958
2098
|
const handleChange = () => {
|
|
1959
2099
|
ctx.instance.triggerOnChange(selectInput.name, selectInput.value);
|
|
@@ -2006,7 +2146,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
2006
2146
|
const handle = createAddItemRow(
|
|
2007
2147
|
"select",
|
|
2008
2148
|
() => {
|
|
2009
|
-
const defaultValue = element.default
|
|
2149
|
+
const defaultValue = element.default ?? "";
|
|
2010
2150
|
values.push(defaultValue);
|
|
2011
2151
|
addSelectItem(defaultValue);
|
|
2012
2152
|
updateAddButton();
|
|
@@ -2034,55 +2174,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
2034
2174
|
}
|
|
2035
2175
|
function validateSelectElement(element, key, context) {
|
|
2036
2176
|
const errors = [];
|
|
2037
|
-
const { scopeRoot
|
|
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
|
-
const validateMultipleCount = (key2, values, element2, filterFn) => {
|
|
2071
|
-
if (skipValidation) return;
|
|
2072
|
-
const { state } = context;
|
|
2073
|
-
const filteredValues = values.filter(filterFn);
|
|
2074
|
-
const minCount = "minCount" in element2 ? element2.minCount ?? 0 : 0;
|
|
2075
|
-
const maxCount = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
|
|
2076
|
-
if (element2.required && filteredValues.length === 0) {
|
|
2077
|
-
errors.push(`${key2}: ${t("required", state)}`);
|
|
2078
|
-
}
|
|
2079
|
-
if (filteredValues.length < minCount) {
|
|
2080
|
-
errors.push(`${key2}: ${t("minItems", state, { min: minCount })}`);
|
|
2081
|
-
}
|
|
2082
|
-
if (filteredValues.length > maxCount) {
|
|
2083
|
-
errors.push(`${key2}: ${t("maxItems", state, { max: maxCount })}`);
|
|
2084
|
-
}
|
|
2085
|
-
};
|
|
2177
|
+
const { scopeRoot } = context;
|
|
2086
2178
|
if ("multiple" in element && element.multiple) {
|
|
2087
2179
|
const inputs = scopeRoot.querySelectorAll(
|
|
2088
2180
|
`[name^="${key}\\["]`
|
|
@@ -2090,27 +2182,36 @@ function validateSelectElement(element, key, context) {
|
|
|
2090
2182
|
const values = [];
|
|
2091
2183
|
inputs.forEach((input) => {
|
|
2092
2184
|
const val = input?.value ?? "";
|
|
2093
|
-
values.push(val);
|
|
2094
|
-
|
|
2185
|
+
values.push(val === "" ? null : val);
|
|
2186
|
+
markFieldValidity(input, null, context);
|
|
2095
2187
|
});
|
|
2096
|
-
|
|
2188
|
+
const filledCount = values.filter((v) => v != null).length;
|
|
2189
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
2097
2190
|
return { value: values, errors };
|
|
2098
2191
|
} else {
|
|
2099
|
-
const input = scopeRoot.querySelector(
|
|
2100
|
-
`[name="${key}"]`
|
|
2101
|
-
);
|
|
2192
|
+
const input = scopeRoot.querySelector(`[name="${key}"]`);
|
|
2102
2193
|
const val = input?.value ?? "";
|
|
2103
|
-
if (
|
|
2194
|
+
if (element.required && val === "") {
|
|
2104
2195
|
const msg = t("required", context.state);
|
|
2105
2196
|
errors.push(`${key}: ${msg}`);
|
|
2106
|
-
|
|
2197
|
+
markFieldValidity(input, msg, context);
|
|
2107
2198
|
return { value: null, errors };
|
|
2108
|
-
} else {
|
|
2109
|
-
markValidity(input, null);
|
|
2110
2199
|
}
|
|
2200
|
+
markFieldValidity(input, null, context);
|
|
2111
2201
|
return { value: val === "" ? null : val, errors };
|
|
2112
2202
|
}
|
|
2113
2203
|
}
|
|
2204
|
+
function assertValueInOptions(select, strValue, fieldPath) {
|
|
2205
|
+
if (strValue === "") return;
|
|
2206
|
+
const match = Array.from(select.options).some(
|
|
2207
|
+
(option) => option.value === strValue
|
|
2208
|
+
);
|
|
2209
|
+
if (!match) {
|
|
2210
|
+
throw new Error(
|
|
2211
|
+
`updateSelectField: value "${strValue}" is not among the options of "${fieldPath}"`
|
|
2212
|
+
);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2114
2215
|
function updateSelectField(element, fieldPath, value, context) {
|
|
2115
2216
|
const { scopeRoot } = context;
|
|
2116
2217
|
if ("multiple" in element && element.multiple) {
|
|
@@ -2125,13 +2226,18 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
2125
2226
|
);
|
|
2126
2227
|
selects.forEach((select, index) => {
|
|
2127
2228
|
if (index < value.length) {
|
|
2128
|
-
|
|
2229
|
+
const strValue = value[index] != null ? String(value[index]) : "";
|
|
2230
|
+
assertValueInOptions(select, strValue, `${fieldPath}[${index}]`);
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2233
|
+
selects.forEach((select, index) => {
|
|
2234
|
+
if (index < value.length) {
|
|
2235
|
+
const strValue = value[index] != null ? String(value[index]) : "";
|
|
2236
|
+
select.value = strValue;
|
|
2129
2237
|
const options = select.querySelectorAll("option");
|
|
2130
2238
|
options.forEach((option) => {
|
|
2131
|
-
option.selected = option.value ===
|
|
2239
|
+
option.selected = option.value === strValue;
|
|
2132
2240
|
});
|
|
2133
|
-
select.classList.remove("invalid");
|
|
2134
|
-
select.title = "";
|
|
2135
2241
|
clearFieldError(select);
|
|
2136
2242
|
}
|
|
2137
2243
|
});
|
|
@@ -2145,13 +2251,13 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
2145
2251
|
`[name="${fieldPath}"]`
|
|
2146
2252
|
);
|
|
2147
2253
|
if (select) {
|
|
2148
|
-
|
|
2254
|
+
const strValue = value != null ? String(value) : "";
|
|
2255
|
+
assertValueInOptions(select, strValue, fieldPath);
|
|
2256
|
+
select.value = strValue;
|
|
2149
2257
|
const options = select.querySelectorAll("option");
|
|
2150
2258
|
options.forEach((option) => {
|
|
2151
|
-
option.selected = option.value ===
|
|
2259
|
+
option.selected = option.value === strValue;
|
|
2152
2260
|
});
|
|
2153
|
-
select.classList.remove("invalid");
|
|
2154
|
-
select.title = "";
|
|
2155
2261
|
clearFieldError(select);
|
|
2156
2262
|
}
|
|
2157
2263
|
}
|
|
@@ -2351,7 +2457,7 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
|
2351
2457
|
const minCount = element.minCount ?? (element.required ? 1 : 0);
|
|
2352
2458
|
const maxCount = element.maxCount ?? Infinity;
|
|
2353
2459
|
while (values.length < minCount) {
|
|
2354
|
-
values.push(element.default
|
|
2460
|
+
values.push(element.default ?? "");
|
|
2355
2461
|
}
|
|
2356
2462
|
const readonly = isElementReadonly(element, state, ctx);
|
|
2357
2463
|
const container = document.createElement("div");
|
|
@@ -2445,7 +2551,7 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
|
2445
2551
|
const handle = createAddItemRow(
|
|
2446
2552
|
"switcher",
|
|
2447
2553
|
() => {
|
|
2448
|
-
const defaultValue = element.default
|
|
2554
|
+
const defaultValue = element.default ?? "";
|
|
2449
2555
|
values.push(defaultValue);
|
|
2450
2556
|
addSwitcherItem(defaultValue);
|
|
2451
2557
|
updateAddButton();
|
|
@@ -2462,69 +2568,22 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
|
2462
2568
|
if (addUpdate) addUpdate(values.length, maxCount);
|
|
2463
2569
|
}
|
|
2464
2570
|
values.forEach((value) => addSwitcherItem(value));
|
|
2465
|
-
updateAddButton();
|
|
2466
|
-
updateRemoveButtons();
|
|
2467
|
-
if (!readonly) {
|
|
2468
|
-
const hint = document.createElement("p");
|
|
2469
|
-
hint.className = "text-xs text-gray-500 mt-1";
|
|
2470
|
-
hint.textContent = makeFieldHint(element, state);
|
|
2471
|
-
wrapper.appendChild(hint);
|
|
2472
|
-
}
|
|
2473
|
-
}
|
|
2474
|
-
function validateSwitcherElement(element, key, context) {
|
|
2475
|
-
const errors = [];
|
|
2476
|
-
const { scopeRoot,
|
|
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
|
-
const validateMultipleCount = (fieldKey, values, el, filterFn) => {
|
|
2510
|
-
if (skipValidation) return;
|
|
2511
|
-
const { state } = context;
|
|
2512
|
-
const filteredValues = values.filter(filterFn);
|
|
2513
|
-
const minCount = "minCount" in el ? el.minCount ?? 0 : 0;
|
|
2514
|
-
const maxCount = "maxCount" in el ? el.maxCount ?? Infinity : Infinity;
|
|
2515
|
-
if (el.required && filteredValues.length === 0) {
|
|
2516
|
-
errors.push(`${fieldKey}: ${t("required", state)}`);
|
|
2517
|
-
}
|
|
2518
|
-
if (filteredValues.length < minCount) {
|
|
2519
|
-
errors.push(`${fieldKey}: ${t("minItems", state, { min: minCount })}`);
|
|
2520
|
-
}
|
|
2521
|
-
if (filteredValues.length > maxCount) {
|
|
2522
|
-
errors.push(`${fieldKey}: ${t("maxItems", state, { max: maxCount })}`);
|
|
2523
|
-
}
|
|
2524
|
-
};
|
|
2571
|
+
updateAddButton();
|
|
2572
|
+
updateRemoveButtons();
|
|
2573
|
+
if (!readonly) {
|
|
2574
|
+
const hint = document.createElement("p");
|
|
2575
|
+
hint.className = "text-xs text-gray-500 mt-1";
|
|
2576
|
+
hint.textContent = makeFieldHint(element, state);
|
|
2577
|
+
wrapper.appendChild(hint);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
function validateSwitcherElement(element, key, context) {
|
|
2581
|
+
const errors = [];
|
|
2582
|
+
const { scopeRoot, state } = context;
|
|
2525
2583
|
const validOptionValues = new Set(
|
|
2526
2584
|
"options" in element ? element.options.map((o) => o.value) : []
|
|
2527
2585
|
);
|
|
2586
|
+
const optionError = (val) => val !== "" && !validOptionValues.has(val) ? t("invalidOption", state) : null;
|
|
2528
2587
|
if ("multiple" in element && element.multiple) {
|
|
2529
2588
|
const inputs = scopeRoot.querySelectorAll(
|
|
2530
2589
|
`input[type="hidden"][name^="${key}\\["]`
|
|
@@ -2532,38 +2591,37 @@ function validateSwitcherElement(element, key, context) {
|
|
|
2532
2591
|
const values = [];
|
|
2533
2592
|
inputs.forEach((input) => {
|
|
2534
2593
|
const val = input?.value ?? "";
|
|
2535
|
-
values.push(val);
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
errors.push(`${key}: ${msg}`);
|
|
2540
|
-
} else {
|
|
2541
|
-
markValidity(input, null);
|
|
2542
|
-
}
|
|
2594
|
+
values.push(val === "" ? null : val);
|
|
2595
|
+
const msg = optionError(val);
|
|
2596
|
+
if (msg !== null) errors.push(`${key}: ${msg}`);
|
|
2597
|
+
markFieldValidity(switcherGroupOf(input), msg, context);
|
|
2543
2598
|
});
|
|
2544
|
-
|
|
2599
|
+
const filledCount = values.filter((v) => v != null).length;
|
|
2600
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
2545
2601
|
return { value: values, errors };
|
|
2546
2602
|
} else {
|
|
2547
2603
|
const input = scopeRoot.querySelector(
|
|
2548
2604
|
`input[type="hidden"][name="${key}"]`
|
|
2549
2605
|
);
|
|
2550
2606
|
const val = input?.value ?? "";
|
|
2551
|
-
|
|
2552
|
-
|
|
2607
|
+
const msg = element.required && val === "" ? t("required", state) : optionError(val);
|
|
2608
|
+
if (input) markFieldValidity(switcherGroupOf(input), msg, context);
|
|
2609
|
+
if (msg !== null) {
|
|
2553
2610
|
errors.push(`${key}: ${msg}`);
|
|
2554
|
-
markValidity(input, msg);
|
|
2555
2611
|
return { value: null, errors };
|
|
2556
2612
|
}
|
|
2557
|
-
if (!skipValidation && val !== "" && !validOptionValues.has(val)) {
|
|
2558
|
-
const msg = t("invalidOption", context.state);
|
|
2559
|
-
errors.push(`${key}: ${msg}`);
|
|
2560
|
-
markValidity(input, msg);
|
|
2561
|
-
return { value: null, errors };
|
|
2562
|
-
}
|
|
2563
|
-
markValidity(input, null);
|
|
2564
2613
|
return { value: val === "" ? null : val, errors };
|
|
2565
2614
|
}
|
|
2566
2615
|
}
|
|
2616
|
+
function switcherGroupOf(input) {
|
|
2617
|
+
const group = input.parentElement?.querySelector(".fb-switcher-group");
|
|
2618
|
+
if (!group) {
|
|
2619
|
+
throw new Error(
|
|
2620
|
+
`switcher "${input.name}": no .fb-switcher-group next to its hidden input`
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
return group;
|
|
2624
|
+
}
|
|
2567
2625
|
function updateSwitcherField(element, fieldPath, value, context) {
|
|
2568
2626
|
const { scopeRoot } = context;
|
|
2569
2627
|
if ("multiple" in element && element.multiple) {
|
|
@@ -2591,9 +2649,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2591
2649
|
}
|
|
2592
2650
|
});
|
|
2593
2651
|
}
|
|
2594
|
-
input
|
|
2595
|
-
input.title = "";
|
|
2596
|
-
clearFieldError(input);
|
|
2652
|
+
clearFieldError(switcherGroupOf(input));
|
|
2597
2653
|
}
|
|
2598
2654
|
});
|
|
2599
2655
|
if (value.length !== inputs.length) {
|
|
@@ -2619,9 +2675,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2619
2675
|
}
|
|
2620
2676
|
});
|
|
2621
2677
|
}
|
|
2622
|
-
input
|
|
2623
|
-
input.title = "";
|
|
2624
|
-
clearFieldError(input);
|
|
2678
|
+
clearFieldError(switcherGroupOf(input));
|
|
2625
2679
|
}
|
|
2626
2680
|
}
|
|
2627
2681
|
}
|
|
@@ -2732,6 +2786,7 @@ function renderBooleanElement(element, ctx, wrapper, pathKey) {
|
|
|
2732
2786
|
const hiddenInput = document.createElement("input");
|
|
2733
2787
|
hiddenInput.type = "hidden";
|
|
2734
2788
|
hiddenInput.name = pathKey;
|
|
2789
|
+
hiddenInput.setAttribute("data-boolean-field", "true");
|
|
2735
2790
|
hiddenInput.value = initial ? "true" : "false";
|
|
2736
2791
|
const row = document.createElement("div");
|
|
2737
2792
|
row.className = "fb-toggle-row";
|
|
@@ -3142,14 +3197,20 @@ function ensureFileStyles() {
|
|
|
3142
3197
|
padding: 6px;
|
|
3143
3198
|
}
|
|
3144
3199
|
|
|
3145
|
-
/* \u2500\u2500\u2500
|
|
3146
|
-
.fb-
|
|
3200
|
+
/* \u2500\u2500\u2500 Footer row below multi grid: N/max counter + clear-all \u2500\u2500\u2500 */
|
|
3201
|
+
.fb-multi-footer {
|
|
3147
3202
|
margin-top: 10px;
|
|
3148
3203
|
display: flex;
|
|
3149
3204
|
align-items: center;
|
|
3150
|
-
|
|
3205
|
+
gap: 8px;
|
|
3206
|
+
}
|
|
3207
|
+
.fb-files-counter {
|
|
3208
|
+
font-size: var(--fb-font-size-small, 12px);
|
|
3209
|
+
color: var(--fb-text-secondary-color, #6b7280);
|
|
3210
|
+
font-variant-numeric: tabular-nums;
|
|
3151
3211
|
}
|
|
3152
3212
|
.fb-clear-all-btn {
|
|
3213
|
+
margin-left: auto;
|
|
3153
3214
|
font-size: 12px;
|
|
3154
3215
|
color: #94a3b8;
|
|
3155
3216
|
background: none;
|
|
@@ -3399,22 +3460,54 @@ function createFileTile() {
|
|
|
3399
3460
|
tile.className = "fb-tile";
|
|
3400
3461
|
return tile;
|
|
3401
3462
|
}
|
|
3402
|
-
function
|
|
3403
|
-
const
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3463
|
+
function fileErrorSlot(container) {
|
|
3464
|
+
const wrapper = container.closest("[data-files-wrapper]");
|
|
3465
|
+
const node = wrapper ? Array.from(wrapper.children).find(
|
|
3466
|
+
(child) => child instanceof HTMLElement && child.classList.contains("file-error-message")
|
|
3467
|
+
) ?? null : null;
|
|
3468
|
+
return { wrapper, node };
|
|
3469
|
+
}
|
|
3470
|
+
function showFileError(container, message, state, kind = "action") {
|
|
3471
|
+
const { wrapper, node: existing } = fileErrorSlot(container);
|
|
3472
|
+
if (!wrapper) return;
|
|
3473
|
+
let node = existing;
|
|
3474
|
+
if (!node) {
|
|
3475
|
+
node = createErrorNode(state, "file-error-message error-message");
|
|
3476
|
+
wrapper.appendChild(node);
|
|
3477
|
+
}
|
|
3478
|
+
setAttr(node, "data-error-kind", kind);
|
|
3479
|
+
if (node.textContent !== message) node.textContent = message;
|
|
3480
|
+
if (kind === "action") {
|
|
3481
|
+
setAttr(node, "role", "alert");
|
|
3482
|
+
unsetInvalidState(wrapper);
|
|
3483
|
+
linkDescription(wrapper, node);
|
|
3484
|
+
} else {
|
|
3485
|
+
node.removeAttribute("role");
|
|
3486
|
+
setInvalidMark(wrapper, node, state);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
function clearFileError(container, kind = "action") {
|
|
3490
|
+
const { wrapper, node } = fileErrorSlot(container);
|
|
3491
|
+
if (wrapper && node?.dataset.errorKind === kind) {
|
|
3492
|
+
clearInvalidMark(wrapper, node);
|
|
3493
|
+
}
|
|
3414
3494
|
}
|
|
3415
|
-
function
|
|
3416
|
-
const
|
|
3417
|
-
if (
|
|
3495
|
+
function markFileValidity(wrapper, message, scope) {
|
|
3496
|
+
const mark = resolveMark(wrapper, message, scope);
|
|
3497
|
+
if (mark === void 0) return;
|
|
3498
|
+
const { node } = fileErrorSlot(wrapper);
|
|
3499
|
+
if (scope.readonly) {
|
|
3500
|
+
clearInvalidMark(wrapper, node);
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
if (mark === null) {
|
|
3504
|
+
clearFileError(wrapper, "validation");
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
if (scope.draftMarks && node && node.dataset.errorKind !== "validation") {
|
|
3508
|
+
return;
|
|
3509
|
+
}
|
|
3510
|
+
showFileError(wrapper, mark, scope.state, "validation");
|
|
3418
3511
|
}
|
|
3419
3512
|
function addDeleteButton(container, state, onDelete) {
|
|
3420
3513
|
const existingOverlay = container.querySelector(".delete-overlay");
|
|
@@ -4269,7 +4362,8 @@ async function handleFileSelect(opts) {
|
|
|
4269
4362
|
const formats = allowedExtensions.join(", ");
|
|
4270
4363
|
showFileError(
|
|
4271
4364
|
container,
|
|
4272
|
-
t("invalidFileExtension", state, { name: file.name, formats })
|
|
4365
|
+
t("invalidFileExtension", state, { name: file.name, formats }),
|
|
4366
|
+
state
|
|
4273
4367
|
);
|
|
4274
4368
|
return;
|
|
4275
4369
|
}
|
|
@@ -4277,14 +4371,16 @@ async function handleFileSelect(opts) {
|
|
|
4277
4371
|
const mimes = allowedMimes.join(", ");
|
|
4278
4372
|
showFileError(
|
|
4279
4373
|
container,
|
|
4280
|
-
t("invalidFileMime", state, { name: file.name, type: file.type, mimes })
|
|
4374
|
+
t("invalidFileMime", state, { name: file.name, type: file.type, mimes }),
|
|
4375
|
+
state
|
|
4281
4376
|
);
|
|
4282
4377
|
return;
|
|
4283
4378
|
}
|
|
4284
4379
|
if (!isFileSizeAllowed(file, maxSizeMB)) {
|
|
4285
4380
|
showFileError(
|
|
4286
4381
|
container,
|
|
4287
|
-
t("fileTooLarge", state, { name: file.name, maxSize: maxSizeMB })
|
|
4382
|
+
t("fileTooLarge", state, { name: file.name, maxSize: maxSizeMB }),
|
|
4383
|
+
state
|
|
4288
4384
|
);
|
|
4289
4385
|
return;
|
|
4290
4386
|
}
|
|
@@ -4480,7 +4576,7 @@ async function runMultiFileBatch(opts, files, listEl, errorTarget) {
|
|
|
4480
4576
|
state
|
|
4481
4577
|
);
|
|
4482
4578
|
if (errorTarget) {
|
|
4483
|
-
if (errorMessage) showFileError(errorTarget, errorMessage);
|
|
4579
|
+
if (errorMessage) showFileError(errorTarget, errorMessage, state);
|
|
4484
4580
|
else clearFileError(errorTarget);
|
|
4485
4581
|
}
|
|
4486
4582
|
const handle = coordinator.beginBatch(accepted.length);
|
|
@@ -4499,11 +4595,8 @@ async function runMultiFileBatch(opts, files, listEl, errorTarget) {
|
|
|
4499
4595
|
}
|
|
4500
4596
|
const { wasLast } = handle.end();
|
|
4501
4597
|
if (wasLast) updateCallback();
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
if (combined) showFileError(errorTarget, combined);
|
|
4505
|
-
else clearFileError(errorTarget);
|
|
4506
|
-
}
|
|
4598
|
+
const combined = buildBatchErrorMessage(errorMessage, failures, state);
|
|
4599
|
+
if (errorTarget && combined) showFileError(errorTarget, combined, state);
|
|
4507
4600
|
}
|
|
4508
4601
|
function setupFilesDropHandler(opts) {
|
|
4509
4602
|
const { filesContainer } = opts;
|
|
@@ -4613,7 +4706,7 @@ async function handleLibraryPickMulti(opts) {
|
|
|
4613
4706
|
selectedResourceIds: knownRids
|
|
4614
4707
|
});
|
|
4615
4708
|
} catch (error) {
|
|
4616
|
-
showFileError(wrapper, extractPickerError(error, state));
|
|
4709
|
+
showFileError(wrapper, extractPickerError(error, state), state);
|
|
4617
4710
|
return;
|
|
4618
4711
|
}
|
|
4619
4712
|
if (picked.length === 0) return;
|
|
@@ -4641,7 +4734,8 @@ async function handleLibraryPickMulti(opts) {
|
|
|
4641
4734
|
if (skipped > 0) {
|
|
4642
4735
|
showFileError(
|
|
4643
4736
|
wrapper,
|
|
4644
|
-
t("filesLimitExceeded", state, { skipped, max: maxCount })
|
|
4737
|
+
t("filesLimitExceeded", state, { skipped, max: maxCount }),
|
|
4738
|
+
state
|
|
4645
4739
|
);
|
|
4646
4740
|
}
|
|
4647
4741
|
return;
|
|
@@ -4650,7 +4744,8 @@ async function handleLibraryPickMulti(opts) {
|
|
|
4650
4744
|
if (skipped > 0) {
|
|
4651
4745
|
showFileError(
|
|
4652
4746
|
wrapper,
|
|
4653
|
-
t("filesLimitExceeded", state, { skipped, max: maxCount })
|
|
4747
|
+
t("filesLimitExceeded", state, { skipped, max: maxCount }),
|
|
4748
|
+
state
|
|
4654
4749
|
);
|
|
4655
4750
|
}
|
|
4656
4751
|
for (const resource of accepted) {
|
|
@@ -4687,7 +4782,7 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
|
|
|
4687
4782
|
selectedResourceIds: []
|
|
4688
4783
|
});
|
|
4689
4784
|
} catch (error) {
|
|
4690
|
-
showFileError(container, extractPickerError(error, state));
|
|
4785
|
+
showFileError(container, extractPickerError(error, state), state);
|
|
4691
4786
|
return;
|
|
4692
4787
|
}
|
|
4693
4788
|
if (picked.length === 0) return;
|
|
@@ -4700,7 +4795,7 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
|
|
|
4700
4795
|
state
|
|
4701
4796
|
);
|
|
4702
4797
|
if (validationError !== null) {
|
|
4703
|
-
showFileError(container, validationError);
|
|
4798
|
+
showFileError(container, validationError, state);
|
|
4704
4799
|
return;
|
|
4705
4800
|
}
|
|
4706
4801
|
clearFileError(container);
|
|
@@ -4937,10 +5032,7 @@ function buildPlaceholderTile(isDragOver = false) {
|
|
|
4937
5032
|
div.className = `fb-multi-placeholder fb-checker${isDragOver ? " fb-drag-over" : ""}`;
|
|
4938
5033
|
return div;
|
|
4939
5034
|
}
|
|
4940
|
-
function
|
|
4941
|
-
if (ridCount <= 1) return null;
|
|
4942
|
-
const row = document.createElement("div");
|
|
4943
|
-
row.className = "fb-clear-all-row";
|
|
5035
|
+
function buildClearAllButton(state, onClearAll) {
|
|
4944
5036
|
const clearBtn = document.createElement("button");
|
|
4945
5037
|
clearBtn.type = "button";
|
|
4946
5038
|
clearBtn.className = "fb-clear-all-btn";
|
|
@@ -4951,9 +5043,38 @@ function buildClearAllRow(state, ridCount, onClearAll) {
|
|
|
4951
5043
|
onClearAll();
|
|
4952
5044
|
}
|
|
4953
5045
|
};
|
|
4954
|
-
|
|
5046
|
+
return clearBtn;
|
|
5047
|
+
}
|
|
5048
|
+
function buildFooterRow(state, ridCount, maxCount, onClearAll) {
|
|
5049
|
+
const showCounter = maxCount !== Infinity;
|
|
5050
|
+
const showClearAll = onClearAll !== void 0 && ridCount > 1;
|
|
5051
|
+
if (!showCounter && !showClearAll) return null;
|
|
5052
|
+
const row = document.createElement("div");
|
|
5053
|
+
row.className = "fb-multi-footer";
|
|
5054
|
+
if (showCounter) {
|
|
5055
|
+
const counter = document.createElement("span");
|
|
5056
|
+
counter.className = "fb-files-counter";
|
|
5057
|
+
counter.textContent = t("filesCounter", state, {
|
|
5058
|
+
count: ridCount,
|
|
5059
|
+
max: maxCount
|
|
5060
|
+
});
|
|
5061
|
+
row.appendChild(counter);
|
|
5062
|
+
}
|
|
5063
|
+
if (showClearAll) row.appendChild(buildClearAllButton(state, onClearAll));
|
|
4955
5064
|
return row;
|
|
4956
5065
|
}
|
|
5066
|
+
function syncOverLimitError(container, ridCount, maxCount, state) {
|
|
5067
|
+
if (ridCount > maxCount) {
|
|
5068
|
+
showFileError(
|
|
5069
|
+
container,
|
|
5070
|
+
t("maxFiles", state, { max: maxCount }),
|
|
5071
|
+
state,
|
|
5072
|
+
"limit"
|
|
5073
|
+
);
|
|
5074
|
+
} else {
|
|
5075
|
+
clearFileError(container, "limit");
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
4957
5078
|
var gridResizeObservers = /* @__PURE__ */ new WeakMap();
|
|
4958
5079
|
var gridMeasureFrames = /* @__PURE__ */ new WeakMap();
|
|
4959
5080
|
function cancelPendingMeasure(container) {
|
|
@@ -5046,6 +5167,7 @@ function renderResourcePills(opts) {
|
|
|
5046
5167
|
grid2.appendChild(tile);
|
|
5047
5168
|
}
|
|
5048
5169
|
}
|
|
5170
|
+
clearFileError(container, "limit");
|
|
5049
5171
|
return;
|
|
5050
5172
|
}
|
|
5051
5173
|
const outerDiv = document.createElement("div");
|
|
@@ -5125,10 +5247,14 @@ function renderResourcePills(opts) {
|
|
|
5125
5247
|
}
|
|
5126
5248
|
}
|
|
5127
5249
|
});
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5250
|
+
const footer = buildFooterRow(
|
|
5251
|
+
state,
|
|
5252
|
+
ridList.length,
|
|
5253
|
+
effectiveMax,
|
|
5254
|
+
onClearAll
|
|
5255
|
+
);
|
|
5256
|
+
if (footer) container.appendChild(footer);
|
|
5257
|
+
syncOverLimitError(container, ridList.length, effectiveMax, state);
|
|
5132
5258
|
}
|
|
5133
5259
|
function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
5134
5260
|
const state = ctx.state;
|
|
@@ -5278,8 +5404,9 @@ function buildAcceptAttribute(accept) {
|
|
|
5278
5404
|
...accept.mime ?? []
|
|
5279
5405
|
].join(",");
|
|
5280
5406
|
}
|
|
5281
|
-
function
|
|
5407
|
+
function renderMultiFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
5282
5408
|
const state = ctx.state;
|
|
5409
|
+
const maxFiles = element.maxCount ?? Infinity;
|
|
5283
5410
|
const filesWrapper = document.createElement("div");
|
|
5284
5411
|
filesWrapper.className = "fb-row";
|
|
5285
5412
|
filesWrapper.dataset.filesWrapper = pathKey;
|
|
@@ -5483,21 +5610,23 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
|
|
|
5483
5610
|
};
|
|
5484
5611
|
setupFilesDropHandler({ ...sharedHandlerOpts, filesContainer });
|
|
5485
5612
|
setupFilesPickerHandler({ ...sharedHandlerOpts, filesPicker });
|
|
5613
|
+
state.multiFileSetters.set(filesWrapper, (resourceIds) => {
|
|
5614
|
+
if (coordinator.hasInFlightBatches()) {
|
|
5615
|
+
throw new Error(
|
|
5616
|
+
`setFormData/updateField: file field "${pathKey}" has uploads in flight; set its value after they settle`
|
|
5617
|
+
);
|
|
5618
|
+
}
|
|
5619
|
+
for (const rid of initialFiles) {
|
|
5620
|
+
if (!resourceIds.includes(rid)) {
|
|
5621
|
+
releaseLocalFileUrl(state.resourceIndex.get(rid)?.file);
|
|
5622
|
+
}
|
|
5623
|
+
}
|
|
5624
|
+
initialFiles.splice(0, initialFiles.length, ...resourceIds);
|
|
5625
|
+
updateFilesDisplay();
|
|
5626
|
+
});
|
|
5486
5627
|
updateFilesDisplay();
|
|
5487
5628
|
wrapper.appendChild(filesWrapper);
|
|
5488
5629
|
}
|
|
5489
|
-
function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
|
|
5490
|
-
setupMultiFileEditMode(element, ctx, wrapper, pathKey, Infinity);
|
|
5491
|
-
}
|
|
5492
|
-
function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
5493
|
-
setupMultiFileEditMode(
|
|
5494
|
-
element,
|
|
5495
|
-
ctx,
|
|
5496
|
-
wrapper,
|
|
5497
|
-
pathKey,
|
|
5498
|
-
element.maxCount ?? Infinity
|
|
5499
|
-
);
|
|
5500
|
-
}
|
|
5501
5630
|
|
|
5502
5631
|
// src/components/file/validate.ts
|
|
5503
5632
|
function readMultiFileResourceIds(scopeRoot, fullKey) {
|
|
@@ -5519,86 +5648,100 @@ function readMultiFileResourceIds(scopeRoot, fullKey) {
|
|
|
5519
5648
|
}
|
|
5520
5649
|
return parsed;
|
|
5521
5650
|
}
|
|
5522
|
-
function
|
|
5523
|
-
const
|
|
5524
|
-
const maxFiles = "maxCount" in element ? element.maxCount ?? Infinity : Infinity;
|
|
5525
|
-
if (element.required && resourceIds.length === 0) {
|
|
5526
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
5527
|
-
}
|
|
5528
|
-
if (resourceIds.length < minFiles) {
|
|
5529
|
-
errors.push(`${key}: ${t("minFiles", state, { min: minFiles })}`);
|
|
5530
|
-
}
|
|
5531
|
-
if (resourceIds.length > maxFiles) {
|
|
5532
|
-
errors.push(`${key}: ${t("maxFiles", state, { max: maxFiles })}`);
|
|
5533
|
-
}
|
|
5534
|
-
}
|
|
5535
|
-
function validateFileTypes(key, resourceIds, element, state, errors) {
|
|
5651
|
+
function validateFileTypes(resourceIds, element, state) {
|
|
5652
|
+
const messages = [];
|
|
5536
5653
|
const acceptField = "accept" in element ? element.accept : void 0;
|
|
5537
5654
|
const allowedExtensions = getAllowedExtensions(acceptField);
|
|
5538
5655
|
const allowedMimes = getAllowedMimes(acceptField);
|
|
5539
|
-
if (allowedExtensions.length === 0 && allowedMimes.length === 0)
|
|
5656
|
+
if (allowedExtensions.length === 0 && allowedMimes.length === 0) {
|
|
5657
|
+
return messages;
|
|
5658
|
+
}
|
|
5540
5659
|
const formats = allowedExtensions.join(", ");
|
|
5541
5660
|
const mimes = allowedMimes.join(", ");
|
|
5542
5661
|
for (const rid of resourceIds) {
|
|
5543
5662
|
const meta = state.resourceIndex.get(rid);
|
|
5544
5663
|
const fileName = meta?.name ?? rid;
|
|
5545
5664
|
if (allowedExtensions.length > 0 && !isFileExtensionAllowed(fileName, allowedExtensions)) {
|
|
5546
|
-
|
|
5547
|
-
|
|
5665
|
+
messages.push(
|
|
5666
|
+
t("invalidFileExtension", state, { name: fileName, formats })
|
|
5548
5667
|
);
|
|
5549
5668
|
continue;
|
|
5550
5669
|
}
|
|
5551
5670
|
if (allowedMimes.length > 0 && !meta?.inferredFromExtension) {
|
|
5552
5671
|
const mimeType = meta?.type ?? "";
|
|
5553
5672
|
if (!isMimeAllowed(mimeType, allowedMimes)) {
|
|
5554
|
-
|
|
5555
|
-
|
|
5673
|
+
messages.push(
|
|
5674
|
+
t("invalidFileMime", state, {
|
|
5675
|
+
name: fileName,
|
|
5676
|
+
type: mimeType,
|
|
5677
|
+
mimes
|
|
5678
|
+
})
|
|
5556
5679
|
);
|
|
5557
5680
|
}
|
|
5558
5681
|
}
|
|
5559
5682
|
}
|
|
5683
|
+
return messages;
|
|
5560
5684
|
}
|
|
5561
|
-
function validateFileSizes(
|
|
5685
|
+
function validateFileSizes(resourceIds, element, state) {
|
|
5686
|
+
const messages = [];
|
|
5562
5687
|
const maxSizeMB = "maxSize" in element ? element.maxSize ?? Infinity : Infinity;
|
|
5563
|
-
if (maxSizeMB === Infinity) return;
|
|
5688
|
+
if (maxSizeMB === Infinity) return messages;
|
|
5564
5689
|
for (const rid of resourceIds) {
|
|
5565
5690
|
const meta = state.resourceIndex.get(rid);
|
|
5566
5691
|
if (!meta) continue;
|
|
5567
5692
|
if (meta.size > maxSizeMB * 1024 * 1024) {
|
|
5568
|
-
|
|
5569
|
-
|
|
5693
|
+
messages.push(
|
|
5694
|
+
t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })
|
|
5570
5695
|
);
|
|
5571
5696
|
}
|
|
5572
5697
|
}
|
|
5698
|
+
return messages;
|
|
5699
|
+
}
|
|
5700
|
+
function reportFileMessages(scopeRoot, wrapperKey, key, messages, context) {
|
|
5701
|
+
const wrapper = scopeRoot.querySelector(
|
|
5702
|
+
`[data-files-wrapper="${wrapperKey}"]`
|
|
5703
|
+
);
|
|
5704
|
+
if (wrapper) {
|
|
5705
|
+
markFileValidity(wrapper, joinErrorMessages(messages), context);
|
|
5706
|
+
}
|
|
5707
|
+
return messages.map((message) => `${key}: ${message}`);
|
|
5573
5708
|
}
|
|
5574
5709
|
function validateMultiFile(element, key, context) {
|
|
5575
|
-
const { scopeRoot,
|
|
5576
|
-
const errors = [];
|
|
5710
|
+
const { scopeRoot, path, state } = context;
|
|
5577
5711
|
const fullKey = pathJoin(path, key);
|
|
5578
5712
|
const resourceIds = readMultiFileResourceIds(scopeRoot, fullKey);
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5713
|
+
const messages = [
|
|
5714
|
+
...countRuleMessages(element, resourceIds.length, state, {
|
|
5715
|
+
min: "minFiles",
|
|
5716
|
+
max: "maxFiles"
|
|
5717
|
+
}),
|
|
5718
|
+
...validateFileTypes(resourceIds, element, state),
|
|
5719
|
+
...validateFileSizes(resourceIds, element, state)
|
|
5720
|
+
];
|
|
5721
|
+
return {
|
|
5722
|
+
value: resourceIds,
|
|
5723
|
+
errors: reportFileMessages(scopeRoot, fullKey, key, messages, context)
|
|
5724
|
+
};
|
|
5585
5725
|
}
|
|
5586
5726
|
function validateSingleFile(element, key, context) {
|
|
5587
|
-
const { scopeRoot,
|
|
5588
|
-
const errors = [];
|
|
5727
|
+
const { scopeRoot, state } = context;
|
|
5589
5728
|
const input = scopeRoot.querySelector(
|
|
5590
5729
|
`input[name="${key}"][type="hidden"]`
|
|
5591
5730
|
);
|
|
5592
5731
|
const rid = input?.value ?? "";
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
}
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5732
|
+
let messages = [];
|
|
5733
|
+
if (element.required && rid === "") {
|
|
5734
|
+
messages = [t("required", state)];
|
|
5735
|
+
} else if (rid !== "") {
|
|
5736
|
+
messages = [
|
|
5737
|
+
...validateFileTypes([rid], element, state),
|
|
5738
|
+
...validateFileSizes([rid], element, state)
|
|
5739
|
+
];
|
|
5600
5740
|
}
|
|
5601
|
-
return {
|
|
5741
|
+
return {
|
|
5742
|
+
value: rid || null,
|
|
5743
|
+
errors: reportFileMessages(scopeRoot, key, key, messages, context)
|
|
5744
|
+
};
|
|
5602
5745
|
}
|
|
5603
5746
|
function validateFileElement(element, key, context) {
|
|
5604
5747
|
const isMultipleField = element.type === "files" || "multiple" in element && Boolean(element.multiple);
|
|
@@ -5648,12 +5791,20 @@ function buildEmptyReadonlyTile(state) {
|
|
|
5648
5791
|
return emptyState;
|
|
5649
5792
|
}
|
|
5650
5793
|
function renderMultiFileReadonly(rids, state, wrapper, pathKey, _marginTop) {
|
|
5651
|
-
addPrefillFilesToIndex(rids, state.resourceIndex);
|
|
5652
|
-
ensureFileStyles();
|
|
5653
5794
|
const filesWrapper = document.createElement("div");
|
|
5654
5795
|
filesWrapper.dataset.filesWrapper = pathKey;
|
|
5655
|
-
filesWrapper.dataset.resourceIds = JSON.stringify(rids);
|
|
5656
5796
|
wrapper.appendChild(filesWrapper);
|
|
5797
|
+
state.multiFileSetters.set(
|
|
5798
|
+
filesWrapper,
|
|
5799
|
+
(resourceIds) => fillReadonlyGrid(resourceIds, state, filesWrapper)
|
|
5800
|
+
);
|
|
5801
|
+
fillReadonlyGrid(rids, state, filesWrapper);
|
|
5802
|
+
}
|
|
5803
|
+
function fillReadonlyGrid(rids, state, filesWrapper) {
|
|
5804
|
+
addPrefillFilesToIndex(rids, state.resourceIndex);
|
|
5805
|
+
ensureFileStyles();
|
|
5806
|
+
filesWrapper.dataset.resourceIds = JSON.stringify(rids);
|
|
5807
|
+
filesWrapper.replaceChildren();
|
|
5657
5808
|
if (rids.length === 0) {
|
|
5658
5809
|
const emptyEl = document.createElement("div");
|
|
5659
5810
|
emptyEl.className = "fb-tile-empty-text";
|
|
@@ -5713,14 +5864,14 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
|
5713
5864
|
if (isElementReadonly(element, ctx.state, ctx)) {
|
|
5714
5865
|
renderFilesElementReadonly(element, ctx, wrapper, pathKey);
|
|
5715
5866
|
} else {
|
|
5716
|
-
|
|
5867
|
+
renderMultiFileElementEdit(element, ctx, wrapper, pathKey);
|
|
5717
5868
|
}
|
|
5718
5869
|
}
|
|
5719
5870
|
function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
|
|
5720
5871
|
if (isElementReadonly(element, ctx.state, ctx)) {
|
|
5721
5872
|
renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey);
|
|
5722
5873
|
} else {
|
|
5723
|
-
|
|
5874
|
+
renderMultiFileElementEdit(element, ctx, wrapper, pathKey);
|
|
5724
5875
|
}
|
|
5725
5876
|
}
|
|
5726
5877
|
function updateFileField(element, fieldPath, value, context) {
|
|
@@ -5740,13 +5891,19 @@ function updateFileField(element, fieldPath, value, context) {
|
|
|
5740
5891
|
const filesWrapper = scopeRoot.querySelector(
|
|
5741
5892
|
`[data-files-wrapper="${fieldPath}"]`
|
|
5742
5893
|
);
|
|
5743
|
-
if (filesWrapper) {
|
|
5744
|
-
filesWrapper.dataset.resourceIds = JSON.stringify(value);
|
|
5745
|
-
} else {
|
|
5894
|
+
if (!filesWrapper) {
|
|
5746
5895
|
console.warn(
|
|
5747
5896
|
`updateFileField: [data-files-wrapper="${fieldPath}"] not found in DOM; data-resource-ids not updated`
|
|
5748
5897
|
);
|
|
5898
|
+
return;
|
|
5899
|
+
}
|
|
5900
|
+
const setFiles = state.multiFileSetters.get(filesWrapper);
|
|
5901
|
+
if (!setFiles) {
|
|
5902
|
+
throw new Error(
|
|
5903
|
+
`updateFileField: [data-files-wrapper="${fieldPath}"] has no registered setter; this is a render bug`
|
|
5904
|
+
);
|
|
5749
5905
|
}
|
|
5906
|
+
setFiles(value);
|
|
5750
5907
|
} else {
|
|
5751
5908
|
const hiddenInput = scopeRoot.querySelector(
|
|
5752
5909
|
`input[name="${fieldPath}"][type="hidden"]`
|
|
@@ -5807,7 +5964,7 @@ function createReadonlyColourUI(value) {
|
|
|
5807
5964
|
container.appendChild(hexText);
|
|
5808
5965
|
return container;
|
|
5809
5966
|
}
|
|
5810
|
-
function createEditColourUI(value, pathKey, ctx) {
|
|
5967
|
+
function createEditColourUI(value, pathKey, ctx, placeholder) {
|
|
5811
5968
|
const normalizedValue = normalizeColourValue(value);
|
|
5812
5969
|
const pickerWrapper = document.createElement("div");
|
|
5813
5970
|
pickerWrapper.className = "colour-picker-wrapper";
|
|
@@ -5831,9 +5988,10 @@ function createEditColourUI(value, pathKey, ctx) {
|
|
|
5831
5988
|
const hexInput = document.createElement("input");
|
|
5832
5989
|
hexInput.type = "text";
|
|
5833
5990
|
hexInput.className = "colour-hex-input";
|
|
5991
|
+
hexInput.setAttribute("data-colour-field", "true");
|
|
5834
5992
|
hexInput.name = pathKey;
|
|
5835
5993
|
hexInput.value = normalizedValue;
|
|
5836
|
-
hexInput.placeholder = "#000000";
|
|
5994
|
+
hexInput.placeholder = placeholder ?? "#000000";
|
|
5837
5995
|
hexInput.style.cssText = `
|
|
5838
5996
|
width: 100px;
|
|
5839
5997
|
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
@@ -5922,12 +6080,17 @@ function createEditColourUI(value, pathKey, ctx) {
|
|
|
5922
6080
|
function renderColourElement(element, ctx, wrapper, pathKey) {
|
|
5923
6081
|
const state = ctx.state;
|
|
5924
6082
|
const readonly = isElementReadonly(element, state, ctx);
|
|
5925
|
-
const initialValue = ctx.prefill[element.key]
|
|
6083
|
+
const initialValue = ctx.prefill[element.key] ?? element.default ?? "#000000";
|
|
5926
6084
|
if (readonly) {
|
|
5927
6085
|
const readonlyUI = createReadonlyColourUI(initialValue);
|
|
5928
6086
|
wrapper.appendChild(readonlyUI);
|
|
5929
6087
|
} else {
|
|
5930
|
-
const editUI = createEditColourUI(
|
|
6088
|
+
const editUI = createEditColourUI(
|
|
6089
|
+
initialValue,
|
|
6090
|
+
pathKey,
|
|
6091
|
+
ctx,
|
|
6092
|
+
element.placeholder
|
|
6093
|
+
);
|
|
5931
6094
|
wrapper.appendChild(editUI);
|
|
5932
6095
|
}
|
|
5933
6096
|
if (!readonly) {
|
|
@@ -5949,7 +6112,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
5949
6112
|
const minCount = element.minCount ?? (element.required ? 1 : 0);
|
|
5950
6113
|
const maxCount = element.maxCount ?? Infinity;
|
|
5951
6114
|
while (values.length < minCount) {
|
|
5952
|
-
values.push(element.default
|
|
6115
|
+
values.push(element.default ?? "#000000");
|
|
5953
6116
|
}
|
|
5954
6117
|
const container = document.createElement("div");
|
|
5955
6118
|
container.className = "fb-row";
|
|
@@ -5973,7 +6136,12 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
5973
6136
|
}
|
|
5974
6137
|
} else {
|
|
5975
6138
|
const tempPathKey = `${pathKey}[${container.children.length}]`;
|
|
5976
|
-
const editUI = createEditColourUI(
|
|
6139
|
+
const editUI = createEditColourUI(
|
|
6140
|
+
value,
|
|
6141
|
+
tempPathKey,
|
|
6142
|
+
ctx,
|
|
6143
|
+
element.placeholder
|
|
6144
|
+
);
|
|
5977
6145
|
editUI.style.flex = "1";
|
|
5978
6146
|
itemWrapper.appendChild(editUI);
|
|
5979
6147
|
}
|
|
@@ -6035,7 +6203,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
6035
6203
|
const handle = createAddItemRow(
|
|
6036
6204
|
"colour",
|
|
6037
6205
|
() => {
|
|
6038
|
-
const defaultColour = element.default
|
|
6206
|
+
const defaultColour = element.default ?? "#000000";
|
|
6039
6207
|
values.push(defaultColour);
|
|
6040
6208
|
addColourItem(defaultColour);
|
|
6041
6209
|
updateAddButton();
|
|
@@ -6067,60 +6235,18 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
6067
6235
|
}
|
|
6068
6236
|
function validateColourElement(element, key, context) {
|
|
6069
6237
|
const errors = [];
|
|
6070
|
-
const { scopeRoot,
|
|
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
|
-
};
|
|
6238
|
+
const { scopeRoot, state } = context;
|
|
6103
6239
|
const validateColourValue = (input, val, fieldKey) => {
|
|
6104
|
-
const
|
|
6240
|
+
const normalized = val ? normalizeColourValue(val) : "";
|
|
6241
|
+
let msg = null;
|
|
6105
6242
|
if (!val) {
|
|
6106
|
-
if (
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
return "";
|
|
6114
|
-
}
|
|
6115
|
-
const normalized = normalizeColourValue(val);
|
|
6116
|
-
if (!skipValidation && !isValidHexColour(normalized)) {
|
|
6117
|
-
const msg = t("invalidHexColour", state);
|
|
6118
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
6119
|
-
markValidity(input, msg);
|
|
6120
|
-
return val;
|
|
6121
|
-
}
|
|
6122
|
-
markValidity(input, null);
|
|
6123
|
-
return normalized;
|
|
6243
|
+
if (element.required) msg = t("required", state);
|
|
6244
|
+
} else if (!isValidHexColour(normalized)) {
|
|
6245
|
+
msg = t("invalidHexColour", state);
|
|
6246
|
+
}
|
|
6247
|
+
if (msg !== null) errors.push(`${fieldKey}: ${msg}`);
|
|
6248
|
+
markFieldValidity(input, msg, context);
|
|
6249
|
+
return val && msg !== null ? val : normalized;
|
|
6124
6250
|
};
|
|
6125
6251
|
if (element.multiple) {
|
|
6126
6252
|
const hexInputs = scopeRoot.querySelectorAll(
|
|
@@ -6129,38 +6255,17 @@ function validateColourElement(element, key, context) {
|
|
|
6129
6255
|
const values = [];
|
|
6130
6256
|
hexInputs.forEach((input, index) => {
|
|
6131
6257
|
const val = input?.value ?? "";
|
|
6132
|
-
|
|
6133
|
-
values.push(validated);
|
|
6258
|
+
values.push(validateColourValue(input, val, `${key}[${index}]`));
|
|
6134
6259
|
});
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
const minCount = element.minCount ?? 0;
|
|
6138
|
-
const maxCount = element.maxCount ?? Infinity;
|
|
6139
|
-
const filteredValues = values.filter((v) => v !== "");
|
|
6140
|
-
if (element.required && filteredValues.length === 0) {
|
|
6141
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
6142
|
-
}
|
|
6143
|
-
if (filteredValues.length < minCount) {
|
|
6144
|
-
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
6145
|
-
}
|
|
6146
|
-
if (filteredValues.length > maxCount) {
|
|
6147
|
-
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
6148
|
-
}
|
|
6149
|
-
}
|
|
6260
|
+
const filledCount = values.filter((v) => v !== "").length;
|
|
6261
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
6150
6262
|
return { value: values, errors };
|
|
6151
6263
|
} else {
|
|
6152
6264
|
const hexInput = scopeRoot.querySelector(
|
|
6153
6265
|
`[name="${key}"].colour-hex-input`
|
|
6154
6266
|
);
|
|
6155
6267
|
const val = hexInput?.value ?? "";
|
|
6156
|
-
|
|
6157
|
-
const msg = t("required", context.state);
|
|
6158
|
-
errors.push(`${key}: ${msg}`);
|
|
6159
|
-
markValidity(hexInput, msg);
|
|
6160
|
-
return { value: "", errors };
|
|
6161
|
-
}
|
|
6162
|
-
const validated = validateColourValue(hexInput, val, key);
|
|
6163
|
-
return { value: validated, errors };
|
|
6268
|
+
return { value: validateColourValue(hexInput, val, key), errors };
|
|
6164
6269
|
}
|
|
6165
6270
|
}
|
|
6166
6271
|
function updateColourField(element, fieldPath, value, context) {
|
|
@@ -6179,8 +6284,6 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
6179
6284
|
if (index < value.length) {
|
|
6180
6285
|
const normalized = normalizeColourValue(value[index]);
|
|
6181
6286
|
hexInput.value = normalized;
|
|
6182
|
-
hexInput.classList.remove("invalid");
|
|
6183
|
-
hexInput.title = "";
|
|
6184
6287
|
clearFieldError(hexInput);
|
|
6185
6288
|
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
6186
6289
|
if (wrapper) {
|
|
@@ -6209,8 +6312,6 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
6209
6312
|
if (hexInput) {
|
|
6210
6313
|
const normalized = normalizeColourValue(value);
|
|
6211
6314
|
hexInput.value = normalized;
|
|
6212
|
-
hexInput.classList.remove("invalid");
|
|
6213
|
-
hexInput.title = "";
|
|
6214
6315
|
clearFieldError(hexInput);
|
|
6215
6316
|
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
6216
6317
|
if (wrapper) {
|
|
@@ -6524,7 +6625,7 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
6524
6625
|
}
|
|
6525
6626
|
function validateSliderElement(element, key, context) {
|
|
6526
6627
|
const errors = [];
|
|
6527
|
-
const { scopeRoot
|
|
6628
|
+
const { scopeRoot } = context;
|
|
6528
6629
|
if (element.min === void 0 || element.min === null) {
|
|
6529
6630
|
throw new Error(
|
|
6530
6631
|
`Slider validation: field "${key}" requires "min" property`
|
|
@@ -6539,79 +6640,24 @@ function validateSliderElement(element, key, context) {
|
|
|
6539
6640
|
const max = element.max;
|
|
6540
6641
|
const step = element.step ?? 1;
|
|
6541
6642
|
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
6643
|
const validateSliderValue = (slider, fieldKey) => {
|
|
6579
6644
|
const { state } = context;
|
|
6580
6645
|
const rawValue = slider.value;
|
|
6581
6646
|
if (!rawValue) {
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
markValidity(slider, msg);
|
|
6586
|
-
return null;
|
|
6587
|
-
}
|
|
6588
|
-
markValidity(slider, null);
|
|
6647
|
+
const msg2 = element.required ? t("required", state) : null;
|
|
6648
|
+
if (msg2 !== null) errors.push(`${fieldKey}: ${msg2}`);
|
|
6649
|
+
markFieldValidity(slider, msg2, context);
|
|
6589
6650
|
return null;
|
|
6590
6651
|
}
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
if (!skipValidation) {
|
|
6601
|
-
if (value < min) {
|
|
6602
|
-
const msg = t("minValue", state, { min });
|
|
6603
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
6604
|
-
markValidity(slider, msg);
|
|
6605
|
-
return value;
|
|
6606
|
-
}
|
|
6607
|
-
if (value > max) {
|
|
6608
|
-
const msg = t("maxValue", state, { max });
|
|
6609
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
6610
|
-
markValidity(slider, msg);
|
|
6611
|
-
return value;
|
|
6612
|
-
}
|
|
6613
|
-
}
|
|
6614
|
-
markValidity(slider, null);
|
|
6652
|
+
const value = scale === "exponential" ? alignToStep(
|
|
6653
|
+
positionToExponential(parseFloat(rawValue) / 1e3, min, max),
|
|
6654
|
+
step
|
|
6655
|
+
) : alignToStep(parseFloat(rawValue), step);
|
|
6656
|
+
let msg = null;
|
|
6657
|
+
if (value < min) msg = t("minValue", state, { min });
|
|
6658
|
+
else if (value > max) msg = t("maxValue", state, { max });
|
|
6659
|
+
if (msg !== null) errors.push(`${fieldKey}: ${msg}`);
|
|
6660
|
+
markFieldValidity(slider, msg, context);
|
|
6615
6661
|
return value;
|
|
6616
6662
|
};
|
|
6617
6663
|
if (element.multiple) {
|
|
@@ -6620,31 +6666,17 @@ function validateSliderElement(element, key, context) {
|
|
|
6620
6666
|
);
|
|
6621
6667
|
const values = [];
|
|
6622
6668
|
sliders.forEach((slider, index) => {
|
|
6623
|
-
|
|
6624
|
-
values.push(value);
|
|
6669
|
+
values.push(validateSliderValue(slider, `${key}[${index}]`));
|
|
6625
6670
|
});
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
const minCount = element.minCount ?? 0;
|
|
6629
|
-
const maxCount = element.maxCount ?? Infinity;
|
|
6630
|
-
const filteredValues = values.filter((v) => v !== null);
|
|
6631
|
-
if (element.required && filteredValues.length === 0) {
|
|
6632
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
6633
|
-
}
|
|
6634
|
-
if (filteredValues.length < minCount) {
|
|
6635
|
-
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
6636
|
-
}
|
|
6637
|
-
if (filteredValues.length > maxCount) {
|
|
6638
|
-
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
6639
|
-
}
|
|
6640
|
-
}
|
|
6671
|
+
const filledCount = values.filter((v) => v !== null).length;
|
|
6672
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
6641
6673
|
return { value: values, errors };
|
|
6642
6674
|
} else {
|
|
6643
6675
|
const slider = scopeRoot.querySelector(
|
|
6644
6676
|
`input[type="range"][name="${key}"]`
|
|
6645
6677
|
);
|
|
6646
6678
|
if (!slider) {
|
|
6647
|
-
if (
|
|
6679
|
+
if (element.required) {
|
|
6648
6680
|
errors.push(`${key}: ${t("required", context.state)}`);
|
|
6649
6681
|
}
|
|
6650
6682
|
return { value: null, errors };
|
|
@@ -6693,8 +6725,6 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
6693
6725
|
var(--fb-border-color) 100%
|
|
6694
6726
|
)`;
|
|
6695
6727
|
}
|
|
6696
|
-
slider.classList.remove("invalid");
|
|
6697
|
-
slider.title = "";
|
|
6698
6728
|
clearFieldError(slider);
|
|
6699
6729
|
}
|
|
6700
6730
|
});
|
|
@@ -6730,8 +6760,6 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
6730
6760
|
var(--fb-border-color) 100%
|
|
6731
6761
|
)`;
|
|
6732
6762
|
}
|
|
6733
|
-
slider.classList.remove("invalid");
|
|
6734
|
-
slider.title = "";
|
|
6735
6763
|
clearFieldError(slider);
|
|
6736
6764
|
}
|
|
6737
6765
|
}
|
|
@@ -6756,22 +6784,12 @@ function extractRootFormData(formRoot) {
|
|
|
6756
6784
|
inputs.forEach((input) => {
|
|
6757
6785
|
const fieldName = input.getAttribute("name");
|
|
6758
6786
|
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 {
|
|
6787
|
+
if (input instanceof HTMLInputElement && input.type === "radio") {
|
|
6788
|
+
if (input.checked) {
|
|
6771
6789
|
data[fieldName] = input.value;
|
|
6772
6790
|
}
|
|
6773
|
-
} else
|
|
6774
|
-
data[fieldName] = input
|
|
6791
|
+
} else {
|
|
6792
|
+
data[fieldName] = readTypedInputValue(input);
|
|
6775
6793
|
}
|
|
6776
6794
|
}
|
|
6777
6795
|
});
|
|
@@ -6840,7 +6858,7 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
6840
6858
|
};
|
|
6841
6859
|
element.elements.forEach((child) => {
|
|
6842
6860
|
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
6843
|
-
const prefillVal = containerPrefill[child.key]
|
|
6861
|
+
const prefillVal = child.key in containerPrefill ? containerPrefill[child.key] : ("default" in child ? child.default : null) ?? null;
|
|
6844
6862
|
itemsWrap.appendChild(
|
|
6845
6863
|
createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
|
|
6846
6864
|
);
|
|
@@ -6941,7 +6959,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
6941
6959
|
);
|
|
6942
6960
|
element.elements.forEach((child) => {
|
|
6943
6961
|
if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
|
|
6944
|
-
const hiddenValue = rowPrefill
|
|
6962
|
+
const hiddenValue = rowPrefill && child.key in rowPrefill ? rowPrefill[child.key] : ("default" in child ? child.default : null) ?? null;
|
|
6945
6963
|
childWrapper.appendChild(
|
|
6946
6964
|
createHiddenInput(pathJoin(subCtx.path, child.key), hiddenValue)
|
|
6947
6965
|
);
|
|
@@ -7050,39 +7068,21 @@ function renderMultipleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
7050
7068
|
}
|
|
7051
7069
|
}
|
|
7052
7070
|
}
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
validateElementFunc = fn;
|
|
7056
|
-
}
|
|
7057
|
-
function validateElement(element, ctx, customScopeRoot) {
|
|
7058
|
-
if (!validateElementFunc) {
|
|
7071
|
+
function requireValidateElement(context) {
|
|
7072
|
+
if (!context.validateElement) {
|
|
7059
7073
|
throw new Error(
|
|
7060
|
-
"validateElement
|
|
7074
|
+
"validateContainerElement: context.validateElement missing \u2014 container validation requires the instance validator"
|
|
7061
7075
|
);
|
|
7062
7076
|
}
|
|
7063
|
-
return
|
|
7077
|
+
return context.validateElement;
|
|
7064
7078
|
}
|
|
7065
7079
|
function validateContainerElement(element, key, context) {
|
|
7080
|
+
const validateChild = requireValidateElement(context);
|
|
7066
7081
|
const errors = [];
|
|
7067
|
-
const { scopeRoot,
|
|
7082
|
+
const { scopeRoot, path } = context;
|
|
7068
7083
|
if (!("elements" in element)) {
|
|
7069
7084
|
return { value: null, errors };
|
|
7070
7085
|
}
|
|
7071
|
-
const validateContainerCount = (key2, items, element2) => {
|
|
7072
|
-
if (skipValidation) return;
|
|
7073
|
-
const { state } = context;
|
|
7074
|
-
const minItems = "minCount" in element2 ? element2.minCount ?? 0 : 0;
|
|
7075
|
-
const maxItems = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
|
|
7076
|
-
if (element2.required && items.length === 0) {
|
|
7077
|
-
errors.push(`${key2}: ${t("required", state)}`);
|
|
7078
|
-
}
|
|
7079
|
-
if (items.length < minItems) {
|
|
7080
|
-
errors.push(`${key2}: ${t("minItems", state, { min: minItems })}`);
|
|
7081
|
-
}
|
|
7082
|
-
if (items.length > maxItems) {
|
|
7083
|
-
errors.push(`${key2}: ${t("maxItems", state, { max: maxItems })}`);
|
|
7084
|
-
}
|
|
7085
|
-
};
|
|
7086
7086
|
if ("multiple" in element && element.multiple) {
|
|
7087
7087
|
const items = [];
|
|
7088
7088
|
const containerWrappers = findDirectContainerRows(scopeRoot, key);
|
|
@@ -7113,9 +7113,9 @@ function validateContainerElement(element, key, context) {
|
|
|
7113
7113
|
}
|
|
7114
7114
|
}
|
|
7115
7115
|
const childKey = `${key}[${domIndex}].${child.key}`;
|
|
7116
|
-
const childResult =
|
|
7116
|
+
const childResult = validateChild(
|
|
7117
7117
|
{ ...child, key: childKey },
|
|
7118
|
-
{ path },
|
|
7118
|
+
{ path, inheritedReadonly: context.readonly },
|
|
7119
7119
|
itemContainer
|
|
7120
7120
|
);
|
|
7121
7121
|
if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
|
|
@@ -7126,7 +7126,7 @@ function validateContainerElement(element, key, context) {
|
|
|
7126
7126
|
});
|
|
7127
7127
|
items.push(itemData);
|
|
7128
7128
|
});
|
|
7129
|
-
|
|
7129
|
+
validateItemCount(element, key, items.length, context, errors);
|
|
7130
7130
|
return { value: items, errors };
|
|
7131
7131
|
} else {
|
|
7132
7132
|
const containerData = {};
|
|
@@ -7154,9 +7154,9 @@ function validateContainerElement(element, key, context) {
|
|
|
7154
7154
|
}
|
|
7155
7155
|
{
|
|
7156
7156
|
const childKey = `${key}.${child.key}`;
|
|
7157
|
-
const childResult =
|
|
7157
|
+
const childResult = validateChild(
|
|
7158
7158
|
{ ...child, key: childKey },
|
|
7159
|
-
{ path },
|
|
7159
|
+
{ path, inheritedReadonly: context.readonly },
|
|
7160
7160
|
containerContainer
|
|
7161
7161
|
);
|
|
7162
7162
|
if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
|
|
@@ -7645,7 +7645,7 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
|
|
|
7645
7645
|
rebuild();
|
|
7646
7646
|
} catch (e) {
|
|
7647
7647
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
7648
|
-
console.error(t("tableImportError", state
|
|
7648
|
+
console.error(t("tableImportError", state, { error: errMsg }));
|
|
7649
7649
|
} finally {
|
|
7650
7650
|
overlay.remove();
|
|
7651
7651
|
}
|
|
@@ -8525,8 +8525,24 @@ function renderTableElement(element, ctx, wrapper, pathKey) {
|
|
|
8525
8525
|
renderEditTable(element, initialData, pathKey, ctx, wrapper);
|
|
8526
8526
|
}
|
|
8527
8527
|
}
|
|
8528
|
+
function parseTableValue(raw, cellsKey) {
|
|
8529
|
+
let parsed;
|
|
8530
|
+
try {
|
|
8531
|
+
parsed = JSON.parse(raw);
|
|
8532
|
+
} catch {
|
|
8533
|
+
return null;
|
|
8534
|
+
}
|
|
8535
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8536
|
+
return null;
|
|
8537
|
+
}
|
|
8538
|
+
const cells = parsed[cellsKey];
|
|
8539
|
+
const isGrid = Array.isArray(cells) && cells.every(
|
|
8540
|
+
(row) => Array.isArray(row) && row.every((cell) => typeof cell === "string")
|
|
8541
|
+
);
|
|
8542
|
+
return isGrid ? parsed : null;
|
|
8543
|
+
}
|
|
8528
8544
|
function validateTableElement(element, key, context) {
|
|
8529
|
-
const { scopeRoot
|
|
8545
|
+
const { scopeRoot } = context;
|
|
8530
8546
|
const errors = [];
|
|
8531
8547
|
const cellsKey = element.fieldNames?.cells ?? "cells";
|
|
8532
8548
|
const hiddenInput = scopeRoot.querySelector(
|
|
@@ -8535,22 +8551,20 @@ function validateTableElement(element, key, context) {
|
|
|
8535
8551
|
if (!hiddenInput) {
|
|
8536
8552
|
return { value: null, errors };
|
|
8537
8553
|
}
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8554
|
+
const value = parseTableValue(hiddenInput.value, cellsKey);
|
|
8555
|
+
if (value === null) {
|
|
8556
|
+
const msg2 = "invalid table data";
|
|
8557
|
+
errors.push(`${key}: ${msg2}`);
|
|
8558
|
+
markFieldGroupValidity(scopeRoot, key, msg2, context);
|
|
8543
8559
|
return { value: null, errors };
|
|
8544
8560
|
}
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
}
|
|
8553
|
-
}
|
|
8561
|
+
const cells = value[cellsKey];
|
|
8562
|
+
const hasContent = cells.some(
|
|
8563
|
+
(row) => row.some((cell) => cell.trim() !== "")
|
|
8564
|
+
);
|
|
8565
|
+
const msg = element.required && !hasContent ? t("required", context.state) : null;
|
|
8566
|
+
if (msg !== null) errors.push(`${key}: ${msg}`);
|
|
8567
|
+
markFieldGroupValidity(scopeRoot, key, msg, context);
|
|
8554
8568
|
return { value, errors };
|
|
8555
8569
|
}
|
|
8556
8570
|
function updateTableField(element, fieldPath, value, context) {
|
|
@@ -8587,7 +8601,7 @@ function updateTableField(element, fieldPath, value, context) {
|
|
|
8587
8601
|
}
|
|
8588
8602
|
|
|
8589
8603
|
// src/components/richinput.ts
|
|
8590
|
-
function applyAutoExpand2(textarea, backdrop) {
|
|
8604
|
+
function applyAutoExpand2(textarea, backdrop, observers) {
|
|
8591
8605
|
textarea.style.overflow = "hidden";
|
|
8592
8606
|
textarea.style.resize = "none";
|
|
8593
8607
|
const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
|
|
@@ -8609,6 +8623,7 @@ function applyAutoExpand2(textarea, backdrop) {
|
|
|
8609
8623
|
const ro = new ResizeObserver((entries) => {
|
|
8610
8624
|
if (!textarea.isConnected) {
|
|
8611
8625
|
ro.disconnect();
|
|
8626
|
+
observers.delete(ro);
|
|
8612
8627
|
return;
|
|
8613
8628
|
}
|
|
8614
8629
|
const entry = entries[0];
|
|
@@ -8618,6 +8633,7 @@ function applyAutoExpand2(textarea, backdrop) {
|
|
|
8618
8633
|
resize();
|
|
8619
8634
|
});
|
|
8620
8635
|
ro.observe(textarea);
|
|
8636
|
+
observers.add(ro);
|
|
8621
8637
|
}
|
|
8622
8638
|
function buildFileLabels(files, state) {
|
|
8623
8639
|
const labels = /* @__PURE__ */ new Map();
|
|
@@ -9187,7 +9203,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
9187
9203
|
`;
|
|
9188
9204
|
const textarea = document.createElement("textarea");
|
|
9189
9205
|
textarea.name = `${pathKey}__text`;
|
|
9190
|
-
textarea.placeholder = element.placeholder
|
|
9206
|
+
textarea.placeholder = element.placeholder ?? t("richinputPlaceholder", state);
|
|
9191
9207
|
const rawInitialText = initialValue.text ?? "";
|
|
9192
9208
|
textarea.value = rawInitialText ? replaceRidsWithFilenames(rawInitialText, files, state) : "";
|
|
9193
9209
|
textarea.style.cssText = `
|
|
@@ -9204,7 +9220,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
|
|
|
9204
9220
|
z-index: 1;
|
|
9205
9221
|
caret-color: var(--fb-text-color, #111827);
|
|
9206
9222
|
`;
|
|
9207
|
-
applyAutoExpand2(textarea, backdrop);
|
|
9223
|
+
applyAutoExpand2(textarea, backdrop, ctx.state.autoExpandObservers);
|
|
9208
9224
|
textarea.addEventListener("scroll", () => {
|
|
9209
9225
|
backdrop.scrollTop = textarea.scrollTop;
|
|
9210
9226
|
});
|
|
@@ -9849,8 +9865,16 @@ function renderRichInputElement(element, ctx, wrapper, pathKey) {
|
|
|
9849
9865
|
renderEditMode(element, ctx, wrapper, pathKey, initialValue);
|
|
9850
9866
|
}
|
|
9851
9867
|
}
|
|
9868
|
+
function parseRichInputValue(raw) {
|
|
9869
|
+
try {
|
|
9870
|
+
const parsed = JSON.parse(raw);
|
|
9871
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
9872
|
+
} catch {
|
|
9873
|
+
return null;
|
|
9874
|
+
}
|
|
9875
|
+
}
|
|
9852
9876
|
function validateRichInputElement(element, key, context) {
|
|
9853
|
-
const { scopeRoot, state
|
|
9877
|
+
const { scopeRoot, state } = context;
|
|
9854
9878
|
const errors = [];
|
|
9855
9879
|
const textKey = element.textKey ?? "text";
|
|
9856
9880
|
const filesKey = element.filesKey ?? "files";
|
|
@@ -9860,17 +9884,11 @@ function validateRichInputElement(element, key, context) {
|
|
|
9860
9884
|
if (!hiddenInput) {
|
|
9861
9885
|
return { value: null, errors };
|
|
9862
9886
|
}
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
const
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
} else {
|
|
9869
|
-
errors.push(`${key}: invalid richinput data`);
|
|
9870
|
-
return { value: null, errors };
|
|
9871
|
-
}
|
|
9872
|
-
} catch {
|
|
9873
|
-
errors.push(`${key}: invalid richinput data`);
|
|
9887
|
+
const rawValue = parseRichInputValue(hiddenInput.value);
|
|
9888
|
+
if (rawValue === null) {
|
|
9889
|
+
const msg = "invalid richinput data";
|
|
9890
|
+
errors.push(`${key}: ${msg}`);
|
|
9891
|
+
markFieldGroupValidity(scopeRoot, key, msg, context);
|
|
9874
9892
|
return { value: null, errors };
|
|
9875
9893
|
}
|
|
9876
9894
|
const textVal = rawValue[textKey];
|
|
@@ -9881,28 +9899,24 @@ function validateRichInputElement(element, key, context) {
|
|
|
9881
9899
|
[textKey]: text ?? null,
|
|
9882
9900
|
[filesKey]: files
|
|
9883
9901
|
};
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
|
|
9889
|
-
|
|
9890
|
-
if (
|
|
9891
|
-
|
|
9892
|
-
errors.push(
|
|
9893
|
-
`${key}: ${t("minLength", state, { min: element.minLength })}`
|
|
9894
|
-
);
|
|
9895
|
-
}
|
|
9896
|
-
if (element.maxLength != null && text.length > element.maxLength) {
|
|
9897
|
-
errors.push(
|
|
9898
|
-
`${key}: ${t("maxLength", state, { max: element.maxLength })}`
|
|
9899
|
-
);
|
|
9900
|
-
}
|
|
9902
|
+
const textEmpty = !text || text.trim() === "";
|
|
9903
|
+
const messages = [];
|
|
9904
|
+
if (element.required && textEmpty && files.length === 0) {
|
|
9905
|
+
messages.push(t("required", state));
|
|
9906
|
+
}
|
|
9907
|
+
if (!textEmpty && text) {
|
|
9908
|
+
if (element.minLength != null && text.length < element.minLength) {
|
|
9909
|
+
messages.push(t("minLength", state, { min: element.minLength }));
|
|
9901
9910
|
}
|
|
9902
|
-
if (element.
|
|
9903
|
-
|
|
9911
|
+
if (element.maxLength != null && text.length > element.maxLength) {
|
|
9912
|
+
messages.push(t("maxLength", state, { max: element.maxLength }));
|
|
9904
9913
|
}
|
|
9905
9914
|
}
|
|
9915
|
+
if (element.maxFiles != null && files.length > element.maxFiles) {
|
|
9916
|
+
messages.push(t("maxFiles", state, { max: element.maxFiles }));
|
|
9917
|
+
}
|
|
9918
|
+
errors.push(...messages.map((message) => `${key}: ${message}`));
|
|
9919
|
+
markFieldGroupValidity(scopeRoot, key, joinErrorMessages(messages), context);
|
|
9906
9920
|
return { value, errors, spread: !!element.flatOutput };
|
|
9907
9921
|
}
|
|
9908
9922
|
function updateRichInputField(element, fieldPath, value, context) {
|
|
@@ -10177,12 +10191,7 @@ function validateHiddenElement(element, key, context) {
|
|
|
10177
10191
|
const input = scopeRoot.querySelector(
|
|
10178
10192
|
`input[type="hidden"][data-hidden-field="true"][name="${key}"]`
|
|
10179
10193
|
);
|
|
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: [] };
|
|
10194
|
+
return { value: deserializeHiddenValue(input?.value ?? ""), errors: [] };
|
|
10186
10195
|
}
|
|
10187
10196
|
function updateHiddenField(_element, fieldPath, value, context) {
|
|
10188
10197
|
const { scopeRoot } = context;
|
|
@@ -10373,25 +10382,13 @@ function extractDOMValue(fieldPath, formRoot) {
|
|
|
10373
10382
|
if (!input) {
|
|
10374
10383
|
return void 0;
|
|
10375
10384
|
}
|
|
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;
|
|
10385
|
+
if (input instanceof HTMLInputElement && input.type === "radio") {
|
|
10386
|
+
const checked = formRoot.querySelector(
|
|
10387
|
+
`[name="${fieldPath}"]:checked`
|
|
10388
|
+
);
|
|
10389
|
+
return checked ? checked.value : void 0;
|
|
10393
10390
|
}
|
|
10394
|
-
return
|
|
10391
|
+
return readTypedInputValue(input);
|
|
10395
10392
|
}
|
|
10396
10393
|
function buildScopedDataAtPath(path, value) {
|
|
10397
10394
|
const segments = path.match(/[^.[\]]+|\[\d+\]/g);
|
|
@@ -10520,11 +10517,19 @@ function createFieldLabel(element) {
|
|
|
10520
10517
|
}
|
|
10521
10518
|
return title;
|
|
10522
10519
|
}
|
|
10520
|
+
function ensureTooltipStyles(doc) {
|
|
10521
|
+
if (doc.head.querySelector("[data-fb-tooltip-styles]")) return;
|
|
10522
|
+
const style = doc.createElement("style");
|
|
10523
|
+
style.setAttribute("data-fb-tooltip-styles", "");
|
|
10524
|
+
style.textContent = `[id^="tooltip-"].hidden { display: none; }`;
|
|
10525
|
+
doc.head.appendChild(style);
|
|
10526
|
+
}
|
|
10523
10527
|
function createInfoButton(element, state) {
|
|
10524
10528
|
const infoBtn = document.createElement("button");
|
|
10525
10529
|
infoBtn.type = "button";
|
|
10526
10530
|
infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
|
|
10527
10531
|
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>';
|
|
10532
|
+
ensureTooltipStyles(document);
|
|
10528
10533
|
const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
|
|
10529
10534
|
const tooltip = document.createElement("div");
|
|
10530
10535
|
tooltip.id = tooltipId;
|
|
@@ -10674,12 +10679,13 @@ function renderElement2(element, ctx) {
|
|
|
10674
10679
|
wrapper.className = `fb-field-wrapper fb-size-${element.size || "md"}`;
|
|
10675
10680
|
wrapper.setAttribute("data-field-key", element.key);
|
|
10676
10681
|
wrapper.setAttribute("data-fb-width", element.width || "full");
|
|
10682
|
+
const pathKey = pathJoin(ctx.path, element.key);
|
|
10683
|
+
wrapper.setAttribute("data-field-path", pathKey);
|
|
10677
10684
|
const ops = getComponentOperations(element.type);
|
|
10678
10685
|
if (!ops?.ownsLabel) {
|
|
10679
10686
|
const label = createLabelContainer(element, ctx.state);
|
|
10680
10687
|
wrapper.appendChild(label);
|
|
10681
10688
|
}
|
|
10682
|
-
const pathKey = pathJoin(ctx.path, element.key);
|
|
10683
10689
|
dispatchToRenderer(element, ctx, wrapper, pathKey);
|
|
10684
10690
|
if (initiallyDisabled) {
|
|
10685
10691
|
wrapper.style.display = "none";
|
|
@@ -10705,6 +10711,7 @@ var defaultConfig = {
|
|
|
10705
10711
|
onDownloadError: null,
|
|
10706
10712
|
debounceMs: 300,
|
|
10707
10713
|
verboseErrors: false,
|
|
10714
|
+
postMessageTarget: null,
|
|
10708
10715
|
enableFilePreview: true,
|
|
10709
10716
|
maxPreviewSize: "200px",
|
|
10710
10717
|
readonly: false,
|
|
@@ -10726,6 +10733,7 @@ var defaultConfig = {
|
|
|
10726
10733
|
openInNewTab: "Open in new tab",
|
|
10727
10734
|
changeButton: "Change",
|
|
10728
10735
|
placeholderText: "Enter text",
|
|
10736
|
+
selectPlaceholder: "Select\u2026",
|
|
10729
10737
|
previewAlt: "Preview",
|
|
10730
10738
|
previewUnavailable: "Preview unavailable",
|
|
10731
10739
|
previewError: "Preview error",
|
|
@@ -10801,6 +10809,7 @@ var defaultConfig = {
|
|
|
10801
10809
|
openInNewTab: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0432 \u043D\u043E\u0432\u043E\u0439 \u0432\u043A\u043B\u0430\u0434\u043A\u0435",
|
|
10802
10810
|
changeButton: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C",
|
|
10803
10811
|
placeholderText: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442",
|
|
10812
|
+
selectPlaceholder: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435\u2026",
|
|
10804
10813
|
previewAlt: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",
|
|
10805
10814
|
previewUnavailable: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
|
|
10806
10815
|
previewError: "\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430",
|
|
@@ -10865,21 +10874,27 @@ var defaultConfig = {
|
|
|
10865
10874
|
},
|
|
10866
10875
|
theme: {}
|
|
10867
10876
|
};
|
|
10868
|
-
function
|
|
10869
|
-
const
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
config.translations
|
|
10875
|
-
)) {
|
|
10876
|
-
mergedTranslations[locale] = {
|
|
10877
|
-
...defaultConfig.translations[locale] || {},
|
|
10877
|
+
function mergeTranslations(base, overrides) {
|
|
10878
|
+
const merged = { ...base };
|
|
10879
|
+
if (overrides) {
|
|
10880
|
+
for (const [locale, userTranslations] of Object.entries(overrides)) {
|
|
10881
|
+
merged[locale] = {
|
|
10882
|
+
...base[locale] || {},
|
|
10878
10883
|
...userTranslations
|
|
10879
10884
|
};
|
|
10880
10885
|
}
|
|
10881
10886
|
}
|
|
10887
|
+
return merged;
|
|
10888
|
+
}
|
|
10889
|
+
function createInstanceState(config) {
|
|
10890
|
+
const mergedTranslations = mergeTranslations(
|
|
10891
|
+
defaultConfig.translations,
|
|
10892
|
+
config?.translations
|
|
10893
|
+
);
|
|
10882
10894
|
return {
|
|
10895
|
+
instanceId: generateInstanceId(),
|
|
10896
|
+
domIdCounter: 0,
|
|
10897
|
+
reportedInvalid: /* @__PURE__ */ new WeakSet(),
|
|
10883
10898
|
schema: null,
|
|
10884
10899
|
formRoot: null,
|
|
10885
10900
|
resourceIndex: /* @__PURE__ */ new Map(),
|
|
@@ -10894,7 +10909,9 @@ function createInstanceState(config) {
|
|
|
10894
10909
|
prefill: {},
|
|
10895
10910
|
syntheticElementIds: /* @__PURE__ */ new WeakMap(),
|
|
10896
10911
|
syntheticElementIdCounter: 0,
|
|
10912
|
+
multiFileSetters: /* @__PURE__ */ new WeakMap(),
|
|
10897
10913
|
enableIfObservers: /* @__PURE__ */ new Set(),
|
|
10914
|
+
autoExpandObservers: /* @__PURE__ */ new Set(),
|
|
10898
10915
|
tooltipElements: /* @__PURE__ */ new Set()
|
|
10899
10916
|
};
|
|
10900
10917
|
}
|
|
@@ -11196,8 +11213,13 @@ function findOwnField(scope, lookupKey, ownBoundary) {
|
|
|
11196
11213
|
}
|
|
11197
11214
|
var FormBuilderInstance = class {
|
|
11198
11215
|
constructor(config) {
|
|
11199
|
-
|
|
11216
|
+
// The bound prefill-hint click handler currently attached to the form root.
|
|
11217
|
+
// Kept so renderForm()/destroy() can remove it — re-binding on every render
|
|
11218
|
+
// stacked listeners (hint clicks applied values N times) and destroy()
|
|
11219
|
+
// left the last one on the host-owned root, retaining the instance.
|
|
11220
|
+
this.prefillHintHandler = null;
|
|
11200
11221
|
this.state = createInstanceState(config);
|
|
11222
|
+
this.instanceId = this.state.instanceId;
|
|
11201
11223
|
if (this.state.config.verboseErrors) {
|
|
11202
11224
|
if (!globalThis.__formBuilderInstances) {
|
|
11203
11225
|
globalThis.__formBuilderInstances = /* @__PURE__ */ new Set();
|
|
@@ -11229,10 +11251,21 @@ var FormBuilderInstance = class {
|
|
|
11229
11251
|
this.state.formRoot = element;
|
|
11230
11252
|
}
|
|
11231
11253
|
/**
|
|
11232
|
-
* Configure the form builder
|
|
11254
|
+
* Configure the form builder. Translations deep-merge per locale (same as
|
|
11255
|
+
* the constructor); a locale without translations — configured or default —
|
|
11256
|
+
* is rejected, matching setLocale.
|
|
11233
11257
|
*/
|
|
11234
11258
|
configure(config) {
|
|
11235
|
-
|
|
11259
|
+
const translations = mergeTranslations(
|
|
11260
|
+
this.state.config.translations,
|
|
11261
|
+
config.translations
|
|
11262
|
+
);
|
|
11263
|
+
if (config.locale !== void 0 && !translations[config.locale]) {
|
|
11264
|
+
throw new Error(
|
|
11265
|
+
`configure: no translations configured for locale "${config.locale}"`
|
|
11266
|
+
);
|
|
11267
|
+
}
|
|
11268
|
+
Object.assign(this.state.config, config, { translations });
|
|
11236
11269
|
}
|
|
11237
11270
|
/**
|
|
11238
11271
|
* Set file upload handler
|
|
@@ -11265,12 +11298,16 @@ var FormBuilderInstance = class {
|
|
|
11265
11298
|
this.state.config.readonly = mode === "readonly";
|
|
11266
11299
|
}
|
|
11267
11300
|
/**
|
|
11268
|
-
* Set locale
|
|
11301
|
+
* Set locale. Custom locales are allowed — their translations must have
|
|
11302
|
+
* been provided via the constructor or configure() first.
|
|
11269
11303
|
*/
|
|
11270
11304
|
setLocale(locale) {
|
|
11271
|
-
if (this.state.config.translations[locale]) {
|
|
11272
|
-
|
|
11305
|
+
if (!this.state.config.translations[locale]) {
|
|
11306
|
+
throw new Error(
|
|
11307
|
+
`setLocale: no translations configured for locale "${locale}"`
|
|
11308
|
+
);
|
|
11273
11309
|
}
|
|
11310
|
+
this.state.config.locale = locale;
|
|
11274
11311
|
}
|
|
11275
11312
|
/**
|
|
11276
11313
|
* Trigger onChange callbacks with debouncing
|
|
@@ -11623,11 +11660,13 @@ var FormBuilderInstance = class {
|
|
|
11623
11660
|
renderForm(root, schema, prefill, actions) {
|
|
11624
11661
|
const errors = validateSchema(schema);
|
|
11625
11662
|
if (errors.length > 0) {
|
|
11626
|
-
|
|
11627
|
-
|
|
11663
|
+
throw new Error(`renderForm: invalid schema:
|
|
11664
|
+
- ${errors.join("\n- ")}`);
|
|
11628
11665
|
}
|
|
11629
11666
|
this.disconnectEnableIfObservers();
|
|
11667
|
+
this.disconnectAutoExpandObservers();
|
|
11630
11668
|
this.removeTooltipElements();
|
|
11669
|
+
this.removePrefillHintListener();
|
|
11631
11670
|
this.state.formRoot = root;
|
|
11632
11671
|
this.state.schema = schema;
|
|
11633
11672
|
this.state.externalActions = actions || null;
|
|
@@ -11650,7 +11689,7 @@ var FormBuilderInstance = class {
|
|
|
11650
11689
|
}
|
|
11651
11690
|
schema.elements.forEach((element) => {
|
|
11652
11691
|
if (element.type !== "markdown" && (element.hidden || element.type === "hidden")) {
|
|
11653
|
-
const val = prefill
|
|
11692
|
+
const val = prefill && element.key in prefill ? prefill[element.key] : element.default ?? null;
|
|
11654
11693
|
fieldsWrapper.appendChild(createHiddenInput(element.key, val));
|
|
11655
11694
|
return;
|
|
11656
11695
|
}
|
|
@@ -11667,23 +11706,36 @@ var FormBuilderInstance = class {
|
|
|
11667
11706
|
rootContainer.appendChild(fieldsWrapper);
|
|
11668
11707
|
root.appendChild(rootContainer);
|
|
11669
11708
|
if (!this.state.config.readonly) {
|
|
11670
|
-
|
|
11709
|
+
this.prefillHintHandler = this.handlePrefillHintClick.bind(
|
|
11710
|
+
this
|
|
11711
|
+
);
|
|
11712
|
+
root.addEventListener("click", this.prefillHintHandler);
|
|
11671
11713
|
}
|
|
11672
11714
|
if (this.state.config.readonly && this.state.externalActions && Array.isArray(this.state.externalActions)) {
|
|
11673
11715
|
this.renderExternalActions();
|
|
11674
11716
|
}
|
|
11675
11717
|
}
|
|
11676
11718
|
/**
|
|
11677
|
-
* Validate form and extract data
|
|
11678
|
-
*
|
|
11679
|
-
*
|
|
11719
|
+
* Validate the form and extract its data. `skipValidation` is the draft
|
|
11720
|
+
* contract of saveDraft() and the onChange payload: marks are only
|
|
11721
|
+
* refreshed or cleared, and the result reports `valid: true, errors: []`.
|
|
11680
11722
|
*/
|
|
11681
11723
|
validateForm(skipValidation = false) {
|
|
11724
|
+
if (!skipValidation) return this.runValidation("full");
|
|
11725
|
+
return { ...this.runValidation("draft"), valid: true, errors: [] };
|
|
11726
|
+
}
|
|
11727
|
+
/**
|
|
11728
|
+
* Run every rule and return the real result. `marks` decides only what is
|
|
11729
|
+
* painted: "full" raises and clears marks and records reported fields;
|
|
11730
|
+
* "draft" refreshes or clears marks of reported fields and raises none
|
|
11731
|
+
* (see ValidityScope in utils/styles.ts).
|
|
11732
|
+
*/
|
|
11733
|
+
runValidation(marks) {
|
|
11682
11734
|
if (!this.state.schema || !this.state.formRoot)
|
|
11683
11735
|
return { valid: true, errors: [], data: {} };
|
|
11684
11736
|
const errors = [];
|
|
11685
11737
|
const data = {};
|
|
11686
|
-
const
|
|
11738
|
+
const validateElement = (element, ctx, customScopeRoot = null) => {
|
|
11687
11739
|
const key = element.key ?? "";
|
|
11688
11740
|
const scopeRoot = customScopeRoot || this.state.formRoot;
|
|
11689
11741
|
const componentContext = {
|
|
@@ -11691,7 +11743,11 @@ var FormBuilderInstance = class {
|
|
|
11691
11743
|
state: this.state,
|
|
11692
11744
|
instance: this,
|
|
11693
11745
|
path: ctx.path,
|
|
11694
|
-
|
|
11746
|
+
draftMarks: marks === "draft",
|
|
11747
|
+
readonly: isElementReadonly(element, this.state, ctx),
|
|
11748
|
+
// Containers recurse into their children through this — threaded per
|
|
11749
|
+
// pass, never module state (see ComponentContext.validateElement).
|
|
11750
|
+
validateElement
|
|
11695
11751
|
};
|
|
11696
11752
|
const componentResult = validateElementWithComponent(
|
|
11697
11753
|
element,
|
|
@@ -11709,7 +11765,6 @@ var FormBuilderInstance = class {
|
|
|
11709
11765
|
console.warn(`Unknown field type "${element.type}" for key "${key}"`);
|
|
11710
11766
|
return { value: null, spread: false };
|
|
11711
11767
|
};
|
|
11712
|
-
setValidateElement(validateElement2);
|
|
11713
11768
|
this.state.schema.elements.forEach((element) => {
|
|
11714
11769
|
if (element.enableIf) {
|
|
11715
11770
|
try {
|
|
@@ -11727,7 +11782,7 @@ var FormBuilderInstance = class {
|
|
|
11727
11782
|
if (element.type === "markdown") {
|
|
11728
11783
|
return;
|
|
11729
11784
|
}
|
|
11730
|
-
const result =
|
|
11785
|
+
const result = validateElement(element, { path: "" });
|
|
11731
11786
|
if (result.skip) return;
|
|
11732
11787
|
if (result.spread && result.value !== null && typeof result.value === "object") {
|
|
11733
11788
|
Object.assign(data, result.value);
|
|
@@ -11742,10 +11797,56 @@ var FormBuilderInstance = class {
|
|
|
11742
11797
|
};
|
|
11743
11798
|
}
|
|
11744
11799
|
/**
|
|
11745
|
-
*
|
|
11800
|
+
* Read the form: every rule runs and the result is the real
|
|
11801
|
+
* `{valid, errors, data}`. Safe to poll — it never paints a new error
|
|
11802
|
+
* mark (so a pristine form never turns red), it only refreshes or clears
|
|
11803
|
+
* marks that showErrors()/submitForm() drew, and a repeated call on
|
|
11804
|
+
* unchanged state touches no DOM at all.
|
|
11746
11805
|
*/
|
|
11747
11806
|
getFormData() {
|
|
11748
|
-
return this.
|
|
11807
|
+
return this.runValidation("draft");
|
|
11808
|
+
}
|
|
11809
|
+
/**
|
|
11810
|
+
* Paint every validation error next to its field (and clear marks of
|
|
11811
|
+
* fields that are now valid), then return the same result as
|
|
11812
|
+
* getFormData(). Call it when the user asks to submit, followed by
|
|
11813
|
+
* focusFirstError() to take them to the first problem.
|
|
11814
|
+
*/
|
|
11815
|
+
showErrors() {
|
|
11816
|
+
return this.runValidation("full");
|
|
11817
|
+
}
|
|
11818
|
+
/**
|
|
11819
|
+
* Focus the first field marked invalid, in DOM order, and scroll it into
|
|
11820
|
+
* view. A marked group (container, multi-value field, file field) gets
|
|
11821
|
+
* focus on its first focusable control, else on the group itself.
|
|
11822
|
+
* Does not validate: marks are drawn by showErrors() (or submitForm()),
|
|
11823
|
+
* so call showErrors() first. Fields hidden by enableIf are skipped, as is
|
|
11824
|
+
* any field that cannot take focus (e.g. inside a hidden slide).
|
|
11825
|
+
* @returns true only when focus actually landed on an invalid field
|
|
11826
|
+
*/
|
|
11827
|
+
focusFirstError() {
|
|
11828
|
+
const root = this.state.formRoot;
|
|
11829
|
+
if (!root) return false;
|
|
11830
|
+
const marked = root.querySelectorAll('[aria-invalid="true"]');
|
|
11831
|
+
for (const target of Array.from(marked)) {
|
|
11832
|
+
if (target.closest('[data-conditionally-disabled="true"]')) continue;
|
|
11833
|
+
const candidates = target.matches("input, select, textarea, button") ? [target] : [
|
|
11834
|
+
...Array.from(
|
|
11835
|
+
target.querySelectorAll(
|
|
11836
|
+
"input, select, textarea, button, [tabindex]"
|
|
11837
|
+
)
|
|
11838
|
+
),
|
|
11839
|
+
target
|
|
11840
|
+
];
|
|
11841
|
+
for (const candidate of candidates) {
|
|
11842
|
+
candidate.focus({ preventScroll: true });
|
|
11843
|
+
if (document.activeElement === candidate) {
|
|
11844
|
+
candidate.scrollIntoView({ block: "center" });
|
|
11845
|
+
return true;
|
|
11846
|
+
}
|
|
11847
|
+
}
|
|
11848
|
+
}
|
|
11849
|
+
return false;
|
|
11749
11850
|
}
|
|
11750
11851
|
/**
|
|
11751
11852
|
* Submit form with validation
|
|
@@ -11753,16 +11854,7 @@ var FormBuilderInstance = class {
|
|
|
11753
11854
|
submitForm() {
|
|
11754
11855
|
const result = this.validateForm(false);
|
|
11755
11856
|
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
|
-
}
|
|
11857
|
+
this.postToParent("formSubmit", result.data);
|
|
11766
11858
|
}
|
|
11767
11859
|
return result;
|
|
11768
11860
|
}
|
|
@@ -11771,17 +11863,27 @@ var FormBuilderInstance = class {
|
|
|
11771
11863
|
*/
|
|
11772
11864
|
saveDraft() {
|
|
11773
11865
|
const result = this.validateForm(true);
|
|
11774
|
-
|
|
11775
|
-
|
|
11776
|
-
|
|
11777
|
-
|
|
11778
|
-
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11866
|
+
this.postToParent("formDraft", result.data);
|
|
11867
|
+
return result;
|
|
11868
|
+
}
|
|
11869
|
+
/**
|
|
11870
|
+
* Post form data to the parent frame — only when the host opted in via
|
|
11871
|
+
* `postMessageTarget`. Outside an iframe `window.parent === window`, so an
|
|
11872
|
+
* unconditional post broadcast form data and the full schema to any
|
|
11873
|
+
* embedding page (targetOrigin "*") on every submit. See CHANGELOG 0.6.0.
|
|
11874
|
+
*/
|
|
11875
|
+
postToParent(type, data) {
|
|
11876
|
+
const target = this.state.config.postMessageTarget;
|
|
11877
|
+
if (target === "") {
|
|
11878
|
+
throw new Error(
|
|
11879
|
+
'postMessageTarget: "" is not a valid target origin \u2014 use null to disable posting or "*" to knowingly broadcast'
|
|
11782
11880
|
);
|
|
11783
11881
|
}
|
|
11784
|
-
return
|
|
11882
|
+
if (!target || typeof window === "undefined" || !window.parent) return;
|
|
11883
|
+
window.parent.postMessage(
|
|
11884
|
+
{ type, data, schema: this.state.schema },
|
|
11885
|
+
target
|
|
11886
|
+
);
|
|
11785
11887
|
}
|
|
11786
11888
|
/**
|
|
11787
11889
|
* Clear the form - reset all field values to empty while preserving form structure
|
|
@@ -11985,6 +12087,7 @@ var FormBuilderInstance = class {
|
|
|
11985
12087
|
getElementLookupKey(element, this.state)
|
|
11986
12088
|
);
|
|
11987
12089
|
disabledWrapper.setAttribute("data-conditionally-disabled", "true");
|
|
12090
|
+
disabledWrapper.setAttribute("data-field-path", fullDomPath);
|
|
11988
12091
|
wrapper.parentNode?.replaceChild(disabledWrapper, wrapper);
|
|
11989
12092
|
}
|
|
11990
12093
|
} catch (error) {
|
|
@@ -12051,7 +12154,9 @@ var FormBuilderInstance = class {
|
|
|
12051
12154
|
this.state.debounceTimer = null;
|
|
12052
12155
|
}
|
|
12053
12156
|
this.disconnectEnableIfObservers();
|
|
12157
|
+
this.disconnectAutoExpandObservers();
|
|
12054
12158
|
this.removeTooltipElements();
|
|
12159
|
+
this.removePrefillHintListener();
|
|
12055
12160
|
this.state.resourceIndex.clear();
|
|
12056
12161
|
if (this.state.formRoot) {
|
|
12057
12162
|
clear(this.state.formRoot);
|
|
@@ -12069,6 +12174,18 @@ var FormBuilderInstance = class {
|
|
|
12069
12174
|
}
|
|
12070
12175
|
this.state.enableIfObservers.clear();
|
|
12071
12176
|
}
|
|
12177
|
+
disconnectAutoExpandObservers() {
|
|
12178
|
+
for (const observer of this.state.autoExpandObservers) {
|
|
12179
|
+
observer.disconnect();
|
|
12180
|
+
}
|
|
12181
|
+
this.state.autoExpandObservers.clear();
|
|
12182
|
+
}
|
|
12183
|
+
removePrefillHintListener() {
|
|
12184
|
+
if (this.prefillHintHandler && this.state.formRoot) {
|
|
12185
|
+
this.state.formRoot.removeEventListener("click", this.prefillHintHandler);
|
|
12186
|
+
}
|
|
12187
|
+
this.prefillHintHandler = null;
|
|
12188
|
+
}
|
|
12072
12189
|
removeTooltipElements() {
|
|
12073
12190
|
for (const tooltip of this.state.tooltipElements) {
|
|
12074
12191
|
tooltip.remove();
|