@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.
package/dist/esm/index.js CHANGED
@@ -117,6 +117,48 @@ function validateSchema(schema) {
117
117
  });
118
118
  }
119
119
  }
120
+ function checkFlatOutputCollisions(elements, scopePath) {
121
+ const allOutputKeys = /* @__PURE__ */ new Set();
122
+ for (const el of elements) {
123
+ if (el.type === "richinput" && el.flatOutput) {
124
+ const richEl = el;
125
+ const textKey = richEl.textKey ?? "text";
126
+ const filesKey = richEl.filesKey ?? "files";
127
+ for (const otherEl of elements) {
128
+ if (otherEl === el) continue;
129
+ if (otherEl.key === textKey) {
130
+ errors.push(
131
+ `${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with element key "${otherEl.key}"`
132
+ );
133
+ }
134
+ if (otherEl.key === filesKey) {
135
+ errors.push(
136
+ `${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with element key "${otherEl.key}"`
137
+ );
138
+ }
139
+ }
140
+ if (allOutputKeys.has(textKey)) {
141
+ errors.push(
142
+ `${scopePath}: RichInput "${el.key}" flatOutput textKey "${textKey}" collides with another flatOutput key`
143
+ );
144
+ }
145
+ if (allOutputKeys.has(filesKey)) {
146
+ errors.push(
147
+ `${scopePath}: RichInput "${el.key}" flatOutput filesKey "${filesKey}" collides with another flatOutput key`
148
+ );
149
+ }
150
+ allOutputKeys.add(textKey);
151
+ allOutputKeys.add(filesKey);
152
+ } else {
153
+ if (allOutputKeys.has(el.key)) {
154
+ errors.push(
155
+ `${scopePath}: Element key "${el.key}" collides with a flatOutput richinput key`
156
+ );
157
+ }
158
+ allOutputKeys.add(el.key);
159
+ }
160
+ }
161
+ }
120
162
  function validateElements(elements, path) {
121
163
  elements.forEach((element, index) => {
122
164
  const elementPath = `${path}[${index}]`;
@@ -182,6 +224,7 @@ function validateSchema(schema) {
182
224
  }
183
225
  }
184
226
  validateElements(element.elements, `${elementPath}.elements`);
227
+ checkFlatOutputCollisions(element.elements, `${elementPath}.elements`);
185
228
  }
186
229
  if (element.type === "select" && element.options) {
187
230
  const defaultValue = element.default;
@@ -198,8 +241,10 @@ function validateSchema(schema) {
198
241
  }
199
242
  });
200
243
  }
201
- if (Array.isArray(schema.elements))
244
+ if (Array.isArray(schema.elements)) {
202
245
  validateElements(schema.elements, "elements");
246
+ checkFlatOutputCollisions(schema.elements, "elements");
247
+ }
203
248
  return errors;
204
249
  }
205
250
 
@@ -223,6 +268,26 @@ function formatFileSize(bytes) {
223
268
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
224
269
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
225
270
  }
271
+ function serializeHiddenValue(value) {
272
+ if (value === null || value === void 0) return "";
273
+ return typeof value === "object" ? JSON.stringify(value) : String(value);
274
+ }
275
+ function deserializeHiddenValue(raw) {
276
+ if (raw === "") return null;
277
+ try {
278
+ return JSON.parse(raw);
279
+ } catch {
280
+ return raw;
281
+ }
282
+ }
283
+ function createHiddenInput(name, value) {
284
+ const input = document.createElement("input");
285
+ input.type = "hidden";
286
+ input.name = name;
287
+ input.setAttribute("data-hidden-field", "true");
288
+ input.value = serializeHiddenValue(value);
289
+ return input;
290
+ }
226
291
 
227
292
  // src/utils/enable-conditions.ts
