@dmitryvim/form-builder 0.5.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,15 +4,16 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  // src/utils/translation.ts
6
6
  function t(key, state, params) {
7
+ var _a, _b;
7
8
  const locale = state.config.locale || "en";
8
9
  const localeTranslations = state.config.translations[locale];
9
10
  const fallbackTranslations = state.config.translations.en;
10
- let text = (localeTranslations == null ? void 0 : localeTranslations[key]) || (fallbackTranslations == null ? void 0 : fallbackTranslations[key]) || key;
11
+ let text = (_b = (_a = localeTranslations == null ? void 0 : localeTranslations[key]) != null ? _a : fallbackTranslations == null ? void 0 : fallbackTranslations[key]) != null ? _b : key;
11
12
  if (params) {
12
13
  for (const [paramKey, paramValue] of Object.entries(params)) {
13
14
  text = text.replace(
14
15
  new RegExp(`\\{${paramKey}\\}`, "g"),
15
- String(paramValue)
16
+ () => String(paramValue)
16
17
  );
17
18
  }
18
19
  }
@@ -62,7 +63,7 @@ function formatFileSize(bytes) {
62
63
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
63
64
  }
64
65
  function serializeHiddenValue(value) {
65
- if (value === null || value === void 0) return "";
66
+ if (value === void 0) return "";
66
67
  return JSON.stringify(value);
67
68
  }
68
69
  function deserializeHiddenValue(raw) {
@@ -73,6 +74,23 @@ function deserializeHiddenValue(raw) {
73
74
  return raw;
74
75
  }
75
76
  }
77
+ function readTypedInputValue(input) {
78
+ if (input instanceof HTMLInputElement) {
79
+ if (input.type === "checkbox") return input.checked;
80
+ if (input.dataset.hiddenField) return deserializeHiddenValue(input.value);
81
+ if (input.dataset.booleanField) return input.value === "true";
82
+ if (input.type === "number" || input.type === "range") {
83
+ if (input.value === "") return null;
84
+ const parsed = parseFloat(input.value);
85
+ const decimals = input.dataset.decimals;
86
+ return decimals !== void 0 ? Number(parsed.toFixed(parseInt(decimals, 10))) : parsed;
87
+ }
88
+ if (input.dataset.colourField) {
89
+ return input.value.toUpperCase();
90
+ }
91
+ }
92
+ return input.value === "" ? null : input.value;
93
+ }
76
94
  function createHiddenInput(name, value) {
77
95
  const input = document.createElement("input");
78
96
  input.type = "hidden";
@@ -82,413 +100,201 @@ function createHiddenInput(name, value) {
82
100
  return input;
83
101
  }
84
102
 
85
- // src/utils/validation.ts
86
- function addLengthHint(element, parts, state) {
87
- if (element.minLength != null || element.maxLength != null) {
88
- if (element.minLength != null && element.maxLength != null) {
89
- parts.push(
90
- t("hintLengthRange", state, {
91
- min: element.minLength,
92
- max: element.maxLength
93
- })
94
- );
95
- } else if (element.maxLength != null) {
96
- parts.push(t("hintMaxLength", state, { max: element.maxLength }));
97
- } else if (element.minLength != null) {
98
- 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
+ }
99
117
  }
100
118
  }
119
+ const sibling = anchor.nextElementSibling;
120
+ return sibling && isInputErrorNode(sibling) ? sibling : null;
101
121
  }
102
- function addRangeHint(element, parts, state) {
103
- if (element.min != null || element.max != null) {
104
- if (element.min != null && element.max != null) {
105
- parts.push(
106
- t("hintValueRange", state, { min: element.min, max: element.max })
107
- );
108
- } else if (element.max != null) {
109
- parts.push(t("hintMaxValue", state, { max: element.max }));
110
- } else if (element.min != null) {
111
- parts.push(t("hintMinValue", state, { min: element.min }));
112
- }
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;
113
130
  }
131
+ if (scope.draftMarks && !reported.has(target)) return void 0;
132
+ reported.add(target);
133
+ return message;
114
134
  }
115
- 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) {
116
157
  var _a;
117
- const sizeMB = (_a = element.maxSize) != null ? _a : element.maxSizeMB;
118
- if (sizeMB && sizeMB !== Infinity) {
119
- parts.push(t("hintMaxSize", state, { size: sizeMB }));
120
- }
158
+ return ((_a = target.getAttribute("aria-describedby")) != null ? _a : "").split(/\s+/).filter(Boolean);
121
159
  }
122
- 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) {
123
163
  var _a;
124
- if ((_a = element.accept) == null ? void 0 : _a.extensions) {
125
- parts.push(
126
- t("hintFormats", state, {
127
- formats: element.accept.extensions.map((ext) => ext.toUpperCase()).join(",")
128
- })
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(" ")
129
194
  );
130
195
  }
131
196
  }
132
- function addPatternHint(element, parts, state) {
133
- if (element.pattern) {
134
- parts.push(t("hintPattern", state, { pattern: element.pattern }));
135
- }
136
- }
137
- function makeFieldHint(element, state) {
138
- const parts = [];
139
- addLengthHint(element, parts, state);
140
- if (element.type !== "slider") {
141
- addRangeHint(element, parts, state);
142
- }
143
- addFileSizeHint(element, parts, state);
144
- addFormatHint(element, parts, state);
145
- addPatternHint(element, parts, state);
146
- 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);
147
201
  }
148
- function validateSchema(schema) {
149
- const errors = [];
150
- if (!schema || typeof schema !== "object") {
151
- errors.push("Schema must be an object");
152
- return errors;
153
- }
154
- if (!Array.isArray(schema.elements)) {
155
- errors.push("Schema missing elements array");
156
- return errors;
157
- }
158
- if ("columns" in schema && schema.columns !== void 0) {
159
- const columns = schema.columns;
160
- const validColumns = [1, 2, 3, 4];
161
- if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
162
- 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);
163
208
  }
209
+ target.removeAttribute(ADDED_ATTRS);
164
210
  }
165
- if ("prefillHints" in schema && schema.prefillHints) {
166
- const prefillHints = schema.prefillHints;
167
- if (Array.isArray(prefillHints)) {
168
- prefillHints.forEach((hint, hintIndex) => {
169
- if (!hint.label || typeof hint.label !== "string") {
170
- errors.push(
171
- `schema.prefillHints[${hintIndex}] must have a 'label' property of type string`
172
- );
173
- }
174
- if (!hint.values || typeof hint.values !== "object") {
175
- errors.push(
176
- `schema.prefillHints[${hintIndex}] must have a 'values' property of type object`
177
- );
178
- } else {
179
- for (const fieldKey in hint.values) {
180
- const fieldExists = schema.elements.some(
181
- (element) => element.key === fieldKey
182
- );
183
- if (!fieldExists) {
184
- errors.push(
185
- `schema.prefillHints[${hintIndex}] references non-existent field "${fieldKey}"`
186
- );
187
- }
188
- }
189
- }
190
- });
191
- }
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();
192
219
  }
193
- function validateContainerProps(element, elementPath, errors2) {
194
- if ("columns" in element && element.columns !== void 0) {
195
- const columns = element.columns;
196
- const validColumns = [1, 2, 3, 4];
197
- if (!Number.isInteger(columns) || !validColumns.includes(columns)) {
198
- errors2.push(
199
- `${elementPath}: columns must be 1, 2, 3, or 4 (got ${columns})`
200
- );
201
- }
202
- }
203
- if ("displayMode" in element && element.displayMode !== void 0) {
204
- const displayMode = element.displayMode;
205
- if (displayMode !== "stack" && displayMode !== "slides") {
206
- errors2.push(
207
- `${elementPath}: displayMode must be "stack" or "slides" (got ${JSON.stringify(displayMode)})`
208
- );
209
- }
210
- }
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;
211
226
  }
