@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.
@@ -100,467 +100,201 @@ function createHiddenInput(name, value) {
100
100
  return input;
101
101
  }
102
102
 
103
- // src/utils/validation.ts
104
- function addLengthHint(element, parts, state) {
105
- if (element.minLength != null || element.maxLength != null) {
106
- if (element.minLength != null && element.maxLength != null) {
107
- parts.push(
108
- t("hintLengthRange", state, {
109
- min: element.minLength,
110
- max: element.maxLength
111
- })
112
- );
113
- } else if (element.maxLength != null) {
114
- parts.push(t("hintMaxLength", state, { max: element.maxLength }));
115
- } else if (element.minLength != null) {
116
- parts.push(t("hintMinLength", state, { min: element.minLength }));
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 addRangeHint(element, parts, state) {
121
- if (element.min != null || element.max != null) {
122
- if (element.min != null && element.max != null) {
123
- parts.push(
124
- t("hintValueRange", state, { min: element.min, max: element.max })
125
- );
126
- } else if (element.max != null) {
127
- parts.push(t("hintMaxValue", state, { max: element.max }));
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 addFileSizeHint(element, parts, state) {
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
- const sizeMB = (_a = element.maxSize) != null ? _a : element.maxSizeMB;
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
- function addFormatHint(element, parts, state) {
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
- if ((_a = element.accept) == null ? void 0 : _a.extensions) {
143
- parts.push(
144
- t("hintFormats", state, {
145
- formats: element.accept.extensions.map((ext) => ext.toUpperCase()).join(",")
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 addPatternHint(element, parts, state) {
151
- if (element.pattern) {
152
- parts.push(t("hintPattern", state, { pattern: element.pattern }));
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 validateSchema(schema) {
167
- const errors = [];
168
- if (!schema || typeof schema !== "object") {
169
- errors.push("Schema must be an object");
170
- return errors;
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
- if ("prefillHints" in schema && schema.prefillHints) {
184
- const prefillHints = schema.prefillHints;
185
- if (Array.isArray(prefillHints)) {
186
- prefillHints.forEach((hint, hintIndex) => {
187
- if (!hint.label || typeof hint.label !== "string") {
188
- errors.push(
189
- `schema.prefillHints[${hintIndex}] must have a 'label' property of type string`
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
- function validateContainerProps(element, elementPath, errors2) {
212
- if ("columns" in element && element.columns !== void 0) {
213
- const columns = element.columns;
214
- const validColumns = [1, 2, 3, 4];
215
- if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
216
- errors2.push(
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
- function checkFlatOutputCollisions(elements, scopePath) {
231
- var _a, _b;
232
- const allOutputKeys = /* @__PURE__ */ new Set();
233
- for (const el of elements) {
234
- if (el.type === "richinput" && el.flatOutput) {
235
- const richEl = el;
236
- const textKey = (_a = richEl.textKey) != null ? _a : "text";
237
- const filesKey = (_b = richEl.filesKey) != null ? _b : "files";
238
- for (const otherEl of elements) {
239
- if (otherEl === el) continue;
240
- if (otherEl.key === textKey) {
241
- errors.push(
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
- function validateCountBounds(element, elementPath, errors2) {
276
- var _a, _b;
277
- const el = element;
278
- if (el.type === "group") {
279
- if (!isPlainObject(el.repeat)) return;
280
- checkBounds(
281
- elementPath,
282
- (_a = el.repeat) == null ? void 0 : _a.min,
283
- (_b = el.repeat) == null ? void 0 : _b.max,
284
- "repeat.min",
285
- "repeat.max",
286
- el.required === true,
287
- errors2
288
- );
289
- return;
290
- }
291
- const isMultiple = el.multiple === true || el.type === "files";
292
- if (!isMultiple) return;
293
- checkBounds(
294
- elementPath,
295
- el.minCount,
296
- el.maxCount,
297
- "minCount",
298
- "maxCount",
299
- el.required === true,
300
- errors2
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
- function checkBounds(elementPath, minCount, maxCount, minName, maxName, requiredImpliesFloor, errors2) {
304
- for (const [name, bound] of [
305
- [minName, minCount],
306
- [maxName, maxCount]
307
- ]) {
308
- if (bound !== void 0 && typeof bound !== "number") {
309
- errors2.push(
310
- `${elementPath}: ${name} must be a number (got ${typeof bound})`
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
- function validateElements(elements, path) {
335
- const seenKeys = /* @__PURE__ */ new Set();
336
- elements.forEach((element, index) => {
337
- if (!element.key) return;
338
- if (seenKeys.has(element.key)) {
339
- errors.push(`${path}[${index}]: duplicate key "${element.key}"`);
340
- }
341
- seenKeys.add(element.key);
342
- });
343
- elements.forEach((element, index) => {
344
- const elementPath = `${path}[${index}]`;
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
- textarea.addEventListener("paste", (e) => {
735
- var _a, _b, _c, _d;
736
- const pasted = (_b = (_a = e.clipboardData) == null ? void 0 : _a.getData("text")) != null ? _b : "";
737
- if (!/[\r\n]/.test(pasted)) return;
738
- e.preventDefault();
739
- const cleaned = pasted.replace(/[\r\n]+/g, " ");
740
- const start = (_c = textarea.selectionStart) != null ? _c : textarea.value.length;
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 { row, button, counter, update };
1033
+ }
1034
+ return current;
814
1035
  }
815
- function createSlideAddTile(onClick, options = {}) {
1036
+ function evaluateEnableCondition(condition, formData, containerData) {
816
1037
  var _a;
817
- const label = (_a = options.label) != null ? _a : "";
818
- const tile = document.createElement("button");
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
- tile.addEventListener("mouseenter", () => {
865
- if (tile.disabled) return;
866
- tile.style.borderStyle = "solid";
867
- tile.style.backgroundColor = "var(--fb-background-hover-color)";
868
- });
869
- tile.addEventListener("mouseleave", () => {
870
- tile.style.borderStyle = "dashed";
871
- tile.style.backgroundColor = "transparent";
872
- });
873
- tile.onclick = onClick;
874
- const counter = document.createElement("span");
875
- counter.className = "fb-add-counter";
876
- counter.style.cssText = `
877
- margin-left: auto;
878
- font-size: var(--fb-font-size-small, 0.875rem);
879
- color: var(--fb-text-secondary-color);
880
- font-weight: 400;
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 applyActionButtonStyles(button, isFormLevel = false) {
891
- button.style.cssText = `
892
- background-color: var(--fb-action-bg-color);
893
- color: var(--fb-action-text-color);
894
- border: var(--fb-border-width) solid var(--fb-action-border-color);
895
- padding: ${isFormLevel ? "0.5rem 1rem" : "0.5rem 0.75rem"};
896
- font-size: var(--fb-font-size);
897
- font-weight: var(--fb-font-weight-medium);
898
- border-radius: var(--fb-border-radius);
899
- transition: all var(--fb-transition-duration);
900
- box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
901
- `;
902
- button.addEventListener("mouseenter", () => {
903
- button.style.backgroundColor = "var(--fb-action-hover-bg-color)";
904
- button.style.borderColor = "var(--fb-action-hover-border-color)";
905
- });
906
- button.addEventListener("mouseleave", () => {
907
- button.style.backgroundColor = "var(--fb-action-bg-color)";
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, _b, _c;
1366
+ var _a;
1199
1367
  const errors = [];
1200
- const { scopeRoot, skipValidation } = context;
1201
- const validateTextInput = (input, val, fieldKey) => {
1202
- let hasError = false;
1203
- const { state } = context;
1204
- if (!skipValidation && val) {
1205
- if (element.minLength !== void 0 && element.minLength !== null && val.length < element.minLength) {
1206
- const msg = t("minLength", state, { min: element.minLength });
1207
- errors.push(`${fieldKey}: ${msg}`);
1208
- markFieldValidity(input, msg);
1209
- hasError = true;
1210
- } else if (element.maxLength !== void 0 && element.maxLength !== null && val.length > element.maxLength) {
1211
- const msg = t("maxLength", state, { max: element.maxLength });
1212
- errors.push(`${fieldKey}: ${msg}`);
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
- if (!hasError) {
1233
- markFieldValidity(input, null);
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
- const rawValues = [];
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
- if (!skipValidation) {
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 = (_c = input == null ? void 0 : input.value) != null ? _c : "";
1266
- if (!skipValidation && element.required && val === "") {
1267
- const msg = t("required", context.state);
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, _b, _c;
1952
+ var _a;
1814
1953
  const errors = [];
1815
- const { scopeRoot, skipValidation } = context;
1816
- const validateNumberInput = (input, v, fieldKey) => {
1817
- let hasError = false;
1818
- const { state } = context;
1819
- if (!skipValidation && element.min !== void 0 && element.min !== null && v < element.min) {
1820
- const msg = t("minValue", state, { min: element.min });
1821
- errors.push(`${fieldKey}: ${msg}`);
1822
- markFieldValidity(input, msg);
1823
- hasError = true;
1824
- } else if (!skipValidation && element.max !== void 0 && element.max !== null && v > element.max) {
1825
- const msg = t("maxValue", state, { max: element.max });
1826
- errors.push(`${fieldKey}: ${msg}`);
1827
- markFieldValidity(input, msg);
1828
- hasError = true;
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
- var _a2;
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
- if (!skipValidation) {
1859
- const { state } = context;
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
- const raw = (_c = input == null ? void 0 : input.value) != null ? _c : "";
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
- validateNumberInput(input, v, key);
1896
- return { value: applyDecimals(v, element.decimals), errors };
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, skipValidation } = context;
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
- validateMultipleCount(key, values, element, (v) => v != null);
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 (!skipValidation && element.required && val === "") {
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, skipValidation } = context;
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
- if (!skipValidation && val !== "" && !validOptionValues.has(val)) {
2592
- const msg = t("invalidOption", context.state);
2593
- markFieldValidity(input, msg);
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
- validateMultipleCount(key, values, element, (v) => v != null);
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
- if (!skipValidation && element.required && val === "") {
2607
- const msg = t("required", context.state);
2608
- errors.push(`${key}: ${msg}`);
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.classList.remove("invalid");
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.classList.remove("invalid");
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 Clear-all row below multi grid \u2500\u2500\u2500 */
3210
- .fb-clear-all-row {
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
- justify-content: flex-end;
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 showFileError(container, message) {
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 existing = (_a = container.closest("[data-files-wrapper]")) == null ? void 0 : _a.querySelector(".file-error-message");
3483
- if (existing) existing.remove();
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
- if (errorTarget) {
4585
- const combined = buildBatchErrorMessage(errorMessage, failures, state);
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 buildClearAllRow(state, ridCount, onClearAll) {
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
- row.appendChild(clearBtn);
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
- if (onClearAll) {
5226
- const row = buildClearAllRow(state, ridList.length, onClearAll);
5227
- if (row) container.appendChild(row);
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 setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
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: (_b = (_a = element.maxSize) != null ? _a : element.maxSizeMB) != null ? _b : Infinity
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, _c;
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
- (_c = list.querySelector(`[data-resource-id="${rid}"]`)) == null ? void 0 : _c.remove();
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 validateFileCount(key, resourceIds, element, state, errors) {
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) return;
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
- errors.push(
5655
- `${key}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
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
- errors.push(
5663
- `${key}: ${t("invalidFileMime", state, { name: fileName, type: mimeType, mimes })}`
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(key, resourceIds, element, state, errors) {
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
- errors.push(
5678
- `${key}: ${t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })}`
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, skipValidation, path, state } = context;
5685
- const errors = [];
5817
+ const { scopeRoot, path, state } = context;
5686
5818
  const fullKey = pathJoin(path, key);
5687
5819
  const resourceIds = readMultiFileResourceIds(scopeRoot, fullKey);
5688
- if (!skipValidation) {
5689
- validateFileCount(key, resourceIds, element, state, errors);
5690
- validateFileTypes(key, resourceIds, element, state, errors);
5691
- validateFileSizes(key, resourceIds, element, state, errors);
5692
- }
5693
- return { value: resourceIds, errors };
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, skipValidation, state } = context;
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
- if (!skipValidation && element.required && rid === "") {
5704
- errors.push(`${key}: ${t("required", state)}`);
5705
- return { value: null, errors };
5706
- }
5707
- if (!skipValidation && rid !== "") {
5708
- validateFileTypes(key, [rid], element, state, errors);
5709
- validateFileSizes(key, [rid], element, state, errors);
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 { value: rid || null, errors };
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
- renderFilesElementEdit(element, ctx, wrapper, pathKey);
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
- renderMultipleFileElementEdit(element, ctx, wrapper, pathKey);
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, _b, _c;
6349
+ var _a;
6195
6350
  const errors = [];
6196
- const { scopeRoot, skipValidation } = context;
6351
+ const { scopeRoot, state } = context;
6197
6352
  const validateColourValue = (input, val, fieldKey) => {
6198
- const { state } = context;
6353
+ const normalized = val ? normalizeColourValue(val) : "";
6354
+ let msg = null;
6199
6355
  if (!val) {
6200
- if (!skipValidation && element.required) {
6201
- const msg = t("required", state);
6202
- errors.push(`${fieldKey}: ${msg}`);
6203
- markFieldValidity(input, msg);
6204
- return "";
6205
- }
6206
- markFieldValidity(input, null);
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
- const validated = validateColourValue(input, val, `${key}[${index}]`);
6228
- values.push(validated);
6372
+ values.push(validateColourValue(input, val, `${key}[${index}]`));
6229
6373
  });
6230
- if (!skipValidation) {
6231
- const { state } = context;
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 = (_c = hexInput == null ? void 0 : hexInput.value) != null ? _c : "";
6251
- if (!skipValidation && element.required && val === "") {
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, _b, _c;
6746
+ var _a;
6627
6747
  const errors = [];
6628
- const { scopeRoot, skipValidation } = context;
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
- if (!skipValidation && element.required) {
6648
- const msg = t("required", state);
6649
- errors.push(`${fieldKey}: ${msg}`);
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
- let value;
6657
- if (scale === "exponential") {
6658
- const position = parseFloat(rawValue) / 1e3;
6659
- value = positionToExponential(position, min, max);
6660
- value = alignToStep(value, step);
6661
- } else {
6662
- value = parseFloat(rawValue);
6663
- value = alignToStep(value, step);
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
- const value = validateSliderValue(slider, `${key}[${index}]`);
6689
- values.push(value);
6789
+ values.push(validateSliderValue(slider, `${key}[${index}]`));
6690
6790
  });
6691
- if (!skipValidation) {
6692
- const { state } = context;
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 (!skipValidation && element.required) {
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, skipValidation, path } = context;
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
- validateContainerCount(key, items, element);
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, skipValidation } = context;
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
- let value;
8631
- try {
8632
- value = JSON.parse(hiddenInput.value);
8633
- } catch {
8634
- errors.push(`${key}: invalid table data`);
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
- if (!skipValidation && element.required) {
8638
- const cells = value[cellsKey];
8639
- const hasContent = cells == null ? void 0 : cells.some(
8640
- (row) => row.some((cell) => cell.trim() !== "")
8641
- );
8642
- if (!hasContent) {
8643
- errors.push(`${key}: ${t("required", context.state)}`);
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, skipValidation } = context;
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
- let rawValue;
10000
- try {
10001
- const parsed = JSON.parse(hiddenInput.value);
10002
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
10003
- rawValue = parsed;
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
- if (!skipValidation) {
10021
- const textEmpty = !text || text.trim() === "";
10022
- const filesEmpty = files.length === 0;
10023
- if (element.required && textEmpty && filesEmpty) {
10024
- errors.push(`${key}: ${t("required", state)}`);
10025
- }
10026
- if (!textEmpty && text) {
10027
- if (element.minLength != null && text.length < element.minLength) {
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.maxFiles != null && files.length > element.maxFiles) {
10039
- errors.push(`${key}: ${t("maxFiles", state, { max: element.maxFiles })}`);
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
- * This is a complete copy of the validateForm logic from form-builder.ts
11847
- * but uses instance state instead of global state
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
- skipValidation,
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
- * Get form data
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.validateForm(false);
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) {