@dmitryvim/form-builder 0.2.22 → 0.2.23

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
 
@@ -2036,6 +2082,26 @@ function updateSwitcherField(element, fieldPath, value, context) {
2036
2082
  }
2037
2083
 
2038
2084
  // src/components/file.ts
2085
+ function getAllowedExtensions(accept) {
2086
+ if (!accept) return [];
2087
+ if (typeof accept === "object" && Array.isArray(accept.extensions)) {
2088
+ return accept.extensions.map((ext) => ext.toLowerCase());
2089
+ }
2090
+ if (typeof accept === "string") {
2091
+ return accept.split(",").map((s) => s.trim()).filter((s) => s.startsWith(".")).map((s) => s.substring(1).toLowerCase());
2092
+ }
2093
+ return [];
2094
+ }
2095
+ function isFileExtensionAllowed(fileName, allowedExtensions) {
2096
+ var _a;
2097
+ if (allowedExtensions.length === 0) return true;
2098
+ const ext = ((_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "";
2099
+ return allowedExtensions.includes(ext);
2100
+ }
2101
+ function isFileSizeAllowed(file, maxSizeMB) {
2102
+ if (maxSizeMB === Infinity) return true;
2103
+ return file.size <= maxSizeMB * 1024 * 1024;
2104
+ }
2039
2105
  function renderLocalImagePreview(container, file, fileName, state) {
2040
2106
  const img = document.createElement("img");
2041
2107
  img.className = "w-full h-full object-contain";
@@ -2571,8 +2637,43 @@ function setEmptyFileContainer(fileContainer, state, hint) {
2571
2637
  </div>
2572
2638
  `;
2573
2639
  }
2574
- async function handleFileSelect(file, container, fieldName, state, deps = null, instance) {
2640
+ function showFileError(container, message) {
2575
2641
  var _a, _b;
2642
+ const existing = (_a = container.closest(".space-y-2")) == null ? void 0 : _a.querySelector(".file-error-message");
2643
+ if (existing) existing.remove();
2644
+ const errorEl = document.createElement("div");
2645
+ errorEl.className = "file-error-message error-message";
2646
+ errorEl.style.cssText = `
2647
+ color: var(--fb-error-color);
2648
+ font-size: var(--fb-font-size-small);
2649
+ margin-top: 0.25rem;
2650
+ `;
2651
+ errorEl.textContent = message;
2652
+ (_b = container.closest(".space-y-2")) == null ? void 0 : _b.appendChild(errorEl);
2653
+ }
2654
+ function clearFileError(container) {
2655
+ var _a;
2656
+ const existing = (_a = container.closest(".space-y-2")) == null ? void 0 : _a.querySelector(".file-error-message");
2657
+ if (existing) existing.remove();
2658
+ }
2659
+ async function handleFileSelect(file, container, fieldName, state, deps = null, instance, allowedExtensions = [], maxSizeMB = Infinity) {
2660
+ var _a, _b;
2661
+ if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
2662
+ const formats = allowedExtensions.join(", ");
2663
+ showFileError(
2664
+ container,
2665
+ t("invalidFileExtension", state, { name: file.name, formats })
2666
+ );
2667
+ return;
2668
+ }
2669
+ if (!isFileSizeAllowed(file, maxSizeMB)) {
2670
+ showFileError(
2671
+ container,
2672
+ t("fileTooLarge", state, { name: file.name, maxSize: maxSizeMB })
2673
+ );
2674
+ return;
2675
+ }
2676
+ clearFileError(container);
2576
2677
  let rid;
2577
2678
  if (state.config.uploadFile) {
2578
2679
  try {
@@ -2778,9 +2879,51 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
2778
2879
  hiddenInput.value = initial;
2779
2880
  fileWrapper.appendChild(hiddenInput);
2780
2881
  }
2781
- function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallback, pathKey, instance) {
2882
+ function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallback, constraints, pathKey, instance) {
2782
2883
  setupDragAndDrop(filesContainer, async (files) => {
2783
- const arr = Array.from(files);
2884
+ const allFiles = Array.from(files);
2885
+ const rejectedByExtension = allFiles.filter(
2886
+ (f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2887
+ );
2888
+ const afterExtension = allFiles.filter(
2889
+ (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2890
+ );
2891
+ const rejectedBySize = afterExtension.filter(
2892
+ (f) => !isFileSizeAllowed(f, constraints.maxSize)
2893
+ );
2894
+ const validFiles = afterExtension.filter(
2895
+ (f) => isFileSizeAllowed(f, constraints.maxSize)
2896
+ );
2897
+ const remaining = constraints.maxCount === Infinity ? validFiles.length : Math.max(0, constraints.maxCount - initialFiles.length);
2898
+ const arr = validFiles.slice(0, remaining);
2899
+ const skippedByCount = validFiles.length - arr.length;
2900
+ const errorParts = [];
2901
+ if (rejectedByExtension.length > 0) {
2902
+ const formats = constraints.allowedExtensions.join(", ");
2903
+ const names = rejectedByExtension.map((f) => f.name).join(", ");
2904
+ errorParts.push(
2905
+ t("invalidFileExtension", state, { name: names, formats })
2906
+ );
2907
+ }
2908
+ if (rejectedBySize.length > 0) {
2909
+ const names = rejectedBySize.map((f) => f.name).join(", ");
2910
+ errorParts.push(
2911
+ t("fileTooLarge", state, { name: names, maxSize: constraints.maxSize })
2912
+ );
2913
+ }
2914
+ if (skippedByCount > 0) {
2915
+ errorParts.push(
2916
+ t("filesLimitExceeded", state, {
2917
+ skipped: skippedByCount,
2918
+ max: constraints.maxCount
2919
+ })
2920
+ );
2921
+ }
2922
+ if (errorParts.length > 0) {
2923
+ showFileError(filesContainer, errorParts.join(" \u2022 "));
2924
+ } else {
2925
+ clearFileError(filesContainer);
2926
+ }
2784
2927
  for (const file of arr) {
2785
2928
  const rid = await uploadSingleFile(file, state);
2786
2929
  state.resourceIndex.set(rid, {
@@ -2798,10 +2941,57 @@ function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallba
2798
2941
  }
2799
2942
  });
2800
2943
  }
2801
- function setupFilesPickerHandler(filesPicker, initialFiles, state, updateCallback, pathKey, instance) {
2944
+ function setupFilesPickerHandler(filesPicker, initialFiles, state, updateCallback, constraints, pathKey, instance) {
2802
2945
  filesPicker.onchange = async () => {
2803
2946
  if (filesPicker.files) {
2804
- for (const file of Array.from(filesPicker.files)) {
2947
+ const allFiles = Array.from(filesPicker.files);
2948
+ const rejectedByExtension = allFiles.filter(
2949
+ (f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2950
+ );
2951
+ const afterExtension = allFiles.filter(
2952
+ (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2953
+ );
2954
+ const rejectedBySize = afterExtension.filter(
2955
+ (f) => !isFileSizeAllowed(f, constraints.maxSize)
2956
+ );
2957
+ const validFiles = afterExtension.filter(
2958
+ (f) => isFileSizeAllowed(f, constraints.maxSize)
2959
+ );
2960
+ const remaining = constraints.maxCount === Infinity ? validFiles.length : Math.max(0, constraints.maxCount - initialFiles.length);
2961
+ const arr = validFiles.slice(0, remaining);
2962
+ const skippedByCount = validFiles.length - arr.length;
2963
+ const errorParts = [];
2964
+ if (rejectedByExtension.length > 0) {
2965
+ const formats = constraints.allowedExtensions.join(", ");
2966
+ const names = rejectedByExtension.map((f) => f.name).join(", ");
2967
+ errorParts.push(
2968
+ t("invalidFileExtension", state, { name: names, formats })
2969
+ );
2970
+ }
2971
+ if (rejectedBySize.length > 0) {
2972
+ const names = rejectedBySize.map((f) => f.name).join(", ");
2973
+ errorParts.push(
2974
+ t("fileTooLarge", state, {
2975
+ name: names,
2976
+ maxSize: constraints.maxSize
2977
+ })
2978
+ );
2979
+ }
2980
+ if (skippedByCount > 0) {
2981
+ errorParts.push(
2982
+ t("filesLimitExceeded", state, {
2983
+ skipped: skippedByCount,
2984
+ max: constraints.maxCount
2985
+ })
2986
+ );
2987
+ }
2988
+ const wrapper = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
2989
+ if (errorParts.length > 0 && wrapper) {
2990
+ showFileError(wrapper, errorParts.join(" \u2022 "));
2991
+ } else if (wrapper) {
2992
+ clearFileError(wrapper);
2993
+ }
2994
+ for (const file of arr) {
2805
2995
  const rid = await uploadSingleFile(file, state);
2806
2996
  state.resourceIndex.set(rid, {
2807
2997
  name: file.name,
@@ -2821,7 +3011,7 @@ function setupFilesPickerHandler(filesPicker, initialFiles, state, updateCallbac
2821
3011
  };
2822
3012
  }
2823
3013
  function renderFileElement(element, ctx, wrapper, pathKey) {
2824
- var _a;
3014
+ var _a, _b;
2825
3015
  const state = ctx.state;
2826
3016
  if (state.config.readonly) {
2827
3017
  const initial = ctx.prefill[element.key];
@@ -2854,6 +3044,8 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2854
3044
  const fileContainer = document.createElement("div");
2855
3045
  fileContainer.className = "file-preview-container w-full aspect-square max-w-xs bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
2856
3046
  const initial = ctx.prefill[element.key];
3047
+ const allowedExts = getAllowedExtensions(element.accept);
3048
+ const maxSizeMB = (_b = element.maxSize) != null ? _b : Infinity;
2857
3049
  const fileUploadHandler = () => picker.click();
2858
3050
  const dragHandler = (files) => {
2859
3051
  if (files.length > 0) {
@@ -2864,7 +3056,9 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2864
3056
  pathKey,
2865
3057
  state,
2866
3058
  deps,
2867
- ctx.instance
3059
+ ctx.instance,
3060
+ allowedExts,
3061
+ maxSizeMB
2868
3062
  );
2869
3063
  }
2870
3064
  };
@@ -2896,7 +3090,9 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2896
3090
  pathKey,
2897
3091
  state,
2898
3092
  deps,
2899
- ctx.instance
3093
+ ctx.instance,
3094
+ allowedExts,
3095
+ maxSizeMB
2900
3096
  );
2901
3097
  }
2902
3098
  };
@@ -2906,7 +3102,7 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2906
3102
  }
2907
3103
  }
2908
3104
  function renderFilesElement(element, ctx, wrapper, pathKey) {
2909
- var _a;
3105
+ var _a, _b;
2910
3106
  const state = ctx.state;
2911
3107
  if (state.config.readonly) {
2912
3108
  const resultsWrapper = document.createElement("div");
@@ -2957,12 +3153,18 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2957
3153
  const initialFiles = ctx.prefill[element.key] || [];
2958
3154
  addPrefillFilesToIndex(initialFiles, state);
2959
3155
  const filesFieldHint = makeFieldHint(element, state);
3156
+ const filesConstraints = {
3157
+ maxCount: Infinity,
3158
+ allowedExtensions: getAllowedExtensions(element.accept),
3159
+ maxSize: (_b = element.maxSize) != null ? _b : Infinity
3160
+ };
2960
3161
  updateFilesList2();
2961
3162
  setupFilesDropHandler(
2962
3163
  filesContainer,
2963
3164
  initialFiles,
2964
3165
  state,
2965
3166
  updateFilesList2,
3167
+ filesConstraints,
2966
3168
  pathKey,
2967
3169
  ctx.instance
2968
3170
  );
@@ -2971,6 +3173,7 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2971
3173
  initialFiles,
2972
3174
  state,
2973
3175
  updateFilesList2,
3176
+ filesConstraints,
2974
3177
  pathKey,
2975
3178
  ctx.instance
2976
3179
  );
@@ -2981,7 +3184,7 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2981
3184
  }
2982
3185
  }
2983
3186
  function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2984
- var _a, _b, _c;
3187
+ var _a, _b, _c, _d;
2985
3188
  const state = ctx.state;
2986
3189
  const minFiles = (_a = element.minCount) != null ? _a : 0;
2987
3190
  const maxFiles = (_b = element.maxCount) != null ? _b : Infinity;
@@ -3019,6 +3222,11 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
3019
3222
  const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
3020
3223
  addPrefillFilesToIndex(initialFiles, state);
3021
3224
  const multipleFilesHint = makeFieldHint(element, state);
3225
+ const multipleConstraints = {
3226
+ maxCount: maxFiles,
3227
+ allowedExtensions: getAllowedExtensions(element.accept),
3228
+ maxSize: (_d = element.maxSize) != null ? _d : Infinity
3229
+ };
3022
3230
  const buildCountInfo = () => {
3023
3231
  const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
3024
3232
  const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
@@ -3042,6 +3250,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
3042
3250
  initialFiles,
3043
3251
  state,
3044
3252
  updateFilesDisplay,
3253
+ multipleConstraints,
3045
3254
  pathKey,
3046
3255
  ctx.instance
3047
3256
  );
@@ -3050,6 +3259,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
3050
3259
  initialFiles,
3051
3260
  state,
3052
3261
  updateFilesDisplay,
3262
+ multipleConstraints,
3053
3263
  pathKey,
3054
3264
  ctx.instance
3055
3265
  );
@@ -3078,6 +3288,40 @@ function validateFileElement(element, key, context) {
3078
3288
  errors.push(`${key2}: ${t("maxFiles", state, { max: maxFiles })}`);
3079
3289
  }
3080
3290
  };
3291
+ const validateFileExtensions = (key2, resourceIds, element2) => {
3292
+ var _a2;
3293
+ if (skipValidation) return;
3294
+ const { state } = context;
3295
+ const acceptField = "accept" in element2 ? element2.accept : void 0;
3296
+ const allowedExtensions = getAllowedExtensions(acceptField);
3297
+ if (allowedExtensions.length === 0) return;
3298
+ const formats = allowedExtensions.join(", ");
3299
+ for (const rid of resourceIds) {
3300
+ const meta = state.resourceIndex.get(rid);
3301
+ const fileName = (_a2 = meta == null ? void 0 : meta.name) != null ? _a2 : rid;
3302
+ if (!isFileExtensionAllowed(fileName, allowedExtensions)) {
3303
+ errors.push(
3304
+ `${key2}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
3305
+ );
3306
+ }
3307
+ }
3308
+ };
3309
+ const validateFileSizes = (key2, resourceIds, element2) => {
3310
+ var _a2;
3311
+ if (skipValidation) return;
3312
+ const { state } = context;
3313
+ const maxSizeMB = "maxSize" in element2 ? (_a2 = element2.maxSize) != null ? _a2 : Infinity : Infinity;
3314
+ if (maxSizeMB === Infinity) return;
3315
+ for (const rid of resourceIds) {
3316
+ const meta = state.resourceIndex.get(rid);
3317
+ if (!meta) continue;
3318
+ if (meta.size > maxSizeMB * 1024 * 1024) {
3319
+ errors.push(
3320
+ `${key2}: ${t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })}`
3321
+ );
3322
+ }
3323
+ }
3324
+ };
3081
3325
  if (isMultipleField) {
3082
3326
  const fullKey = pathJoin(path, key);
3083
3327
  const pickerInput = scopeRoot.querySelector(
@@ -3096,6 +3340,8 @@ function validateFileElement(element, key, context) {
3096
3340
  });
3097
3341
  }