212
- function checkFlatOutputCollisions(elements, scopePath) {
213
- var _a, _b;
214
- const allOutputKeys = /* @__PURE__ */ new Set();
215
- for (const el of elements) {
216
- if (el.type === "richinput" && el.flatOutput) {
217
- const richEl = el;
218
- const textKey = (_a = richEl.textKey) != null ? _a : "text";
219
- const filesKey = (_b = richEl.filesKey) != null ? _b : "files";
220
- for (const otherEl of elements) {
221
- if (otherEl === el) continue;
222
- if (otherEl.key === textKey) {
223
- errors.push(
224
- `${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with element key "${otherEl.key}"`
225
- );
226
- }
227
- if (otherEl.key === filesKey) {
228
- errors.push(
229
- `${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with element key "${otherEl.key}"`
230
- );
231
- }
232
- }
233
- if (allOutputKeys.has(textKey)) {
234
- errors.push(
235
- `${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with another flatOutput key`
236
- );
237
- }
238
- if (allOutputKeys.has(filesKey)) {
239
- errors.push(
240
- `${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with another flatOutput key`
241
- );
242
- }
243
- allOutputKeys.add(textKey);
244
- allOutputKeys.add(filesKey);
245
- } else {
246
- if (el.key) {
247
- if (allOutputKeys.has(el.key)) {
248
- errors.push(
249
- `${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
250
- );
251
- }
252
- allOutputKeys.add(el.key);
253
- }
254
- }
255
- }
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;
256
239
  }
257
- function validateCountBounds(element, elementPath, errors2) {
258
- var _a, _b;
259
- const el = element;
260
- if (el.type === "group") {
261
- if (!isPlainObject(el.repeat)) return;
262
- checkBounds(
263
- elementPath,
264
- (_a = el.repeat) == null ? void 0 : _a.min,
265
- (_b = el.repeat) == null ? void 0 : _b.max,
266
- "repeat.min",
267
- "repeat.max",
268
- el.required === true,
269
- errors2
270
- );
271
- return;
272
- }
273
- const isMultiple = el.multiple === true || el.type === "files";
274
- if (!isMultiple) return;
275
- checkBounds(
276
- elementPath,
277
- el.minCount,
278
- el.maxCount,
279
- "minCount",
280
- "maxCount",
281
- el.required === true,
282
- 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`
283
273
  );
284
274
  }
285
- function checkBounds(elementPath, minCount, maxCount, minName, maxName, requiredImpliesFloor, errors2) {
286
- for (const [name, bound] of [
287
- [minName, minCount],
288
- [maxName, maxCount]
289
- ]) {
290
- if (bound !== void 0 && typeof bound !== "number") {
291
- errors2.push(
292
- `${elementPath}: ${name} must be a number (got ${typeof bound})`
293
- );
294
- }
295
- }
296
- const min = typeof minCount === "number" ? minCount : void 0;
297
- const max = typeof maxCount === "number" ? maxCount : void 0;
298
- if (max !== void 0 && (max < 0 || Number.isNaN(max))) {
299
- errors2.push(
300
- `${elementPath}: ${maxName} must be a non-negative number or Infinity (got ${max})`
301
- );
302
- }
303
- if (min !== void 0 && (min < 0 || !Number.isFinite(min))) {
304
- errors2.push(
305
- `${elementPath}: ${minName} must be a finite non-negative number (got ${min})`
306
- );
307
- }
308
- const effectiveMin = min != null ? min : requiredImpliesFloor ? 1 : void 0;
309
- if (effectiveMin !== void 0 && max !== void 0 && effectiveMin > max) {
310
- const shown = min !== void 0 ? `${minName} (${min})` : `required: true (implies ${minName} 1)`;
311
- errors2.push(
312
- `${elementPath}: ${shown} cannot be greater than ${maxName} (${max})`
313
- );
314
- }
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;
315
284
  }
316
- function validateElements(elements, path) {
317
- elements.forEach((element, index) => {
318
- const elementPath = `${path}[${index}]`;
319
- if (!element.type) {
320
- errors.push(`${elementPath}: missing type`);
321
- }
322
- if (!element.key && element.type !== "markdown") {
323
- errors.push(`${elementPath}: missing key`);
324
- }
325
- validateCountBounds(element, elementPath, errors);
326
- if (element.type === "number" && "decimals" in element) {
327
- const decimals = element.decimals;
328
- if (decimals !== void 0 && (!Number.isInteger(decimals) || decimals < 0)) {
329
- errors.push(
330
- `${elementPath}: decimals must be a non-negative integer (got ${JSON.stringify(decimals)})`
331
- );
332
- }
333
- }
334
- if (element.type === "markdown") {
335
- const content = element.content;
336
- if (typeof content !== "string") {
337
- errors.push(
338
- `${elementPath}: markdown element requires "content" to be a string (got ${content === null ? "null" : typeof content})`
339
- );
340
- }
341
- }
342
- if (element.enableIf) {
343
- const enableIf = element.enableIf;
344
- if (!enableIf.key || typeof enableIf.key !== "string") {
345
- errors.push(
346
- `${elementPath}: enableIf must have a 'key' property of type string`
347
- );
348
- }
349
- const hasOperator = "equals" in enableIf;
350
- if (!hasOperator) {
351
- errors.push(
352
- `${elementPath}: enableIf must have at least one operator (equals, etc.)`
353
- );
354
- }
355
- }
356
- if (element.type === "group" && "elements" in element && element.elements) {
357
- validateElements(element.elements, `${elementPath}.elements`);
358
- }
359
- if (element.type === "container" && element.elements) {
360
- validateContainerProps(element, elementPath, errors);
361
- if ("prefillHints" in element && element.prefillHints) {
362
- const prefillHints = element.prefillHints;
363
- if (Array.isArray(prefillHints)) {
364
- prefillHints.forEach((hint, hintIndex) => {
365
- if (!hint.label || typeof hint.label !== "string") {
366
- errors.push(
367
- `${elementPath}: prefillHints[${hintIndex}] must have a 'label' property of type string`
368
- );
369
- }
370
- if (!hint.values || typeof hint.values !== "object") {
371
- errors.push(
372
- `${elementPath}: prefillHints[${hintIndex}] must have a 'values' property of type object`
373
- );
374
- } else {
375
- for (const fieldKey in hint.values) {
376
- const fieldExists = element.elements.some(
377
- (childElement) => childElement.key === fieldKey
378
- );
379
- if (!fieldExists) {
380
- errors.push(
381
- `container "${element.key}": prefillHints[${hintIndex}] references non-existent field "${fieldKey}"`
382
- );
383
- }
384
- }
385
- }
386
- });
387
- }
388
- }
389
- validateElements(element.elements, `${elementPath}.elements`);
390
- checkFlatOutputCollisions(element.elements, `${elementPath}.elements`);
391
- }
392
- if (element.type === "select" && element.options) {
393
- const defaultValue = element.default;
394
- if (defaultValue !== void 0 && defaultValue !== null && defaultValue !== "") {
395
- const hasMatchingOption = element.options.some(
396
- (opt) => opt.value === defaultValue
397
- );
398
- if (!hasMatchingOption) {
399
- errors.push(
400
- `${elementPath}: default "${defaultValue}" not in options`
401
- );
402
- }
403
- }
404
- }
405
- });
406
- }
407
- if (Array.isArray(schema.elements)) {
408
- validateElements(schema.elements, "elements");
409
- checkFlatOutputCollisions(schema.elements, "elements");
410
- }
411
- return errors;
412
- }
413
-
414
- // src/utils/enable-conditions.ts
415
- function getValueByPath(data, path) {
416
- if (!data || typeof data !== "object") {
417
- return void 0;
418
- }
419
- const segments = path.match(/[^.[\]]+|\[\d+\]/g);
420
- if (!segments || segments.length === 0) {
421
- return void 0;
422
- }
423
- let current = data;
424
- for (const segment of segments) {
425
- if (current === void 0 || current === null) {
426
- return void 0;
427
- }
428
- if (segment.startsWith("[") && segment.endsWith("]")) {
429
- const index = parseInt(segment.slice(1, -1), 10);
430
- if (!Array.isArray(current) || isNaN(index)) {
431
- return void 0;
432
- }
433
- current = current[index];
434
- } else {
435
- current = current[segment];
436
- }
437
- }
438
- return current;
439
- }
440
- function evaluateEnableCondition(condition, formData, containerData) {
441
- var _a;
442
- if (!condition || !condition.key) {
443
- throw new Error("Invalid enableIf condition: must have a 'key' property");
444
- }
445
- const scope = (_a = condition.scope) != null ? _a : "relative";
446
- let dataSource;
447
- if (scope === "relative") {
448
- dataSource = containerData != null ? containerData : formData;
449
- } else if (scope === "absolute") {
450
- dataSource = formData;
451
- } else {
452
- throw new Error(
453
- `Invalid enableIf scope: must be "relative" or "absolute" (got "${scope}")`
454
- );
455
- }
456
- const actualValue = getValueByPath(dataSource, condition.key);
457
- if ("equals" in condition) {
458
- return deepEqual(actualValue, condition.equals);
459
- }
460
- throw new Error(
461
- `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
462
296
  );
463
297
  }
464
- function deepEqual(a, b) {
465
- if (a === b) return true;
466
- if (a == null || b == null) return a === b;
467
- if (typeof a !== typeof b) return false;
468
- if (typeof a === "object" && typeof b === "object") {
469
- try {
470
- return JSON.stringify(a) === JSON.stringify(b);
471
- } catch (e) {
472
- if (e instanceof TypeError && (e.message.includes("circular") || e.message.includes("cyclic"))) {
473
- console.warn(
474
- "deepEqual: Circular reference detected in enableIf comparison, using reference equality"
475
- );
476
- return a === b;
477
- }
478
- throw e;
479
- }
480
- }
481
- return a === b;
482
- }
483
-
484
- // src/utils/styles.ts
485
- function clearFieldError(input) {
486
- const name = input.getAttribute("name");
487
- if (!name) return;
488
- const doc = input.ownerDocument || document;
489
- const errorNode = doc.getElementById(`error-${name}`);
490
- if (errorNode) errorNode.remove();
491
- }
492
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>';
493
299
  function ensureThemingHooks(doc) {
494
300
  if (doc.head.querySelector("[data-fb-theming-hooks]")) return;
@@ -606,11 +412,18 @@ function ensureThemingHooks(doc) {
606
412
  /* .fb-size-md uses defaults \u2014 no override needed */
607
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; }
608
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
+ }
609
422
  `;
610
423
  doc.head.appendChild(style);
611
424
  }
612
- function applyAutoExpand(textarea, options = {}) {
613
- var _a;
425
+ function applyAutoExpand(textarea, options) {
426
+ var _a, _b;
614
427
  textarea.style.overflow = "hidden";
615
428
  textarea.style.resize = "none";
616
429
  const minRows = Math.max(1, (_a = options.minRows) != null ? _a : 1);
@@ -638,18 +451,20 @@ function applyAutoExpand(textarea, options = {}) {
638
451
  if (typeof ResizeObserver === "undefined") return;
639
452
  let lastWidth = -1;
640
453
  const ro = new ResizeObserver((entries) => {
641
- var _a2, _b, _c, _d, _e;
454
+ var _a2, _b2, _c, _d, _e, _f;
642
455
  if (!textarea.isConnected) {
643
456
  ro.disconnect();
457
+ (_a2 = options.observers) == null ? void 0 : _a2.delete(ro);
644
458
  return;
645
459
  }
646
460
  const entry = entries[0];
647
- const w = (_e = (_d = (_b = (_a2 = entry == null ? void 0 : entry.contentBoxSize) == null ? void 0 : _a2[0]) == null ? void 0 : _b.inlineSize) != null ? _d : (_c = entry == null ? void 0 : entry.contentRect) == null ? void 0 : _c.width) != null ? _e : 0;
461
+ const w = (_f = (_e = (_c = (_b2 = entry == null ? void 0 : entry.contentBoxSize) == null ? void 0 : _b2[0]) == null ? void 0 : _c.inlineSize) != null ? _e : (_d = entry == null ? void 0 : entry.contentRect) == null ? void 0 : _d.width) != null ? _f : 0;
648
462
  if (w === lastWidth) return;
649
463
  lastWidth = w;
650
464
  resize();
651
465
  });
652
466
  ro.observe(textarea);
467
+ (_b = options.observers) == null ? void 0 : _b.add(ro);
653
468
  }
654
469
  function applySingleLineMode(textarea) {
655
470
  textarea.addEventListener("keydown", (e) => {
@@ -813,26 +628,453 @@ function createSlideAddTile(onClick, options = {}) {
813
628
  };
814
629
  return { tile, counter, update };
815
630
  }
816
- function applyActionButtonStyles(button, isFormLevel = false) {
817
- button.style.cssText = `
818
- background-color: var(--fb-action-bg-color);
819
- color: var(--fb-action-text-color);
820
- border: var(--fb-border-width) solid var(--fb-action-border-color);
821
- padding: ${isFormLevel ? "0.5rem 1rem" : "0.5rem 0.75rem"};
822
- font-size: var(--fb-font-size);
823
- font-weight: var(--fb-font-weight-medium);
824
- border-radius: var(--fb-border-radius);
825
- transition: all var(--fb-transition-duration);
826
- box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
827
- `;
828
- button.addEventListener("mouseenter", () => {
829
- button.style.backgroundColor = "var(--fb-action-hover-bg-color)";
830
- button.style.borderColor = "var(--fb-action-hover-border-color)";
831
- });
832
- button.addEventListener("mouseleave", () => {
833
- button.style.backgroundColor = "var(--fb-action-bg-color)";
834
- button.style.borderColor = "var(--fb-action-border-color)";
835
- });
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;
1023
+ }
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];
1032
+ }
1033
+ }
1034
+ return current;
1035
+ }
1036
+ function evaluateEnableCondition(condition, formData, containerData) {
1037
+ var _a;
1038
+ if (!condition || !condition.key) {
1039
+ throw new Error("Invalid enableIf condition: must have a 'key' property");
1040
+ }
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
+ );
1059
+ }
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;
836
1078
  }
837
1079
 
838
1080
  // src/components/text.ts
@@ -937,7 +1179,7 @@ function createCharCounter(element, input) {
937
1179
  return counter;
938
1180
  }
939
1181
  function renderTextElement(element, ctx, wrapper, pathKey) {
940
- var _a;
1182
+ var _a, _b, _c;
941
1183
  const state = ctx.state;
942
1184
  const readonly = isElementReadonly(element, state, ctx);
943
1185
  const inputWrapper = document.createElement("div");
@@ -965,10 +1207,10 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
965
1207
  `;
966
1208
  textInput.name = pathKey;
967
1209
  textInput.placeholder = (_a = element.placeholder) != null ? _a : t("placeholderText", state);
968
- textInput.value = ctx.prefill[element.key] || element.default || "";
1210
+ textInput.value = (_c = (_b = ctx.prefill[element.key]) != null ? _b : element.default) != null ? _c : "";
969
1211
  textInput.readOnly = readonly;
970
1212
  applySingleLineMode(textInput);
971
- applyAutoExpand(textInput);
1213
+ applyAutoExpand(textInput, { observers: state.autoExpandObservers });
972
1214
  if (!readonly) {
973
1215
  textInput.addEventListener("focus", () => {
974
1216
  textInput.style.borderColor = "var(--fb-border-focus-color)";
@@ -1027,11 +1269,12 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1027
1269
  const chip = input.closest(".fb-chip");
1028
1270
  const sib = chip == null ? void 0 : chip.nextElementSibling;
1029
1271
  if (sib && sib.classList.contains("error-message")) {
1030
- sib.id = `error-${input.name}`;
1272
+ sib.setAttribute("data-error-for", input.name);
1031
1273
  }
1032
1274
  });
1033
1275
  }
1034
1276
  function addChip(value = "") {
1277
+ var _a2;
1035
1278
  const chip = document.createElement("div");
1036
1279
  chip.className = "fb-chip";
1037
1280
  const dot = document.createElement("span");
@@ -1042,7 +1285,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1042
1285
  input.rows = 1;
1043
1286
  input.className = "fb-chip-input";
1044
1287
  input.value = value;
1045
- input.placeholder = element.placeholder || t("placeholderText", state);
1288
+ input.placeholder = (_a2 = element.placeholder) != null ? _a2 : t("placeholderText", state);
1046
1289
  input.readOnly = readonly;
1047
1290
  chip.appendChild(input);
1048
1291
  if (!readonly && ctx.instance) {
@@ -1056,7 +1299,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1056
1299
  input.addEventListener("input", handleChange);
1057
1300
  }
1058
1301
  applySingleLineMode(input);
1059
- applyAutoExpand(input);
1302
+ applyAutoExpand(input, { observers: state.autoExpandObservers });
1060
1303
  if (!readonly) {
1061
1304
  const rem = document.createElement("button");
1062
1305
  rem.type = "button";
@@ -1064,7 +1307,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1064
1307
  rem.setAttribute("aria-label", t("removeElement", state));
1065
1308
  rem.innerHTML = BIN_ICON_SVG;
1066
1309
  rem.onclick = () => {
1067
- var _a2;
1310
+ var _a3;
1068
1311
  const chips = list.querySelectorAll(".fb-chip");
1069
1312
  const idx = Array.prototype.indexOf.call(chips, chip);
1070
1313
  if (idx < 0) return;
@@ -1078,7 +1321,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1078
1321
  updateIndices();
1079
1322
  updateAddButton();
1080
1323
  updateRemoveButtons();
1081
- (_a2 = ctx.instance) == null ? void 0 : _a2.triggerOnChange(pathKey);
1324
+ (_a3 = ctx.instance) == null ? void 0 : _a3.triggerOnChange(pathKey);
1082
1325
  };
1083
1326
  chip.appendChild(rem);
1084
1327
  }
@@ -1120,113 +1363,53 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1120
1363
  updateRemoveButtons();
1121
1364
  }
1122
1365
  function validateTextElement(element, key, context) {
1123
- var _a, _b, _c;
1366
+ var _a;
1124
1367
  const errors = [];
1125
- const { scopeRoot, skipValidation } = context;
1126
- const markValidity = (input, errorMessage) => {
1127
- var _a2, _b2, _c2;
1128
- if (!input) return;
1129
- const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
1130
- let errorElement = document.getElementById(errorId);
1131
- if (errorMessage) {
1132
- input.classList.add("invalid");
1133
- input.title = errorMessage;
1134
- if (!errorElement) {
1135
- errorElement = document.createElement("div");
1136
- errorElement.id = errorId;
1137
- errorElement.className = "error-message";
1138
- errorElement.style.cssText = `
1139
- color: var(--fb-error-color);
1140
- font-size: var(--fb-font-size-small);
1141
- margin-top: 0.25rem;
1142
- `;
1143
- const chipAncestor = (_a2 = input.closest) == null ? void 0 : _a2.call(input, ".fb-chip");
1144
- const anchor = chipAncestor || input;
1145
- if (anchor.nextSibling) {
1146
- (_b2 = anchor.parentNode) == null ? void 0 : _b2.insertBefore(errorElement, anchor.nextSibling);
1147
- } else {
1148
- (_c2 = anchor.parentNode) == null ? void 0 : _c2.appendChild(errorElement);
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);
1149
1381
  }
1150
- }
1151
- errorElement.textContent = errorMessage;
1152
- errorElement.style.display = "block";
1153
- } else {
1154
- input.classList.remove("invalid");
1155
- input.title = "";
1156
- if (errorElement) {
1157
- errorElement.remove();
1382
+ } catch {
1383
+ return t("invalidPattern", state);
1158
1384
  }
1159
1385
  }
1386
+ return null;
1160
1387
  };
1161
1388
  const validateTextInput = (input, val, fieldKey) => {
1162
- let hasError = false;
1163
- const { state } = context;
1164
- if (!skipValidation && val) {
1165
- if (element.minLength !== void 0 && element.minLength !== null && val.length < element.minLength) {
1166
- const msg = t("minLength", state, { min: element.minLength });
1167
- errors.push(`${fieldKey}: ${msg}`);
1168
- markValidity(input, msg);
1169
- hasError = true;
1170
- } else if (element.maxLength !== void 0 && element.maxLength !== null && val.length > element.maxLength) {
1171
- const msg = t("maxLength", state, { max: element.maxLength });
1172
- errors.push(`${fieldKey}: ${msg}`);
1173
- markValidity(input, msg);
1174
- hasError = true;
1175
- } else if (element.pattern) {
1176
- try {
1177
- const re = new RegExp(element.pattern);
1178
- if (!re.test(val)) {
1179
- const msg = t("patternMismatch", state);
1180
- errors.push(`${fieldKey}: ${msg}`);
1181
- markValidity(input, msg);
1182
- hasError = true;
1183
- }
1184
- } catch {
1185
- const msg = t("invalidPattern", state);
1186
- errors.push(`${fieldKey}: ${msg}`);
1187
- markValidity(input, msg);
1188
- hasError = true;
1189
- }
1190
- }
1191
- }
1192
- if (!hasError) {
1193
- markValidity(input, null);
1194
- }
1389
+ const msg = lengthOrPatternError(val);
1390
+ if (msg !== null) errors.push(`${fieldKey}: ${msg}`);
1391
+ markFieldValidity(input, msg, context);
1195
1392
  };
1196
1393
  if (element.multiple) {
1197
1394
  const inputs = scopeRoot.querySelectorAll(`[name^="${key}\\["]`);
1198
1395
  const values = [];
1199
- const rawValues = [];
1396
+ let filledCount = 0;
1200
1397
  inputs.forEach((input, index) => {
1201
1398
  var _a2;
1202
1399
  const val = (_a2 = input == null ? void 0 : input.value) != null ? _a2 : "";
1203
- rawValues.push(val);
1204
1400
  values.push(val === "" ? null : val);
1401
+ if (val.trim() !== "") filledCount++;
1205
1402
  validateTextInput(input, val, `${key}[${index}]`);
1206
1403
  });
1207
- if (!skipValidation) {
1208
- const { state } = context;
1209
- const minCount = (_a = element.minCount) != null ? _a : 0;
1210
- const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
1211
- const filteredValues = rawValues.filter((v) => v.trim() !== "");
1212
- if (element.required && filteredValues.length === 0) {
1213
- errors.push(`${key}: ${t("required", state)}`);
1214
- }
1215
- if (filteredValues.length < minCount) {
1216
- errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
1217
- }
1218
- if (filteredValues.length > maxCount) {
1219
- errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
1220
- }
1221
- }
1404
+ validateItemCount(element, key, filledCount, context, errors);
1222
1405
  return { value: values, errors };
1223
1406
  } else {
1224
1407
  const input = scopeRoot.querySelector(`[name="${key}"]`);
1225
- const val = (_c = input == null ? void 0 : input.value) != null ? _c : "";
1226
- if (!skipValidation && element.required && val === "") {
1227
- 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);
1228
1411
  errors.push(`${key}: ${msg}`);
1229
- markValidity(input, msg);
1412
+ markFieldValidity(input, msg, context);
1230
1413
  return { value: null, errors };
1231
1414
  }
1232
1415
  if (input) {
@@ -1248,8 +1431,6 @@ function updateTextField(element, fieldPath, value, context) {
1248
1431
  inputs.forEach((input, index) => {
1249
1432
  if (index < value.length) {
1250
1433
  input.value = value[index] != null ? String(value[index]) : "";
1251
- input.classList.remove("invalid");
1252
- input.title = "";
1253
1434
  clearFieldError(input);
1254
1435
  input.dispatchEvent(new Event("input", { bubbles: true }));
1255
1436
  }
@@ -1263,8 +1444,6 @@ function updateTextField(element, fieldPath, value, context) {
1263
1444
  const input = scopeRoot.querySelector(`[name="${fieldPath}"]`);
1264
1445
  if (input) {
1265
1446
  input.value = value != null ? String(value) : "";
1266
- input.classList.remove("invalid");
1267
- input.title = "";
1268
1447
  clearFieldError(input);
1269
1448
  if (input instanceof HTMLTextAreaElement) {
1270
1449
  input.dispatchEvent(new Event("input", { bubbles: true }));
@@ -1275,7 +1454,7 @@ function updateTextField(element, fieldPath, value, context) {
1275
1454
 
1276
1455
  // src/components/textarea.ts
1277
1456
  function renderTextareaElement(element, ctx, wrapper, pathKey) {
1278
- var _a, _b;
1457
+ var _a, _b, _c, _d;
1279
1458
  const state = ctx.state;
1280
1459
  const readonly = isElementReadonly(element, state, ctx);
1281
1460
  const textareaWrapper = document.createElement("div");
@@ -1289,8 +1468,8 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
1289
1468
  line-height: var(--fb-line-height, 1.5);
1290
1469
  `;
1291
1470
  textareaInput.name = pathKey;
1292
- textareaInput.placeholder = (_a = element.placeholder) != null ? _a : "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442";
1293
- textareaInput.value = ctx.prefill[element.key] || element.default || "";
1471
+ textareaInput.placeholder = (_a = element.placeholder) != null ? _a : t("placeholderText", state);
1472
+ textareaInput.value = (_c = (_b = ctx.prefill[element.key]) != null ? _b : element.default) != null ? _c : "";
1294
1473
  textareaInput.readOnly = readonly;
1295
1474
  if (!readonly && ctx.instance) {
1296
1475
  const handleChange = () => {
@@ -1300,7 +1479,10 @@ function renderTextareaElement(element, ctx, wrapper, pathKey) {
1300
1479
  textareaInput.addEventListener("blur", handleChange);
1301
1480
  textareaInput.addEventListener("input", handleChange);
1302
1481
  }
1303
- applyAutoExpand(textareaInput, { minRows: (_b = element.rows) != null ? _b : 1 });
1482
+ applyAutoExpand(textareaInput, {
1483
+ minRows: (_d = element.rows) != null ? _d : 1,
1484
+ observers: state.autoExpandObservers
1485
+ });
1304
1486
  textareaWrapper.appendChild(textareaInput);
1305
1487
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
1306
1488
  const counter = createCharCounter(element, textareaInput);
@@ -1332,7 +1514,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1332
1514
  });
1333
1515
  }
1334
1516
  function addTextareaItem(value = "", index = -1) {
1335
- var _a2;
1517
+ var _a2, _b2;
1336
1518
  const itemWrapper = document.createElement("div");
1337
1519
  itemWrapper.className = "multiple-textarea-item";
1338
1520
  const textareaContainer = document.createElement("div");
@@ -1345,7 +1527,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1345
1527
  font-family: var(--fb-font-family);
1346
1528
  line-height: var(--fb-line-height, 1.5);
1347
1529
  `;
1348
- textareaInput.placeholder = element.placeholder || t("placeholderText", state);
1530
+ textareaInput.placeholder = (_a2 = element.placeholder) != null ? _a2 : t("placeholderText", state);
1349
1531
  textareaInput.value = value;
1350
1532
  textareaInput.readOnly = readonly;
1351
1533
  if (!readonly && ctx.instance) {
@@ -1356,7 +1538,10 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1356
1538
  textareaInput.addEventListener("blur", handleChange);
1357
1539
  textareaInput.addEventListener("input", handleChange);
1358
1540
  }
1359
- applyAutoExpand(textareaInput, { minRows: (_a2 = element.rows) != null ? _a2 : 1 });
1541
+ applyAutoExpand(textareaInput, {
1542
+ minRows: (_b2 = element.rows) != null ? _b2 : 1,
1543
+ observers: state.autoExpandObservers
1544
+ });
1360
1545
  textareaContainer.appendChild(textareaInput);
1361
1546
  if (!readonly && (element.minLength != null || element.maxLength != null)) {
1362
1547
  const counter = createCharCounter(element, textareaInput);
@@ -1579,7 +1764,19 @@ function createNumberRangeHint(element, input) {
1579
1764
  updateColor();
1580
1765
  return hint;
1581
1766
  }
1767
+ function numberStepAttr(element) {
1768
+ if (element.step !== void 0) return element.step.toString();
1769
+ if (element.decimals !== void 0)
1770
+ return (10 ** -element.decimals).toString();
1771
+ return "any";
1772
+ }
1773
+ function applyDecimalsMarker(input, element) {
1774
+ if (element.decimals !== void 0) {
1775
+ input.setAttribute("data-decimals", String(element.decimals));
1776
+ }
1777
+ }
1582
1778
  function renderNumberElement(element, ctx, wrapper, pathKey) {
1779
+ var _a, _b, _c;
1583
1780
  const state = ctx.state;
1584
1781
  const readonly = isElementReadonly(element, state, ctx);
1585
1782
  const inputWrapper = document.createElement("div");
@@ -1587,11 +1784,12 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1587
1784
  const numberInput = document.createElement("input");
1588
1785
  numberInput.type = "number";
1589
1786
  numberInput.name = pathKey;
1590
- numberInput.placeholder = element.placeholder || "0";
1787
+ numberInput.placeholder = (_a = element.placeholder) != null ? _a : "0";
1591
1788
  if (element.min !== void 0) numberInput.min = element.min.toString();
1592
1789
  if (element.max !== void 0) numberInput.max = element.max.toString();
1593
- if (element.step !== void 0) numberInput.step = element.step.toString();
1594
- numberInput.value = ctx.prefill[element.key] || element.default || "";
1790
+ numberInput.step = numberStepAttr(element);
1791
+ applyDecimalsMarker(numberInput, element);
1792
+ numberInput.value = (_c = (_b = ctx.prefill[element.key]) != null ? _b : element.default) != null ? _c : "";
1595
1793
  numberInput.readOnly = readonly;
1596
1794
  if (!element.stepper) {
1597
1795
  numberInput.className = "w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
@@ -1623,7 +1821,7 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
1623
1821
  wrapper.appendChild(inputWrapper);
1624
1822
  }
1625
1823
  function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1626
- var _a, _b;
1824
+ var _a, _b, _c;
1627
1825
  const state = ctx.state;
1628
1826
  const readonly = isElementReadonly(element, state, ctx);
1629
1827
  const prefillValues = ctx.prefill[element.key] || [];
@@ -1631,7 +1829,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1631
1829
  const minCount = (_a = element.minCount) != null ? _a : element.required ? 1 : 0;
1632
1830
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
1633
1831
  while (values.length < minCount) {
1634
- values.push(element.default || "");
1832
+ values.push((_c = element.default) != null ? _c : "");
1635
1833
  }
1636
1834
  const container = document.createElement("div");
1637
1835
  container.className = "fb-row";
@@ -1646,6 +1844,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1646
1844
  });
1647
1845
  }
1648
1846
  function addNumberItem(value = "", index = -1) {
1847
+ var _a2;
1649
1848
  const itemWrapper = document.createElement("div");
1650
1849
  itemWrapper.className = "multiple-number-item flex items-center gap-2";
1651
1850
  const inputContainer = document.createElement("div");
@@ -1660,10 +1859,11 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1660
1859
  width: 100%;
1661
1860
  box-sizing: border-box;
1662
1861
  `;
1663
- numberInput.placeholder = element.placeholder || "0";
1862
+ numberInput.placeholder = (_a2 = element.placeholder) != null ? _a2 : "0";
1664
1863
  if (element.min !== void 0) numberInput.min = element.min.toString();
1665
1864
  if (element.max !== void 0) numberInput.max = element.max.toString();
1666
- if (element.step !== void 0) numberInput.step = element.step.toString();
1865
+ numberInput.step = numberStepAttr(element);
1866
+ applyDecimalsMarker(numberInput, element);
1667
1867
  numberInput.value = value.toString();
1668
1868
  numberInput.readOnly = readonly;
1669
1869
  if (!readonly && ctx.instance) {
@@ -1728,12 +1928,12 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1728
1928
  const handle = createAddItemRow(
1729
1929
  "number",
1730
1930
  () => {
1731
- var _a2;
1732
- values.push(element.default || "");
1733
- addNumberItem(element.default || "");
1931
+ var _a2, _b2, _c2;
1932
+ values.push((_a2 = element.default) != null ? _a2 : "");
1933
+ addNumberItem((_b2 = element.default) != null ? _b2 : "");
1734
1934
  updateAddButton();
1735
1935
  updateRemoveButtons();
1736
- (_a2 = ctx.instance) == null ? void 0 : _a2.triggerOnChange(pathKey);
1936
+ (_c2 = ctx.instance) == null ? void 0 : _c2.triggerOnChange(pathKey);
1737
1937
  },
1738
1938
  { label: element.addLabel }
1739
1939
  );
@@ -1749,59 +1949,29 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1749
1949
  updateRemoveButtons();
1750
1950
  }
1751
1951
  function validateNumberElement(element, key, context) {
1752
- var _a, _b, _c;
1952
+ var _a;
1753
1953
  const errors = [];
1754
- const { scopeRoot, skipValidation } = context;
1755
- const markValidity = (input, errorMessage) => {
1756
- var _a2, _b2;
1757
- if (!input) return;
1758
- const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
1759
- let errorElement = document.getElementById(errorId);
1760
- if (errorMessage) {
1761
- input.classList.add("invalid");
1762
- input.title = errorMessage;
1763
- if (!errorElement) {
1764
- errorElement = document.createElement("div");
1765
- errorElement.id = errorId;
1766
- errorElement.className = "error-message";
1767
- errorElement.style.cssText = `
1768
- color: var(--fb-error-color);
1769
- font-size: var(--fb-font-size-small);
1770
- margin-top: 0.25rem;
1771
- `;
1772
- if (input.nextSibling) {
1773
- (_a2 = input.parentNode) == null ? void 0 : _a2.insertBefore(errorElement, input.nextSibling);
1774
- } else {
1775
- (_b2 = input.parentNode) == null ? void 0 : _b2.appendChild(errorElement);
1776
- }
1777
- }
1778
- errorElement.textContent = errorMessage;
1779
- errorElement.style.display = "block";
1780
- } else {
1781
- input.classList.remove("invalid");
1782
- input.title = "";
1783
- if (errorElement) {
1784
- errorElement.remove();
1785
- }
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 });
1786
1961
  }
1962
+ return null;
1787
1963
  };
1788
- const validateNumberInput = (input, v, fieldKey) => {
1789
- let hasError = false;
1790
- const { state } = context;
1791
- if (!skipValidation && element.min !== void 0 && element.min !== null && v < element.min) {
1792
- const msg = t("minValue", state, { min: element.min });
1793
- errors.push(`${fieldKey}: ${msg}`);
1794
- markValidity(input, msg);
1795
- hasError = true;
1796
- } else if (!skipValidation && element.max !== void 0 && element.max !== null && v > element.max) {
1797
- const msg = t("maxValue", state, { max: element.max });
1798
- errors.push(`${fieldKey}: ${msg}`);
1799
- markValidity(input, msg);
1800
- hasError = true;
1801
- }
1802
- if (!hasError) {
1803
- markValidity(input, null);
1964
+ const validateNumberInput = (input, fieldKey) => {
1965
+ const raw = input.value;
1966
+ if (raw === "") {
1967
+ markFieldValidity(input, null, context);
1968
+ return null;
1804
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;
1805
1975
  };
1806
1976
  if (element.multiple) {
1807
1977
  const inputs = scopeRoot.querySelectorAll(
@@ -1809,63 +1979,21 @@ function validateNumberElement(element, key, context) {
1809
1979
  );
1810
1980
  const values = [];
1811
1981
  inputs.forEach((input, index) => {
1812
- var _a2;
1813
- const raw = (_a2 = input == null ? void 0 : input.value) != null ? _a2 : "";
1814
- if (raw === "") {
1815
- values.push(null);
1816
- markValidity(input, null);
1817
- return;
1818
- }
1819
- const v = parseFloat(raw);
1820
- if (!skipValidation && !Number.isFinite(v)) {
1821
- const msg = t("notANumber", context.state);
1822
- errors.push(`${key}[${index}]: ${msg}`);
1823
- markValidity(input, msg);
1824
- values.push(null);
1825
- return;
1826
- }
1827
- validateNumberInput(input, v, `${key}[${index}]`);
1828
- values.push(applyDecimals(v, element.decimals));
1982
+ values.push(validateNumberInput(input, `${key}[${index}]`));
1829
1983
  });
1830
- if (!skipValidation) {
1831
- const { state } = context;
1832
- const minCount = (_a = element.minCount) != null ? _a : 0;
1833
- const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
1834
- const filteredValues = values.filter((v) => v !== null);
1835
- if (element.required && filteredValues.length === 0) {
1836
- errors.push(`${key}: ${t("required", state)}`);
1837
- }
1838
- if (filteredValues.length < minCount) {
1839
- errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
1840
- }
1841
- if (filteredValues.length > maxCount) {
1842
- errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
1843
- }
1844
- }
1984
+ const filledCount = values.filter((v) => v !== null).length;
1985
+ validateItemCount(element, key, filledCount, context, errors);
1845
1986
  return { value: values, errors };
1846
1987
  } else {
1847
1988
  const input = scopeRoot.querySelector(`[name="${key}"]`);
1848
- const raw = (_c = input == null ? void 0 : input.value) != null ? _c : "";
1849
- const { state } = context;
1850
- if (!skipValidation && element.required && raw === "") {
1989
+ if (element.required && ((_a = input == null ? void 0 : input.value) != null ? _a : "") === "") {
1851
1990
  const msg = t("required", state);
1852
1991
  errors.push(`${key}: ${msg}`);
1853
- markValidity(input, msg);
1854
- return { value: null, errors };
1855
- }
1856
- if (raw === "") {
1857
- markValidity(input, null);
1858
- return { value: null, errors };
1859
- }
1860
- const v = parseFloat(raw);
1861
- if (!skipValidation && !Number.isFinite(v)) {
1862
- const msg = t("notANumber", state);
1863
- errors.push(`${key}: ${msg}`);
1864
- markValidity(input, msg);
1992
+ markFieldValidity(input, msg, context);
1865
1993
  return { value: null, errors };
1866
1994
  }
1867
- validateNumberInput(input, v, key);
1868
- return { value: applyDecimals(v, element.decimals), errors };
1995
+ if (!input) return { value: null, errors };
1996
+ return { value: validateNumberInput(input, key), errors };
1869
1997
  }
1870
1998
  }
1871
1999
  function applyDecimals(v, decimals) {
@@ -1887,8 +2015,6 @@ function updateNumberField(element, fieldPath, value, context) {
1887
2015
  inputs.forEach((input, index) => {
1888
2016
  if (index < value.length) {
1889
2017
  input.value = value[index] != null ? String(value[index]) : "";
1890
- input.classList.remove("invalid");
1891
- input.title = "";
1892
2018
  clearFieldError(input);
1893
2019
  }
1894
2020
  });
@@ -1903,15 +2029,47 @@ function updateNumberField(element, fieldPath, value, context) {
1903
2029
  );
1904
2030
  if (input) {
1905
2031
  input.value = value != null ? String(value) : "";
1906
- input.classList.remove("invalid");
1907
- input.title = "";
1908
2032
  clearFieldError(input);
1909
2033
  }
1910
2034
  }
1911
2035
  }
1912
2036
 
1913
2037
  // src/components/select.ts
2038
+ function appendSelectOptions(select, element, selectedValue, state) {
2039
+ var _a;
2040
+ const options = element.options || [];
2041
+ if (!options.some((option) => option.value === "")) {
2042
+ const emptyOption = document.createElement("option");
2043
+ emptyOption.value = "";
2044
+ emptyOption.textContent = (_a = element.placeholder) != null ? _a : t("selectPlaceholder", state);
2045
+ select.appendChild(emptyOption);
2046
+ }
2047
+ const strSelected = selectedValue == null ? null : String(selectedValue);
2048
+ let anySelected = false;
2049
+ options.forEach((option) => {
2050
+ const optionEl = document.createElement("option");
2051
+ optionEl.value = option.value;
2052
+ optionEl.textContent = option.label;
2053
+ if (strSelected === option.value) {
2054
+ optionEl.selected = true;
2055
+ anySelected = true;
2056
+ }
2057
+ select.appendChild(optionEl);
2058
+ });
2059
+ if (!anySelected && strSelected !== null && strSelected !== "") {
2060
+ console.warn(
2061
+ `select "${element.key}": prefill value "${strSelected}" is not among the options; leaving the field unselected`
2062
+ );
2063
+ }
2064
+ if (!anySelected) {
2065
+ const empty = Array.from(select.options).find(
2066
+ (option) => option.value === ""
2067
+ );
2068
+ if (empty) empty.selected = true;
2069
+ }
2070
+ }
1914
2071
  function renderSelectElement(element, ctx, wrapper, pathKey) {
2072
+ var _a;
1915
2073
  const state = ctx.state;
1916
2074
  const readonly = isElementReadonly(element, state, ctx);
1917
2075
  const selectInput = document.createElement("select");
@@ -1923,18 +2081,18 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
1923
2081
  `;
1924
2082
  selectInput.name = pathKey;
1925
2083
  selectInput.disabled = readonly;
1926
- (element.options || []).forEach((option) => {
1927
- const optionEl = document.createElement("option");
1928
- optionEl.value = option.value;
1929
- optionEl.textContent = option.label;
1930
- if ((ctx.prefill[element.key] || element.default) === option.value) {
1931
- optionEl.selected = true;
1932
- }
1933
- selectInput.appendChild(optionEl);
1934
- });
2084
+ appendSelectOptions(
2085
+ selectInput,
2086
+ element,
2087
+ (_a = ctx.prefill[element.key]) != null ? _a : element.default,
2088
+ state
2089
+ );
1935
2090
  if (!readonly && ctx.instance) {
1936
2091
  const handleChange = () => {
1937
- ctx.instance.triggerOnChange(pathKey, selectInput.value);
2092
+ ctx.instance.triggerOnChange(
2093
+ pathKey,
2094
+ selectInput.value === "" ? null : selectInput.value
2095
+ );
1938
2096
  };
1939
2097
  selectInput.addEventListener("change", handleChange);
1940
2098
  }
@@ -1947,7 +2105,7 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
1947
2105
  }
1948
2106
  }
1949
2107
  function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1950
- var _a, _b, _c, _d;
2108
+ var _a, _b, _c;
1951
2109
  const state = ctx.state;
1952
2110
  const readonly = isElementReadonly(element, state, ctx);
1953
2111
  const prefillValues = ctx.prefill[element.key] || [];
@@ -1955,7 +2113,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1955
2113
  const minCount = (_a = element.minCount) != null ? _a : element.required ? 1 : 0;
1956
2114
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
1957
2115
  while (values.length < minCount) {
1958
- values.push(element.default || ((_d = (_c = element.options) == null ? void 0 : _c[0]) == null ? void 0 : _d.value) || "");
2116
+ values.push((_c = element.default) != null ? _c : "");
1959
2117
  }
1960
2118
  const container = document.createElement("div");
1961
2119
  container.className = "fb-row";
@@ -1980,15 +2138,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1980
2138
  font-family: var(--fb-font-family);
1981
2139
  `;
1982
2140
  selectInput.disabled = readonly;
1983
- (element.options || []).forEach((option) => {
1984
- const optionElement = document.createElement("option");
1985
- optionElement.value = option.value;
1986
- optionElement.textContent = option.label;
1987
- if (value === option.value) {
1988
- optionElement.selected = true;
1989
- }
1990
- selectInput.appendChild(optionElement);
1991
- });
2141
+ appendSelectOptions(selectInput, element, value, state);
1992
2142
  if (!readonly && ctx.instance) {
1993
2143
  const handleChange = () => {
1994
2144
  ctx.instance.triggerOnChange(selectInput.name, selectInput.value);
@@ -2042,13 +2192,13 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
2042
2192
  const handle = createAddItemRow(
2043
2193
  "select",
2044
2194
  () => {
2045
- var _a2, _b2, _c2;
2046
- const defaultValue = element.default || ((_b2 = (_a2 = element.options) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.value) || "";
2195
+ var _a2, _b2;
2196
+ const defaultValue = (_a2 = element.default) != null ? _a2 : "";
2047
2197
  values.push(defaultValue);
2048
2198
  addSelectItem(defaultValue);
2049
2199
  updateAddButton();
2050
2200
  updateRemoveButtons();
2051
- (_c2 = ctx.instance) == null ? void 0 : _c2.triggerOnChange(pathKey);
2201
+ (_b2 = ctx.instance) == null ? void 0 : _b2.triggerOnChange(pathKey);
2052
2202
  },
2053
2203
  { label: element.addLabel }
2054
2204
  );
@@ -2072,57 +2222,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
2072
2222
  function validateSelectElement(element, key, context) {
2073
2223
  var _a;
2074
2224
  const errors = [];
2075
- const { scopeRoot, skipValidation } = context;
2076
- const markValidity = (input, errorMessage) => {
2077
- var _a2, _b;
2078
- if (!input) return;
2079
- const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
2080
- let errorElement = document.getElementById(errorId);
2081
- if (errorMessage) {
2082
- input.classList.add("invalid");
2083
- input.title = errorMessage;
2084
- if (!errorElement) {
2085
- errorElement = document.createElement("div");
2086
- errorElement.id = errorId;
2087
- errorElement.className = "error-message";
2088
- errorElement.style.cssText = `
2089
- color: var(--fb-error-color);
2090
- font-size: var(--fb-font-size-small);
2091
- margin-top: 0.25rem;
2092
- `;
2093
- if (input.nextSibling) {
2094
- (_a2 = input.parentNode) == null ? void 0 : _a2.insertBefore(errorElement, input.nextSibling);
2095
- } else {
2096
- (_b = input.parentNode) == null ? void 0 : _b.appendChild(errorElement);
2097
- }
2098
- }
2099
- errorElement.textContent = errorMessage;
2100
- errorElement.style.display = "block";
2101
- } else {
2102
- input.classList.remove("invalid");
2103
- input.title = "";
2104
- if (errorElement) {
2105
- errorElement.remove();
2106
- }
2107
- }
2108
- };
2109
- const validateMultipleCount = (key2, values, element2, filterFn) => {
2110
- var _a2, _b;
2111
- if (skipValidation) return;
2112
- const { state } = context;
2113
- const filteredValues = values.filter(filterFn);
2114
- const minCount = "minCount" in element2 ? (_a2 = element2.minCount) != null ? _a2 : 0 : 0;
2115
- const maxCount = "maxCount" in element2 ? (_b = element2.maxCount) != null ? _b : Infinity : Infinity;
2116
- if (element2.required && filteredValues.length === 0) {
2117
- errors.push(`${key2}: ${t("required", state)}`);
2118
- }
2119
- if (filteredValues.length < minCount) {
2120
- errors.push(`${key2}: ${t("minItems", state, { min: minCount })}`);
2121
- }
2122
- if (filteredValues.length > maxCount) {
2123
- errors.push(`${key2}: ${t("maxItems", state, { max: maxCount })}`);
2124
- }
2125
- };
2225
+ const { scopeRoot } = context;
2126
2226
  if ("multiple" in element && element.multiple) {
2127
2227
  const inputs = scopeRoot.querySelectorAll(
2128
2228
  `[name^="${key}\\["]`
@@ -2131,27 +2231,36 @@ function validateSelectElement(element, key, context) {
2131
2231
  inputs.forEach((input) => {
2132
2232
  var _a2;
2133
2233
  const val = (_a2 = input == null ? void 0 : input.value) != null ? _a2 : "";
2134
- values.push(val);
2135
- markValidity(input, null);
2234
+ values.push(val === "" ? null : val);
2235
+ markFieldValidity(input, null, context);
2136
2236
  });
2137
- validateMultipleCount(key, values, element, (v) => v !== "");
2237
+ const filledCount = values.filter((v) => v != null).length;
2238
+ validateItemCount(element, key, filledCount, context, errors);
2138
2239
  return { value: values, errors };
2139
2240
  } else {
2140
- const input = scopeRoot.querySelector(
2141
- `[name="${key}"]`
2142
- );
2241
+ const input = scopeRoot.querySelector(`[name="${key}"]`);
2143
2242
  const val = (_a = input == null ? void 0 : input.value) != null ? _a : "";
2144
- if (!skipValidation && element.required && val === "") {
2243
+ if (element.required && val === "") {
2145
2244
  const msg = t("required", context.state);
2146
2245
  errors.push(`${key}: ${msg}`);
2147
- markValidity(input, msg);
2246
+ markFieldValidity(input, msg, context);
2148
2247
  return { value: null, errors };
2149
- } else {
2150
- markValidity(input, null);
2151
2248
  }
2249
+ markFieldValidity(input, null, context);
2152
2250
  return { value: val === "" ? null : val, errors };
2153
2251
  }
2154
2252
  }
2253
+ function assertValueInOptions(select, strValue, fieldPath) {
2254
+ if (strValue === "") return;
2255
+ const match = Array.from(select.options).some(
2256
+ (option) => option.value === strValue
2257
+ );
2258
+ if (!match) {
2259
+ throw new Error(
2260
+ `updateSelectField: value "${strValue}" is not among the options of "${fieldPath}"`
2261
+ );
2262
+ }
2263
+ }
2155
2264
  function updateSelectField(element, fieldPath, value, context) {
2156
2265
  const { scopeRoot } = context;
2157
2266
  if ("multiple" in element && element.multiple) {
@@ -2166,13 +2275,18 @@ function updateSelectField(element, fieldPath, value, context) {
2166
2275
  );
2167
2276
  selects.forEach((select, index) => {
2168
2277
  if (index < value.length) {
2169
- select.value = value[index] != null ? String(value[index]) : "";
2278
+ const strValue = value[index] != null ? String(value[index]) : "";
2279
+ assertValueInOptions(select, strValue, `${fieldPath}[${index}]`);
2280
+ }
2281
+ });
2282
+ selects.forEach((select, index) => {
2283
+ if (index < value.length) {
2284
+ const strValue = value[index] != null ? String(value[index]) : "";
2285
+ select.value = strValue;
2170
2286
  const options = select.querySelectorAll("option");
2171
2287
  options.forEach((option) => {
2172
- option.selected = option.value === String(value[index]);
2288
+ option.selected = option.value === strValue;
2173
2289
  });
2174
- select.classList.remove("invalid");
2175
- select.title = "";
2176
2290
  clearFieldError(select);
2177
2291
  }
2178
2292
  });
@@ -2186,13 +2300,13 @@ function updateSelectField(element, fieldPath, value, context) {
2186
2300
  `[name="${fieldPath}"]`
2187
2301
  );
2188
2302
  if (select) {
2189
- select.value = value != null ? String(value) : "";
2303
+ const strValue = value != null ? String(value) : "";
2304
+ assertValueInOptions(select, strValue, fieldPath);
2305
+ select.value = strValue;
2190
2306
  const options = select.querySelectorAll("option");
2191
2307
  options.forEach((option) => {
2192
- option.selected = option.value === String(value);
2308
+ option.selected = option.value === strValue;
2193
2309
  });
2194
- select.classList.remove("invalid");
2195
- select.title = "";
2196
2310
  clearFieldError(select);
2197
2311
  }
2198
2312
  }
@@ -2387,14 +2501,14 @@ function renderSwitcherElement(element, ctx, wrapper, pathKey) {
2387
2501
  }
2388
2502
  }
2389
2503
  function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
2390
- var _a, _b, _c, _d;
2504
+ var _a, _b, _c;
2391
2505
  const state = ctx.state;
2392
2506
  const prefillValues = ctx.prefill[element.key] || [];
2393
2507
  const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
2394
2508
  const minCount = (_a = element.minCount) != null ? _a : element.required ? 1 : 0;
2395
2509
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
2396
2510
  while (values.length < minCount) {
2397
- values.push(element.default || ((_d = (_c = element.options) == null ? void 0 : _c[0]) == null ? void 0 : _d.value) || "");
2511
+ values.push((_c = element.default) != null ? _c : "");
2398
2512
  }
2399
2513
  const readonly = isElementReadonly(element, state, ctx);
2400
2514
  const container = document.createElement("div");
@@ -2489,13 +2603,13 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
2489
2603
  const handle = createAddItemRow(
2490
2604
  "switcher",
2491
2605
  () => {
2492
- var _a2, _b2, _c2;
2493
- const defaultValue = element.default || ((_b2 = (_a2 = element.options) == null ? void 0 : _a2[0]) == null ? void 0 : _b2.value) || "";
2606
+ var _a2, _b2;
2607
+ const defaultValue = (_a2 = element.default) != null ? _a2 : "";
2494
2608
  values.push(defaultValue);
2495
2609
  addSwitcherItem(defaultValue);
2496
2610
  updateAddButton();
2497
2611
  updateRemoveButtons();
2498
- (_c2 = ctx.instance) == null ? void 0 : _c2.triggerOnChange(pathKey);
2612
+ (_b2 = ctx.instance) == null ? void 0 : _b2.triggerOnChange(pathKey);
2499
2613
  },
2500
2614
  { label: element.addLabel }
2501
2615
  );
@@ -2519,60 +2633,11 @@ function renderMultipleSwitcherElement(element, ctx, wrapper, pathKey) {
2519
2633
  function validateSwitcherElement(element, key, context) {
2520
2634
  var _a;
2521
2635
  const errors = [];
2522
- const { scopeRoot, skipValidation } = context;
2523
- const markValidity = (input, errorMessage) => {
2524
- var _a2, _b;
2525
- if (!input) return;
2526
- const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
2527
- let errorElement = document.getElementById(errorId);
2528
- if (errorMessage) {
2529
- input.classList.add("invalid");
2530
- input.title = errorMessage;
2531
- if (!errorElement) {
2532
- errorElement = document.createElement("div");
2533
- errorElement.id = errorId;
2534
- errorElement.className = "error-message";
2535
- errorElement.style.cssText = `
2536
- color: var(--fb-error-color);
2537
- font-size: var(--fb-font-size-small);
2538
- margin-top: 0.25rem;
2539
- `;
2540
- if (input.nextSibling) {
2541
- (_a2 = input.parentNode) == null ? void 0 : _a2.insertBefore(errorElement, input.nextSibling);
2542
- } else {
2543
- (_b = input.parentNode) == null ? void 0 : _b.appendChild(errorElement);
2544
- }
2545
- }
2546
- errorElement.textContent = errorMessage;
2547
- errorElement.style.display = "block";
2548
- } else {
2549
- input.classList.remove("invalid");
2550
- input.title = "";
2551
- if (errorElement) {
2552
- errorElement.remove();
2553
- }
2554
- }
2555
- };
2556
- const validateMultipleCount = (fieldKey, values, el, filterFn) => {
2557
- var _a2, _b;
2558
- if (skipValidation) return;
2559
- const { state } = context;
2560
- const filteredValues = values.filter(filterFn);
2561
- const minCount = "minCount" in el ? (_a2 = el.minCount) != null ? _a2 : 0 : 0;
2562
- const maxCount = "maxCount" in el ? (_b = el.maxCount) != null ? _b : Infinity : Infinity;
2563
- if (el.required && filteredValues.length === 0) {
2564
- errors.push(`${fieldKey}: ${t("required", state)}`);
2565
- }
2566
- if (filteredValues.length < minCount) {
2567
- errors.push(`${fieldKey}: ${t("minItems", state, { min: minCount })}`);
2568
- }
2569
- if (filteredValues.length > maxCount) {
2570
- errors.push(`${fieldKey}: ${t("maxItems", state, { max: maxCount })}`);
2571
- }
2572
- };
2636
+ const { scopeRoot, state } = context;
2573
2637
  const validOptionValues = new Set(
2574
2638
  "options" in element ? element.options.map((o) => o.value) : []
2575
2639
  );
2640
+ const optionError = (val) => val !== "" && !validOptionValues.has(val) ? t("invalidOption", state) : null;
2576
2641
  if ("multiple" in element && element.multiple) {
2577
2642
  const inputs = scopeRoot.querySelectorAll(
2578
2643
  `input[type="hidden"][name^="${key}\\["]`
@@ -2581,38 +2646,38 @@ function validateSwitcherElement(element, key, context) {
2581
2646
  inputs.forEach((input) => {
2582
2647
  var _a2;
2583
2648
  const val = (_a2 = input == null ? void 0 : input.value) != null ? _a2 : "";
2584
- values.push(val);
2585
- if (!skipValidation && val !== "" && !validOptionValues.has(val)) {
2586
- const msg = t("invalidOption", context.state);
2587
- markValidity(input, msg);
2588
- errors.push(`${key}: ${msg}`);
2589
- } else {
2590
- markValidity(input, null);
2591
- }
2649
+ values.push(val === "" ? null : val);
2650
+ const msg = optionError(val);
2651
+ if (msg !== null) errors.push(`${key}: ${msg}`);
2652
+ markFieldValidity(switcherGroupOf(input), msg, context);
2592
2653
  });
2593
- validateMultipleCount(key, values, element, (v) => v !== "");
2654
+ const filledCount = values.filter((v) => v != null).length;
2655
+ validateItemCount(element, key, filledCount, context, errors);
2594
2656
  return { value: values, errors };
2595
2657
  } else {
2596
2658
  const input = scopeRoot.querySelector(
2597
2659
  `input[type="hidden"][name="${key}"]`
2598
2660
  );
2599
2661
  const val = (_a = input == null ? void 0 : input.value) != null ? _a : "";
2600
- if (!skipValidation && element.required && val === "") {
2601
- const msg = t("required", 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) {
2602
2665
  errors.push(`${key}: ${msg}`);
2603
- markValidity(input, msg);
2604
2666
  return { value: null, errors };
2605
2667
  }
2606
- if (!skipValidation && val !== "" && !validOptionValues.has(val)) {
2607
- const msg = t("invalidOption", context.state);
2608
- errors.push(`${key}: ${msg}`);
2609
- markValidity(input, msg);
2610
- return { value: null, errors };
2611
- }
2612
- markValidity(input, null);
2613
2668
  return { value: val === "" ? null : val, errors };
2614
2669
  }
2615
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
+ }
2616
2681
  function updateSwitcherField(element, fieldPath, value, context) {
2617
2682
  var _a;
2618
2683
  const { scopeRoot } = context;
@@ -2642,9 +2707,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
2642
2707
  }
2643
2708
  });
2644
2709
  }
2645
- input.classList.remove("invalid");
2646
- input.title = "";
2647
- clearFieldError(input);
2710
+ clearFieldError(switcherGroupOf(input));
2648
2711
  }
2649
2712
  });
2650
2713
  if (value.length !== inputs.length) {
@@ -2670,9 +2733,7 @@ function updateSwitcherField(element, fieldPath, value, context) {
2670
2733
  }
2671
2734
  });
2672
2735
  }
2673
- input.classList.remove("invalid");
2674
- input.title = "";
2675
- clearFieldError(input);
2736
+ clearFieldError(switcherGroupOf(input));
2676
2737
  }
2677
2738
  }
2678
2739
  }
@@ -2784,6 +2845,7 @@ function renderBooleanElement(element, ctx, wrapper, pathKey) {
2784
2845
  const hiddenInput = document.createElement("input");
2785
2846
  hiddenInput.type = "hidden";
2786
2847
  hiddenInput.name = pathKey;
2848
+ hiddenInput.setAttribute("data-boolean-field", "true");
2787
2849
  hiddenInput.value = initial ? "true" : "false";
2788
2850
  const row = document.createElement("div");
2789
2851
  row.className = "fb-toggle-row";
@@ -3199,14 +3261,20 @@ function ensureFileStyles() {
3199
3261
  padding: 6px;
3200
3262
  }
3201
3263
 
3202
- /* \u2500\u2500\u2500 Clear-all row below multi grid \u2500\u2500\u2500 */
3203
- .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 {
3204
3266
  margin-top: 10px;
3205
3267
  display: flex;
3206
3268
  align-items: center;
3207
- 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;
3208
3275
  }
3209
3276
  .fb-clear-all-btn {
3277
+ margin-left: auto;
3210
3278
  font-size: 12px;
3211
3279
  color: #94a3b8;
3212
3280
  background: none;
@@ -3456,24 +3524,55 @@ function createFileTile() {
3456
3524
  tile.className = "fb-tile";
3457
3525
  return tile;
3458
3526
  }
3459
- function showFileError(container, message) {
3460
- var _a, _b;
3461
- const existing = (_a = container.closest("[data-files-wrapper]")) == null ? void 0 : _a.querySelector(".file-error-message");
3462
- if (existing) existing.remove();
3463
- const errorEl = document.createElement("div");
3464
- errorEl.className = "file-error-message error-message";
3465
- errorEl.style.cssText = `
3466
- color: var(--fb-error-color);
3467
- font-size: var(--fb-font-size-small);
3468
- margin-top: 0.25rem;
3469
- `;
3470
- errorEl.textContent = message;
3471
- (_b = container.closest("[data-files-wrapper]")) == null ? void 0 : _b.appendChild(errorEl);
3527
+ function fileErrorSlot(container) {
3528
+ var _a;
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
+ }
3472
3553
  }
3473
- function clearFileError(container) {
3474
- var _a;
3475
- const existing = (_a = container.closest("[data-files-wrapper]")) == null ? void 0 : _a.querySelector(".file-error-message");
3476
- if (existing) existing.remove();
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");
3477
3576
  }
3478
3577
  function addDeleteButton(container, state, onDelete) {
3479
3578
  const existingOverlay = container.querySelector(".delete-overlay");
@@ -4343,7 +4442,8 @@ async function handleFileSelect(opts) {
4343
4442
  const formats = allowedExtensions.join(", ");
4344
4443
  showFileError(
4345
4444
  container,
4346
- t("invalidFileExtension", state, { name: file.name, formats })
4445
+ t("invalidFileExtension", state, { name: file.name, formats }),
4446
+ state
4347
4447
  );
4348
4448
  return;
4349
4449
  }
@@ -4351,14 +4451,16 @@ async function handleFileSelect(opts) {
4351
4451
  const mimes = allowedMimes.join(", ");
4352
4452
  showFileError(
4353
4453
  container,
4354
- t("invalidFileMime", state, { name: file.name, type: file.type, mimes })
4454
+ t("invalidFileMime", state, { name: file.name, type: file.type, mimes }),
4455
+ state
4355
4456
  );
4356
4457
  return;
4357
4458
  }
4358
4459
  if (!isFileSizeAllowed(file, maxSizeMB)) {
4359
4460
  showFileError(
4360
4461
  container,
4361
- t("fileTooLarge", state, { name: file.name, maxSize: maxSizeMB })
4462
+ t("fileTooLarge", state, { name: file.name, maxSize: maxSizeMB }),
4463
+ state
4362
4464
  );
4363
4465
  return;
4364
4466
  }
@@ -4555,7 +4657,7 @@ async function runMultiFileBatch(opts, files, listEl, errorTarget) {
4555
4657
  state
4556
4658
  );
4557
4659
  if (errorTarget) {
4558
- if (errorMessage) showFileError(errorTarget, errorMessage);
4660
+ if (errorMessage) showFileError(errorTarget, errorMessage, state);
4559
4661
  else clearFileError(errorTarget);
4560
4662
  }
4561
4663
  const handle = coordinator.beginBatch(accepted.length);
@@ -4574,11 +4676,8 @@ async function runMultiFileBatch(opts, files, listEl, errorTarget) {
4574
4676
  }
4575
4677
  const { wasLast } = handle.end();
4576
4678
  if (wasLast) updateCallback();
4577
- if (errorTarget) {
4578
- const combined = buildBatchErrorMessage(errorMessage, failures, state);
4579
- if (combined) showFileError(errorTarget, combined);
4580
- else clearFileError(errorTarget);
4581
- }
4679
+ const combined = buildBatchErrorMessage(errorMessage, failures, state);
4680
+ if (errorTarget && combined) showFileError(errorTarget, combined, state);
4582
4681
  }
4583
4682
  function setupFilesDropHandler(opts) {
4584
4683
  const { filesContainer } = opts;
@@ -4693,7 +4792,7 @@ async function handleLibraryPickMulti(opts) {
4693
4792
  selectedResourceIds: knownRids
4694
4793
  });
4695
4794
  } catch (error) {
4696
- showFileError(wrapper, extractPickerError(error, state));
4795
+ showFileError(wrapper, extractPickerError(error, state), state);
4697
4796
  return;
4698
4797
  }
4699
4798
  if (picked.length === 0) return;
@@ -4721,7 +4820,8 @@ async function handleLibraryPickMulti(opts) {
4721
4820
  if (skipped > 0) {
4722
4821
  showFileError(
4723
4822
  wrapper,
4724
- t("filesLimitExceeded", state, { skipped, max: maxCount })
4823
+ t("filesLimitExceeded", state, { skipped, max: maxCount }),
4824
+ state
4725
4825
  );
4726
4826
  }
4727
4827
  return;
@@ -4730,7 +4830,8 @@ async function handleLibraryPickMulti(opts) {
4730
4830
  if (skipped > 0) {
4731
4831
  showFileError(
4732
4832
  wrapper,
4733
- t("filesLimitExceeded", state, { skipped, max: maxCount })
4833
+ t("filesLimitExceeded", state, { skipped, max: maxCount }),
4834
+ state
4734
4835
  );
4735
4836
  }
4736
4837
  for (const resource of accepted) {
@@ -4768,7 +4869,7 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
4768
4869
  selectedResourceIds: []
4769
4870
  });
4770
4871
  } catch (error) {
4771
- showFileError(container, extractPickerError(error, state));
4872
+ showFileError(container, extractPickerError(error, state), state);
4772
4873
  return;
4773
4874
  }
4774
4875
  if (picked.length === 0) return;
@@ -4781,7 +4882,7 @@ async function handleLibraryPickSingle(state, element, container, fileWrapper, p
4781
4882
  state
4782
4883
  );
4783
4884
  if (validationError !== null) {
4784
- showFileError(container, validationError);
4885
+ showFileError(container, validationError, state);
4785
4886
  return;
4786
4887
  }
4787
4888
  clearFileError(container);
@@ -5024,10 +5125,7 @@ function buildPlaceholderTile(isDragOver = false) {
5024
5125
  div.className = `fb-multi-placeholder fb-checker${isDragOver ? " fb-drag-over" : ""}`;
5025
5126
  return div;
5026
5127
  }
5027
- function buildClearAllRow(state, ridCount, onClearAll) {
5028
- if (ridCount <= 1) return null;
5029
- const row = document.createElement("div");
5030
- row.className = "fb-clear-all-row";
5128
+ function buildClearAllButton(state, onClearAll) {
5031
5129
  const clearBtn = document.createElement("button");
5032
5130
  clearBtn.type = "button";
5033
5131
  clearBtn.className = "fb-clear-all-btn";
@@ -5038,9 +5136,38 @@ function buildClearAllRow(state, ridCount, onClearAll) {
5038
5136
  onClearAll();
5039
5137
  }
5040
5138
  };
5041
- 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));
5042
5157
  return row;
5043
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
+ }
5044
5171
  var gridResizeObservers = /* @__PURE__ */ new WeakMap();
5045
5172
  var gridMeasureFrames = /* @__PURE__ */ new WeakMap();
5046
5173
  function cancelPendingMeasure(container) {
@@ -5136,6 +5263,7 @@ function renderResourcePills(opts) {
5136
5263
  grid2.appendChild(tile);
5137
5264
  }
5138
5265
  }
5266
+ clearFileError(container, "limit");
5139
5267
  return;
5140
5268
  }
5141
5269
  const outerDiv = document.createElement("div");
@@ -5215,10 +5343,14 @@ function renderResourcePills(opts) {
5215
5343
  }
5216
5344
  }
5217
5345
  });
5218
- if (onClearAll) {
5219
- const row = buildClearAllRow(state, ridList.length, onClearAll);
5220
- if (row) container.appendChild(row);
5221
- }
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);
5222
5354
  }
