@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/cjs/index.cjs
CHANGED
|
@@ -100,467 +100,201 @@ function createHiddenInput(name, value) {
|
|
|
100
100
|
return input;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
// src/utils/
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
103
|
+
// src/utils/styles.ts
|
|
104
|
+
function findErrorAnchor(input) {
|
|
105
|
+
var _a, _b, _c, _d;
|
|
106
|
+
return (_d = (_c = (_a = input.closest) == null ? void 0 : _a.call(input, ".fb-chip")) != null ? _c : (_b = input.closest) == null ? void 0 : _b.call(input, ".slider-container")) != null ? _d : input;
|
|
107
|
+
}
|
|
108
|
+
function findErrorNode(input) {
|
|
109
|
+
const anchor = findErrorAnchor(input);
|
|
110
|
+
const name = input.getAttribute("name");
|
|
111
|
+
const parent = anchor.parentElement;
|
|
112
|
+
if (name && parent) {
|
|
113
|
+
for (const child of Array.from(parent.children)) {
|
|
114
|
+
if (isInputErrorNode(child) && child.getAttribute("data-error-for") === name) {
|
|
115
|
+
return child;
|
|
116
|
+
}
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
+
const sibling = anchor.nextElementSibling;
|
|
120
|
+
return sibling && isInputErrorNode(sibling) ? sibling : null;
|
|
119
121
|
}
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
} else if (element.min != null) {
|
|
129
|
-
parts.push(t("hintMinValue", state, { min: element.min }));
|
|
130
|
-
}
|
|
122
|
+
function isInputErrorNode(node) {
|
|
123
|
+
return node.classList.contains("error-message") && !node.classList.contains("fb-field-error");
|
|
124
|
+
}
|
|
125
|
+
function resolveMark(target, message, scope) {
|
|
126
|
+
const reported = scope.state.reportedInvalid;
|
|
127
|
+
if (message === null || scope.readonly) {
|
|
128
|
+
reported.delete(target);
|
|
129
|
+
return null;
|
|
131
130
|
}
|
|
131
|
+
if (scope.draftMarks && !reported.has(target)) return void 0;
|
|
132
|
+
reported.add(target);
|
|
133
|
+
return message;
|
|
132
134
|
}
|
|
133
|
-
function
|
|
135
|
+
function joinErrorMessages(messages) {
|
|
136
|
+
return messages.length > 0 ? messages.join(" \u2022 ") : null;
|
|
137
|
+
}
|
|
138
|
+
function createErrorNode(state, className) {
|
|
139
|
+
const node = document.createElement("div");
|
|
140
|
+
node.className = className;
|
|
141
|
+
node.id = nextDomId(state, "error");
|
|
142
|
+
node.style.cssText = `
|
|
143
|
+
display: block;
|
|
144
|
+
color: var(--fb-error-color);
|
|
145
|
+
font-size: var(--fb-font-size-small);
|
|
146
|
+
margin-top: 0.25rem;
|
|
147
|
+
`;
|
|
148
|
+
return node;
|
|
149
|
+
}
|
|
150
|
+
function setAttr(el, name, value) {
|
|
151
|
+
if (el.getAttribute(name) !== value) el.setAttribute(name, value);
|
|
152
|
+
}
|
|
153
|
+
function nextDomId(state, kind) {
|
|
154
|
+
return `${state.instanceId}-${kind}-${++state.domIdCounter}`;
|
|
155
|
+
}
|
|
156
|
+
function describedByTokens(target) {
|
|
134
157
|
var _a;
|
|
135
|
-
|
|
136
|
-
if (sizeMB && sizeMB !== Infinity) {
|
|
137
|
-
parts.push(t("hintMaxSize", state, { size: sizeMB }));
|
|
138
|
-
}
|
|
158
|
+
return ((_a = target.getAttribute("aria-describedby")) != null ? _a : "").split(/\s+/).filter(Boolean);
|
|
139
159
|
}
|
|
140
|
-
|
|
160
|
+
var FORM_CONTROL = "input, select, textarea, button";
|
|
161
|
+
var ADDED_ATTRS = "data-fb-mark-added";
|
|
162
|
+
function fieldLabelOf(target) {
|
|
141
163
|
var _a;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
164
|
+
const field = target.closest(".fb-field-wrapper");
|
|
165
|
+
const labelRow = field ? Array.from(field.children).find(
|
|
166
|
+
(child) => child.hasAttribute("data-fb-label-row")
|
|
167
|
+
) : void 0;
|
|
168
|
+
return (_a = labelRow == null ? void 0 : labelRow.querySelector("label")) != null ? _a : null;
|
|
169
|
+
}
|
|
170
|
+
function exposeAsGroup(target, state) {
|
|
171
|
+
if (target.matches(FORM_CONTROL) || target.hasAttribute(ADDED_ATTRS)) return;
|
|
172
|
+
const added = [];
|
|
173
|
+
if (!target.hasAttribute("tabindex")) {
|
|
174
|
+
target.tabIndex = -1;
|
|
175
|
+
added.push("tabindex");
|
|
176
|
+
}
|
|
177
|
+
if (!target.hasAttribute("role")) {
|
|
178
|
+
target.setAttribute("role", "group");
|
|
179
|
+
added.push("role");
|
|
180
|
+
}
|
|
181
|
+
const label = fieldLabelOf(target);
|
|
182
|
+
if (label && !target.hasAttribute("aria-labelledby")) {
|
|
183
|
+
if (!label.id) label.id = nextDomId(state, "label");
|
|
184
|
+
target.setAttribute("aria-labelledby", label.id);
|
|
185
|
+
added.push("aria-labelledby");
|
|
186
|
+
}
|
|
187
|
+
target.setAttribute(ADDED_ATTRS, added.join(" "));
|
|
188
|
+
}
|
|
189
|
+
function linkDescription(target, node) {
|
|
190
|
+
if (!describedByTokens(target).includes(node.id)) {
|
|
191
|
+
target.setAttribute(
|
|
192
|
+
"aria-describedby",
|
|
193
|
+
[...describedByTokens(target), node.id].join(" ")
|
|
147
194
|
);
|
|
148
195
|
}
|
|
149
196
|
}
|
|
150
|
-
function
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
function makeFieldHint(element, state) {
|
|
156
|
-
const parts = [];
|
|
157
|
-
addLengthHint(element, parts, state);
|
|
158
|
-
if (element.type !== "slider") {
|
|
159
|
-
addRangeHint(element, parts, state);
|
|
160
|
-
}
|
|
161
|
-
addFileSizeHint(element, parts, state);
|
|
162
|
-
addFormatHint(element, parts, state);
|
|
163
|
-
addPatternHint(element, parts, state);
|
|
164
|
-
return parts.join(" \u2022 ");
|
|
197
|
+
function setInvalidMark(target, node, state) {
|
|
198
|
+
setAttr(target, "aria-invalid", "true");
|
|
199
|
+
exposeAsGroup(target, state);
|
|
200
|
+
if (node) linkDescription(target, node);
|
|
165
201
|
}
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (!Array.isArray(schema.elements)) {
|
|
173
|
-
errors.push("Schema missing elements array");
|
|
174
|
-
return errors;
|
|
175
|
-
}
|
|
176
|
-
if ("columns" in schema && schema.columns !== void 0) {
|
|
177
|
-
const columns = schema.columns;
|
|
178
|
-
const validColumns = [1, 2, 3, 4];
|
|
179
|
-
if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
|
|
180
|
-
errors.push(`schema.columns must be 1, 2, 3, or 4 (got ${columns})`);
|
|
202
|
+
function unsetInvalidState(target) {
|
|
203
|
+
target.removeAttribute("aria-invalid");
|
|
204
|
+
const added = target.getAttribute(ADDED_ATTRS);
|
|
205
|
+
if (added !== null) {
|
|
206
|
+
for (const attr of added.split(" ").filter(Boolean)) {
|
|
207
|
+
target.removeAttribute(attr);
|
|
181
208
|
}
|
|
209
|
+
target.removeAttribute(ADDED_ATTRS);
|
|
182
210
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
if (!hint.values || typeof hint.values !== "object") {
|
|
193
|
-
errors.push(
|
|
194
|
-
`schema.prefillHints[${hintIndex}] must have a 'values' property of type object`
|
|
195
|
-
);
|
|
196
|
-
} else {
|
|
197
|
-
for (const fieldKey in hint.values) {
|
|
198
|
-
const fieldExists = schema.elements.some(
|
|
199
|
-
(element) => element.key === fieldKey
|
|
200
|
-
);
|
|
201
|
-
if (!fieldExists) {
|
|
202
|
-
errors.push(
|
|
203
|
-
`schema.prefillHints[${hintIndex}] references non-existent field "${fieldKey}"`
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
}
|
|
211
|
+
}
|
|
212
|
+
function clearInvalidMark(target, node) {
|
|
213
|
+
unsetInvalidState(target);
|
|
214
|
+
if (node) {
|
|
215
|
+
const rest = describedByTokens(target).filter((id) => id !== node.id);
|
|
216
|
+
if (rest.length > 0) setAttr(target, "aria-describedby", rest.join(" "));
|
|
217
|
+
else target.removeAttribute("aria-describedby");
|
|
218
|
+
node.remove();
|
|
210
219
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if ("displayMode" in element && element.displayMode !== void 0) {
|
|
222
|
-
const displayMode = element.displayMode;
|
|
223
|
-
if (displayMode !== "stack" && displayMode !== "slides") {
|
|
224
|
-
errors2.push(
|
|
225
|
-
`${elementPath}: displayMode must be "stack" or "slides" (got ${JSON.stringify(displayMode)})`
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
220
|
+
}
|
|
221
|
+
function drawMark(target, message, existing, createNode, state) {
|
|
222
|
+
if (message === "") {
|
|
223
|
+
if (existing) clearInvalidMark(target, existing);
|
|
224
|
+
setInvalidMark(target, null, state);
|
|
225
|
+
return;
|
|
229
226
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
`${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with element key "${otherEl.key}"`
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
if (otherEl.key === filesKey) {
|
|
246
|
-
errors.push(
|
|
247
|
-
`${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with element key "${otherEl.key}"`
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
if (allOutputKeys.has(textKey)) {
|
|
252
|
-
errors.push(
|
|
253
|
-
`${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with another flatOutput key`
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
if (allOutputKeys.has(filesKey)) {
|
|
257
|
-
errors.push(
|
|
258
|
-
`${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with another flatOutput key`
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
allOutputKeys.add(textKey);
|
|
262
|
-
allOutputKeys.add(filesKey);
|
|
263
|
-
} else {
|
|
264
|
-
if (el.key) {
|
|
265
|
-
if (allOutputKeys.has(el.key)) {
|
|
266
|
-
errors.push(
|
|
267
|
-
`${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
allOutputKeys.add(el.key);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
227
|
+
const node = existing != null ? existing : createNode();
|
|
228
|
+
if (node.textContent !== message) node.textContent = message;
|
|
229
|
+
setInvalidMark(target, node, state);
|
|
230
|
+
}
|
|
231
|
+
function markFieldValidity(input, errorMessage, scope) {
|
|
232
|
+
var _a;
|
|
233
|
+
if (!input) return;
|
|
234
|
+
const mark = resolveMark(input, errorMessage, scope);
|
|
235
|
+
if (mark === void 0) return;
|
|
236
|
+
if (mark === null) {
|
|
237
|
+
clearFieldError(input);
|
|
238
|
+
return;
|
|
274
239
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
240
|
+
if (!input.classList.contains("invalid")) input.classList.add("invalid");
|
|
241
|
+
if (input.title !== mark) input.title = mark;
|
|
242
|
+
const errorFor = (_a = input.getAttribute("name")) != null ? _a : "";
|
|
243
|
+
const existing = findErrorNode(input);
|
|
244
|
+
if (existing) setAttr(existing, "data-error-for", errorFor);
|
|
245
|
+
drawMark(
|
|
246
|
+
input,
|
|
247
|
+
mark,
|
|
248
|
+
existing,
|
|
249
|
+
() => {
|
|
250
|
+
var _a2;
|
|
251
|
+
const node = createErrorNode(scope.state, "error-message");
|
|
252
|
+
node.setAttribute("data-error-for", errorFor);
|
|
253
|
+
const anchor = findErrorAnchor(input);
|
|
254
|
+
(_a2 = anchor.parentNode) == null ? void 0 : _a2.insertBefore(node, anchor.nextSibling);
|
|
255
|
+
return node;
|
|
256
|
+
},
|
|
257
|
+
scope.state
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
function clearFieldError(input) {
|
|
261
|
+
if (input.classList.contains("invalid")) input.classList.remove("invalid");
|
|
262
|
+
if (input.title !== "") input.title = "";
|
|
263
|
+
clearInvalidMark(input, findErrorNode(input));
|
|
264
|
+
}
|
|
265
|
+
function markFieldGroupValidity(scopeRoot, fieldPath, errorMessage, scope) {
|
|
266
|
+
var _a;
|
|
267
|
+
const wrapper = scopeRoot.querySelector(
|
|
268
|
+
`[data-field-path="${fieldPath}"]`
|
|
269
|
+
);
|
|
270
|
+
if (!wrapper) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`markFieldGroupValidity: no [data-field-path="${fieldPath}"] in scope`
|
|
301
273
|
);
|
|
302
274
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
const min = typeof minCount === "number" ? minCount : void 0;
|
|
315
|
-
const max = typeof maxCount === "number" ? maxCount : void 0;
|
|
316
|
-
if (max !== void 0 && (max < 0 || Number.isNaN(max))) {
|
|
317
|
-
errors2.push(
|
|
318
|
-
`${elementPath}: ${maxName} must be a non-negative number or Infinity (got ${max})`
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
if (min !== void 0 && (min < 0 || !Number.isFinite(min))) {
|
|
322
|
-
errors2.push(
|
|
323
|
-
`${elementPath}: ${minName} must be a finite non-negative number (got ${min})`
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
const effectiveMin = min != null ? min : requiredImpliesFloor ? 1 : void 0;
|
|
327
|
-
if (effectiveMin !== void 0 && max !== void 0 && effectiveMin > max) {
|
|
328
|
-
const shown = min !== void 0 ? `${minName} (${min})` : `required: true (implies ${minName} 1)`;
|
|
329
|
-
errors2.push(
|
|
330
|
-
`${elementPath}: ${shown} cannot be greater than ${maxName} (${max})`
|
|
331
|
-
);
|
|
332
|
-
}
|
|
275
|
+
if (wrapper.getAttribute("data-conditionally-disabled") === "true") return;
|
|
276
|
+
const mark = resolveMark(wrapper, errorMessage, scope);
|
|
277
|
+
if (mark === void 0) return;
|
|
278
|
+
const existing = (_a = Array.from(wrapper.children).find(
|
|
279
|
+
(child) => child instanceof HTMLElement && child.classList.contains("fb-field-error")
|
|
280
|
+
)) != null ? _a : null;
|
|
281
|
+
if (mark === null) {
|
|
282
|
+
clearInvalidMark(wrapper, existing);
|
|
283
|
+
return;
|
|
333
284
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (!element.type) {
|
|
346
|
-
errors.push(`${elementPath}: missing type`);
|
|
347
|
-
}
|
|
348
|
-
if (!element.key && element.type !== "markdown") {
|
|
349
|
-
errors.push(`${elementPath}: missing key`);
|
|
350
|
-
}
|
|
351
|
-
validateCountBounds(element, elementPath, errors);
|
|
352
|
-
if (element.type === "number" && "decimals" in element) {
|
|
353
|
-
const decimals = element.decimals;
|
|
354
|
-
if (decimals !== void 0 && (!Number.isInteger(decimals) || decimals < 0)) {
|
|
355
|
-
errors.push(
|
|
356
|
-
`${elementPath}: decimals must be a non-negative integer (got ${JSON.stringify(decimals)})`
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
if (element.type === "markdown") {
|
|
361
|
-
const content = element.content;
|
|
362
|
-
if (typeof content !== "string") {
|
|
363
|
-
errors.push(
|
|
364
|
-
`${elementPath}: markdown element requires "content" to be a string (got ${content === null ? "null" : typeof content})`
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
if (element.enableIf) {
|
|
369
|
-
const enableIf = element.enableIf;
|
|
370
|
-
if (!enableIf.key || typeof enableIf.key !== "string") {
|
|
371
|
-
errors.push(
|
|
372
|
-
`${elementPath}: enableIf must have a 'key' property of type string`
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
const hasOperator = "equals" in enableIf;
|
|
376
|
-
if (!hasOperator) {
|
|
377
|
-
errors.push(
|
|
378
|
-
`${elementPath}: enableIf must have at least one operator (equals, etc.)`
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
if (element.type === "group" && "elements" in element && element.elements) {
|
|
383
|
-
validateElements(element.elements, `${elementPath}.elements`);
|
|
384
|
-
}
|
|
385
|
-
if (element.type === "container" && element.elements) {
|
|
386
|
-
validateContainerProps(element, elementPath, errors);
|
|
387
|
-
if ("prefillHints" in element && element.prefillHints) {
|
|
388
|
-
const prefillHints = element.prefillHints;
|
|
389
|
-
if (Array.isArray(prefillHints)) {
|
|
390
|
-
prefillHints.forEach((hint, hintIndex) => {
|
|
391
|
-
if (!hint.label || typeof hint.label !== "string") {
|
|
392
|
-
errors.push(
|
|
393
|
-
`${elementPath}: prefillHints[${hintIndex}] must have a 'label' property of type string`
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
if (!hint.values || typeof hint.values !== "object") {
|
|
397
|
-
errors.push(
|
|
398
|
-
`${elementPath}: prefillHints[${hintIndex}] must have a 'values' property of type object`
|
|
399
|
-
);
|
|
400
|
-
} else {
|
|
401
|
-
for (const fieldKey in hint.values) {
|
|
402
|
-
const fieldExists = element.elements.some(
|
|
403
|
-
(childElement) => childElement.key === fieldKey
|
|
404
|
-
);
|
|
405
|
-
if (!fieldExists) {
|
|
406
|
-
errors.push(
|
|
407
|
-
`container "${element.key}": prefillHints[${hintIndex}] references non-existent field "${fieldKey}"`
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
validateElements(element.elements, `${elementPath}.elements`);
|
|
416
|
-
checkFlatOutputCollisions(element.elements, `${elementPath}.elements`);
|
|
417
|
-
}
|
|
418
|
-
if (element.type === "select" && element.options) {
|
|
419
|
-
const defaultValue = element.default;
|
|
420
|
-
if (defaultValue !== void 0 && defaultValue !== null && defaultValue !== "") {
|
|
421
|
-
const hasMatchingOption = element.options.some(
|
|
422
|
-
(opt) => opt.value === defaultValue
|
|
423
|
-
);
|
|
424
|
-
if (!hasMatchingOption) {
|
|
425
|
-
errors.push(
|
|
426
|
-
`${elementPath}: default "${defaultValue}" not in options`
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
if (Array.isArray(schema.elements)) {
|
|
434
|
-
validateElements(schema.elements, "elements");
|
|
435
|
-
checkFlatOutputCollisions(schema.elements, "elements");
|
|
436
|
-
}
|
|
437
|
-
return errors;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// src/utils/enable-conditions.ts
|
|
441
|
-
function getValueByPath(data, path) {
|
|
442
|
-
if (!data || typeof data !== "object") {
|
|
443
|
-
return void 0;
|
|
444
|
-
}
|
|
445
|
-
const segments = path.match(/[^.[\]]+|\[\d+\]/g);
|
|
446
|
-
if (!segments || segments.length === 0) {
|
|
447
|
-
return void 0;
|
|
448
|
-
}
|
|
449
|
-
let current = data;
|
|
450
|
-
for (const segment of segments) {
|
|
451
|
-
if (current === void 0 || current === null) {
|
|
452
|
-
return void 0;
|
|
453
|
-
}
|
|
454
|
-
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
455
|
-
const index = parseInt(segment.slice(1, -1), 10);
|
|
456
|
-
if (!Array.isArray(current) || isNaN(index)) {
|
|
457
|
-
return void 0;
|
|
458
|
-
}
|
|
459
|
-
current = current[index];
|
|
460
|
-
} else {
|
|
461
|
-
current = current[segment];
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
return current;
|
|
465
|
-
}
|
|
466
|
-
function evaluateEnableCondition(condition, formData, containerData) {
|
|
467
|
-
var _a;
|
|
468
|
-
if (!condition || !condition.key) {
|
|
469
|
-
throw new Error("Invalid enableIf condition: must have a 'key' property");
|
|
470
|
-
}
|
|
471
|
-
const scope = (_a = condition.scope) != null ? _a : "relative";
|
|
472
|
-
let dataSource;
|
|
473
|
-
if (scope === "relative") {
|
|
474
|
-
dataSource = containerData != null ? containerData : formData;
|
|
475
|
-
} else if (scope === "absolute") {
|
|
476
|
-
dataSource = formData;
|
|
477
|
-
} else {
|
|
478
|
-
throw new Error(
|
|
479
|
-
`Invalid enableIf scope: must be "relative" or "absolute" (got "${scope}")`
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
const actualValue = getValueByPath(dataSource, condition.key);
|
|
483
|
-
if ("equals" in condition) {
|
|
484
|
-
return deepEqual(actualValue, condition.equals);
|
|
485
|
-
}
|
|
486
|
-
throw new Error(
|
|
487
|
-
`Invalid enableIf condition: no recognized operator (equals, etc.)`
|
|
285
|
+
drawMark(
|
|
286
|
+
wrapper,
|
|
287
|
+
mark,
|
|
288
|
+
existing,
|
|
289
|
+
() => {
|
|
290
|
+
const node = createErrorNode(scope.state, "error-message fb-field-error");
|
|
291
|
+
node.setAttribute("data-error-for", fieldPath);
|
|
292
|
+
wrapper.appendChild(node);
|
|
293
|
+
return node;
|
|
294
|
+
},
|
|
295
|
+
scope.state
|
|
488
296
|
);
|
|
489
297
|
}
|
|
490
|
-
function deepEqual(a, b) {
|
|
491
|
-
if (a === b) return true;
|
|
492
|
-
if (a == null || b == null) return a === b;
|
|
493
|
-
if (typeof a !== typeof b) return false;
|
|
494
|
-
if (typeof a === "object" && typeof b === "object") {
|
|
495
|
-
try {
|
|
496
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
497
|
-
} catch (e) {
|
|
498
|
-
if (e instanceof TypeError && (e.message.includes("circular") || e.message.includes("cyclic"))) {
|
|
499
|
-
console.warn(
|
|
500
|
-
"deepEqual: Circular reference detected in enableIf comparison, using reference equality"
|
|
501
|
-
);
|
|
502
|
-
return a === b;
|
|
503
|
-
}
|
|
504
|
-
throw e;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
return a === b;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// src/utils/styles.ts
|
|
511
|
-
function findErrorAnchor(input) {
|
|
512
|
-
var _a, _b, _c, _d;
|
|
513
|
-
return (_d = (_c = (_a = input.closest) == null ? void 0 : _a.call(input, ".fb-chip")) != null ? _c : (_b = input.closest) == null ? void 0 : _b.call(input, ".slider-container")) != null ? _d : input;
|
|
514
|
-
}
|
|
515
|
-
function findErrorNode(input) {
|
|
516
|
-
const anchor = findErrorAnchor(input);
|
|
517
|
-
const name = input.getAttribute("name");
|
|
518
|
-
const parent = anchor.parentElement;
|
|
519
|
-
if (name && parent) {
|
|
520
|
-
for (const child of Array.from(parent.children)) {
|
|
521
|
-
if (child.classList.contains("error-message") && child.getAttribute("data-error-for") === name) {
|
|
522
|
-
return child;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
const sibling = anchor.nextElementSibling;
|
|
527
|
-
return sibling && sibling.classList.contains("error-message") ? sibling : null;
|
|
528
|
-
}
|
|
529
|
-
function markFieldValidity(input, errorMessage) {
|
|
530
|
-
var _a, _b, _c, _d;
|
|
531
|
-
if (!input) return;
|
|
532
|
-
if (errorMessage == null) {
|
|
533
|
-
input.classList.remove("invalid");
|
|
534
|
-
input.title = "";
|
|
535
|
-
(_a = findErrorNode(input)) == null ? void 0 : _a.remove();
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
input.classList.add("invalid");
|
|
539
|
-
input.title = errorMessage;
|
|
540
|
-
if (errorMessage === "") {
|
|
541
|
-
(_b = findErrorNode(input)) == null ? void 0 : _b.remove();
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
let errorElement = findErrorNode(input);
|
|
545
|
-
if (!errorElement) {
|
|
546
|
-
const anchor = findErrorAnchor(input);
|
|
547
|
-
errorElement = document.createElement("div");
|
|
548
|
-
errorElement.className = "error-message";
|
|
549
|
-
errorElement.style.cssText = `
|
|
550
|
-
color: var(--fb-error-color);
|
|
551
|
-
font-size: var(--fb-font-size-small);
|
|
552
|
-
margin-top: 0.25rem;
|
|
553
|
-
`;
|
|
554
|
-
(_c = anchor.parentNode) == null ? void 0 : _c.insertBefore(errorElement, anchor.nextSibling);
|
|
555
|
-
}
|
|
556
|
-
errorElement.setAttribute("data-error-for", (_d = input.getAttribute("name")) != null ? _d : "");
|
|
557
|
-
errorElement.textContent = errorMessage;
|
|
558
|
-
errorElement.style.display = "block";
|
|
559
|
-
}
|
|
560
|
-
function clearFieldError(input) {
|
|
561
|
-
var _a;
|
|
562
|
-
(_a = findErrorNode(input)) == null ? void 0 : _a.remove();
|
|
563
|
-
}
|
|
564
298
|
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>';
|
|
565
299
|
function ensureThemingHooks(doc) {
|
|
566
300
|
if (doc.head.querySelector("[data-fb-theming-hooks]")) return;
|
|
@@ -678,6 +412,13 @@ function ensureThemingHooks(doc) {
|
|
|
678
412
|
/* .fb-size-md uses defaults \u2014 no override needed */
|
|
679
413
|
.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; }
|
|
680
414
|
.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; }
|
|
415
|
+
/* !important: controls carry their border inline and JS focus/hover
|
|
416
|
+
handlers rewrite it, so no weaker rule would ever show the mark. */
|
|
417
|
+
[data-fb-root] input[aria-invalid="true"],
|
|
418
|
+
[data-fb-root] select[aria-invalid="true"],
|
|
419
|
+
[data-fb-root] textarea[aria-invalid="true"] {
|
|
420
|
+
border-color: var(--fb-error-color) !important;
|
|
421
|
+
}
|
|
681
422
|
`;
|
|
682
423
|
doc.head.appendChild(style);
|
|
683
424
|
}
|
|
@@ -725,188 +466,615 @@ function applyAutoExpand(textarea, options) {
|
|
|
725
466
|
ro.observe(textarea);
|
|
726
467
|
(_b = options.observers) == null ? void 0 : _b.add(ro);
|
|
727
468
|
}
|
|
728
|
-
function applySingleLineMode(textarea) {
|
|
729
|
-
textarea.addEventListener("keydown", (e) => {
|
|
730
|
-
if (e.key === "Enter") {
|
|
731
|
-
e.preventDefault();
|
|
469
|
+
function applySingleLineMode(textarea) {
|
|
470
|
+
textarea.addEventListener("keydown", (e) => {
|
|
471
|
+
if (e.key === "Enter") {
|
|
472
|
+
e.preventDefault();
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
textarea.addEventListener("paste", (e) => {
|
|
476
|
+
var _a, _b, _c, _d;
|
|
477
|
+
const pasted = (_b = (_a = e.clipboardData) == null ? void 0 : _a.getData("text")) != null ? _b : "";
|
|
478
|
+
if (!/[\r\n]/.test(pasted)) return;
|
|
479
|
+
e.preventDefault();
|
|
480
|
+
const cleaned = pasted.replace(/[\r\n]+/g, " ");
|
|
481
|
+
const start = (_c = textarea.selectionStart) != null ? _c : textarea.value.length;
|
|
482
|
+
const end = (_d = textarea.selectionEnd) != null ? _d : textarea.value.length;
|
|
483
|
+
const before = textarea.value.slice(0, start);
|
|
484
|
+
const after = textarea.value.slice(end);
|
|
485
|
+
textarea.value = before + cleaned + after;
|
|
486
|
+
const pos = start + cleaned.length;
|
|
487
|
+
textarea.setSelectionRange(pos, pos);
|
|
488
|
+
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
function mountCounterInLabel(wrapper, counter) {
|
|
492
|
+
const labelRow = wrapper.querySelector(
|
|
493
|
+
":scope > [data-fb-label-row]"
|
|
494
|
+
);
|
|
495
|
+
if (labelRow) labelRow.appendChild(counter);
|
|
496
|
+
}
|
|
497
|
+
function createAddItemRow(classNameSuffix, onClick, options = {}) {
|
|
498
|
+
var _a;
|
|
499
|
+
const label = (_a = options.label) != null ? _a : "";
|
|
500
|
+
const showCounter = options.showCounter !== false;
|
|
501
|
+
const row = document.createElement("div");
|
|
502
|
+
row.className = "fb-add-row";
|
|
503
|
+
row.style.cssText = "display:flex;align-items:stretch;width:100%;margin-top:4px;";
|
|
504
|
+
const button = document.createElement("button");
|
|
505
|
+
button.type = "button";
|
|
506
|
+
button.className = `add-${classNameSuffix}-btn`;
|
|
507
|
+
button.style.cssText = `
|
|
508
|
+
flex: 1 1 auto;
|
|
509
|
+
display: inline-flex;
|
|
510
|
+
align-items: center;
|
|
511
|
+
justify-content: center;
|
|
512
|
+
gap: 4px;
|
|
513
|
+
padding: 3px 10px;
|
|
514
|
+
border: 1px dashed var(--fb-primary-color);
|
|
515
|
+
border-radius: var(--fb-border-radius);
|
|
516
|
+
background: transparent;
|
|
517
|
+
color: var(--fb-primary-color);
|
|
518
|
+
font-size: var(--fb-font-size-small, var(--fb-font-size));
|
|
519
|
+
font-weight: 500;
|
|
520
|
+
font-family: var(--fb-font-family);
|
|
521
|
+
cursor: pointer;
|
|
522
|
+
transition: border-color var(--fb-transition-duration), color var(--fb-transition-duration), background-color var(--fb-transition-duration);
|
|
523
|
+
`;
|
|
524
|
+
button.textContent = label ? `+ ${label}` : "+";
|
|
525
|
+
button.addEventListener("mouseenter", () => {
|
|
526
|
+
if (button.disabled) return;
|
|
527
|
+
button.style.borderStyle = "solid";
|
|
528
|
+
button.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
529
|
+
});
|
|
530
|
+
button.addEventListener("mouseleave", () => {
|
|
531
|
+
button.style.borderStyle = "dashed";
|
|
532
|
+
button.style.backgroundColor = "transparent";
|
|
533
|
+
});
|
|
534
|
+
button.onclick = onClick;
|
|
535
|
+
const counter = document.createElement("span");
|
|
536
|
+
counter.className = "fb-add-counter";
|
|
537
|
+
counter.style.cssText = `
|
|
538
|
+
margin-left: auto;
|
|
539
|
+
font-size: var(--fb-font-size-small, 0.875rem);
|
|
540
|
+
color: var(--fb-text-secondary-color);
|
|
541
|
+
font-weight: 400;
|
|
542
|
+
`;
|
|
543
|
+
if (!showCounter) counter.style.display = "none";
|
|
544
|
+
row.appendChild(button);
|
|
545
|
+
const update = (current, max) => {
|
|
546
|
+
const reached = current >= max;
|
|
547
|
+
row.style.display = reached ? "none" : "flex";
|
|
548
|
+
button.style.display = reached ? "none" : "inline-flex";
|
|
549
|
+
button.disabled = reached;
|
|
550
|
+
if (showCounter) {
|
|
551
|
+
counter.textContent = `${current}/${max === Infinity ? "\u221E" : max}`;
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
return { row, button, counter, update };
|
|
555
|
+
}
|
|
556
|
+
function createSlideAddTile(onClick, options = {}) {
|
|
557
|
+
var _a;
|
|
558
|
+
const label = (_a = options.label) != null ? _a : "";
|
|
559
|
+
const tile = document.createElement("button");
|
|
560
|
+
tile.type = "button";
|
|
561
|
+
tile.className = "add-container-btn fb-slide-add";
|
|
562
|
+
tile.style.cssText = `
|
|
563
|
+
display: flex;
|
|
564
|
+
flex-direction: column;
|
|
565
|
+
align-items: center;
|
|
566
|
+
justify-content: center;
|
|
567
|
+
gap: 12px;
|
|
568
|
+
width: 100%;
|
|
569
|
+
min-height: 180px;
|
|
570
|
+
align-self: stretch;
|
|
571
|
+
padding: 24px 16px;
|
|
572
|
+
border: 1.5px dashed var(--fb-primary-color);
|
|
573
|
+
border-radius: var(--fb-border-radius);
|
|
574
|
+
background: transparent;
|
|
575
|
+
color: var(--fb-primary-color);
|
|
576
|
+
font-size: var(--fb-font-size-small, var(--fb-font-size));
|
|
577
|
+
font-weight: 500;
|
|
578
|
+
font-family: var(--fb-font-family);
|
|
579
|
+
cursor: pointer;
|
|
580
|
+
transition: border-color var(--fb-transition-duration), color var(--fb-transition-duration), background-color var(--fb-transition-duration);
|
|
581
|
+
`;
|
|
582
|
+
const circle = document.createElement("span");
|
|
583
|
+
circle.className = "fb-slide-add-circle";
|
|
584
|
+
circle.style.cssText = `
|
|
585
|
+
display: inline-flex;
|
|
586
|
+
align-items: center;
|
|
587
|
+
justify-content: center;
|
|
588
|
+
width: 36px;
|
|
589
|
+
height: 36px;
|
|
590
|
+
border: 1px solid var(--fb-primary-color);
|
|
591
|
+
border-radius: 50%;
|
|
592
|
+
background: var(--fb-background-color);
|
|
593
|
+
font-size: 20px;
|
|
594
|
+
line-height: 1;
|
|
595
|
+
color: inherit;
|
|
596
|
+
transition: inherit;
|
|
597
|
+
`;
|
|
598
|
+
circle.textContent = "+";
|
|
599
|
+
tile.appendChild(circle);
|
|
600
|
+
if (label) {
|
|
601
|
+
const text = document.createElement("span");
|
|
602
|
+
text.textContent = label;
|
|
603
|
+
tile.appendChild(text);
|
|
604
|
+
}
|
|
605
|
+
tile.addEventListener("mouseenter", () => {
|
|
606
|
+
if (tile.disabled) return;
|
|
607
|
+
tile.style.borderStyle = "solid";
|
|
608
|
+
tile.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
609
|
+
});
|
|
610
|
+
tile.addEventListener("mouseleave", () => {
|
|
611
|
+
tile.style.borderStyle = "dashed";
|
|
612
|
+
tile.style.backgroundColor = "transparent";
|
|
613
|
+
});
|
|
614
|
+
tile.onclick = onClick;
|
|
615
|
+
const counter = document.createElement("span");
|
|
616
|
+
counter.className = "fb-add-counter";
|
|
617
|
+
counter.style.cssText = `
|
|
618
|
+
margin-left: auto;
|
|
619
|
+
font-size: var(--fb-font-size-small, 0.875rem);
|
|
620
|
+
color: var(--fb-text-secondary-color);
|
|
621
|
+
font-weight: 400;
|
|
622
|
+
`;
|
|
623
|
+
const update = (current, max) => {
|
|
624
|
+
const reached = current >= max;
|
|
625
|
+
tile.style.display = reached ? "none" : "flex";
|
|
626
|
+
tile.disabled = reached;
|
|
627
|
+
counter.textContent = `${current}/${max === Infinity ? "\u221E" : max}`;
|
|
628
|
+
};
|
|
629
|
+
return { tile, counter, update };
|
|
630
|
+
}
|
|
631
|
+
function applyActionButtonStyles(button, isFormLevel = false) {
|
|
632
|
+
button.style.cssText = `
|
|
633
|
+
background-color: var(--fb-action-bg-color);
|
|
634
|
+
color: var(--fb-action-text-color);
|
|
635
|
+
border: var(--fb-border-width) solid var(--fb-action-border-color);
|
|
636
|
+
padding: ${isFormLevel ? "0.5rem 1rem" : "0.5rem 0.75rem"};
|
|
637
|
+
font-size: var(--fb-font-size);
|
|
638
|
+
font-weight: var(--fb-font-weight-medium);
|
|
639
|
+
border-radius: var(--fb-border-radius);
|
|
640
|
+
transition: all var(--fb-transition-duration);
|
|
641
|
+
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
642
|
+
`;
|
|
643
|
+
button.addEventListener("mouseenter", () => {
|
|
644
|
+
button.style.backgroundColor = "var(--fb-action-hover-bg-color)";
|
|
645
|
+
button.style.borderColor = "var(--fb-action-hover-border-color)";
|
|
646
|
+
});
|
|
647
|
+
button.addEventListener("mouseleave", () => {
|
|
648
|
+
button.style.backgroundColor = "var(--fb-action-bg-color)";
|
|
649
|
+
button.style.borderColor = "var(--fb-action-border-color)";
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/utils/validation.ts
|
|
654
|
+
function countRuleMessages(element, count, state, keys = { min: "minItems", max: "maxItems" }) {
|
|
655
|
+
var _a, _b;
|
|
656
|
+
const minCount = "minCount" in element ? (_a = element.minCount) != null ? _a : 0 : 0;
|
|
657
|
+
const maxCount = "maxCount" in element ? (_b = element.maxCount) != null ? _b : Infinity : Infinity;
|
|
658
|
+
const messages = [];
|
|
659
|
+
if (element.required && count === 0) messages.push(t("required", state));
|
|
660
|
+
if (count < minCount) messages.push(t(keys.min, state, { min: minCount }));
|
|
661
|
+
if (count > maxCount) messages.push(t(keys.max, state, { max: maxCount }));
|
|
662
|
+
return messages;
|
|
663
|
+
}
|
|
664
|
+
function validateItemCount(element, key, filledCount, context, errors) {
|
|
665
|
+
const messages = countRuleMessages(element, filledCount, context.state);
|
|
666
|
+
errors.push(...messages.map((message) => `${key}: ${message}`));
|
|
667
|
+
markFieldGroupValidity(
|
|
668
|
+
context.scopeRoot,
|
|
669
|
+
key,
|
|
670
|
+
joinErrorMessages(messages),
|
|
671
|
+
context
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
function addLengthHint(element, parts, state) {
|
|
675
|
+
if (element.minLength != null || element.maxLength != null) {
|
|
676
|
+
if (element.minLength != null && element.maxLength != null) {
|
|
677
|
+
parts.push(
|
|
678
|
+
t("hintLengthRange", state, {
|
|
679
|
+
min: element.minLength,
|
|
680
|
+
max: element.maxLength
|
|
681
|
+
})
|
|
682
|
+
);
|
|
683
|
+
} else if (element.maxLength != null) {
|
|
684
|
+
parts.push(t("hintMaxLength", state, { max: element.maxLength }));
|
|
685
|
+
} else if (element.minLength != null) {
|
|
686
|
+
parts.push(t("hintMinLength", state, { min: element.minLength }));
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function addRangeHint(element, parts, state) {
|
|
691
|
+
if (element.min != null || element.max != null) {
|
|
692
|
+
if (element.min != null && element.max != null) {
|
|
693
|
+
parts.push(
|
|
694
|
+
t("hintValueRange", state, { min: element.min, max: element.max })
|
|
695
|
+
);
|
|
696
|
+
} else if (element.max != null) {
|
|
697
|
+
parts.push(t("hintMaxValue", state, { max: element.max }));
|
|
698
|
+
} else if (element.min != null) {
|
|
699
|
+
parts.push(t("hintMinValue", state, { min: element.min }));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function addFileSizeHint(element, parts, state) {
|
|
704
|
+
var _a;
|
|
705
|
+
const sizeMB = (_a = element.maxSize) != null ? _a : element.maxSizeMB;
|
|
706
|
+
if (sizeMB && sizeMB !== Infinity) {
|
|
707
|
+
parts.push(t("hintMaxSize", state, { size: sizeMB }));
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function addFormatHint(element, parts, state) {
|
|
711
|
+
var _a;
|
|
712
|
+
if ((_a = element.accept) == null ? void 0 : _a.extensions) {
|
|
713
|
+
parts.push(
|
|
714
|
+
t("hintFormats", state, {
|
|
715
|
+
formats: element.accept.extensions.map((ext) => ext.toUpperCase()).join(",")
|
|
716
|
+
})
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function addPatternHint(element, parts, state) {
|
|
721
|
+
if (element.pattern) {
|
|
722
|
+
parts.push(t("hintPattern", state, { pattern: element.pattern }));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
function makeFieldHint(element, state) {
|
|
726
|
+
const parts = [];
|
|
727
|
+
addLengthHint(element, parts, state);
|
|
728
|
+
if (element.type !== "slider") {
|
|
729
|
+
addRangeHint(element, parts, state);
|
|
730
|
+
}
|
|
731
|
+
addFileSizeHint(element, parts, state);
|
|
732
|
+
addFormatHint(element, parts, state);
|
|
733
|
+
addPatternHint(element, parts, state);
|
|
734
|
+
return parts.join(" \u2022 ");
|
|
735
|
+
}
|
|
736
|
+
function validateSchema(schema) {
|
|
737
|
+
const errors = [];
|
|
738
|
+
if (!schema || typeof schema !== "object") {
|
|
739
|
+
errors.push("Schema must be an object");
|
|
740
|
+
return errors;
|
|
741
|
+
}
|
|
742
|
+
if (!Array.isArray(schema.elements)) {
|
|
743
|
+
errors.push("Schema missing elements array");
|
|
744
|
+
return errors;
|
|
745
|
+
}
|
|
746
|
+
if ("columns" in schema && schema.columns !== void 0) {
|
|
747
|
+
const columns = schema.columns;
|
|
748
|
+
const validColumns = [1, 2, 3, 4];
|
|
749
|
+
if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
|
|
750
|
+
errors.push(`schema.columns must be 1, 2, 3, or 4 (got ${columns})`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if ("prefillHints" in schema && schema.prefillHints) {
|
|
754
|
+
const prefillHints = schema.prefillHints;
|
|
755
|
+
if (Array.isArray(prefillHints)) {
|
|
756
|
+
prefillHints.forEach((hint, hintIndex) => {
|
|
757
|
+
if (!hint.label || typeof hint.label !== "string") {
|
|
758
|
+
errors.push(
|
|
759
|
+
`schema.prefillHints[${hintIndex}] must have a 'label' property of type string`
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
if (!hint.values || typeof hint.values !== "object") {
|
|
763
|
+
errors.push(
|
|
764
|
+
`schema.prefillHints[${hintIndex}] must have a 'values' property of type object`
|
|
765
|
+
);
|
|
766
|
+
} else {
|
|
767
|
+
for (const fieldKey in hint.values) {
|
|
768
|
+
const fieldExists = schema.elements.some(
|
|
769
|
+
(element) => element.key === fieldKey
|
|
770
|
+
);
|
|
771
|
+
if (!fieldExists) {
|
|
772
|
+
errors.push(
|
|
773
|
+
`schema.prefillHints[${hintIndex}] references non-existent field "${fieldKey}"`
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function validateContainerProps(element, elementPath, errors2) {
|
|
782
|
+
if ("columns" in element && element.columns !== void 0) {
|
|
783
|
+
const columns = element.columns;
|
|
784
|
+
const validColumns = [1, 2, 3, 4];
|
|
785
|
+
if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
|
|
786
|
+
errors2.push(
|
|
787
|
+
`${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if ("displayMode" in element && element.displayMode !== void 0) {
|
|
792
|
+
const displayMode = element.displayMode;
|
|
793
|
+
if (displayMode !== "stack" && displayMode !== "slides") {
|
|
794
|
+
errors2.push(
|
|
795
|
+
`${elementPath}: displayMode must be "stack" or "slides" (got ${JSON.stringify(displayMode)})`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function checkFlatOutputCollisions(elements, scopePath) {
|
|
801
|
+
var _a, _b;
|
|
802
|
+
const allOutputKeys = /* @__PURE__ */ new Set();
|
|
803
|
+
for (const el of elements) {
|
|
804
|
+
if (el.type === "richinput" && el.flatOutput) {
|
|
805
|
+
const richEl = el;
|
|
806
|
+
const textKey = (_a = richEl.textKey) != null ? _a : "text";
|
|
807
|
+
const filesKey = (_b = richEl.filesKey) != null ? _b : "files";
|
|
808
|
+
for (const otherEl of elements) {
|
|
809
|
+
if (otherEl === el) continue;
|
|
810
|
+
if (otherEl.key === textKey) {
|
|
811
|
+
errors.push(
|
|
812
|
+
`${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with element key "${otherEl.key}"`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
if (otherEl.key === filesKey) {
|
|
816
|
+
errors.push(
|
|
817
|
+
`${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with element key "${otherEl.key}"`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (allOutputKeys.has(textKey)) {
|
|
822
|
+
errors.push(
|
|
823
|
+
`${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with another flatOutput key`
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
if (allOutputKeys.has(filesKey)) {
|
|
827
|
+
errors.push(
|
|
828
|
+
`${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with another flatOutput key`
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
allOutputKeys.add(textKey);
|
|
832
|
+
allOutputKeys.add(filesKey);
|
|
833
|
+
} else {
|
|
834
|
+
if (el.key) {
|
|
835
|
+
if (allOutputKeys.has(el.key)) {
|
|
836
|
+
errors.push(
|
|
837
|
+
`${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
allOutputKeys.add(el.key);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function validateCountBounds(element, elementPath, errors2) {
|
|
846
|
+
var _a, _b;
|
|
847
|
+
const el = element;
|
|
848
|
+
if (el.type === "group") {
|
|
849
|
+
if (!isPlainObject(el.repeat)) return;
|
|
850
|
+
checkBounds(
|
|
851
|
+
elementPath,
|
|
852
|
+
(_a = el.repeat) == null ? void 0 : _a.min,
|
|
853
|
+
(_b = el.repeat) == null ? void 0 : _b.max,
|
|
854
|
+
"repeat.min",
|
|
855
|
+
"repeat.max",
|
|
856
|
+
el.required === true,
|
|
857
|
+
errors2
|
|
858
|
+
);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const isMultiple = el.multiple === true || el.type === "files";
|
|
862
|
+
if (!isMultiple) return;
|
|
863
|
+
checkBounds(
|
|
864
|
+
elementPath,
|
|
865
|
+
el.minCount,
|
|
866
|
+
el.maxCount,
|
|
867
|
+
"minCount",
|
|
868
|
+
"maxCount",
|
|
869
|
+
el.required === true,
|
|
870
|
+
errors2
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
function checkBounds(elementPath, minCount, maxCount, minName, maxName, requiredImpliesFloor, errors2) {
|
|
874
|
+
for (const [name, bound] of [
|
|
875
|
+
[minName, minCount],
|
|
876
|
+
[maxName, maxCount]
|
|
877
|
+
]) {
|
|
878
|
+
if (bound !== void 0 && typeof bound !== "number") {
|
|
879
|
+
errors2.push(
|
|
880
|
+
`${elementPath}: ${name} must be a number (got ${typeof bound})`
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const min = typeof minCount === "number" ? minCount : void 0;
|
|
885
|
+
const max = typeof maxCount === "number" ? maxCount : void 0;
|
|
886
|
+
if (max !== void 0 && (max < 0 || Number.isNaN(max))) {
|
|
887
|
+
errors2.push(
|
|
888
|
+
`${elementPath}: ${maxName} must be a non-negative number or Infinity (got ${max})`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
if (min !== void 0 && (min < 0 || !Number.isFinite(min))) {
|
|
892
|
+
errors2.push(
|
|
893
|
+
`${elementPath}: ${minName} must be a finite non-negative number (got ${min})`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
const effectiveMin = min != null ? min : requiredImpliesFloor ? 1 : void 0;
|
|
897
|
+
if (effectiveMin !== void 0 && max !== void 0 && effectiveMin > max) {
|
|
898
|
+
const shown = min !== void 0 ? `${minName} (${min})` : `required: true (implies ${minName} 1)`;
|
|
899
|
+
errors2.push(
|
|
900
|
+
`${elementPath}: ${shown} cannot be greater than ${maxName} (${max})`
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
function validateElements(elements, path) {
|
|
905
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
906
|
+
elements.forEach((element, index) => {
|
|
907
|
+
if (!element.key) return;
|
|
908
|
+
if (seenKeys.has(element.key)) {
|
|
909
|
+
errors.push(`${path}[${index}]: duplicate key "${element.key}"`);
|
|
910
|
+
}
|
|
911
|
+
seenKeys.add(element.key);
|
|
912
|
+
});
|
|
913
|
+
elements.forEach((element, index) => {
|
|
914
|
+
const elementPath = `${path}[${index}]`;
|
|
915
|
+
if (!element.type) {
|
|
916
|
+
errors.push(`${elementPath}: missing type`);
|
|
917
|
+
}
|
|
918
|
+
if (!element.key && element.type !== "markdown") {
|
|
919
|
+
errors.push(`${elementPath}: missing key`);
|
|
920
|
+
}
|
|
921
|
+
validateCountBounds(element, elementPath, errors);
|
|
922
|
+
if (element.type === "number" && "decimals" in element) {
|
|
923
|
+
const decimals = element.decimals;
|
|
924
|
+
if (decimals !== void 0 && (!Number.isInteger(decimals) || decimals < 0)) {
|
|
925
|
+
errors.push(
|
|
926
|
+
`${elementPath}: decimals must be a non-negative integer (got ${JSON.stringify(decimals)})`
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (element.type === "markdown") {
|
|
931
|
+
const content = element.content;
|
|
932
|
+
if (typeof content !== "string") {
|
|
933
|
+
errors.push(
|
|
934
|
+
`${elementPath}: markdown element requires "content" to be a string (got ${content === null ? "null" : typeof content})`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (element.enableIf) {
|
|
939
|
+
const enableIf = element.enableIf;
|
|
940
|
+
if (!enableIf.key || typeof enableIf.key !== "string") {
|
|
941
|
+
errors.push(
|
|
942
|
+
`${elementPath}: enableIf must have a 'key' property of type string`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
const hasOperator = "equals" in enableIf;
|
|
946
|
+
if (!hasOperator) {
|
|
947
|
+
errors.push(
|
|
948
|
+
`${elementPath}: enableIf must have at least one operator (equals, etc.)`
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (element.type === "group" && "elements" in element && element.elements) {
|
|
953
|
+
validateElements(element.elements, `${elementPath}.elements`);
|
|
954
|
+
}
|
|
955
|
+
if (element.type === "container" && element.elements) {
|
|
956
|
+
validateContainerProps(element, elementPath, errors);
|
|
957
|
+
if ("prefillHints" in element && element.prefillHints) {
|
|
958
|
+
const prefillHints = element.prefillHints;
|
|
959
|
+
if (Array.isArray(prefillHints)) {
|
|
960
|
+
prefillHints.forEach((hint, hintIndex) => {
|
|
961
|
+
if (!hint.label || typeof hint.label !== "string") {
|
|
962
|
+
errors.push(
|
|
963
|
+
`${elementPath}: prefillHints[${hintIndex}] must have a 'label' property of type string`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
if (!hint.values || typeof hint.values !== "object") {
|
|
967
|
+
errors.push(
|
|
968
|
+
`${elementPath}: prefillHints[${hintIndex}] must have a 'values' property of type object`
|
|
969
|
+
);
|
|
970
|
+
} else {
|
|
971
|
+
for (const fieldKey in hint.values) {
|
|
972
|
+
const fieldExists = element.elements.some(
|
|
973
|
+
(childElement) => childElement.key === fieldKey
|
|
974
|
+
);
|
|
975
|
+
if (!fieldExists) {
|
|
976
|
+
errors.push(
|
|
977
|
+
`container "${element.key}": prefillHints[${hintIndex}] references non-existent field "${fieldKey}"`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
validateElements(element.elements, `${elementPath}.elements`);
|
|
986
|
+
checkFlatOutputCollisions(element.elements, `${elementPath}.elements`);
|
|
987
|
+
}
|
|
988
|
+
if (element.type === "select" && element.options) {
|
|
989
|
+
const defaultValue = element.default;
|
|
990
|
+
if (defaultValue !== void 0 && defaultValue !== null && defaultValue !== "") {
|
|
991
|
+
const hasMatchingOption = element.options.some(
|
|
992
|
+
(opt) => opt.value === defaultValue
|
|
993
|
+
);
|
|
994
|
+
if (!hasMatchingOption) {
|
|
995
|
+
errors.push(
|
|
996
|
+
`${elementPath}: default "${defaultValue}" not in options`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
if (Array.isArray(schema.elements)) {
|
|
1004
|
+
validateElements(schema.elements, "elements");
|
|
1005
|
+
checkFlatOutputCollisions(schema.elements, "elements");
|
|
1006
|
+
}
|
|
1007
|
+
return errors;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// src/utils/enable-conditions.ts
|
|
1011
|
+
function getValueByPath(data, path) {
|
|
1012
|
+
if (!data || typeof data !== "object") {
|
|
1013
|
+
return void 0;
|
|
1014
|
+
}
|
|
1015
|
+
const segments = path.match(/[^.[\]]+|\[\d+\]/g);
|
|
1016
|
+
if (!segments || segments.length === 0) {
|
|
1017
|
+
return void 0;
|
|
1018
|
+
}
|
|
1019
|
+
let current = data;
|
|
1020
|
+
for (const segment of segments) {
|
|
1021
|
+
if (current === void 0 || current === null) {
|
|
1022
|
+
return void 0;
|
|
732
1023
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const end = (_d = textarea.selectionEnd) != null ? _d : textarea.value.length;
|
|
742
|
-
const before = textarea.value.slice(0, start);
|
|
743
|
-
const after = textarea.value.slice(end);
|
|
744
|
-
textarea.value = before + cleaned + after;
|
|
745
|
-
const pos = start + cleaned.length;
|
|
746
|
-
textarea.setSelectionRange(pos, pos);
|
|
747
|
-
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
function mountCounterInLabel(wrapper, counter) {
|
|
751
|
-
const labelRow = wrapper.querySelector(
|
|
752
|
-
":scope > [data-fb-label-row]"
|
|
753
|
-
);
|
|
754
|
-
if (labelRow) labelRow.appendChild(counter);
|
|
755
|
-
}
|
|
756
|
-
function createAddItemRow(classNameSuffix, onClick, options = {}) {
|
|
757
|
-
var _a;
|
|
758
|
-
const label = (_a = options.label) != null ? _a : "";
|
|
759
|
-
const showCounter = options.showCounter !== false;
|
|
760
|
-
const row = document.createElement("div");
|
|
761
|
-
row.className = "fb-add-row";
|
|
762
|
-
row.style.cssText = "display:flex;align-items:stretch;width:100%;margin-top:4px;";
|
|
763
|
-
const button = document.createElement("button");
|
|
764
|
-
button.type = "button";
|
|
765
|
-
button.className = `add-${classNameSuffix}-btn`;
|
|
766
|
-
button.style.cssText = `
|
|
767
|
-
flex: 1 1 auto;
|
|
768
|
-
display: inline-flex;
|
|
769
|
-
align-items: center;
|
|
770
|
-
justify-content: center;
|
|
771
|
-
gap: 4px;
|
|
772
|
-
padding: 3px 10px;
|
|
773
|
-
border: 1px dashed var(--fb-primary-color);
|
|
774
|
-
border-radius: var(--fb-border-radius);
|
|
775
|
-
background: transparent;
|
|
776
|
-
color: var(--fb-primary-color);
|
|
777
|
-
font-size: var(--fb-font-size-small, var(--fb-font-size));
|
|
778
|
-
font-weight: 500;
|
|
779
|
-
font-family: var(--fb-font-family);
|
|
780
|
-
cursor: pointer;
|
|
781
|
-
transition: border-color var(--fb-transition-duration), color var(--fb-transition-duration), background-color var(--fb-transition-duration);
|
|
782
|
-
`;
|
|
783
|
-
button.textContent = label ? `+ ${label}` : "+";
|
|
784
|
-
button.addEventListener("mouseenter", () => {
|
|
785
|
-
if (button.disabled) return;
|
|
786
|
-
button.style.borderStyle = "solid";
|
|
787
|
-
button.style.backgroundColor = "var(--fb-background-hover-color)";
|
|
788
|
-
});
|
|
789
|
-
button.addEventListener("mouseleave", () => {
|
|
790
|
-
button.style.borderStyle = "dashed";
|
|
791
|
-
button.style.backgroundColor = "transparent";
|
|
792
|
-
});
|
|
793
|
-
button.onclick = onClick;
|
|
794
|
-
const counter = document.createElement("span");
|
|
795
|
-
counter.className = "fb-add-counter";
|
|
796
|
-
counter.style.cssText = `
|
|
797
|
-
margin-left: auto;
|
|
798
|
-
font-size: var(--fb-font-size-small, 0.875rem);
|
|
799
|
-
color: var(--fb-text-secondary-color);
|
|
800
|
-
font-weight: 400;
|
|
801
|
-
`;
|
|
802
|
-
if (!showCounter) counter.style.display = "none";
|
|
803
|
-
row.appendChild(button);
|
|
804
|
-
const update = (current, max) => {
|
|
805
|
-
const reached = current >= max;
|
|
806
|
-
row.style.display = reached ? "none" : "flex";
|
|
807
|
-
button.style.display = reached ? "none" : "inline-flex";
|
|
808
|
-
button.disabled = reached;
|
|
809
|
-
if (showCounter) {
|
|
810
|
-
counter.textContent = `${current}/${max === Infinity ? "\u221E" : max}`;
|
|
1024
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
1025
|
+
const index = parseInt(segment.slice(1, -1), 10);
|
|
1026
|
+
if (!Array.isArray(current) || isNaN(index)) {
|
|
1027
|
+
return void 0;
|
|
1028
|
+
}
|
|
1029
|
+
current = current[index];
|
|
1030
|
+
} else {
|
|
1031
|
+
current = current[segment];
|
|
811
1032
|
}
|
|
812
|
-
}
|
|
813
|
-
return
|
|
1033
|
+
}
|
|
1034
|
+
return current;
|
|
814
1035
|
}
|
|
815
|
-
function
|
|
1036
|
+
function evaluateEnableCondition(condition, formData, containerData) {
|
|
816
1037
|
var _a;
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
tile.type = "button";
|
|
820
|
-
tile.className = "add-container-btn fb-slide-add";
|
|
821
|
-
tile.style.cssText = `
|
|
822
|
-
display: flex;
|
|
823
|
-
flex-direction: column;
|
|
824
|
-
align-items: center;
|
|
825
|
-
justify-content: center;
|
|
826
|
-
gap: 12px;
|
|
827
|
-
width: 100%;
|
|
828
|
-
min-height: 180px;
|
|
829
|
-
align-self: stretch;
|
|
830
|
-
padding: 24px 16px;
|
|
831
|
-
border: 1.5px dashed var(--fb-primary-color);
|
|
832
|
-
border-radius: var(--fb-border-radius);
|
|
833
|
-
background: transparent;
|
|
834
|
-
color: var(--fb-primary-color);
|
|
835
|
-
font-size: var(--fb-font-size-small, var(--fb-font-size));
|
|
836
|
-
font-weight: 500;
|
|
837
|
-
font-family: var(--fb-font-family);
|
|
838
|
-
cursor: pointer;
|
|
839
|
-
transition: border-color var(--fb-transition-duration), color var(--fb-transition-duration), background-color var(--fb-transition-duration);
|
|
840
|
-
`;
|
|
841
|
-
const circle = document.createElement("span");
|
|
842
|
-
circle.className = "fb-slide-add-circle";
|
|
843
|
-
circle.style.cssText = `
|
|
844
|
-
display: inline-flex;
|
|
845
|
-
align-items: center;
|
|
846
|
-
justify-content: center;
|
|
847
|
-
width: 36px;
|
|
848
|
-
height: 36px;
|
|
849
|
-
border: 1px solid var(--fb-primary-color);
|
|
850
|
-
border-radius: 50%;
|
|
851
|
-
background: var(--fb-background-color);
|
|
852
|
-
font-size: 20px;
|
|
853
|
-
line-height: 1;
|
|
854
|
-
color: inherit;
|
|
855
|
-
transition: inherit;
|
|
856
|
-
`;
|
|
857
|
-
circle.textContent = "+";
|
|
858
|
-
tile.appendChild(circle);
|
|
859
|
-
if (label) {
|
|
860
|
-
const text = document.createElement("span");
|
|
861
|
-
text.textContent = label;
|
|
862
|
-
tile.appendChild(text);
|
|
1038
|
+
if (!condition || !condition.key) {
|
|
1039
|
+
throw new Error("Invalid enableIf condition: must have a 'key' property");
|
|
863
1040
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
})
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
const update = (current, max) => {
|
|
883
|
-
const reached = current >= max;
|
|
884
|
-
tile.style.display = reached ? "none" : "flex";
|
|
885
|
-
tile.disabled = reached;
|
|
886
|
-
counter.textContent = `${current}/${max === Infinity ? "\u221E" : max}`;
|
|
887
|
-
};
|
|
888
|
-
return { tile, counter, update };
|
|
1041
|
+
const scope = (_a = condition.scope) != null ? _a : "relative";
|
|
1042
|
+
let dataSource;
|
|
1043
|
+
if (scope === "relative") {
|
|
1044
|
+
dataSource = containerData != null ? containerData : formData;
|
|
1045
|
+
} else if (scope === "absolute") {
|
|
1046
|
+
dataSource = formData;
|
|
1047
|
+
} else {
|
|
1048
|
+
throw new Error(
|
|
1049
|
+
`Invalid enableIf scope: must be "relative" or "absolute" (got "${scope}")`
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
const actualValue = getValueByPath(dataSource, condition.key);
|
|
1053
|
+
if ("equals" in condition) {
|
|
1054
|
+
return deepEqual(actualValue, condition.equals);
|
|
1055
|
+
}
|
|
1056
|
+
throw new Error(
|
|
1057
|
+
`Invalid enableIf condition: no recognized operator (equals, etc.)`
|
|
1058
|
+
);
|
|
889
1059
|
}
|
|
890
|
-
function
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
button.style.borderColor = "var(--fb-action-border-color)";
|
|
909
|
-
});
|
|
1060
|
+
function deepEqual(a, b) {
|
|
1061
|
+
if (a === b) return true;
|
|
1062
|
+
if (a == null || b == null) return a === b;
|
|
1063
|
+
if (typeof a !== typeof b) return false;
|
|
1064
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
1065
|
+
try {
|
|
1066
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
1067
|
+
} catch (e) {
|
|
1068
|
+
if (e instanceof TypeError && (e.message.includes("circular") || e.message.includes("cyclic"))) {
|
|
1069
|
+
console.warn(
|
|
1070
|
+
"deepEqual: Circular reference detected in enableIf comparison, using reference equality"
|
|
1071
|
+
);
|
|
1072
|
+
return a === b;
|
|
1073
|
+
}
|
|
1074
|
+
throw e;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return a === b;
|
|
910
1078
|
}
|
|
911
1079
|
|
|
912
1080
|
// src/components/text.ts
|
|
@@ -1195,78 +1363,53 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1195
1363
|
updateRemoveButtons();
|
|
1196
1364
|
}
|
|
1197
1365
|
function validateTextElement(element, key, context) {
|
|
1198
|
-
var _a
|
|
1366
|
+
var _a;
|
|
1199
1367
|
const errors = [];
|
|
1200
|
-
const { scopeRoot,
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
markFieldValidity(input, msg);
|
|
1214
|
-
hasError = true;
|
|
1215
|
-
} else if (element.pattern) {
|
|
1216
|
-
try {
|
|
1217
|
-
const re = new RegExp(element.pattern);
|
|
1218
|
-
if (!re.test(val)) {
|
|
1219
|
-
const msg = t("patternMismatch", state);
|
|
1220
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
1221
|
-
markFieldValidity(input, msg);
|
|
1222
|
-
hasError = true;
|
|
1223
|
-
}
|
|
1224
|
-
} catch {
|
|
1225
|
-
const msg = t("invalidPattern", state);
|
|
1226
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
1227
|
-
markFieldValidity(input, msg);
|
|
1228
|
-
hasError = true;
|
|
1368
|
+
const { scopeRoot, state } = context;
|
|
1369
|
+
const lengthOrPatternError = (val) => {
|
|
1370
|
+
if (!val) return null;
|
|
1371
|
+
if (element.minLength != null && val.length < element.minLength) {
|
|
1372
|
+
return t("minLength", state, { min: element.minLength });
|
|
1373
|
+
}
|
|
1374
|
+
if (element.maxLength != null && val.length > element.maxLength) {
|
|
1375
|
+
return t("maxLength", state, { max: element.maxLength });
|
|
1376
|
+
}
|
|
1377
|
+
if (element.pattern) {
|
|
1378
|
+
try {
|
|
1379
|
+
if (!new RegExp(element.pattern).test(val)) {
|
|
1380
|
+
return t("patternMismatch", state);
|
|
1229
1381
|
}
|
|
1382
|
+
} catch {
|
|
1383
|
+
return t("invalidPattern", state);
|
|
1230
1384
|
}
|
|
1231
1385
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1386
|
+
return null;
|
|
1387
|
+
};
|
|
1388
|
+
const validateTextInput = (input, val, fieldKey) => {
|
|
1389
|
+
const msg = lengthOrPatternError(val);
|
|
1390
|
+
if (msg !== null) errors.push(`${fieldKey}: ${msg}`);
|
|
1391
|
+
markFieldValidity(input, msg, context);
|
|
1235
1392
|
};
|
|
1236
1393
|
if (element.multiple) {
|
|
1237
1394
|
const inputs = scopeRoot.querySelectorAll(`[name^="${key}\\["]`);
|
|
1238
1395
|
const values = [];
|
|
1239
|
-
|
|
1396
|
+
let filledCount = 0;
|
|
1240
1397
|
inputs.forEach((input, index) => {
|
|
1241
1398
|
var _a2;
|
|
1242
1399
|
const val = (_a2 = input == null ? void 0 : input.value) != null ? _a2 : "";
|
|
1243
|
-
rawValues.push(val);
|
|
1244
1400
|
values.push(val === "" ? null : val);
|
|
1401
|
+
if (val.trim() !== "") filledCount++;
|
|
1245
1402
|
validateTextInput(input, val, `${key}[${index}]`);
|
|
1246
1403
|
});
|
|
1247
|
-
|
|
1248
|
-
const { state } = context;
|
|
1249
|
-
const minCount = (_a = element.minCount) != null ? _a : 0;
|
|
1250
|
-
const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
|
|
1251
|
-
const filteredValues = rawValues.filter((v) => v.trim() !== "");
|
|
1252
|
-
if (element.required && filteredValues.length === 0) {
|
|
1253
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
1254
|
-
}
|
|
1255
|
-
if (filteredValues.length < minCount) {
|
|
1256
|
-
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
1257
|
-
}
|
|
1258
|
-
if (filteredValues.length > maxCount) {
|
|
1259
|
-
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1404
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
1262
1405
|
return { value: values, errors };
|
|
1263
1406
|
} else {
|
|
1264
1407
|
const input = scopeRoot.querySelector(`[name="${key}"]`);
|
|
1265
|
-
const val = (
|
|
1266
|
-
if (
|
|
1267
|
-
const msg = t("required",
|
|
1408
|
+
const val = (_a = input == null ? void 0 : input.value) != null ? _a : "";
|
|
1409
|
+
if (element.required && val === "") {
|
|
1410
|
+
const msg = t("required", state);
|
|
1268
1411
|
errors.push(`${key}: ${msg}`);
|
|
1269
|
-
markFieldValidity(input, msg);
|
|
1412
|
+
markFieldValidity(input, msg, context);
|
|
1270
1413
|
return { value: null, errors };
|
|
1271
1414
|
}
|
|
1272
1415
|
if (input) {
|
|
@@ -1288,8 +1431,6 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
1288
1431
|
inputs.forEach((input, index) => {
|
|
1289
1432
|
if (index < value.length) {
|
|
1290
1433
|
input.value = value[index] != null ? String(value[index]) : "";
|
|
1291
|
-
input.classList.remove("invalid");
|
|
1292
|
-
input.title = "";
|
|
1293
1434
|
clearFieldError(input);
|
|
1294
1435
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1295
1436
|
}
|
|
@@ -1303,8 +1444,6 @@ function updateTextField(element, fieldPath, value, context) {
|
|
|
1303
1444
|
const input = scopeRoot.querySelector(`[name="${fieldPath}"]`);
|
|
1304
1445
|
if (input) {
|
|
1305
1446
|
input.value = value != null ? String(value) : "";
|
|
1306
|
-
input.classList.remove("invalid");
|
|
1307
|
-
input.title = "";
|
|
1308
1447
|
clearFieldError(input);
|
|
1309
1448
|
if (input instanceof HTMLTextAreaElement) {
|
|
1310
1449
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
@@ -1810,26 +1949,29 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1810
1949
|
updateRemoveButtons();
|
|
1811
1950
|
}
|
|
1812
1951
|
function validateNumberElement(element, key, context) {
|
|
1813
|
-
var _a
|
|
1952
|
+
var _a;
|
|
1814
1953
|
const errors = [];
|
|
1815
|
-
const { scopeRoot,
|
|
1816
|
-
const
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
if (!hasError) {
|
|
1831
|
-
markFieldValidity(input, null);
|
|
1954
|
+
const { scopeRoot, state } = context;
|
|
1955
|
+
const rangeError = (v) => {
|
|
1956
|
+
if (element.min != null && v < element.min) {
|
|
1957
|
+
return t("minValue", state, { min: element.min });
|
|
1958
|
+
}
|
|
1959
|
+
if (element.max != null && v > element.max) {
|
|
1960
|
+
return t("maxValue", state, { max: element.max });
|
|
1961
|
+
}
|
|
1962
|
+
return null;
|
|
1963
|
+
};
|
|
1964
|
+
const validateNumberInput = (input, fieldKey) => {
|
|
1965
|
+
const raw = input.value;
|
|
1966
|
+
if (raw === "") {
|
|
1967
|
+
markFieldValidity(input, null, context);
|
|
1968
|
+
return null;
|
|
1832
1969
|
}
|
|
1970
|
+
const v = parseFloat(raw);
|
|
1971
|
+
const msg = Number.isFinite(v) ? rangeError(v) : t("notANumber", state);
|
|
1972
|
+
if (msg !== null) errors.push(`${fieldKey}: ${msg}`);
|
|
1973
|
+
markFieldValidity(input, msg, context);
|
|
1974
|
+
return Number.isFinite(v) ? applyDecimals(v, element.decimals) : null;
|
|
1833
1975
|
};
|
|
1834
1976
|
if (element.multiple) {
|
|
1835
1977
|
const inputs = scopeRoot.querySelectorAll(
|
|
@@ -1837,63 +1979,21 @@ function validateNumberElement(element, key, context) {
|
|
|
1837
1979
|
);
|
|
1838
1980
|
const values = [];
|
|
1839
1981
|
inputs.forEach((input, index) => {
|
|
1840
|
-
|
|
1841
|
-
const raw = (_a2 = input == null ? void 0 : input.value) != null ? _a2 : "";
|
|
1842
|
-
if (raw === "") {
|
|
1843
|
-
values.push(null);
|
|
1844
|
-
markFieldValidity(input, null);
|
|
1845
|
-
return;
|
|
1846
|
-
}
|
|
1847
|
-
const v = parseFloat(raw);
|
|
1848
|
-
if (!skipValidation && !Number.isFinite(v)) {
|
|
1849
|
-
const msg = t("notANumber", context.state);
|
|
1850
|
-
errors.push(`${key}[${index}]: ${msg}`);
|
|
1851
|
-
markFieldValidity(input, msg);
|
|
1852
|
-
values.push(null);
|
|
1853
|
-
return;
|
|
1854
|
-
}
|
|
1855
|
-
validateNumberInput(input, v, `${key}[${index}]`);
|
|
1856
|
-
values.push(applyDecimals(v, element.decimals));
|
|
1982
|
+
values.push(validateNumberInput(input, `${key}[${index}]`));
|
|
1857
1983
|
});
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
const minCount = (_a = element.minCount) != null ? _a : 0;
|
|
1861
|
-
const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
|
|
1862
|
-
const filteredValues = values.filter((v) => v !== null);
|
|
1863
|
-
if (element.required && filteredValues.length === 0) {
|
|
1864
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
1865
|
-
}
|
|
1866
|
-
if (filteredValues.length < minCount) {
|
|
1867
|
-
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
1868
|
-
}
|
|
1869
|
-
if (filteredValues.length > maxCount) {
|
|
1870
|
-
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1984
|
+
const filledCount = values.filter((v) => v !== null).length;
|
|
1985
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
1873
1986
|
return { value: values, errors };
|
|
1874
1987
|
} else {
|
|
1875
1988
|
const input = scopeRoot.querySelector(`[name="${key}"]`);
|
|
1876
|
-
|
|
1877
|
-
const { state } = context;
|
|
1878
|
-
if (!skipValidation && element.required && raw === "") {
|
|
1989
|
+
if (element.required && ((_a = input == null ? void 0 : input.value) != null ? _a : "") === "") {
|
|
1879
1990
|
const msg = t("required", state);
|
|
1880
1991
|
errors.push(`${key}: ${msg}`);
|
|
1881
|
-
markFieldValidity(input, msg);
|
|
1882
|
-
return { value: null, errors };
|
|
1883
|
-
}
|
|
1884
|
-
if (raw === "") {
|
|
1885
|
-
markFieldValidity(input, null);
|
|
1886
|
-
return { value: null, errors };
|
|
1887
|
-
}
|
|
1888
|
-
const v = parseFloat(raw);
|
|
1889
|
-
if (!skipValidation && !Number.isFinite(v)) {
|
|
1890
|
-
const msg = t("notANumber", state);
|
|
1891
|
-
errors.push(`${key}: ${msg}`);
|
|
1892
|
-
markFieldValidity(input, msg);
|
|
1992
|
+
markFieldValidity(input, msg, context);
|
|
1893
1993
|
return { value: null, errors };
|
|
1894
1994
|
}
|
|
1895
|
-
|
|
1896
|
-
return { value:
|
|
1995
|
+
if (!input) return { value: null, errors };
|
|
1996
|
+
return { value: validateNumberInput(input, key), errors };
|
|
1897
1997
|
}
|
|
1898
1998
|
}
|
|
1899
1999
|
function applyDecimals(v, decimals) {
|
|
@@ -1915,8 +2015,6 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1915
2015
|
inputs.forEach((input, index) => {
|
|
1916
2016
|
if (index < value.length) {
|
|
1917
2017
|
input.value = value[index] != null ? String(value[index]) : "";
|
|
1918
|
-
input.classList.remove("invalid");
|
|
1919
|
-
input.title = "";
|
|
1920
2018
|
clearFieldError(input);
|
|
1921
2019
|
}
|
|
1922
2020
|
});
|
|
@@ -1931,8 +2029,6 @@ function updateNumberField(element, fieldPath, value, context) {
|
|
|
1931
2029
|
);
|
|
1932
2030
|
if (input) {
|
|
1933
2031
|
input.value = value != null ? String(value) : "";
|
|
1934
|
-
input.classList.remove("invalid");
|
|
1935
|
-
input.title = "";
|
|
1936
2032
|
clearFieldError(input);
|
|
1937
2033
|
}
|
|
1938
2034
|
}
|
|
@@ -2126,24 +2222,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
2126
2222
|
function validateSelectElement(element, key, context) {
|
|
2127
2223
|
var _a;
|
|
2128
2224
|
const errors = [];
|
|
2129
|
-
const { scopeRoot
|
|
2130
|
-
const validateMultipleCount = (key2, values, element2, filterFn) => {
|
|
2131
|
-
var _a2, _b;
|
|
2132
|
-
if (skipValidation) return;
|
|
2133
|
-
const { state } = context;
|
|
2134
|
-
const filteredValues = values.filter(filterFn);
|
|
2135
|
-
const minCount = "minCount" in element2 ? (_a2 = element2.minCount) != null ? _a2 : 0 : 0;
|
|
2136
|
-
const maxCount = "maxCount" in element2 ? (_b = element2.maxCount) != null ? _b : Infinity : Infinity;
|
|
2137
|
-
if (element2.required && filteredValues.length === 0) {
|
|
2138
|
-
errors.push(`${key2}: ${t("required", state)}`);
|
|
2139
|
-
}
|
|
2140
|
-
if (filteredValues.length < minCount) {
|
|
2141
|
-
errors.push(`${key2}: ${t("minItems", state, { min: minCount })}`);
|
|
2142
|
-
}
|
|
2143
|
-
if (filteredValues.length > maxCount) {
|
|
2144
|
-
errors.push(`${key2}: ${t("maxItems", state, { max: maxCount })}`);
|
|
2145
|
-
}
|
|
2146
|
-
};
|
|
2225
|
+
const { scopeRoot } = context;
|
|
2147
2226
|
if ("multiple" in element && element.multiple) {
|
|
2148
2227
|
const inputs = scopeRoot.querySelectorAll(
|
|
2149
2228
|
`[name^="${key}\\["]`
|
|
@@ -2153,21 +2232,21 @@ function validateSelectElement(element, key, context) {
|
|
|
2153
2232
|
var _a2;
|
|
2154
2233
|
const val = (_a2 = input == null ? void 0 : input.value) != null ? _a2 : "";
|
|
2155
2234
|
values.push(val === "" ? null : val);
|
|
2156
|
-
markFieldValidity(input, null);
|
|
2235
|
+
markFieldValidity(input, null, context);
|
|
2157
2236
|
});
|
|
2158
|
-
|
|
2237
|
+
const filledCount = values.filter((v) => v != null).length;
|
|
2238
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
2159
2239
|
return { value: values, errors };
|
|
2160
2240
|
} else {
|
|
2161
2241
|
const input = scopeRoot.querySelector(`[name="${key}"]`);
|
|
2162
2242
|
const val = (_a = input == null ? void 0 : input.value) != null ? _a : "";
|
|
2163
|
-
if (
|
|
2243
|
+
if (element.required && val === "") {
|
|
2164
2244
|
const msg = t("required", context.state);
|
|
2165
2245
|
errors.push(`${key}: ${msg}`);
|
|
2166
|
-
markFieldValidity(input, msg);
|
|
2246
|
+
markFieldValidity(input, msg, context);
|
|
2167
2247
|
return { value: null, errors };
|
|
2168
|
-
} else {
|
|
2169
|
-
markFieldValidity(input, null);
|
|
2170
2248
|
}
|
|
2249
|
+
markFieldValidity(input, null, context);
|
|
2171
2250
|
return { value: val === "" ? null : val, errors };
|
|
2172
2251
|
}
|
|
2173
2252
|
}
|
|
@@ -2208,8 +2287,6 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
2208
2287
|
options.forEach((option) => {
|
|
2209
2288
|
option.selected = option.value === strValue;
|
|
2210
2289
|
});
|
|
2211
|
-
select.classList.remove("invalid");
|
|
2212
|
-
select.title = "";
|
|
2213
2290
|
clearFieldError(select);
|
|
2214
2291
|
}
|
|
2215
2292
|
});
|
|
@@ -2230,8 +2307,6 @@ function updateSelectField(element, fieldPath, value, context) {
|
|
|
2230
2307
|
options.forEach((option) => {
|
|
2231
2308
|
option.selected = option.value === strValue;
|
|
2232
2309
|
});
|
|
2233
|
-
select.classList.remove("invalid");
|
|
2234
|
-
select.title = "";
|
|
2235
2310
|
clearFieldError(select);
|
|
2236
2311
|
}
|
|
2237
2312
|
}
|
|
@@ -2558,27 +2633,11 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
|
|
|
2558
2633
|
function validateSwitcherElement(element, key, context) {
|
|
2559
2634
|
var _a;
|
|
2560
2635
|
const errors = [];
|
|
2561
|
-
const { scopeRoot,
|
|
2562
|
-
const validateMultipleCount = (fieldKey, values, el, filterFn) => {
|
|
2563
|
-
var _a2, _b;
|
|
2564
|
-
if (skipValidation) return;
|
|
2565
|
-
const { state } = context;
|
|
2566
|
-
const filteredValues = values.filter(filterFn);
|
|
2567
|
-
const minCount = "minCount" in el ? (_a2 = el.minCount) != null ? _a2 : 0 : 0;
|
|
2568
|
-
const maxCount = "maxCount" in el ? (_b = el.maxCount) != null ? _b : Infinity : Infinity;
|
|
2569
|
-
if (el.required && filteredValues.length === 0) {
|
|
2570
|
-
errors.push(`${fieldKey}: ${t("required", state)}`);
|
|
2571
|
-
}
|
|
2572
|
-
if (filteredValues.length < minCount) {
|
|
2573
|
-
errors.push(`${fieldKey}: ${t("minItems", state, { min: minCount })}`);
|
|
2574
|
-
}
|
|
2575
|
-
if (filteredValues.length > maxCount) {
|
|
2576
|
-
errors.push(`${fieldKey}: ${t("maxItems", state, { max: maxCount })}`);
|
|
2577
|
-
}
|
|
2578
|
-
};
|
|
2636
|
+
const { scopeRoot, state } = context;
|
|
2579
2637
|
const validOptionValues = new Set(
|
|
2580
2638
|
"options" in element ? element.options.map((o) => o.value) : []
|
|
2581
2639
|
);
|
|
2640
|
+
const optionError = (val) => val !== "" && !validOptionValues.has(val) ? t("invalidOption", state) : null;
|
|
2582
2641
|
if ("multiple" in element && element.multiple) {
|
|
2583
2642
|
const inputs = scopeRoot.querySelectorAll(
|
|
2584
2643
|
`input[type="hidden"][name^="${key}\\["]`
|
|
@@ -2588,37 +2647,37 @@ function validateSwitcherElement(element, key, context) {
|
|
|
2588
2647
|
var _a2;
|
|
2589
2648
|
const val = (_a2 = input == null ? void 0 : input.value) != null ? _a2 : "";
|
|
2590
2649
|
values.push(val === "" ? null : val);
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
errors.push(`${key}: ${msg}`);
|
|
2595
|
-
} else {
|
|
2596
|
-
markFieldValidity(input, null);
|
|
2597
|
-
}
|
|
2650
|
+
const msg = optionError(val);
|
|
2651
|
+
if (msg !== null) errors.push(`${key}: ${msg}`);
|
|
2652
|
+
markFieldValidity(switcherGroupOf(input), msg, context);
|
|
2598
2653
|
});
|
|
2599
|
-
|
|
2654
|
+
const filledCount = values.filter((v) => v != null).length;
|
|
2655
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
2600
2656
|
return { value: values, errors };
|
|
2601
2657
|
} else {
|
|
2602
2658
|
const input = scopeRoot.querySelector(
|
|
2603
2659
|
`input[type="hidden"][name="${key}"]`
|
|
2604
2660
|
);
|
|
2605
2661
|
const val = (_a = input == null ? void 0 : input.value) != null ? _a : "";
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
markFieldValidity(input, msg);
|
|
2610
|
-
return { value: null, errors };
|
|
2611
|
-
}
|
|
2612
|
-
if (!skipValidation && val !== "" && !validOptionValues.has(val)) {
|
|
2613
|
-
const msg = t("invalidOption", context.state);
|
|
2662
|
+
const msg = element.required && val === "" ? t("required", state) : optionError(val);
|
|
2663
|
+
if (input) markFieldValidity(switcherGroupOf(input), msg, context);
|
|
2664
|
+
if (msg !== null) {
|
|
2614
2665
|
errors.push(`${key}: ${msg}`);
|
|
2615
|
-
markFieldValidity(input, msg);
|
|
2616
2666
|
return { value: null, errors };
|
|
2617
2667
|
}
|
|
2618
|
-
markFieldValidity(input, null);
|
|
2619
2668
|
return { value: val === "" ? null : val, errors };
|
|
2620
2669
|
}
|
|
2621
2670
|
}
|
|
2671
|
+
function switcherGroupOf(input) {
|
|
2672
|
+
var _a;
|
|
2673
|
+
const group = (_a = input.parentElement) == null ? void 0 : _a.querySelector(".fb-switcher-group");
|
|
2674
|
+
if (!group) {
|
|
2675
|
+
throw new Error(
|
|
2676
|
+
`switcher "${input.name}": no .fb-switcher-group next to its hidden input`
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
return group;
|
|
2680
|
+
}
|
|
2622
2681
|
function updateSwitcherField(element, fieldPath, value, context) {
|
|
2623
2682
|
var _a;
|
|
2624
2683
|
const { scopeRoot } = context;
|
|
@@ -2648,9 +2707,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2648
2707
|
}
|
|
2649
2708
|
});
|
|
2650
2709
|
}
|
|
2651
|
-
input
|
|
2652
|
-
input.title = "";
|
|
2653
|
-
clearFieldError(input);
|
|
2710
|
+
clearFieldError(switcherGroupOf(input));
|
|
2654
2711
|
}
|
|
2655
2712
|
});
|
|
2656
2713
|
if (value.length !== inputs.length) {
|
|
@@ -2676,9 +2733,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
|
|
|
2676
2733
|
}
|
|
2677
2734
|
});
|
|
2678
2735
|
}
|
|
2679
|
-
input
|
|
2680
|
-
input.title = "";
|
|
2681
|
-
clearFieldError(input);
|
|
2736
|
+
clearFieldError(switcherGroupOf(input));
|
|
2682
2737
|
}
|
|
2683
2738
|
}
|
|
2684
2739
|
}
|
|
@@ -3206,14 +3261,20 @@ function ensureFileStyles() {
|
|
|
3206
3261
|
padding: 6px;
|
|
3207
3262
|
}
|
|
3208
3263
|
|
|
3209
|
-
/* \u2500\u2500\u2500
|
|
3210
|
-
.fb-
|
|
3264
|
+
/* \u2500\u2500\u2500 Footer row below multi grid: N/max counter + clear-all \u2500\u2500\u2500 */
|
|
3265
|
+
.fb-multi-footer {
|
|
3211
3266
|
margin-top: 10px;
|
|
3212
3267
|
display: flex;
|
|
3213
3268
|
align-items: center;
|
|
3214
|
-
|
|
3269
|
+
gap: 8px;
|
|
3270
|
+
}
|
|
3271
|
+
.fb-files-counter {
|
|
3272
|
+
font-size: var(--fb-font-size-small, 12px);
|
|
3273
|
+
color: var(--fb-text-secondary-color, #6b7280);
|
|
3274
|
+
font-variant-numeric: tabular-nums;
|
|
3215
3275
|
}
|
|
3216
3276
|
.fb-clear-all-btn {
|
|
3277
|
+
margin-left: auto;
|
|
3217
3278
|
font-size: 12px;
|
|
3218
3279
|
color: #94a3b8;
|
|
3219
3280
|
background: none;
|
|
@@ -3463,24 +3524,55 @@ function createFileTile() {
|
|
|
3463
3524
|
tile.className = "fb-tile";
|
|
3464
3525
|
return tile;
|
|
3465
3526
|
}
|
|
3466
|
-
function
|
|
3467
|
-
var _a, _b;
|
|
3468
|
-
const existing = (_a = container.closest("[data-files-wrapper]")) == null ? void 0 : _a.querySelector(".file-error-message");
|
|
3469
|
-
if (existing) existing.remove();
|
|
3470
|
-
const errorEl = document.createElement("div");
|
|
3471
|
-
errorEl.className = "file-error-message error-message";
|
|
3472
|
-
errorEl.style.cssText = `
|
|
3473
|
-
color: var(--fb-error-color);
|
|
3474
|
-
font-size: var(--fb-font-size-small);
|
|
3475
|
-
margin-top: 0.25rem;
|
|
3476
|
-
`;
|
|
3477
|
-
errorEl.textContent = message;
|
|
3478
|
-
(_b = container.closest("[data-files-wrapper]")) == null ? void 0 : _b.appendChild(errorEl);
|
|
3479
|
-
}
|
|
3480
|
-
function clearFileError(container) {
|
|
3527
|
+
function fileErrorSlot(container) {
|
|
3481
3528
|
var _a;
|
|
3482
|
-
const
|
|
3483
|
-
|
|
3529
|
+
const wrapper = container.closest("[data-files-wrapper]");
|
|
3530
|
+
const node = wrapper ? (_a = Array.from(wrapper.children).find(
|
|
3531
|
+
(child) => child instanceof HTMLElement && child.classList.contains("file-error-message")
|
|
3532
|
+
)) != null ? _a : null : null;
|
|
3533
|
+
return { wrapper, node };
|
|
3534
|
+
}
|
|
3535
|
+
function showFileError(container, message, state, kind = "action") {
|
|
3536
|
+
const { wrapper, node: existing } = fileErrorSlot(container);
|
|
3537
|
+
if (!wrapper) return;
|
|
3538
|
+
let node = existing;
|
|
3539
|
+
if (!node) {
|
|
3540
|
+
node = createErrorNode(state, "file-error-message error-message");
|
|
3541
|
+
wrapper.appendChild(node);
|
|
3542
|
+
}
|
|
3543
|
+
setAttr(node, "data-error-kind", kind);
|
|
3544
|
+
if (node.textContent !== message) node.textContent = message;
|
|
3545
|
+
if (kind === "action") {
|
|
3546
|
+
setAttr(node, "role", "alert");
|
|
3547
|
+
unsetInvalidState(wrapper);
|
|
3548
|
+
linkDescription(wrapper, node);
|
|
3549
|
+
} else {
|
|
3550
|
+
node.removeAttribute("role");
|
|
3551
|
+
setInvalidMark(wrapper, node, state);
|
|
3552
|
+
}
|
|
3553
|
+
}
|
|
3554
|
+
function clearFileError(container, kind = "action") {
|
|
3555
|
+
const { wrapper, node } = fileErrorSlot(container);
|
|
3556
|
+
if (wrapper && (node == null ? void 0 : node.dataset.errorKind) === kind) {
|
|
3557
|
+
clearInvalidMark(wrapper, node);
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
function markFileValidity(wrapper, message, scope) {
|
|
3561
|
+
const mark = resolveMark(wrapper, message, scope);
|
|
3562
|
+
if (mark === void 0) return;
|
|
3563
|
+
const { node } = fileErrorSlot(wrapper);
|
|
3564
|
+
if (scope.readonly) {
|
|
3565
|
+
clearInvalidMark(wrapper, node);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
if (mark === null) {
|
|
3569
|
+
clearFileError(wrapper, "validation");
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
if (scope.draftMarks && node && node.dataset.errorKind !== "validation") {
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3575
|
+
showFileError(wrapper, mark, scope.state, "validation");
|
|
3484
3576
|
}
|
|
3485
3577
|
function addDeleteButton(container, state, onDelete) {
|
|
3486
3578
|
const existingOverlay = container.querySelector(".delete-overlay");
|
|
@@ -4350,7 +4442,8 @@ async function handleFileSelect(opts) {
|
|
|
4350
4442
|
const formats = allowedExtensions.join(", ");
|
|
4351
4443
|
showFileError(
|
|
4352
4444
|
container,
|
|
4353
|
-
t("invalidFileExtension", state, { name: file.name, formats })
|
|
4445
|
+
t("invalidFileExtension", state, { name: file.name, formats }),
|
|
4446
|
+
state
|
|
4354
4447
|
);
|
|
4355
4448
|
return;
|
|
4356
4449
|
}
|
|
@@ -4358,14 +4451,16 @@ async function handleFileSelect(opts) {
|
|
|
4358
4451
|
const mimes = allowedMimes.join(", ");
|
|
4359
4452
|
showFileError(
|
|
4360
4453
|
container,
|
|
4361
|
-
t("invalidFileMime", state, { name: file.name, type: file.type, mimes })
|
|
4454
|
+
t("invalidFileMime", state, { name: file.name, type: file.type, mimes }),
|
|
4455
|
+
state
|
|
4362
4456
|
);
|
|
4363
4457
|
return;
|
|
4364
4458
|
}
|
|
4365
4459
|
if (!isFileSizeAllowed(file, maxSizeMB)) {
|
|
4366
4460
|
showFileError(
|
|
4367
4461
|
container,
|
|
4368
|
-
t("fileTooLarge", state, { name: file.name, maxSize: maxSizeMB })
|
|
4462
|
+
t("fileTooLarge", state, { name: file.name, maxSize: maxSizeMB }),
|
|
4463
|
+
state
|
|
4369
4464
|
);
|
|
4370
4465
|
return;
|
|
4371
4466
|
}
|
|
@@ -4562,7 +4657,7 @@ async function runMultiFileBatch(opts, files, listEl, errorTarget) {
|
|
|
4562
4657
|
state
|
|
4563
4658
|
);
|
|
4564
4659
|
if (errorTarget) {
|
|
4565
|
-
if (errorMessage) showFileError(errorTarget, errorMessage);
|
|
4660
|
+
if (errorMessage) showFileError(errorTarget, errorMessage, state);
|
|
4566
4661
|
else clearFileError(errorTarget);
|
|
4567
4662
|
}
|
|
4568
4663
|
const handle = coordinator.beginBatch(accepted.length);
|
|
@@ -4581,11 +4676,8 @@ async function runMultiFileBatch(opts, files, listEl, errorTarget) {
|
|
|
4581
4676
|
}
|
|
4582
4677
|
const { wasLast } = handle.end();
|
|
4583
4678
|
if (wasLast) updateCallback();
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
if (combined) showFileError(errorTarget, combined);
|
|
4587
|
-
else clearFileError(errorTarget);
|
|
4588
|
-
}
|
|
4679
|
+
const combined = buildBatchErrorMessage(errorMessage, failures, state);
|
|
4680
|
+
if (errorTarget && combined) showFileError(errorTarget, combined, state);
|
|
4589
4681
|
}
|
|
4590
4682
|
function setupFilesDropHandler(opts) {
|
|
4591
4683
|
const { filesContainer } = opts;
|
|
@@ -4700,7 +4792,7 @@ async function handleLibraryPickMulti(opts) {
|
|
|
4700
4792
|
selectedResourceIds: knownRids
|
|
4701
4793
|
});
|
|
4702
4794
|
} catch (error) {
|
|
4703
|
-
showFileError(wrapper, extractPickerError(error, state));
|
|
4795
|
+
showFileError(wrapper, extractPickerError(error, state), state);
|
|
4704
4796
|
return;
|
|
4705
4797
|
}
|
|
4706
4798
|
if (picked.length === 0) return;
|
|
@@ -4728,7 +4820,8 @@ async function handleLibraryPickMulti(opts) {
|
|
|
4728
4820
|
if (skipped > 0) {
|
|
4729
4821
|
showFileError(
|
|
4730
4822
|
wrapper,
|
|
4731
|
-
t("filesLimitExceeded", state, { skipped, max: maxCount })
|
|
4823
|
+
t("filesLimitExceeded", state, { skipped, max: maxCount }),
|
|
4824
|
+
state
|
|
4732
4825
|
);
|
|
4733
4826
|
}
|
|
4734
4827
|
return;
|
|
@@ -4737,7 +4830,8 @@ async function handleLibraryPickMulti(opts) {
|
|
|
4737
4830
|
if (skipped > 0) {
|
|
4738
4831
|
showFileError(
|
|
4739
4832
|
wrapper,
|
|
4740
|
-
t("filesLimitExceeded", state, { skipped, max: maxCount })
|
|
4833
|
+
t("filesLimitExceeded", state, { skipped, max: maxCount }),
|
|
4834
|
+
state
|
|
4741
4835
|
);
|
|
4742
4836
|
}
|
|
4743
4837
|
for (const resource of accepted) {
|
|
@@ -4775,7 +4869,7 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
|
|
|
4775
4869
|
selectedResourceIds: []
|
|
4776
4870
|
});
|
|
4777
4871
|
} catch (error) {
|
|
4778
|
-
showFileError(container, extractPickerError(error, state));
|
|
4872
|
+
showFileError(container, extractPickerError(error, state), state);
|
|
4779
4873
|
return;
|
|
4780
4874
|
}
|
|
4781
4875
|
if (picked.length === 0) return;
|
|
@@ -4788,7 +4882,7 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
|
|
|
4788
4882
|
state
|
|
4789
4883
|
);
|
|
4790
4884
|
if (validationError !== null) {
|
|
4791
|
-
showFileError(container, validationError);
|
|
4885
|
+
showFileError(container, validationError, state);
|
|
4792
4886
|
return;
|
|
4793
4887
|
}
|
|
4794
4888
|
clearFileError(container);
|
|
@@ -5031,10 +5125,7 @@ function buildPlaceholderTile(isDragOver = false) {
|
|
|
5031
5125
|
div.className = `fb-multi-placeholder fb-checker${isDragOver ? " fb-drag-over" : ""}`;
|
|
5032
5126
|
return div;
|
|
5033
5127
|
}
|
|
5034
|
-
function
|
|
5035
|
-
if (ridCount <= 1) return null;
|
|
5036
|
-
const row = document.createElement("div");
|
|
5037
|
-
row.className = "fb-clear-all-row";
|
|
5128
|
+
function buildClearAllButton(state, onClearAll) {
|
|
5038
5129
|
const clearBtn = document.createElement("button");
|
|
5039
5130
|
clearBtn.type = "button";
|
|
5040
5131
|
clearBtn.className = "fb-clear-all-btn";
|
|
@@ -5045,9 +5136,38 @@ function buildClearAllRow(state, ridCount, onClearAll) {
|
|
|
5045
5136
|
onClearAll();
|
|
5046
5137
|
}
|
|
5047
5138
|
};
|
|
5048
|
-
|
|
5139
|
+
return clearBtn;
|
|
5140
|
+
}
|
|
5141
|
+
function buildFooterRow(state, ridCount, maxCount, onClearAll) {
|
|
5142
|
+
const showCounter = maxCount !== Infinity;
|
|
5143
|
+
const showClearAll = onClearAll !== void 0 && ridCount > 1;
|
|
5144
|
+
if (!showCounter && !showClearAll) return null;
|
|
5145
|
+
const row = document.createElement("div");
|
|
5146
|
+
row.className = "fb-multi-footer";
|
|
5147
|
+
if (showCounter) {
|
|
5148
|
+
const counter = document.createElement("span");
|
|
5149
|
+
counter.className = "fb-files-counter";
|
|
5150
|
+
counter.textContent = t("filesCounter", state, {
|
|
5151
|
+
count: ridCount,
|
|
5152
|
+
max: maxCount
|
|
5153
|
+
});
|
|
5154
|
+
row.appendChild(counter);
|
|
5155
|
+
}
|
|
5156
|
+
if (showClearAll) row.appendChild(buildClearAllButton(state, onClearAll));
|
|
5049
5157
|
return row;
|
|
5050
5158
|
}
|
|
5159
|
+
function syncOverLimitError(container, ridCount, maxCount, state) {
|
|
5160
|
+
if (ridCount > maxCount) {
|
|
5161
|
+
showFileError(
|
|
5162
|
+
container,
|
|
5163
|
+
t("maxFiles", state, { max: maxCount }),
|
|
5164
|
+
state,
|
|
5165
|
+
"limit"
|
|
5166
|
+
);
|
|
5167
|
+
} else {
|
|
5168
|
+
clearFileError(container, "limit");
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5051
5171
|
var gridResizeObservers = /* @__PURE__ */ new WeakMap();
|
|
5052
5172
|
var gridMeasureFrames = /* @__PURE__ */ new WeakMap();
|
|
5053
5173
|
function cancelPendingMeasure(container) {
|
|
@@ -5143,6 +5263,7 @@ function renderResourcePills(opts) {
|
|
|
5143
5263
|
grid2.appendChild(tile);
|
|
5144
5264
|
}
|
|
5145
5265
|
}
|
|
5266
|
+
clearFileError(container, "limit");
|
|
5146
5267
|
return;
|
|
5147
5268
|
}
|
|
5148
5269
|
const outerDiv = document.createElement("div");
|
|
@@ -5222,10 +5343,14 @@ function renderResourcePills(opts) {
|
|
|
5222
5343
|
}
|
|
5223
5344
|
}
|
|
5224
5345
|
});
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5346
|
+
const footer = buildFooterRow(
|
|
5347
|
+
state,
|
|
5348
|
+
ridList.length,
|
|
5349
|
+
effectiveMax,
|
|
5350
|
+
onClearAll
|
|
5351
|
+
);
|
|
5352
|
+
if (footer) container.appendChild(footer);
|
|
5353
|
+
syncOverLimitError(container, ridList.length, effectiveMax, state);
|
|
5229
5354
|
}
|
|
5230
5355
|
function renderFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
5231
5356
|
var _a, _b;
|
|
@@ -5378,9 +5503,10 @@ function buildAcceptAttribute(accept) {
|
|
|
5378
5503
|
...(_c = accept.mime) != null ? _c : []
|
|
5379
5504
|
].join(",");
|
|
5380
5505
|
}
|
|
5381
|
-
function
|
|
5382
|
-
var _a, _b;
|
|
5506
|
+
function renderMultiFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
5507
|
+
var _a, _b, _c;
|
|
5383
5508
|
const state = ctx.state;
|
|
5509
|
+
const maxFiles = (_a = element.maxCount) != null ? _a : Infinity;
|
|
5384
5510
|
const filesWrapper = document.createElement("div");
|
|
5385
5511
|
filesWrapper.className = "fb-row";
|
|
5386
5512
|
filesWrapper.dataset.filesWrapper = pathKey;
|
|
@@ -5406,7 +5532,7 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
|
|
|
5406
5532
|
allowedMimes: getAllowedMimes(element.accept),
|
|
5407
5533
|
// Prefer schema's `maxSize`; fall back to legacy `maxSizeMB` for
|
|
5408
5534
|
// backward compatibility (matches addFileSizeHint in validation.ts).
|
|
5409
|
-
maxSize: (
|
|
5535
|
+
maxSize: (_c = (_b = element.maxSize) != null ? _b : element.maxSizeMB) != null ? _c : Infinity
|
|
5410
5536
|
};
|
|
5411
5537
|
const openPicker = () => {
|
|
5412
5538
|
filesPicker.click();
|
|
@@ -5549,7 +5675,7 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
|
|
|
5549
5675
|
state,
|
|
5550
5676
|
!currentlyReadonly,
|
|
5551
5677
|
currentlyReadonly ? null : () => {
|
|
5552
|
-
var _a2, _b2,
|
|
5678
|
+
var _a2, _b2, _c2;
|
|
5553
5679
|
releaseLocalFileUrl((_a2 = state.resourceIndex.get(rid)) == null ? void 0 : _a2.file);
|
|
5554
5680
|
const idx = initialFiles.indexOf(rid);
|
|
5555
5681
|
if (idx > -1) {
|
|
@@ -5567,7 +5693,7 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
|
|
|
5567
5693
|
return;
|
|
5568
5694
|
}
|
|
5569
5695
|
pendingRemovals.add(rid);
|
|
5570
|
-
(
|
|
5696
|
+
(_c2 = list.querySelector(`[data-resource-id="${rid}"]`)) == null ? void 0 : _c2.remove();
|
|
5571
5697
|
if (ctx.instance && pathKey && !state.config.readonly) {
|
|
5572
5698
|
ctx.instance.triggerOnChange(pathKey, initialFiles);
|
|
5573
5699
|
}
|
|
@@ -5588,22 +5714,24 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
|
|
|
5588
5714
|
};
|
|
5589
5715
|
setupFilesDropHandler({ ...sharedHandlerOpts, filesContainer });
|
|
5590
5716
|
setupFilesPickerHandler({ ...sharedHandlerOpts, filesPicker });
|
|
5717
|
+
state.multiFileSetters.set(filesWrapper, (resourceIds) => {
|
|
5718
|
+
var _a2;
|
|
5719
|
+
if (coordinator.hasInFlightBatches()) {
|
|
5720
|
+
throw new Error(
|
|
5721
|
+
`setFormData/updateField: file field "${pathKey}" has uploads in flight; set its value after they settle`
|
|
5722
|
+
);
|
|
5723
|
+
}
|
|
5724
|
+
for (const rid of initialFiles) {
|
|
5725
|
+
if (!resourceIds.includes(rid)) {
|
|
5726
|
+
releaseLocalFileUrl((_a2 = state.resourceIndex.get(rid)) == null ? void 0 : _a2.file);
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
5729
|
+
initialFiles.splice(0, initialFiles.length, ...resourceIds);
|
|
5730
|
+
updateFilesDisplay();
|
|
5731
|
+
});
|
|
5591
5732
|
updateFilesDisplay();
|
|
5592
5733
|
wrapper.appendChild(filesWrapper);
|
|
5593
5734
|
}
|
|
5594
|
-
function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
|
|
5595
|
-
setupMultiFileEditMode(element, ctx, wrapper, pathKey, Infinity);
|
|
5596
|
-
}
|
|
5597
|
-
function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
|
|
5598
|
-
var _a;
|
|
5599
|
-
setupMultiFileEditMode(
|
|
5600
|
-
element,
|
|
5601
|
-
ctx,
|
|
5602
|
-
wrapper,
|
|
5603
|
-
pathKey,
|
|
5604
|
-
(_a = element.maxCount) != null ? _a : Infinity
|
|
5605
|
-
);
|
|
5606
|
-
}
|
|
5607
5735
|
|
|
5608
5736
|
// src/components/file/validate.ts
|
|
5609
5737
|
function readMultiFileResourceIds(scopeRoot, fullKey) {
|
|
@@ -5625,90 +5753,103 @@ function readMultiFileResourceIds(scopeRoot, fullKey) {
|
|
|
5625
5753
|
}
|
|
5626
5754
|
return parsed;
|
|
5627
5755
|
}
|
|
5628
|
-
function
|
|
5629
|
-
var _a, _b;
|
|
5630
|
-
const minFiles = "minCount" in element ? (_a = element.minCount) != null ? _a : 0 : 0;
|
|
5631
|
-
const maxFiles = "maxCount" in element ? (_b = element.maxCount) != null ? _b : Infinity : Infinity;
|
|
5632
|
-
if (element.required && resourceIds.length === 0) {
|
|
5633
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
5634
|
-
}
|
|
5635
|
-
if (resourceIds.length < minFiles) {
|
|
5636
|
-
errors.push(`${key}: ${t("minFiles", state, { min: minFiles })}`);
|
|
5637
|
-
}
|
|
5638
|
-
if (resourceIds.length > maxFiles) {
|
|
5639
|
-
errors.push(`${key}: ${t("maxFiles", state, { max: maxFiles })}`);
|
|
5640
|
-
}
|
|
5641
|
-
}
|
|
5642
|
-
function validateFileTypes(key, resourceIds, element, state, errors) {
|
|
5756
|
+
function validateFileTypes(resourceIds, element, state) {
|
|
5643
5757
|
var _a, _b;
|
|
5758
|
+
const messages = [];
|
|
5644
5759
|
const acceptField = "accept" in element ? element.accept : void 0;
|
|
5645
5760
|
const allowedExtensions = getAllowedExtensions(acceptField);
|
|
5646
5761
|
const allowedMimes = getAllowedMimes(acceptField);
|
|
5647
|
-
if (allowedExtensions.length === 0 && allowedMimes.length === 0)
|
|
5762
|
+
if (allowedExtensions.length === 0 && allowedMimes.length === 0) {
|
|
5763
|
+
return messages;
|
|
5764
|
+
}
|
|
5648
5765
|
const formats = allowedExtensions.join(", ");
|
|
5649
5766
|
const mimes = allowedMimes.join(", ");
|
|
5650
5767
|
for (const rid of resourceIds) {
|
|
5651
5768
|
const meta = state.resourceIndex.get(rid);
|
|
5652
5769
|
const fileName = (_a = meta == null ? void 0 : meta.name) != null ? _a : rid;
|
|
5653
5770
|
if (allowedExtensions.length > 0 && !isFileExtensionAllowed(fileName, allowedExtensions)) {
|
|
5654
|
-
|
|
5655
|
-
|
|
5771
|
+
messages.push(
|
|
5772
|
+
t("invalidFileExtension", state, { name: fileName, formats })
|
|
5656
5773
|
);
|
|
5657
5774
|
continue;
|
|
5658
5775
|
}
|
|
5659
5776
|
if (allowedMimes.length > 0 && !(meta == null ? void 0 : meta.inferredFromExtension)) {
|
|
5660
5777
|
const mimeType = (_b = meta == null ? void 0 : meta.type) != null ? _b : "";
|
|
5661
5778
|
if (!isMimeAllowed(mimeType, allowedMimes)) {
|
|
5662
|
-
|
|
5663
|
-
|
|
5779
|
+
messages.push(
|
|
5780
|
+
t("invalidFileMime", state, {
|
|
5781
|
+
name: fileName,
|
|
5782
|
+
type: mimeType,
|
|
5783
|
+
mimes
|
|
5784
|
+
})
|
|
5664
5785
|
);
|
|
5665
5786
|
}
|
|
5666
5787
|
}
|
|
5667
5788
|
}
|
|
5789
|
+
return messages;
|
|
5668
5790
|
}
|
|
5669
|
-
function validateFileSizes(
|
|
5791
|
+
function validateFileSizes(resourceIds, element, state) {
|
|
5670
5792
|
var _a;
|
|
5793
|
+
const messages = [];
|
|
5671
5794
|
const maxSizeMB = "maxSize" in element ? (_a = element.maxSize) != null ? _a : Infinity : Infinity;
|
|
5672
|
-
if (maxSizeMB === Infinity) return;
|
|
5795
|
+
if (maxSizeMB === Infinity) return messages;
|
|
5673
5796
|
for (const rid of resourceIds) {
|
|
5674
5797
|
const meta = state.resourceIndex.get(rid);
|
|
5675
5798
|
if (!meta) continue;
|
|
5676
5799
|
if (meta.size > maxSizeMB * 1024 * 1024) {
|
|
5677
|
-
|
|
5678
|
-
|
|
5800
|
+
messages.push(
|
|
5801
|
+
t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })
|
|
5679
5802
|
);
|
|
5680
5803
|
}
|
|
5681
5804
|
}
|
|
5805
|
+
return messages;
|
|
5806
|
+
}
|
|
5807
|
+
function reportFileMessages(scopeRoot, wrapperKey, key, messages, context) {
|
|
5808
|
+
const wrapper = scopeRoot.querySelector(
|
|
5809
|
+
`[data-files-wrapper="${wrapperKey}"]`
|
|
5810
|
+
);
|
|
5811
|
+
if (wrapper) {
|
|
5812
|
+
markFileValidity(wrapper, joinErrorMessages(messages), context);
|
|
5813
|
+
}
|
|
5814
|
+
return messages.map((message) => `${key}: ${message}`);
|
|
5682
5815
|
}
|
|
5683
5816
|
function validateMultiFile(element, key, context) {
|
|
5684
|
-
const { scopeRoot,
|
|
5685
|
-
const errors = [];
|
|
5817
|
+
const { scopeRoot, path, state } = context;
|
|
5686
5818
|
const fullKey = pathJoin(path, key);
|
|
5687
5819
|
const resourceIds = readMultiFileResourceIds(scopeRoot, fullKey);
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5820
|
+
const messages = [
|
|
5821
|
+
...countRuleMessages(element, resourceIds.length, state, {
|
|
5822
|
+
min: "minFiles",
|
|
5823
|
+
max: "maxFiles"
|
|
5824
|
+
}),
|
|
5825
|
+
...validateFileTypes(resourceIds, element, state),
|
|
5826
|
+
...validateFileSizes(resourceIds, element, state)
|
|
5827
|
+
];
|
|
5828
|
+
return {
|
|
5829
|
+
value: resourceIds,
|
|
5830
|
+
errors: reportFileMessages(scopeRoot, fullKey, key, messages, context)
|
|
5831
|
+
};
|
|
5694
5832
|
}
|
|
5695
5833
|
function validateSingleFile(element, key, context) {
|
|
5696
5834
|
var _a;
|
|
5697
|
-
const { scopeRoot,
|
|
5698
|
-
const errors = [];
|
|
5835
|
+
const { scopeRoot, state } = context;
|
|
5699
5836
|
const input = scopeRoot.querySelector(
|
|
5700
5837
|
`input[name="${key}"][type="hidden"]`
|
|
5701
5838
|
);
|
|
5702
5839
|
const rid = (_a = input == null ? void 0 : input.value) != null ? _a : "";
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
}
|
|
5707
|
-
|
|
5708
|
-
|
|
5709
|
-
|
|
5840
|
+
let messages = [];
|
|
5841
|
+
if (element.required && rid === "") {
|
|
5842
|
+
messages = [t("required", state)];
|
|
5843
|
+
} else if (rid !== "") {
|
|
5844
|
+
messages = [
|
|
5845
|
+
...validateFileTypes([rid], element, state),
|
|
5846
|
+
...validateFileSizes([rid], element, state)
|
|
5847
|
+
];
|
|
5710
5848
|
}
|
|
5711
|
-
return {
|
|
5849
|
+
return {
|
|
5850
|
+
value: rid || null,
|
|
5851
|
+
errors: reportFileMessages(scopeRoot, key, key, messages, context)
|
|
5852
|
+
};
|
|
5712
5853
|
}
|
|
5713
5854
|
function validateFileElement(element, key, context) {
|
|
5714
5855
|
const isMultipleField = element.type === "files" || "multiple" in element && Boolean(element.multiple);
|
|
@@ -5758,12 +5899,20 @@ function buildEmptyReadonlyTile(state) {
|
|
|
5758
5899
|
return emptyState;
|
|
5759
5900
|
}
|
|
5760
5901
|
function renderMultiFileReadonly(rids, state, wrapper, pathKey, _marginTop) {
|
|
5761
|
-
addPrefillFilesToIndex(rids, state.resourceIndex);
|
|
5762
|
-
ensureFileStyles();
|
|
5763
5902
|
const filesWrapper = document.createElement("div");
|
|
5764
5903
|
filesWrapper.dataset.filesWrapper = pathKey;
|
|
5765
|
-
filesWrapper.dataset.resourceIds = JSON.stringify(rids);
|
|
5766
5904
|
wrapper.appendChild(filesWrapper);
|
|
5905
|
+
state.multiFileSetters.set(
|
|
5906
|
+
filesWrapper,
|
|
5907
|
+
(resourceIds) => fillReadonlyGrid(resourceIds, state, filesWrapper)
|
|
5908
|
+
);
|
|
5909
|
+
fillReadonlyGrid(rids, state, filesWrapper);
|
|
5910
|
+
}
|
|
5911
|
+
function fillReadonlyGrid(rids, state, filesWrapper) {
|
|
5912
|
+
addPrefillFilesToIndex(rids, state.resourceIndex);
|
|
5913
|
+
ensureFileStyles();
|
|
5914
|
+
filesWrapper.dataset.resourceIds = JSON.stringify(rids);
|
|
5915
|
+
filesWrapper.replaceChildren();
|
|
5767
5916
|
if (rids.length === 0) {
|
|
5768
5917
|
const emptyEl = document.createElement("div");
|
|
5769
5918
|
emptyEl.className = "fb-tile-empty-text";
|
|
@@ -5823,14 +5972,14 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
|
5823
5972
|
if (isElementReadonly(element, ctx.state, ctx)) {
|
|
5824
5973
|
renderFilesElementReadonly(element, ctx, wrapper, pathKey);
|
|
5825
5974
|
} else {
|
|
5826
|
-
|
|
5975
|
+
renderMultiFileElementEdit(element, ctx, wrapper, pathKey);
|
|
5827
5976
|
}
|
|
5828
5977
|
}
|
|
5829
5978
|
function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
|
|
5830
5979
|
if (isElementReadonly(element, ctx.state, ctx)) {
|
|
5831
5980
|
renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey);
|
|
5832
5981
|
} else {
|
|
5833
|
-
|
|
5982
|
+
renderMultiFileElementEdit(element, ctx, wrapper, pathKey);
|
|
5834
5983
|
}
|
|
5835
5984
|
}
|
|
5836
5985
|
function updateFileField(element, fieldPath, value, context) {
|
|
@@ -5850,13 +5999,19 @@ function updateFileField(element, fieldPath, value, context) {
|
|
|
5850
5999
|
const filesWrapper = scopeRoot.querySelector(
|
|
5851
6000
|
`[data-files-wrapper="${fieldPath}"]`
|
|
5852
6001
|
);
|
|
5853
|
-
if (filesWrapper) {
|
|
5854
|
-
filesWrapper.dataset.resourceIds = JSON.stringify(value);
|
|
5855
|
-
} else {
|
|
6002
|
+
if (!filesWrapper) {
|
|
5856
6003
|
console.warn(
|
|
5857
6004
|
`updateFileField: [data-files-wrapper="${fieldPath}"] not found in DOM; data-resource-ids not updated`
|
|
5858
6005
|
);
|
|
6006
|
+
return;
|
|
6007
|
+
}
|
|
6008
|
+
const setFiles = state.multiFileSetters.get(filesWrapper);
|
|
6009
|
+
if (!setFiles) {
|
|
6010
|
+
throw new Error(
|
|
6011
|
+
`updateFileField: [data-files-wrapper="${fieldPath}"] has no registered setter; this is a render bug`
|
|
6012
|
+
);
|
|
5859
6013
|
}
|
|
6014
|
+
setFiles(value);
|
|
5860
6015
|
} else {
|
|
5861
6016
|
const hiddenInput = scopeRoot.querySelector(
|
|
5862
6017
|
`input[name="${fieldPath}"][type="hidden"]`
|
|
@@ -6191,30 +6346,20 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
|
|
|
6191
6346
|
}
|
|
6192
6347
|
}
|
|
6193
6348
|
function validateColourElement(element, key, context) {
|
|
6194
|
-
var _a
|
|
6349
|
+
var _a;
|
|
6195
6350
|
const errors = [];
|
|
6196
|
-
const { scopeRoot,
|
|
6351
|
+
const { scopeRoot, state } = context;
|
|
6197
6352
|
const validateColourValue = (input, val, fieldKey) => {
|
|
6198
|
-
const
|
|
6353
|
+
const normalized = val ? normalizeColourValue(val) : "";
|
|
6354
|
+
let msg = null;
|
|
6199
6355
|
if (!val) {
|
|
6200
|
-
if (
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
return "";
|
|
6208
|
-
}
|
|
6209
|
-
const normalized = normalizeColourValue(val);
|
|
6210
|
-
if (!skipValidation && !isValidHexColour(normalized)) {
|
|
6211
|
-
const msg = t("invalidHexColour", state);
|
|
6212
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
6213
|
-
markFieldValidity(input, msg);
|
|
6214
|
-
return val;
|
|
6215
|
-
}
|
|
6216
|
-
markFieldValidity(input, null);
|
|
6217
|
-
return normalized;
|
|
6356
|
+
if (element.required) msg = t("required", state);
|
|
6357
|
+
} else if (!isValidHexColour(normalized)) {
|
|
6358
|
+
msg = t("invalidHexColour", state);
|
|
6359
|
+
}
|
|
6360
|
+
if (msg !== null) errors.push(`${fieldKey}: ${msg}`);
|
|
6361
|
+
markFieldValidity(input, msg, context);
|
|
6362
|
+
return val && msg !== null ? val : normalized;
|
|
6218
6363
|
};
|
|
6219
6364
|
if (element.multiple) {
|
|
6220
6365
|
const hexInputs = scopeRoot.querySelectorAll(
|
|
@@ -6224,38 +6369,17 @@ function validateColourElement(element, key, context) {
|
|
|
6224
6369
|
hexInputs.forEach((input, index) => {
|
|
6225
6370
|
var _a2;
|
|
6226
6371
|
const val = (_a2 = input == null ? void 0 : input.value) != null ? _a2 : "";
|
|
6227
|
-
|
|
6228
|
-
values.push(validated);
|
|
6372
|
+
values.push(validateColourValue(input, val, `${key}[${index}]`));
|
|
6229
6373
|
});
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
const minCount = (_a = element.minCount) != null ? _a : 0;
|
|
6233
|
-
const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
|
|
6234
|
-
const filteredValues = values.filter((v) => v !== "");
|
|
6235
|
-
if (element.required && filteredValues.length === 0) {
|
|
6236
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
6237
|
-
}
|
|
6238
|
-
if (filteredValues.length < minCount) {
|
|
6239
|
-
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
6240
|
-
}
|
|
6241
|
-
if (filteredValues.length > maxCount) {
|
|
6242
|
-
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
6243
|
-
}
|
|
6244
|
-
}
|
|
6374
|
+
const filledCount = values.filter((v) => v !== "").length;
|
|
6375
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
6245
6376
|
return { value: values, errors };
|
|
6246
6377
|
} else {
|
|
6247
6378
|
const hexInput = scopeRoot.querySelector(
|
|
6248
6379
|
`[name="${key}"].colour-hex-input`
|
|
6249
6380
|
);
|
|
6250
|
-
const val = (
|
|
6251
|
-
|
|
6252
|
-
const msg = t("required", context.state);
|
|
6253
|
-
errors.push(`${key}: ${msg}`);
|
|
6254
|
-
markFieldValidity(hexInput, msg);
|
|
6255
|
-
return { value: "", errors };
|
|
6256
|
-
}
|
|
6257
|
-
const validated = validateColourValue(hexInput, val, key);
|
|
6258
|
-
return { value: validated, errors };
|
|
6381
|
+
const val = (_a = hexInput == null ? void 0 : hexInput.value) != null ? _a : "";
|
|
6382
|
+
return { value: validateColourValue(hexInput, val, key), errors };
|
|
6259
6383
|
}
|
|
6260
6384
|
}
|
|
6261
6385
|
function updateColourField(element, fieldPath, value, context) {
|
|
@@ -6274,8 +6398,6 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
6274
6398
|
if (index < value.length) {
|
|
6275
6399
|
const normalized = normalizeColourValue(value[index]);
|
|
6276
6400
|
hexInput.value = normalized;
|
|
6277
|
-
hexInput.classList.remove("invalid");
|
|
6278
|
-
hexInput.title = "";
|
|
6279
6401
|
clearFieldError(hexInput);
|
|
6280
6402
|
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
6281
6403
|
if (wrapper) {
|
|
@@ -6304,8 +6426,6 @@ function updateColourField(element, fieldPath, value, context) {
|
|
|
6304
6426
|
if (hexInput) {
|
|
6305
6427
|
const normalized = normalizeColourValue(value);
|
|
6306
6428
|
hexInput.value = normalized;
|
|
6307
|
-
hexInput.classList.remove("invalid");
|
|
6308
|
-
hexInput.title = "";
|
|
6309
6429
|
clearFieldError(hexInput);
|
|
6310
6430
|
const wrapper = hexInput.closest(".colour-picker-wrapper");
|
|
6311
6431
|
if (wrapper) {
|
|
@@ -6623,9 +6743,9 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
|
|
|
6623
6743
|
}
|
|
6624
6744
|
}
|
|
6625
6745
|
function validateSliderElement(element, key, context) {
|
|
6626
|
-
var _a
|
|
6746
|
+
var _a;
|
|
6627
6747
|
const errors = [];
|
|
6628
|
-
const { scopeRoot
|
|
6748
|
+
const { scopeRoot } = context;
|
|
6629
6749
|
if (element.min === void 0 || element.min === null) {
|
|
6630
6750
|
throw new Error(
|
|
6631
6751
|
`Slider validation: field "${key}" requires "min" property`
|
|
@@ -6644,39 +6764,20 @@ function validateSliderElement(element, key, context) {
|
|
|
6644
6764
|
const { state } = context;
|
|
6645
6765
|
const rawValue = slider.value;
|
|
6646
6766
|
if (!rawValue) {
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
markFieldValidity(slider, msg);
|
|
6651
|
-
return null;
|
|
6652
|
-
}
|
|
6653
|
-
markFieldValidity(slider, null);
|
|
6767
|
+
const msg2 = element.required ? t("required", state) : null;
|
|
6768
|
+
if (msg2 !== null) errors.push(`${fieldKey}: ${msg2}`);
|
|
6769
|
+
markFieldValidity(slider, msg2, context);
|
|
6654
6770
|
return null;
|
|
6655
6771
|
}
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
if (!skipValidation) {
|
|
6666
|
-
if (value < min) {
|
|
6667
|
-
const msg = t("minValue", state, { min });
|
|
6668
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
6669
|
-
markFieldValidity(slider, msg);
|
|
6670
|
-
return value;
|
|
6671
|
-
}
|
|
6672
|
-
if (value > max) {
|
|
6673
|
-
const msg = t("maxValue", state, { max });
|
|
6674
|
-
errors.push(`${fieldKey}: ${msg}`);
|
|
6675
|
-
markFieldValidity(slider, msg);
|
|
6676
|
-
return value;
|
|
6677
|
-
}
|
|
6678
|
-
}
|
|
6679
|
-
markFieldValidity(slider, null);
|
|
6772
|
+
const value = scale === "exponential" ? alignToStep(
|
|
6773
|
+
positionToExponential(parseFloat(rawValue) / 1e3, min, max),
|
|
6774
|
+
step
|
|
6775
|
+
) : alignToStep(parseFloat(rawValue), step);
|
|
6776
|
+
let msg = null;
|
|
6777
|
+
if (value < min) msg = t("minValue", state, { min });
|
|
6778
|
+
else if (value > max) msg = t("maxValue", state, { max });
|
|
6779
|
+
if (msg !== null) errors.push(`${fieldKey}: ${msg}`);
|
|
6780
|
+
markFieldValidity(slider, msg, context);
|
|
6680
6781
|
return value;
|
|
6681
6782
|
};
|
|
6682
6783
|
if (element.multiple) {
|
|
@@ -6685,31 +6786,17 @@ function validateSliderElement(element, key, context) {
|
|
|
6685
6786
|
);
|
|
6686
6787
|
const values = [];
|
|
6687
6788
|
sliders.forEach((slider, index) => {
|
|
6688
|
-
|
|
6689
|
-
values.push(value);
|
|
6789
|
+
values.push(validateSliderValue(slider, `${key}[${index}]`));
|
|
6690
6790
|
});
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
const minCount = (_b = element.minCount) != null ? _b : 0;
|
|
6694
|
-
const maxCount = (_c = element.maxCount) != null ? _c : Infinity;
|
|
6695
|
-
const filteredValues = values.filter((v) => v !== null);
|
|
6696
|
-
if (element.required && filteredValues.length === 0) {
|
|
6697
|
-
errors.push(`${key}: ${t("required", state)}`);
|
|
6698
|
-
}
|
|
6699
|
-
if (filteredValues.length < minCount) {
|
|
6700
|
-
errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
|
|
6701
|
-
}
|
|
6702
|
-
if (filteredValues.length > maxCount) {
|
|
6703
|
-
errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
|
|
6704
|
-
}
|
|
6705
|
-
}
|
|
6791
|
+
const filledCount = values.filter((v) => v !== null).length;
|
|
6792
|
+
validateItemCount(element, key, filledCount, context, errors);
|
|
6706
6793
|
return { value: values, errors };
|
|
6707
6794
|
} else {
|
|
6708
6795
|
const slider = scopeRoot.querySelector(
|
|
6709
6796
|
`input[type="range"][name="${key}"]`
|
|
6710
6797
|
);
|
|
6711
6798
|
if (!slider) {
|
|
6712
|
-
if (
|
|
6799
|
+
if (element.required) {
|
|
6713
6800
|
errors.push(`${key}: ${t("required", context.state)}`);
|
|
6714
6801
|
}
|
|
6715
6802
|
return { value: null, errors };
|
|
@@ -6759,8 +6846,6 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
6759
6846
|
var(--fb-border-color) 100%
|
|
6760
6847
|
)`;
|
|
6761
6848
|
}
|
|
6762
|
-
slider.classList.remove("invalid");
|
|
6763
|
-
slider.title = "";
|
|
6764
6849
|
clearFieldError(slider);
|
|
6765
6850
|
}
|
|
6766
6851
|
});
|
|
@@ -6796,8 +6881,6 @@ function updateSliderField(element, fieldPath, value, context) {
|
|
|
6796
6881
|
var(--fb-border-color) 100%
|
|
6797
6882
|
)`;
|
|
6798
6883
|
}
|
|
6799
|
-
slider.classList.remove("invalid");
|
|
6800
|
-
slider.title = "";
|
|
6801
6884
|
clearFieldError(slider);
|
|
6802
6885
|
}
|
|
6803
6886
|
}
|
|
@@ -7124,26 +7207,10 @@ function requireValidateElement(context) {
|
|
|
7124
7207
|
function validateContainerElement(element, key, context) {
|
|
7125
7208
|
const validateChild = requireValidateElement(context);
|
|
7126
7209
|
const errors = [];
|
|
7127
|
-
const { scopeRoot,
|
|
7210
|
+
const { scopeRoot, path } = context;
|
|
7128
7211
|
if (!("elements" in element)) {
|
|
7129
7212
|
return { value: null, errors };
|
|
7130
7213
|
}
|
|
7131
|
-
const validateContainerCount = (key2, items, element2) => {
|
|
7132
|
-
var _a, _b;
|
|
7133
|
-
if (skipValidation) return;
|
|
7134
|
-
const { state } = context;
|
|
7135
|
-
const minItems = "minCount" in element2 ? (_a = element2.minCount) != null ? _a : 0 : 0;
|
|
7136
|
-
const maxItems = "maxCount" in element2 ? (_b = element2.maxCount) != null ? _b : Infinity : Infinity;
|
|
7137
|
-
if (element2.required && items.length === 0) {
|
|
7138
|
-
errors.push(`${key2}: ${t("required", state)}`);
|
|
7139
|
-
}
|
|
7140
|
-
if (items.length < minItems) {
|
|
7141
|
-
errors.push(`${key2}: ${t("minItems", state, { min: minItems })}`);
|
|
7142
|
-
}
|
|
7143
|
-
if (items.length > maxItems) {
|
|
7144
|
-
errors.push(`${key2}: ${t("maxItems", state, { max: maxItems })}`);
|
|
7145
|
-
}
|
|
7146
|
-
};
|
|
7147
7214
|
if ("multiple" in element && element.multiple) {
|
|
7148
7215
|
const items = [];
|
|
7149
7216
|
const containerWrappers = findDirectContainerRows(scopeRoot, key);
|
|
@@ -7177,7 +7244,7 @@ function validateContainerElement(element, key, context) {
|
|
|
7177
7244
|
const childKey = `${key}[${domIndex}].${child.key}`;
|
|
7178
7245
|
const childResult = validateChild(
|
|
7179
7246
|
{ ...child, key: childKey },
|
|
7180
|
-
{ path },
|
|
7247
|
+
{ path, inheritedReadonly: context.readonly },
|
|
7181
7248
|
itemContainer
|
|
7182
7249
|
);
|
|
7183
7250
|
if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
|
|
@@ -7188,7 +7255,7 @@ function validateContainerElement(element, key, context) {
|
|
|
7188
7255
|
});
|
|
7189
7256
|
items.push(itemData);
|
|
7190
7257
|
});
|
|
7191
|
-
|
|
7258
|
+
validateItemCount(element, key, items.length, context, errors);
|
|
7192
7259
|
return { value: items, errors };
|
|
7193
7260
|
} else {
|
|
7194
7261
|
const containerData = {};
|
|
@@ -7219,7 +7286,7 @@ function validateContainerElement(element, key, context) {
|
|
|
7219
7286
|
const childKey = `${key}.${child.key}`;
|
|
7220
7287
|
const childResult = validateChild(
|
|
7221
7288
|
{ ...child, key: childKey },
|
|
7222
|
-
{ path },
|
|
7289
|
+
{ path, inheritedReadonly: context.readonly },
|
|
7223
7290
|
containerContainer
|
|
7224
7291
|
);
|
|
7225
7292
|
if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
|
|
@@ -8616,9 +8683,25 @@ function renderTableElement(element, ctx, wrapper, pathKey) {
|
|
|
8616
8683
|
renderEditTable(element, initialData, pathKey, ctx, wrapper);
|
|
8617
8684
|
}
|
|
8618
8685
|
}
|
|
8686
|
+
function parseTableValue(raw, cellsKey) {
|
|
8687
|
+
let parsed;
|
|
8688
|
+
try {
|
|
8689
|
+
parsed = JSON.parse(raw);
|
|
8690
|
+
} catch {
|
|
8691
|
+
return null;
|
|
8692
|
+
}
|
|
8693
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8694
|
+
return null;
|
|
8695
|
+
}
|
|
8696
|
+
const cells = parsed[cellsKey];
|
|
8697
|
+
const isGrid = Array.isArray(cells) && cells.every(
|
|
8698
|
+
(row) => Array.isArray(row) && row.every((cell) => typeof cell === "string")
|
|
8699
|
+
);
|
|
8700
|
+
return isGrid ? parsed : null;
|
|
8701
|
+
}
|
|
8619
8702
|
function validateTableElement(element, key, context) {
|
|
8620
8703
|
var _a, _b;
|
|
8621
|
-
const { scopeRoot
|
|
8704
|
+
const { scopeRoot } = context;
|
|
8622
8705
|
const errors = [];
|
|
8623
8706
|
const cellsKey = (_b = (_a = element.fieldNames) == null ? void 0 : _a.cells) != null ? _b : "cells";
|
|
8624
8707
|
const hiddenInput = scopeRoot.querySelector(
|
|
@@ -8627,22 +8710,20 @@ function validateTableElement(element, key, context) {
|
|
|
8627
8710
|
if (!hiddenInput) {
|
|
8628
8711
|
return { value: null, errors };
|
|
8629
8712
|
}
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8713
|
+
const value = parseTableValue(hiddenInput.value, cellsKey);
|
|
8714
|
+
if (value === null) {
|
|
8715
|
+
const msg2 = "invalid table data";
|
|
8716
|
+
errors.push(`${key}: ${msg2}`);
|
|
8717
|
+
markFieldGroupValidity(scopeRoot, key, msg2, context);
|
|
8635
8718
|
return { value: null, errors };
|
|
8636
8719
|
}
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
}
|
|
8645
|
-
}
|
|
8720
|
+
const cells = value[cellsKey];
|
|
8721
|
+
const hasContent = cells.some(
|
|
8722
|
+
(row) => row.some((cell) => cell.trim() !== "")
|
|
8723
|
+
);
|
|
8724
|
+
const msg = element.required && !hasContent ? t("required", context.state) : null;
|
|
8725
|
+
if (msg !== null) errors.push(`${key}: ${msg}`);
|
|
8726
|
+
markFieldGroupValidity(scopeRoot, key, msg, context);
|
|
8646
8727
|
return { value, errors };
|
|
8647
8728
|
}
|
|
8648
8729
|
function updateTableField(element, fieldPath, value, context) {
|
|
@@ -9984,9 +10065,17 @@ function renderRichInputElement(element, ctx, wrapper, pathKey) {
|
|
|
9984
10065
|
renderEditMode(element, ctx, wrapper, pathKey, initialValue);
|
|
9985
10066
|
}
|
|
9986
10067
|
}
|
|
10068
|
+
function parseRichInputValue(raw) {
|
|
10069
|
+
try {
|
|
10070
|
+
const parsed = JSON.parse(raw);
|
|
10071
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
10072
|
+
} catch {
|
|
10073
|
+
return null;
|
|
10074
|
+
}
|
|
10075
|
+
}
|
|
9987
10076
|
function validateRichInputElement(element, key, context) {
|
|
9988
10077
|
var _a, _b;
|
|
9989
|
-
const { scopeRoot, state
|
|
10078
|
+
const { scopeRoot, state } = context;
|
|
9990
10079
|
const errors = [];
|
|
9991
10080
|
const textKey = (_a = element.textKey) != null ? _a : "text";
|
|
9992
10081
|
const filesKey = (_b = element.filesKey) != null ? _b : "files";
|
|
@@ -9996,17 +10085,11 @@ function validateRichInputElement(element, key, context) {
|
|
|
9996
10085
|
if (!hiddenInput) {
|
|
9997
10086
|
return { value: null, errors };
|
|
9998
10087
|
}
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
const
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
} else {
|
|
10005
|
-
errors.push(`${key}: invalid richinput data`);
|
|
10006
|
-
return { value: null, errors };
|
|
10007
|
-
}
|
|
10008
|
-
} catch {
|
|
10009
|
-
errors.push(`${key}: invalid richinput data`);
|
|
10088
|
+
const rawValue = parseRichInputValue(hiddenInput.value);
|
|
10089
|
+
if (rawValue === null) {
|
|
10090
|
+
const msg = "invalid richinput data";
|
|
10091
|
+
errors.push(`${key}: ${msg}`);
|
|
10092
|
+
markFieldGroupValidity(scopeRoot, key, msg, context);
|
|
10010
10093
|
return { value: null, errors };
|
|
10011
10094
|
}
|
|
10012
10095
|
const textVal = rawValue[textKey];
|
|
@@ -10017,28 +10100,24 @@ function validateRichInputElement(element, key, context) {
|
|
|
10017
10100
|
[textKey]: text != null ? text : null,
|
|
10018
10101
|
[filesKey]: files
|
|
10019
10102
|
};
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
if (
|
|
10027
|
-
|
|
10028
|
-
errors.push(
|
|
10029
|
-
`${key}: ${t("minLength", state, { min: element.minLength })}`
|
|
10030
|
-
);
|
|
10031
|
-
}
|
|
10032
|
-
if (element.maxLength != null && text.length > element.maxLength) {
|
|
10033
|
-
errors.push(
|
|
10034
|
-
`${key}: ${t("maxLength", state, { max: element.maxLength })}`
|
|
10035
|
-
);
|
|
10036
|
-
}
|
|
10103
|
+
const textEmpty = !text || text.trim() === "";
|
|
10104
|
+
const messages = [];
|
|
10105
|
+
if (element.required && textEmpty && files.length === 0) {
|
|
10106
|
+
messages.push(t("required", state));
|
|
10107
|
+
}
|
|
10108
|
+
if (!textEmpty && text) {
|
|
10109
|
+
if (element.minLength != null && text.length < element.minLength) {
|
|
10110
|
+
messages.push(t("minLength", state, { min: element.minLength }));
|
|
10037
10111
|
}
|
|
10038
|
-
if (element.
|
|
10039
|
-
|
|
10112
|
+
if (element.maxLength != null && text.length > element.maxLength) {
|
|
10113
|
+
messages.push(t("maxLength", state, { max: element.maxLength }));
|
|
10040
10114
|
}
|
|
10041
10115
|
}
|
|
10116
|
+
if (element.maxFiles != null && files.length > element.maxFiles) {
|
|
10117
|
+
messages.push(t("maxFiles", state, { max: element.maxFiles }));
|
|
10118
|
+
}
|
|
10119
|
+
errors.push(...messages.map((message) => `${key}: ${message}`));
|
|
10120
|
+
markFieldGroupValidity(scopeRoot, key, joinErrorMessages(messages), context);
|
|
10042
10121
|
return { value, errors, spread: !!element.flatOutput };
|
|
10043
10122
|
}
|
|
10044
10123
|
function updateRichInputField(element, fieldPath, value, context) {
|
|
@@ -10809,12 +10888,13 @@ function renderElement2(element, ctx) {
|
|
|
10809
10888
|
wrapper.className = `fb-field-wrapper fb-size-${element.size || "md"}`;
|
|
10810
10889
|
wrapper.setAttribute("data-field-key", element.key);
|
|
10811
10890
|
wrapper.setAttribute("data-fb-width", element.width || "full");
|
|
10891
|
+
const pathKey = pathJoin(ctx.path, element.key);
|
|
10892
|
+
wrapper.setAttribute("data-field-path", pathKey);
|
|
10812
10893
|
const ops = getComponentOperations(element.type);
|
|
10813
10894
|
if (!(ops == null ? void 0 : ops.ownsLabel)) {
|
|
10814
10895
|
const label = createLabelContainer(element, ctx.state);
|
|
10815
10896
|
wrapper.appendChild(label);
|
|
10816
10897
|
}
|
|
10817
|
-
const pathKey = pathJoin(ctx.path, element.key);
|
|
10818
10898
|
dispatchToRenderer(element, ctx, wrapper, pathKey);
|
|
10819
10899
|
if (initiallyDisabled) {
|
|
10820
10900
|
wrapper.style.display = "none";
|
|
@@ -11021,6 +11101,9 @@ function createInstanceState(config) {
|
|
|
11021
11101
|
config == null ? void 0 : config.translations
|
|
11022
11102
|
);
|
|
11023
11103
|
return {
|
|
11104
|
+
instanceId: generateInstanceId(),
|
|
11105
|
+
domIdCounter: 0,
|
|
11106
|
+
reportedInvalid: /* @__PURE__ */ new WeakSet(),
|
|
11024
11107
|
schema: null,
|
|
11025
11108
|
formRoot: null,
|
|
11026
11109
|
resourceIndex: /* @__PURE__ */ new Map(),
|
|
@@ -11035,6 +11118,7 @@ function createInstanceState(config) {
|
|
|
11035
11118
|
prefill: {},
|
|
11036
11119
|
syntheticElementIds: /* @__PURE__ */ new WeakMap(),
|
|
11037
11120
|
syntheticElementIdCounter: 0,
|
|
11121
|
+
multiFileSetters: /* @__PURE__ */ new WeakMap(),
|
|
11038
11122
|
enableIfObservers: /* @__PURE__ */ new Set(),
|
|
11039
11123
|
autoExpandObservers: /* @__PURE__ */ new Set(),
|
|
11040
11124
|
tooltipElements: /* @__PURE__ */ new Set()
|
|
@@ -11343,8 +11427,8 @@ var FormBuilderInstance = class {
|
|
|
11343
11427
|
// stacked listeners (hint clicks applied values N times) and destroy()
|
|
11344
11428
|
// left the last one on the host-owned root, retaining the instance.
|
|
11345
11429
|
this.prefillHintHandler = null;
|
|
11346
|
-
this.instanceId = generateInstanceId();
|
|
11347
11430
|
this.state = createInstanceState(config);
|
|
11431
|
+
this.instanceId = this.state.instanceId;
|
|
11348
11432
|
if (this.state.config.verboseErrors) {
|
|
11349
11433
|
if (!globalThis.__formBuilderInstances) {
|
|
11350
11434
|
globalThis.__formBuilderInstances = /* @__PURE__ */ new Set();
|
|
@@ -11842,11 +11926,21 @@ var FormBuilderInstance = class {
|
|
|
11842
11926
|
}
|
|
11843
11927
|
}
|
|
11844
11928
|
/**
|
|
11845
|
-
* Validate form and extract data
|
|
11846
|
-
*
|
|
11847
|
-
*
|
|
11929
|
+
* Validate the form and extract its data. `skipValidation` is the draft
|
|
11930
|
+
* contract of saveDraft() and the onChange payload: marks are only
|
|
11931
|
+
* refreshed or cleared, and the result reports `valid: true, errors: []`.
|
|
11848
11932
|
*/
|
|
11849
11933
|
validateForm(skipValidation = false) {
|
|
11934
|
+
if (!skipValidation) return this.runValidation("full");
|
|
11935
|
+
return { ...this.runValidation("draft"), valid: true, errors: [] };
|
|
11936
|
+
}
|
|
11937
|
+
/**
|
|
11938
|
+
* Run every rule and return the real result. `marks` decides only what is
|
|
11939
|
+
* painted: "full" raises and clears marks and records reported fields;
|
|
11940
|
+
* "draft" refreshes or clears marks of reported fields and raises none
|
|
11941
|
+
* (see ValidityScope in utils/styles.ts).
|
|
11942
|
+
*/
|
|
11943
|
+
runValidation(marks) {
|
|
11850
11944
|
if (!this.state.schema || !this.state.formRoot)
|
|
11851
11945
|
return { valid: true, errors: [], data: {} };
|
|
11852
11946
|
const errors = [];
|
|
@@ -11860,7 +11954,8 @@ var FormBuilderInstance = class {
|
|
|
11860
11954
|
state: this.state,
|
|
11861
11955
|
instance: this,
|
|
11862
11956
|
path: ctx.path,
|
|
11863
|
-
|
|
11957
|
+
draftMarks: marks === "draft",
|
|
11958
|
+
readonly: isElementReadonly(element, this.state, ctx),
|
|
11864
11959
|
// Containers recurse into their children through this — threaded per
|
|
11865
11960
|
// pass, never module state (see ComponentContext.validateElement).
|
|
11866
11961
|
validateElement
|
|
@@ -11913,10 +12008,56 @@ var FormBuilderInstance = class {
|
|
|
11913
12008
|
};
|
|
11914
12009
|
}
|
|
11915
12010
|
/**
|
|
11916
|
-
*
|
|
12011
|
+
* Read the form: every rule runs and the result is the real
|
|
12012
|
+
* `{valid, errors, data}`. Safe to poll — it never paints a new error
|
|
12013
|
+
* mark (so a pristine form never turns red), it only refreshes or clears
|
|
12014
|
+
* marks that showErrors()/submitForm() drew, and a repeated call on
|
|
12015
|
+
* unchanged state touches no DOM at all.
|
|
11917
12016
|
*/
|
|
11918
12017
|
getFormData() {
|
|
11919
|
-
return this.
|
|
12018
|
+
return this.runValidation("draft");
|
|
12019
|
+
}
|
|
12020
|
+
/**
|
|
12021
|
+
* Paint every validation error next to its field (and clear marks of
|
|
12022
|
+
* fields that are now valid), then return the same result as
|
|
12023
|
+
* getFormData(). Call it when the user asks to submit, followed by
|
|
12024
|
+
* focusFirstError() to take them to the first problem.
|
|
12025
|
+
*/
|
|
12026
|
+
showErrors() {
|
|
12027
|
+
return this.runValidation("full");
|
|
12028
|
+
}
|
|
12029
|
+
/**
|
|
12030
|
+
* Focus the first field marked invalid, in DOM order, and scroll it into
|
|
12031
|
+
* view. A marked group (container, multi-value field, file field) gets
|
|
12032
|
+
* focus on its first focusable control, else on the group itself.
|
|
12033
|
+
* Does not validate: marks are drawn by showErrors() (or submitForm()),
|
|
12034
|
+
* so call showErrors() first. Fields hidden by enableIf are skipped, as is
|
|
12035
|
+
* any field that cannot take focus (e.g. inside a hidden slide).
|
|
12036
|
+
* @returns true only when focus actually landed on an invalid field
|
|
12037
|
+
*/
|
|
12038
|
+
focusFirstError() {
|
|
12039
|
+
const root = this.state.formRoot;
|
|
12040
|
+
if (!root) return false;
|
|
12041
|
+
const marked = root.querySelectorAll('[aria-invalid="true"]');
|
|
12042
|
+
for (const target of Array.from(marked)) {
|
|
12043
|
+
if (target.closest('[data-conditionally-disabled="true"]')) continue;
|
|
12044
|
+
const candidates = target.matches("input, select, textarea, button") ? [target] : [
|
|
12045
|
+
...Array.from(
|
|
12046
|
+
target.querySelectorAll(
|
|
12047
|
+
"input, select, textarea, button, [tabindex]"
|
|
12048
|
+
)
|
|
12049
|
+
),
|
|
12050
|
+
target
|
|
12051
|
+
];
|
|
12052
|
+
for (const candidate of candidates) {
|
|
12053
|
+
candidate.focus({ preventScroll: true });
|
|
12054
|
+
if (document.activeElement === candidate) {
|
|
12055
|
+
candidate.scrollIntoView({ block: "center" });
|
|
12056
|
+
return true;
|
|
12057
|
+
}
|
|
12058
|
+
}
|
|
12059
|
+
}
|
|
12060
|
+
return false;
|
|
11920
12061
|
}
|
|
11921
12062
|
/**
|
|
11922
12063
|
* Submit form with validation
|
|
@@ -12159,6 +12300,7 @@ var FormBuilderInstance = class {
|
|
|
12159
12300
|
getElementLookupKey(element, this.state)
|
|
12160
12301
|
);
|
|
12161
12302
|
disabledWrapper.setAttribute("data-conditionally-disabled", "true");
|
|
12303
|
+
disabledWrapper.setAttribute("data-field-path", fullDomPath);
|
|
12162
12304
|
(_c = wrapper.parentNode) == null ? void 0 : _c.replaceChild(disabledWrapper, wrapper);
|
|
12163
12305
|
}
|
|
12164
12306
|
} catch (error) {
|