@dmitryvim/form-builder 0.2.22 → 0.2.24

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.
@@ -122,6 +122,49 @@ function validateSchema(schema) {
122
122
  });
123
123
  }
124
124
  }
125
+ function checkFlatOutputCollisions(elements, scopePath) {
126
+ var _a, _b;
127
+ const allOutputKeys = /* @__PURE__ */ new Set();
128
+ for (const el of elements) {
129
+ if (el.type === "richinput" && el.flatOutput) {
130
+ const richEl = el;
131
+ const textKey = (_a = richEl.textKey) != null ? _a : "text";
132
+ const filesKey = (_b = richEl.filesKey) != null ? _b : "files";
133
+ for (const otherEl of elements) {
134
+ if (otherEl === el) continue;
135
+ if (otherEl.key === textKey) {
136
+ errors.push(
137
+ `${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with element key "${otherEl.key}"`
138
+ );
139
+ }
140
+ if (otherEl.key === filesKey) {
141
+ errors.push(
142
+ `${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with element key "${otherEl.key}"`
143
+ );
144
+ }
145
+ }
146
+ if (allOutputKeys.has(textKey)) {
147
+ errors.push(
148
+ `${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with another flatOutput key`
149
+ );
150
+ }
151
+ if (allOutputKeys.has(filesKey)) {
152
+ errors.push(
153
+ `${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with another flatOutput key`
154
+ );
155
+ }
156
+ allOutputKeys.add(textKey);
157
+ allOutputKeys.add(filesKey);
158
+ } else {
159
+ if (allOutputKeys.has(el.key)) {
160
+ errors.push(
161
+ `${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
162
+ );
163
+ }
164
+ allOutputKeys.add(el.key);
165
+ }
166
+ }
167
+ }
125
168
  function validateElements(elements, path) {
126
169
  elements.forEach((element, index) => {
127
170
  const elementPath = `${path}[${index}]`;
@@ -187,6 +230,7 @@ function validateSchema(schema) {
187
230
  }
188
231
  }
189
232
  validateElements(element.elements, `${elementPath}.elements`);
233
+ checkFlatOutputCollisions(element.elements, `${elementPath}.elements`);
190
234
  }
191
235
  if (element.type === "select" && element.options) {
192
236
  const defaultValue = element.default;
@@ -203,8 +247,10 @@ function validateSchema(schema) {
203
247
  }
204
248
  });
205
249
  }
206
- if (Array.isArray(schema.elements))
250
+ if (Array.isArray(schema.elements)) {
207
251
  validateElements(schema.elements, "elements");
252
+ checkFlatOutputCollisions(schema.elements, "elements");
253
+ }
208
254
  return errors;
209
255
  }
210
256
 
@@ -228,6 +274,26 @@ function formatFileSize(bytes) {
228
274
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
229
275
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
230
276
  }
277
+ function serializeHiddenValue(value) {
278
+ if (value === null || value === void 0) return "";
279
+ return typeof value === "object" ? JSON.stringify(value) : String(value);
280
+ }
281
+ function deserializeHiddenValue(raw) {
282
+ if (raw === "") return null;
283
+ try {
284
+ return JSON.parse(raw);
285
+ } catch {
286
+ return raw;
287
+ }
288
+ }
289
+ function createHiddenInput(name, value) {
290
+ const input = document.createElement("input");
291
+ input.type = "hidden";
292
+ input.name = name;
293
+ input.setAttribute("data-hidden-field", "true");
294
+ input.value = serializeHiddenValue(value);
295
+ return input;
296
+ }
231
297
 
232
298
  // src/utils/enable-conditions.ts
233
299
  function getValueByPath(data, path) {
@@ -2036,6 +2102,26 @@ function updateSwitcherField(element, fieldPath, value, context) {
2036
2102
  }
2037
2103
 
2038
2104
  // src/components/file.ts
2105
+ function getAllowedExtensions(accept) {
2106
+ if (!accept) return [];
2107
+ if (typeof accept === "object" && Array.isArray(accept.extensions)) {
2108
+ return accept.extensions.map((ext) => ext.toLowerCase());
2109
+ }
2110
+ if (typeof accept === "string") {
2111
+ return accept.split(",").map((s) => s.trim()).filter((s) => s.startsWith(".")).map((s) => s.substring(1).toLowerCase());
2112
+ }
2113
+ return [];
2114
+ }
2115
+ function isFileExtensionAllowed(fileName, allowedExtensions) {
2116
+ var _a;
2117
+ if (allowedExtensions.length === 0) return true;
2118
+ const ext = ((_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "";
2119
+ return allowedExtensions.includes(ext);
2120
+ }
2121
+ function isFileSizeAllowed(file, maxSizeMB) {
2122
+ if (maxSizeMB === Infinity) return true;
2123
+ return file.size <= maxSizeMB * 1024 * 1024;
2124
+ }
2039
2125
  function renderLocalImagePreview(container, file, fileName, state) {
2040
2126
  const img = document.createElement("img");
2041
2127
  img.className = "w-full h-full object-contain";
@@ -2571,8 +2657,43 @@ function setEmptyFileContainer(fileContainer, state, hint) {
2571
2657
  </div>
2572
2658
  `;
2573
2659
  }
2574
- async function handleFileSelect(file, container, fieldName, state, deps = null, instance) {
2660
+ function showFileError(container, message) {
2575
2661
  var _a, _b;
2662
+ const existing = (_a = container.closest(".space-y-2")) == null ? void 0 : _a.querySelector(".file-error-message");
2663
+ if (existing) existing.remove();
2664
+ const errorEl = document.createElement("div");
2665
+ errorEl.className = "file-error-message error-message";
2666
+ errorEl.style.cssText = `
2667
+ color: var(--fb-error-color);
2668
+ font-size: var(--fb-font-size-small);
2669
+ margin-top: 0.25rem;
2670
+ `;
2671
+ errorEl.textContent = message;
2672
+ (_b = container.closest(".space-y-2")) == null ? void 0 : _b.appendChild(errorEl);
2673
+ }
2674
+ function clearFileError(container) {
2675
+ var _a;
2676
+ const existing = (_a = container.closest(".space-y-2")) == null ? void 0 : _a.querySelector(".file-error-message");
2677
+ if (existing) existing.remove();
2678
+ }
2679
+ async function handleFileSelect(file, container, fieldName, state, deps = null, instance, allowedExtensions = [], maxSizeMB = Infinity) {
2680
+ var _a, _b;
2681
+ if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
2682
+ const formats = allowedExtensions.join(", ");
2683
+ showFileError(
2684
+ container,
2685
+ t("invalidFileExtension", state, { name: file.name, formats })
2686
+ );
2687
+ return;
2688
+ }
2689
+ if (!isFileSizeAllowed(file, maxSizeMB)) {
2690
+ showFileError(
2691
+ container,
2692
+ t("fileTooLarge", state, { name: file.name, maxSize: maxSizeMB })
2693
+ );
2694
+ return;
2695
+ }
2696
+ clearFileError(container);
2576
2697
  let rid;
2577
2698
  if (state.config.uploadFile) {
2578
2699
  try {
@@ -2778,9 +2899,51 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
2778
2899
  hiddenInput.value = initial;
2779
2900
  fileWrapper.appendChild(hiddenInput);
2780
2901
  }
2781
- function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallback, pathKey, instance) {
2902
+ function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallback, constraints, pathKey, instance) {
2782
2903
  setupDragAndDrop(filesContainer, async (files) => {
2783
- const arr = Array.from(files);
2904
+ const allFiles = Array.from(files);
2905
+ const rejectedByExtension = allFiles.filter(
2906
+ (f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2907
+ );
2908
+ const afterExtension = allFiles.filter(
2909
+ (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2910
+ );
2911
+ const rejectedBySize = afterExtension.filter(
2912
+ (f) => !isFileSizeAllowed(f, constraints.maxSize)
2913
+ );
2914
+ const validFiles = afterExtension.filter(
2915
+ (f) => isFileSizeAllowed(f, constraints.maxSize)
2916
+ );
2917
+ const remaining = constraints.maxCount === Infinity ? validFiles.length : Math.max(0, constraints.maxCount - initialFiles.length);
2918
+ const arr = validFiles.slice(0, remaining);
2919
+ const skippedByCount = validFiles.length - arr.length;
2920
+ const errorParts = [];
2921
+ if (rejectedByExtension.length > 0) {
2922
+ const formats = constraints.allowedExtensions.join(", ");
2923
+ const names = rejectedByExtension.map((f) => f.name).join(", ");
2924
+ errorParts.push(
2925
+ t("invalidFileExtension", state, { name: names, formats })
2926
+ );
2927
+ }
2928
+ if (rejectedBySize.length > 0) {
2929
+ const names = rejectedBySize.map((f) => f.name).join(", ");
2930
+ errorParts.push(
2931
+ t("fileTooLarge", state, { name: names, maxSize: constraints.maxSize })
2932
+ );
2933
+ }
2934
+ if (skippedByCount > 0) {
2935
+ errorParts.push(
2936
+ t("filesLimitExceeded", state, {
2937
+ skipped: skippedByCount,
2938
+ max: constraints.maxCount
2939
+ })
2940
+ );
2941
+ }
2942
+ if (errorParts.length > 0) {
2943
+ showFileError(filesContainer, errorParts.join(" \u2022 "));
2944
+ } else {
2945
+ clearFileError(filesContainer);
2946
+ }
2784
2947
  for (const file of arr) {
2785
2948
  const rid = await uploadSingleFile(file, state);
2786
2949
  state.resourceIndex.set(rid, {
@@ -2798,10 +2961,57 @@ function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallba
2798
2961
  }
2799
2962
  });
2800
2963
  }
2801
- function setupFilesPickerHandler(filesPicker, initialFiles, state, updateCallback, pathKey, instance) {
2964
+ function setupFilesPickerHandler(filesPicker, initialFiles, state, updateCallback, constraints, pathKey, instance) {
2802
2965
  filesPicker.onchange = async () => {
2803
2966
  if (filesPicker.files) {
2804
- for (const file of Array.from(filesPicker.files)) {
2967
+ const allFiles = Array.from(filesPicker.files);
2968
+ const rejectedByExtension = allFiles.filter(
2969
+ (f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2970
+ );
2971
+ const afterExtension = allFiles.filter(
2972
+ (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2973
+ );
2974
+ const rejectedBySize = afterExtension.filter(
2975
+ (f) => !isFileSizeAllowed(f, constraints.maxSize)
2976
+ );
2977
+ const validFiles = afterExtension.filter(
2978
+ (f) => isFileSizeAllowed(f, constraints.maxSize)
2979
+ );
2980
+ const remaining = constraints.maxCount === Infinity ? validFiles.length : Math.max(0, constraints.maxCount - initialFiles.length);
2981
+ const arr = validFiles.slice(0, remaining);
2982
+ const skippedByCount = validFiles.length - arr.length;
2983
+ const errorParts = [];
2984
+ if (rejectedByExtension.length > 0) {
2985
+ const formats = constraints.allowedExtensions.join(", ");
2986
+ const names = rejectedByExtension.map((f) => f.name).join(", ");
2987
+ errorParts.push(
2988
+ t("invalidFileExtension", state, { name: names, formats })
2989
+ );
2990
+ }
2991
+ if (rejectedBySize.length > 0) {
2992
+ const names = rejectedBySize.map((f) => f.name).join(", ");
2993
+ errorParts.push(
2994
+ t("fileTooLarge", state, {
2995
+ name: names,
2996
+ maxSize: constraints.maxSize
2997
+ })
2998
+ );
2999
+ }
3000
+ if (skippedByCount > 0) {
3001
+ errorParts.push(
3002
+ t("filesLimitExceeded", state, {
3003
+ skipped: skippedByCount,
3004
+ max: constraints.maxCount
3005
+ })
3006
+ );
3007
+ }
3008
+ const wrapper = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
3009
+ if (errorParts.length > 0 && wrapper) {
3010
+ showFileError(wrapper, errorParts.join(" \u2022 "));
3011
+ } else if (wrapper) {
3012
+ clearFileError(wrapper);
3013
+ }
3014
+ for (const file of arr) {
2805
3015
  const rid = await uploadSingleFile(file, state);
2806
3016
  state.resourceIndex.set(rid, {
2807
3017
  name: file.name,
@@ -2821,7 +3031,7 @@ function setupFilesPickerHandler(filesPicker, initialFiles, state, updateCallbac
2821
3031
  };
2822
3032
  }
2823
3033
  function renderFileElement(element, ctx, wrapper, pathKey) {
2824
- var _a;
3034
+ var _a, _b;
2825
3035
  const state = ctx.state;
2826
3036
  if (state.config.readonly) {
2827
3037
  const initial = ctx.prefill[element.key];
@@ -2854,6 +3064,8 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2854
3064
  const fileContainer = document.createElement("div");
2855
3065
  fileContainer.className = "file-preview-container w-full aspect-square max-w-xs bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
2856
3066
  const initial = ctx.prefill[element.key];
3067
+ const allowedExts = getAllowedExtensions(element.accept);
3068
+ const maxSizeMB = (_b = element.maxSize) != null ? _b : Infinity;
2857
3069
  const fileUploadHandler = () => picker.click();
2858
3070
  const dragHandler = (files) => {
2859
3071
  if (files.length > 0) {
@@ -2864,7 +3076,9 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2864
3076
  pathKey,
2865
3077
  state,
2866
3078
  deps,
2867
- ctx.instance
3079
+ ctx.instance,
3080
+ allowedExts,
3081
+ maxSizeMB
2868
3082
  );
2869
3083
  }
2870
3084
  };
@@ -2896,7 +3110,9 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2896
3110
  pathKey,
2897
3111
  state,
2898
3112
  deps,
2899
- ctx.instance
3113
+ ctx.instance,
3114
+ allowedExts,
3115
+ maxSizeMB
2900
3116
  );
2901
3117
  }
2902
3118
  };
@@ -2906,7 +3122,7 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2906
3122
  }
2907
3123
  }
2908
3124
  function renderFilesElement(element, ctx, wrapper, pathKey) {
2909
- var _a;
3125
+ var _a, _b;
2910
3126
  const state = ctx.state;
2911
3127
  if (state.config.readonly) {
2912
3128
  const resultsWrapper = document.createElement("div");
@@ -2957,12 +3173,18 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2957
3173
  const initialFiles = ctx.prefill[element.key] || [];
2958
3174
  addPrefillFilesToIndex(initialFiles, state);
2959
3175
  const filesFieldHint = makeFieldHint(element, state);
3176
+ const filesConstraints = {
3177
+ maxCount: Infinity,
3178
+ allowedExtensions: getAllowedExtensions(element.accept),
3179
+ maxSize: (_b = element.maxSize) != null ? _b : Infinity
3180
+ };
2960
3181
  updateFilesList2();
2961
3182
  setupFilesDropHandler(
2962
3183
  filesContainer,
2963
3184
  initialFiles,
2964
3185
  state,
2965
3186
  updateFilesList2,
3187
+ filesConstraints,
2966
3188
  pathKey,
2967
3189
  ctx.instance
2968
3190
  );
@@ -2971,6 +3193,7 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2971
3193
  initialFiles,
2972
3194
  state,
2973
3195
  updateFilesList2,
3196
+ filesConstraints,
2974
3197
  pathKey,
2975
3198
  ctx.instance
2976
3199
  );
@@ -2981,7 +3204,7 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2981
3204
  }
2982
3205
  }
2983
3206
  function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2984
- var _a, _b, _c;
3207
+ var _a, _b, _c, _d;
2985
3208
  const state = ctx.state;
2986
3209
  const minFiles = (_a = element.minCount) != null ? _a : 0;
2987
3210
  const maxFiles = (_b = element.maxCount) != null ? _b : Infinity;
@@ -3019,6 +3242,11 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
3019
3242
  const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
3020
3243
  addPrefillFilesToIndex(initialFiles, state);
3021
3244
  const multipleFilesHint = makeFieldHint(element, state);
3245
+ const multipleConstraints = {
3246
+ maxCount: maxFiles,
3247
+ allowedExtensions: getAllowedExtensions(element.accept),
3248
+ maxSize: (_d = element.maxSize) != null ? _d : Infinity
3249
+ };
3022
3250
  const buildCountInfo = () => {
3023
3251
  const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
3024
3252
  const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
@@ -3042,6 +3270,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
3042
3270
  initialFiles,
3043
3271
  state,
3044
3272
  updateFilesDisplay,
3273
+ multipleConstraints,
3045
3274
  pathKey,
3046
3275
  ctx.instance
3047
3276
  );
@@ -3050,6 +3279,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
3050
3279
  initialFiles,
3051
3280
  state,
3052
3281
  updateFilesDisplay,
3282
+ multipleConstraints,
3053
3283
  pathKey,
3054
3284
  ctx.instance
3055
3285
  );
@@ -3078,6 +3308,40 @@ function validateFileElement(element, key, context) {
3078
3308
  errors.push(`${key2}: ${t("maxFiles", state, { max: maxFiles })}`);
3079
3309
  }
3080
3310
  };
3311
+ const validateFileExtensions = (key2, resourceIds, element2) => {
3312
+ var _a2;
3313
+ if (skipValidation) return;
3314
+ const { state } = context;
3315
+ const acceptField = "accept" in element2 ? element2.accept : void 0;
3316
+ const allowedExtensions = getAllowedExtensions(acceptField);
3317
+ if (allowedExtensions.length === 0) return;
3318
+ const formats = allowedExtensions.join(", ");
3319
+ for (const rid of resourceIds) {
3320
+ const meta = state.resourceIndex.get(rid);
3321
+ const fileName = (_a2 = meta == null ? void 0 : meta.name) != null ? _a2 : rid;
3322
+ if (!isFileExtensionAllowed(fileName, allowedExtensions)) {
3323
+ errors.push(
3324
+ `${key2}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
3325
+ );
3326
+ }
3327
+ }
3328
+ };
3329
+ const validateFileSizes = (key2, resourceIds, element2) => {
3330
+ var _a2;
3331
+ if (skipValidation) return;
3332
+ const { state } = context;
3333
+ const maxSizeMB = "maxSize" in element2 ? (_a2 = element2.maxSize) != null ? _a2 : Infinity : Infinity;
3334
+ if (maxSizeMB === Infinity) return;
3335
+ for (const rid of resourceIds) {
3336
+ const meta = state.resourceIndex.get(rid);
3337
+ if (!meta) continue;
3338
+ if (meta.size > maxSizeMB * 1024 * 1024) {
3339
+ errors.push(
3340
+ `${key2}: ${t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })}`
3341
+ );
3342
+ }
3343
+ }
3344
+ };
3081
3345
  if (isMultipleField) {
3082
3346
  const fullKey = pathJoin(path, key);
3083
3347
  const pickerInput = scopeRoot.querySelector(
@@ -3096,6 +3360,8 @@ function validateFileElement(element, key, context) {
3096
3360
  });
3097
3361
  }
3098
3362
  validateFileCount(key, resourceIds, element);
3363
+ validateFileExtensions(key, resourceIds, element);
3364
+ validateFileSizes(key, resourceIds, element);
3099
3365
  return { value: resourceIds, errors };
3100
3366
  } else {
3101
3367
  const input = scopeRoot.querySelector(
@@ -3106,6 +3372,10 @@ function validateFileElement(element, key, context) {
3106
3372
  errors.push(`${key}: ${t("required", context.state)}`);
3107
3373
  return { value: null, errors };
3108
3374
  }
3375
+ if (!skipValidation && rid !== "") {
3376
+ validateFileExtensions(key, [rid], element);
3377
+ validateFileSizes(key, [rid], element);
3378
+ }
3109
3379
  return { value: rid || null, errors };
3110
3380
  }
3111
3381
  }
@@ -4306,7 +4576,11 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
4306
4576
  state: ctx.state
4307
4577
  };
4308
4578
  element.elements.forEach((child) => {
4309
- if (!child.hidden) {
4579
+ var _a2, _b2;
4580
+ if (child.hidden || child.type === "hidden") {
4581
+ const prefillVal = (_b2 = (_a2 = containerPrefill[child.key]) != null ? _a2 : child.default) != null ? _b2 : null;
4582
+ itemsWrap.appendChild(createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal));
4583
+ } else {
4310
4584
  itemsWrap.appendChild(renderElement(child, subCtx));
4311
4585
  }
4312
4586
  });
@@ -4374,7 +4648,10 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
4374
4648
  childWrapper.className = `grid grid-cols-${columns} gap-4`;
4375
4649
  }
4376
4650
  element.elements.forEach((child) => {
4377
- if (!child.hidden) {
4651
+ var _a2;
4652
+ if (child.hidden || child.type === "hidden") {
4653
+ childWrapper.appendChild(createHiddenInput(pathJoin(subCtx.path, child.key), (_a2 = child.default) != null ? _a2 : null));
4654
+ } else {
4378
4655
  childWrapper.appendChild(renderElement(child, subCtx));
4379
4656
  }
4380
4657
  });
@@ -4444,7 +4721,11 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
4444
4721
  childWrapper.className = `grid grid-cols-${columns} gap-4`;
4445
4722
  }
4446
4723
  element.elements.forEach((child) => {
4447
- if (!child.hidden) {
4724
+ var _a3, _b2;
4725
+ if (child.hidden || child.type === "hidden") {
4726
+ const prefillVal = (_b2 = (_a3 = prefillObj == null ? void 0 : prefillObj[child.key]) != null ? _a3 : child.default) != null ? _b2 : null;
4727
+ childWrapper.appendChild(createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal));
4728
+ } else {
4448
4729
  childWrapper.appendChild(renderElement(child, subCtx));
4449
4730
  }
4450
4731
  });
@@ -4494,7 +4775,10 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
4494
4775
  childWrapper.className = `grid grid-cols-${columns} gap-4`;
4495
4776
  }
4496
4777
  element.elements.forEach((child) => {
4497
- if (!child.hidden) {
4778
+ var _a2;
4779
+ if (child.hidden || child.type === "hidden") {
4780
+ childWrapper.appendChild(createHiddenInput(pathJoin(subCtx.path, child.key), (_a2 = child.default) != null ? _a2 : null));
4781
+ } else {
4498
4782
  childWrapper.appendChild(renderElement(child, subCtx));
4499
4783
  }
4500
4784
  });
@@ -4607,15 +4891,16 @@ function validateContainerElement(element, key, context) {
4607
4891
  );
4608
4892
  }
4609
4893
  }
4610
- if (child.hidden || child.type === "hidden") {
4611
- itemData[child.key] = child.default !== void 0 ? child.default : null;
4894
+ const childKey = `${key}[${domIndex}].${child.key}`;
4895
+ const childResult = validateElement(
4896
+ { ...child, key: childKey },
4897
+ { path },
4898
+ itemContainer
4899
+ );
4900
+ if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
4901
+ Object.assign(itemData, childResult.value);
4612
4902
  } else {
4613
- const childKey = `${key}[${domIndex}].${child.key}`;
4614
- itemData[child.key] = validateElement(
4615
- { ...child, key: childKey },
4616
- { path },
4617
- itemContainer
4618
- );
4903
+ itemData[child.key] = childResult.value;
4619
4904
  }
4620
4905
  });
4621
4906
  items.push(itemData);
@@ -4647,15 +4932,18 @@ function validateContainerElement(element, key, context) {
4647
4932
  );
4648
4933
  }
4649
4934
  }
4650
- if (child.hidden || child.type === "hidden") {
4651
- containerData[child.key] = child.default !== void 0 ? child.default : null;
4652
- } else {
4935
+ {
4653
4936
  const childKey = `${key}.${child.key}`;
4654
- containerData[child.key] = validateElement(
4937
+ const childResult = validateElement(
4655
4938
  { ...child, key: childKey },
4656
4939
  { path },
4657
4940
  containerContainer
4658
4941
  );
4942
+ if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
4943
+ Object.assign(containerData, childResult.value);
4944
+ } else {
4945
+ containerData[child.key] = childResult.value;
4946
+ }
4659
4947
  }
4660
4948
  });
4661
4949
  return { value: containerData, errors };
@@ -4676,11 +4964,24 @@ function updateContainerField(element, fieldPath, value, context) {
4676
4964
  value.forEach((itemValue, index) => {
4677
4965
  if (isPlainObject(itemValue)) {
4678
4966
  element.elements.forEach((childElement) => {
4679
- const childKey = childElement.key;
4680
- const childPath = `${fieldPath}[${index}].${childKey}`;
4681
- const childValue = itemValue[childKey];
4682
- if (childValue !== void 0) {
4683
- instance.updateField(childPath, childValue);
4967
+ var _a, _b;
4968
+ const childPath = `${fieldPath}[${index}].${childElement.key}`;
4969
+ if (childElement.type === "richinput" && childElement.flatOutput) {
4970
+ const richChild = childElement;
4971
+ const textKey = (_a = richChild.textKey) != null ? _a : "text";
4972
+ const filesKey = (_b = richChild.filesKey) != null ? _b : "files";
4973
+ const containerValue = itemValue;
4974
+ const compositeValue = {};
4975
+ if (textKey in containerValue) compositeValue[textKey] = containerValue[textKey];
4976
+ if (filesKey in containerValue) compositeValue[filesKey] = containerValue[filesKey];
4977
+ if (Object.keys(compositeValue).length > 0) {
4978
+ instance.updateField(childPath, compositeValue);
4979
+ }
4980
+ } else {
4981
+ const childValue = itemValue[childElement.key];
4982
+ if (childValue !== void 0) {
4983
+ instance.updateField(childPath, childValue);
4984
+ }
4684
4985
  }
4685
4986
  });
4686
4987
  }
@@ -4701,11 +5002,24 @@ function updateContainerField(element, fieldPath, value, context) {
4701
5002
  return;
4702
5003
  }
4703
5004
  element.elements.forEach((childElement) => {
4704
- const childKey = childElement.key;
4705
- const childPath = `${fieldPath}.${childKey}`;
4706
- const childValue = value[childKey];
4707
- if (childValue !== void 0) {
4708
- instance.updateField(childPath, childValue);
5005
+ var _a, _b;
5006
+ const childPath = `${fieldPath}.${childElement.key}`;
5007
+ if (childElement.type === "richinput" && childElement.flatOutput) {
5008
+ const richChild = childElement;
5009
+ const textKey = (_a = richChild.textKey) != null ? _a : "text";
5010
+ const filesKey = (_b = richChild.filesKey) != null ? _b : "files";
5011
+ const containerValue = value;
5012
+ const compositeValue = {};
5013
+ if (textKey in containerValue) compositeValue[textKey] = containerValue[textKey];
5014
+ if (filesKey in containerValue) compositeValue[filesKey] = containerValue[filesKey];
5015
+ if (Object.keys(compositeValue).length > 0) {
5016
+ instance.updateField(childPath, compositeValue);
5017
+ }
5018
+ } else {
5019
+ const childValue = value[childElement.key];
5020
+ if (childValue !== void 0) {
5021
+ instance.updateField(childPath, childValue);
5022
+ }
4709
5023
  }
4710
5024
  });
4711
5025
  }
@@ -6540,6 +6854,42 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6540
6854
  outerDiv.style.borderColor = "var(--fb-border-color, #d1d5db)";
6541
6855
  outerDiv.style.boxShadow = "none";
6542
6856
  });
6857
+ const errorEl = document.createElement("div");
6858
+ errorEl.className = "fb-richinput-error";
6859
+ errorEl.style.cssText = "display: none; color: var(--fb-error-color, #ef4444); font-size: var(--fb-font-size-small, 12px); padding: 4px 14px 8px;";
6860
+ let errorTimer = null;
6861
+ function showUploadError(message) {
6862
+ errorEl.textContent = message;
6863
+ errorEl.style.display = "block";
6864
+ if (errorTimer) clearTimeout(errorTimer);
6865
+ errorTimer = setTimeout(() => {
6866
+ errorEl.style.display = "none";
6867
+ errorEl.textContent = "";
6868
+ errorTimer = null;
6869
+ }, 5e3);
6870
+ }
6871
+ function validateFileForUpload(file) {
6872
+ var _a2;
6873
+ const allowedExtensions = getAllowedExtensions(element.accept);
6874
+ if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
6875
+ const formats = allowedExtensions.join(", ");
6876
+ showUploadError(
6877
+ t("invalidFileExtension", state, { name: file.name, formats })
6878
+ );
6879
+ return false;
6880
+ }
6881
+ const maxSizeMB = (_a2 = element.maxSize) != null ? _a2 : Infinity;
6882
+ if (!isFileSizeAllowed(file, maxSizeMB)) {
6883
+ showUploadError(
6884
+ t("fileTooLarge", state, {
6885
+ name: file.name,
6886
+ maxSize: maxSizeMB
6887
+ })
6888
+ );
6889
+ return false;
6890
+ }
6891
+ return true;
6892
+ }
6543
6893
  let dragCounter = 0;
6544
6894
  outerDiv.addEventListener("dragenter", (e) => {
6545
6895
  e.preventDefault();
@@ -6568,7 +6918,17 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6568
6918
  const droppedFiles = (_a2 = e.dataTransfer) == null ? void 0 : _a2.files;
6569
6919
  if (!droppedFiles || !state.config.uploadFile) return;
6570
6920
  const maxFiles = (_b = element.maxFiles) != null ? _b : Infinity;
6571
- for (let i = 0; i < droppedFiles.length && files.length < maxFiles; i++) {
6921
+ for (let i = 0; i < droppedFiles.length; i++) {
6922
+ if (files.length >= maxFiles) {
6923
+ showUploadError(
6924
+ t("filesLimitExceeded", state, {
6925
+ skipped: droppedFiles.length - i,
6926
+ max: maxFiles
6927
+ })
6928
+ );
6929
+ break;
6930
+ }
6931
+ if (!validateFileForUpload(droppedFiles[i])) continue;
6572
6932
  uploadFile(droppedFiles[i]);
6573
6933
  }
6574
6934
  });
@@ -6728,9 +7088,13 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6728
7088
  paperclipBtn.addEventListener("click", () => {
6729
7089
  var _a2;
6730
7090
  const maxFiles = (_a2 = element.maxFiles) != null ? _a2 : Infinity;
6731
- if (files.length < maxFiles) {
6732
- fileInput.click();
7091
+ if (files.length >= maxFiles) {
7092
+ showUploadError(
7093
+ t("filesLimitExceeded", state, { skipped: 1, max: maxFiles })
7094
+ );
7095
+ return;
6733
7096
  }
7097
+ fileInput.click();
6734
7098
  });
6735
7099
  const dropdown = document.createElement("div");
6736
7100
  dropdown.className = "fb-richinput-dropdown";
@@ -7051,7 +7415,17 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7051
7415
  const selected = fileInput.files;
7052
7416
  if (!selected || selected.length === 0) return;
7053
7417
  const maxFiles = (_a2 = element.maxFiles) != null ? _a2 : Infinity;
7054
- for (let i = 0; i < selected.length && files.length < maxFiles; i++) {
7418
+ for (let i = 0; i < selected.length; i++) {
7419
+ if (files.length >= maxFiles) {
7420
+ showUploadError(
7421
+ t("filesLimitExceeded", state, {
7422
+ skipped: selected.length - i,
7423
+ max: maxFiles
7424
+ })
7425
+ );
7426
+ break;
7427
+ }
7428
+ if (!validateFileForUpload(selected[i])) continue;
7055
7429
  uploadFile(selected[i]);
7056
7430
  }
7057
7431
  fileInput.value = "";
@@ -7062,6 +7436,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7062
7436
  textareaArea.appendChild(dropdown);
7063
7437
  outerDiv.appendChild(filesRow);
7064
7438
  outerDiv.appendChild(textareaArea);
7439
+ outerDiv.appendChild(errorEl);
7065
7440
  if (element.minLength != null || element.maxLength != null) {
7066
7441
  const counterRow = document.createElement("div");
7067
7442
  counterRow.style.cssText = "position: relative; padding: 2px 14px 6px; text-align: right;";
@@ -7224,20 +7599,29 @@ function renderRichInputElement(element, ctx, wrapper, pathKey) {
7224
7599
  const state = ctx.state;
7225
7600
  const textKey = (_a = element.textKey) != null ? _a : "text";
7226
7601
  const filesKey = (_b = element.filesKey) != null ? _b : "files";
7227
- const rawPrefill = ctx.prefill[element.key];
7228
7602
  let initialValue;
7229
- if (rawPrefill && typeof rawPrefill === "object" && !Array.isArray(rawPrefill)) {
7230
- const obj = rawPrefill;
7231
- const textVal = (_c = obj[textKey]) != null ? _c : obj["text"];
7232
- const filesVal = (_d = obj[filesKey]) != null ? _d : obj["files"];
7603
+ if (element.flatOutput) {
7604
+ const textVal = ctx.prefill[textKey];
7605
+ const filesVal = ctx.prefill[filesKey];
7233
7606
  initialValue = {
7234
7607
  text: typeof textVal === "string" ? textVal : null,
7235
7608
  files: Array.isArray(filesVal) ? filesVal : []
7236
7609
  };
7237
- } else if (typeof rawPrefill === "string") {
7238
- initialValue = { text: rawPrefill || null, files: [] };
7239
7610
  } else {
7240
- initialValue = { text: null, files: [] };
7611
+ const rawPrefill = ctx.prefill[element.key];
7612
+ if (rawPrefill && typeof rawPrefill === "object" && !Array.isArray(rawPrefill)) {
7613
+ const obj = rawPrefill;
7614
+ const textVal = (_c = obj[textKey]) != null ? _c : obj["text"];
7615
+ const filesVal = (_d = obj[filesKey]) != null ? _d : obj["files"];
7616
+ initialValue = {
7617
+ text: typeof textVal === "string" ? textVal : null,
7618
+ files: Array.isArray(filesVal) ? filesVal : []
7619
+ };
7620
+ } else if (typeof rawPrefill === "string") {
7621
+ initialValue = { text: rawPrefill || null, files: [] };
7622
+ } else {
7623
+ initialValue = { text: null, files: [] };
7624
+ }
7241
7625
  }
7242
7626
  for (const rid of initialValue.files) {
7243
7627
  if (!state.resourceIndex.has(rid)) {
@@ -7318,7 +7702,7 @@ function validateRichInputElement(element, key, context) {
7318
7702
  );
7319
7703
  }
7320
7704
  }
7321
- return { value, errors };
7705
+ return { value, errors, spread: !!element.flatOutput };
7322
7706
  }
7323
7707
  function updateRichInputField(element, fieldPath, value, context) {
7324
7708
  var _a, _b, _c, _d;
@@ -7798,6 +8182,9 @@ var defaultConfig = {
7798
8182
  invalidHexColour: "Invalid hex color",
7799
8183
  minFiles: "Minimum {min} files required",
7800
8184
  maxFiles: "Maximum {max} files allowed",
8185
+ invalidFileExtension: 'File "{name}" has unsupported format. Allowed: {formats}',
8186
+ fileTooLarge: 'File "{name}" exceeds maximum size of {maxSize}MB',
8187
+ filesLimitExceeded: "{skipped} file(s) skipped: maximum {max} files allowed",
7801
8188
  unsupportedFieldType: "Unsupported field type: {type}",
7802
8189
  invalidOption: "Invalid option",
7803
8190
  tableAddRow: "Add row",
@@ -7857,6 +8244,9 @@ var defaultConfig = {
7857
8244
  invalidHexColour: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u0446\u0432\u0435\u0442\u0430",
7858
8245
  minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
7859
8246
  maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
8247
+ invalidFileExtension: '\u0424\u0430\u0439\u043B "{name}" \u0438\u043C\u0435\u0435\u0442 \u043D\u0435\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442. \u0414\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0435: {formats}',
8248
+ fileTooLarge: '\u0424\u0430\u0439\u043B "{name}" \u043F\u0440\u0435\u0432\u044B\u0448\u0430\u0435\u0442 \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 {maxSize}\u041C\u0411',
8249
+ filesLimitExceeded: "{skipped} \u0444\u0430\u0439\u043B(\u043E\u0432) \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u043E: \u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
7860
8250
  unsupportedFieldType: "\u041D\u0435\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0439 \u0442\u0438\u043F \u043F\u043E\u043B\u044F: {type}",
7861
8251
  invalidOption: "\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
7862
8252
  tableAddRow: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443",
@@ -7901,7 +8291,8 @@ function createInstanceState(config) {
7901
8291
  ...config,
7902
8292
  translations: mergedTranslations
7903
8293
  },
7904
- debounceTimer: null
8294
+ debounceTimer: null,
8295
+ prefill: {}
7905
8296
  };
7906
8297
  }
7907
8298
  function generateInstanceId() {
@@ -8099,6 +8490,27 @@ function applyActionButtonStyles(button, isFormLevel = false) {
8099
8490
  }
8100
8491
 
8101
8492
  // src/components/registry.ts
8493
+ function validateHiddenElement(element, key, context) {
8494
+ var _a;
8495
+ const { scopeRoot } = context;
8496
+ const input = scopeRoot.querySelector(
8497
+ `input[type="hidden"][data-hidden-field="true"][name="${key}"]`
8498
+ );
8499
+ const raw = (_a = input == null ? void 0 : input.value) != null ? _a : "";
8500
+ if (raw === "") {
8501
+ const defaultVal = "default" in element ? element.default : null;
8502
+ return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
8503
+ }
8504
+ return { value: deserializeHiddenValue(raw), errors: [] };
8505
+ }
8506
+ function updateHiddenField(_element, fieldPath, value, context) {
8507
+ const { scopeRoot } = context;
8508
+ const input = scopeRoot.querySelector(
8509
+ `input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
8510
+ );
8511
+ if (!input) return;
8512
+ input.value = serializeHiddenValue(value);
8513
+ }
8102
8514
  var componentRegistry = {
8103
8515
  text: {
8104
8516
  validate: validateTextElement,
@@ -8153,6 +8565,11 @@ var componentRegistry = {
8153
8565
  richinput: {
8154
8566
  validate: validateRichInputElement,
8155
8567
  update: updateRichInputField
8568
+ },
8569
+ hidden: {
8570
+ // Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
8571
+ validate: validateHiddenElement,
8572
+ update: updateHiddenField
8156
8573
  }
8157
8574
  };
8158
8575
  function getComponentOperations(elementType) {
@@ -8562,6 +8979,7 @@ var FormBuilderInstance = class {
8562
8979
  this.state.formRoot = root;
8563
8980
  this.state.schema = schema;
8564
8981
  this.state.externalActions = actions || null;
8982
+ this.state.prefill = prefill || {};
8565
8983
  clear(root);
8566
8984
  root.setAttribute("data-fb-root", "true");
8567
8985
  injectThemeVariables(root, this.state.config.theme);
@@ -8579,7 +8997,10 @@ var FormBuilderInstance = class {
8579
8997
  fieldsWrapper.className = `grid grid-cols-${columns} gap-4`;
8580
8998
  }
8581
8999
  schema.elements.forEach((element) => {
8582
- if (element.hidden) {
9000
+ var _a, _b;
9001
+ if (element.hidden || element.type === "hidden") {
9002
+ const val = (_b = (_a = prefill == null ? void 0 : prefill[element.key]) != null ? _a : element.default) != null ? _b : null;
9003
+ fieldsWrapper.appendChild(createHiddenInput(element.key, val));
8583
9004
  return;
8584
9005
  }
8585
9006
  const block = renderElement2(element, {
@@ -8628,13 +9049,14 @@ var FormBuilderInstance = class {
8628
9049
  );
8629
9050
  if (componentResult !== null) {
8630
9051
  errors.push(...componentResult.errors);
8631
- return componentResult.value;
9052
+ return { value: componentResult.value, spread: !!componentResult.spread };
8632
9053
  }
8633
9054
  console.warn(`Unknown field type "${element.type}" for key "${key}"`);
8634
- return null;
9055
+ return { value: null, spread: false };
8635
9056
  };
8636
9057
  setValidateElement(validateElement2);
8637
9058
  this.state.schema.elements.forEach((element) => {
9059
+ var _a;
8638
9060
  if (element.enableIf) {
8639
9061
  try {
8640
9062
  const shouldEnable = evaluateEnableCondition(
@@ -8651,10 +9073,23 @@ var FormBuilderInstance = class {
8651
9073
  );
8652
9074
  }
8653
9075
  }
8654
- if (element.hidden) {
8655
- data[element.key] = element.default !== void 0 ? element.default : null;
9076
+ if (element.hidden || element.type === "hidden") {
9077
+ const hiddenInput = this.state.formRoot.querySelector(
9078
+ `input[type="hidden"][data-hidden-field="true"][name="${element.key}"]`
9079
+ );
9080
+ const raw = (_a = hiddenInput == null ? void 0 : hiddenInput.value) != null ? _a : "";
9081
+ if (raw !== "") {
9082
+ data[element.key] = deserializeHiddenValue(raw);
9083
+ } else {
9084
+ data[element.key] = element.default !== void 0 ? element.default : null;
9085
+ }
8656
9086
  } else {
8657
- data[element.key] = validateElement2(element, { path: "" });
9087
+ const result = validateElement2(element, { path: "" });
9088
+ if (result.spread && result.value !== null && typeof result.value === "object") {
9089
+ Object.assign(data, result.value);
9090
+ } else {
9091
+ data[element.key] = result.value;
9092
+ }
8658
9093
  }
8659
9094
  });
8660
9095
  return {
@@ -8748,6 +9183,24 @@ var FormBuilderInstance = class {
8748
9183
  }
8749
9184
  return data;
8750
9185
  }
9186
+ /**
9187
+ * Build a map from flat output keys (textKey/filesKey) to the richinput schema element info.
9188
+ * Used by setFormData to detect flat richinput keys and remap them to their composite values.
9189
+ */
9190
+ buildFlatKeyMap(elements) {
9191
+ var _a, _b;
9192
+ const map = /* @__PURE__ */ new Map();
9193
+ for (const el of elements) {
9194
+ if (el.type === "richinput" && el.flatOutput) {
9195
+ const richEl = el;
9196
+ const textKey = (_a = richEl.textKey) != null ? _a : "text";
9197
+ const filesKey = (_b = richEl.filesKey) != null ? _b : "files";
9198
+ map.set(textKey, { schemaKey: el.key, role: "text" });
9199
+ map.set(filesKey, { schemaKey: el.key, role: "files" });
9200
+ }
9201
+ }
9202
+ return map;
9203
+ }
8751
9204
  /**
8752
9205
  * Set form data - update multiple fields without full re-render
8753
9206
  * @param data - Object with field paths and their values
@@ -8759,8 +9212,21 @@ var FormBuilderInstance = class {
8759
9212
  );
8760
9213
  return;
8761
9214
  }
9215
+ const flatKeyMap = this.buildFlatKeyMap(this.state.schema.elements);
9216
+ const flatUpdates = /* @__PURE__ */ new Map();
8762
9217
  for (const fieldPath in data) {
8763
- this.updateField(fieldPath, data[fieldPath]);
9218
+ const flatInfo = flatKeyMap.get(fieldPath);
9219
+ if (flatInfo) {
9220
+ if (!flatUpdates.has(flatInfo.schemaKey)) {
9221
+ flatUpdates.set(flatInfo.schemaKey, {});
9222
+ }
9223
+ flatUpdates.get(flatInfo.schemaKey)[fieldPath] = data[fieldPath];
9224
+ } else {
9225
+ this.updateField(fieldPath, data[fieldPath]);
9226
+ }
9227
+ }
9228
+ for (const [schemaKey, compositeValue] of flatUpdates) {
9229
+ this.updateField(schemaKey, compositeValue);
8764
9230
  }
8765
9231
  }
8766
9232
  /**