5223
5355
  function renderFileElementEdit(element, ctx, wrapper, pathKey) {
5224
5356
  var _a, _b;
@@ -5371,9 +5503,10 @@ function buildAcceptAttribute(accept) {
5371
5503
  ...(_c = accept.mime) != null ? _c : []
5372
5504
  ].join(",");
5373
5505
  }
5374
- function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
5375
- var _a, _b;
5506
+ function renderMultiFileElementEdit(element, ctx, wrapper, pathKey) {
5507
+ var _a, _b, _c;
5376
5508
  const state = ctx.state;
5509
+ const maxFiles = (_a = element.maxCount) != null ? _a : Infinity;
5377
5510
  const filesWrapper = document.createElement("div");
5378
5511
  filesWrapper.className = "fb-row";
5379
5512
  filesWrapper.dataset.filesWrapper = pathKey;
@@ -5399,7 +5532,7 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
5399
5532
  allowedMimes: getAllowedMimes(element.accept),
5400
5533
  // Prefer schema's `maxSize`; fall back to legacy `maxSizeMB` for
5401
5534
  // backward compatibility (matches addFileSizeHint in validation.ts).
5402
- 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
5403
5536
  };
5404
5537
  const openPicker = () => {
5405
5538
  filesPicker.click();
@@ -5542,7 +5675,7 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
5542
5675
  state,
5543
5676
  !currentlyReadonly,
5544
5677
  currentlyReadonly ? null : () => {
5545
- var _a2, _b2, _c;
5678
+ var _a2, _b2, _c2;
5546
5679
  releaseLocalFileUrl((_a2 = state.resourceIndex.get(rid)) == null ? void 0 : _a2.file);
5547
5680
  const idx = initialFiles.indexOf(rid);
5548
5681
  if (idx > -1) {
@@ -5560,7 +5693,7 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
5560
5693
  return;
5561
5694
  }
5562
5695
  pendingRemovals.add(rid);
5563
- (_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();
5564
5697
  if (ctx.instance && pathKey && !state.config.readonly) {
5565
5698
  ctx.instance.triggerOnChange(pathKey, initialFiles);
5566
5699
  }
@@ -5581,22 +5714,24 @@ function setupMultiFileEditMode(element, ctx, wrapper, pathKey, maxFiles) {
5581
5714
  };
5582
5715
  setupFilesDropHandler({ ...sharedHandlerOpts, filesContainer });
5583
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
+ });
5584
5732
  updateFilesDisplay();
5585
5733
  wrapper.appendChild(filesWrapper);
5586
5734
  }