3098
3342
  validateFileCount(key, resourceIds, element);
3343
+ validateFileExtensions(key, resourceIds, element);
3344
+ validateFileSizes(key, resourceIds, element);
3099
3345
  return { value: resourceIds, errors };
3100
3346
  } else {
3101
3347
  const input = scopeRoot.querySelector(
@@ -3106,6 +3352,10 @@ function validateFileElement(element, key, context) {
3106
3352
  errors.push(`${key}: ${t("required", context.state)}`);
3107
3353
  return { value: null, errors };
3108
3354
  }
3355
+ if (!skipValidation && rid !== "") {
3356
+ validateFileExtensions(key, [rid], element);
3357
+ validateFileSizes(key, [rid], element);
3358
+ }
3109
3359
  return { value: rid || null, errors };
3110
3360
  }
3111
3361
  }
@@ -4611,11 +4861,16 @@ function validateContainerElement(element, key, context) {
4611
4861
  itemData[child.key] = child.default !== void 0 ? child.default : null;
4612
4862
  } else {
4613
4863
  const childKey = `${key}[${domIndex}].${child.key}`;
4614
- itemData[child.key] = validateElement(
4864
+ const childResult = validateElement(
4615
4865
  { ...child, key: childKey },
4616
4866
  { path },
4617
4867
  itemContainer
4618
4868
  );
4869
+ if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
4870
+ Object.assign(itemData, childResult.value);
4871
+ } else {
4872
+ itemData[child.key] = childResult.value;
4873
+ }
4619
4874
  }