228
293
  function getValueByPath(data, path) {
@@ -2006,6 +2071,25 @@ function updateSwitcherField(element, fieldPath, value, context) {
2006
2071
  }
2007
2072
 
2008
2073
  // src/components/file.ts
2074
+ function getAllowedExtensions(accept) {
2075
+ if (!accept) return [];
2076
+ if (typeof accept === "object" && Array.isArray(accept.extensions)) {
2077
+ return accept.extensions.map((ext) => ext.toLowerCase());
2078
+ }
2079
+ if (typeof accept === "string") {
2080
+ return accept.split(",").map((s) => s.trim()).filter((s) => s.startsWith(".")).map((s) => s.substring(1).toLowerCase());
2081
+ }
2082
+ return [];
2083
+ }
2084
+ function isFileExtensionAllowed(fileName, allowedExtensions) {
2085
+ if (allowedExtensions.length === 0) return true;
2086
+ const ext = fileName.split(".").pop()?.toLowerCase() || "";
2087
+ return allowedExtensions.includes(ext);
2088
+ }
2089
+ function isFileSizeAllowed(file, maxSizeMB) {
2090
+ if (maxSizeMB === Infinity) return true;
2091
+ return file.size <= maxSizeMB * 1024 * 1024;
2092
+ }
2009
2093
  function renderLocalImagePreview(container, file, fileName, state) {
2010
2094
  const img = document.createElement("img");
2011
2095
  img.className = "w-full h-full object-contain";
@@ -2535,7 +2619,40 @@ function setEmptyFileContainer(fileContainer, state, hint) {
2535
2619
  </div>
2536
2620
  `;
2537
2621
  }
2538
- async function handleFileSelect(file, container, fieldName, state, deps = null, instance) {
2622
+ function showFileError(container, message) {
2623
+ const existing = container.closest(".space-y-2")?.querySelector(".file-error-message");
2624
+ if (existing) existing.remove();
2625
+ const errorEl = document.createElement("div");
2626
+ errorEl.className = "file-error-message error-message";
2627
+ errorEl.style.cssText = `
2628
+ color: var(--fb-error-color);
2629
+ font-size: var(--fb-font-size-small);
2630
+ margin-top: 0.25rem;
2631
+ `;
2632
+ errorEl.textContent = message;
2633
+ container.closest(".space-y-2")?.appendChild(errorEl);
2634
+ }
2635
+ function clearFileError(container) {
2636
+ const existing = container.closest(".space-y-2")?.querySelector(".file-error-message");
2637
+ if (existing) existing.remove();
2638
+ }
2639
+ async function handleFileSelect(file, container, fieldName, state, deps = null, instance, allowedExtensions = [], maxSizeMB = Infinity) {
2640
+ if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
2641
+ const formats = allowedExtensions.join(", ");
2642
+ showFileError(
2643
+ container,
2644
+ t("invalidFileExtension", state, { name: file.name, formats })
2645
+ );
2646
+ return;
2647
+ }
2648
+ if (!isFileSizeAllowed(file, maxSizeMB)) {
2649
+ showFileError(
2650
+ container,
2651
+ t("fileTooLarge", state, { name: file.name, maxSize: maxSizeMB })
2652
+ );
2653
+ return;
2654
+ }
2655
+ clearFileError(container);
2539
2656
  let rid;
2540
2657
  if (state.config.uploadFile) {
2541
2658
  try {
@@ -2738,9 +2855,51 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
2738
2855
  hiddenInput.value = initial;
2739
2856
  fileWrapper.appendChild(hiddenInput);
2740
2857
  }
2741
- function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallback, pathKey, instance) {
2858
+ function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallback, constraints, pathKey, instance) {
2742
2859
  setupDragAndDrop(filesContainer, async (files) => {
2743
- const arr = Array.from(files);
2860
+ const allFiles = Array.from(files);
2861
+ const rejectedByExtension = allFiles.filter(
2862
+ (f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2863
+ );
2864
+ const afterExtension = allFiles.filter(
2865
+ (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2866
+ );
2867
+ const rejectedBySize = afterExtension.filter(
2868
+ (f) => !isFileSizeAllowed(f, constraints.maxSize)
2869
+ );
2870
+ const validFiles = afterExtension.filter(
2871
+ (f) => isFileSizeAllowed(f, constraints.maxSize)
2872
+ );
2873
+ const remaining = constraints.maxCount === Infinity ? validFiles.length : Math.max(0, constraints.maxCount - initialFiles.length);
2874
+ const arr = validFiles.slice(0, remaining);
2875
+ const skippedByCount = validFiles.length - arr.length;
2876
+ const errorParts = [];
2877
+ if (rejectedByExtension.length > 0) {
2878
+ const formats = constraints.allowedExtensions.join(", ");
2879
+ const names = rejectedByExtension.map((f) => f.name).join(", ");
2880
+ errorParts.push(
2881
+ t("invalidFileExtension", state, { name: names, formats })
2882
+ );
2883
+ }
2884
+ if (rejectedBySize.length > 0) {
2885
+ const names = rejectedBySize.map((f) => f.name).join(", ");
2886
+ errorParts.push(
2887
+ t("fileTooLarge", state, { name: names, maxSize: constraints.maxSize })
2888
+ );
2889
+ }
2890
+ if (skippedByCount > 0) {
2891
+ errorParts.push(
2892
+ t("filesLimitExceeded", state, {
2893
+ skipped: skippedByCount,
2894
+ max: constraints.maxCount
2895
+ })
2896
+ );
2897
+ }
2898
+ if (errorParts.length > 0) {
2899
+ showFileError(filesContainer, errorParts.join(" \u2022 "));
2900
+ } else {
2901
+ clearFileError(filesContainer);
2902
+ }
2744
2903
  for (const file of arr) {
2745
2904
  const rid = await uploadSingleFile(file, state);
2746
2905
  state.resourceIndex.set(rid, {
@@ -2758,10 +2917,57 @@ function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallba
2758
2917
  }
2759
2918
  });
2760
2919
  }
2761
- function setupFilesPickerHandler(filesPicker, initialFiles, state, updateCallback, pathKey, instance) {
2920
+ function setupFilesPickerHandler(filesPicker, initialFiles, state, updateCallback, constraints, pathKey, instance) {
2762
2921
  filesPicker.onchange = async () => {
2763
2922
  if (filesPicker.files) {
2764
- for (const file of Array.from(filesPicker.files)) {
2923
+ const allFiles = Array.from(filesPicker.files);
2924
+ const rejectedByExtension = allFiles.filter(
2925
+ (f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2926
+ );
2927
+ const afterExtension = allFiles.filter(
2928
+ (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2929
+ );
2930
+ const rejectedBySize = afterExtension.filter(
2931
+ (f) => !isFileSizeAllowed(f, constraints.maxSize)
2932
+ );
2933
+ const validFiles = afterExtension.filter(
2934
+ (f) => isFileSizeAllowed(f, constraints.maxSize)
2935
+ );
2936
+ const remaining = constraints.maxCount === Infinity ? validFiles.length : Math.max(0, constraints.maxCount - initialFiles.length);
2937
+ const arr = validFiles.slice(0, remaining);
2938
+ const skippedByCount = validFiles.length - arr.length;
2939
+ const errorParts = [];
2940
+ if (rejectedByExtension.length > 0) {
2941
+ const formats = constraints.allowedExtensions.join(", ");
2942
+ const names = rejectedByExtension.map((f) => f.name).join(", ");
2943
+ errorParts.push(
2944
+ t("invalidFileExtension", state, { name: names, formats })
2945
+ );
2946
+ }
2947
+ if (rejectedBySize.length > 0) {
2948
+ const names = rejectedBySize.map((f) => f.name).join(", ");
2949
+ errorParts.push(
2950
+ t("fileTooLarge", state, {
2951
+ name: names,
2952
+ maxSize: constraints.maxSize
2953
+ })
2954
+ );
2955
+ }
2956
+ if (skippedByCount > 0) {
2957
+ errorParts.push(
2958
+ t("filesLimitExceeded", state, {
2959
+ skipped: skippedByCount,
2960
+ max: constraints.maxCount
2961
+ })
2962
+ );
2963
+ }
2964
+ const wrapper = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
2965
+ if (errorParts.length > 0 && wrapper) {
2966
+ showFileError(wrapper, errorParts.join(" \u2022 "));
2967
+ } else if (wrapper) {
2968
+ clearFileError(wrapper);
2969
+ }
2970
+ for (const file of arr) {
2765
2971
  const rid = await uploadSingleFile(file, state);
2766
2972
  state.resourceIndex.set(rid, {
2767
2973
  name: file.name,
@@ -2813,6 +3019,8 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2813
3019
  const fileContainer = document.createElement("div");
2814
3020
  fileContainer.className = "file-preview-container w-full aspect-square max-w-xs bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
2815
3021
  const initial = ctx.prefill[element.key];
3022
+ const allowedExts = getAllowedExtensions(element.accept);
3023
+ const maxSizeMB = element.maxSize ?? Infinity;
2816
3024
  const fileUploadHandler = () => picker.click();
2817
3025
  const dragHandler = (files) => {
2818
3026
  if (files.length > 0) {
@@ -2823,7 +3031,9 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2823
3031
  pathKey,
2824
3032
  state,
2825
3033
  deps,
2826
- ctx.instance
3034
+ ctx.instance,
3035
+ allowedExts,
3036
+ maxSizeMB
2827
3037
  );
2828
3038
  }
2829
3039
  };
@@ -2855,7 +3065,9 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2855
3065
  pathKey,
2856
3066
  state,
2857
3067
  deps,
2858
- ctx.instance
3068
+ ctx.instance,
3069
+ allowedExts,
3070
+ maxSizeMB
2859
3071
  );
2860
3072
  }
2861
3073
  };
@@ -2915,12 +3127,18 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2915
3127
  const initialFiles = ctx.prefill[element.key] || [];
2916
3128
  addPrefillFilesToIndex(initialFiles, state);
2917
3129
  const filesFieldHint = makeFieldHint(element, state);
3130
+ const filesConstraints = {
3131
+ maxCount: Infinity,
3132
+ allowedExtensions: getAllowedExtensions(element.accept),
3133
+ maxSize: element.maxSize ?? Infinity
3134
+ };
2918
3135
  updateFilesList2();
2919
3136
  setupFilesDropHandler(
2920
3137
  filesContainer,
2921
3138
  initialFiles,
2922
3139
  state,
2923
3140
  updateFilesList2,
3141
+ filesConstraints,
2924
3142
  pathKey,
2925
3143
  ctx.instance
2926
3144
  );
@@ -2929,6 +3147,7 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2929
3147
  initialFiles,
2930
3148
  state,
2931
3149
  updateFilesList2,
3150
+ filesConstraints,
2932
3151
  pathKey,
2933
3152
  ctx.instance
2934
3153
  );
@@ -2976,6 +3195,11 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2976
3195
  const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
2977
3196
  addPrefillFilesToIndex(initialFiles, state);
2978
3197
  const multipleFilesHint = makeFieldHint(element, state);
3198
+ const multipleConstraints = {
3199
+ maxCount: maxFiles,
3200
+ allowedExtensions: getAllowedExtensions(element.accept),
3201
+ maxSize: element.maxSize ?? Infinity
3202
+ };
2979
3203
  const buildCountInfo = () => {
2980
3204
  const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
2981
3205
  const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
@@ -2999,6 +3223,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2999
3223
  initialFiles,
3000
3224
  state,
3001
3225
  updateFilesDisplay,
3226
+ multipleConstraints,
3002
3227
  pathKey,
3003
3228
  ctx.instance
3004
3229
  );
@@ -3007,6 +3232,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
3007
3232
  initialFiles,
3008
3233
  state,
3009
3234
  updateFilesDisplay,
3235
+ multipleConstraints,
3010
3236
  pathKey,
3011
3237
  ctx.instance
3012
3238
  );
@@ -3033,6 +3259,38 @@ function validateFileElement(element, key, context) {
3033
3259
  errors.push(`${key2}: ${t("maxFiles", state, { max: maxFiles })}`);
3034
3260
  }
3035
3261
  };
3262
+ const validateFileExtensions = (key2, resourceIds, element2) => {
3263
+ if (skipValidation) return;
3264
+ const { state } = context;
3265
+ const acceptField = "accept" in element2 ? element2.accept : void 0;
3266
+ const allowedExtensions = getAllowedExtensions(acceptField);
3267
+ if (allowedExtensions.length === 0) return;
3268
+ const formats = allowedExtensions.join(", ");
3269
+ for (const rid of resourceIds) {
3270
+ const meta = state.resourceIndex.get(rid);
3271
+ const fileName = meta?.name ?? rid;
3272
+ if (!isFileExtensionAllowed(fileName, allowedExtensions)) {
3273
+ errors.push(
3274
+ `${key2}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
3275
+ );
3276
+ }
3277
+ }
3278
+ };
3279
+ const validateFileSizes = (key2, resourceIds, element2) => {
3280
+ if (skipValidation) return;
3281
+ const { state } = context;
3282
+ const maxSizeMB = "maxSize" in element2 ? element2.maxSize ?? Infinity : Infinity;
3283
+ if (maxSizeMB === Infinity) return;
3284
+ for (const rid of resourceIds) {
3285
+ const meta = state.resourceIndex.get(rid);
3286
+ if (!meta) continue;
3287
+ if (meta.size > maxSizeMB * 1024 * 1024) {
3288
+ errors.push(
3289
+ `${key2}: ${t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })}`
3290
+ );
3291
+ }
3292
+ }
3293
+ };
3036
3294
  if (isMultipleField) {
3037
3295
  const fullKey = pathJoin(path, key);
3038
3296
  const pickerInput = scopeRoot.querySelector(
@@ -3051,6 +3309,8 @@ function validateFileElement(element, key, context) {
3051
3309
  });
3052
3310
  }