5587
- function renderFilesElementEdit(element, ctx, wrapper, pathKey) {
5588
- setupMultiFileEditMode(element, ctx, wrapper, pathKey, Infinity);
5589
- }
5590
- function renderMultipleFileElementEdit(element, ctx, wrapper, pathKey) {
5591
- var _a;
5592
- setupMultiFileEditMode(
5593
- element,
5594
- ctx,
5595
- wrapper,
5596
- pathKey,
5597
- (_a = element.maxCount) != null ? _a : Infinity
5598
- );
5599
- }
5600
5735
 
5601
5736
  // src/components/file/validate.ts
5602
5737
  function readMultiFileResourceIds(scopeRoot, fullKey) {
@@ -5618,90 +5753,103 @@ function readMultiFileResourceIds(scopeRoot, fullKey) {
5618
5753
  }
5619
5754
  return parsed;
5620
5755
  }
5621
- function validateFileCount(key, resourceIds, element, state, errors) {
5622
- var _a, _b;
5623
- const minFiles = "minCount" in element ? (_a = element.minCount) != null ? _a : 0 : 0;
5624
- const maxFiles = "maxCount" in element ? (_b = element.maxCount) != null ? _b : Infinity : Infinity;
5625
- if (element.required && resourceIds.length === 0) {
5626
- errors.push(`${key}: ${t("required", state)}`);
5627
- }
5628
- if (resourceIds.length < minFiles) {
5629
- errors.push(`${key}: ${t("minFiles", state, { min: minFiles })}`);
5630
- }
5631
- if (resourceIds.length > maxFiles) {
5632
- errors.push(`${key}: ${t("maxFiles", state, { max: maxFiles })}`);
5633
- }
5634
- }
5635
- function validateFileTypes(key, resourceIds, element, state, errors) {
5756
+ function validateFileTypes(resourceIds, element, state) {
5636
5757
  var _a, _b;
5758
+ const messages = [];
5637
5759
  const acceptField = "accept" in element ? element.accept : void 0;
5638
5760
  const allowedExtensions = getAllowedExtensions(acceptField);
5639
5761
  const allowedMimes = getAllowedMimes(acceptField);
5640
- if (allowedExtensions.length === 0 && allowedMimes.length === 0) return;
5762
+ if (allowedExtensions.length === 0 && allowedMimes.length === 0) {
5763
+ return messages;
5764
+ }
5641
5765
  const formats = allowedExtensions.join(", ");
5642
5766
  const mimes = allowedMimes.join(", ");
5643
5767
  for (const rid of resourceIds) {
5644
5768
  const meta = state.resourceIndex.get(rid);
5645
5769
  const fileName = (_a = meta == null ? void 0 : meta.name) != null ? _a : rid;
5646
5770
  if (allowedExtensions.length > 0 && !isFileExtensionAllowed(fileName, allowedExtensions)) {
5647
- errors.push(
5648
- `${key}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
5771
+ messages.push(
5772
+ t("invalidFileExtension", state, { name: fileName, formats })
5649
5773
  );
5650
5774
  continue;
5651
5775
  }
5652
5776
  if (allowedMimes.length > 0 && !(meta == null ? void 0 : meta.inferredFromExtension)) {
5653
5777
  const mimeType = (_b = meta == null ? void 0 : meta.type) != null ? _b : "";
5654
5778
  if (!isMimeAllowed(mimeType, allowedMimes)) {
5655
- errors.push(
5656
- `${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
+ })
5657
5785
  );
5658
5786
  }
5659
5787
  }
5660
5788
  }