4620
4875
  });
4621
4876
  items.push(itemData);
@@ -4651,11 +4906,16 @@ function validateContainerElement(element, key, context) {
4651
4906
  containerData[child.key] = child.default !== void 0 ? child.default : null;
4652
4907
  } else {
4653
4908
  const childKey = `${key}.${child.key}`;
4654
- containerData[child.key] = validateElement(
4909
+ const childResult = validateElement(
4655
4910
  { ...child, key: childKey },
4656
4911
  { path },
4657
4912
  containerContainer
4658
4913
  );
4914
+ if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
4915
+ Object.assign(containerData, childResult.value);
4916
+ } else {
4917
+ containerData[child.key] = childResult.value;
4918
+ }
4659
4919
  }
4660
4920
  });
4661
4921
  return { value: containerData, errors };
@@ -4676,11 +4936,24 @@ function updateContainerField(element, fieldPath, value, context) {
4676
4936
  value.forEach((itemValue, index) => {
4677
4937
  if (isPlainObject(itemValue)) {
4678
4938
  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);
4939
+ var _a, _b;
4940
+ const childPath = `${fieldPath}[${index}].${childElement.key}`;
4941
+ if (childElement.type === "richinput" && childElement.flatOutput) {
4942
+ const richChild = childElement;
4943
+ const textKey = (_a = richChild.textKey) != null ? _a : "text";
4944
+ const filesKey = (_b = richChild.filesKey) != null ? _b : "files";
4945
+ const containerValue = itemValue;
4946
+ const compositeValue = {};
4947
+ if (textKey in containerValue) compositeValue[textKey] = containerValue[textKey];
4948
+ if (filesKey in containerValue) compositeValue[filesKey] = containerValue[filesKey];
4949
+ if (Object.keys(compositeValue).length > 0) {
4950
+ instance.updateField(childPath, compositeValue);
4951
+ }
4952
+ } else {
4953
+ const childValue = itemValue[childElement.key];
4954
+ if (childValue !== void 0) {
4955
+ instance.updateField(childPath, childValue);
4956
+ }
4684
4957
  }
4685
4958
  });
4686
4959
  }
