@dmitryvim/form-builder 0.6.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 +6 -1
- package/dist/browser/formbuilder.min.js +138 -128
- package/dist/browser/formbuilder.v0.7.0.min.js +1482 -0
- package/dist/cjs/index.cjs +1247 -1105
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +1048 -905
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +138 -128
- package/dist/types/components/file/dom.d.ts +16 -4
- package/dist/types/components/file/render-edit.d.ts +6 -5
- package/dist/types/instance/FormBuilderInstance.d.ts +32 -4
- package/dist/types/types/component-operations.d.ts +13 -2
- package/dist/types/types/state.d.ts +17 -0
- package/dist/types/utils/styles.d.ts +68 -6
- package/dist/types/utils/validation.d.ts +14 -0
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.6.4.min.js +0 -1472
package/dist/esm/index.js
CHANGED
|
@@ -95,459 +95,195 @@ function createHiddenInput(name, value) {
|
|
|
95
95
|
return input;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
// src/utils/
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
if (element.minLength != null && element.maxLength != null) {
|
|
102
|
-
parts.push(
|
|
103
|
-
t("hintLengthRange", state, {
|
|
104
|
-
min: element.minLength,
|
|
105
|
-
max: element.maxLength
|
|
106
|
-
})
|
|
107
|
-
);
|
|
108
|
-
} else if (element.maxLength != null) {
|
|
109
|
-
parts.push(t("hintMaxLength", state, { max: element.maxLength }));
|
|
110
|
-
} else if (element.minLength != null) {
|
|
111
|
-
parts.push(t("hintMinLength", state, { min: element.minLength }));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
98
|
+
// src/utils/styles.ts
|
|
99
|
+
function findErrorAnchor(input) {
|
|
100
|
+
return input.closest?.(".fb-chip") ?? input.closest?.(".slider-container") ?? input;
|
|
114
101
|
}
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
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
|
+
}
|
|
125
111
|
}
|
|
126
112
|
}
|
|
113
|
+
const sibling = anchor.nextElementSibling;
|
|
114
|
+
return sibling && isInputErrorNode(sibling) ? sibling : null;
|
|
127
115
|
}
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
if (sizeMB && sizeMB !== Infinity) {
|
|
131
|
-
parts.push(t("hintMaxSize", state, { size: sizeMB }));
|
|
132
|
-
}
|
|
116
|
+
function isInputErrorNode(node) {
|
|
117
|
+
return node.classList.contains("error-message") && !node.classList.contains("fb-field-error");
|
|
133
118
|
}
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
})
|
|
140
|
-
);
|
|
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;
|
|
141
124
|
}
|
|
125
|
+
if (scope.draftMarks && !reported.has(target)) return void 0;
|
|
126
|
+
reported.add(target);
|
|
127
|
+
return message;
|
|
142
128
|
}
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
parts.push(t("hintPattern", state, { pattern: element.pattern }));
|
|
146
|
-
}
|
|
129
|
+
function joinErrorMessages(messages) {
|
|
130
|
+
return messages.length > 0 ? messages.join(" \u2022 ") : null;
|
|
147
131
|
}
|
|
148
|
-
function
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
);
|
|
153
187
|
}
|
|
154
|
-
addFileSizeHint(element, parts, state);
|
|
155
|
-
addFormatHint(element, parts, state);
|
|
156
|
-
addPatternHint(element, parts, state);
|
|
157
|
-
return parts.join(" \u2022 ");
|
|
158
188
|
}
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const columns = schema.columns;
|
|
171
|
-
const validColumns = [1, 2, 3, 4];
|
|
172
|
-
if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
|
|
173
|
-
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);
|
|
174
200
|
}
|
|
201
|
+
target.removeAttribute(ADDED_ATTRS);
|
|
175
202
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
if (!hint.values || typeof hint.values !== "object") {
|
|
186
|
-
errors.push(
|
|
187
|
-
`schema.prefillHints[${hintIndex}] must have a 'values' property of type object`
|
|
188
|
-
);
|
|
189
|
-
} else {
|
|
190
|
-
for (const fieldKey in hint.values) {
|
|
191
|
-
const fieldExists = schema.elements.some(
|
|
192
|
-
(element) => element.key === fieldKey
|
|
193
|
-
);
|
|
194
|
-
if (!fieldExists) {
|
|
195
|
-
errors.push(
|
|
196
|
-
`schema.prefillHints[${hintIndex}] references non-existent field "${fieldKey}"`
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
}
|
|
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();
|
|
203
211
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
`${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
if ("displayMode" in element && element.displayMode !== void 0) {
|
|
215
|
-
const displayMode = element.displayMode;
|
|
216
|
-
if (displayMode !== "stack" && displayMode !== "slides") {
|
|
217
|
-
errors2.push(
|
|
218
|
-
`${elementPath}: displayMode must be "stack" or "slides" (got ${JSON.stringify(displayMode)})`
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
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;
|
|
222
218
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
`${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with element key "${otherEl.key}"`
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
if (otherEl.key === filesKey) {
|
|
238
|
-
errors.push(
|
|
239
|
-
`${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with element key "${otherEl.key}"`
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (allOutputKeys.has(textKey)) {
|
|
244
|
-
errors.push(
|
|
245
|
-
`${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with another flatOutput key`
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
if (allOutputKeys.has(filesKey)) {
|
|
249
|
-
errors.push(
|
|
250
|
-
`${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with another flatOutput key`
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
allOutputKeys.add(textKey);
|
|
254
|
-
allOutputKeys.add(filesKey);
|
|
255
|
-
} else {
|
|
256
|
-
if (el.key) {
|
|
257
|
-
if (allOutputKeys.has(el.key)) {
|
|
258
|
-
errors.push(
|
|
259
|
-
`${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
allOutputKeys.add(el.key);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
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;
|
|
266
230
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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`
|
|
292
262
|
);
|
|
293
263
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
const min = typeof minCount === "number" ? minCount : void 0;
|
|
306
|
-
const max = typeof maxCount === "number" ? maxCount : void 0;
|
|
307
|
-
if (max !== void 0 && (max < 0 || Number.isNaN(max))) {
|
|
308
|
-
errors2.push(
|
|
309
|
-
`${elementPath}: ${maxName} must be a non-negative number or Infinity (got ${max})`
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
if (min !== void 0 && (min < 0 || !Number.isFinite(min))) {
|
|
313
|
-
errors2.push(
|
|
314
|
-
`${elementPath}: ${minName} must be a finite non-negative number (got ${min})`
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
const effectiveMin = min ?? (requiredImpliesFloor ? 1 : void 0);
|
|
318
|
-
if (effectiveMin !== void 0 && max !== void 0 && effectiveMin > max) {
|
|
319
|
-
const shown = min !== void 0 ? `${minName} (${min})` : `required: true (implies ${minName} 1)`;
|
|
320
|
-
errors2.push(
|
|
321
|
-
`${elementPath}: ${shown} cannot be greater than ${maxName} (${max})`
|
|
322
|
-
);
|
|
323
|
-
}
|
|
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;
|
|
324
273
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if (!element.type) {
|
|
337
|
-
errors.push(`${elementPath}: missing type`);
|
|
338
|
-
}
|
|
339
|
-
if (!element.key && element.type !== "markdown") {
|
|
340
|
-
errors.push(`${elementPath}: missing key`);
|
|
341
|
-
}
|
|
342
|
-
validateCountBounds(element, elementPath, errors);
|
|
343
|
-
if (element.type === "number" && "decimals" in element) {
|
|
344
|
-
const decimals = element.decimals;
|
|
345
|
-
if (decimals !== void 0 && (!Number.isInteger(decimals) || decimals < 0)) {
|
|
346
|
-
errors.push(
|
|
347
|
-
`${elementPath}: decimals must be a non-negative integer (got ${JSON.stringify(decimals)})`
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
if (element.type === "markdown") {
|
|
352
|
-
const content = element.content;
|
|
353
|
-
if (typeof content !== "string") {
|
|
354
|
-
errors.push(
|
|
355
|
-
`${elementPath}: markdown element requires "content" to be a string (got ${content === null ? "null" : typeof content})`
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
if (element.enableIf) {
|
|
360
|
-
const enableIf = element.enableIf;
|
|
361
|
-
if (!enableIf.key || typeof enableIf.key !== "string") {
|
|
362
|
-
errors.push(
|
|
363
|
-
`${elementPath}: enableIf must have a 'key' property of type string`
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
const hasOperator = "equals" in enableIf;
|
|
367
|
-
if (!hasOperator) {
|
|
368
|
-
errors.push(
|
|
369
|
-
`${elementPath}: enableIf must have at least one operator (equals, etc.)`
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
if (element.type === "group" && "elements" in element && element.elements) {
|
|
374
|
-
validateElements(element.elements, `${elementPath}.elements`);
|
|
375
|
-
}
|
|
376
|
-
if (element.type === "container" && element.elements) {
|
|
377
|
-
validateContainerProps(element, elementPath, errors);
|
|
378
|
-
if ("prefillHints" in element && element.prefillHints) {
|
|
379
|
-
const prefillHints = element.prefillHints;
|
|
380
|
-
if (Array.isArray(prefillHints)) {
|
|
381
|
-
prefillHints.forEach((hint, hintIndex) => {
|
|
382
|
-
if (!hint.label || typeof hint.label !== "string") {
|
|
383
|
-
errors.push(
|
|
384
|
-
`${elementPath}: prefillHints[${hintIndex}] must have a 'label' property of type string`
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
if (!hint.values || typeof hint.values !== "object") {
|
|
388
|
-
errors.push(
|
|
389
|
-
`${elementPath}: prefillHints[${hintIndex}] must have a 'values' property of type object`
|
|
390
|
-
);
|
|
391
|
-
} else {
|
|
392
|
-
for (const fieldKey in hint.values) {
|
|
393
|
-
const fieldExists = element.elements.some(
|
|
394
|
-
(childElement) => childElement.key === fieldKey
|
|
395
|
-
);
|
|
396
|
-
if (!fieldExists) {
|
|
397
|
-
errors.push(
|
|
398
|
-
`container "${element.key}": prefillHints[${hintIndex}] references non-existent field "${fieldKey}"`
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
validateElements(element.elements, `${elementPath}.elements`);
|
|
407
|
-
checkFlatOutputCollisions(element.elements, `${elementPath}.elements`);
|
|
408
|
-
}
|
|
409
|
-
if (element.type === "select" && element.options) {
|
|
410
|
-
const defaultValue = element.default;
|
|
411
|
-
if (defaultValue !== void 0 && defaultValue !== null && defaultValue !== "") {
|
|
412
|
-
const hasMatchingOption = element.options.some(
|
|
413
|
-
(opt) => opt.value === defaultValue
|
|
414
|
-
);
|
|
415
|
-
if (!hasMatchingOption) {
|
|
416
|
-
errors.push(
|
|
417
|
-
`${elementPath}: default "${defaultValue}" not in options`
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
if (Array.isArray(schema.elements)) {
|
|
425
|
-
validateElements(schema.elements, "elements");
|
|
426
|
-
checkFlatOutputCollisions(schema.elements, "elements");
|
|
427
|
-
}
|
|
428
|
-
return errors;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// src/utils/enable-conditions.ts
|
|
432
|
-
function getValueByPath(data, path) {
|
|
433
|
-
if (!data || typeof data !== "object") {
|
|
434
|
-
return void 0;
|
|
435
|
-
}
|
|
436
|
-
const segments = path.match(/[^.[\]]+|\[\d+\]/g);
|
|
437
|
-
if (!segments || segments.length === 0) {
|
|
438
|
-
return void 0;
|
|
439
|
-
}
|
|
440
|
-
let current = data;
|
|
441
|
-
for (const segment of segments) {
|
|
442
|
-
if (current === void 0 || current === null) {
|
|
443
|
-
return void 0;
|
|
444
|
-
}
|
|
445
|
-
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
446
|
-
const index = parseInt(segment.slice(1, -1), 10);
|
|
447
|
-
if (!Array.isArray(current) || isNaN(index)) {
|
|
448
|
-
return void 0;
|
|
449
|
-
}
|
|
450
|
-
current = current[index];
|
|
451
|
-
} else {
|
|
452
|
-
current = current[segment];
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return current;
|
|
456
|
-
}
|
|
457
|
-
function evaluateEnableCondition(condition, formData, containerData) {
|
|
458
|
-
if (!condition || !condition.key) {
|
|
459
|
-
throw new Error("Invalid enableIf condition: must have a 'key' property");
|
|
460
|
-
}
|
|
461
|
-
const scope = condition.scope ?? "relative";
|
|
462
|
-
let dataSource;
|
|
463
|
-
if (scope === "relative") {
|
|
464
|
-
dataSource = containerData ?? formData;
|
|
465
|
-
} else if (scope === "absolute") {
|
|
466
|
-
dataSource = formData;
|
|
467
|
-
} else {
|
|
468
|
-
throw new Error(
|
|
469
|
-
`Invalid enableIf scope: must be "relative" or "absolute" (got "${scope}")`
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
const actualValue = getValueByPath(dataSource, condition.key);
|
|
473
|
-
if ("equals" in condition) {
|
|
474
|
-
return deepEqual(actualValue, condition.equals);
|
|
475
|
-
}
|
|
476
|
-
throw new Error(
|
|
477
|
-
`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
|
|
478
285
|
);
|
|
479
286
|
}
|
|
480
|
-
function deepEqual(a, b) {
|
|
481
|
-
if (a === b) return true;
|
|
482
|
-
if (a == null || b == null) return a === b;
|
|
483
|
-
if (typeof a !== typeof b) return false;
|
|
484
|
-
if (typeof a === "object" && typeof b === "object") {
|
|
485
|
-
try {
|
|
486
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
487
|
-
} catch (e) {
|
|
488
|
-
if (e instanceof TypeError && (e.message.includes("circular") || e.message.includes("cyclic"))) {
|
|
489
|
-
console.warn(
|
|
490
|
-
"deepEqual: Circular reference detected in enableIf comparison, using reference equality"
|
|
491
|
-
);
|
|
492
|
-
return a === b;
|
|
493
|
-
}
|
|
494
|
-
throw e;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
return a === b;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// src/utils/styles.ts
|
|
501
|
-
function findErrorAnchor(input) {
|
|
502
|
-
return input.closest?.(".fb-chip") ?? input.closest?.(".slider-container") ?? input;
|
|
503
|
-
}
|
|
504
|
-
function findErrorNode(input) {
|
|
505
|
-
const anchor = findErrorAnchor(input);
|
|
506
|
-
const name = input.getAttribute("name");
|
|
507
|
-
const parent = anchor.parentElement;
|
|
508
|
-
if (name && parent) {
|
|
509
|
-
for (const child of Array.from(parent.children)) {
|
|
510
|
-
if (child.classList.contains("error-message") && child.getAttribute("data-error-for") === name) {
|
|
511
|
-
return child;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
const sibling = anchor.nextElementSibling;
|
|
516
|
-
return sibling && sibling.classList.contains("error-message") ? sibling : null;
|
|
517
|
-
}
|
|
518
|
-
function markFieldValidity(input, errorMessage) {
|
|
519
|
-
if (!input) return;
|
|
520
|
-
if (errorMessage == null) {
|
|
521
|
-
input.classList.remove("invalid");
|
|
522
|
-
input.title = "";
|
|
523
|
-
findErrorNode(input)?.remove();
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
input.classList.add("invalid");
|
|
527
|
-
input.title = errorMessage;
|
|
528
|
-
if (errorMessage === "") {
|
|
529
|
-
findErrorNode(input)?.remove();
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
let errorElement = findErrorNode(input);
|
|
533
|
-
if (!errorElement) {
|
|
534
|
-
const anchor = findErrorAnchor(input);
|
|
535
|
-
errorElement = document.createElement("div");
|
|
536
|
-
errorElement.className = "error-message";
|
|
537
|
-
errorElement.style.cssText = `
|
|
538
|
-
color: var(--fb-error-color);
|
|
539
|
-
font-size: var(--fb-font-size-small);
|
|
540
|
-
margin-top: 0.25rem;
|
|
541
|
-
`;
|
|
542
|
-
anchor.parentNode?.insertBefore(errorElement, anchor.nextSibling);
|
|
543
|
-
}
|
|
544
|
-
errorElement.setAttribute("data-error-for", input.getAttribute("name") ?? "");
|
|
545
|
-
errorElement.textContent = errorMessage;
|
|
546
|
-
errorElement.style.display = "block";
|
|
547
|
-
}
|
|
548
|
-
function clearFieldError(input) {
|
|
549
|
-
findErrorNode(input)?.remove();
|
|
550
|
-
}
|
|
551
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>';
|
|
552
288
|
function ensureThemingHooks(doc) {
|
|
553
289
|
if (doc.head.querySelector("[data-fb-theming-hooks]")) return;
|
|
@@ -665,6 +401,13 @@ function ensureThemingHooks(doc) {
|
|
|
665
401
|
/* .fb-size-md uses defaults \u2014 no override needed */
|
|
666
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; }
|
|
667
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
|
+
}
|
|
668
411
|
`;
|
|
669
412
|
doc.head.appendChild(style);
|
|
670
413
|
}
|
|
@@ -891,6 +634,427 @@ function applyActionButtonStyles(button, isFormLevel = false) {
|
|
|
891
634
|
});
|
|
892
635
|
}
|
|
893
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;
|
|
987
|
+
}
|
|
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;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
894
1058
|
// src/components/text.ts
|
|
895
1059
|
function ensureChipStyles(doc) {
|
|
896
1060
|
if (doc.head.querySelector("[data-fb-chip-styles]")) return;
|
|
@@ -1173,75 +1337,50 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1173
1337
|
}
|
|
1174
1338
|
function validateTextElement(element, key, context) {
|
|
1175
1339
|
const errors = [];
|
|
1176
|
-
const { scopeRoot,
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
markFieldValidity(input, msg);
|
|
1190
|
-
hasError = true;
|
|
1191
|
-
} else if (element.pattern) {
|
|
1192
|
-
try {
|
|
1193
|
-
const re = new RegExp(element.pattern);
|
|
1194
|
-
if (!re.test(val)) {
|
|
1195
|
-
const msg = t("patternMismatch", state);
|
|
1196
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
1197
|
-
markFieldValidity(input, msg);
|
|
1198
|
-
hasError = true;
|
|
1199
|
-
}
|
|
1200
|
-
} catch {
|
|
1201
|
-
const msg = t("invalidPattern", state);
|
|
1202
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
1203
|
-
markFieldValidity(input, msg);
|
|
1204
|
-
hasError = true;
|
|
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);
|
|
1205
1353
|
}
|
|
1354
|
+
} catch {
|
|
1355
|
+
return t("invalidPattern", state);
|
|
1206
1356
|
}
|
|
1207
1357
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1358
|
+
return null;
|
|
1359
|
+
};
|
|
1360
|
+
const validateTextInput = (input, val, fieldKey) => {
|
|
1361
|
+
const msg = lengthOrPatternError(val);
|
|
1362
|
+
if (msg !== null) errors.push(`${fieldKey}: ${msg}`);
|
|
1363
|
+
markFieldValidity(input, msg, context);
|
|
1211
1364
|
};
|
|
1212
1365
|
if (element.multiple) {
|
|
1213
1366
|
const inputs = scopeRoot.querySelectorAll(`[name^="${key}\\["]`);
|
|
1214
1367
|
const values = [];
|
|
1215
|
-
|
|
1368
|
+
let filledCount = 0;
|
|
1216
1369
|
inputs.forEach((input, index) => {
|
|
1217
1370
|
const val = input?.value ?? "";
|
|
1218
|
-
rawValues.push(val);
|
|
1219
1371
|
values.push(val === "" ? null : val);
|
|
1372
|
+
if (val.trim() !== "") filledCount++;
|
|
1220
1373
|
validateTextInput(input, val, `${key}[${index}]`);
|
|
1221
1374
|
});
|
|
1222
|
-
|
|
1223
|
-
const { state } = context;
|
|
1224
|
-
const minCount = element.minCount ?? 0;
|
|
1225
|
-
const maxCount = element.maxCount ?? Infinity;
|
|
1226
|
-
const filteredValues = rawValues.filter((v) => v.trim() !== "");
|
|
1227
|
-
if (element.required && filteredValues.length === 0) {
|
|
1228
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
1229
|
-
}
|
|
1230
|
-
if (filteredValues.length < minCount) {
|
|
1231
|
-
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
1232
|
-
}
|
|
1233
|
-
if (filteredValues.length > maxCount) {
|
|
1234
|
-
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1375
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
1237
1376
|
return { value: values, errors };
|
|
1238
1377
|
} else {
|
|
1239
1378
|
const input = scopeRoot.querySelector(`[name="${key}"]`);
|
|
1240
1379
|
const val = input?.value ?? "";
|
|
1241
|
-
if (
|
|
1242
|
-
const msg = t("required",
|
|
1380
|
+
if (element.required && val === "") {
|
|
1381
|
+
const msg = t("required", state);
|
|
1243
1382
|
errors.push(`${key}: ${msg}`);
|
|
1244
|
-
markFieldValidity(input, msg);
|
|
1383
|
+
markFieldValidity(input, msg, context);
|
|
1245
1384
|
return { value: null, errors };
|
|
1246
1385
|
}
|
|
1247
1386
|
if (input) {
|
|
@@ -1263,8 +1402,6 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
1263
1402
|
inputs.forEach((input, index) => {
|
|
1264
1403
|
if (index < value.length) {
|
|
1265
1404
|
input.value = value[index] != null ? String(value[index]) : "";
|
|
1266
|
-
input.classList.remove("invalid");
|
|
1267
|
-
input.title = "";
|
|
1268
1405
|
clearFieldError(input);
|
|
1269
1406
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1270
1407
|
}
|
|
@@ -1278,8 +1415,6 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
1278
1415
|
const input = scopeRoot.querySelector(`[name="${fieldPath}"]`);
|
|
1279
1416
|
if (input) {
|
|
1280
1417
|
input.value = value != null ? String(value) : "";
|
|
1281
|
-
input.classList.remove("invalid");
|
|
1282
|
-
input.title = "";
|
|
1283
1418
|
clearFieldError(input);
|
|
1284
1419
|
if (input instanceof HTMLTextAreaElement) {
|
|
1285
1420
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
@@ -1774,24 +1909,27 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1774
1909
|
}
|
|
1775
1910
|
function validateNumberElement(element, key, context) {
|
|
1776
1911
|
const errors = [];
|
|
1777
|
-
const { scopeRoot,
|
|
1778
|
-
const
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
if (!hasError) {
|
|
1793
|
-
markFieldValidity(input, null);
|
|
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 });
|
|
1916
|
+
}
|
|
1917
|
+
if (element.max != null && v > element.max) {
|
|
1918
|
+
return t("maxValue", state, { max: element.max });
|
|
1919
|
+
}
|
|
1920
|
+
return null;
|
|
1921
|
+
};
|
|
1922
|
+
const validateNumberInput = (input, fieldKey) => {
|
|
1923
|
+
const raw = input.value;
|
|
1924
|
+
if (raw === "") {
|
|
1925
|
+
markFieldValidity(input, null, context);
|
|
1926
|
+
return null;
|
|
1794
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;
|
|
1795
1933
|
};
|
|
1796
1934
|
if (element.multiple) {
|
|
1797
1935
|
const inputs = scopeRoot.querySelectorAll(
|
|
@@ -1799,62 +1937,21 @@ function validateNumberElement(element, key, context) {
|
|
|
1799
1937
|
);
|
|
1800
1938
|
const values = [];
|
|
1801
1939
|
inputs.forEach((input, index) => {
|
|
1802
|
-
|
|
1803
|
-
if (raw === "") {
|
|
1804
|
-
values.push(null);
|
|
1805
|
-
markFieldValidity(input, null);
|
|
1806
|
-
return;
|
|
1807
|
-
}
|
|
1808
|
-
const v = parseFloat(raw);
|
|
1809
|
-
if (!skipValidation && !Number.isFinite(v)) {
|
|
1810
|
-
const msg = t("notANumber", context.state);
|
|
1811
|
-
errors.push(`${key}[${index}]: ${msg}`);
|
|
1812
|
-
markFieldValidity(input, msg);
|
|
1813
|
-
values.push(null);
|
|
1814
|
-
return;
|
|
1815
|
-
}
|
|
1816
|
-
validateNumberInput(input, v, `${key}[${index}]`);
|
|
1817
|
-
values.push(applyDecimals(v, element.decimals));
|
|
1940
|
+
values.push(validateNumberInput(input, `${key}[${index}]`));
|
|
1818
1941
|
});
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
const minCount = element.minCount ?? 0;
|
|
1822
|
-
const maxCount = element.maxCount ?? Infinity;
|
|
1823
|
-
const filteredValues = values.filter((v) => v !== null);
|
|
1824
|
-
if (element.required && filteredValues.length === 0) {
|
|
1825
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
1826
|
-
}
|
|
1827
|
-
if (filteredValues.length < minCount) {
|
|
1828
|
-
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
1829
|
-
}
|
|
1830
|
-
if (filteredValues.length > maxCount) {
|
|
1831
|
-
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1942
|
+
const filledCount = values.filter((v) => v !== null).length;
|
|
1943
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
1834
1944
|
return { value: values, errors };
|
|
1835
1945
|
} else {
|
|
1836
1946
|
const input = scopeRoot.querySelector(`[name="${key}"]`);
|
|
1837
|
-
|
|
1838
|
-
const { state } = context;
|
|
1839
|
-
if (!skipValidation && element.required && raw === "") {
|
|
1947
|
+
if (element.required && (input?.value ?? "") === "") {
|
|
1840
1948
|
const msg = t("required", state);
|
|
1841
1949
|
errors.push(`${key}: ${msg}`);
|
|
1842
|
-
markFieldValidity(input, msg);
|
|
1843
|
-
return { value: null, errors };
|
|
1844
|
-
}
|
|
1845
|
-
if (raw === "") {
|
|
1846
|
-
markFieldValidity(input, null);
|
|
1847
|
-
return { value: null, errors };
|
|
1848
|
-
}
|
|
1849
|
-
const v = parseFloat(raw);
|
|
1850
|
-
if (!skipValidation && !Number.isFinite(v)) {
|
|
1851
|
-
const msg = t("notANumber", state);
|
|
1852
|
-
errors.push(`${key}: ${msg}`);
|
|
1853
|
-
markFieldValidity(input, msg);
|
|
1950
|
+
markFieldValidity(input, msg, context);
|
|
1854
1951
|
return { value: null, errors };
|
|
1855
1952
|
}
|
|
1856
|
-
|
|
1857
|
-
return { value:
|
|
1953
|
+
if (!input) return { value: null, errors };
|
|
1954
|
+
return { value: validateNumberInput(input, key), errors };
|
|
1858
1955
|
}
|
|
1859
1956
|
}
|
|
1860
1957
|
function applyDecimals(v, decimals) {
|
|
@@ -1876,8 +1973,6 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1876
1973
|
inputs.forEach((input, index) => {
|
|
1877
1974
|
if (index < value.length) {
|
|
1878
1975
|
input.value = value[index] != null ? String(value[index]) : "";
|
|
1879
|
-
input.classList.remove("invalid");
|
|
1880
|
-
input.title = "";
|
|
1881
1976
|
clearFieldError(input);
|
|
1882
1977
|
}
|
|
1883
1978
|
});
|
|
@@ -1892,8 +1987,6 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1892
1987
|
);
|
|
1893
1988
|
if (input) {
|
|
1894
1989
|
input.value = value != null ? String(value) : "";
|
|
1895
|
-
input.classList.remove("invalid");
|
|
1896
|
-
input.title = "";
|
|
1897
1990
|
clearFieldError(input);
|
|
1898
1991
|
}
|
|
1899
1992
|
}
|
|
@@ -2081,23 +2174,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
2081
2174
|
}
|
|
2082
2175
|
function validateSelectElement(element, key, context) {
|
|
2083
2176
|
const errors = [];
|
|
2084
|
-
const { scopeRoot
|
|
2085
|
-
const validateMultipleCount = (key2, values, element2, filterFn) => {
|
|
2086
|
-
if (skipValidation) return;
|
|
2087
|
-
const { state } = context;
|
|
2088
|
-
const filteredValues = values.filter(filterFn);
|
|
2089
|
-
const minCount = "minCount" in element2 ? element2.minCount ?? 0 : 0;
|
|
2090
|
-
const maxCount = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
|
|
2091
|
-
if (element2.required && filteredValues.length === 0) {
|
|
2092
|
-
errors.push(`${key2}: ${t("required", state)}`);
|
|
2093
|
-
}
|
|
2094
|
-
if (filteredValues.length < minCount) {
|
|
2095
|
-
errors.push(`${key2}: ${t("minItems", state, { min: minCount })}`);
|
|
2096
|
-
}
|
|
2097
|
-
if (filteredValues.length > maxCount) {
|
|
2098
|
-
errors.push(`${key2}: ${t("maxItems", state, { max: maxCount })}`);
|
|
2099
|
-
}
|
|
2100
|
-
};
|
|
2177
|
+
const { scopeRoot } = context;
|
|
2101
2178
|
if ("multiple" in element && element.multiple) {
|
|
2102
2179
|
const inputs = scopeRoot.querySelectorAll(
|
|
2103
2180
|
`[name^="${key}\\["]`
|
|
@@ -2106,21 +2183,21 @@ function validateSelectElement(element, key, context) {
|
|
|
2106
2183
|
inputs.forEach((input) => {
|
|
2107
2184
|
const val = input?.value ?? "";
|
|
2108
2185
|
values.push(val === "" ? null : val);
|
|
2109
|
-
markFieldValidity(input, null);
|
|
2186
|
+
markFieldValidity(input, null, context);
|
|
2110
2187
|
});
|
|
2111
|
-
|
|
2188
|
+
const filledCount = values.filter((v) => v != null).length;
|
|
2189
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
2112
2190
|
return { value: values, errors };
|
|
2113
2191
|
} else {
|
|
2114
2192
|
const input = scopeRoot.querySelector(`[name="${key}"]`);
|
|
2115
2193
|
const val = input?.value ?? "";
|
|
2116
|
-
if (
|
|
2194
|
+
if (element.required && val === "") {
|
|
2117
2195
|
const msg = t("required", context.state);
|
|
2118
2196
|
errors.push(`${key}: ${msg}`);
|
|
2119
|
-
markFieldValidity(input, msg);
|
|
2197
|
+
markFieldValidity(input, msg, context);
|
|
2120
2198
|
return { value: null, errors };
|
|
2121
|
-
} else {
|
|
2122
|
-
markFieldValidity(input, null);
|
|
2123
2199
|
}
|
|
2200
|
+
markFieldValidity(input, null, context);
|
|
2124
2201
|
return { value: val === "" ? null : val, errors };
|
|
2125
2202
|
}
|
|
2126
2203
|
}
|
|
@@ -2161,8 +2238,6 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
2161
2238
|
options.forEach((option) => {
|
|
2162
2239
|
option.selected = option.value === strValue;
|
|
2163
2240
|
});
|
|
2164
|
-
select.classList.remove("invalid");
|
|
2165
|
-
select.title = "";
|
|
2166
2241
|
clearFieldError(select);
|
|
2167
2242
|
}
|
|
2168
2243
|
});
|
|
@@ -2183,8 +2258,6 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
2183
2258
|
options.forEach((option) => {
|
|
2184
2259
|
option.selected = option.value === strValue;
|
|
2185
2260
|
});
|
|
2186
|
-
select.classList.remove("invalid");
|
|
2187
|
-
select.title = "";
|
|
2188
2261
|
clearFieldError(select);
|
|
2189
2262
|
}
|
|
2190
2263
|
}
|
|
@@ -2506,26 +2579,11 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
|
2506
2579
|
}
|
|
2507
2580
|
function validateSwitcherElement(element, key, context) {
|
|
2508
2581
|
const errors = [];
|
|
2509
|
-
const { scopeRoot,
|
|
2510
|
-
const validateMultipleCount = (fieldKey, values, el, filterFn) => {
|
|
2511
|
-
if (skipValidation) return;
|
|
2512
|
-
const { state } = context;
|
|
2513
|
-
const filteredValues = values.filter(filterFn);
|
|
2514
|
-
const minCount = "minCount" in el ? el.minCount ?? 0 : 0;
|
|
2515
|
-
const maxCount = "maxCount" in el ? el.maxCount ?? Infinity : Infinity;
|
|
2516
|
-
if (el.required && filteredValues.length === 0) {
|
|
2517
|
-
errors.push(`${fieldKey}: ${t("required", state)}`);
|
|
2518
|
-
}
|
|
2519
|
-
if (filteredValues.length < minCount) {
|
|
2520
|
-
errors.push(`${fieldKey}: ${t("minItems", state, { min: minCount })}`);
|
|
2521
|
-
}
|
|
2522
|
-
if (filteredValues.length > maxCount) {
|
|
2523
|
-
errors.push(`${fieldKey}: ${t("maxItems", state, { max: maxCount })}`);
|
|
2524
|
-
}
|
|
2525
|
-
};
|
|
2582
|
+
const { scopeRoot, state } = context;
|
|
2526
2583
|
const validOptionValues = new Set(
|
|
2527
2584
|
"options" in element ? element.options.map((o) => o.value) : []
|
|
2528
2585
|
);
|
|
2586
|
+
const optionError = (val) => val !== "" && !validOptionValues.has(val) ? t("invalidOption", state) : null;
|
|
2529
2587
|
if ("multiple" in element && element.multiple) {
|
|
2530
2588
|
const inputs = scopeRoot.querySelectorAll(
|
|
2531
2589
|
`input[type="hidden"][name^="${key}\\["]`
|
|
@@ -2534,37 +2592,36 @@ function validateSwitcherElement(element, key, context) {
|
|
|
2534
2592
|
inputs.forEach((input) => {
|
|
2535
2593
|
const val = input?.value ?? "";
|
|
2536
2594
|
values.push(val === "" ? null : val);
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
errors.push(`${key}: ${msg}`);
|
|
2541
|
-
} else {
|
|
2542
|
-
markFieldValidity(input, null);
|
|
2543
|
-
}
|
|
2595
|
+
const msg = optionError(val);
|
|
2596
|
+
if (msg !== null) errors.push(`${key}: ${msg}`);
|
|
2597
|
+
markFieldValidity(switcherGroupOf(input), msg, context);
|
|
2544
2598
|
});
|
|
2545
|
-
|
|
2599
|
+
const filledCount = values.filter((v) => v != null).length;
|
|
2600
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
2546
2601
|
return { value: values, errors };
|
|
2547
2602
|
} else {
|
|
2548
2603
|
const input = scopeRoot.querySelector(
|
|
2549
2604
|
`input[type="hidden"][name="${key}"]`
|
|
2550
2605
|
);
|
|
2551
2606
|
const val = input?.value ?? "";
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
markFieldValidity(input, msg);
|
|
2556
|
-
return { value: null, errors };
|
|
2557
|
-
}
|
|
2558
|
-
if (!skipValidation && val !== "" && !validOptionValues.has(val)) {
|
|
2559
|
-
const msg = t("invalidOption", context.state);
|
|
2607
|
+
const msg = element.required && val === "" ? t("required", state) : optionError(val);
|
|
2608
|
+
if (input) markFieldValidity(switcherGroupOf(input), msg, context);
|
|
2609
|
+
if (msg !== null) {
|
|
2560
2610
|
errors.push(`${key}: ${msg}`);
|
|
2561
|
-
markFieldValidity(input, msg);
|
|
2562
2611
|
return { value: null, errors };
|
|
2563
2612
|
}
|
|
2564
|
-
markFieldValidity(input, null);
|
|
2565
2613
|
return { value: val === "" ? null : val, errors };
|
|
2566
2614
|
}
|
|
2567
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
|
+
}
|
|
2568
2625
|
function updateSwitcherField(element, fieldPath, value, context) {
|
|
2569
2626
|
const { scopeRoot } = context;
|
|
2570
2627
|
if ("multiple" in element && element.multiple) {
|
|
@@ -2592,9 +2649,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2592
2649
|
}
|
|
2593
2650
|
});
|
|
2594
2651
|
}
|
|
2595
|
-
input
|
|
2596
|
-
input.title = "";
|
|
2597
|
-
clearFieldError(input);
|
|
2652
|
+
clearFieldError(switcherGroupOf(input));
|
|
2598
2653
|
}
|
|
2599
2654
|
});
|
|
2600
2655
|
if (value.length !== inputs.length) {
|
|
@@ -2620,9 +2675,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2620
2675
|
}
|
|
2621
2676
|
});
|
|
2622
2677
|
}
|
|
2623
|
-
input
|
|
2624
|
-
input.title = "";
|
|
2625
|
-
clearFieldError(input);
|
|
2678
|
+
clearFieldError(switcherGroupOf(input));
|
|
2626
2679
|
}
|
|
2627
2680
|
}
|
|
2628
2681
|
}
|
|
@@ -3144,14 +3197,20 @@ function ensureFileStyles() {
|
|
|
3144
3197
|
padding: 6px;
|
|
3145
3198
|
}
|
|
3146
3199
|
|
|
3147
|
-
/* \u2500\u2500\u2500
|
|
3148
|
-
.fb-
|
|
3200
|
+
/* \u2500\u2500\u2500 Footer row below multi grid: N/max counter + clear-all \u2500\u2500\u2500 */
|
|
3201
|
+
.fb-multi-footer {
|
|
3149
3202
|
margin-top: 10px;
|
|
3150
3203
|
display: flex;
|
|
3151
3204
|
align-items: center;
|
|
3152
|
-
|
|
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;
|
|
3153
3211
|
}
|
|
3154
3212
|
.fb-clear-all-btn {
|
|
3213
|
+
margin-left: auto;
|
|
3155
3214
|
font-size: 12px;
|
|
3156
3215
|
color: #94a3b8;
|
|
3157
3216
|
background: none;
|
|
@@ -3401,22 +3460,54 @@ function createFileTile() {
|
|
|
3401
3460
|
tile.className = "fb-tile";
|
|
3402
3461
|
return tile;
|
|
3403
3462
|
}
|
|
3404
|
-
function
|
|
3405
|
-
const
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
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
|
+
}
|
|
3416
3494
|
}
|
|
3417
|
-
function
|
|
3418
|
-
const
|
|
3419
|
-
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");
|
|
3420
3511
|
}
|
|
3421
3512
|
function addDeleteButton(container, state, onDelete) {
|
|
3422
3513
|
const existingOverlay = container.querySelector(".delete-overlay");
|
|
@@ -4271,7 +4362,8 @@ async function handleFileSelect(opts) {
|
|
|
4271
4362
|
const formats = allowedExtensions.join(", ");
|
|
4272
4363
|
showFileError(
|
|
4273
4364
|
container,
|
|
4274
|
-
t("invalidFileExtension", state, { name: file.name, formats })
|
|
4365
|
+
t("invalidFileExtension", state, { name: file.name, formats }),
|
|
4366
|
+
state
|
|
4275
4367
|
);
|
|
4276
4368
|
return;
|
|
4277
4369
|
}
|
|
@@ -4279,14 +4371,16 @@ async function handleFileSelect(opts) {
|
|
|
4279
4371
|
const mimes = allowedMimes.join(", ");
|
|
4280
4372
|
showFileError(
|
|
4281
4373
|
container,
|
|
4282
|
-
t("invalidFileMime", state, { name: file.name, type: file.type, mimes })
|
|
4374
|
+
t("invalidFileMime", state, { name: file.name, type: file.type, mimes }),
|
|
4375
|
+
state
|
|
4283
4376
|
);
|
|
4284
4377
|
return;
|
|
4285
4378
|
}
|
|
4286
4379
|
if (!isFileSizeAllowed(file, maxSizeMB)) {
|
|
4287
4380
|
showFileError(
|
|
4288
4381
|
container,
|
|
4289
|
-
t("fileTooLarge", state, { name: file.name, maxSize: maxSizeMB })
|
|
4382
|
+
t("fileTooLarge", state, { name: file.name, maxSize: maxSizeMB }),
|
|
4383
|
+
state
|
|
4290
4384
|
);
|
|
4291
4385
|
return;
|
|
4292
4386
|
}
|
|
@@ -4482,7 +4576,7 @@ async function runMultiFileBatch(opts, files, listEl, errorTarget) {
|
|
|
4482
4576
|
state
|
|
4483
4577
|
);
|
|
4484
4578
|
if (errorTarget) {
|
|
4485
|
-
if (errorMessage) showFileError(errorTarget, errorMessage);
|
|
4579
|
+
if (errorMessage) showFileError(errorTarget, errorMessage, state);
|
|
4486
4580
|
else clearFileError(errorTarget);
|
|
4487
4581
|
}
|
|
4488
4582
|
const handle = coordinator.beginBatch(accepted.length);
|
|
@@ -4501,11 +4595,8 @@ async function runMultiFileBatch(opts, files, listEl, errorTarget) {
|
|
|
4501
4595
|
}
|
|
4502
4596
|
const { wasLast } = handle.end();
|
|
4503
4597
|
if (wasLast) updateCallback();
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
if (combined) showFileError(errorTarget, combined);
|
|
4507
|
-
else clearFileError(errorTarget);
|
|
4508
|
-
}
|
|
4598
|
+
const combined = buildBatchErrorMessage(errorMessage, failures, state);
|
|
4599
|
+
if (errorTarget && combined) showFileError(errorTarget, combined, state);
|
|
4509
4600
|
}
|
|
4510
4601
|
function setupFilesDropHandler(opts) {
|
|
4511
4602
|
const { filesContainer } = opts;
|
|
@@ -4615,7 +4706,7 @@ async function handleLibraryPickMulti(opts) {
|
|
|
4615
4706
|
selectedResourceIds: knownRids
|
|
4616
4707
|
});
|
|
4617
4708
|
} catch (error) {
|
|
4618
|
-
showFileError(wrapper, extractPickerError(error, state));
|
|
4709
|
+
showFileError(wrapper, extractPickerError(error, state), state);
|
|
4619
4710
|
return;
|
|
4620
4711
|
}
|
|
4621
4712
|
if (picked.length === 0) return;
|
|
@@ -4643,7 +4734,8 @@ async function handleLibraryPickMulti(opts) {
|
|
|
4643
4734
|
if (skipped > 0) {
|
|
4644
4735
|
showFileError(
|
|
4645
4736
|
wrapper,
|
|
4646
|
-
t("filesLimitExceeded", state, { skipped, max: maxCount })
|
|
4737
|
+
t("filesLimitExceeded", state, { skipped, max: maxCount }),
|
|
4738
|
+
state
|
|
4647
4739
|
);
|
|
4648
4740
|
}
|
|
4649
4741
|
return;
|
|
@@ -4652,7 +4744,8 @@ async function handleLibraryPickMulti(opts) {
|
|
|
4652
4744
|
if (skipped > 0) {
|
|
4653
4745
|
showFileError(
|
|
4654
4746
|
wrapper,
|
|
4655
|
-
t("filesLimitExceeded", state, { skipped, max: maxCount })
|
|
4747
|
+
t("filesLimitExceeded", state, { skipped, max: maxCount }),
|
|
4748
|
+
state
|
|
4656
4749
|
);
|
|
4657
4750
|
}
|
|
4658
4751
|
for (const resource of accepted) {
|
|
@@ -4689,7 +4782,7 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
|
|
|
4689
4782
|
selectedResourceIds: []
|
|
4690
4783
|
});
|
|
4691
4784
|
} catch (error) {
|
|
4692
|
-
showFileError(container, extractPickerError(error, state));
|
|
4785
|
+
showFileError(container, extractPickerError(error, state), state);
|
|
4693
4786
|
return;
|
|
4694
4787
|
}
|
|
4695
4788
|
if (picked.length === 0) return;
|
|
@@ -4702,7 +4795,7 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
|
|
|
4702
4795
|
state
|
|
4703
4796
|
);
|
|
4704
4797
|
if (validationError !== null) {
|
|
4705
|
-
showFileError(container, validationError);
|
|
4798
|
+
showFileError(container, validationError, state);
|
|
4706
4799
|
return;
|
|
4707
4800
|
}
|
|
4708
4801
|
clearFileError(container);
|
|
@@ -4939,10 +5032,7 @@ function buildPlaceholderTile(isDragOver = false) {
|
|
|
4939
5032
|
div.className = `fb-multi-placeholder fb-checker${isDragOver ? " fb-drag-over" : ""}`;
|
|
4940
5033
|
return div;
|
|
4941
5034
|
}
|
|
4942
|
-
function
|
|
4943
|
-
if (ridCount <= 1) return null;
|
|
4944
|
-
const row = document.createElement("div");
|
|
4945
|
-
row.className = "fb-clear-all-row";
|
|
5035
|
+
function buildClearAllButton(state, onClearAll) {
|
|
4946
5036
|
const clearBtn = document.createElement("button");
|
|
4947
5037
|
clearBtn.type = "button";
|
|
4948
5038
|
clearBtn.className = "fb-clear-all-btn";
|
|
@@ -4953,9 +5043,38 @@ function buildClearAllRow(state, ridCount, onClearAll) {
|
|
|
4953
5043
|
onClearAll();
|
|
4954
5044
|
}
|
|
4955
5045
|
};
|
|
4956
|
-
|
|
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));
|
|
4957
5064
|
return row;
|
|
4958
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
|
+
}
|
|
4959
5078
|
var gridResizeObservers = /* @__PURE__ */ new WeakMap();
|
|
4960
5079
|
var gridMeasureFrames = /* @__PURE__ */ new WeakMap();
|
|
4961
5080
|
function cancelPendingMeasure(container) {
|
|
@@ -5048,6 +5167,7 @@ function renderResourcePills(opts) {
|
|
|
5048
5167
|
grid2.appendChild(tile);
|
|
5049
5168
|
}
|
|
5050
5169
|
}
|
|
5170
|
+
clearFileError(container, "limit");
|
|
5051
5171
|
return;
|
|
5052
5172
|
}
|
|
5053
5173
|
const outerDiv = document.createElement("div");
|
|
@@ -5127,10 +5247,14 @@ function renderResourcePills(opts) {
|
|
|
5127
5247
|
}
|
|
5128
5248
|
}
|
|
5129
5249
|
});
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
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);
|
|
5134
5258
|
}
|
|
5135
5259
|
function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
5136
5260
|
const state = ctx.state;
|
|
@@ -5280,8 +5404,9 @@ function buildAcceptAttribute(accept) {
|
|
|
5280
5404
|
...accept.mime ?? []
|
|
5281
5405
|
].join(",");
|
|
5282
5406
|
}
|
|
5283
|
-
function
|
|
5407
|
+
function renderMultiFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
5284
5408
|
const state = ctx.state;
|
|
5409
|
+
const maxFiles = element.maxCount ?? Infinity;
|
|
5285
5410
|
const filesWrapper = document.createElement("div");
|
|
5286
5411
|
filesWrapper.className = "fb-row";
|
|
5287
5412
|
filesWrapper.dataset.filesWrapper = pathKey;
|
|
@@ -5485,21 +5610,23 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
|
|
|
5485
5610
|
};
|
|
5486
5611
|
setupFilesDropHandler({ ...sharedHandlerOpts, filesContainer });
|
|
5487
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
|
+
});
|
|
5488
5627
|
updateFilesDisplay();
|
|
5489
5628
|
wrapper.appendChild(filesWrapper);
|
|
5490
5629
|
}
|
|
5491
|
-
function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
|
|
5492
|
-
setupMultiFileEditMode(element, ctx, wrapper, pathKey, Infinity);
|
|
5493
|
-
}
|
|
5494
|
-
function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
5495
|
-
setupMultiFileEditMode(
|
|
5496
|
-
element,
|
|
5497
|
-
ctx,
|
|
5498
|
-
wrapper,
|
|
5499
|
-
pathKey,
|
|
5500
|
-
element.maxCount ?? Infinity
|
|
5501
|
-
);
|
|
5502
|
-
}
|
|
5503
5630
|
|
|
5504
5631
|
// src/components/file/validate.ts
|
|
5505
5632
|
function readMultiFileResourceIds(scopeRoot, fullKey) {
|
|
@@ -5521,86 +5648,100 @@ function readMultiFileResourceIds(scopeRoot, fullKey) {
|
|
|
5521
5648
|
}
|
|
5522
5649
|
return parsed;
|
|
5523
5650
|
}
|
|
5524
|
-
function
|
|
5525
|
-
const
|
|
5526
|
-
const maxFiles = "maxCount" in element ? element.maxCount ?? Infinity : Infinity;
|
|
5527
|
-
if (element.required && resourceIds.length === 0) {
|
|
5528
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
5529
|
-
}
|
|
5530
|
-
if (resourceIds.length < minFiles) {
|
|
5531
|
-
errors.push(`${key}: ${t("minFiles", state, { min: minFiles })}`);
|
|
5532
|
-
}
|
|
5533
|
-
if (resourceIds.length > maxFiles) {
|
|
5534
|
-
errors.push(`${key}: ${t("maxFiles", state, { max: maxFiles })}`);
|
|
5535
|
-
}
|
|
5536
|
-
}
|
|
5537
|
-
function validateFileTypes(key, resourceIds, element, state, errors) {
|
|
5651
|
+
function validateFileTypes(resourceIds, element, state) {
|
|
5652
|
+
const messages = [];
|
|
5538
5653
|
const acceptField = "accept" in element ? element.accept : void 0;
|
|
5539
5654
|
const allowedExtensions = getAllowedExtensions(acceptField);
|
|
5540
5655
|
const allowedMimes = getAllowedMimes(acceptField);
|
|
5541
|
-
if (allowedExtensions.length === 0 && allowedMimes.length === 0)
|
|
5656
|
+
if (allowedExtensions.length === 0 && allowedMimes.length === 0) {
|
|
5657
|
+
return messages;
|
|
5658
|
+
}
|
|
5542
5659
|
const formats = allowedExtensions.join(", ");
|
|
5543
5660
|
const mimes = allowedMimes.join(", ");
|
|
5544
5661
|
for (const rid of resourceIds) {
|
|
5545
5662
|
const meta = state.resourceIndex.get(rid);
|
|
5546
5663
|
const fileName = meta?.name ?? rid;
|
|
5547
5664
|
if (allowedExtensions.length > 0 && !isFileExtensionAllowed(fileName, allowedExtensions)) {
|
|
5548
|
-
|
|
5549
|
-
|
|
5665
|
+
messages.push(
|
|
5666
|
+
t("invalidFileExtension", state, { name: fileName, formats })
|
|
5550
5667
|
);
|
|
5551
5668
|
continue;
|
|
5552
5669
|
}
|
|
5553
5670
|
if (allowedMimes.length > 0 && !meta?.inferredFromExtension) {
|
|
5554
5671
|
const mimeType = meta?.type ?? "";
|
|
5555
5672
|
if (!isMimeAllowed(mimeType, allowedMimes)) {
|
|
5556
|
-
|
|
5557
|
-
|
|
5673
|
+
messages.push(
|
|
5674
|
+
t("invalidFileMime", state, {
|
|
5675
|
+
name: fileName,
|
|
5676
|
+
type: mimeType,
|
|
5677
|
+
mimes
|
|
5678
|
+
})
|
|
5558
5679
|
);
|
|
5559
5680
|
}
|
|
5560
5681
|
}
|
|
5561
5682
|
}
|
|
5683
|
+
return messages;
|
|
5562
5684
|
}
|
|
5563
|
-
function validateFileSizes(
|
|
5685
|
+
function validateFileSizes(resourceIds, element, state) {
|
|
5686
|
+
const messages = [];
|
|
5564
5687
|
const maxSizeMB = "maxSize" in element ? element.maxSize ?? Infinity : Infinity;
|
|
5565
|
-
if (maxSizeMB === Infinity) return;
|
|
5688
|
+
if (maxSizeMB === Infinity) return messages;
|
|
5566
5689
|
for (const rid of resourceIds) {
|
|
5567
5690
|
const meta = state.resourceIndex.get(rid);
|
|
5568
5691
|
if (!meta) continue;
|
|
5569
5692
|
if (meta.size > maxSizeMB * 1024 * 1024) {
|
|
5570
|
-
|
|
5571
|
-
|
|
5693
|
+
messages.push(
|
|
5694
|
+
t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })
|
|
5572
5695
|
);
|
|
5573
5696
|
}
|
|
5574
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}`);
|
|
5575
5708
|
}
|
|
5576
5709
|
function validateMultiFile(element, key, context) {
|
|
5577
|
-
const { scopeRoot,
|
|
5578
|
-
const errors = [];
|
|
5710
|
+
const { scopeRoot, path, state } = context;
|
|
5579
5711
|
const fullKey = pathJoin(path, key);
|
|
5580
5712
|
const resourceIds = readMultiFileResourceIds(scopeRoot, fullKey);
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
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
|
+
};
|
|
5587
5725
|
}
|
|
5588
5726
|
function validateSingleFile(element, key, context) {
|
|
5589
|
-
const { scopeRoot,
|
|
5590
|
-
const errors = [];
|
|
5727
|
+
const { scopeRoot, state } = context;
|
|
5591
5728
|
const input = scopeRoot.querySelector(
|
|
5592
5729
|
`input[name="${key}"][type="hidden"]`
|
|
5593
5730
|
);
|
|
5594
5731
|
const rid = input?.value ?? "";
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
}
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
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
|
+
];
|
|
5602
5740
|
}
|
|
5603
|
-
return {
|
|
5741
|
+
return {
|
|
5742
|
+
value: rid || null,
|
|
5743
|
+
errors: reportFileMessages(scopeRoot, key, key, messages, context)
|
|
5744
|
+
};
|
|
5604
5745
|
}
|
|
5605
5746
|
function validateFileElement(element, key, context) {
|
|
5606
5747
|
const isMultipleField = element.type === "files" || "multiple" in element && Boolean(element.multiple);
|
|
@@ -5650,12 +5791,20 @@ function buildEmptyReadonlyTile(state) {
|
|
|
5650
5791
|
return emptyState;
|
|
5651
5792
|
}
|
|
5652
5793
|
function renderMultiFileReadonly(rids, state, wrapper, pathKey, _marginTop) {
|
|
5653
|
-
addPrefillFilesToIndex(rids, state.resourceIndex);
|
|
5654
|
-
ensureFileStyles();
|
|
5655
5794
|
const filesWrapper = document.createElement("div");
|
|
5656
5795
|
filesWrapper.dataset.filesWrapper = pathKey;
|
|
5657
|
-
filesWrapper.dataset.resourceIds = JSON.stringify(rids);
|
|
5658
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();
|
|
5659
5808
|
if (rids.length === 0) {
|
|
5660
5809
|
const emptyEl = document.createElement("div");
|
|
5661
5810
|
emptyEl.className = "fb-tile-empty-text";
|
|
@@ -5715,14 +5864,14 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
|
5715
5864
|
if (isElementReadonly(element, ctx.state, ctx)) {
|
|
5716
5865
|
renderFilesElementReadonly(element, ctx, wrapper, pathKey);
|
|
5717
5866
|
} else {
|
|
5718
|
-
|
|
5867
|
+
renderMultiFileElementEdit(element, ctx, wrapper, pathKey);
|
|
5719
5868
|
}
|
|
5720
5869
|
}
|
|
5721
5870
|
function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
|
|
5722
5871
|
if (isElementReadonly(element, ctx.state, ctx)) {
|
|
5723
5872
|
renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey);
|
|
5724
5873
|
} else {
|
|
5725
|
-
|
|
5874
|
+
renderMultiFileElementEdit(element, ctx, wrapper, pathKey);
|
|
5726
5875
|
}
|
|
5727
5876
|
}
|
|
5728
5877
|
function updateFileField(element, fieldPath, value, context) {
|
|
@@ -5742,13 +5891,19 @@ function updateFileField(element, fieldPath, value, context) {
|
|
|
5742
5891
|
const filesWrapper = scopeRoot.querySelector(
|
|
5743
5892
|
`[data-files-wrapper="${fieldPath}"]`
|
|
5744
5893
|
);
|
|
5745
|
-
if (filesWrapper) {
|
|
5746
|
-
filesWrapper.dataset.resourceIds = JSON.stringify(value);
|
|
5747
|
-
} else {
|
|
5894
|
+
if (!filesWrapper) {
|
|
5748
5895
|
console.warn(
|
|
5749
5896
|
`updateFileField: [data-files-wrapper="${fieldPath}"] not found in DOM; data-resource-ids not updated`
|
|
5750
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
|
+
);
|
|
5751
5905
|
}
|
|
5906
|
+
setFiles(value);
|
|
5752
5907
|
} else {
|
|
5753
5908
|
const hiddenInput = scopeRoot.querySelector(
|
|
5754
5909
|
`input[name="${fieldPath}"][type="hidden"]`
|
|
@@ -6080,28 +6235,18 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
6080
6235
|
}
|
|
6081
6236
|
function validateColourElement(element, key, context) {
|
|
6082
6237
|
const errors = [];
|
|
6083
|
-
const { scopeRoot,
|
|
6238
|
+
const { scopeRoot, state } = context;
|
|
6084
6239
|
const validateColourValue = (input, val, fieldKey) => {
|
|
6085
|
-
const
|
|
6240
|
+
const normalized = val ? normalizeColourValue(val) : "";
|
|
6241
|
+
let msg = null;
|
|
6086
6242
|
if (!val) {
|
|
6087
|
-
if (
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
return "";
|
|
6095
|
-
}
|
|
6096
|
-
const normalized = normalizeColourValue(val);
|
|
6097
|
-
if (!skipValidation && !isValidHexColour(normalized)) {
|
|
6098
|
-
const msg = t("invalidHexColour", state);
|
|
6099
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
6100
|
-
markFieldValidity(input, msg);
|
|
6101
|
-
return val;
|
|
6102
|
-
}
|
|
6103
|
-
markFieldValidity(input, null);
|
|
6104
|
-
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;
|
|
6105
6250
|
};
|
|
6106
6251
|
if (element.multiple) {
|
|
6107
6252
|
const hexInputs = scopeRoot.querySelectorAll(
|
|
@@ -6110,38 +6255,17 @@ function validateColourElement(element, key, context) {
|
|
|
6110
6255
|
const values = [];
|
|
6111
6256
|
hexInputs.forEach((input, index) => {
|
|
6112
6257
|
const val = input?.value ?? "";
|
|
6113
|
-
|
|
6114
|
-
values.push(validated);
|
|
6258
|
+
values.push(validateColourValue(input, val, `${key}[${index}]`));
|
|
6115
6259
|
});
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
const minCount = element.minCount ?? 0;
|
|
6119
|
-
const maxCount = element.maxCount ?? Infinity;
|
|
6120
|
-
const filteredValues = values.filter((v) => v !== "");
|
|
6121
|
-
if (element.required && filteredValues.length === 0) {
|
|
6122
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
6123
|
-
}
|
|
6124
|
-
if (filteredValues.length < minCount) {
|
|
6125
|
-
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
6126
|
-
}
|
|
6127
|
-
if (filteredValues.length > maxCount) {
|
|
6128
|
-
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
6129
|
-
}
|
|
6130
|
-
}
|
|
6260
|
+
const filledCount = values.filter((v) => v !== "").length;
|
|
6261
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
6131
6262
|
return { value: values, errors };
|
|
6132
6263
|
} else {
|
|
6133
6264
|
const hexInput = scopeRoot.querySelector(
|
|
6134
6265
|
`[name="${key}"].colour-hex-input`
|
|
6135
6266
|
);
|
|
6136
6267
|
const val = hexInput?.value ?? "";
|
|
6137
|
-
|
|
6138
|
-
const msg = t("required", context.state);
|
|
6139
|
-
errors.push(`${key}: ${msg}`);
|
|
6140
|
-
markFieldValidity(hexInput, msg);
|
|
6141
|
-
return { value: "", errors };
|
|
6142
|
-
}
|
|
6143
|
-
const validated = validateColourValue(hexInput, val, key);
|
|
6144
|
-
return { value: validated, errors };
|
|
6268
|
+
return { value: validateColourValue(hexInput, val, key), errors };
|
|
6145
6269
|
}
|
|
6146
6270
|
}
|
|
6147
6271
|
function updateColourField(element, fieldPath, value, context) {
|
|
@@ -6160,8 +6284,6 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
6160
6284
|
if (index < value.length) {
|
|
6161
6285
|
const normalized = normalizeColourValue(value[index]);
|
|
6162
6286
|
hexInput.value = normalized;
|
|
6163
|
-
hexInput.classList.remove("invalid");
|
|
6164
|
-
hexInput.title = "";
|
|
6165
6287
|
clearFieldError(hexInput);
|
|
6166
6288
|
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
6167
6289
|
if (wrapper) {
|
|
@@ -6190,8 +6312,6 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
6190
6312
|
if (hexInput) {
|
|
6191
6313
|
const normalized = normalizeColourValue(value);
|
|
6192
6314
|
hexInput.value = normalized;
|
|
6193
|
-
hexInput.classList.remove("invalid");
|
|
6194
|
-
hexInput.title = "";
|
|
6195
6315
|
clearFieldError(hexInput);
|
|
6196
6316
|
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
6197
6317
|
if (wrapper) {
|
|
@@ -6505,7 +6625,7 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
6505
6625
|
}
|
|
6506
6626
|
function validateSliderElement(element, key, context) {
|
|
6507
6627
|
const errors = [];
|
|
6508
|
-
const { scopeRoot
|
|
6628
|
+
const { scopeRoot } = context;
|
|
6509
6629
|
if (element.min === void 0 || element.min === null) {
|
|
6510
6630
|
throw new Error(
|
|
6511
6631
|
`Slider validation: field "${key}" requires "min" property`
|
|
@@ -6524,39 +6644,20 @@ function validateSliderElement(element, key, context) {
|
|
|
6524
6644
|
const { state } = context;
|
|
6525
6645
|
const rawValue = slider.value;
|
|
6526
6646
|
if (!rawValue) {
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
markFieldValidity(slider, msg);
|
|
6531
|
-
return null;
|
|
6532
|
-
}
|
|
6533
|
-
markFieldValidity(slider, null);
|
|
6647
|
+
const msg2 = element.required ? t("required", state) : null;
|
|
6648
|
+
if (msg2 !== null) errors.push(`${fieldKey}: ${msg2}`);
|
|
6649
|
+
markFieldValidity(slider, msg2, context);
|
|
6534
6650
|
return null;
|
|
6535
6651
|
}
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
if (!skipValidation) {
|
|
6546
|
-
if (value < min) {
|
|
6547
|
-
const msg = t("minValue", state, { min });
|
|
6548
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
6549
|
-
markFieldValidity(slider, msg);
|
|
6550
|
-
return value;
|
|
6551
|
-
}
|
|
6552
|
-
if (value > max) {
|
|
6553
|
-
const msg = t("maxValue", state, { max });
|
|
6554
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
6555
|
-
markFieldValidity(slider, msg);
|
|
6556
|
-
return value;
|
|
6557
|
-
}
|
|
6558
|
-
}
|
|
6559
|
-
markFieldValidity(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);
|
|
6560
6661
|
return value;
|
|
6561
6662
|
};
|
|
6562
6663
|
if (element.multiple) {
|
|
@@ -6565,31 +6666,17 @@ function validateSliderElement(element, key, context) {
|
|
|
6565
6666
|
);
|
|
6566
6667
|
const values = [];
|
|
6567
6668
|
sliders.forEach((slider, index) => {
|
|
6568
|
-
|
|
6569
|
-
values.push(value);
|
|
6669
|
+
values.push(validateSliderValue(slider, `${key}[${index}]`));
|
|
6570
6670
|
});
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
const minCount = element.minCount ?? 0;
|
|
6574
|
-
const maxCount = element.maxCount ?? Infinity;
|
|
6575
|
-
const filteredValues = values.filter((v) => v !== null);
|
|
6576
|
-
if (element.required && filteredValues.length === 0) {
|
|
6577
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
6578
|
-
}
|
|
6579
|
-
if (filteredValues.length < minCount) {
|
|
6580
|
-
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
6581
|
-
}
|
|
6582
|
-
if (filteredValues.length > maxCount) {
|
|
6583
|
-
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
6584
|
-
}
|
|
6585
|
-
}
|
|
6671
|
+
const filledCount = values.filter((v) => v !== null).length;
|
|
6672
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
6586
6673
|
return { value: values, errors };
|
|
6587
6674
|
} else {
|
|
6588
6675
|
const slider = scopeRoot.querySelector(
|
|
6589
6676
|
`input[type="range"][name="${key}"]`
|
|
6590
6677
|
);
|
|
6591
6678
|
if (!slider) {
|
|
6592
|
-
if (
|
|
6679
|
+
if (element.required) {
|
|
6593
6680
|
errors.push(`${key}: ${t("required", context.state)}`);
|
|
6594
6681
|
}
|
|
6595
6682
|
return { value: null, errors };
|
|
@@ -6638,8 +6725,6 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
6638
6725
|
var(--fb-border-color) 100%
|
|
6639
6726
|
)`;
|
|
6640
6727
|
}
|
|
6641
|
-
slider.classList.remove("invalid");
|
|
6642
|
-
slider.title = "";
|
|
6643
6728
|
clearFieldError(slider);
|
|
6644
6729
|
}
|
|
6645
6730
|
});
|
|
@@ -6675,8 +6760,6 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
6675
6760
|
var(--fb-border-color) 100%
|
|
6676
6761
|
)`;
|
|
6677
6762
|
}
|
|
6678
|
-
slider.classList.remove("invalid");
|
|
6679
|
-
slider.title = "";
|
|
6680
6763
|
clearFieldError(slider);
|
|
6681
6764
|
}
|
|
6682
6765
|
}
|
|
@@ -6996,25 +7079,10 @@ function requireValidateElement(context) {
|
|
|
6996
7079
|
function validateContainerElement(element, key, context) {
|
|
6997
7080
|
const validateChild = requireValidateElement(context);
|
|
6998
7081
|
const errors = [];
|
|
6999
|
-
const { scopeRoot,
|
|
7082
|
+
const { scopeRoot, path } = context;
|
|
7000
7083
|
if (!("elements" in element)) {
|
|
7001
7084
|
return { value: null, errors };
|
|
7002
7085
|
}
|
|
7003
|
-
const validateContainerCount = (key2, items, element2) => {
|
|
7004
|
-
if (skipValidation) return;
|
|
7005
|
-
const { state } = context;
|
|
7006
|
-
const minItems = "minCount" in element2 ? element2.minCount ?? 0 : 0;
|
|
7007
|
-
const maxItems = "maxCount" in element2 ? element2.maxCount ?? Infinity : Infinity;
|
|
7008
|
-
if (element2.required && items.length === 0) {
|
|
7009
|
-
errors.push(`${key2}: ${t("required", state)}`);
|
|
7010
|
-
}
|
|
7011
|
-
if (items.length < minItems) {
|
|
7012
|
-
errors.push(`${key2}: ${t("minItems", state, { min: minItems })}`);
|
|
7013
|
-
}
|
|
7014
|
-
if (items.length > maxItems) {
|
|
7015
|
-
errors.push(`${key2}: ${t("maxItems", state, { max: maxItems })}`);
|
|
7016
|
-
}
|
|
7017
|
-
};
|
|
7018
7086
|
if ("multiple" in element && element.multiple) {
|
|
7019
7087
|
const items = [];
|
|
7020
7088
|
const containerWrappers = findDirectContainerRows(scopeRoot, key);
|
|
@@ -7047,7 +7115,7 @@ function validateContainerElement(element, key, context) {
|
|
|
7047
7115
|
const childKey = `${key}[${domIndex}].${child.key}`;
|
|
7048
7116
|
const childResult = validateChild(
|
|
7049
7117
|
{ ...child, key: childKey },
|
|
7050
|
-
{ path },
|
|
7118
|
+
{ path, inheritedReadonly: context.readonly },
|
|
7051
7119
|
itemContainer
|
|
7052
7120
|
);
|
|
7053
7121
|
if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
|
|
@@ -7058,7 +7126,7 @@ function validateContainerElement(element, key, context) {
|
|
|
7058
7126
|
});
|
|
7059
7127
|
items.push(itemData);
|
|
7060
7128
|
});
|
|
7061
|
-
|
|
7129
|
+
validateItemCount(element, key, items.length, context, errors);
|
|
7062
7130
|
return { value: items, errors };
|
|
7063
7131
|
} else {
|
|
7064
7132
|
const containerData = {};
|
|
@@ -7088,7 +7156,7 @@ function validateContainerElement(element, key, context) {
|
|
|
7088
7156
|
const childKey = `${key}.${child.key}`;
|
|
7089
7157
|
const childResult = validateChild(
|
|
7090
7158
|
{ ...child, key: childKey },
|
|
7091
|
-
{ path },
|
|
7159
|
+
{ path, inheritedReadonly: context.readonly },
|
|
7092
7160
|
containerContainer
|
|
7093
7161
|
);
|
|
7094
7162
|
if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
|
|
@@ -8457,8 +8525,24 @@ function renderTableElement(element, ctx, wrapper, pathKey) {
|
|
|
8457
8525
|
renderEditTable(element, initialData, pathKey, ctx, wrapper);
|
|
8458
8526
|
}
|
|
8459
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
|
+
}
|
|
8460
8544
|
function validateTableElement(element, key, context) {
|
|
8461
|
-
const { scopeRoot
|
|
8545
|
+
const { scopeRoot } = context;
|
|
8462
8546
|
const errors = [];
|
|
8463
8547
|
const cellsKey = element.fieldNames?.cells ?? "cells";
|
|
8464
8548
|
const hiddenInput = scopeRoot.querySelector(
|
|
@@ -8467,22 +8551,20 @@ function validateTableElement(element, key, context) {
|
|
|
8467
8551
|
if (!hiddenInput) {
|
|
8468
8552
|
return { value: null, errors };
|
|
8469
8553
|
}
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
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);
|
|
8475
8559
|
return { value: null, errors };
|
|
8476
8560
|
}
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
}
|
|
8485
|
-
}
|
|
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);
|
|
8486
8568
|
return { value, errors };
|
|
8487
8569
|
}
|
|
8488
8570
|
function updateTableField(element, fieldPath, value, context) {
|
|
@@ -9783,8 +9865,16 @@ function renderRichInputElement(element, ctx, wrapper, pathKey) {
|
|
|
9783
9865
|
renderEditMode(element, ctx, wrapper, pathKey, initialValue);
|
|
9784
9866
|
}
|
|
9785
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
|
+
}
|
|
9786
9876
|
function validateRichInputElement(element, key, context) {
|
|
9787
|
-
const { scopeRoot, state
|
|
9877
|
+
const { scopeRoot, state } = context;
|
|
9788
9878
|
const errors = [];
|
|
9789
9879
|
const textKey = element.textKey ?? "text";
|
|
9790
9880
|
const filesKey = element.filesKey ?? "files";
|
|
@@ -9794,17 +9884,11 @@ function validateRichInputElement(element, key, context) {
|
|
|
9794
9884
|
if (!hiddenInput) {
|
|
9795
9885
|
return { value: null, errors };
|
|
9796
9886
|
}
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
const
|
|
9800
|
-
|
|
9801
|
-
|
|
9802
|
-
} else {
|
|
9803
|
-
errors.push(`${key}: invalid richinput data`);
|
|
9804
|
-
return { value: null, errors };
|
|
9805
|
-
}
|
|
9806
|
-
} catch {
|
|
9807
|
-
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);
|
|
9808
9892
|
return { value: null, errors };
|
|
9809
9893
|
}
|
|
9810
9894
|
const textVal = rawValue[textKey];
|
|
@@ -9815,28 +9899,24 @@ function validateRichInputElement(element, key, context) {
|
|
|
9815
9899
|
[textKey]: text ?? null,
|
|
9816
9900
|
[filesKey]: files
|
|
9817
9901
|
};
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
if (
|
|
9825
|
-
|
|
9826
|
-
errors.push(
|
|
9827
|
-
`${key}: ${t("minLength", state, { min: element.minLength })}`
|
|
9828
|
-
);
|
|
9829
|
-
}
|
|
9830
|
-
if (element.maxLength != null && text.length > element.maxLength) {
|
|
9831
|
-
errors.push(
|
|
9832
|
-
`${key}: ${t("maxLength", state, { max: element.maxLength })}`
|
|
9833
|
-
);
|
|
9834
|
-
}
|
|
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 }));
|
|
9835
9910
|
}
|
|
9836
|
-
if (element.
|
|
9837
|
-
|
|
9911
|
+
if (element.maxLength != null && text.length > element.maxLength) {
|
|
9912
|
+
messages.push(t("maxLength", state, { max: element.maxLength }));
|
|
9838
9913
|
}
|
|
9839
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);
|
|
9840
9920
|
return { value, errors, spread: !!element.flatOutput };
|
|
9841
9921
|
}
|
|
9842
9922
|
function updateRichInputField(element, fieldPath, value, context) {
|
|
@@ -10599,12 +10679,13 @@ function renderElement2(element, ctx) {
|
|
|
10599
10679
|
wrapper.className = `fb-field-wrapper fb-size-${element.size || "md"}`;
|
|
10600
10680
|
wrapper.setAttribute("data-field-key", element.key);
|
|
10601
10681
|
wrapper.setAttribute("data-fb-width", element.width || "full");
|
|
10682
|
+
const pathKey = pathJoin(ctx.path, element.key);
|
|
10683
|
+
wrapper.setAttribute("data-field-path", pathKey);
|
|
10602
10684
|
const ops = getComponentOperations(element.type);
|
|
10603
10685
|
if (!ops?.ownsLabel) {
|
|
10604
10686
|
const label = createLabelContainer(element, ctx.state);
|
|
10605
10687
|
wrapper.appendChild(label);
|
|
10606
10688
|
}
|
|
10607
|
-
const pathKey = pathJoin(ctx.path, element.key);
|
|
10608
10689
|
dispatchToRenderer(element, ctx, wrapper, pathKey);
|
|
10609
10690
|
if (initiallyDisabled) {
|
|
10610
10691
|
wrapper.style.display = "none";
|
|
@@ -10811,6 +10892,9 @@ function createInstanceState(config) {
|
|
|
10811
10892
|
config?.translations
|
|
10812
10893
|
);
|
|
10813
10894
|
return {
|
|
10895
|
+
instanceId: generateInstanceId(),
|
|
10896
|
+
domIdCounter: 0,
|
|
10897
|
+
reportedInvalid: /* @__PURE__ */ new WeakSet(),
|
|
10814
10898
|
schema: null,
|
|
10815
10899
|
formRoot: null,
|
|
10816
10900
|
resourceIndex: /* @__PURE__ */ new Map(),
|
|
@@ -10825,6 +10909,7 @@ function createInstanceState(config) {
|
|
|
10825
10909
|
prefill: {},
|
|
10826
10910
|
syntheticElementIds: /* @__PURE__ */ new WeakMap(),
|
|
10827
10911
|
syntheticElementIdCounter: 0,
|
|
10912
|
+
multiFileSetters: /* @__PURE__ */ new WeakMap(),
|
|
10828
10913
|
enableIfObservers: /* @__PURE__ */ new Set(),
|
|
10829
10914
|
autoExpandObservers: /* @__PURE__ */ new Set(),
|
|
10830
10915
|
tooltipElements: /* @__PURE__ */ new Set()
|
|
@@ -11133,8 +11218,8 @@ var FormBuilderInstance = class {
|
|
|
11133
11218
|
// stacked listeners (hint clicks applied values N times) and destroy()
|
|
11134
11219
|
// left the last one on the host-owned root, retaining the instance.
|
|
11135
11220
|
this.prefillHintHandler = null;
|
|
11136
|
-
this.instanceId = generateInstanceId();
|
|
11137
11221
|
this.state = createInstanceState(config);
|
|
11222
|
+
this.instanceId = this.state.instanceId;
|
|
11138
11223
|
if (this.state.config.verboseErrors) {
|
|
11139
11224
|
if (!globalThis.__formBuilderInstances) {
|
|
11140
11225
|
globalThis.__formBuilderInstances = /* @__PURE__ */ new Set();
|
|
@@ -11631,11 +11716,21 @@ var FormBuilderInstance = class {
|
|
|
11631
11716
|
}
|
|
11632
11717
|
}
|
|
11633
11718
|
/**
|
|
11634
|
-
* Validate form and extract data
|
|
11635
|
-
*
|
|
11636
|
-
*
|
|
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: []`.
|
|
11637
11722
|
*/
|
|
11638
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) {
|
|
11639
11734
|
if (!this.state.schema || !this.state.formRoot)
|
|
11640
11735
|
return { valid: true, errors: [], data: {} };
|
|
11641
11736
|
const errors = [];
|
|
@@ -11648,7 +11743,8 @@ var FormBuilderInstance = class {
|
|
|
11648
11743
|
state: this.state,
|
|
11649
11744
|
instance: this,
|
|
11650
11745
|
path: ctx.path,
|
|
11651
|
-
|
|
11746
|
+
draftMarks: marks === "draft",
|
|
11747
|
+
readonly: isElementReadonly(element, this.state, ctx),
|
|
11652
11748
|
// Containers recurse into their children through this — threaded per
|
|
11653
11749
|
// pass, never module state (see ComponentContext.validateElement).
|
|
11654
11750
|
validateElement
|
|
@@ -11701,10 +11797,56 @@ var FormBuilderInstance = class {
|
|
|
11701
11797
|
};
|
|
11702
11798
|
}
|
|
11703
11799
|
/**
|
|
11704
|
-
*
|
|
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.
|
|
11705
11805
|
*/
|
|
11706
11806
|
getFormData() {
|
|
11707
|
-
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;
|
|
11708
11850
|
}
|
|
11709
11851
|
/**
|
|
11710
11852
|
* Submit form with validation
|
|
@@ -11945,6 +12087,7 @@ var FormBuilderInstance = class {
|
|
|
11945
12087
|
getElementLookupKey(element, this.state)
|
|
11946
12088
|
);
|
|
11947
12089
|
disabledWrapper.setAttribute("data-conditionally-disabled", "true");
|
|
12090
|
+
disabledWrapper.setAttribute("data-field-path", fullDomPath);
|
|
11948
12091
|
wrapper.parentNode?.replaceChild(disabledWrapper, wrapper);
|
|
11949
12092
|
}
|
|
11950
12093
|
} catch (error) {
|