5789
+ return messages;
5661
5790
  }
5662
- function validateFileSizes(key, resourceIds, element, state, errors) {
5791
+ function validateFileSizes(resourceIds, element, state) {
5663
5792
  var _a;
5793
+ const messages = [];
5664
5794
  const maxSizeMB = "maxSize" in element ? (_a = element.maxSize) != null ? _a : Infinity : Infinity;
5665
- if (maxSizeMB === Infinity) return;
5795
+ if (maxSizeMB === Infinity) return messages;
5666
5796
  for (const rid of resourceIds) {
5667
5797
  const meta = state.resourceIndex.get(rid);
5668
5798
  if (!meta) continue;
5669
5799
  if (meta.size > maxSizeMB * 1024 * 1024) {
5670
- errors.push(
5671
- `${key}: ${t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })}`
5800
+ messages.push(
5801
+ t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })
5672
5802
  );
5673
5803
  }
5674
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}`);
5675
5815
  }
5676
5816
  function validateMultiFile(element, key, context) {
5677
- const { scopeRoot, skipValidation, path, state } = context;
5678
- const errors = [];
5817
+ const { scopeRoot, path, state } = context;
5679
5818
  const fullKey = pathJoin(path, key);
5680
5819
  const resourceIds = readMultiFileResourceIds(scopeRoot, fullKey);
5681
- if (!skipValidation) {
5682
- validateFileCount(key, resourceIds, element, state, errors);
5683
- validateFileTypes(key, resourceIds, element, state, errors);
5684
- validateFileSizes(key, resourceIds, element, state, errors);
5685
- }
5686
- 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
+ };
5687
5832
  }
5688
5833
  function validateSingleFile(element, key, context) {
5689
5834
  var _a;
5690
- const { scopeRoot, skipValidation, state } = context;
5691
- const errors = [];
5835
+ const { scopeRoot, state } = context;
5692
5836
  const input = scopeRoot.querySelector(
5693
5837
  `input[name="${key}"][type="hidden"]`
5694
5838
  );
5695
5839
  const rid = (_a = input == null ? void 0 : input.value) != null ? _a : "";
5696
- if (!skipValidation && element.required && rid === "") {
5697
- errors.push(`${key}: ${t("required", state)}`);
5698
- return { value: null, errors };
5699
- }
5700
- if (!skipValidation && rid !== "") {
5701
- validateFileTypes(key, [rid], element, state, errors);
5702
- 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
+ ];
5703
5848
  }
5704
- return { value: rid || null, errors };
5849
+ return {
5850
+ value: rid || null,
5851
+ errors: reportFileMessages(scopeRoot, key, key, messages, context)
5852
+ };
5705
5853
  }
5706
5854
  function validateFileElement(element, key, context) {
5707
5855
  const isMultipleField = element.type === "files" || "multiple" in element && Boolean(element.multiple);
@@ -5751,12 +5899,20 @@ function buildEmptyReadonlyTile(state) {
5751
5899
  return emptyState;
5752
5900
  }
5753
5901
  function renderMultiFileReadonly(rids, state, wrapper, pathKey, _marginTop) {
5754
- addPrefillFilesToIndex(rids, state.resourceIndex);
5755
- ensureFileStyles();
5756
5902
  const filesWrapper = document.createElement("div");
5757
5903
  filesWrapper.dataset.filesWrapper = pathKey;
5758
- filesWrapper.dataset.resourceIds = JSON.stringify(rids);
5759
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();
5760
5916
  if (rids.length === 0) {
5761
5917
  const emptyEl = document.createElement("div");
5762
5918
  emptyEl.className = "fb-tile-empty-text";
@@ -5816,14 +5972,14 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
5816
5972
  if (isElementReadonly(element, ctx.state, ctx)) {
5817
5973
  renderFilesElementReadonly(element, ctx, wrapper, pathKey);
5818
5974
  } else {
5819
- renderFilesElementEdit(element, ctx, wrapper, pathKey);
5975
+ renderMultiFileElementEdit(element, ctx, wrapper, pathKey);
5820
5976
  }
5821
5977
  }
5822
5978
  function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
5823
5979
  if (isElementReadonly(element, ctx.state, ctx)) {
5824
5980
  renderMultipleFileElementReadonly(element, ctx, wrapper, pathKey);
5825
5981
  } else {
5826
- renderMultipleFileElementEdit(element, ctx, wrapper, pathKey);
5982
+ renderMultiFileElementEdit(element, ctx, wrapper, pathKey);
5827
5983
  }
5828
5984
  }
5829
5985
  function updateFileField(element, fieldPath, value, context) {
@@ -5843,13 +5999,19 @@ function updateFileField(element, fieldPath, value, context) {
5843
5999
  const filesWrapper = scopeRoot.querySelector(
5844
6000
  `[data-files-wrapper="${fieldPath}"]`
5845
6001
  );
5846
- if (filesWrapper) {
5847
- filesWrapper.dataset.resourceIds = JSON.stringify(value);
5848
- } else {
6002
+ if (!filesWrapper) {
5849
6003
  console.warn(
5850
6004
  `updateFileField: [data-files-wrapper="${fieldPath}"] not found in DOM; data-resource-ids not updated`
5851
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
+ );
5852
6013
  }
6014
+ setFiles(value);
5853
6015
  } else {
5854
6016
  const hiddenInput = scopeRoot.querySelector(
5855
6017
  `input[name="${fieldPath}"][type="hidden"]`
@@ -5910,7 +6072,7 @@ function createReadonlyColourUI(value) {
5910
6072
  container.appendChild(hexText);
5911
6073
  return container;
5912
6074
  }
5913
- function createEditColourUI(value, pathKey, ctx) {
6075
+ function createEditColourUI(value, pathKey, ctx, placeholder) {
5914
6076
  const normalizedValue = normalizeColourValue(value);
5915
6077
  const pickerWrapper = document.createElement("div");
5916
6078
  pickerWrapper.className = "colour-picker-wrapper";
@@ -5934,9 +6096,10 @@ function createEditColourUI(value, pathKey, ctx) {
5934
6096
  const hexInput = document.createElement("input");
5935
6097
  hexInput.type = "text";
5936
6098
  hexInput.className = "colour-hex-input";
6099
+ hexInput.setAttribute("data-colour-field", "true");
5937
6100
  hexInput.name = pathKey;
5938
6101
  hexInput.value = normalizedValue;
5939
- hexInput.placeholder = "#000000";
6102
+ hexInput.placeholder = placeholder != null ? placeholder : "#000000";
5940
6103
  hexInput.style.cssText = `
5941
6104
  width: 100px;
5942
6105
  padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
@@ -6023,14 +6186,20 @@ function createEditColourUI(value, pathKey, ctx) {
6023
6186
  return pickerWrapper;
6024
6187
  }
6025
6188
  function renderColourElement(element, ctx, wrapper, pathKey) {
6189
+ var _a, _b;
6026
6190
  const state = ctx.state;
6027
6191
  const readonly = isElementReadonly(element, state, ctx);
6028
- const initialValue = ctx.prefill[element.key] || element.default || "#000000";
6192
+ const initialValue = (_b = (_a = ctx.prefill[element.key]) != null ? _a : element.default) != null ? _b : "#000000";
6029
6193
  if (readonly) {
6030
6194
  const readonlyUI = createReadonlyColourUI(initialValue);
6031
6195
  wrapper.appendChild(readonlyUI);
6032
6196
  } else {
6033
- const editUI = createEditColourUI(initialValue, pathKey, ctx);
6197
+ const editUI = createEditColourUI(
6198
+ initialValue,
6199
+ pathKey,
6200
+ ctx,
6201
+ element.placeholder
6202
+ );
6034
6203
  wrapper.appendChild(editUI);
6035
6204
  }
6036
6205
  if (!readonly) {
@@ -6045,7 +6214,7 @@ function renderColourElement(element, ctx, wrapper, pathKey) {
6045
6214
  }
6046
6215
  }
6047
6216
  function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
6048
- var _a, _b;
6217
+ var _a, _b, _c;
6049
6218
  const state = ctx.state;
6050
6219
  const readonly = isElementReadonly(element, state, ctx);
6051
6220
  const prefillValues = ctx.prefill[element.key] || [];
@@ -6053,7 +6222,7 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
6053
6222
  const minCount = (_a = element.minCount) != null ? _a : element.required ? 1 : 0;
6054
6223
  const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
6055
6224
  while (values.length < minCount) {
6056
- values.push(element.default || "#000000");
6225
+ values.push((_c = element.default) != null ? _c : "#000000");
6057
6226
  }
6058
6227
  const container = document.createElement("div");
6059
6228
  container.className = "fb-row";
@@ -6077,7 +6246,12 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
6077
6246
  }
6078
6247
  } else {
6079
6248
  const tempPathKey = `${pathKey}[${container.children.length}]`;
6080
- const editUI = createEditColourUI(value, tempPathKey, ctx);
6249
+ const editUI = createEditColourUI(
6250
+ value,
6251
+ tempPathKey,
6252
+ ctx,
6253
+ element.placeholder
6254
+ );
6081
6255
  editUI.style.flex = "1";
6082
6256
  itemWrapper.appendChild(editUI);
6083
6257
  }