@@ -4701,11 +4974,24 @@ function updateContainerField(element, fieldPath, value, context) {
4701
4974
  return;
4702
4975
  }
4703
4976
  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);
4977
+ var _a, _b;
4978
+ const childPath = `${fieldPath}.${childElement.key}`;
4979
+ if (childElement.type === "richinput" && childElement.flatOutput) {
4980
+ const richChild = childElement;
4981
+ const textKey = (_a = richChild.textKey) != null ? _a : "text";
4982
+ const filesKey = (_b = richChild.filesKey) != null ? _b : "files";
4983
+ const containerValue = value;
4984
+ const compositeValue = {};
4985
+ if (textKey in containerValue) compositeValue[textKey] = containerValue[textKey];
4986
+ if (filesKey in containerValue) compositeValue[filesKey] = containerValue[filesKey];
4987
+ if (Object.keys(compositeValue).length > 0) {
4988
+ instance.updateField(childPath, compositeValue);
4989
+ }
4990
+ } else {
4991
+ const childValue = value[childElement.key];
4992
+ if (childValue !== void 0) {
4993
+ instance.updateField(childPath, childValue);
4994
+ }
4709
4995
  }
4710
4996
  });
4711
4997
  }
@@ -6540,6 +6826,42 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6540
6826
  outerDiv.style.borderColor = "var(--fb-border-color, #d1d5db)";