3053
3311
  validateFileCount(key, resourceIds, element);
3312
+ validateFileExtensions(key, resourceIds, element);
3313
+ validateFileSizes(key, resourceIds, element);
3054
3314
  return { value: resourceIds, errors };
3055
3315
  } else {
3056
3316
  const input = scopeRoot.querySelector(
@@ -3061,6 +3321,10 @@ function validateFileElement(element, key, context) {
3061
3321
  errors.push(`${key}: ${t("required", context.state)}`);
3062
3322
  return { value: null, errors };
3063
3323
  }
3324
+ if (!skipValidation && rid !== "") {
3325
+ validateFileExtensions(key, [rid], element);
3326
+ validateFileSizes(key, [rid], element);
3327
+ }
3064
3328
  return { value: rid || null, errors };
3065
3329
  }
3066
3330
  }
@@ -4248,7 +4512,10 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
4248
4512
  state: ctx.state
4249
4513
  };
4250
4514
  element.elements.forEach((child) => {
4251
- if (!child.hidden) {
4515
+ if (child.hidden || child.type === "hidden") {
4516
+ const prefillVal = containerPrefill[child.key] ?? child.default ?? null;
4517
+ itemsWrap.appendChild(createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal));
4518
+ } else {
4252
4519
  itemsWrap.appendChild(renderElement(child, subCtx));
4253
4520
  }
4254
4521
  });
