@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.
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
 
@@ -2006,6 +2051,25 @@ function updateSwitcherField(element, fieldPath, value, context) {
2006
2051
  }
2007
2052
 
2008
2053
  // src/components/file.ts
2054
+ function getAllowedExtensions(accept) {
2055
+ if (!accept) return [];
2056
+ if (typeof accept === "object" && Array.isArray(accept.extensions)) {
2057
+ return accept.extensions.map((ext) => ext.toLowerCase());
2058
+ }
2059
+ if (typeof accept === "string") {
2060
+ return accept.split(",").map((s) => s.trim()).filter((s) => s.startsWith(".")).map((s) => s.substring(1).toLowerCase());
2061
+ }
2062
+ return [];
2063
+ }
2064
+ function isFileExtensionAllowed(fileName, allowedExtensions) {
2065
+ if (allowedExtensions.length === 0) return true;
2066
+ const ext = fileName.split(".").pop()?.toLowerCase() || "";
2067
+ return allowedExtensions.includes(ext);
2068
+ }
2069
+ function isFileSizeAllowed(file, maxSizeMB) {
2070
+ if (maxSizeMB === Infinity) return true;
2071
+ return file.size <= maxSizeMB * 1024 * 1024;
2072
+ }
2009
2073
  function renderLocalImagePreview(container, file, fileName, state) {
2010
2074
  const img = document.createElement("img");
2011
2075
  img.className = "w-full h-full object-contain";
@@ -2535,7 +2599,40 @@ function setEmptyFileContainer(fileContainer, state, hint) {
2535
2599
  </div>
2536
2600
  `;
2537
2601
  }
2538
- async function handleFileSelect(file, container, fieldName, state, deps = null, instance) {
2602
+ function showFileError(container, message) {
2603
+ const existing = container.closest(".space-y-2")?.querySelector(".file-error-message");
2604
+ if (existing) existing.remove();
2605
+ const errorEl = document.createElement("div");
2606
+ errorEl.className = "file-error-message error-message";
2607
+ errorEl.style.cssText = `
2608
+ color: var(--fb-error-color);
2609
+ font-size: var(--fb-font-size-small);
2610
+ margin-top: 0.25rem;
2611
+ `;
2612
+ errorEl.textContent = message;
2613
+ container.closest(".space-y-2")?.appendChild(errorEl);
2614
+ }
2615
+ function clearFileError(container) {
2616
+ const existing = container.closest(".space-y-2")?.querySelector(".file-error-message");
2617
+ if (existing) existing.remove();
2618
+ }
2619
+ async function handleFileSelect(file, container, fieldName, state, deps = null, instance, allowedExtensions = [], maxSizeMB = Infinity) {
2620
+ if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
2621
+ const formats = allowedExtensions.join(", ");
2622
+ showFileError(
2623
+ container,
2624
+ t("invalidFileExtension", state, { name: file.name, formats })
2625
+ );
2626
+ return;
2627
+ }
2628
+ if (!isFileSizeAllowed(file, maxSizeMB)) {
2629
+ showFileError(
2630
+ container,
2631
+ t("fileTooLarge", state, { name: file.name, maxSize: maxSizeMB })
2632
+ );
2633
+ return;
2634
+ }
2635
+ clearFileError(container);
2539
2636
  let rid;
2540
2637
  if (state.config.uploadFile) {
2541
2638
  try {
@@ -2738,9 +2835,51 @@ function handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, sta
2738
2835
  hiddenInput.value = initial;
2739
2836
  fileWrapper.appendChild(hiddenInput);
2740
2837
  }
2741
- function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallback, pathKey, instance) {
2838
+ function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallback, constraints, pathKey, instance) {
2742
2839
  setupDragAndDrop(filesContainer, async (files) => {
2743
- const arr = Array.from(files);
2840
+ const allFiles = Array.from(files);
2841
+ const rejectedByExtension = allFiles.filter(
2842
+ (f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2843
+ );
2844
+ const afterExtension = allFiles.filter(
2845
+ (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2846
+ );
2847
+ const rejectedBySize = afterExtension.filter(
2848
+ (f) => !isFileSizeAllowed(f, constraints.maxSize)
2849
+ );
2850
+ const validFiles = afterExtension.filter(
2851
+ (f) => isFileSizeAllowed(f, constraints.maxSize)
2852
+ );
2853
+ const remaining = constraints.maxCount === Infinity ? validFiles.length : Math.max(0, constraints.maxCount - initialFiles.length);
2854
+ const arr = validFiles.slice(0, remaining);
2855
+ const skippedByCount = validFiles.length - arr.length;
2856
+ const errorParts = [];
2857
+ if (rejectedByExtension.length > 0) {
2858
+ const formats = constraints.allowedExtensions.join(", ");
2859
+ const names = rejectedByExtension.map((f) => f.name).join(", ");
2860
+ errorParts.push(
2861
+ t("invalidFileExtension", state, { name: names, formats })
2862
+ );
2863
+ }
2864
+ if (rejectedBySize.length > 0) {
2865
+ const names = rejectedBySize.map((f) => f.name).join(", ");
2866
+ errorParts.push(
2867
+ t("fileTooLarge", state, { name: names, maxSize: constraints.maxSize })
2868
+ );
2869
+ }
2870
+ if (skippedByCount > 0) {
2871
+ errorParts.push(
2872
+ t("filesLimitExceeded", state, {
2873
+ skipped: skippedByCount,
2874
+ max: constraints.maxCount
2875
+ })
2876
+ );
2877
+ }
2878
+ if (errorParts.length > 0) {
2879
+ showFileError(filesContainer, errorParts.join(" \u2022 "));
2880
+ } else {
2881
+ clearFileError(filesContainer);
2882
+ }
2744
2883
  for (const file of arr) {
2745
2884
  const rid = await uploadSingleFile(file, state);
2746
2885
  state.resourceIndex.set(rid, {
@@ -2758,10 +2897,57 @@ function setupFilesDropHandler(filesContainer, initialFiles, state, updateCallba
2758
2897
  }
2759
2898
  });
2760
2899
  }
2761
- function setupFilesPickerHandler(filesPicker, initialFiles, state, updateCallback, pathKey, instance) {
2900
+ function setupFilesPickerHandler(filesPicker, initialFiles, state, updateCallback, constraints, pathKey, instance) {
2762
2901
  filesPicker.onchange = async () => {
2763
2902
  if (filesPicker.files) {
2764
- for (const file of Array.from(filesPicker.files)) {
2903
+ const allFiles = Array.from(filesPicker.files);
2904
+ const rejectedByExtension = allFiles.filter(
2905
+ (f) => !isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2906
+ );
2907
+ const afterExtension = allFiles.filter(
2908
+ (f) => isFileExtensionAllowed(f.name, constraints.allowedExtensions)
2909
+ );
2910
+ const rejectedBySize = afterExtension.filter(
2911
+ (f) => !isFileSizeAllowed(f, constraints.maxSize)
2912
+ );
2913
+ const validFiles = afterExtension.filter(
2914
+ (f) => isFileSizeAllowed(f, constraints.maxSize)
2915
+ );
2916
+ const remaining = constraints.maxCount === Infinity ? validFiles.length : Math.max(0, constraints.maxCount - initialFiles.length);
2917
+ const arr = validFiles.slice(0, remaining);
2918
+ const skippedByCount = validFiles.length - arr.length;
2919
+ const errorParts = [];
2920
+ if (rejectedByExtension.length > 0) {
2921
+ const formats = constraints.allowedExtensions.join(", ");
2922
+ const names = rejectedByExtension.map((f) => f.name).join(", ");
2923
+ errorParts.push(
2924
+ t("invalidFileExtension", state, { name: names, formats })
2925
+ );
2926
+ }
2927
+ if (rejectedBySize.length > 0) {
2928
+ const names = rejectedBySize.map((f) => f.name).join(", ");
2929
+ errorParts.push(
2930
+ t("fileTooLarge", state, {
2931
+ name: names,
2932
+ maxSize: constraints.maxSize
2933
+ })
2934
+ );
2935
+ }
2936
+ if (skippedByCount > 0) {
2937
+ errorParts.push(
2938
+ t("filesLimitExceeded", state, {
2939
+ skipped: skippedByCount,
2940
+ max: constraints.maxCount
2941
+ })
2942
+ );
2943
+ }
2944
+ const wrapper = filesPicker.closest(".space-y-2") || filesPicker.parentElement;
2945
+ if (errorParts.length > 0 && wrapper) {
2946
+ showFileError(wrapper, errorParts.join(" \u2022 "));
2947
+ } else if (wrapper) {
2948
+ clearFileError(wrapper);
2949
+ }
2950
+ for (const file of arr) {
2765
2951
  const rid = await uploadSingleFile(file, state);
2766
2952
  state.resourceIndex.set(rid, {
2767
2953
  name: file.name,
@@ -2813,6 +2999,8 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2813
2999
  const fileContainer = document.createElement("div");
2814
3000
  fileContainer.className = "file-preview-container w-full aspect-square max-w-xs bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
2815
3001
  const initial = ctx.prefill[element.key];
3002
+ const allowedExts = getAllowedExtensions(element.accept);
3003
+ const maxSizeMB = element.maxSize ?? Infinity;
2816
3004
  const fileUploadHandler = () => picker.click();
2817
3005
  const dragHandler = (files) => {
2818
3006
  if (files.length > 0) {
@@ -2823,7 +3011,9 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2823
3011
  pathKey,
2824
3012
  state,
2825
3013
  deps,
2826
- ctx.instance
3014
+ ctx.instance,
3015
+ allowedExts,
3016
+ maxSizeMB
2827
3017
  );
2828
3018
  }
2829
3019
  };
@@ -2855,7 +3045,9 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2855
3045
  pathKey,
2856
3046
  state,
2857
3047
  deps,
2858
- ctx.instance
3048
+ ctx.instance,
3049
+ allowedExts,
3050
+ maxSizeMB
2859
3051
  );
2860
3052
  }
2861
3053
  };
@@ -2915,12 +3107,18 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2915
3107
  const initialFiles = ctx.prefill[element.key] || [];
2916
3108
  addPrefillFilesToIndex(initialFiles, state);
2917
3109
  const filesFieldHint = makeFieldHint(element, state);
3110
+ const filesConstraints = {
3111
+ maxCount: Infinity,
3112
+ allowedExtensions: getAllowedExtensions(element.accept),
3113
+ maxSize: element.maxSize ?? Infinity
3114
+ };
2918
3115
  updateFilesList2();
2919
3116
  setupFilesDropHandler(
2920
3117
  filesContainer,
2921
3118
  initialFiles,
2922
3119
  state,
2923
3120
  updateFilesList2,
3121
+ filesConstraints,
2924
3122
  pathKey,
2925
3123
  ctx.instance
2926
3124
  );
@@ -2929,6 +3127,7 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2929
3127
  initialFiles,
2930
3128
  state,
2931
3129
  updateFilesList2,
3130
+ filesConstraints,
2932
3131
  pathKey,
2933
3132
  ctx.instance
2934
3133
  );
@@ -2976,6 +3175,11 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2976
3175
  const initialFiles = Array.isArray(ctx.prefill[element.key]) ? [...ctx.prefill[element.key]] : [];
2977
3176
  addPrefillFilesToIndex(initialFiles, state);
2978
3177
  const multipleFilesHint = makeFieldHint(element, state);
3178
+ const multipleConstraints = {
3179
+ maxCount: maxFiles,
3180
+ allowedExtensions: getAllowedExtensions(element.accept),
3181
+ maxSize: element.maxSize ?? Infinity
3182
+ };
2979
3183
  const buildCountInfo = () => {
2980
3184
  const countText = initialFiles.length === 1 ? t("fileCountSingle", state, { count: initialFiles.length }) : t("fileCountPlural", state, { count: initialFiles.length });
2981
3185
  const minMaxText = minFiles > 0 || maxFiles < Infinity ? ` ${t("fileCountRange", state, { min: minFiles, max: maxFiles })}` : "";
@@ -2999,6 +3203,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2999
3203
  initialFiles,
3000
3204
  state,
3001
3205
  updateFilesDisplay,
3206
+ multipleConstraints,
3002
3207
  pathKey,
3003
3208
  ctx.instance
3004
3209
  );
@@ -3007,6 +3212,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
3007
3212
  initialFiles,
3008
3213
  state,
3009
3214
  updateFilesDisplay,
3215
+ multipleConstraints,
3010
3216
  pathKey,
3011
3217
  ctx.instance
3012
3218
  );
@@ -3033,6 +3239,38 @@ function validateFileElement(element, key, context) {
3033
3239
  errors.push(`${key2}: ${t("maxFiles", state, { max: maxFiles })}`);
3034
3240
  }
3035
3241
  };
3242
+ const validateFileExtensions = (key2, resourceIds, element2) => {
3243
+ if (skipValidation) return;
3244
+ const { state } = context;
3245
+ const acceptField = "accept" in element2 ? element2.accept : void 0;
3246
+ const allowedExtensions = getAllowedExtensions(acceptField);
3247
+ if (allowedExtensions.length === 0) return;
3248
+ const formats = allowedExtensions.join(", ");
3249
+ for (const rid of resourceIds) {
3250
+ const meta = state.resourceIndex.get(rid);
3251
+ const fileName = meta?.name ?? rid;
3252
+ if (!isFileExtensionAllowed(fileName, allowedExtensions)) {
3253
+ errors.push(
3254
+ `${key2}: ${t("invalidFileExtension", state, { name: fileName, formats })}`
3255
+ );
3256
+ }
3257
+ }
3258
+ };
3259
+ const validateFileSizes = (key2, resourceIds, element2) => {
3260
+ if (skipValidation) return;
3261
+ const { state } = context;
3262
+ const maxSizeMB = "maxSize" in element2 ? element2.maxSize ?? Infinity : Infinity;
3263
+ if (maxSizeMB === Infinity) return;
3264
+ for (const rid of resourceIds) {
3265
+ const meta = state.resourceIndex.get(rid);
3266
+ if (!meta) continue;
3267
+ if (meta.size > maxSizeMB * 1024 * 1024) {
3268
+ errors.push(
3269
+ `${key2}: ${t("fileTooLarge", state, { name: meta.name, maxSize: maxSizeMB })}`
3270
+ );
3271
+ }
3272
+ }
3273
+ };
3036
3274
  if (isMultipleField) {
3037
3275
  const fullKey = pathJoin(path, key);
3038
3276
  const pickerInput = scopeRoot.querySelector(
@@ -3051,6 +3289,8 @@ function validateFileElement(element, key, context) {
3051
3289
  });
3052
3290
  }
3053
3291
  validateFileCount(key, resourceIds, element);
3292
+ validateFileExtensions(key, resourceIds, element);
3293
+ validateFileSizes(key, resourceIds, element);
3054
3294
  return { value: resourceIds, errors };
3055
3295
  } else {
3056
3296
  const input = scopeRoot.querySelector(
@@ -3061,6 +3301,10 @@ function validateFileElement(element, key, context) {
3061
3301
  errors.push(`${key}: ${t("required", context.state)}`);
3062
3302
  return { value: null, errors };
3063
3303
  }
3304
+ if (!skipValidation && rid !== "") {
3305
+ validateFileExtensions(key, [rid], element);
3306
+ validateFileSizes(key, [rid], element);
3307
+ }
3064
3308
  return { value: rid || null, errors };
3065
3309
  }
3066
3310
  }
@@ -4549,11 +4793,16 @@ function validateContainerElement(element, key, context) {
4549
4793
  itemData[child.key] = child.default !== void 0 ? child.default : null;
4550
4794
  } else {
4551
4795
  const childKey = `${key}[${domIndex}].${child.key}`;
4552
- itemData[child.key] = validateElement(
4796
+ const childResult = validateElement(
4553
4797
  { ...child, key: childKey },
4554
4798
  { path },
4555
4799
  itemContainer
4556
4800
  );
4801
+ if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
4802
+ Object.assign(itemData, childResult.value);
4803
+ } else {
4804
+ itemData[child.key] = childResult.value;
4805
+ }
4557
4806
  }
4558
4807
  });
4559
4808
  items.push(itemData);
@@ -4588,11 +4837,16 @@ function validateContainerElement(element, key, context) {
4588
4837
  containerData[child.key] = child.default !== void 0 ? child.default : null;
4589
4838
  } else {
4590
4839
  const childKey = `${key}.${child.key}`;
4591
- containerData[child.key] = validateElement(
4840
+ const childResult = validateElement(
4592
4841
  { ...child, key: childKey },
4593
4842
  { path },
4594
4843
  containerContainer
4595
4844
  );
4845
+ if (childResult.spread && childResult.value !== null && typeof childResult.value === "object") {
4846
+ Object.assign(containerData, childResult.value);
4847
+ } else {
4848
+ containerData[child.key] = childResult.value;
4849
+ }
4596
4850
  }
4597
4851
  });
4598
4852
  return { value: containerData, errors };
@@ -4613,11 +4867,23 @@ function updateContainerField(element, fieldPath, value, context) {
4613
4867
  value.forEach((itemValue, index) => {
4614
4868
  if (isPlainObject(itemValue)) {
4615
4869
  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);
4870
+ const childPath = `${fieldPath}[${index}].${childElement.key}`;
4871
+ if (childElement.type === "richinput" && childElement.flatOutput) {
4872
+ const richChild = childElement;
4873
+ const textKey = richChild.textKey ?? "text";
4874
+ const filesKey = richChild.filesKey ?? "files";
4875
+ const containerValue = itemValue;
4876
+ const compositeValue = {};
4877
+ if (textKey in containerValue) compositeValue[textKey] = containerValue[textKey];
4878
+ if (filesKey in containerValue) compositeValue[filesKey] = containerValue[filesKey];
4879
+ if (Object.keys(compositeValue).length > 0) {
4880
+ instance.updateField(childPath, compositeValue);
4881
+ }
4882
+ } else {
4883
+ const childValue = itemValue[childElement.key];
4884
+ if (childValue !== void 0) {
4885
+ instance.updateField(childPath, childValue);
4886
+ }
4621
4887
  }
4622
4888
  });
4623
4889
  }
@@ -4638,11 +4904,23 @@ function updateContainerField(element, fieldPath, value, context) {
4638
4904
  return;
4639
4905
  }
4640
4906
  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);
4907
+ const childPath = `${fieldPath}.${childElement.key}`;
4908
+ if (childElement.type === "richinput" && childElement.flatOutput) {
4909
+ const richChild = childElement;
4910
+ const textKey = richChild.textKey ?? "text";
4911
+ const filesKey = richChild.filesKey ?? "files";
4912
+ const containerValue = value;
4913
+ const compositeValue = {};
4914
+ if (textKey in containerValue) compositeValue[textKey] = containerValue[textKey];
4915
+ if (filesKey in containerValue) compositeValue[filesKey] = containerValue[filesKey];
4916
+ if (Object.keys(compositeValue).length > 0) {
4917
+ instance.updateField(childPath, compositeValue);
4918
+ }
4919
+ } else {
4920
+ const childValue = value[childElement.key];
4921
+ if (childValue !== void 0) {
4922
+ instance.updateField(childPath, childValue);
4923
+ }
4646
4924
  }
4647
4925
  });
4648
4926
  }
@@ -6436,6 +6714,41 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6436
6714
  outerDiv.style.borderColor = "var(--fb-border-color, #d1d5db)";
6437
6715
  outerDiv.style.boxShadow = "none";
6438
6716
  });
6717
+ const errorEl = document.createElement("div");
6718
+ errorEl.className = "fb-richinput-error";
6719
+ errorEl.style.cssText = "display: none; color: var(--fb-error-color, #ef4444); font-size: var(--fb-font-size-small, 12px); padding: 4px 14px 8px;";
6720
+ let errorTimer = null;
6721
+ function showUploadError(message) {
6722
+ errorEl.textContent = message;
6723
+ errorEl.style.display = "block";
6724
+ if (errorTimer) clearTimeout(errorTimer);
6725
+ errorTimer = setTimeout(() => {
6726
+ errorEl.style.display = "none";
6727
+ errorEl.textContent = "";
6728
+ errorTimer = null;
6729
+ }, 5e3);
6730
+ }
6731
+ function validateFileForUpload(file) {
6732
+ const allowedExtensions = getAllowedExtensions(element.accept);
6733
+ if (!isFileExtensionAllowed(file.name, allowedExtensions)) {
6734
+ const formats = allowedExtensions.join(", ");
6735
+ showUploadError(
6736
+ t("invalidFileExtension", state, { name: file.name, formats })
6737
+ );
6738
+ return false;
6739
+ }
6740
+ const maxSizeMB = element.maxSize ?? Infinity;
6741
+ if (!isFileSizeAllowed(file, maxSizeMB)) {
6742
+ showUploadError(
6743
+ t("fileTooLarge", state, {
6744
+ name: file.name,
6745
+ maxSize: maxSizeMB
6746
+ })
6747
+ );
6748
+ return false;
6749
+ }
6750
+ return true;
6751
+ }
6439
6752
  let dragCounter = 0;
6440
6753
  outerDiv.addEventListener("dragenter", (e) => {
6441
6754
  e.preventDefault();
@@ -6463,7 +6776,17 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6463
6776
  const droppedFiles = e.dataTransfer?.files;
6464
6777
  if (!droppedFiles || !state.config.uploadFile) return;
6465
6778
  const maxFiles = element.maxFiles ?? Infinity;
6466
- for (let i = 0; i < droppedFiles.length && files.length < maxFiles; i++) {
6779
+ for (let i = 0; i < droppedFiles.length; i++) {
6780
+ if (files.length >= maxFiles) {
6781
+ showUploadError(
6782
+ t("filesLimitExceeded", state, {
6783
+ skipped: droppedFiles.length - i,
6784
+ max: maxFiles
6785
+ })
6786
+ );
6787
+ break;
6788
+ }
6789
+ if (!validateFileForUpload(droppedFiles[i])) continue;
6467
6790
  uploadFile(droppedFiles[i]);
6468
6791
  }
6469
6792
  });
@@ -6619,9 +6942,13 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6619
6942
  });
6620
6943
  paperclipBtn.addEventListener("click", () => {
6621
6944
  const maxFiles = element.maxFiles ?? Infinity;
6622
- if (files.length < maxFiles) {
6623
- fileInput.click();
6945
+ if (files.length >= maxFiles) {
6946
+ showUploadError(
6947
+ t("filesLimitExceeded", state, { skipped: 1, max: maxFiles })
6948
+ );
6949
+ return;
6624
6950
  }
6951
+ fileInput.click();
6625
6952
  });
6626
6953
  const dropdown = document.createElement("div");
6627
6954
  dropdown.className = "fb-richinput-dropdown";
@@ -6929,7 +7256,17 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6929
7256
  const selected = fileInput.files;
6930
7257
  if (!selected || selected.length === 0) return;
6931
7258
  const maxFiles = element.maxFiles ?? Infinity;
6932
- for (let i = 0; i < selected.length && files.length < maxFiles; i++) {
7259
+ for (let i = 0; i < selected.length; i++) {
7260
+ if (files.length >= maxFiles) {
7261
+ showUploadError(
7262
+ t("filesLimitExceeded", state, {
7263
+ skipped: selected.length - i,
7264
+ max: maxFiles
7265
+ })
7266
+ );
7267
+ break;
7268
+ }
7269
+ if (!validateFileForUpload(selected[i])) continue;
6933
7270
  uploadFile(selected[i]);
6934
7271
  }
6935
7272
  fileInput.value = "";
@@ -6940,6 +7277,7 @@ function renderEditMode(element, ctx, wrapper, pathKey, initialValue) {
6940
7277
  textareaArea.appendChild(dropdown);
6941
7278
  outerDiv.appendChild(filesRow);
6942
7279
  outerDiv.appendChild(textareaArea);
7280
+ outerDiv.appendChild(errorEl);
6943
7281
  if (element.minLength != null || element.maxLength != null) {
6944
7282
  const counterRow = document.createElement("div");
6945
7283
  counterRow.style.cssText = "position: relative; padding: 2px 14px 6px; text-align: right;";
@@ -7099,20 +7437,29 @@ function renderRichInputElement(element, ctx, wrapper, pathKey) {
7099
7437
  const state = ctx.state;
7100
7438
  const textKey = element.textKey ?? "text";
7101
7439
  const filesKey = element.filesKey ?? "files";
7102
- const rawPrefill = ctx.prefill[element.key];
7103
7440
  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"];
7441
+ if (element.flatOutput) {
7442
+ const textVal = ctx.prefill[textKey];
7443
+ const filesVal = ctx.prefill[filesKey];
7108
7444
  initialValue = {
7109
7445
  text: typeof textVal === "string" ? textVal : null,
7110
7446
  files: Array.isArray(filesVal) ? filesVal : []
7111
7447
  };
7112
- } else if (typeof rawPrefill === "string") {
7113
- initialValue = { text: rawPrefill || null, files: [] };
7114
7448
  } else {
7115
- initialValue = { text: null, files: [] };
7449
+ const rawPrefill = ctx.prefill[element.key];
7450
+ if (rawPrefill && typeof rawPrefill === "object" && !Array.isArray(rawPrefill)) {
7451
+ const obj = rawPrefill;
7452
+ const textVal = obj[textKey] ?? obj["text"];
7453
+ const filesVal = obj[filesKey] ?? obj["files"];
7454
+ initialValue = {
7455
+ text: typeof textVal === "string" ? textVal : null,
7456
+ files: Array.isArray(filesVal) ? filesVal : []
7457
+ };
7458
+ } else if (typeof rawPrefill === "string") {
7459
+ initialValue = { text: rawPrefill || null, files: [] };
7460
+ } else {
7461
+ initialValue = { text: null, files: [] };
7462
+ }
7116
7463
  }
7117
7464
  for (const rid of initialValue.files) {
7118
7465
  if (!state.resourceIndex.has(rid)) {
@@ -7192,7 +7539,7 @@ function validateRichInputElement(element, key, context) {
7192
7539
  );
7193
7540
  }
7194
7541
  }
7195
- return { value, errors };
7542
+ return { value, errors, spread: !!element.flatOutput };
7196
7543
  }
7197
7544
  function updateRichInputField(element, fieldPath, value, context) {
7198
7545
  const { scopeRoot } = context;
@@ -7668,6 +8015,9 @@ var defaultConfig = {
7668
8015
  invalidHexColour: "Invalid hex color",
7669
8016
  minFiles: "Minimum {min} files required",
7670
8017
  maxFiles: "Maximum {max} files allowed",
8018
+ invalidFileExtension: 'File "{name}" has unsupported format. Allowed: {formats}',
8019
+ fileTooLarge: 'File "{name}" exceeds maximum size of {maxSize}MB',
8020
+ filesLimitExceeded: "{skipped} file(s) skipped: maximum {max} files allowed",
7671
8021
  unsupportedFieldType: "Unsupported field type: {type}",
7672
8022
  invalidOption: "Invalid option",
7673
8023
  tableAddRow: "Add row",
@@ -7727,6 +8077,9 @@ var defaultConfig = {
7727
8077
  invalidHexColour: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442 \u0446\u0432\u0435\u0442\u0430",
7728
8078
  minFiles: "\u041C\u0438\u043D\u0438\u043C\u0443\u043C {min} \u0444\u0430\u0439\u043B\u043E\u0432",
7729
8079
  maxFiles: "\u041C\u0430\u043A\u0441\u0438\u043C\u0443\u043C {max} \u0444\u0430\u0439\u043B\u043E\u0432",
8080
+ 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}',
8081
+ 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',
8082
+ 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
8083
  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
8084
  invalidOption: "\u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
7732
8085
  tableAddRow: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443",
@@ -8498,10 +8851,10 @@ var FormBuilderInstance = class {
8498
8851
  );
8499
8852
  if (componentResult !== null) {
8500
8853
  errors.push(...componentResult.errors);
8501
- return componentResult.value;
8854
+ return { value: componentResult.value, spread: !!componentResult.spread };
8502
8855
  }
8503
8856
  console.warn(`Unknown field type "${element.type}" for key "${key}"`);
8504
- return null;
8857
+ return { value: null, spread: false };
8505
8858
  };
8506
8859
  setValidateElement(validateElement2);
8507
8860
  this.state.schema.elements.forEach((element) => {
@@ -8524,7 +8877,12 @@ var FormBuilderInstance = class {
8524
8877
  if (element.hidden) {
8525
8878
  data[element.key] = element.default !== void 0 ? element.default : null;
8526
8879
  } else {
8527
- data[element.key] = validateElement2(element, { path: "" });
8880
+ const result = validateElement2(element, { path: "" });
8881
+ if (result.spread && result.value !== null && typeof result.value === "object") {
8882
+ Object.assign(data, result.value);
8883
+ } else {
8884
+ data[element.key] = result.value;
8885
+ }
8528
8886
  }
8529
8887
  });
8530
8888
  return {
@@ -8618,6 +8976,23 @@ var FormBuilderInstance = class {
8618
8976
  }
8619
8977
  return data;
8620
8978
  }
8979
+ /**
8980
+ * Build a map from flat output keys (textKey/filesKey) to the richinput schema element info.
8981
+ * Used by setFormData to detect flat richinput keys and remap them to their composite values.
8982
+ */
8983
+ buildFlatKeyMap(elements) {
8984
+ const map = /* @__PURE__ */ new Map();
8985
+ for (const el of elements) {
8986
+ if (el.type === "richinput" && el.flatOutput) {
8987
+ const richEl = el;
8988
+ const textKey = richEl.textKey ?? "text";
8989
+ const filesKey = richEl.filesKey ?? "files";
8990
+ map.set(textKey, { schemaKey: el.key, role: "text" });
8991
+ map.set(filesKey, { schemaKey: el.key, role: "files" });
8992
+ }
8993
+ }
8994
+ return map;
8995
+ }
8621
8996
  /**
8622
8997
  * Set form data - update multiple fields without full re-render
8623
8998
  * @param data - Object with field paths and their values
@@ -8629,8 +9004,21 @@ var FormBuilderInstance = class {
8629
9004
  );
8630
9005
  return;
8631
9006
  }
9007
+ const flatKeyMap = this.buildFlatKeyMap(this.state.schema.elements);
9008
+ const flatUpdates = /* @__PURE__ */ new Map();
8632
9009
  for (const fieldPath in data) {
8633
- this.updateField(fieldPath, data[fieldPath]);
9010
+ const flatInfo = flatKeyMap.get(fieldPath);
9011
+ if (flatInfo) {
9012
+ if (!flatUpdates.has(flatInfo.schemaKey)) {
9013
+ flatUpdates.set(flatInfo.schemaKey, {});
9014
+ }
9015
+ flatUpdates.get(flatInfo.schemaKey)[fieldPath] = data[fieldPath];
9016
+ } else {
9017
+ this.updateField(fieldPath, data[fieldPath]);
9018
+ }
9019
+ }
9020
+ for (const [schemaKey, compositeValue] of flatUpdates) {
9021
+ this.updateField(schemaKey, compositeValue);
8634
9022
  }
8635
9023
  }
8636
9024
  /**