@@ -6140,13 +6314,13 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
6140
6314
  const handle = createAddItemRow(
6141
6315
  "colour",
6142
6316
  () => {
6143
- var _a2;
6144
- const defaultColour = element.default || "#000000";
6317
+ var _a2, _b2;
6318
+ const defaultColour = (_a2 = element.default) != null ? _a2 : "#000000";
6145
6319
  values.push(defaultColour);
6146
6320
  addColourItem(defaultColour);
6147
6321
  updateAddButton();
6148
6322
  updateRemoveButtons();
6149
- (_a2 = ctx.instance) == null ? void 0 : _a2.triggerOnChange(pathKey);
6323
+ (_b2 = ctx.instance) == null ? void 0 : _b2.triggerOnChange(pathKey);
6150
6324
  },
6151
6325
  { label: element.addLabel }
6152
6326
  );
@@ -6172,63 +6346,20 @@ function renderMultipleColourElement(element, ctx, wrapper, pathKey) {
6172
6346
  }
6173
6347
  }
6174
6348
  function validateColourElement(element, key, context) {
6175
- var _a, _b, _c;
6349
+ var _a;
6176
6350
  const errors = [];
6177
- const { scopeRoot, skipValidation } = context;
6178
- const markValidity = (input, errorMessage) => {
6179
- var _a2, _b2;
6180
- if (!input) return;
6181
- const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
6182
- let errorElement = document.getElementById(errorId);
6183
- if (errorMessage) {
6184
- input.classList.add("invalid");
6185
- input.title = errorMessage;
6186
- if (!errorElement) {
6187
- errorElement = document.createElement("div");
6188
- errorElement.id = errorId;
6189
- errorElement.className = "error-message";
6190
- errorElement.style.cssText = `
6191
- color: var(--fb-error-color);
6192
- font-size: var(--fb-font-size-small);
6193
- margin-top: 0.25rem;
6194
- `;
6195
- if (input.nextSibling) {
6196
- (_a2 = input.parentNode) == null ? void 0 : _a2.insertBefore(errorElement, input.nextSibling);
6197
- } else {
6198
- (_b2 = input.parentNode) == null ? void 0 : _b2.appendChild(errorElement);
6199
- }
6200
- }
6201
- errorElement.textContent = errorMessage;
6202
- errorElement.style.display = "block";
6203
- } else {
6204
- input.classList.remove("invalid");
6205
- input.title = "";
6206
- if (errorElement) {
6207
- errorElement.remove();
6208
- }
6209
- }
6210
- };
6351
+ const { scopeRoot, state } = context;
6211
6352
  const validateColourValue = (input, val, fieldKey) => {
6212
- const { state } = context;
6353
+ const normalized = val ? normalizeColourValue(val) : "";
6354
+ let msg = null;
6213
6355
  if (!val) {
6214
- if (!skipValidation && element.required) {
6215
- const msg = t("required", state);
6216
- errors.push(`${fieldKey}: ${msg}`);
6217
- markValidity(input, msg);
6218
- return "";
6219
- }
6220
- markValidity(input, null);
6221
- return "";
6222
- }
6223
- const normalized = normalizeColourValue(val);
6224
- if (!skipValidation && !isValidHexColour(normalized)) {
6225
- const msg = t("invalidHexColour", state);
6226
- errors.push(`${fieldKey}: ${msg}`);
6227
- markValidity(input, msg);
6228
- return val;
6229
- }
6230
- markValidity(input, null);
6231
- 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;
6232
6363
  };
6233
6364
  if (element.multiple) {
6234
6365
  const hexInputs = scopeRoot.querySelectorAll(
@@ -6238,38 +6369,17 @@ function validateColourElement(element, key, context) {
6238
6369
  hexInputs.forEach((input, index) => {
6239
6370
  var _a2;
6240
6371
  const val = (_a2 = input == null ? void 0 : input.value) != null ? _a2 : "";
6241
- const validated = validateColourValue(input, val, `${key}[${index}]`);
6242
- values.push(validated);
6372
+ values.push(validateColourValue(input, val, `${key}[${index}]`));
6243
6373
  });
6244
- if (!skipValidation) {
6245
- const { state } = context;
6246
- const minCount = (_a = element.minCount) != null ? _a : 0;
6247
- const maxCount = (_b = element.maxCount) != null ? _b : Infinity;
6248
- const filteredValues = values.filter((v) => v !== "");
6249
- if (element.required && filteredValues.length === 0) {
6250
- errors.push(`${key}: ${t("required", state)}`);
6251
- }
6252
- if (filteredValues.length < minCount) {
6253
- errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
6254
- }
6255
- if (filteredValues.length > maxCount) {
6256
- errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
6257
- }
6258
- }
6374
+ const filledCount = values.filter((v) => v !== "").length;
6375
+ validateItemCount(element, key, filledCount, context, errors);
6259
6376
  return { value: values, errors };
6260
6377
  } else {
6261
6378
  const hexInput = scopeRoot.querySelector(
6262
6379
  `[name="${key}"].colour-hex-input`
6263
6380
  );
6264
- const val = (_c = hexInput == null ? void 0 : hexInput.value) != null ? _c : "";
6265
- if (!skipValidation && element.required && val === "") {
6266
- const msg = t("required", context.state);
6267
- errors.push(`${key}: ${msg}`);
6268
- markValidity(hexInput, msg);
6269
- return { value: "", errors };
6270
- }
6271
- const validated = validateColourValue(hexInput, val, key);
6272
- return { value: validated, errors };
6381
+ const val = (_a = hexInput == null ? void 0 : hexInput.value) != null ? _a : "";
6382
+ return { value: validateColourValue(hexInput, val, key), errors };
6273
6383
  }
6274
6384
  }
6275
6385
  function updateColourField(element, fieldPath, value, context) {
@@ -6288,8 +6398,6 @@ function updateColourField(element, fieldPath, value, context) {
6288
6398
  if (index < value.length) {
6289
6399
  const normalized = normalizeColourValue(value[index]);
6290
6400
  hexInput.value = normalized;
6291
- hexInput.classList.remove("invalid");
6292
- hexInput.title = "";
6293
6401
  clearFieldError(hexInput);
6294
6402
  const wrapper = hexInput.closest(".colour-picker-wrapper");
6295
6403
  if (wrapper) {
@@ -6318,8 +6426,6 @@ function updateColourField(element, fieldPath, value, context) {
6318
6426
  if (hexInput) {
6319
6427
  const normalized = normalizeColourValue(value);
6320
6428
  hexInput.value = normalized;
6321
- hexInput.classList.remove("invalid");
6322
- hexInput.title = "";
6323
6429
  clearFieldError(hexInput);
6324
6430
  const wrapper = hexInput.closest(".colour-picker-wrapper");
6325
6431
  if (wrapper) {
@@ -6637,9 +6743,9 @@ function renderMultipleSliderElement(element, ctx, wrapper, pathKey) {
6637
6743
  }
6638
6744
  }
6639
6745
  function validateSliderElement(element, key, context) {
6640
- var _a, _b, _c;
6746
+ var _a;
6641
6747
  const errors = [];
6642
- const { scopeRoot, skipValidation } = context;
6748
+ const { scopeRoot } = context;
6643
6749
  if (element.min === void 0 || element.min === null) {
6644
6750
  throw new Error(
6645
6751
  `Slider validation: field "${key}" requires "min" property`
@@ -6654,80 +6760,24 @@ function validateSliderElement(element, key, context) {
6654
6760
  const max = element.max;
6655
6761
  const step = (_a = element.step) != null ? _a : 1;
6656
6762
  const scale = element.scale || "linear";
6657
- const markValidity = (input, errorMessage) => {
6658
- var _a2, _b2;
6659
- if (!input) return;
6660
- const errorId = `error-${input.getAttribute("name") || Math.random().toString(36).substring(7)}`;
6661
- let errorElement = document.getElementById(errorId);
6662
- if (errorMessage) {
6663
- input.classList.add("invalid");
6664
- input.title = errorMessage;
6665
- if (!errorElement) {
6666
- errorElement = document.createElement("div");
6667
- errorElement.id = errorId;
6668
- errorElement.className = "error-message";
6669
- errorElement.style.cssText = `
6670
- color: var(--fb-error-color);
6671
- font-size: var(--fb-font-size-small);
6672
- margin-top: 0.25rem;
6673
- `;
6674
- const sliderContainer = input.closest(".slider-container");
6675
- if (sliderContainer && sliderContainer.nextSibling) {
6676
- (_a2 = sliderContainer.parentNode) == null ? void 0 : _a2.insertBefore(
6677
- errorElement,
6678
- sliderContainer.nextSibling
6679
- );
6680
- } else if (sliderContainer) {
6681
- (_b2 = sliderContainer.parentNode) == null ? void 0 : _b2.appendChild(errorElement);
6682
- }
6683
- }
6684
- errorElement.textContent = errorMessage;
6685
- errorElement.style.display = "block";
6686
- } else {
6687
- input.classList.remove("invalid");
6688
- input.title = "";
6689
- if (errorElement) {
6690
- errorElement.remove();
6691
- }
6692
- }
6693
- };
6694
6763
  const validateSliderValue = (slider, fieldKey) => {
6695
6764
  const { state } = context;
6696
6765
  const rawValue = slider.value;
6697
6766
  if (!rawValue) {
6698
- if (!skipValidation && element.required) {
6699
- const msg = t("required", state);
6700
- errors.push(`${fieldKey}: ${msg}`);
6701
- markValidity(slider, msg);
6702
- return null;
6703
- }
6704
- markValidity(slider, null);
6767
+ const msg2 = element.required ? t("required", state) : null;
6768
+ if (msg2 !== null) errors.push(`${fieldKey}: ${msg2}`);
6769
+ markFieldValidity(slider, msg2, context);
6705
6770
  return null;
6706
6771
  }
6707
- let value;
6708
- if (scale === "exponential") {
6709
- const position = parseFloat(rawValue) / 1e3;
6710
- value = positionToExponential(position, min, max);
6711
- value = alignToStep(value, step);
6712
- } else {
6713
- value = parseFloat(rawValue);
6714
- value = alignToStep(value, step);
6715
- }
6716
- if (!skipValidation) {
6717
- if (value < min) {
6718
- const msg = t("minValue", state, { min });
6719
- errors.push(`${fieldKey}: ${msg}`);
6720
- markValidity(slider, msg);
6721
- return value;
6722
- }
6723
- if (value > max) {
6724
- const msg = t("maxValue", state, { max });
6725
- errors.push(`${fieldKey}: ${msg}`);
6726
- markValidity(slider, msg);
6727
- return value;
6728
- }
6729
- }
6730
- markValidity(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);
6731
6781
  return value;
6732
6782
  };
6733
6783
  if (element.multiple) {
@@ -6736,31 +6786,17 @@ function validateSliderElement(element, key, context) {
6736
6786
  );
6737
6787
  const values = [];
6738
6788
  sliders.forEach((slider, index) => {
6739
- const value = validateSliderValue(slider, `${key}[${index}]`);
6740
- values.push(value);
6789
+ values.push(validateSliderValue(slider, `${key}[${index}]`));
6741
6790
  });
6742
- if (!skipValidation) {
6743
- const { state } = context;
6744
- const minCount = (_b = element.minCount) != null ? _b : 0;
6745
- const maxCount = (_c = element.maxCount) != null ? _c : Infinity;
6746
- const filteredValues = values.filter((v) => v !== null);
6747
- if (element.required && filteredValues.length === 0) {
6748
- errors.push(`${key}: ${t("required", state)}`);
6749
- }
6750
- if (filteredValues.length < minCount) {
6751
- errors.push(`${key}: ${t("minItems", state, { min: minCount })}`);
6752
- }
6753
- if (filteredValues.length > maxCount) {
6754
- errors.push(`${key}: ${t("maxItems", state, { max: maxCount })}`);
6755
- }
6756
- }
6791
+ const filledCount = values.filter((v) => v !== null).length;
6792
+ validateItemCount(element, key, filledCount, context, errors);
6757
6793
  return { value: values, errors };
6758
6794
  } else {
6759
6795
  const slider = scopeRoot.querySelector(
6760
6796
  `input[type="range"][name="${key}"]`
6761
6797
  );
6762
6798
  if (!slider) {
6763
- if (!skipValidation && element.required) {
6799
+ if (element.required) {
6764
6800
  errors.push(`${key}: ${t("required", context.state)}`);
6765
6801
  }
6766
6802
  return { value: null, errors };
@@ -6810,8 +6846,6 @@ function updateSliderField(element, fieldPath, value, context) {
6810
6846
  var(--fb-border-color) 100%
6811
6847
  )`;
6812
6848
  }
6813
- slider.classList.remove("invalid");
6814
- slider.title = "";
6815
6849
  clearFieldError(slider);
6816
6850
  }
6817
6851
  });
@@ -6847,8 +6881,6 @@ function updateSliderField(element, fieldPath, value, context) {
6847
6881
  var(--fb-border-color) 100%
6848
6882
  )`;
6849
6883
  }
6850
- slider.classList.remove("invalid");
6851
- slider.title = "";
6852
6884
  clearFieldError(slider);
6853
6885
  }
6854
6886
  }
@@ -6873,22 +6905,12 @@ function extractRootFormData(formRoot) {
6873
6905
  inputs.forEach((input) => {
6874
6906
  const fieldName = input.getAttribute("name");
6875
6907
  if (fieldName && !fieldName.includes("[") && !fieldName.includes(".")) {
6876
- if (input instanceof HTMLSelectElement) {
6877
- data[fieldName] = input.value;
6878
- } else if (input instanceof HTMLInputElement) {
6879
- if (input.type === "checkbox") {
6880
- data[fieldName] = input.checked;
6881
- } else if (input.type === "radio") {
6882
- if (input.checked) {
6883
- data[fieldName] = input.value;
6884
- }
6885
- } else if (input.dataset.hiddenField) {
6886
- data[fieldName] = deserializeHiddenValue(input.value);
6887
- } else {
6908
+ if (input instanceof HTMLInputElement && input.type === "radio") {
6909
+ if (input.checked) {
6888
6910
  data[fieldName] = input.value;
6889
6911
  }
6890
- } else if (input instanceof HTMLTextAreaElement) {
6891
- data[fieldName] = input.value;
6912
+ } else {
6913
+ data[fieldName] = readTypedInputValue(input);
6892
6914
  }
6893
6915
  }
6894
6916
  });
@@ -6957,9 +6979,9 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
6957
6979
  inheritedReadonly: containerIsReadonly || ctx.inheritedReadonly
6958
6980
  };
6959
6981
  element.elements.forEach((child) => {
6960
- var _a2, _b2;
6982
+ var _a2;
6961
6983
  if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
6962
- const prefillVal = (_b2 = (_a2 = containerPrefill[child.key]) != null ? _a2 : "default" in child ? child.default : null) != null ? _b2 : null;
6984
+ const prefillVal = child.key in containerPrefill ? containerPrefill[child.key] : (_a2 = "default" in child ? child.default : null) != null ? _a2 : null;
6963
6985
  itemsWrap.appendChild(
6964
6986
  createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal)
6965
6987
  );
@@ -7061,9 +7083,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, pathKey) {
7061
7083
  isSlides ? void 0 : element.columns
7062
7084
  );
7063
7085
  element.elements.forEach((child) => {
7064
- var _a2, _b2;
7086
+ var _a2;
7065
7087
  if (child.type !== "markdown" && (child.hidden || child.type === "hidden")) {
7066
- const hiddenValue = (_b2 = (_a2 = rowPrefill == null ? void 0 : rowPrefill[child.key]) != null ? _a2 : "default" in child ? child.default : null) != null ? _b2 : null;
7088
+ const hiddenValue = rowPrefill && child.key in rowPrefill ? rowPrefill[child.key] : (_a2 = "default" in child ? child.default : null) != null ? _a2 : null;
7067
7089
  childWrapper.appendChild(
7068
7090
  createHiddenInput(pathJoin(subCtx.path, child.key), hiddenValue)
7069
7091
  );
@@ -7174,40 +7196,21 @@ function renderMultipleContainerElement(element, ctx, wrapper, pathKey) {
7174
7196
  }
7175
7197
  }
7176
7198
  }
7177
- var validateElementFunc = null;
7178
- function setValidateElement(fn) {
7179
- validateElementFunc = fn;
7180
- }
7181
- function validateElement(element, ctx, customScopeRoot) {
7182
- if (!validateElementFunc) {
7199
+ function requireValidateElement(context) {
7200
+ if (!context.validateElement) {
7183
7201
  throw new Error(
7184
- "validateElement not initialized. Should be set from FormBuilderInstance"
7202
+ "validateContainerElement: context.validateElement missing \u2014 container validation requires the instance validator"
7185
7203
  );
7186
7204
  }
7187
- return validateElementFunc(element, ctx, customScopeRoot);
7205
+ return context.validateElement;
7188
7206
  }
7189
7207
  function validateContainerElement(element, key, context) {
7208
+ const validateChild = requireValidateElement(context);
7190
7209
  const errors = [];
7191
- const { scopeRoot, skipValidation, path } = context;
7210
+ const { scopeRoot, path } = context;
7192
7211
  if (!("elements" in element)) {
7193
7212
  return { value: null, errors };
7194
7213
  }
7195
- const validateContainerCount = (key2, items, element2) => {
7196
- var _a, _b;
7197
- if (skipValidation) return;
7198
- const { state } = context;
7199
- const minItems = "minCount" in element2 ? (_a = element2.minCount) != null ? _a : 0 : 0;
7200
- const maxItems = "maxCount" in element2 ? (_b = element2.maxCount) != null ? _b : Infinity : Infinity;
7201
- if (element2.required && items.length === 0) {
7202
- errors.push(`${key2}: ${t("required", state)}`);
7203
- }
7204
- if (items.length < minItems) {
7205
- errors.push(`${key2}: ${t("minItems", state, { min: minItems })}`);
7206
- }
7207
- if (items.length > maxItems) {
7208
- errors.push(`${key2}: ${t("maxItems", state, { max: maxItems })}`);
7209
- }
7210
- };
7211
7214
  if ("multiple" in element && element.multiple) {
7212
7215
  const items = [];
7213
7216
  const containerWrappers = findDirectContainerRows(scopeRoot, key);
@@ -7239,9 +7242,9 @@ function validateContainerElement(element, key, context) {
7239
7242
  }
7240
7243
  }
7241
7244
  const childKey = `${key}[${domIndex}].${child.key}`;
7242
- const childResult = validateElement(
7245
+ const childResult = validateChild(
7243
7246
  { ...child, key: childKey },
7244
- { path },
7247
+ { path, inheritedReadonly: context.readonly },
7245
7248
  itemContainer
7246
7249
  );
7247
7250
  if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
@@ -7252,7 +7255,7 @@ function validateContainerElement(element, key, context) {
7252
7255
  });
7253
7256
  items.push(itemData);
7254
7257
  });
7255
- validateContainerCount(key, items, element);
7258
+ validateItemCount(element, key, items.length, context, errors);
7256
7259
  return { value: items, errors };
7257
7260
  } else {
7258
7261
  const containerData = {};
@@ -7281,9 +7284,9 @@ function validateContainerElement(element, key, context) {
7281
7284
  }
7282
7285
  {
7283
7286
  const childKey = `${key}.${child.key}`;
7284
- const childResult = validateElement(
7287
+ const childResult = validateChild(
7285
7288
  { ...child, key: childKey },
7286
- { path },
7289
+ { path, inheritedReadonly: context.readonly },
7287
7290
  containerContainer
7288
7291
  );
7289
7292
  if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
@@ -7784,7 +7787,7 @@ function renderEditTable(element, initialData, pathKey, ctx, wrapper) {
7784
7787
  rebuild();
7785
7788
  } catch (e) {
7786
7789
  const errMsg = e instanceof Error ? e.message : String(e);
7787
- console.error(t("tableImportError", state).replace("{error}", errMsg));
7790
+ console.error(t("tableImportError", state, { error: errMsg }));
7788
7791
  } finally {
7789
7792
  overlay.remove();
7790
7793
  }
@@ -8680,9 +8683,25 @@ function renderTableElement(element, ctx, wrapper, pathKey) {
8680
8683
  renderEditTable(element, initialData, pathKey, ctx, wrapper);
8681
8684
  }
8682
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
+ }
8683
8702
  function validateTableElement(element, key, context) {
8684
8703
  var _a, _b;
8685
- const { scopeRoot, skipValidation } = context;
8704
+ const { scopeRoot } = context;
8686
8705
  const errors = [];
8687
8706
  const cellsKey = (_b = (_a = element.fieldNames) == null ? void 0 : _a.cells) != null ? _b : "cells";
8688
8707
  const hiddenInput = scopeRoot.querySelector(
@@ -8691,22 +8710,20 @@ function validateTableElement(element, key, context) {
8691
8710
  if (!hiddenInput) {
8692
8711
  return { value: null, errors };
8693
8712
  }
8694
- let value;
8695
- try {
8696
- value = JSON.parse(hiddenInput.value);
8697
- } catch {
8698
- 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);
8699
8718
  return { value: null, errors };
8700
8719
  }
8701
- if (!skipValidation && element.required) {
8702
- const cells = value[cellsKey];
8703
- const hasContent = cells == null ? void 0 : cells.some(
8704
- (row) => row.some((cell) => cell.trim() !== "")
8705
- );
8706
- if (!hasContent) {
8707
- errors.push(`${key}: ${t("required", context.state)}`);
8708
- }
8709
- }
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);
8710
8727
  return { value, errors };
8711
8728
  }
8712
8729
  function updateTableField(element, fieldPath, value, context) {
@@ -8744,7 +8761,7 @@ function updateTableField(element, fieldPath, value, context) {
8744
8761
  }
8745
8762
 
8746
8763
  // src/components/richinput.ts
8747
- function applyAutoExpand2(textarea, backdrop) {
8764
+ function applyAutoExpand2(textarea, backdrop, observers) {
8748
8765
  textarea.style.overflow = "hidden";
8749
8766
  textarea.style.resize = "none";
8750
8767
  const lineCount = (textarea.value.match(/\n/g) || []).length + 1;
@@ -8767,6 +8784,7 @@ function applyAutoExpand2(textarea, backdrop) {
8767
8784
  var _a, _b, _c, _d, _e;
8768
8785
  if (!textarea.isConnected) {
8769
8786
  ro.disconnect();
8787
+ observers.delete(ro);
8770
8788
  return;
8771
8789
  }
8772
8790
  const entry = entries[0];
@@ -8776,6 +8794,7 @@ function applyAutoExpand2(textarea, backdrop) {
8776
8794
  resize();
8777
8795
  });
8778
8796
  ro.observe(textarea);
8797
+ observers.add(ro);
8779
8798
  }
8780
8799
  function buildFileLabels(files, state) {
8781
8800
  var _a, _b, _c, _d;
@@ -9204,7 +9223,7 @@ function filterFilesForDropdown(query, files, labels) {
9204
9223
  var TEXTAREA_FONT = "font-size: var(--fb-font-size, 14px); font-family: var(--fb-font-family, inherit); line-height: 1.6;";
9205
9224
  var TEXTAREA_PADDING = "padding: 8px 40px 8px 10px;";
9206
9225
  function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
9207
- var _a;
9226
+ var _a, _b;
9208
9227
  const state = ctx.state;
9209
9228
  const files = [...initialValue.files];
9210
9229
  const dropdownState = {
@@ -9218,12 +9237,12 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
9218
9237
  hiddenInput.type = "hidden";
9219
9238
  hiddenInput.name = pathKey;
9220
9239
  function getCurrentValue() {
9221
- var _a2, _b;
9240
+ var _a2, _b2;
9222
9241
  const rawText = textarea.value;
9223
9242
  const nameToRid = buildNameToRid(files, state);
9224
9243
  const submissionText = rawText ? replaceFilenamesWithRids(rawText, nameToRid) : null;
9225
9244
  const textKey = (_a2 = element.textKey) != null ? _a2 : "text";
9226
- const filesKey = (_b = element.filesKey) != null ? _b : "files";
9245
+ const filesKey = (_b2 = element.filesKey) != null ? _b2 : "files";
9227
9246
  return {
9228
9247
  [textKey]: rawText === "" ? null : submissionText,
9229
9248
  [filesKey]: [...files]
@@ -9305,14 +9324,14 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
9305
9324
  }
9306
9325
  });
9307
9326
  outerDiv.addEventListener("drop", (e) => {
9308
- var _a2, _b;
9327
+ var _a2, _b2;
9309
9328
  e.preventDefault();
9310
9329
  dragCounter = 0;
9311
9330
  outerDiv.style.borderColor = "var(--fb-border-color, #d1d5db)";
9312
9331
  outerDiv.style.boxShadow = "none";
9313
9332
  const droppedFiles = (_a2 = e.dataTransfer) == null ? void 0 : _a2.files;
9314
9333
  if (!droppedFiles || !state.config.uploadFile) return;
9315
- const maxFiles = (_b = element.maxFiles) != null ? _b : Infinity;
9334
+ const maxFiles = (_b2 = element.maxFiles) != null ? _b2 : Infinity;
9316
9335
  for (let i = 0; i < droppedFiles.length; i++) {
9317
9336
  if (files.length >= maxFiles) {
9318
9337
  showUploadError(
@@ -9361,8 +9380,8 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
9361
9380
  `;
9362
9381
  const textarea = document.createElement("textarea");
9363
9382
  textarea.name = `${pathKey}__text`;
9364
- textarea.placeholder = element.placeholder || t("richinputPlaceholder", state);
9365
- const rawInitialText = (_a = initialValue.text) != null ? _a : "";
9383
+ textarea.placeholder = (_a = element.placeholder) != null ? _a : t("richinputPlaceholder", state);
9384
+ const rawInitialText = (_b = initialValue.text) != null ? _b : "";
9366
9385
  textarea.value = rawInitialText ? replaceRidsWithFilenames(rawInitialText, files, state) : "";
9367
9386
  textarea.style.cssText = `
9368
9387
  width: 100%;
@@ -9378,14 +9397,14 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
9378
9397
  z-index: 1;
9379
9398
  caret-color: var(--fb-text-color, #111827);
9380
9399
  `;
9381
- applyAutoExpand2(textarea, backdrop);
9400
+ applyAutoExpand2(textarea, backdrop, ctx.state.autoExpandObservers);
9382
9401
  textarea.addEventListener("scroll", () => {
9383
9402
  backdrop.scrollTop = textarea.scrollTop;
9384
9403
  });
9385
9404
  let mentionTooltip = null;
9386
9405
  backdrop.addEventListener("mouseover", (e) => {
9387
- var _a2, _b;
9388
- const mark = (_b = (_a2 = e.target).closest) == null ? void 0 : _b.call(
9406
+ var _a2, _b2;
9407
+ const mark = (_b2 = (_a2 = e.target).closest) == null ? void 0 : _b2.call(
9389
9408
  _a2,
9390
9409
  "mark"
9391
9410
  );
@@ -9394,8 +9413,8 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
9394
9413
  mentionTooltip = showMentionTooltip(mark, mark.dataset.rid, state);
9395
9414
  });
9396
9415
  backdrop.addEventListener("mouseout", (e) => {
9397
- var _a2, _b, _c;
9398
- const mark = (_b = (_a2 = e.target).closest) == null ? void 0 : _b.call(
9416
+ var _a2, _b2, _c;
9417
+ const mark = (_b2 = (_a2 = e.target).closest) == null ? void 0 : _b2.call(
9399
9418
  _a2,
9400
9419
  "mark"
9401
9420
  );
@@ -9405,8 +9424,8 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
9405
9424
  mentionTooltip = removePortalTooltip(mentionTooltip);
9406
9425
  });
9407
9426
  backdrop.addEventListener("mousedown", (e) => {
9408
- var _a2, _b;
9409
- const mark = (_b = (_a2 = e.target).closest) == null ? void 0 : _b.call(
9427
+ var _a2, _b2;
9428
+ const mark = (_b2 = (_a2 = e.target).closest) == null ? void 0 : _b2.call(
9410
9429
  _a2,
9411
9430
  "mark"
9412
9431
  );
@@ -9579,8 +9598,8 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
9579
9598
  dropdown.appendChild(item);
9580
9599
  });
9581
9600
  dropdown.onmousemove = (e) => {
9582
- var _a2, _b, _c;
9583
- const target = (_b = (_a2 = e.target).closest) == null ? void 0 : _b.call(
9601
+ var _a2, _b2, _c;
9602
+ const target = (_b2 = (_a2 = e.target).closest) == null ? void 0 : _b2.call(
9584
9603
  _a2,
9585
9604
  ".fb-richinput-dropdown-item"
9586
9605
  );
@@ -9596,10 +9615,10 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
9596
9615
  dropdownState.selectedIndex = newIdx;
9597
9616
  };
9598
9617
  dropdown.onmousedown = (e) => {
9599
- var _a2, _b;
9618
+ var _a2, _b2;
9600
9619
  e.preventDefault();
9601
9620
  e.stopPropagation();
9602
- const target = (_b = (_a2 = e.target).closest) == null ? void 0 : _b.call(
9621
+ const target = (_b2 = (_a2 = e.target).closest) == null ? void 0 : _b2.call(
9603
9622
  _a2,
9604
9623
  ".fb-richinput-dropdown-item"
9605
9624
  );
@@ -9627,9 +9646,9 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
9627
9646
  dropdownState.open = false;
9628
9647
  }
9629
9648
  function insertMention(rid) {
9630
- var _a2, _b, _c, _d;
9649
+ var _a2, _b2, _c, _d;
9631
9650
  const labels = buildFileLabelsFromClosure();
9632
- const label = (_c = (_b = labels.get(rid)) != null ? _b : (_a2 = state.resourceIndex.get(rid)) == null ? void 0 : _a2.name) != null ? _c : rid;
9651
+ const label = (_c = (_b2 = labels.get(rid)) != null ? _b2 : (_a2 = state.resourceIndex.get(rid)) == null ? void 0 : _a2.name) != null ? _c : rid;
9633
9652
  const cursorPos = (_d = textarea.selectionStart) != null ? _d : 0;
9634
9653
  const before = textarea.value.slice(0, dropdownState.triggerPos);
9635
9654
  const after = textarea.value.slice(cursorPos);
@@ -9721,10 +9740,10 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
9721
9740
  thumbWrapper.appendChild(thumbInner);
9722
9741
  const tooltipHandle = createTooltipHandle();
9723
9742
  const doMention = () => {
9724
- var _a2, _b, _c;
9743
+ var _a2, _b2, _c;
9725
9744
  const cursorPos = (_a2 = textarea.selectionStart) != null ? _a2 : textarea.value.length;
9726
9745
  const labels = buildFileLabelsFromClosure();
9727
- const label = (_c = (_b = labels.get(rid)) != null ? _b : meta == null ? void 0 : meta.name) != null ? _c : rid;
9746
+ const label = (_c = (_b2 = labels.get(rid)) != null ? _b2 : meta == null ? void 0 : meta.name) != null ? _c : rid;
9728
9747
  const before = textarea.value.slice(0, cursorPos);
9729
9748
  const after = textarea.value.slice(cursorPos);
9730
9749
  const prefix = before.length > 0 && !/[\s\n]$/.test(before) ? "\n" : "";
@@ -9802,12 +9821,12 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
9802
9821
  writeHidden();
9803
9822
  (_a2 = ctx.instance) == null ? void 0 : _a2.triggerOnChange(pathKey, getCurrentValue());
9804
9823
  }).catch((err) => {
9805
- var _a2, _b;
9824
+ var _a2, _b2;
9806
9825
  const idx = files.indexOf(tempId);
9807
9826
  if (idx !== -1) files.splice(idx, 1);
9808
9827
  state.resourceIndex.delete(tempId);
9809
9828
  renderFilesRow();
9810
- (_b = (_a2 = state.config).onUploadError) == null ? void 0 : _b.call(_a2, err, file);
9829
+ (_b2 = (_a2 = state.config).onUploadError) == null ? void 0 : _b2.call(_a2, err, file);
9811
9830
  });
9812
9831
  }
9813
9832
  fileInput.addEventListener("change", () => {
@@ -10046,9 +10065,17 @@ function renderRichInputElement(element, ctx, wrapper, pathKey) {
10046
10065
  renderEditMode(element, ctx, wrapper, pathKey, initialValue);
10047
10066
  }
10048
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
+ }
10049
10076
  function validateRichInputElement(element, key, context) {
10050
10077
  var _a, _b;
10051
- const { scopeRoot, state, skipValidation } = context;
10078
+ const { scopeRoot, state } = context;
10052
10079
  const errors = [];
10053
10080
  const textKey = (_a = element.textKey) != null ? _a : "text";
10054
10081
  const filesKey = (_b = element.filesKey) != null ? _b : "files";
@@ -10058,17 +10085,11 @@ function validateRichInputElement(element, key, context) {
10058
10085
  if (!hiddenInput) {
10059
10086
  return { value: null, errors };
10060
10087
  }
10061
- let rawValue;
10062
- try {
10063
- const parsed = JSON.parse(hiddenInput.value);
10064
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
10065
- rawValue = parsed;
10066
- } else {
10067
- errors.push(`${key}: invalid richinput data`);
10068
- return { value: null, errors };
10069
- }
10070
- } catch {
10071
- 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);
10072
10093
  return { value: null, errors };
10073
10094
  }
10074
10095
  const textVal = rawValue[textKey];
@@ -10079,28 +10100,24 @@ function validateRichInputElement(element, key, context) {
10079
10100
  [textKey]: text != null ? text : null,
10080
10101
  [filesKey]: files
10081
10102
  };
10082
- if (!skipValidation) {
10083
- const textEmpty = !text || text.trim() === "";
10084
- const filesEmpty = files.length === 0;
10085
- if (element.required && textEmpty && filesEmpty) {
10086
- errors.push(`${key}: ${t("required", state)}`);
10087
- }
10088
- if (!textEmpty && text) {
10089
- if (element.minLength != null && text.length < element.minLength) {
10090
- errors.push(
10091
- `${key}: ${t("minLength", state, { min: element.minLength })}`
10092
- );
10093
- }
10094
- if (element.maxLength != null && text.length > element.maxLength) {
10095
- errors.push(
10096
- `${key}: ${t("maxLength", state, { max: element.maxLength })}`
10097
- );
10098
- }
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 }));
10099
10111
  }
10100
- if (element.maxFiles != null && files.length > element.maxFiles) {
10101
- 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 }));
10102
10114
  }
10103
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);
10104
10121
  return { value, errors, spread: !!element.flatOutput };
10105
10122
  }
10106
10123
  function updateRichInputField(element, fieldPath, value, context) {
@@ -10379,12 +10396,7 @@ function validateHiddenElement(element, key, context) {
10379
10396
  const input = scopeRoot.querySelector(
10380
10397
  `input[type="hidden"][data-hidden-field="true"][name="${key}"]`
10381
10398
  );
10382
- const raw = (_a = input == null ? void 0 : input.value) != null ? _a : "";
10383
- if (raw === "") {
10384
- const defaultVal = "default" in element ? element.default : null;
10385
- return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
10386
- }
10387
- return { value: deserializeHiddenValue(raw), errors: [] };
10399
+ return { value: deserializeHiddenValue((_a = input == null ? void 0 : input.value) != null ? _a : ""), errors: [] };
10388
10400
  }
10389
10401
  function updateHiddenField(_element, fieldPath, value, context) {
10390
10402
  const { scopeRoot } = context;
@@ -10576,25 +10588,13 @@ function extractDOMValue(fieldPath, formRoot) {
10576
10588
  if (!input) {
10577
10589
  return void 0;
10578
10590
  }
10579
- if (input instanceof HTMLSelectElement) {
10580
- return input.value;
10581
- } else if (input instanceof HTMLInputElement) {
10582
- if (input.type === "checkbox") {
10583
- return input.checked;
10584
- } else if (input.type === "radio") {
10585
- const checked = formRoot.querySelector(
10586
- `[name="${fieldPath}"]:checked`
10587
- );
10588
- return checked ? checked.value : void 0;
10589
- } else if (input.dataset.hiddenField) {
10590
- return deserializeHiddenValue(input.value);
10591
- } else {
10592
- return input.value;
10593
- }
10594
- } else if (input instanceof HTMLTextAreaElement) {
10595
- return input.value;
10591
+ if (input instanceof HTMLInputElement && input.type === "radio") {
10592
+ const checked = formRoot.querySelector(
10593
+ `[name="${fieldPath}"]:checked`
10594
+ );
10595
+ return checked ? checked.value : void 0;
10596
10596
  }
10597
- return void 0;
10597
+ return readTypedInputValue(input);
10598
10598
  }
10599
10599
  function buildScopedDataAtPath(path, value) {
10600
10600
  const segments = path.match(/[^.[\]]+|\[\d+\]/g);
@@ -10726,11 +10726,19 @@ function createFieldLabel(element) {
10726
10726
  }
10727
10727
  return title;
10728
10728
  }
10729
+ function ensureTooltipStyles(doc) {
10730
+ if (doc.head.querySelector("[data-fb-tooltip-styles]")) return;
10731
+ const style = doc.createElement("style");
10732
+ style.setAttribute("data-fb-tooltip-styles", "");
10733
+ style.textContent = `[id^="tooltip-"].hidden { display: none; }`;
10734
+ doc.head.appendChild(style);
10735
+ }
10729
10736
  function createInfoButton(element, state) {
10730
10737
  const infoBtn = document.createElement("button");
10731
10738
  infoBtn.type = "button";
10732
10739
  infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
10733
10740
  infoBtn.innerHTML = '<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>';
10741
+ ensureTooltipStyles(document);
10734
10742
  const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
10735
10743
  const tooltip = document.createElement("div");
10736
10744
  tooltip.id = tooltipId;
@@ -10880,12 +10888,13 @@ function renderElement2(element, ctx) {
10880
10888
  wrapper.className = `fb-field-wrapper fb-size-${element.size || "md"}`;
10881
10889
  wrapper.setAttribute("data-field-key", element.key);
10882
10890
  wrapper.setAttribute("data-fb-width", element.width || "full");
10891
+ const pathKey = pathJoin(ctx.path, element.key);
10892
+ wrapper.setAttribute("data-field-path", pathKey);
10883
10893
  const ops = getComponentOperations(element.type);
10884
10894
  if (!(ops == null ? void 0 : ops.ownsLabel)) {
10885
10895
  const label = createLabelContainer(element, ctx.state);
10886
10896
  wrapper.appendChild(label);
10887
10897
  }
10888
- const pathKey = pathJoin(ctx.path, element.key);
10889
10898
  dispatchToRenderer(element, ctx, wrapper, pathKey);
10890
10899
  if (initiallyDisabled) {
10891
10900
  wrapper.style.display = "none";
@@ -10911,6 +10920,7 @@ var defaultConfig = {
10911
10920
  onDownloadError: null,
10912
10921
  debounceMs: 300,
10913
10922
  verboseErrors: false,
10923
+ postMessageTarget: null,
10914
10924
  enableFilePreview: true,
10915
10925
  maxPreviewSize: "200px",
10916
10926
  readonly: false,
@@ -10932,6 +10942,7 @@ var defaultConfig = {
10932
10942
  openInNewTab: "Open in new tab",
10933
10943
  changeButton: "Change",
10934
10944
  placeholderText: "Enter text",
10945
+ selectPlaceholder: "Select\u2026",
10935
10946
  previewAlt: "Preview",
10936
10947
  previewUnavailable: "Preview unavailable",
10937
10948
  previewError: "Preview error",
@@ -11007,6 +11018,7 @@ var defaultConfig = {
11007
11018
  openInNewTab: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0432 \u043D\u043E\u0432\u043E\u0439 \u0432\u043A\u043B\u0430\u0434\u043A\u0435",
11008
11019
  changeButton: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C",
11009
11020
  placeholderText: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442",
11021
+ selectPlaceholder: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435\u2026",
11010
11022
  previewAlt: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",
11011
11023
  previewUnavailable: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D",
11012
11024
  previewError: "\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430",
@@ -11071,21 +11083,27 @@ var defaultConfig = {
11071
11083
  },
11072
11084
  theme: {}
11073
11085
  };
11074
- function createInstanceState(config) {
11075
- const mergedTranslations = {
11076
- ...defaultConfig.translations
11077
- };
11078
- if (config == null ? void 0 : config.translations) {
11079
- for (const [locale, userTranslations] of Object.entries(
11080
- config.translations
11081
- )) {
11082
- mergedTranslations[locale] = {
11083
- ...defaultConfig.translations[locale] || {},
11086
+ function mergeTranslations(base, overrides) {
11087
+ const merged = { ...base };
11088
+ if (overrides) {
11089
+ for (const [locale, userTranslations] of Object.entries(overrides)) {
11090
+ merged[locale] = {
11091
+ ...base[locale] || {},
11084
11092
  ...userTranslations
11085
11093
  };
11086
11094
  }
11087
11095
  }
11096
+ return merged;
11097
+ }
11098
+ function createInstanceState(config) {
11099
+ const mergedTranslations = mergeTranslations(
11100
+ defaultConfig.translations,
11101
+ config == null ? void 0 : config.translations
11102
+ );
11088
11103
  return {
11104
+ instanceId: generateInstanceId(),
11105
+ domIdCounter: 0,
11106
+ reportedInvalid: /* @__PURE__ */ new WeakSet(),
11089
11107
  schema: null,
11090
11108
  formRoot: null,
11091
11109
  resourceIndex: /* @__PURE__ */ new Map(),
@@ -11100,7 +11118,9 @@ function createInstanceState(config) {
11100
11118
  prefill: {},
11101
11119
  syntheticElementIds: /* @__PURE__ */ new WeakMap(),
11102
11120
  syntheticElementIdCounter: 0,
11121
+ multiFileSetters: /* @__PURE__ */ new WeakMap(),
11103
11122
  enableIfObservers: /* @__PURE__ */ new Set(),
11123
+ autoExpandObservers: /* @__PURE__ */ new Set(),
11104
11124
  tooltipElements: /* @__PURE__ */ new Set()
11105
11125
  };
11106
11126
  }
@@ -11402,8 +11422,13 @@ function findOwnField(scope, lookupKey, ownBoundary) {
11402
11422
  }
11403
11423
  var FormBuilderInstance = class {
11404
11424
  constructor(config) {
11405
- this.instanceId = generateInstanceId();
11425
+ // The bound prefill-hint click handler currently attached to the form root.
11426
+ // Kept so renderForm()/destroy() can remove it — re-binding on every render
11427
+ // stacked listeners (hint clicks applied values N times) and destroy()
11428
+ // left the last one on the host-owned root, retaining the instance.
11429
+ this.prefillHintHandler = null;
11406
11430
  this.state = createInstanceState(config);
11431
+ this.instanceId = this.state.instanceId;
11407
11432
  if (this.state.config.verboseErrors) {
11408
11433
  if (!globalThis.__formBuilderInstances) {
11409
11434
  globalThis.__formBuilderInstances = /* @__PURE__ */ new Set();
@@ -11435,10 +11460,21 @@ var FormBuilderInstance = class {
11435
11460
  this.state.formRoot = element;
11436
11461
  }
11437
11462
  /**
11438
- * Configure the form builder
11463
+ * Configure the form builder. Translations deep-merge per locale (same as
11464
+ * the constructor); a locale without translations — configured or default —
11465
+ * is rejected, matching setLocale.
11439
11466
  */
11440
11467
  configure(config) {
11441
- Object.assign(this.state.config, config);
11468
+ const translations = mergeTranslations(
11469
+ this.state.config.translations,
11470
+ config.translations
11471
+ );
11472
+ if (config.locale !== void 0 && !translations[config.locale]) {
11473
+ throw new Error(
11474
+ `configure: no translations configured for locale "${config.locale}"`
11475
+ );
11476
+ }
11477
+ Object.assign(this.state.config, config, { translations });
11442
11478
  }
11443
11479
  /**
11444
11480
  * Set file upload handler
@@ -11471,12 +11507,16 @@ var FormBuilderInstance = class {
11471
11507
  this.state.config.readonly = mode === "readonly";
11472
11508
  }
11473
11509
  /**
11474
- * Set locale
11510
+ * Set locale. Custom locales are allowed — their translations must have
11511
+ * been provided via the constructor or configure() first.
11475
11512
  */
11476
11513
  setLocale(locale) {
11477
- if (this.state.config.translations[locale]) {
11478
- this.state.config.locale = locale;
11514
+ if (!this.state.config.translations[locale]) {
11515
+ throw new Error(
11516
+ `setLocale: no translations configured for locale "${locale}"`
11517
+ );
11479
11518
  }
11519
+ this.state.config.locale = locale;
11480
11520
  }
11481
11521
  /**
11482
11522
  * Trigger onChange callbacks with debouncing
@@ -11829,11 +11869,13 @@ var FormBuilderInstance = class {
11829
11869
  renderForm(root, schema, prefill, actions) {
11830
11870
  const errors = validateSchema(schema);
11831
11871
  if (errors.length > 0) {
11832
- console.error("Schema validation errors:", errors);
11833
- return;
11872
+ throw new Error(`renderForm: invalid schema:
11873
+ - ${errors.join("\n- ")}`);
11834
11874
  }
11835
11875
  this.disconnectEnableIfObservers();
11876
+ this.disconnectAutoExpandObservers();
11836
11877
  this.removeTooltipElements();
11878
+ this.removePrefillHintListener();
11837
11879
  this.state.formRoot = root;
11838
11880
  this.state.schema = schema;
11839
11881
  this.state.externalActions = actions || null;
@@ -11855,9 +11897,9 @@ var FormBuilderInstance = class {
11855
11897
  fieldsWrapper.className = `grid grid-cols-${columns} gap-2`;
11856
11898
  }
11857
11899
  schema.elements.forEach((element) => {
11858
- var _a, _b;
11900
+ var _a;
11859
11901
  if (element.type !== "markdown" && (element.hidden || element.type === "hidden")) {
11860
- const val = (_b = (_a = prefill == null ? void 0 : prefill[element.key]) != null ? _a : element.default) != null ? _b : null;
11902
+ const val = prefill && element.key in prefill ? prefill[element.key] : (_a = element.default) != null ? _a : null;
11861
11903
  fieldsWrapper.appendChild(createHiddenInput(element.key, val));
11862
11904
  return;
11863
11905
  }
@@ -11874,23 +11916,36 @@ var FormBuilderInstance = class {
11874
11916
  rootContainer.appendChild(fieldsWrapper);
11875
11917
  root.appendChild(rootContainer);
11876
11918
  if (!this.state.config.readonly) {
11877
- root.addEventListener("click", this.handlePrefillHintClick.bind(this));
11919
+ this.prefillHintHandler = this.handlePrefillHintClick.bind(
11920
+ this
11921
+ );
11922
+ root.addEventListener("click", this.prefillHintHandler);
11878
11923
  }
11879
11924
  if (this.state.config.readonly && this.state.externalActions && Array.isArray(this.state.externalActions)) {
11880
11925
  this.renderExternalActions();
11881
11926
  }
11882
11927
  }
11883
11928
  /**
11884
- * Validate form and extract data
11885
- * This is a complete copy of the validateForm logic from form-builder.ts
11886
- * 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: []`.
11887
11932
  */
11888
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) {
11889
11944
  if (!this.state.schema || !this.state.formRoot)
11890
11945
  return { valid: true, errors: [], data: {} };
11891
11946
  const errors = [];
11892
11947
  const data = {};
11893
- const validateElement2 = (element, ctx, customScopeRoot = null) => {
11948
+ const validateElement = (element, ctx, customScopeRoot = null) => {
11894
11949
  var _a;
11895
11950
  const key = (_a = element.key) != null ? _a : "";
11896
11951
  const scopeRoot = customScopeRoot || this.state.formRoot;
@@ -11899,7 +11954,11 @@ var FormBuilderInstance = class {
11899
11954
  state: this.state,
11900
11955
  instance: this,
11901
11956
  path: ctx.path,
11902
- skipValidation
11957
+ draftMarks: marks === "draft",
11958
+ readonly: isElementReadonly(element, this.state, ctx),
11959
+ // Containers recurse into their children through this — threaded per
11960
+ // pass, never module state (see ComponentContext.validateElement).
11961
+ validateElement
11903
11962
  };
11904
11963
  const componentResult = validateElementWithComponent(
11905
11964
  element,
@@ -11917,7 +11976,6 @@ var FormBuilderInstance = class {
11917
11976
  console.warn(`Unknown field type "${element.type}" for key "${key}"`);
11918
11977
  return { value: null, spread: false };
11919
11978
  };
11920
- setValidateElement(validateElement2);
11921
11979
  this.state.schema.elements.forEach((element) => {
11922
11980
  if (element.enableIf) {
11923
11981
  try {
@@ -11935,7 +11993,7 @@ var FormBuilderInstance = class {
11935
11993
  if (element.type === "markdown") {
11936
11994
  return;
11937
11995
  }
11938
- const result = validateElement2(element, { path: "" });
11996
+ const result = validateElement(element, { path: "" });
11939
11997
  if (result.skip) return;
11940
11998
  if (result.spread && result.value !== null && typeof result.value === "object") {
11941
11999
  Object.assign(data, result.value);
@@ -11950,10 +12008,56 @@ var FormBuilderInstance = class {
11950
12008
  };
11951
12009
  }
11952
12010
  /**
11953
- * 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.
11954
12016
  */
11955
12017
  getFormData() {
11956
- 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;
11957
12061
  }
11958
12062
  /**
11959
12063
  * Submit form with validation
@@ -11961,16 +12065,7 @@ var FormBuilderInstance = class {
11961
12065
  submitForm() {
11962
12066
  const result = this.validateForm(false);
11963
12067
  if (result.valid) {
11964
- if (typeof window !== "undefined" && window.parent) {
11965
- window.parent.postMessage(
11966
- {
11967
- type: "formSubmit",
11968
- data: result.data,
11969
- schema: this.state.schema
11970
- },
11971
- "*"
11972
- );
11973
- }
12068
+ this.postToParent("formSubmit", result.data);
11974
12069
  }
11975
12070
  return result;
11976
12071
  }
@@ -11979,17 +12074,27 @@ var FormBuilderInstance = class {
11979
12074
  */
11980
12075
  saveDraft() {
11981
12076
  const result = this.validateForm(true);
11982
- if (typeof window !== "undefined" && window.parent) {
11983
- window.parent.postMessage(
11984
- {
11985
- type: "formDraft",
11986
- data: result.data,
11987
- schema: this.state.schema
11988
- },
11989
- "*"
12077
+ this.postToParent("formDraft", result.data);
12078
+ return result;
12079
+ }
12080
+ /**
12081
+ * Post form data to the parent frame — only when the host opted in via
12082
+ * `postMessageTarget`. Outside an iframe `window.parent === window`, so an
12083
+ * unconditional post broadcast form data and the full schema to any
12084
+ * embedding page (targetOrigin "*") on every submit. See CHANGELOG 0.6.0.
12085
+ */
12086
+ postToParent(type, data) {
12087
+ const target = this.state.config.postMessageTarget;
12088
+ if (target === "") {
12089
+ throw new Error(
12090
+ 'postMessageTarget: "" is not a valid target origin \u2014 use null to disable posting or "*" to knowingly broadcast'
11990
12091
  );
11991
12092
  }
11992
- return result;
12093
+ if (!target || typeof window === "undefined" || !window.parent) return;
12094
+ window.parent.postMessage(
12095
+ { type, data, schema: this.state.schema },
12096
+ target
12097
+ );
11993
12098
  }
11994
12099
  /**
11995
12100
  * Clear the form - reset all field values to empty while preserving form structure
@@ -12195,6 +12300,7 @@ var FormBuilderInstance = class {
12195
12300
  getElementLookupKey(element, this.state)
12196
12301
  );
12197
12302
  disabledWrapper.setAttribute("data-conditionally-disabled", "true");
12303
+ disabledWrapper.setAttribute("data-field-path", fullDomPath);
12198
12304
  (_c = wrapper.parentNode) == null ? void 0 : _c.replaceChild(disabledWrapper, wrapper);
12199
12305
  }
12200
12306
  } catch (error) {
@@ -12263,7 +12369,9 @@ var FormBuilderInstance = class {
12263
12369
  this.state.debounceTimer = null;
12264
12370
  }
12265
12371
  this.disconnectEnableIfObservers();
12372
+ this.disconnectAutoExpandObservers();
12266
12373
  this.removeTooltipElements();
12374
+ this.removePrefillHintListener();
12267
12375
  this.state.resourceIndex.clear();
12268
12376
  if (this.state.formRoot) {
12269
12377
  clear(this.state.formRoot);
@@ -12281,6 +12389,18 @@ var FormBuilderInstance = class {
12281
12389
  }
12282
12390
  this.state.enableIfObservers.clear();
12283
12391
  }
12392
+ disconnectAutoExpandObservers() {
12393
+ for (const observer of this.state.autoExpandObservers) {
12394
+ observer.disconnect();
12395
+ }
12396
+ this.state.autoExpandObservers.clear();
12397
+ }
12398
+ removePrefillHintListener() {
12399
+ if (this.prefillHintHandler && this.state.formRoot) {
12400
+ this.state.formRoot.removeEventListener("click", this.prefillHintHandler);
12401
+ }
12402
+ this.prefillHintHandler = null;
12403
+ }
12284
12404
  removeTooltipElements() {
12285
12405
  for (const tooltip of this.state.tooltipElements) {
12286
12406
  tooltip.remove();