@@ -4315,7 +4582,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
4315
4582
  childWrapper.className = `grid grid-cols-${columns} gap-4`;
4316
4583
  }
4317
4584
  element.elements.forEach((child) => {
4318
- if (!child.hidden) {
4585
+ if (child.hidden || child.type === "hidden") {
4586
+ childWrapper.appendChild(createHiddenInput(pathJoin(subCtx.path, child.key), child.default ?? null));
4587
+ } else {
4319
4588
  childWrapper.appendChild(renderElement(child, subCtx));
4320
4589
  }
4321
4590
  });
@@ -4384,7 +4653,10 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
4384
4653
  childWrapper.className = `grid grid-cols-${columns} gap-4`;
4385
4654
  }
4386
4655
  element.elements.forEach((child) => {
4387
- if (!child.hidden) {
4656
+ if (child.hidden || child.type === "hidden") {
4657
+ const prefillVal = prefillObj?.[child.key] ?? child.default ?? null;
4658
+ childWrapper.appendChild(createHiddenInput(pathJoin(subCtx.path, child.key), prefillVal));
4659
+ } else {
4388
4660
  childWrapper.appendChild(renderElement(child, subCtx));
4389
4661
  }
4390
4662
  });
@@ -4434,7 +4706,9 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
4434
4706
  childWrapper.className = `grid grid-cols-${columns} gap-4`;
4435
4707
  }
