@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/browser/formbuilder.min.js +108 -104
- package/dist/browser/formbuilder.v0.2.23.min.js +606 -0
- package/dist/cjs/index.cjs +438 -40
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +425 -37
- package/dist/esm/index.js.map +1 -1
- package/dist/form-builder.js +108 -104
- package/dist/types/components/container.d.ts +4 -1
- package/dist/types/components/file.d.ts +5 -0
- package/dist/types/instance/FormBuilderInstance.d.ts +5 -0
- package/dist/types/types/component-operations.d.ts +2 -0
- package/dist/types/types/config.d.ts +3 -0
- package/dist/types/types/schema.d.ts +1 -0
- package/package.json +1 -1
- package/dist/browser/formbuilder.v0.2.22.min.js +0 -602
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
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
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
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
|
|
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
|
|
6623
|
-
|
|
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
|
|
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 (
|
|
7105
|
-
const
|
|
7106
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|