6541
6827
  outerDiv.style.boxShadow = "none";
6542
6828
  });
6829
+ const errorEl = document.createElement("div");
6830
+ errorEl.className = "fb-richinput-error";
6831
+ errorEl.style.cssText = "display: none; color: var(--fb-error-color, #ef4444); font-size: var(--fb-font-size-small, 12px); padding: 4px 14px 8px;";
6832
+ let errorTimer = null;
6833
+ function showUploadError(message) {
6834
+ errorEl.textContent = message;
6835
+ errorEl.style.display = "block";
6836
+ if (errorTimer) clearTimeout(errorTimer);
6837
+ errorTimer = setTimeout(() => {
6838
+ errorEl.style.display = "none";
6839
+ errorEl.textContent = "";
6840
+ errorTimer = null;
6841
+ }, 5e3);
6842
+ }
6843
+ function validateFileForUpload(file) {
6844
+ var _a2;
6845
+ const allowedExtensions = getAllowedExtensions(element.accept);
6846
+ if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
6847
+ const formats = allowedExtensions.join(", ");
6848
+ showUploadError(
6849
+ t("invalidFileExtension", state, { name: file.name, formats })
6850
+ );
6851
+ return false;
6852
+ }
6853
+ const maxSizeMB = (_a2 = element.maxSize) != null ? _a2 : Infinity;
6854
+ if (!isFileSizeAllowed(file, maxSizeMB)) {
6855
+ showUploadError(
6856
+ t("fileTooLarge", state, {
6857
+ name: file.name,
6858
+ maxSize: maxSizeMB
6859
+ })
6860
+ );
6861
+ return false;
6862
+ }
6863
+ return true;
6864
+ }
6543
6865
  let dragCounter = 0;