4436
4708
  element.elements.forEach((child) => {
4437
- if (!child.hidden) {
4709
+ if (child.hidden || child.type === "hidden") {
4710
+ childWrapper.appendChild(createHiddenInput(pathJoin(subCtx.path, child.key), child.default ?? null));
4711
+ } else {
4438
4712
  childWrapper.appendChild(renderElement(child, subCtx));
4439
4713
  }
4440
4714
  });
@@ -4545,15 +4819,16 @@ function validateContainerElement(element, key, context) {
4545
4819
  );
4546
4820
  }
4547
4821
  }
4548
- if (child.hidden || child.type === "hidden") {
4549
- itemData[child.key] = child.default !== void 0 ? child.default : null;
4822
+ const childKey = `${key}[${domIndex}].${child.key}`;
4823
+ const childResult = validateElement(
4824
+ { ...child, key: childKey },
4825
+ { path },
4826
+ itemContainer
4827
+ );
4828
+ if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
4829
+ Object.assign(itemData, childResult.value);
4550
4830
  } else {
4551
- const childKey = `${key}[${domIndex}].${child.key}`;
4552
- itemData[child.key] = validateElement(
4553
- { ...child, key: childKey },
4554
- { path },
4555
- itemContainer
4556
- );
4831
+ itemData[child.key] = childResult.value;
4557
4832
  }
4558
4833
  });
4559
4834
  items.push(itemData);
@@ -4584,15 +4859,18 @@ function validateContainerElement(element, key, context) {
4584
4859
  );
4585
4860
  }
4586
4861
  }
4587
- if (child.hidden || child.type === "hidden") {
4588
- containerData[child.key] = child.default !== void 0 ? child.default : null;
4589
- } else {
4862
+ {
4590
4863
  const childKey = `${key}.${child.key}`;
4591
- containerData[child.key] = validateElement(
4864
+ const childResult = validateElement(
4592
4865
  { ...child, key: childKey },
4593
4866
  { path },
4594
4867
  containerContainer
4595
4868
  );
4869
+ if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
4870
+ Object.assign(containerData, childResult.value);
4871
+ } else {
4872
+ containerData[child.key] = childResult.value;
4873
+ }
4596
4874
  }
4597
4875
  });
4598
4876
  return { value: containerData, errors };
@@ -4613,11 +4891,23 @@ function updateContainerField(element, fieldPath, value, context) {
4613
4891
  value.forEach((itemValue, index) => {
4614
4892
  if (isPlainObject(itemValue)) {
4615
4893
  element.elements.forEach((childElement) => {
4616
- const childKey = childElement.key;
4617
- const childPath = `${fieldPath}[${index}].${childKey}`;
4618
- const childValue = itemValue[childKey];
4619
- if (childValue !== void 0) {
4620
- instance.updateField(childPath, childValue);
4894
+ const childPath = `${fieldPath}[${index}].${childElement.key}`;
4895
+ if (childElement.type === "richinput" && childElement.flatOutput) {
4896
+ const richChild = childElement;
4897
+ const textKey = richChild.textKey ?? "text";
4898
+ const filesKey = richChild.filesKey ?? "files";
4899
+ const containerValue = itemValue;
4900
+ const compositeValue = {};
4901
+ if (textKey in containerValue) compositeValue[textKey] = containerValue[textKey];
4902
+ if (filesKey in containerValue) compositeValue[filesKey] = containerValue[filesKey];
4903
+ if (Object.keys(compositeValue).length > 0) {
4904
+ instance.updateField(childPath, compositeValue);
4905
+ }
4906
+ } else {
4907
+ const childValue = itemValue[childElement.key];
4908
+ if (childValue !== void 0) {
4909
+ instance.updateField(childPath, childValue);
4910
+ }
4621
4911
  }
4622
4912
  });
4623
4913
  }
@@ -4638,11 +4928,23 @@ function updateContainerField(element, fieldPath, value, context) {
4638
4928
  return;
4639
4929
  }
4640
4930
  element.elements.forEach((childElement) => {
4641
- const childKey = childElement.key;
4642
- const childPath = `${fieldPath}.${childKey}`;
4643
- const childValue = value[childKey];
4644
- if (childValue !== void 0) {
4645
- instance.updateField(childPath, childValue);
4931
+ const childPath = `${fieldPath}.${childElement.key}`;
4932
+ if (childElement.type === "richinput" && childElement.flatOutput) {
4933
+ const richChild = childElement;
4934
+ const textKey = richChild.textKey ?? "text";
4935
+ const filesKey = richChild.filesKey ?? "files";
4936
+ const containerValue = value;
4937
+ const compositeValue = {};
4938
+ if (textKey in containerValue) compositeValue[textKey] = containerValue[textKey];
4939
+ if (filesKey in containerValue) compositeValue[filesKey] = containerValue[filesKey];
4940
+ if (Object.keys(compositeValue).length > 0) {
4941
+ instance.updateField(childPath, compositeValue);
4942
+ }
4943
+ } else {
4944
+ const childValue = value[childElement.key];
4945
+ if (childValue !== void 0) {
4946
+ instance.updateField(childPath, childValue);
4947
+ }
4646
4948
  }
4647
4949
  });
4648
4950
  }
@@ -6436,6 +6738,41 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6436
6738
  outerDiv.style.borderColor = "var(--fb-border-color, #d1d5db)";
6437
6739
  outerDiv.style.boxShadow = "none";
6438
6740
  });
6741
+ const errorEl = document.createElement("div");
6742
+ errorEl.className = "fb-richinput-error";
6743
+ errorEl.style.cssText = "display: none; color: var(--fb-error-color, #ef4444); font-size: var(--fb-font-size-small, 12px); padding: 4px 14px 8px;";
6744
+ let errorTimer = null;
6745
+ function showUploadError(message) {
6746
+ errorEl.textContent = message;
6747
+ errorEl.style.display = "block";
6748
+ if (errorTimer) clearTimeout(errorTimer);
6749
+ errorTimer = setTimeout(() => {
6750
+ errorEl.style.display = "none";
6751
+ errorEl.textContent = "";
6752
+ errorTimer = null;
6753
+ }, 5e3);
6754
+ }
6755
+ function validateFileForUpload(file) {
6756
+ const allowedExtensions = getAllowedExtensions(element.accept);
6757
+ if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
6758
+ const formats = allowedExtensions.join(", ");
6759
+ showUploadError(
6760
+ t("invalidFileExtension", state, { name: file.name, formats })
6761
+ );
6762
+ return false;
6763
+ }
6764
+ const maxSizeMB = element.maxSize ?? Infinity;
6765
+ if (!isFileSizeAllowed(file, maxSizeMB)) {
6766
+ showUploadError(
6767
+ t("fileTooLarge", state, {
6768
+ name: file.name,
6769
+ maxSize: maxSizeMB
6770
+ })
6771
+ );
6772
+ return false;
6773
+ }
6774
+ return true;
6775
+ }
6439
6776
  let dragCounter = 0;
6440
6777
  outerDiv.addEventListener("dragenter", (e) => {
6441
6778
  e.preventDefault();
@@ -6463,7 +6800,17 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6463
6800
  const droppedFiles = e.dataTransfer?.files;
6464
6801
  if (!droppedFiles || !state.config.uploadFile) return;
6465
6802
  const maxFiles = element.maxFiles ?? Infinity;
6466
- for (let i = 0; i < droppedFiles.length && files.length < maxFiles; i++) {
6803
+ for (let i = 0; i < droppedFiles.length; i++) {
6804
+ if (files.length >= maxFiles) {
6805
+ showUploadError(
6806
+ t("filesLimitExceeded", state, {
6807
+ skipped: droppedFiles.length - i,
6808
+ max: maxFiles
6809
+ })
6810
+ );
6811
+ break;
6812
+ }
6813
+ if (!validateFileForUpload(droppedFiles[i])) continue;
6467
6814
  uploadFile(droppedFiles[i]);
6468
6815
  }
6469
6816
  });
@@ -6619,9 +6966,13 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6619
6966
  });