6544
6866
  outerDiv.addEventListener("dragenter", (e) => {
6545
6867
  e.preventDefault();
@@ -6568,7 +6890,17 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6568
6890
  const droppedFiles = (_a2 = e.dataTransfer) == null ? void 0 : _a2.files;
6569
6891
  if (!droppedFiles || !state.config.uploadFile) return;
6570
6892
  const maxFiles = (_b = element.maxFiles) != null ? _b : Infinity;
6571
- for (let i = 0; i < droppedFiles.length && files.length < maxFiles; i++) {
6893
+ for (let i = 0; i < droppedFiles.length; i++) {
6894
+ if (files.length >= maxFiles) {
6895
+ showUploadError(
6896
+ t("filesLimitExceeded", state, {
6897
+ skipped: droppedFiles.length - i,
6898
+ max: maxFiles
6899
+ })
6900
+ );
6901
+ break;
6902
+ }
6903
+ if (!validateFileForUpload(droppedFiles[i])) continue;
6572
6904
  uploadFile(droppedFiles[i]);
6573
6905
  }
6574
6906
  });
@@ -6728,9 +7060,13 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6728
7060
  paperclipBtn.addEventListener("click", () => {
6729
7061
  var _a2;
6730
7062
  const maxFiles = (_a2 = element.maxFiles) != null ? _a2 : Infinity;
6731
- if (files.length < maxFiles) {
6732
- fileInput.click();
7063
+ if (files.length >= maxFiles) {
7064
+ showUploadError(
7065
+ t("filesLimitExceeded", state, { skipped: 1, max: maxFiles })
7066
+ );
7067
+ return;
6733
7068
  }
7069
+ fileInput.click();
6734
7070
  });
6735
7071
  const dropdown = document.createElement("div");
6736
7072
  dropdown.className = "fb-richinput-dropdown";
@@ -7051,7 +7387,17 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7051
7387
  const selected = fileInput.files;
7052
7388
  if (!selected || selected.length === 0) return;
7053
7389
  const maxFiles = (_a2 = element.maxFiles) != null ? _a2 : Infinity;
7054
- for (let i = 0; i < selected.length && files.length < maxFiles; i++) {
7390
+ for (let i = 0; i < selected.length; i++) {
7391
+ if (files.length >= maxFiles) {
7392
+ showUploadError(
7393
+ t("filesLimitExceeded", state, {
7394
+ skipped: selected.length - i,
7395
+ max: maxFiles
7396
+ })
7397
+ );
7398
+ break;
7399
+ }
7400
+ if (!validateFileForUpload(selected[i])) continue;
7055
7401
  uploadFile(selected[i]);
7056
7402
  }
7057
7403
  fileInput.value = "";
@@ -7062,6 +7408,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
7062
7408
  textareaArea.appendChild(dropdown);
7063
7409
  outerDiv.appendChild(filesRow);
7064
7410
  outerDiv.appendChild(textareaArea);
7411
+ outerDiv.appendChild(errorEl);
7065
7412
  if (element.minLength != null || element.maxLength != null) {
7066
7413
  const counterRow = document.createElement("div");
7067
7414
  counterRow.style.cssText = "position: relative; padding: 2px 14px 6px; text-align: right;";
@@ -7224,20 +7571,29 @@ function renderRichInputElement(element, ctx, wrapper, pathKey) {
7224
7571
  const state = ctx.state;
7225
7572
  const textKey = (_a = element.textKey) != null ? _a : "text";
7226
7573
  const filesKey = (_b = element.filesKey) != null ? _b : "files";
7227
- const rawPrefill = ctx.prefill[element.key];
7228
7574
  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"];
7575
+ if (element.flatOutput) {
7576
+ const textVal = ctx.prefill[textKey];
7577
+ const filesVal = ctx.prefill[filesKey];
7233
7578
  initialValue = {
7234
7579
  text: typeof textVal === "string" ? textVal : null,
7235
7580
  files: Array.isArray(filesVal) ? filesVal : []
7236
7581
  };
7237
- } else if (typeof rawPrefill === "string") {
7238
- initialValue = { text: rawPrefill || null, files: [] };
7239
7582
  } else {
7240
- initialValue = { text: null, files: [] };
7583
+ const rawPrefill = ctx.prefill[element.key];
7584
+ if (rawPrefill && typeof rawPrefill === "object" && !Array.isArray(rawPrefill)) {
7585
+ const obj = rawPrefill;
7586
+ const textVal = (_c = obj[textKey]) != null ? _c : obj["text"];
7587
+ const filesVal = (_d = obj[filesKey]) != null ? _d : obj["files"];
7588
+ initialValue = {
7589
+ text: typeof textVal === "string" ? textVal : null,
7590
+ files: Array.isArray(filesVal) ? filesVal : []
7591
+ };
7592
+ } else if (typeof rawPrefill === "string") {
7593
+ initialValue = { text: rawPrefill || null, files: [] };
7594
+ } else {
7595
+ initialValue = { text: null, files: [] };
7596
+ }
7241
7597
  }
7242
7598
  for (const rid of initialValue.files) {
7243
7599
  if (!state.resourceIndex.has(rid)) {
@@ -7318,7 +7674,7 @@ function validateRichInputElement(element, key, context) {
7318
7674
  );
7319
7675
  }
7320
7676
  }
7321
- return { value, errors };
7677
+ return { value, errors, spread: !!element.flatOutput };
7322
7678
  }
7323
7679
  function updateRichInputField(element, fieldPath, value, context) {
7324
7680
  var _a, _b, _c, _d;
@@ -7798,6 +8154,9 @@ var defaultConfig = {
7798
8154
  invalidHexColour: "Invalid hex color",
7799
8155
  minFiles: "Minimum {min} files required",
7800
8156
  maxFiles: "Maximum {max} files allowed",
8157
+ invalidFileExtension: 'File "{name}" has unsupported format. Allowed: {formats}',
8158
+ fileTooLarge: 'File "{name}" exceeds maximum size of {maxSize}MB',
8159
+ filesLimitExceeded: "{skipped} file(s) skipped: maximum {max} files allowed",
7801
8160
  unsupportedFieldType: "Unsupported field type: {type}",
7802
8161
  invalidOption: "Invalid option",
7803
8162
  tableAddRow: "Add row",
@@ -7857,6 +8216,9 @@ var defaultConfig = {
7857
8216
  invalidHexColour: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u0446\u0432\u0435\u0442\u0430",
7858
8217
  minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
7859
8218
  maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
8219
+ 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}',
8220
+ 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',
8221
+ 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
8222
  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
8223
  invalidOption: "\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
7862
8224
  tableAddRow: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443",
@@ -8628,10 +8990,10 @@ var FormBuilderInstance = class {
8628
8990
  );
8629
8991
  if (componentResult !== null) {
8630
8992
  errors.push(...componentResult.errors);
8631
- return componentResult.value;
8993
+ return { value: componentResult.value, spread: !!componentResult.spread };
8632
8994
  }
8633
8995
  console.warn(`Unknown field type "${element.type}" for key "${key}"`);
8634
- return null;
8996
+ return { value: null, spread: false };
8635
8997
  };
8636
8998
  setValidateElement(validateElement2);
8637
8999
  this.state.schema.elements.forEach((element) => {
@@ -8654,7 +9016,12 @@ var FormBuilderInstance = class {
8654
9016
  if (element.hidden) {
8655
9017
  data[element.key] = element.default !== void 0 ? element.default : null;
8656
9018
  } else {
8657
- data[element.key] = validateElement2(element, { path: "" });
9019
+ const result = validateElement2(element, { path: "" });
9020
+ if (result.spread && result.value !== null && typeof result.value === "object") {
9021
+ Object.assign(data, result.value);
9022
+ } else {
9023
+ data[element.key] = result.value;
9024
+ }
8658
9025
  }
8659
9026
  });
8660
9027
  return {
@@ -8748,6 +9115,24 @@ var FormBuilderInstance = class {
8748
9115
  }
8749
9116
  return data;
8750
9117
  }
9118
+ /**
9119
+ * Build a map from flat output keys (textKey/filesKey) to the richinput schema element info.
9120
+ * Used by setFormData to detect flat richinput keys and remap them to their composite values.
9121
+ */
9122
+ buildFlatKeyMap(elements) {
9123
+ var _a, _b;
9124
+ const map = /* @__PURE__ */ new Map();
9125
+ for (const el of elements) {
9126
+ if (el.type === "richinput" && el.flatOutput) {
9127
+ const richEl = el;
9128
+ const textKey = (_a = richEl.textKey) != null ? _a : "text";
9129
+ const filesKey = (_b = richEl.filesKey) != null ? _b : "files";
9130
+ map.set(textKey, { schemaKey: el.key, role: "text" });
9131
+ map.set(filesKey, { schemaKey: el.key, role: "files" });
9132
+ }
9133
+ }
9134
+ return map;
9135
+ }
8751
9136
  /**
8752
9137
  * Set form data - update multiple fields without full re-render
8753
9138
  * @param data - Object with field paths and their values
@@ -8759,8 +9144,21 @@ var FormBuilderInstance = class {
8759
9144
  );
8760
9145
  return;
8761
9146
  }
9147
+ const flatKeyMap = this.buildFlatKeyMap(this.state.schema.elements);
9148
+ const flatUpdates = /* @__PURE__ */ new Map();
8762
9149
  for (const fieldPath in data) {
8763
- this.updateField(fieldPath, data[fieldPath]);
9150
+ const flatInfo = flatKeyMap.get(fieldPath);
9151
+ if (flatInfo) {
9152
+ if (!flatUpdates.has(flatInfo.schemaKey)) {
9153
+ flatUpdates.set(flatInfo.schemaKey, {});
9154
+ }
9155
+ flatUpdates.get(flatInfo.schemaKey)[fieldPath] = data[fieldPath];
9156
+ } else {
9157
+ this.updateField(fieldPath, data[fieldPath]);
9158
+ }
9159
+ }
9160
+ for (const [schemaKey, compositeValue] of flatUpdates) {
9161
+ this.updateField(schemaKey, compositeValue);
8764
9162
  }
8765
9163
  }
8766
9164
  /**