6620
6967
  paperclipBtn.addEventListener("click", () => {
6621
6968
  const maxFiles = element.maxFiles ?? Infinity;
6622
- if (files.length < maxFiles) {
6623
- fileInput.click();
6969
+ if (files.length >= maxFiles) {
6970
+ showUploadError(
6971
+ t("filesLimitExceeded", state, { skipped: 1, max: maxFiles })
6972
+ );
6973
+ return;
6624
6974
  }
6975
+ fileInput.click();
6625
6976
  });
6626
6977
  const dropdown = document.createElement("div");
6627
6978
  dropdown.className = "fb-richinput-dropdown";
@@ -6929,7 +7280,17 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6929
7280
  const selected = fileInput.files;
6930
7281
  if (!selected || selected.length === 0) return;
6931
7282
  const maxFiles = element.maxFiles ?? Infinity;
6932
- for (let i = 0; i < selected.length && files.length < maxFiles; i++) {
7283
+ for (let i = 0; i < selected.length; i++) {
7284
+ if (files.length >= maxFiles) {
7285
+ showUploadError(
7286
+ t("filesLimitExceeded", state, {
7287
+ skipped: selected.length - i,
7288
+ max: maxFiles
7289
+ })
7290
+ );
7291
+ break;
7292
+ }
7293
+ if (!validateFileForUpload(selected[i])) continue;
6933
7294
  uploadFile(selected[i]);
6934
7295
  }
6935
7296
  fileInput.value = "";
@@ -6940,6 +7301,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6940
7301
  textareaArea.appendChild(dropdown);
6941
7302
  outerDiv.appendChild(filesRow);
6942
7303
  outerDiv.appendChild(textareaArea);
7304
+ outerDiv.appendChild(errorEl);
6943
7305
  if (element.minLength != null || element.maxLength != null) {
6944
7306
  const counterRow = document.createElement("div");
6945
7307
  counterRow.style.cssText = "position: relative; padding: 2px 14px 6px; text-align: right;";
@@ -7099,20 +7461,29 @@ function renderRichInputElement(element, ctx, wrapper, pathKey) {
7099
7461
  const state = ctx.state;
7100
7462
  const textKey = element.textKey ?? "text";
7101
7463
  const filesKey = element.filesKey ?? "files";
7102
- const rawPrefill = ctx.prefill[element.key];
7103
7464
  let initialValue;
7104
- if (rawPrefill && typeof rawPrefill === "object" && !Array.isArray(rawPrefill)) {
7105
- const obj = rawPrefill;
7106
- const textVal = obj[textKey] ?? obj["text"];
7107
- const filesVal = obj[filesKey] ?? obj["files"];
7465
+ if (element.flatOutput) {
7466
+ const textVal = ctx.prefill[textKey];
7467
+ const filesVal = ctx.prefill[filesKey];
7108
7468
  initialValue = {
7109
7469
  text: typeof textVal === "string" ? textVal : null,
7110
7470
  files: Array.isArray(filesVal) ? filesVal : []
7111
7471
  };
7112
- } else if (typeof rawPrefill === "string") {
7113
- initialValue = { text: rawPrefill || null, files: [] };
7114
7472
  } else {
7115
- initialValue = { text: null, files: [] };
7473
+ const rawPrefill = ctx.prefill[element.key];
7474
+ if (rawPrefill && typeof rawPrefill === "object" && !Array.isArray(rawPrefill)) {
7475
+ const obj = rawPrefill;
7476
+ const textVal = obj[textKey] ?? obj["text"];
7477
+ const filesVal = obj[filesKey] ?? obj["files"];
7478
+ initialValue = {
7479
+ text: typeof textVal === "string" ? textVal : null,
7480
+ files: Array.isArray(filesVal) ? filesVal : []
7481
+ };
7482
+ } else if (typeof rawPrefill === "string") {
7483
+ initialValue = { text: rawPrefill || null, files: [] };
7484
+ } else {
7485
+ initialValue = { text: null, files: [] };
7486
+ }
7116
7487
  }
7117
7488
  for (const rid of initialValue.files) {
7118
7489
  if (!state.resourceIndex.has(rid)) {
@@ -7192,7 +7563,7 @@ function validateRichInputElement(element, key, context) {
7192
7563
  );
7193
7564
  }
7194
7565
  }
7195
- return { value, errors };
7566
+ return { value, errors, spread: !!element.flatOutput };
7196
7567
  }
7197
7568
  function updateRichInputField(element, fieldPath, value, context) {
7198
7569
  const { scopeRoot } = context;
@@ -7668,6 +8039,9 @@ var defaultConfig = {
7668
8039
  invalidHexColour: "Invalid hex color",
7669
8040
  minFiles: "Minimum {min} files required",
7670
8041
  maxFiles: "Maximum {max} files allowed",
8042
+ invalidFileExtension: 'File "{name}" has unsupported format. Allowed: {formats}',
8043
+ fileTooLarge: 'File "{name}" exceeds maximum size of {maxSize}MB',
8044
+ filesLimitExceeded: "{skipped} file(s) skipped: maximum {max} files allowed",
7671
8045
  unsupportedFieldType: "Unsupported field type: {type}",
7672
8046
  invalidOption: "Invalid option",
7673
8047
  tableAddRow: "Add row",
@@ -7727,6 +8101,9 @@ var defaultConfig = {
7727
8101
  invalidHexColour: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u0446\u0432\u0435\u0442\u0430",
7728
8102
  minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
7729
8103
  maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
8104
+ 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}',
8105
+ 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',
8106
+ 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",
7730
8107
  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}",
7731
8108
  invalidOption: "\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
7732
8109
  tableAddRow: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443",
@@ -7771,7 +8148,8 @@ function createInstanceState(config) {
7771
8148
  ...config,
7772
8149
  translations: mergedTranslations
7773
8150
  },
7774
- debounceTimer: null
8151
+ debounceTimer: null,
8152
+ prefill: {}
7775
8153
  };
7776
8154
  }
7777
8155
  function generateInstanceId() {
@@ -7969,6 +8347,26 @@ function applyActionButtonStyles(button, isFormLevel = false) {
7969
8347
  }
7970
8348
 
7971
8349
  // src/components/registry.ts
8350
+ function validateHiddenElement(element, key, context) {
8351
+ const { scopeRoot } = context;
8352
+ const input = scopeRoot.querySelector(
8353
+ `input[type="hidden"][data-hidden-field="true"][name="${key}"]`
8354
+ );
8355
+ const raw = input?.value ?? "";
8356
+ if (raw === "") {
8357
+ const defaultVal = "default" in element ? element.default : null;
8358
+ return { value: defaultVal !== void 0 ? defaultVal : null, errors: [] };
8359
+ }
8360
+ return { value: deserializeHiddenValue(raw), errors: [] };
8361
+ }
8362
+ function updateHiddenField(_element, fieldPath, value, context) {
8363
+ const { scopeRoot } = context;
8364
+ const input = scopeRoot.querySelector(
8365
+ `input[type="hidden"][data-hidden-field="true"][name="${fieldPath}"]`
8366
+ );
8367
+ if (!input) return;
8368
+ input.value = serializeHiddenValue(value);
8369
+ }
7972
8370
  var componentRegistry = {
7973
8371
  text: {
7974
8372
  validate: validateTextElement,
@@ -8023,6 +8421,11 @@ var componentRegistry = {
8023
8421
  richinput: {
8024
8422
  validate: validateRichInputElement,
8025
8423
  update: updateRichInputField
8424
+ },
8425
+ hidden: {
8426
+ // Legacy type: `type: "hidden"` — reads/writes DOM <input type="hidden"> element
8427
+ validate: validateHiddenElement,
8428
+ update: updateHiddenField
8026
8429
  }
8027
8430
  };
8028
8431
  function getComponentOperations(elementType) {
@@ -8432,6 +8835,7 @@ var FormBuilderInstance = class {
8432
8835
  this.state.formRoot = root;
8433
8836
  this.state.schema = schema;
8434
8837
  this.state.externalActions = actions || null;
8838
+ this.state.prefill = prefill || {};
8435
8839
  clear(root);
8436
8840
  root.setAttribute("data-fb-root", "true");
8437
8841
  injectThemeVariables(root, this.state.config.theme);
@@ -8449,7 +8853,9 @@ var FormBuilderInstance = class {
8449
8853
  fieldsWrapper.className = `grid grid-cols-${columns} gap-4`;
8450
8854
  }
8451
8855
  schema.elements.forEach((element) => {
8452
- if (element.hidden) {
8856
+ if (element.hidden || element.type === "hidden") {
8857
+ const val = prefill?.[element.key] ?? element.default ?? null;
8858
+ fieldsWrapper.appendChild(createHiddenInput(element.key, val));
8453
8859
  return;
8454
8860
  }
8455
8861
  const block = renderElement2(element, {
@@ -8498,10 +8904,10 @@ var FormBuilderInstance = class {
8498
8904
  );
8499
8905
  if (componentResult !== null) {
8500
8906
  errors.push(...componentResult.errors);
8501
- return componentResult.value;
8907
+ return { value: componentResult.value, spread: !!componentResult.spread };
8502
8908
  }
8503
8909
  console.warn(`Unknown field type "${element.type}" for key "${key}"`);
8504
- return null;
8910
+ return { value: null, spread: false };
8505
8911
  };
8506
8912
  setValidateElement(validateElement2);
8507
8913
  this.state.schema.elements.forEach((element) => {
@@ -8521,10 +8927,23 @@ var FormBuilderInstance = class {
8521
8927
  );
8522
8928
  }
8523
8929
  }
8524
- if (element.hidden) {
8525
- data[element.key] = element.default !== void 0 ? element.default : null;
8930
+ if (element.hidden || element.type === "hidden") {
8931
+ const hiddenInput = this.state.formRoot.querySelector(
8932
+ `input[type="hidden"][data-hidden-field="true"][name="${element.key}"]`
8933
+ );
8934
+ const raw = hiddenInput?.value ?? "";
8935
+ if (raw !== "") {
8936
+ data[element.key] = deserializeHiddenValue(raw);
8937
+ } else {
8938
+ data[element.key] = element.default !== void 0 ? element.default : null;
8939
+ }
8526
8940
  } else {
8527
- data[element.key] = validateElement2(element, { path: "" });
8941
+ const result = validateElement2(element, { path: "" });
8942
+ if (result.spread && result.value !== null && typeof result.value === "object") {
8943
+ Object.assign(data, result.value);
8944
+ } else {
8945
+ data[element.key] = result.value;
8946
+ }
8528
8947
  }
8529
8948
  });
8530
8949
  return {
@@ -8618,6 +9037,23 @@ var FormBuilderInstance = class {
8618
9037
  }
8619
9038
  return data;
8620
9039
  }
9040
+ /**
9041
+ * Build a map from flat output keys (textKey/filesKey) to the richinput schema element info.
9042
+ * Used by setFormData to detect flat richinput keys and remap them to their composite values.
9043
+ */
9044
+ buildFlatKeyMap(elements) {
9045
+ const map = /* @__PURE__ */ new Map();
9046
+ for (const el of elements) {
9047
+ if (el.type === "richinput" && el.flatOutput) {
9048
+ const richEl = el;
9049
+ const textKey = richEl.textKey ?? "text";
9050
+ const filesKey = richEl.filesKey ?? "files";
9051
+ map.set(textKey, { schemaKey: el.key, role: "text" });
9052
+ map.set(filesKey, { schemaKey: el.key, role: "files" });
9053
+ }
9054
+ }
9055
+ return map;
9056
+ }
8621
9057
  /**
8622
9058
  * Set form data - update multiple fields without full re-render
8623
9059
  * @param data - Object with field paths and their values
@@ -8629,8 +9065,21 @@ var FormBuilderInstance = class {
8629
9065
  );
8630
9066
  return;
8631
9067
  }
9068
+ const flatKeyMap = this.buildFlatKeyMap(this.state.schema.elements);
9069
+ const flatUpdates = /* @__PURE__ */ new Map();
8632
9070
  for (const fieldPath in data) {
8633
- this.updateField(fieldPath, data[fieldPath]);
9071
+ const flatInfo = flatKeyMap.get(fieldPath);
9072
+ if (flatInfo) {
9073
+ if (!flatUpdates.has(flatInfo.schemaKey)) {
9074
+ flatUpdates.set(flatInfo.schemaKey, {});
9075
+ }
9076
+ flatUpdates.get(flatInfo.schemaKey)[fieldPath] = data[fieldPath];
9077
+ } else {
9078
+ this.updateField(fieldPath, data[fieldPath]);
9079
+ }
9080
+ }
9081
+ for (const [schemaKey, compositeValue] of flatUpdates) {
9082
+ this.updateField(schemaKey, compositeValue);
8634
9083
  }
8635
9084
  }
8636
9085
  /**