@dmitryvim/form-builder 0.1.41 → 0.2.0
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/README.md +244 -22
- package/dist/browser/formbuilder.min.js +179 -0
- package/dist/browser/formbuilder.v0.2.0.min.js +179 -0
- package/dist/cjs/index.cjs +3582 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/esm/index.js +3534 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/form-builder.js +152 -3372
- package/dist/types/components/container.d.ts +15 -0
- package/dist/types/components/file.d.ts +26 -0
- package/dist/types/components/group.d.ts +24 -0
- package/dist/types/components/index.d.ts +11 -0
- package/dist/types/components/number.d.ts +11 -0
- package/dist/types/components/registry.d.ts +15 -0
- package/dist/types/components/select.d.ts +11 -0
- package/dist/types/components/text.d.ts +11 -0
- package/dist/types/components/textarea.d.ts +11 -0
- package/dist/types/index.d.ts +33 -0
- package/dist/types/instance/FormBuilderInstance.d.ts +134 -0
- package/dist/types/instance/state.d.ts +13 -0
- package/dist/types/styles/theme.d.ts +63 -0
- package/dist/types/types/component-operations.d.ts +45 -0
- package/dist/types/types/config.d.ts +44 -0
- package/dist/types/types/index.d.ts +4 -0
- package/dist/types/types/schema.d.ts +115 -0
- package/dist/types/types/state.d.ts +11 -0
- package/dist/types/utils/helpers.d.ts +4 -0
- package/dist/types/utils/styles.d.ts +21 -0
- package/dist/types/utils/translation.d.ts +8 -0
- package/dist/types/utils/validation.d.ts +2 -0
- package/package.json +35 -15
- package/dist/demo.js +0 -861
- package/dist/elements.html +0 -1130
- package/dist/elements.js +0 -488
- package/dist/index.html +0 -315
package/dist/form-builder.js
CHANGED
|
@@ -1,3399 +1,179 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// Utility functions
|
|
49
|
-
function isPlainObject(obj) {
|
|
50
|
-
return obj && typeof obj === "object" && obj.constructor === Object;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function pathJoin(base, key) {
|
|
54
|
-
return base ? `${base}.${key}` : key;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function pretty(obj) {
|
|
58
|
-
return JSON.stringify(obj, null, 2);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function clear(node) {
|
|
62
|
-
while (node.firstChild) node.removeChild(node.firstChild);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Translation function
|
|
66
|
-
function t(key) {
|
|
67
|
-
const locale = state.config.locale || "en";
|
|
68
|
-
const translations =
|
|
69
|
-
state.config.translations[locale] || state.config.translations.en;
|
|
70
|
-
return translations[key] || key;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Schema validation
|
|
74
|
-
function validateSchema(schema) {
|
|
75
|
-
const errors = [];
|
|
76
|
-
|
|
77
|
-
if (!schema || typeof schema !== "object") {
|
|
78
|
-
errors.push("Schema must be an object");
|
|
79
|
-
return errors;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!schema.version) {
|
|
83
|
-
errors.push("Schema missing version");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!Array.isArray(schema.elements)) {
|
|
87
|
-
errors.push("Schema missing elements array");
|
|
88
|
-
return errors;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function validateElements(elements, path) {
|
|
92
|
-
elements.forEach((element, index) => {
|
|
93
|
-
const elementPath = `${path}[${index}]`;
|
|
94
|
-
|
|
95
|
-
if (!element.type) {
|
|
96
|
-
errors.push(`${elementPath}: missing type`);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!element.key) {
|
|
100
|
-
errors.push(`${elementPath}: missing key`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (element.type === "group" && element.elements) {
|
|
104
|
-
validateElements(element.elements, `${elementPath}.elements`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (element.type === "select" && element.options) {
|
|
108
|
-
const defaultValue = element.default;
|
|
109
|
-
if (
|
|
110
|
-
defaultValue !== undefined &&
|
|
111
|
-
defaultValue !== null &&
|
|
112
|
-
defaultValue !== ""
|
|
113
|
-
) {
|
|
114
|
-
const hasMatchingOption = element.options.some(
|
|
115
|
-
(opt) => opt.value === defaultValue,
|
|
116
|
-
);
|
|
117
|
-
if (!hasMatchingOption) {
|
|
118
|
-
errors.push(
|
|
119
|
-
`${elementPath}: default "${defaultValue}" not in options`,
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (Array.isArray(schema.elements))
|
|
128
|
-
validateElements(schema.elements, "elements");
|
|
129
|
-
return errors;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Form rendering
|
|
133
|
-
function renderForm(schema, prefill, actions) {
|
|
134
|
-
const errors = validateSchema(schema);
|
|
135
|
-
if (errors.length > 0) {
|
|
136
|
-
console.error("Schema validation errors:", errors);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
state.schema = schema;
|
|
141
|
-
state.externalActions = actions || null;
|
|
142
|
-
|
|
143
|
-
if (!state.formRoot) {
|
|
144
|
-
console.error("No form root element set. Call setFormRoot() first.");
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
clear(state.formRoot);
|
|
149
|
-
|
|
150
|
-
const formEl = document.createElement("div");
|
|
151
|
-
formEl.className = "space-y-6";
|
|
152
|
-
|
|
153
|
-
schema.elements.forEach((element, _index) => {
|
|
154
|
-
// Skip rendering hidden elements
|
|
155
|
-
if (element.hidden) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
const block = renderElement(element, {
|
|
159
|
-
path: "",
|
|
160
|
-
prefill: prefill || {},
|
|
161
|
-
});
|
|
162
|
-
formEl.appendChild(block);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
state.formRoot.appendChild(formEl);
|
|
166
|
-
|
|
167
|
-
// Render external actions after form is built (only in readonly mode)
|
|
168
|
-
if (
|
|
169
|
-
state.config.readonly &&
|
|
170
|
-
state.externalActions &&
|
|
171
|
-
Array.isArray(state.externalActions)
|
|
172
|
-
) {
|
|
173
|
-
renderExternalActions();
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function renderElement(element, ctx) {
|
|
178
|
-
const wrapper = document.createElement("div");
|
|
179
|
-
wrapper.className = "mb-6 fb-field-wrapper";
|
|
180
|
-
|
|
181
|
-
const label = document.createElement("div");
|
|
182
|
-
label.className = "flex items-center mb-2";
|
|
183
|
-
const title = document.createElement("label");
|
|
184
|
-
title.className = "text-sm font-medium text-gray-900";
|
|
185
|
-
title.textContent = element.label || element.key;
|
|
186
|
-
if (element.required) {
|
|
187
|
-
const req = document.createElement("span");
|
|
188
|
-
req.className = "text-red-500 ml-1";
|
|
189
|
-
req.textContent = "*";
|
|
190
|
-
title.appendChild(req);
|
|
191
|
-
}
|
|
192
|
-
label.appendChild(title);
|
|
193
|
-
|
|
194
|
-
// Add info button if there's description or hint
|
|
195
|
-
if (element.description || element.hint) {
|
|
196
|
-
const infoBtn = document.createElement("button");
|
|
197
|
-
infoBtn.type = "button";
|
|
198
|
-
infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
|
|
199
|
-
infoBtn.innerHTML =
|
|
200
|
-
'<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>';
|
|
201
|
-
|
|
202
|
-
// Create tooltip
|
|
203
|
-
const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
|
|
204
|
-
const tooltip = document.createElement("div");
|
|
205
|
-
tooltip.id = tooltipId;
|
|
206
|
-
tooltip.className =
|
|
207
|
-
"hidden absolute z-50 bg-gray-200 text-gray-900 text-sm rounded-lg p-3 max-w-sm border border-gray-300 shadow-lg";
|
|
208
|
-
tooltip.style.position = "fixed";
|
|
209
|
-
tooltip.textContent =
|
|
210
|
-
element.description || element.hint || "Field information";
|
|
211
|
-
document.body.appendChild(tooltip);
|
|
212
|
-
|
|
213
|
-
infoBtn.onclick = (e) => {
|
|
214
|
-
e.preventDefault();
|
|
215
|
-
e.stopPropagation();
|
|
216
|
-
showTooltip(tooltipId, infoBtn);
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
label.appendChild(infoBtn);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
wrapper.appendChild(label);
|
|
223
|
-
|
|
224
|
-
const pathKey = pathJoin(ctx.path, element.key);
|
|
225
|
-
|
|
226
|
-
switch (element.type) {
|
|
227
|
-
case "text":
|
|
228
|
-
if (element.multiple) {
|
|
229
|
-
renderMultipleTextElement(element, ctx, wrapper, pathKey);
|
|
230
|
-
} else {
|
|
231
|
-
renderTextElement(element, ctx, wrapper, pathKey);
|
|
232
|
-
}
|
|
233
|
-
break;
|
|
234
|
-
|
|
235
|
-
case "textarea":
|
|
236
|
-
if (element.multiple) {
|
|
237
|
-
renderMultipleTextareaElement(element, ctx, wrapper, pathKey);
|
|
238
|
-
} else {
|
|
239
|
-
renderTextareaElement(element, ctx, wrapper, pathKey);
|
|
240
|
-
}
|
|
241
|
-
break;
|
|
242
|
-
|
|
243
|
-
case "number":
|
|
244
|
-
if (element.multiple) {
|
|
245
|
-
renderMultipleNumberElement(element, ctx, wrapper, pathKey);
|
|
246
|
-
} else {
|
|
247
|
-
renderNumberElement(element, ctx, wrapper, pathKey);
|
|
248
|
-
}
|
|
249
|
-
break;
|
|
250
|
-
|
|
251
|
-
case "select":
|
|
252
|
-
if (element.multiple) {
|
|
253
|
-
renderMultipleSelectElement(element, ctx, wrapper, pathKey);
|
|
254
|
-
} else {
|
|
255
|
-
renderSelectElement(element, ctx, wrapper, pathKey);
|
|
256
|
-
}
|
|
257
|
-
break;
|
|
258
|
-
|
|
259
|
-
case "file":
|
|
260
|
-
// Handle multiple files with file type using multiple property
|
|
261
|
-
if (element.multiple) {
|
|
262
|
-
renderMultipleFileElement(element, ctx, wrapper, pathKey);
|
|
263
|
-
} else {
|
|
264
|
-
renderFileElement(element, ctx, wrapper, pathKey);
|
|
265
|
-
}
|
|
266
|
-
break;
|
|
267
|
-
|
|
268
|
-
case "files":
|
|
269
|
-
renderFilesElement(element, ctx, wrapper, pathKey);
|
|
270
|
-
break;
|
|
271
|
-
|
|
272
|
-
case "group":
|
|
273
|
-
renderGroupElement(element, ctx, wrapper, pathKey);
|
|
274
|
-
break;
|
|
275
|
-
|
|
276
|
-
case "container":
|
|
277
|
-
// Handle containers with multiple property like groups
|
|
278
|
-
if (element.multiple) {
|
|
279
|
-
renderMultipleContainerElement(element, ctx, wrapper, pathKey);
|
|
280
|
-
} else {
|
|
281
|
-
renderSingleContainerElement(element, ctx, wrapper, pathKey);
|
|
282
|
-
}
|
|
283
|
-
break;
|
|
284
|
-
|
|
285
|
-
default: {
|
|
286
|
-
const unsupported = document.createElement("div");
|
|
287
|
-
unsupported.className = "text-red-500 text-sm";
|
|
288
|
-
unsupported.textContent = `Unsupported field type: ${element.type}`;
|
|
289
|
-
wrapper.appendChild(unsupported);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Actions are now only rendered via external actions system in renderExternalActions()
|
|
294
|
-
// element.actions are only used for label lookup, not direct rendering
|
|
295
|
-
|
|
296
|
-
return wrapper;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function makeFieldHint(element) {
|
|
300
|
-
const parts = [];
|
|
301
|
-
|
|
302
|
-
parts.push(element.required ? "required" : "optional");
|
|
303
|
-
|
|
304
|
-
addLengthHint(element, parts);
|
|
305
|
-
addRangeHint(element, parts);
|
|
306
|
-
addFileSizeHint(element, parts);
|
|
307
|
-
addFormatHint(element, parts);
|
|
308
|
-
addPatternHint(element, parts);
|
|
309
|
-
|
|
310
|
-
return parts.join(" • ");
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function addLengthHint(element, parts) {
|
|
314
|
-
if (element.minLength !== null || element.maxLength !== null) {
|
|
315
|
-
if (element.minLength !== null && element.maxLength !== null) {
|
|
316
|
-
parts.push(`length=${element.minLength}-${element.maxLength} characters`);
|
|
317
|
-
} else if (element.maxLength !== null) {
|
|
318
|
-
parts.push(`max=${element.maxLength} characters`);
|
|
319
|
-
} else if (element.minLength !== null) {
|
|
320
|
-
parts.push(`min=${element.minLength} characters`);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function addRangeHint(element, parts) {
|
|
326
|
-
if (element.min !== null || element.max !== null) {
|
|
327
|
-
if (element.min !== null && element.max !== null) {
|
|
328
|
-
parts.push(`range=${element.min}-${element.max}`);
|
|
329
|
-
} else if (element.max !== null) {
|
|
330
|
-
parts.push(`max=${element.max}`);
|
|
331
|
-
} else if (element.min !== null) {
|
|
332
|
-
parts.push(`min=${element.min}`);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function addFileSizeHint(element, parts) {
|
|
338
|
-
if (element.maxSizeMB) {
|
|
339
|
-
parts.push(`max_size=${element.maxSizeMB}MB`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function addFormatHint(element, parts) {
|
|
344
|
-
if (element.accept?.extensions) {
|
|
345
|
-
parts.push(
|
|
346
|
-
`formats=${element.accept.extensions.map((ext) => ext.toUpperCase()).join(",")}`,
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function addPatternHint(element, parts) {
|
|
352
|
-
if (element.pattern && !element.pattern.includes("А-Я")) {
|
|
353
|
-
parts.push("plain text only");
|
|
354
|
-
} else if (element.pattern?.includes("А-Я")) {
|
|
355
|
-
parts.push("text with punctuation");
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
async function renderFilePreview(container, resourceId, options = {}) {
|
|
360
|
-
const { fileName = "", isReadonly = false, deps = null } = options;
|
|
361
|
-
// Runtime validation for dependencies when not in readonly mode
|
|
362
|
-
if (
|
|
363
|
-
!isReadonly &&
|
|
364
|
-
deps &&
|
|
365
|
-
(!deps.picker || !deps.fileUploadHandler || !deps.dragHandler)
|
|
366
|
-
) {
|
|
367
|
-
throw new Error(
|
|
368
|
-
"renderFilePreview: missing deps {picker, fileUploadHandler, dragHandler}",
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
// Don't change container className - preserve max-w-xs and other styling
|
|
372
|
-
|
|
373
|
-
// Clear container content first
|
|
374
|
-
clear(container);
|
|
375
|
-
|
|
376
|
-
if (isReadonly) {
|
|
377
|
-
container.classList.add("cursor-pointer");
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const img = document.createElement("img");
|
|
381
|
-
img.className = "w-full h-full object-contain";
|
|
382
|
-
img.alt = fileName || "Preview";
|
|
383
|
-
|
|
384
|
-
// Use stored file from resourceIndex if available, or try getThumbnail
|
|
385
|
-
const meta = state.resourceIndex.get(resourceId);
|
|
386
|
-
|
|
387
|
-
if (meta && meta.file && meta.file instanceof File) {
|
|
388
|
-
// For local files, use FileReader to display preview
|
|
389
|
-
if (meta.type && meta.type.startsWith("image/")) {
|
|
390
|
-
const reader = new FileReader();
|
|
391
|
-
reader.onload = (e) => {
|
|
392
|
-
img.src = e.target.result;
|
|
393
|
-
};
|
|
394
|
-
reader.readAsDataURL(meta.file);
|
|
395
|
-
container.appendChild(img);
|
|
396
|
-
} else if (meta.type && meta.type.startsWith("video/")) {
|
|
397
|
-
// Video file - use object URL for preview
|
|
398
|
-
const videoUrl = URL.createObjectURL(meta.file);
|
|
399
|
-
|
|
400
|
-
// Remove all conflicting handlers to prevent interference with video controls
|
|
401
|
-
container.onclick = null;
|
|
402
|
-
|
|
403
|
-
// Remove drag and drop event listeners by cloning the element
|
|
404
|
-
const newContainer = container.cloneNode(false);
|
|
405
|
-
container.parentNode.replaceChild(newContainer, container);
|
|
406
|
-
container = newContainer;
|
|
407
|
-
|
|
408
|
-
container.innerHTML = `
|
|
1
|
+
var FormBuilder=(function(F){"use strict";function ie(e,t){(e.minLength!==null||e.maxLength!==null)&&(e.minLength!==null&&e.maxLength!==null?t.push(`length=${e.minLength}-${e.maxLength} characters`):e.maxLength!==null?t.push(`max=${e.maxLength} characters`):e.minLength!==null&&t.push(`min=${e.minLength} characters`))}function se(e,t){(e.min!==null||e.max!==null)&&(e.min!==null&&e.max!==null?t.push(`range=${e.min}-${e.max}`):e.max!==null?t.push(`max=${e.max}`):e.min!==null&&t.push(`min=${e.min}`))}function ce(e,t){e.maxSizeMB&&t.push(`max_size=${e.maxSizeMB}MB`)}function de(e,t){var n;(n=e.accept)!=null&&n.extensions&&t.push(`formats=${e.accept.extensions.map(o=>o.toUpperCase()).join(",")}`)}function ue(e,t){var n;e.pattern&&!e.pattern.includes("\u0410-\u042F")?t.push("plain text only"):(n=e.pattern)!=null&&n.includes("\u0410-\u042F")&&t.push("text with punctuation")}function k(e){const t=[];return t.push(e.required?"required":"optional"),ie(e,t),se(e,t),ce(e,t),de(e,t),ue(e,t),t.join(" \u2022 ")}function M(e){const t=[];if(!e||typeof e!="object")return t.push("Schema must be an object"),t;if(e.version||t.push("Schema missing version"),!Array.isArray(e.elements))return t.push("Schema missing elements array"),t;function n(o,c){o.forEach((r,l)=>{const a=`${c}[${l}]`;if(r.type||t.push(`${a}: missing type`),r.key||t.push(`${a}: missing key`),r.type==="group"&&"elements"in r&&r.elements&&n(r.elements,`${a}.elements`),r.type==="container"&&r.elements&&n(r.elements,`${a}.elements`),r.type==="select"&&r.options){const i=r.default;i!=null&&i!==""&&(r.options.some(s=>s.value===i)||t.push(`${a}: default "${i}" not in options`))}})}return Array.isArray(e.elements)&&n(e.elements,"elements"),t}function T(e){return e&&typeof e=="object"&&e.constructor===Object}function L(e,t){return e?`${e}.${t}`:t}function N(e){for(;e.firstChild;)e.removeChild(e.firstChild)}function me(e,t,n,o){const c=t.state,r=document.createElement("input");if(r.type="text",r.className="w-full rounded-lg",r.style.cssText=`
|
|
2
|
+
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
3
|
+
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
4
|
+
border-radius: var(--fb-border-radius);
|
|
5
|
+
background-color: ${c.config.readonly?"var(--fb-background-readonly-color)":"var(--fb-background-color)"};
|
|
6
|
+
color: var(--fb-text-color);
|
|
7
|
+
font-size: var(--fb-font-size);
|
|
8
|
+
font-family: var(--fb-font-family);
|
|
9
|
+
transition: all var(--fb-transition-duration) ease-in-out;
|
|
10
|
+
`,r.name=o,r.placeholder=e.placeholder||"\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442",r.value=t.prefill[e.key]||e.default||"",r.readOnly=c.config.readonly,c.config.readonly||(r.addEventListener("focus",()=>{r.style.borderColor="var(--fb-border-focus-color)",r.style.outline="var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)",r.style.outlineOffset="0"}),r.addEventListener("blur",()=>{r.style.borderColor="var(--fb-border-color)",r.style.outline="none"}),r.addEventListener("mouseenter",()=>{document.activeElement!==r&&(r.style.borderColor="var(--fb-border-hover-color)")}),r.addEventListener("mouseleave",()=>{document.activeElement!==r&&(r.style.borderColor="var(--fb-border-color)")})),!c.config.readonly&&t.instance){const a=()=>{t.instance.triggerOnChange(o,r.value)};r.addEventListener("blur",a),r.addEventListener("input",a)}n.appendChild(r);const l=document.createElement("p");l.className="mt-1",l.style.cssText=`
|
|
11
|
+
font-size: var(--fb-font-size-small);
|
|
12
|
+
color: var(--fb-text-secondary-color);
|
|
13
|
+
`,l.textContent=k(e),n.appendChild(l)}function pe(e,t,n,o){var c,r;const l=t.state,a=t.prefill[e.key]||[],i=Array.isArray(a)?[...a]:[],s=(c=e.minCount)!=null?c:1,u=(r=e.maxCount)!=null?r:1/0;for(;i.length<s;)i.push(e.default||"");const d=document.createElement("div");d.className="space-y-2",n.appendChild(d);function h(){d.querySelectorAll(".multiple-text-item").forEach((x,g)=>{const y=x.querySelector("input");y&&(y.name=`${o}[${g}]`)})}function p(x="",g=-1){const y=document.createElement("div");y.className="multiple-text-item flex items-center gap-2";const f=document.createElement("input");if(f.type="text",f.className="flex-1",f.style.cssText=`
|
|
14
|
+
padding: var(--fb-input-padding-y) var(--fb-input-padding-x);
|
|
15
|
+
border: var(--fb-border-width) solid var(--fb-border-color);
|
|
16
|
+
border-radius: var(--fb-border-radius);
|
|
17
|
+
background-color: ${l.config.readonly?"var(--fb-background-readonly-color)":"var(--fb-background-color)"};
|
|
18
|
+
color: var(--fb-text-color);
|
|
19
|
+
font-size: var(--fb-font-size);
|
|
20
|
+
font-family: var(--fb-font-family);
|
|
21
|
+
transition: all var(--fb-transition-duration) ease-in-out;
|
|
22
|
+
`,f.placeholder=e.placeholder||"Enter text",f.value=x,f.readOnly=l.config.readonly,l.config.readonly||(f.addEventListener("focus",()=>{f.style.borderColor="var(--fb-border-focus-color)",f.style.outline="var(--fb-focus-ring-width) solid var(--fb-focus-ring-color)",f.style.outlineOffset="0"}),f.addEventListener("blur",()=>{f.style.borderColor="var(--fb-border-color)",f.style.outline="none"}),f.addEventListener("mouseenter",()=>{document.activeElement!==f&&(f.style.borderColor="var(--fb-border-hover-color)")}),f.addEventListener("mouseleave",()=>{document.activeElement!==f&&(f.style.borderColor="var(--fb-border-color)")})),!l.config.readonly&&t.instance){const C=()=>{t.instance.triggerOnChange(f.name,f.value)};f.addEventListener("blur",C),f.addEventListener("input",C)}return y.appendChild(f),g===-1?d.appendChild(y):d.insertBefore(y,d.children[g]),h(),y}function m(){if(l.config.readonly)return;const x=d.querySelectorAll(".multiple-text-item"),g=x.length;x.forEach(y=>{let f=y.querySelector(".remove-item-btn");f||(f=document.createElement("button"),f.type="button",f.className="remove-item-btn px-2 py-1 rounded",f.style.cssText=`
|
|
23
|
+
color: var(--fb-error-color);
|
|
24
|
+
background-color: transparent;
|
|
25
|
+
transition: background-color var(--fb-transition-duration);
|
|
26
|
+
`,f.innerHTML="\u2715",f.addEventListener("mouseenter",()=>{f.style.backgroundColor="var(--fb-background-hover-color)"}),f.addEventListener("mouseleave",()=>{f.style.backgroundColor="transparent"}),f.onclick=()=>{const E=Array.from(d.children).indexOf(y);d.children.length>s&&(i.splice(E,1),y.remove(),h(),v(),m())},y.appendChild(f));const C=g<=s;f.disabled=C,f.style.opacity=C?"0.5":"1",f.style.pointerEvents=C?"none":"auto"})}function v(){const x=n.querySelector(".add-text-btn");if(x&&x.remove(),!l.config.readonly&&i.length<u){const g=document.createElement("button");g.type="button",g.className="add-text-btn mt-2 px-3 py-1 rounded",g.style.cssText=`
|
|
27
|
+
color: var(--fb-primary-color);
|
|
28
|
+
border: var(--fb-border-width) solid var(--fb-primary-color);
|
|
29
|
+
background-color: transparent;
|
|
30
|
+
font-size: var(--fb-font-size);
|
|
31
|
+
transition: all var(--fb-transition-duration);
|
|
32
|
+
`,g.textContent=`+ Add ${e.label||"Text"}`,g.addEventListener("mouseenter",()=>{g.style.backgroundColor="var(--fb-background-hover-color)"}),g.addEventListener("mouseleave",()=>{g.style.backgroundColor="transparent"}),g.onclick=()=>{i.push(e.default||""),p(e.default||""),v(),m()},n.appendChild(g)}}i.forEach(x=>p(x)),v(),m();const b=document.createElement("p");b.className="mt-1",b.style.cssText=`
|
|
33
|
+
font-size: var(--fb-font-size-small);
|
|
34
|
+
color: var(--fb-text-secondary-color);
|
|
35
|
+
`,b.textContent=k(e),n.appendChild(b)}function I(e,t,n){var o,c,r;const l=[],{scopeRoot:a,skipValidation:i}=n,s=(d,h)=>{var p,m;if(!d)return;const v=`error-${d.getAttribute("name")||Math.random().toString(36).substring(7)}`;let b=document.getElementById(v);h?(d.classList.add("invalid"),d.title=h,b||(b=document.createElement("div"),b.id=v,b.className="error-message",b.style.cssText=`
|
|
36
|
+
color: var(--fb-error-color);
|
|
37
|
+
font-size: var(--fb-font-size-small);
|
|
38
|
+
margin-top: 0.25rem;
|
|
39
|
+
`,d.nextSibling?(p=d.parentNode)==null||p.insertBefore(b,d.nextSibling):(m=d.parentNode)==null||m.appendChild(b)),b.textContent=h,b.style.display="block"):(d.classList.remove("invalid"),d.title="",b&&b.remove())},u=(d,h,p)=>{let m=!1;if(!i&&h){if(e.minLength!==void 0&&e.minLength!==null&&h.length<e.minLength)l.push(`${p}: minLength=${e.minLength}`),s(d,`minLength=${e.minLength}`),m=!0;else if(e.maxLength!==void 0&&e.maxLength!==null&&h.length>e.maxLength)l.push(`${p}: maxLength=${e.maxLength}`),s(d,`maxLength=${e.maxLength}`),m=!0;else if(e.pattern)try{new RegExp(e.pattern).test(h)||(l.push(`${p}: pattern mismatch`),s(d,"pattern mismatch"),m=!0)}catch(v){l.push(`${p}: invalid pattern`),s(d,"invalid pattern"),m=!0}}m||s(d,null)};if(e.multiple){const d=a.querySelectorAll(`[name^="${t}["]`),h=[];if(d.forEach((p,m)=>{var v;const b=(v=p==null?void 0:p.value)!=null?v:"";h.push(b),u(p,b,`${t}[${m}]`)}),!i){const p=(o=e.minCount)!=null?o:1,m=(c=e.maxCount)!=null?c:1/0,v=h.filter(b=>b.trim()!=="");e.required&&v.length===0&&l.push(`${t}: required`),v.length<p&&l.push(`${t}: minimum ${p} items required`),v.length>m&&l.push(`${t}: maximum ${m} items allowed`)}return{value:h,errors:l}}else{const d=a.querySelector(`[name$="${t}"]`),h=(r=d==null?void 0:d.value)!=null?r:"";return!i&&e.required&&h===""?(l.push(`${t}: required`),s(d,"required"),{value:"",errors:l}):(u(d,h,t),{value:h,errors:l})}}function O(e,t,n,o){const{scopeRoot:c}=o;if(e.multiple){if(!Array.isArray(n)){console.warn(`updateTextField: Expected array for multiple field "${t}", got ${typeof n}`);return}const r=c.querySelectorAll(`[name^="${t}["]`);r.forEach((l,a)=>{a<n.length&&(l.value=n[a]!=null?String(n[a]):"",l.classList.remove("invalid"),l.title="")}),n.length!==r.length&&console.warn(`updateTextField: Multiple field "${t}" has ${r.length} inputs but received ${n.length} values. Consider re-rendering for add/remove.`)}else{const r=c.querySelector(`[name="${t}"]`);r&&(r.value=n!=null?String(n):"",r.classList.remove("invalid"),r.title="")}}function fe(e,t,n,o){const c=t.state,r=document.createElement("textarea");if(r.className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none",r.name=o,r.placeholder=e.placeholder||"\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442",r.rows=e.rows||4,r.value=t.prefill[e.key]||e.default||"",r.readOnly=c.config.readonly,!c.config.readonly&&t.instance){const a=()=>{t.instance.triggerOnChange(o,r.value)};r.addEventListener("blur",a),r.addEventListener("input",a)}n.appendChild(r);const l=document.createElement("p");l.className="text-xs text-gray-500 mt-1",l.textContent=k(e),n.appendChild(l)}function he(e,t,n,o){var c,r;const l=t.state,a=t.prefill[e.key]||[],i=Array.isArray(a)?[...a]:[],s=(c=e.minCount)!=null?c:1,u=(r=e.maxCount)!=null?r:1/0;for(;i.length<s;)i.push(e.default||"");const d=document.createElement("div");d.className="space-y-2",n.appendChild(d);function h(){d.querySelectorAll(".multiple-textarea-item").forEach((x,g)=>{const y=x.querySelector("textarea");y&&(y.name=`${o}[${g}]`)})}function p(x="",g=-1){const y=document.createElement("div");y.className="multiple-textarea-item";const f=document.createElement("textarea");if(f.className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none",f.placeholder=e.placeholder||"Enter text",f.rows=e.rows||4,f.value=x,f.readOnly=l.config.readonly,!l.config.readonly&&t.instance){const C=()=>{t.instance.triggerOnChange(f.name,f.value)};f.addEventListener("blur",C),f.addEventListener("input",C)}return y.appendChild(f),g===-1?d.appendChild(y):d.insertBefore(y,d.children[g]),h(),y}function m(){if(l.config.readonly)return;const x=d.querySelectorAll(".multiple-textarea-item"),g=x.length;x.forEach(y=>{let f=y.querySelector(".remove-item-btn");f||(f=document.createElement("button"),f.type="button",f.className="remove-item-btn mt-1 px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm",f.innerHTML="\u2715 Remove",f.onclick=()=>{const E=Array.from(d.children).indexOf(y);d.children.length>s&&(i.splice(E,1),y.remove(),h(),v(),m())},y.appendChild(f));const C=g<=s;f.disabled=C,f.style.opacity=C?"0.5":"1",f.style.pointerEvents=C?"none":"auto"})}function v(){const x=n.querySelector(".add-textarea-btn");if(x&&x.remove(),!l.config.readonly&&i.length<u){const g=document.createElement("button");g.type="button",g.className="add-textarea-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm",g.textContent=`+ Add ${e.label||"Textarea"}`,g.onclick=()=>{i.push(e.default||""),p(e.default||""),v(),m()},n.appendChild(g)}}i.forEach(x=>p(x)),v(),m();const b=document.createElement("p");b.className="text-xs text-gray-500 mt-1",b.textContent=k(e),n.appendChild(b)}function ve(e,t,n){return I(e,t,n)}function ge(e,t,n,o){O(e,t,n,o)}function ye(e,t,n,o){const c=t.state,r=document.createElement("input");if(r.type="number",r.className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",r.name=o,r.placeholder=e.placeholder||"0",e.min!==void 0&&(r.min=e.min.toString()),e.max!==void 0&&(r.max=e.max.toString()),e.step!==void 0&&(r.step=e.step.toString()),r.value=t.prefill[e.key]||e.default||"",r.readOnly=c.config.readonly,!c.config.readonly&&t.instance){const a=()=>{const i=r.value?parseFloat(r.value):null;t.instance.triggerOnChange(o,i)};r.addEventListener("blur",a),r.addEventListener("input",a)}n.appendChild(r);const l=document.createElement("p");l.className="text-xs text-gray-500 mt-1",l.textContent=k(e),n.appendChild(l)}function be(e,t,n,o){var c,r;const l=t.state,a=t.prefill[e.key]||[],i=Array.isArray(a)?[...a]:[],s=(c=e.minCount)!=null?c:1,u=(r=e.maxCount)!=null?r:1/0;for(;i.length<s;)i.push(e.default||"");const d=document.createElement("div");d.className="space-y-2",n.appendChild(d);function h(){d.querySelectorAll(".multiple-number-item").forEach((x,g)=>{const y=x.querySelector("input");y&&(y.name=`${o}[${g}]`)})}function p(x="",g=-1){const y=document.createElement("div");y.className="multiple-number-item flex items-center gap-2";const f=document.createElement("input");if(f.type="number",f.className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",f.placeholder=e.placeholder||"0",e.min!==void 0&&(f.min=e.min.toString()),e.max!==void 0&&(f.max=e.max.toString()),e.step!==void 0&&(f.step=e.step.toString()),f.value=x.toString(),f.readOnly=l.config.readonly,!l.config.readonly&&t.instance){const C=()=>{const E=f.value?parseFloat(f.value):null;t.instance.triggerOnChange(f.name,E)};f.addEventListener("blur",C),f.addEventListener("input",C)}return y.appendChild(f),g===-1?d.appendChild(y):d.insertBefore(y,d.children[g]),h(),y}function m(){if(l.config.readonly)return;const x=d.querySelectorAll(".multiple-number-item"),g=x.length;x.forEach(y=>{let f=y.querySelector(".remove-item-btn");f||(f=document.createElement("button"),f.type="button",f.className="remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded",f.innerHTML="\u2715",f.onclick=()=>{const E=Array.from(d.children).indexOf(y);d.children.length>s&&(i.splice(E,1),y.remove(),h(),v(),m())},y.appendChild(f));const C=g<=s;f.disabled=C,f.style.opacity=C?"0.5":"1",f.style.pointerEvents=C?"none":"auto"})}function v(){const x=n.querySelector(".add-number-btn");if(x&&x.remove(),!l.config.readonly&&i.length<u){const g=document.createElement("button");g.type="button",g.className="add-number-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm",g.textContent=`+ Add ${e.label||"Number"}`,g.onclick=()=>{i.push(e.default||""),p(e.default||""),v(),m()},n.appendChild(g)}}i.forEach(x=>p(x)),v(),m();const b=document.createElement("p");b.className="text-xs text-gray-500 mt-1",b.textContent=k(e),n.appendChild(b)}function xe(e,t,n){var o,c,r,l,a;const i=[],{scopeRoot:s,skipValidation:u}=n,d=(p,m)=>{var v,b;if(!p)return;const x=`error-${p.getAttribute("name")||Math.random().toString(36).substring(7)}`;let g=document.getElementById(x);m?(p.classList.add("invalid"),p.title=m,g||(g=document.createElement("div"),g.id=x,g.className="error-message",g.style.cssText=`
|
|
40
|
+
color: var(--fb-error-color);
|
|
41
|
+
font-size: var(--fb-font-size-small);
|
|
42
|
+
margin-top: 0.25rem;
|
|
43
|
+
`,p.nextSibling?(v=p.parentNode)==null||v.insertBefore(g,p.nextSibling):(b=p.parentNode)==null||b.appendChild(g)),g.textContent=m,g.style.display="block"):(p.classList.remove("invalid"),p.title="",g&&g.remove())},h=(p,m,v)=>{let b=!1;!u&&e.min!==void 0&&e.min!==null&&m<e.min?(i.push(`${v}: < min=${e.min}`),d(p,`< min=${e.min}`),b=!0):!u&&e.max!==void 0&&e.max!==null&&m>e.max&&(i.push(`${v}: > max=${e.max}`),d(p,`> max=${e.max}`),b=!0),b||d(p,null)};if(e.multiple){const p=s.querySelectorAll(`[name^="${t}["]`),m=[];if(p.forEach((v,b)=>{var x,g,y;const f=(x=v==null?void 0:v.value)!=null?x:"";if(f===""){m.push(null),d(v,null);return}const C=parseFloat(f);if(!u&&!Number.isFinite(C)){i.push(`${t}[${b}]: not a number`),d(v,"not a number"),m.push(null);return}h(v,C,`${t}[${b}]`);const E=Number.isInteger((g=e.decimals)!=null?g:0)&&(y=e.decimals)!=null?y:0;m.push(Number(C.toFixed(E)))}),!u){const v=(o=e.minCount)!=null?o:1,b=(c=e.maxCount)!=null?c:1/0,x=m.filter(g=>g!==null);e.required&&x.length===0&&i.push(`${t}: required`),x.length<v&&i.push(`${t}: minimum ${v} items required`),x.length>b&&i.push(`${t}: maximum ${b} items allowed`)}return{value:m,errors:i}}else{const p=s.querySelector(`[name$="${t}"]`),m=(r=p==null?void 0:p.value)!=null?r:"";if(!u&&e.required&&m==="")return i.push(`${t}: required`),d(p,"required"),{value:null,errors:i};if(m==="")return d(p,null),{value:null,errors:i};const v=parseFloat(m);if(!u&&!Number.isFinite(v))return i.push(`${t}: not a number`),d(p,"not a number"),{value:null,errors:i};h(p,v,t);const b=Number.isInteger((l=e.decimals)!=null?l:0)&&(a=e.decimals)!=null?a:0;return{value:Number(v.toFixed(b)),errors:i}}}function Ce(e,t,n,o){const{scopeRoot:c}=o;if(e.multiple){if(!Array.isArray(n)){console.warn(`updateNumberField: Expected array for multiple field "${t}", got ${typeof n}`);return}const r=c.querySelectorAll(`[name^="${t}["]`);r.forEach((l,a)=>{a<n.length&&(l.value=n[a]!=null?String(n[a]):"",l.classList.remove("invalid"),l.title="")}),n.length!==r.length&&console.warn(`updateNumberField: Multiple field "${t}" has ${r.length} inputs but received ${n.length} values. Consider re-rendering for add/remove.`)}else{const r=c.querySelector(`[name="${t}"]`);r&&(r.value=n!=null?String(n):"",r.classList.remove("invalid"),r.title="")}}function Ee(e,t,n,o){const c=t.state,r=document.createElement("select");if(r.className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",r.name=o,r.disabled=c.config.readonly,(e.options||[]).forEach(a=>{const i=document.createElement("option");i.value=a.value,i.textContent=a.label,(t.prefill[e.key]||e.default)===a.value&&(i.selected=!0),r.appendChild(i)}),!c.config.readonly&&t.instance){const a=()=>{t.instance.triggerOnChange(o,r.value)};r.addEventListener("change",a)}n.appendChild(r);const l=document.createElement("p");l.className="text-xs text-gray-500 mt-1",l.textContent=k(e),n.appendChild(l)}function we(e,t,n,o){var c,r,l,a;const i=t.state,s=t.prefill[e.key]||[],u=Array.isArray(s)?[...s]:[],d=(c=e.minCount)!=null?c:1,h=(r=e.maxCount)!=null?r:1/0;for(;u.length<d;)u.push(e.default||((a=(l=e.options)==null?void 0:l[0])==null?void 0:a.value)||"");const p=document.createElement("div");p.className="space-y-2",n.appendChild(p);function m(){p.querySelectorAll(".multiple-select-item").forEach((y,f)=>{const C=y.querySelector("select");C&&(C.name=`${o}[${f}]`)})}function v(y="",f=-1){const C=document.createElement("div");C.className="multiple-select-item flex items-center gap-2";const E=document.createElement("select");if(E.className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500",E.disabled=i.config.readonly,(e.options||[]).forEach(w=>{const S=document.createElement("option");S.value=w.value,S.textContent=w.label,y===w.value&&(S.selected=!0),E.appendChild(S)}),!i.config.readonly&&t.instance){const w=()=>{t.instance.triggerOnChange(E.name,E.value)};E.addEventListener("change",w)}return C.appendChild(E),f===-1?p.appendChild(C):p.insertBefore(C,p.children[f]),m(),C}function b(){if(i.config.readonly)return;const y=p.querySelectorAll(".multiple-select-item"),f=y.length;y.forEach(C=>{let E=C.querySelector(".remove-item-btn");E||(E=document.createElement("button"),E.type="button",E.className="remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded",E.innerHTML="\u2715",E.onclick=()=>{const S=Array.from(p.children).indexOf(C);p.children.length>d&&(u.splice(S,1),C.remove(),m(),x(),b())},C.appendChild(E));const w=f<=d;E.disabled=w,E.style.opacity=w?"0.5":"1",E.style.pointerEvents=w?"none":"auto"})}function x(){const y=n.querySelector(".add-select-btn");if(y&&y.remove(),!i.config.readonly&&u.length<h){const f=document.createElement("button");f.type="button",f.className="add-select-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm",f.textContent=`+ Add ${e.label||"Selection"}`,f.onclick=()=>{var C,E;const w=e.default||((E=(C=e.options)==null?void 0:C[0])==null?void 0:E.value)||"";u.push(w),v(w),x(),b()},n.appendChild(f)}}u.forEach(y=>v(y)),x(),b();const g=document.createElement("p");g.className="text-xs text-gray-500 mt-1",g.textContent=k(e),n.appendChild(g)}function $e(e,t,n){var o;const c=[],{scopeRoot:r,skipValidation:l}=n,a=(s,u)=>{var d,h;if(!s)return;const p=`error-${s.getAttribute("name")||Math.random().toString(36).substring(7)}`;let m=document.getElementById(p);u?(s.classList.add("invalid"),s.title=u,m||(m=document.createElement("div"),m.id=p,m.className="error-message",m.style.cssText=`
|
|
44
|
+
color: var(--fb-error-color);
|
|
45
|
+
font-size: var(--fb-font-size-small);
|
|
46
|
+
margin-top: 0.25rem;
|
|
47
|
+
`,s.nextSibling?(d=s.parentNode)==null||d.insertBefore(m,s.nextSibling):(h=s.parentNode)==null||h.appendChild(m)),m.textContent=u,m.style.display="block"):(s.classList.remove("invalid"),s.title="",m&&m.remove())},i=(s,u,d,h)=>{var p,m;if(l)return;const v=u.filter(h),b="minCount"in d&&(p=d.minCount)!=null?p:1,x="maxCount"in d&&(m=d.maxCount)!=null?m:1/0;d.required&&v.length===0&&c.push(`${s}: required`),v.length<b&&c.push(`${s}: minimum ${b} items required`),v.length>x&&c.push(`${s}: maximum ${x} items allowed`)};if("multiple"in e&&e.multiple){const s=r.querySelectorAll(`[name^="${t}["]`),u=[];return s.forEach(d=>{var h;const p=(h=d==null?void 0:d.value)!=null?h:"";u.push(p),a(d,null)}),i(t,u,e,d=>d!==""),{value:u,errors:c}}else{const s=r.querySelector(`[name$="${t}"]`),u=(o=s==null?void 0:s.value)!=null?o:"";return!l&&e.required&&u===""?(c.push(`${t}: required`),a(s,"required"),{value:null,errors:c}):(a(s,null),{value:u===""?null:u,errors:c})}}function ke(e,t,n,o){const{scopeRoot:c}=o;if("multiple"in e&&e.multiple){if(!Array.isArray(n)){console.warn(`updateSelectField: Expected array for multiple field "${t}", got ${typeof n}`);return}const r=c.querySelectorAll(`[name^="${t}["]`);r.forEach((l,a)=>{a<n.length&&(l.value=n[a]!=null?String(n[a]):"",l.querySelectorAll("option").forEach(i=>{i.selected=i.value===String(n[a])}),l.classList.remove("invalid"),l.title="")}),n.length!==r.length&&console.warn(`updateSelectField: Multiple field "${t}" has ${r.length} selects but received ${n.length} values. Consider re-rendering for add/remove.`)}else{const r=c.querySelector(`[name="${t}"]`);r&&(r.value=n!=null?String(n):"",r.querySelectorAll("option").forEach(l=>{l.selected=l.value===String(n)}),r.classList.remove("invalid"),r.title="")}}function $(e,t){const n=t.config.locale||"en";return(t.config.translations[n]||t.config.translations.en)[e]||e}async function U(e,t,n,o={}){const{fileName:c="",isReadonly:r=!1,deps:l=null}=o;if(!r&&l&&(!l.picker||!l.fileUploadHandler||!l.dragHandler))throw new Error("renderFilePreview: missing deps {picker, fileUploadHandler, dragHandler}");N(e),r&&e.classList.add("cursor-pointer");const a=document.createElement("img");a.className="w-full h-full object-contain",a.alt=c||"Preview";const i=n.resourceIndex.get(t);if(i&&i.file&&i.file instanceof File){if(i.type&&i.type.startsWith("image/")){const s=new FileReader;s.onload=u=>{var d;a.src=((d=u.target)==null?void 0:d.result)||""},s.readAsDataURL(i.file),e.appendChild(a)}else if(i.type&&i.type.startsWith("video/")){const s=URL.createObjectURL(i.file);e.onclick=null;const u=e.cloneNode(!1);e.parentNode&&e.parentNode.replaceChild(u,e),e=u,e.innerHTML=`
|
|
409
48
|
<div class="relative group h-full">
|
|
410
49
|
<video class="w-full h-full object-contain" controls preload="auto" muted>
|
|
411
|
-
<source src="${
|
|
50
|
+
<source src="${s}" type="${i.type}">
|
|
412
51
|
Your browser does not support the video tag.
|
|
413
52
|
</video>
|
|
414
53
|
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
|
|
415
54
|
<button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
|
|
416
|
-
${
|
|
55
|
+
${$("removeElement",n)}
|
|
417
56
|
</button>
|
|
418
57
|
<button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
|
|
419
58
|
Change
|
|
420
59
|
</button>
|
|
421
60
|
</div>
|
|
422
61
|
</div>
|
|
423
|
-
`;
|
|
424
|
-
|
|
425
|
-
// Add click handlers to the custom buttons
|
|
426
|
-
const changeBtn = container.querySelector(".change-file-btn");
|
|
427
|
-
if (changeBtn) {
|
|
428
|
-
changeBtn.onclick = (e) => {
|
|
429
|
-
e.stopPropagation();
|
|
430
|
-
if (deps?.picker) {
|
|
431
|
-
deps.picker.click();
|
|
432
|
-
}
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const deleteBtn = container.querySelector(".delete-file-btn");
|
|
437
|
-
if (deleteBtn) {
|
|
438
|
-
deleteBtn.onclick = (e) => {
|
|
439
|
-
e.stopPropagation();
|
|
440
|
-
// Clear the file
|
|
441
|
-
state.resourceIndex.delete(resourceId);
|
|
442
|
-
// Update hidden input
|
|
443
|
-
const hiddenInput = container.parentElement.querySelector(
|
|
444
|
-
'input[type="hidden"]',
|
|
445
|
-
);
|
|
446
|
-
if (hiddenInput) {
|
|
447
|
-
hiddenInput.value = "";
|
|
448
|
-
}
|
|
449
|
-
// Clear preview and show placeholder
|
|
450
|
-
if (deps?.fileUploadHandler) {
|
|
451
|
-
container.onclick = deps.fileUploadHandler;
|
|
452
|
-
}
|
|
453
|
-
if (deps?.dragHandler) {
|
|
454
|
-
setupDragAndDrop(container, deps.dragHandler);
|
|
455
|
-
}
|
|
456
|
-
container.innerHTML = `
|
|
62
|
+
`;const d=e.querySelector(".change-file-btn");d&&(d.onclick=p=>{p.stopPropagation(),l!=null&&l.picker&&l.picker.click()});const h=e.querySelector(".delete-file-btn");h&&(h.onclick=p=>{var m;p.stopPropagation(),n.resourceIndex.delete(t);const v=(m=e.parentElement)==null?void 0:m.querySelector('input[type="hidden"]');v&&(v.value=""),l!=null&&l.fileUploadHandler&&(e.onclick=l.fileUploadHandler),l!=null&&l.dragHandler&&z(e,l.dragHandler),e.innerHTML=`
|
|
457
63
|
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
458
64
|
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
459
65
|
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
460
66
|
</svg>
|
|
461
|
-
<div class="text-sm text-center">${
|
|
67
|
+
<div class="text-sm text-center">${$("clickDragText",n)}</div>
|
|
462
68
|
</div>
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (hiddenInput) {
|
|
483
|
-
hiddenInput.value = "";
|
|
484
|
-
}
|
|
485
|
-
// Clear preview and show placeholder
|
|
486
|
-
container.innerHTML = `
|
|
487
|
-
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
488
|
-
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
489
|
-
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
490
|
-
</svg>
|
|
491
|
-
<div class="text-sm text-center">${t("clickDragText")}</div>
|
|
492
|
-
</div>
|
|
493
|
-
`;
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
} else if (state.config.getThumbnail) {
|
|
497
|
-
// Try to get thumbnail from config for uploaded files
|
|
498
|
-
try {
|
|
499
|
-
const thumbnailUrl = await state.config.getThumbnail(resourceId);
|
|
500
|
-
|
|
501
|
-
if (thumbnailUrl) {
|
|
502
|
-
img.src = thumbnailUrl;
|
|
503
|
-
container.appendChild(img);
|
|
504
|
-
} else {
|
|
505
|
-
// Fallback to file icon
|
|
506
|
-
container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">🖼️</div><div class="text-sm">${
|
|
507
|
-
fileName
|
|
508
|
-
}</div></div>`;
|
|
509
|
-
}
|
|
510
|
-
} catch (error) {
|
|
511
|
-
console.warn("Thumbnail loading failed:", error);
|
|
512
|
-
container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">📁</div><div class="text-sm">${
|
|
513
|
-
fileName
|
|
514
|
-
}</div></div>`;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Add delete button for edit mode
|
|
518
|
-
if (!isReadonly) {
|
|
519
|
-
addDeleteButton(container, () => {
|
|
520
|
-
// Clear the file
|
|
521
|
-
state.resourceIndex.delete(resourceId);
|
|
522
|
-
// Update hidden input
|
|
523
|
-
const hiddenInput = container.parentElement.querySelector(
|
|
524
|
-
'input[type="hidden"]',
|
|
525
|
-
);
|
|
526
|
-
if (hiddenInput) {
|
|
527
|
-
hiddenInput.value = "";
|
|
528
|
-
}
|
|
529
|
-
// Clear preview and show placeholder
|
|
530
|
-
container.innerHTML = `
|
|
531
|
-
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
532
|
-
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
533
|
-
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
534
|
-
</svg>
|
|
535
|
-
<div class="text-sm text-center">${t("clickDragText")}</div>
|
|
536
|
-
</div>
|
|
537
|
-
`;
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
} else {
|
|
541
|
-
// No file and no getThumbnail config - fallback
|
|
542
|
-
container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">🖼️</div><div class="text-sm">${
|
|
543
|
-
fileName
|
|
544
|
-
}</div></div>`;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// Add click handler for download in readonly mode
|
|
548
|
-
if (isReadonly && state.config.downloadFile) {
|
|
549
|
-
container.style.cursor = "pointer";
|
|
550
|
-
container.onclick = () => {
|
|
551
|
-
if (state.config.downloadFile) {
|
|
552
|
-
state.config.downloadFile(resourceId, fileName);
|
|
553
|
-
}
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function renderThumbnailForResource(slot, rid, meta) {
|
|
559
|
-
const url = state.config.getThumbnail(rid);
|
|
560
|
-
if (url) {
|
|
561
|
-
const img = document.createElement("img");
|
|
562
|
-
img.className = "w-full h-full object-contain";
|
|
563
|
-
img.alt = meta.name;
|
|
564
|
-
img.src = url;
|
|
565
|
-
slot.appendChild(img);
|
|
566
|
-
} else {
|
|
567
|
-
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
568
|
-
<svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
|
569
|
-
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
570
|
-
</svg>
|
|
571
|
-
</div>`;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function renderResourcePills(container, rids, onRemove) {
|
|
576
|
-
clear(container);
|
|
577
|
-
|
|
578
|
-
// Show initial placeholder only if this is the first render (no previous grid)
|
|
579
|
-
// Check if container already has grid class to determine if this is initial render
|
|
580
|
-
const isInitialRender = !container.classList.contains("grid");
|
|
581
|
-
|
|
582
|
-
if ((!rids || rids.length === 0) && isInitialRender) {
|
|
583
|
-
// Create grid container
|
|
584
|
-
const gridContainer = document.createElement("div");
|
|
585
|
-
gridContainer.className = "grid grid-cols-4 gap-3 mb-3";
|
|
586
|
-
|
|
587
|
-
// Create 4 placeholder slots
|
|
588
|
-
for (let i = 0; i < 4; i++) {
|
|
589
|
-
const slot = document.createElement("div");
|
|
590
|
-
slot.className =
|
|
591
|
-
"aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors";
|
|
592
|
-
|
|
593
|
-
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
594
|
-
svg.setAttribute("class", "w-12 h-12 text-gray-400");
|
|
595
|
-
svg.setAttribute("fill", "currentColor");
|
|
596
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
597
|
-
|
|
598
|
-
const path = document.createElementNS(
|
|
599
|
-
"http://www.w3.org/2000/svg",
|
|
600
|
-
"path",
|
|
601
|
-
);
|
|
602
|
-
path.setAttribute(
|
|
603
|
-
"d",
|
|
604
|
-
"M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z",
|
|
605
|
-
);
|
|
606
|
-
|
|
607
|
-
svg.appendChild(path);
|
|
608
|
-
slot.appendChild(svg);
|
|
609
|
-
|
|
610
|
-
// Add click handler to each slot
|
|
611
|
-
slot.onclick = () => {
|
|
612
|
-
// Look for file input - check parent containers that have space-y-2 class
|
|
613
|
-
let filesWrapper = container.parentElement;
|
|
614
|
-
while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
|
|
615
|
-
filesWrapper = filesWrapper.parentElement;
|
|
616
|
-
}
|
|
617
|
-
// If no parent with space-y-2, container itself might be the wrapper
|
|
618
|
-
if (!filesWrapper && container.classList.contains("space-y-2")) {
|
|
619
|
-
filesWrapper = container;
|
|
620
|
-
}
|
|
621
|
-
const fileInput = filesWrapper?.querySelector('input[type="file"]');
|
|
622
|
-
if (fileInput) fileInput.click();
|
|
623
|
-
};
|
|
624
|
-
|
|
625
|
-
gridContainer.appendChild(slot);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Create text container
|
|
629
|
-
const textContainer = document.createElement("div");
|
|
630
|
-
textContainer.className = "text-center text-xs text-gray-600";
|
|
631
|
-
|
|
632
|
-
const uploadLink = document.createElement("span");
|
|
633
|
-
uploadLink.className = "underline cursor-pointer";
|
|
634
|
-
uploadLink.textContent = t("uploadText");
|
|
635
|
-
uploadLink.onclick = (e) => {
|
|
636
|
-
e.stopPropagation();
|
|
637
|
-
// Look for file input - check parent containers that have space-y-2 class
|
|
638
|
-
let filesWrapper = container.parentElement;
|
|
639
|
-
while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
|
|
640
|
-
filesWrapper = filesWrapper.parentElement;
|
|
641
|
-
}
|
|
642
|
-
// If no parent with space-y-2, container itself might be the wrapper
|
|
643
|
-
if (!filesWrapper && container.classList.contains("space-y-2")) {
|
|
644
|
-
filesWrapper = container;
|
|
645
|
-
}
|
|
646
|
-
const fileInput = filesWrapper?.querySelector('input[type="file"]');
|
|
647
|
-
if (fileInput) fileInput.click();
|
|
648
|
-
};
|
|
649
|
-
|
|
650
|
-
textContainer.appendChild(uploadLink);
|
|
651
|
-
textContainer.appendChild(document.createTextNode(` ${t("dragDropText")}`));
|
|
652
|
-
|
|
653
|
-
// Clear and append
|
|
654
|
-
container.appendChild(gridContainer);
|
|
655
|
-
container.appendChild(textContainer);
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// Always show files grid if we have files OR if this was already a grid
|
|
660
|
-
// This prevents shrinking when deleting the last file
|
|
661
|
-
// Preserve the original "files-list" class and add grid classes
|
|
662
|
-
container.className = "files-list grid grid-cols-4 gap-3 mt-2";
|
|
663
|
-
|
|
664
|
-
// Calculate how many slots we need (at least 4, then expand by rows of 4)
|
|
665
|
-
const currentImagesCount = rids ? rids.length : 0;
|
|
666
|
-
// Calculate rows needed: always show an extra slot for adding next file
|
|
667
|
-
// 0-3 files → 1 row (4 slots), 4-7 files → 2 rows (8 slots), 8-11 files → 3 rows (12 slots)
|
|
668
|
-
const rowsNeeded = Math.floor(currentImagesCount / 4) + 1;
|
|
669
|
-
const slotsNeeded = rowsNeeded * 4;
|
|
670
|
-
|
|
671
|
-
// Add all slots (filled and empty)
|
|
672
|
-
for (let i = 0; i < slotsNeeded; i++) {
|
|
673
|
-
const slot = document.createElement("div");
|
|
674
|
-
|
|
675
|
-
if (rids && i < rids.length) {
|
|
676
|
-
// Filled slot with image preview
|
|
677
|
-
const rid = rids[i];
|
|
678
|
-
const meta = state.resourceIndex.get(rid);
|
|
679
|
-
slot.className =
|
|
680
|
-
"resource-pill aspect-square bg-gray-100 rounded-lg overflow-hidden relative group border border-gray-300";
|
|
681
|
-
slot.dataset.resourceId = rid;
|
|
682
|
-
|
|
683
|
-
// Add image, video, or file content
|
|
684
|
-
if (meta && meta.type?.startsWith("image/")) {
|
|
685
|
-
if (meta.file && meta.file instanceof File) {
|
|
686
|
-
// Use FileReader for local files
|
|
687
|
-
const img = document.createElement("img");
|
|
688
|
-
img.className = "w-full h-full object-contain";
|
|
689
|
-
img.alt = meta.name;
|
|
690
|
-
|
|
691
|
-
const reader = new FileReader();
|
|
692
|
-
reader.onload = (e) => {
|
|
693
|
-
img.src = e.target.result;
|
|
694
|
-
};
|
|
695
|
-
reader.readAsDataURL(meta.file);
|
|
696
|
-
slot.appendChild(img);
|
|
697
|
-
} else if (state.config.getThumbnail) {
|
|
698
|
-
// Use getThumbnail for uploaded files
|
|
699
|
-
renderThumbnailForResource(slot, rid, meta);
|
|
700
|
-
} else {
|
|
701
|
-
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
702
|
-
<svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
|
703
|
-
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
704
|
-
</svg>
|
|
705
|
-
</div>`;
|
|
706
|
-
}
|
|
707
|
-
} else if (meta && meta.type?.startsWith("video/")) {
|
|
708
|
-
if (meta.file && meta.file instanceof File) {
|
|
709
|
-
// Video file - use object URL for preview in thumbnail format
|
|
710
|
-
const videoUrl = URL.createObjectURL(meta.file);
|
|
711
|
-
slot.innerHTML = `
|
|
712
|
-
<div class="relative group h-full w-full">
|
|
713
|
-
<video class="w-full h-full object-contain" preload="metadata" muted>
|
|
714
|
-
<source src="${videoUrl}" type="${meta.type}">
|
|
69
|
+
`})}else e.innerHTML=`<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">\u{1F4C1}</div><div class="text-sm">${c}</div></div>`;!r&&!(i&&i.type&&i.type.startsWith("video/"))&&Le(e,n,()=>{var s;n.resourceIndex.delete(t);const u=(s=e.parentElement)==null?void 0:s.querySelector('input[type="hidden"]');u&&(u.value=""),e.innerHTML=`
|
|
70
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
71
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
72
|
+
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
73
|
+
</svg>
|
|
74
|
+
<div class="text-sm text-center">${$("clickDragText",n)}</div>
|
|
75
|
+
</div>
|
|
76
|
+
`})}else if(n.config.getThumbnail)try{const s=await n.config.getThumbnail(t);s?(N(e),a.src=s,e.appendChild(a)):j(e,n)}catch(s){console.error("Failed to get thumbnail:",s),e.innerHTML=`
|
|
77
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
78
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
79
|
+
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
80
|
+
</svg>
|
|
81
|
+
<div class="text-sm text-center">${c||"Preview unavailable"}</div>
|
|
82
|
+
</div>
|
|
83
|
+
`}else j(e,n)}async function B(e,t,n){var o,c;const r=t.resourceIndex.get(e),l=(r==null?void 0:r.name)||e.split("/").pop()||"file",a=l.toLowerCase().match(/\.psd$/),i=document.createElement("div");i.className=a?"space-y-2":"space-y-3";const s=document.createElement("div");a?s.className="bg-gray-100 rounded-lg overflow-hidden cursor-pointer hover:opacity-90 transition-opacity flex items-center p-3 max-w-sm":s.className="bg-gray-100 rounded-lg overflow-hidden cursor-pointer hover:opacity-90 transition-opacity";const u=!a&&(((o=r==null?void 0:r.type)==null?void 0:o.startsWith("image/"))||l.toLowerCase().match(/\.(jpg|jpeg|png|gif|webp)$/)),d=((c=r==null?void 0:r.type)==null?void 0:c.startsWith("video/"))||l.toLowerCase().match(/\.(mp4|webm|avi|mov)$/);if(u)if(t.config.getThumbnail)try{const m=await t.config.getThumbnail(e);m?s.innerHTML=`<img src="${m}" alt="${l}" class="w-full h-auto">`:s.innerHTML=`<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${l}</div></div></div>`}catch(m){console.warn("getThumbnail failed for",e,m),s.innerHTML=`<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${l}</div></div></div>`}else s.innerHTML=`<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F5BC}\uFE0F</div><div class="text-sm">${l}</div></div></div>`;else if(d)if(t.config.getThumbnail)try{const m=await t.config.getThumbnail(e);m?s.innerHTML=`
|
|
84
|
+
<div class="relative group">
|
|
85
|
+
<video class="w-full h-auto" controls preload="auto" muted>
|
|
86
|
+
<source src="${m}" type="${(r==null?void 0:r.type)||"video/mp4"}">
|
|
87
|
+
\u0412\u0430\u0448 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0434\u0435\u043E.
|
|
715
88
|
</video>
|
|
716
|
-
<div class="absolute inset-0 bg-black bg-opacity-
|
|
717
|
-
<div class="bg-white bg-opacity-90 rounded-full p-
|
|
718
|
-
<svg class="w-
|
|
89
|
+
<div class="absolute inset-0 bg-black bg-opacity-20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
|
|
90
|
+
<div class="bg-white bg-opacity-90 rounded-full p-3">
|
|
91
|
+
<svg class="w-8 h-8 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
|
|
719
92
|
<path d="M8 5v14l11-7z"/>
|
|
720
93
|
</svg>
|
|
721
94
|
</div>
|
|
722
95
|
</div>
|
|
723
96
|
</div>
|
|
724
|
-
|
|
725
|
-
} else if (state.config.getThumbnail) {
|
|
726
|
-
// Use getThumbnail for uploaded video files
|
|
727
|
-
const videoUrl = state.config.getThumbnail(rid);
|
|
728
|
-
if (videoUrl) {
|
|
729
|
-
slot.innerHTML = `
|
|
730
|
-
<div class="relative group h-full w-full">
|
|
731
|
-
<video class="w-full h-full object-contain" preload="metadata" muted>
|
|
732
|
-
<source src="${videoUrl}" type="${meta.type}">
|
|
733
|
-
</video>
|
|
734
|
-
<div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
|
735
|
-
<div class="bg-white bg-opacity-90 rounded-full p-1">
|
|
736
|
-
<svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
|
|
737
|
-
<path d="M8 5v14l11-7z"/>
|
|
738
|
-
</svg>
|
|
739
|
-
</div>
|
|
740
|
-
</div>
|
|
741
|
-
</div>
|
|
742
|
-
`;
|
|
743
|
-
} else {
|
|
744
|
-
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
745
|
-
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
|
746
|
-
<path d="M8 5v14l11-7z"/>
|
|
747
|
-
</svg>
|
|
748
|
-
<div class="text-xs mt-1">${meta?.name || "Video"}</div>
|
|
749
|
-
</div>`;
|
|
750
|
-
}
|
|
751
|
-
} else {
|
|
752
|
-
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
753
|
-
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
|
754
|
-
<path d="M8 5v14l11-7z"/>
|
|
755
|
-
</svg>
|
|
756
|
-
<div class="text-xs mt-1">${meta?.name || "Video"}</div>
|
|
757
|
-
</div>`;
|
|
758
|
-
}
|
|
759
|
-
} else {
|
|
760
|
-
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
761
|
-
<div class="text-2xl mb-1">📁</div>
|
|
762
|
-
<div class="text-xs">${meta?.name || "File"}</div>
|
|
763
|
-
</div>`;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// Add remove button overlay (similar to file field)
|
|
767
|
-
if (onRemove) {
|
|
768
|
-
const overlay = document.createElement("div");
|
|
769
|
-
overlay.className =
|
|
770
|
-
"absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center";
|
|
771
|
-
|
|
772
|
-
const removeBtn = document.createElement("button");
|
|
773
|
-
removeBtn.className = "bg-red-600 text-white px-2 py-1 rounded text-xs";
|
|
774
|
-
removeBtn.textContent = t("removeElement");
|
|
775
|
-
removeBtn.onclick = (e) => {
|
|
776
|
-
e.stopPropagation();
|
|
777
|
-
onRemove(rid);
|
|
778
|
-
};
|
|
779
|
-
|
|
780
|
-
overlay.appendChild(removeBtn);
|
|
781
|
-
slot.appendChild(overlay);
|
|
782
|
-
}
|
|
783
|
-
} else {
|
|
784
|
-
// Empty slot placeholder
|
|
785
|
-
slot.className =
|
|
786
|
-
"aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors";
|
|
787
|
-
slot.innerHTML =
|
|
788
|
-
'<svg class="w-12 h-12 text-gray-400" fill="currentColor" viewBox="0 0 24 24"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>';
|
|
789
|
-
slot.onclick = () => {
|
|
790
|
-
// Look for file input - check parent containers that have space-y-2 class
|
|
791
|
-
let filesWrapper = container.parentElement;
|
|
792
|
-
while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
|
|
793
|
-
filesWrapper = filesWrapper.parentElement;
|
|
794
|
-
}
|
|
795
|
-
// If no parent with space-y-2, container itself might be the wrapper
|
|
796
|
-
if (!filesWrapper && container.classList.contains("space-y-2")) {
|
|
797
|
-
filesWrapper = container;
|
|
798
|
-
}
|
|
799
|
-
const fileInput = filesWrapper?.querySelector('input[type="file"]');
|
|
800
|
-
if (fileInput) fileInput.click();
|
|
801
|
-
};
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
container.appendChild(slot);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// Utility functions (currently unused but may be needed in future)
|
|
809
|
-
// function formatFileSize(bytes) {
|
|
810
|
-
// if (bytes === 0) return "0 B";
|
|
811
|
-
// const k = 1024;
|
|
812
|
-
// const sizes = ["B", "KB", "MB", "GB"];
|
|
813
|
-
// const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
814
|
-
// return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
815
|
-
// }
|
|
816
|
-
|
|
817
|
-
// function generateResourceId() {
|
|
818
|
-
// return (
|
|
819
|
-
// `res_${Math.random().toString(36).substr(2, 9)}${Date.now().toString(36)}`
|
|
820
|
-
// );
|
|
821
|
-
// }
|
|
822
|
-
|
|
823
|
-
async function handleFileSelect(file, container, fieldName, deps = null) {
|
|
824
|
-
let rid;
|
|
825
|
-
|
|
826
|
-
// If uploadHandler is configured, use it to upload the file
|
|
827
|
-
if (state.config.uploadFile) {
|
|
828
|
-
try {
|
|
829
|
-
rid = await state.config.uploadFile(file);
|
|
830
|
-
if (typeof rid !== "string") {
|
|
831
|
-
throw new Error("Upload handler must return a string resource ID");
|
|
832
|
-
}
|
|
833
|
-
} catch (error) {
|
|
834
|
-
throw new Error(`File upload failed: ${error.message}`);
|
|
835
|
-
}
|
|
836
|
-
} else {
|
|
837
|
-
throw new Error(
|
|
838
|
-
"No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()",
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
state.resourceIndex.set(rid, {
|
|
843
|
-
name: file.name,
|
|
844
|
-
type: file.type,
|
|
845
|
-
size: file.size,
|
|
846
|
-
file, // Store the file object for local preview
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
// Create hidden input to store the resource ID
|
|
850
|
-
let hiddenInput = container.parentElement.querySelector(
|
|
851
|
-
'input[type="hidden"]',
|
|
852
|
-
);
|
|
853
|
-
if (!hiddenInput) {
|
|
854
|
-
hiddenInput = document.createElement("input");
|
|
855
|
-
hiddenInput.type = "hidden";
|
|
856
|
-
hiddenInput.name = fieldName;
|
|
857
|
-
container.parentElement.appendChild(hiddenInput);
|
|
858
|
-
}
|
|
859
|
-
hiddenInput.value = rid;
|
|
860
|
-
|
|
861
|
-
renderFilePreview(container, rid, {
|
|
862
|
-
fileName: file.name,
|
|
863
|
-
isReadonly: false,
|
|
864
|
-
deps,
|
|
865
|
-
}).catch(console.error);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
function setupDragAndDrop(element, dropHandler) {
|
|
869
|
-
element.addEventListener("dragover", (e) => {
|
|
870
|
-
e.preventDefault();
|
|
871
|
-
element.classList.add("border-blue-500", "bg-blue-50");
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
element.addEventListener("dragleave", (e) => {
|
|
875
|
-
e.preventDefault();
|
|
876
|
-
element.classList.remove("border-blue-500", "bg-blue-50");
|
|
877
|
-
});
|
|
878
|
-
|
|
879
|
-
element.addEventListener("drop", (e) => {
|
|
880
|
-
e.preventDefault();
|
|
881
|
-
element.classList.remove("border-blue-500", "bg-blue-50");
|
|
882
|
-
dropHandler(e.dataTransfer.files);
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function addDeleteButton(container, onDelete) {
|
|
887
|
-
// Remove existing overlay if any
|
|
888
|
-
const existingOverlay = container.querySelector(".delete-overlay");
|
|
889
|
-
if (existingOverlay) {
|
|
890
|
-
existingOverlay.remove();
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// Create overlay with center delete button (like in files)
|
|
894
|
-
const overlay = document.createElement("div");
|
|
895
|
-
overlay.className =
|
|
896
|
-
"delete-overlay absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center";
|
|
897
|
-
|
|
898
|
-
const deleteBtn = document.createElement("button");
|
|
899
|
-
deleteBtn.className =
|
|
900
|
-
"bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700 transition-colors";
|
|
901
|
-
deleteBtn.textContent = t("removeElement");
|
|
902
|
-
deleteBtn.onclick = (e) => {
|
|
903
|
-
e.stopPropagation();
|
|
904
|
-
onDelete();
|
|
905
|
-
};
|
|
906
|
-
|
|
907
|
-
overlay.appendChild(deleteBtn);
|
|
908
|
-
container.appendChild(overlay);
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
// JSON path resolution for external actions (currently unused but kept for future use)
|
|
912
|
-
// eslint-disable-next-line no-unused-vars
|
|
913
|
-
function resolveFieldPath(path, formData) {
|
|
914
|
-
// Remove leading $input_data. prefix if present
|
|
915
|
-
const cleanPath = path.replace(/^\$input_data\./, "");
|
|
916
|
-
|
|
917
|
-
// Split path into segments, handling array notation
|
|
918
|
-
const segments = cleanPath.split(/[.[\]]/).filter(Boolean);
|
|
919
|
-
|
|
920
|
-
// Try to find the corresponding form element
|
|
921
|
-
return findElementByPath(segments, formData);
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
function findElementByPath(segments, data, currentPath = "") {
|
|
925
|
-
if (segments.length === 0) return currentPath;
|
|
926
|
-
|
|
927
|
-
const [head, ...tail] = segments;
|
|
928
|
-
|
|
929
|
-
// Check if this is an array index
|
|
930
|
-
const isArrayIndex = /^\d+$/.test(head);
|
|
931
|
-
|
|
932
|
-
if (isArrayIndex) {
|
|
933
|
-
// Array index case: build path like "fieldName[index]"
|
|
934
|
-
const newPath = currentPath ? `${currentPath}[${head}]` : `[${head}]`;
|
|
935
|
-
return findElementByPath(tail, data, newPath);
|
|
936
|
-
} else {
|
|
937
|
-
// Regular field name
|
|
938
|
-
const newPath = currentPath ? `${currentPath}.${head}` : head;
|
|
939
|
-
return findElementByPath(tail, data, newPath);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
function findFormElementByFieldPath(fieldPath) {
|
|
944
|
-
// Try to find the form element that corresponds to the field path
|
|
945
|
-
// This looks for elements with name attributes that match the path pattern
|
|
946
|
-
|
|
947
|
-
if (!state.formRoot) return null;
|
|
948
|
-
|
|
949
|
-
// In edit mode, try to find input elements with name attributes
|
|
950
|
-
if (!state.config.readonly) {
|
|
951
|
-
// Try exact match first
|
|
952
|
-
let element = state.formRoot.querySelector(`[name="${fieldPath}"]`);
|
|
953
|
-
if (element) return element;
|
|
954
|
-
|
|
955
|
-
// Try with array notation variations
|
|
956
|
-
const variations = [
|
|
957
|
-
fieldPath,
|
|
958
|
-
fieldPath.replace(/\[(\d+)\]/g, "[$1]"), // normalize array notation
|
|
959
|
-
fieldPath.replace(/\./g, "[") +
|
|
960
|
-
"]".repeat((fieldPath.match(/\./g) || []).length), // convert dots to brackets
|
|
961
|
-
];
|
|
962
|
-
|
|
963
|
-
for (const variation of variations) {
|
|
964
|
-
element = state.formRoot.querySelector(`[name="${variation}"]`);
|
|
965
|
-
if (element) return element;
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
// In readonly mode, or if no input found, look for field wrappers using data attributes
|
|
970
|
-
// Find the schema element for this field path to match against rendered fields
|
|
971
|
-
const schemaElement = findSchemaElement(fieldPath);
|
|
972
|
-
if (!schemaElement) return null;
|
|
973
|
-
|
|
974
|
-
// Look for field wrappers that contain the field key
|
|
975
|
-
const fieldWrappers = state.formRoot.querySelectorAll('.fb-field-wrapper');
|
|
976
|
-
for (const wrapper of fieldWrappers) {
|
|
977
|
-
// Try to find a label or element that matches this field
|
|
978
|
-
const labelText = schemaElement.label || schemaElement.key;
|
|
979
|
-
const labelElement = wrapper.querySelector('label');
|
|
980
|
-
if (labelElement && (labelElement.textContent === labelText || labelElement.textContent === `${labelText}*`)) {
|
|
981
|
-
// Create a dummy element for the field so actions can attach
|
|
982
|
-
let fieldElement = wrapper.querySelector('.field-placeholder');
|
|
983
|
-
if (!fieldElement) {
|
|
984
|
-
fieldElement = document.createElement('div');
|
|
985
|
-
fieldElement.className = 'field-placeholder';
|
|
986
|
-
fieldElement.style.display = 'none';
|
|
987
|
-
wrapper.appendChild(fieldElement);
|
|
988
|
-
}
|
|
989
|
-
return fieldElement;
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
return null;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
function renderExternalActions() {
|
|
997
|
-
if (!state.externalActions || !Array.isArray(state.externalActions)) return;
|
|
998
|
-
|
|
999
|
-
// Group actions by related_field (null for form-level actions)
|
|
1000
|
-
const actionsByField = new Map();
|
|
1001
|
-
const trueFormLevelActions = [];
|
|
1002
|
-
const movedFormLevelActions = [];
|
|
1003
|
-
|
|
1004
|
-
state.externalActions.forEach((action) => {
|
|
1005
|
-
if (!action.key || !action.value) return;
|
|
1006
|
-
|
|
1007
|
-
if (!action.related_field) {
|
|
1008
|
-
// True form-level action
|
|
1009
|
-
trueFormLevelActions.push(action);
|
|
1010
|
-
} else {
|
|
1011
|
-
// Field-level action
|
|
1012
|
-
if (!actionsByField.has(action.related_field)) {
|
|
1013
|
-
actionsByField.set(action.related_field, []);
|
|
1014
|
-
}
|
|
1015
|
-
actionsByField.get(action.related_field).push(action);
|
|
1016
|
-
}
|
|
1017
|
-
});
|
|
1018
|
-
|
|
1019
|
-
// Render field-level actions
|
|
1020
|
-
actionsByField.forEach((actions, fieldPath) => {
|
|
1021
|
-
// Find the form element for this related field
|
|
1022
|
-
const fieldElement = findFormElementByFieldPath(fieldPath);
|
|
1023
|
-
if (!fieldElement) {
|
|
1024
|
-
console.warn(
|
|
1025
|
-
`External action: Could not find form element for field "${fieldPath}", treating as form-level actions`,
|
|
1026
|
-
);
|
|
1027
|
-
// If field is not found, treat these actions as moved form-level actions
|
|
1028
|
-
movedFormLevelActions.push(...actions);
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
// Find the wrapper element that contains the field using stable class
|
|
1033
|
-
let wrapper = fieldElement.closest(".fb-field-wrapper");
|
|
1034
|
-
if (!wrapper) {
|
|
1035
|
-
wrapper = fieldElement.parentElement;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
if (!wrapper) {
|
|
1039
|
-
console.warn(
|
|
1040
|
-
`External action: Could not find wrapper for field "${fieldPath}"`,
|
|
1041
|
-
);
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
// Remove any existing actions container
|
|
1046
|
-
const existingContainer = wrapper.querySelector(".external-actions-container");
|
|
1047
|
-
if (existingContainer) {
|
|
1048
|
-
existingContainer.remove();
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// Create actions container
|
|
1052
|
-
const actionsContainer = document.createElement("div");
|
|
1053
|
-
actionsContainer.className =
|
|
1054
|
-
"external-actions-container mt-3 flex flex-wrap gap-2";
|
|
1055
|
-
|
|
1056
|
-
// Find the corresponding schema element for label lookup
|
|
1057
|
-
const schemaElement = findSchemaElement(fieldPath);
|
|
1058
|
-
|
|
1059
|
-
// Create action buttons
|
|
1060
|
-
actions.forEach((action) => {
|
|
1061
|
-
const actionBtn = document.createElement("button");
|
|
1062
|
-
actionBtn.type = "button";
|
|
1063
|
-
actionBtn.className =
|
|
1064
|
-
"bg-white text-gray-700 border border-gray-200 px-3 py-2 text-sm rounded-lg hover:bg-gray-50 hover:border-gray-300 transition-all duration-200 shadow-sm";
|
|
1065
|
-
|
|
1066
|
-
// Resolve action label with priority:
|
|
1067
|
-
// 1. Use explicit label from action if provided
|
|
1068
|
-
// 2. Try to find label from schema element labels using key
|
|
1069
|
-
// 3. Fall back to using key as label
|
|
1070
|
-
const resolvedLabel = resolveActionLabel(action.key, action.label, schemaElement);
|
|
1071
|
-
actionBtn.textContent = resolvedLabel;
|
|
1072
|
-
|
|
1073
|
-
actionBtn.addEventListener("click", (e) => {
|
|
1074
|
-
e.preventDefault();
|
|
1075
|
-
e.stopPropagation();
|
|
1076
|
-
|
|
1077
|
-
if (
|
|
1078
|
-
state.config.actionHandler &&
|
|
1079
|
-
typeof state.config.actionHandler === "function"
|
|
1080
|
-
) {
|
|
1081
|
-
// Call with value, key, and related_field for the new actions system
|
|
1082
|
-
state.config.actionHandler(action.value, action.key, action.related_field);
|
|
1083
|
-
}
|
|
1084
|
-
});
|
|
1085
|
-
|
|
1086
|
-
actionsContainer.appendChild(actionBtn);
|
|
1087
|
-
});
|
|
1088
|
-
|
|
1089
|
-
wrapper.appendChild(actionsContainer);
|
|
1090
|
-
});
|
|
1091
|
-
|
|
1092
|
-
// Render form-level actions at the bottom of the form
|
|
1093
|
-
const allFormLevelActions = [...trueFormLevelActions, ...movedFormLevelActions];
|
|
1094
|
-
if (allFormLevelActions.length > 0) {
|
|
1095
|
-
renderFormLevelActions(allFormLevelActions, trueFormLevelActions);
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
function renderFormLevelActions(actions, trueFormLevelActions = []) {
|
|
1100
|
-
if (!state.formRoot) return;
|
|
1101
|
-
|
|
1102
|
-
// Remove any existing form-level actions container
|
|
1103
|
-
const existingContainer = state.formRoot.querySelector(".form-level-actions-container");
|
|
1104
|
-
if (existingContainer) {
|
|
1105
|
-
existingContainer.remove();
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
// Create form-level actions container
|
|
1109
|
-
const actionsContainer = document.createElement("div");
|
|
1110
|
-
actionsContainer.className =
|
|
1111
|
-
"form-level-actions-container mt-6 pt-4 border-t border-gray-200 flex flex-wrap gap-3 justify-center";
|
|
1112
|
-
|
|
1113
|
-
// Create action buttons
|
|
1114
|
-
actions.forEach((action) => {
|
|
1115
|
-
const actionBtn = document.createElement("button");
|
|
1116
|
-
actionBtn.type = "button";
|
|
1117
|
-
actionBtn.className =
|
|
1118
|
-
"bg-white text-gray-700 border border-gray-200 px-4 py-2 text-sm font-medium rounded-lg hover:bg-gray-50 hover:border-gray-300 transition-all duration-200 shadow-sm";
|
|
1119
|
-
|
|
1120
|
-
// Check if this is a true form-level action (no related_field originally)
|
|
1121
|
-
const isTrueFormLevelAction = trueFormLevelActions.includes(action);
|
|
1122
|
-
|
|
1123
|
-
// Resolve action label with priority:
|
|
1124
|
-
// 1. Use explicit label from action if provided
|
|
1125
|
-
// 2. Try to find label from schema element labels using key (only for true form-level actions)
|
|
1126
|
-
// 3. Fall back to using key as label
|
|
1127
|
-
const resolvedLabel = resolveActionLabel(action.key, action.label, null, isTrueFormLevelAction);
|
|
1128
|
-
actionBtn.textContent = resolvedLabel;
|
|
1129
|
-
|
|
1130
|
-
actionBtn.addEventListener("click", (e) => {
|
|
1131
|
-
e.preventDefault();
|
|
1132
|
-
e.stopPropagation();
|
|
1133
|
-
|
|
1134
|
-
if (
|
|
1135
|
-
state.config.actionHandler &&
|
|
1136
|
-
typeof state.config.actionHandler === "function"
|
|
1137
|
-
) {
|
|
1138
|
-
// Call with value, key, and null related_field for form-level actions
|
|
1139
|
-
state.config.actionHandler(action.value, action.key, null);
|
|
1140
|
-
}
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
actionsContainer.appendChild(actionBtn);
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
// Append to form root
|
|
1147
|
-
state.formRoot.appendChild(actionsContainer);
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
// Helper function to resolve action label
|
|
1151
|
-
function resolveActionLabel(actionKey, externalLabel, schemaElement, isTrueFormLevelAction = false) {
|
|
1152
|
-
// 1. Try to find label from predefined actions in schema element using key (highest priority)
|
|
1153
|
-
if (schemaElement && schemaElement.actions && Array.isArray(schemaElement.actions)) {
|
|
1154
|
-
const predefinedAction = schemaElement.actions.find(a => a.key === actionKey);
|
|
1155
|
-
if (predefinedAction && predefinedAction.label) {
|
|
1156
|
-
return predefinedAction.label;
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
// 2. Try to find label from root-level schema actions (only for true form-level actions)
|
|
1161
|
-
if (isTrueFormLevelAction && state.schema && state.schema.actions && Array.isArray(state.schema.actions)) {
|
|
1162
|
-
const rootAction = state.schema.actions.find(a => a.key === actionKey);
|
|
1163
|
-
if (rootAction && rootAction.label) {
|
|
1164
|
-
return rootAction.label;
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// 3. Use explicit label from external action if provided
|
|
1169
|
-
if (externalLabel) {
|
|
1170
|
-
return externalLabel;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
// 4. Fall back to using key as label
|
|
1174
|
-
return actionKey;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
// Helper function to find schema element by field path
|
|
1178
|
-
function findSchemaElement(fieldPath) {
|
|
1179
|
-
if (!state.schema || !state.schema.elements) return null;
|
|
1180
|
-
|
|
1181
|
-
let currentElements = state.schema.elements;
|
|
1182
|
-
let foundElement = null;
|
|
1183
|
-
|
|
1184
|
-
// Handle paths like 'a.b' or 'a[0].b' by looking for keys in sequence
|
|
1185
|
-
const keys = fieldPath.replace(/\[\d+\]/g, '').split('.').filter(Boolean);
|
|
1186
|
-
|
|
1187
|
-
for (const key of keys) {
|
|
1188
|
-
foundElement = currentElements.find(el => el.key === key);
|
|
1189
|
-
if (!foundElement) {
|
|
1190
|
-
return null; // Key not found at this level
|
|
1191
|
-
}
|
|
1192
|
-
if (foundElement.elements) {
|
|
1193
|
-
currentElements = foundElement.elements;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
return foundElement;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
function showTooltip(tooltipId, button) {
|
|
1201
|
-
const tooltip = document.getElementById(tooltipId);
|
|
1202
|
-
const isCurrentlyVisible = !tooltip.classList.contains("hidden");
|
|
1203
|
-
|
|
1204
|
-
// Hide all tooltips first
|
|
1205
|
-
document.querySelectorAll('[id^="tooltip-"]').forEach((t) => {
|
|
1206
|
-
t.classList.add("hidden");
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1209
|
-
// If the tooltip was already visible, keep it hidden (toggle behavior)
|
|
1210
|
-
if (isCurrentlyVisible) {
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
// Position the tooltip intelligently
|
|
1215
|
-
const rect = button.getBoundingClientRect();
|
|
1216
|
-
const viewportWidth = window.innerWidth;
|
|
1217
|
-
const viewportHeight = window.innerHeight;
|
|
1218
|
-
|
|
1219
|
-
// Ensure tooltip is appended to body for proper positioning
|
|
1220
|
-
if (tooltip && tooltip.parentElement !== document.body) {
|
|
1221
|
-
document.body.appendChild(tooltip);
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
// Show tooltip temporarily to measure its size
|
|
1225
|
-
tooltip.style.visibility = "hidden";
|
|
1226
|
-
tooltip.style.position = "fixed";
|
|
1227
|
-
tooltip.classList.remove("hidden");
|
|
1228
|
-
const tooltipRect = tooltip.getBoundingClientRect();
|
|
1229
|
-
tooltip.classList.add("hidden");
|
|
1230
|
-
tooltip.style.visibility = "visible";
|
|
1231
|
-
|
|
1232
|
-
let left = rect.left;
|
|
1233
|
-
let top = rect.bottom + 5;
|
|
1234
|
-
|
|
1235
|
-
// Adjust horizontal position if tooltip would go off-screen
|
|
1236
|
-
if (left + tooltipRect.width > viewportWidth) {
|
|
1237
|
-
left = rect.right - tooltipRect.width;
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
// Adjust vertical position if tooltip would go off-screen
|
|
1241
|
-
if (top + tooltipRect.height > viewportHeight) {
|
|
1242
|
-
top = rect.top - tooltipRect.height - 5;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// Ensure tooltip doesn't go off the left edge
|
|
1246
|
-
if (left < 10) {
|
|
1247
|
-
left = 10;
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
// Ensure tooltip doesn't go off the top edge
|
|
1251
|
-
if (top < 10) {
|
|
1252
|
-
top = rect.bottom + 5;
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
tooltip.style.left = `${left}px`;
|
|
1256
|
-
tooltip.style.top = `${top}px`;
|
|
1257
|
-
|
|
1258
|
-
// Show the tooltip
|
|
1259
|
-
tooltip.classList.remove("hidden");
|
|
1260
|
-
|
|
1261
|
-
// Hide after 25 seconds
|
|
1262
|
-
setTimeout(() => {
|
|
1263
|
-
tooltip.classList.add("hidden");
|
|
1264
|
-
}, 25000);
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
// Close tooltips when clicking outside (only in browser)
|
|
1268
|
-
if (typeof document !== "undefined") {
|
|
1269
|
-
document.addEventListener("click", (e) => {
|
|
1270
|
-
const isInfoButton =
|
|
1271
|
-
e.target.closest("button") && e.target.closest("button").onclick;
|
|
1272
|
-
const isTooltip = e.target.closest('[id^="tooltip-"]');
|
|
1273
|
-
|
|
1274
|
-
if (!isInfoButton && !isTooltip) {
|
|
1275
|
-
document.querySelectorAll('[id^="tooltip-"]').forEach((tooltip) => {
|
|
1276
|
-
tooltip.classList.add("hidden");
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
});
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
// Form validation and data extraction
|
|
1283
|
-
function validateForm(skipValidation = false) {
|
|
1284
|
-
if (!state.schema || !state.formRoot) return { valid: true, data: {} };
|
|
1285
|
-
|
|
1286
|
-
const errors = [];
|
|
1287
|
-
const data = {};
|
|
1288
|
-
|
|
1289
|
-
function markValidity(input, errorMessage) {
|
|
1290
|
-
if (!input) return;
|
|
1291
|
-
if (errorMessage) {
|
|
1292
|
-
input.classList.add("invalid");
|
|
1293
|
-
input.title = errorMessage;
|
|
1294
|
-
} else {
|
|
1295
|
-
input.classList.remove("invalid");
|
|
1296
|
-
input.title = "";
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
function validateElement(element, ctx, customScopeRoot = null) {
|
|
1301
|
-
const key = element.key;
|
|
1302
|
-
const scopeRoot = customScopeRoot || state.formRoot;
|
|
1303
|
-
|
|
1304
|
-
switch (element.type) {
|
|
1305
|
-
case "text":
|
|
1306
|
-
case "textarea": {
|
|
1307
|
-
if (element.multiple) {
|
|
1308
|
-
// Handle multiple text/textarea fields
|
|
1309
|
-
const inputs = scopeRoot.querySelectorAll(`[name^="${key}["]`);
|
|
1310
|
-
const values = [];
|
|
1311
|
-
|
|
1312
|
-
inputs.forEach((input, index) => {
|
|
1313
|
-
const val = input?.value ?? "";
|
|
1314
|
-
values.push(val);
|
|
1315
|
-
|
|
1316
|
-
if (!skipValidation && val) {
|
|
1317
|
-
if (
|
|
1318
|
-
element.minLength !== null &&
|
|
1319
|
-
val.length < element.minLength
|
|
1320
|
-
) {
|
|
1321
|
-
errors.push(`${key}[${index}]: minLength=${element.minLength}`);
|
|
1322
|
-
markValidity(input, `minLength=${element.minLength}`);
|
|
1323
|
-
}
|
|
1324
|
-
if (
|
|
1325
|
-
element.maxLength !== null &&
|
|
1326
|
-
val.length > element.maxLength
|
|
1327
|
-
) {
|
|
1328
|
-
errors.push(`${key}[${index}]: maxLength=${element.maxLength}`);
|
|
1329
|
-
markValidity(input, `maxLength=${element.maxLength}`);
|
|
1330
|
-
}
|
|
1331
|
-
if (element.pattern) {
|
|
1332
|
-
try {
|
|
1333
|
-
const re = new RegExp(element.pattern);
|
|
1334
|
-
if (!re.test(val)) {
|
|
1335
|
-
errors.push(`${key}[${index}]: pattern mismatch`);
|
|
1336
|
-
markValidity(input, "pattern mismatch");
|
|
1337
|
-
}
|
|
1338
|
-
} catch {
|
|
1339
|
-
errors.push(`${key}[${index}]: invalid pattern`);
|
|
1340
|
-
markValidity(input, "invalid pattern");
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
} else {
|
|
1344
|
-
markValidity(input, null);
|
|
1345
|
-
}
|
|
1346
|
-
});
|
|
1347
|
-
|
|
1348
|
-
// Validate minCount/maxCount constraints
|
|
1349
|
-
if (!skipValidation) {
|
|
1350
|
-
const minCount = element.minCount ?? 1;
|
|
1351
|
-
const maxCount = element.maxCount ?? 10;
|
|
1352
|
-
const nonEmptyValues = values.filter((v) => v.trim() !== "");
|
|
1353
|
-
|
|
1354
|
-
if (element.required && nonEmptyValues.length === 0) {
|
|
1355
|
-
errors.push(`${key}: required`);
|
|
1356
|
-
}
|
|
1357
|
-
if (nonEmptyValues.length < minCount) {
|
|
1358
|
-
errors.push(`${key}: minimum ${minCount} items required`);
|
|
1359
|
-
}
|
|
1360
|
-
if (nonEmptyValues.length > maxCount) {
|
|
1361
|
-
errors.push(`${key}: maximum ${maxCount} items allowed`);
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
return values;
|
|
1366
|
-
} else {
|
|
1367
|
-
// Handle single text/textarea field
|
|
1368
|
-
const input = scopeRoot.querySelector(`[name$="${key}"]`);
|
|
1369
|
-
const val = input?.value ?? "";
|
|
1370
|
-
if (!skipValidation && element.required && val === "") {
|
|
1371
|
-
errors.push(`${key}: required`);
|
|
1372
|
-
markValidity(input, "required");
|
|
1373
|
-
return "";
|
|
1374
|
-
}
|
|
1375
|
-
if (!skipValidation && val) {
|
|
1376
|
-
if (element.minLength !== null && val.length < element.minLength) {
|
|
1377
|
-
errors.push(`${key}: minLength=${element.minLength}`);
|
|
1378
|
-
markValidity(input, `minLength=${element.minLength}`);
|
|
1379
|
-
}
|
|
1380
|
-
if (element.maxLength !== null && val.length > element.maxLength) {
|
|
1381
|
-
errors.push(`${key}: maxLength=${element.maxLength}`);
|
|
1382
|
-
markValidity(input, `maxLength=${element.maxLength}`);
|
|
1383
|
-
}
|
|
1384
|
-
if (element.pattern) {
|
|
1385
|
-
try {
|
|
1386
|
-
const re = new RegExp(element.pattern);
|
|
1387
|
-
if (!re.test(val)) {
|
|
1388
|
-
errors.push(`${key}: pattern mismatch`);
|
|
1389
|
-
markValidity(input, "pattern mismatch");
|
|
1390
|
-
}
|
|
1391
|
-
} catch {
|
|
1392
|
-
errors.push(`${key}: invalid pattern`);
|
|
1393
|
-
markValidity(input, "invalid pattern");
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
} else if (skipValidation) {
|
|
1397
|
-
markValidity(input, null);
|
|
1398
|
-
} else {
|
|
1399
|
-
markValidity(input, null);
|
|
1400
|
-
}
|
|
1401
|
-
return val;
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
case "number": {
|
|
1405
|
-
if (element.multiple) {
|
|
1406
|
-
// Handle multiple number fields
|
|
1407
|
-
const inputs = scopeRoot.querySelectorAll(`[name^="${key}["]`);
|
|
1408
|
-
const values = [];
|
|
1409
|
-
|
|
1410
|
-
inputs.forEach((input, index) => {
|
|
1411
|
-
const raw = input?.value ?? "";
|
|
1412
|
-
if (raw === "") {
|
|
1413
|
-
values.push(null);
|
|
1414
|
-
markValidity(input, null);
|
|
1415
|
-
return;
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
const v = parseFloat(raw);
|
|
1419
|
-
if (!skipValidation && !Number.isFinite(v)) {
|
|
1420
|
-
errors.push(`${key}[${index}]: not a number`);
|
|
1421
|
-
markValidity(input, "not a number");
|
|
1422
|
-
values.push(null);
|
|
1423
|
-
return;
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
if (!skipValidation && element.min !== null && v < element.min) {
|
|
1427
|
-
errors.push(`${key}[${index}]: < min=${element.min}`);
|
|
1428
|
-
markValidity(input, `< min=${element.min}`);
|
|
1429
|
-
}
|
|
1430
|
-
if (!skipValidation && element.max !== null && v > element.max) {
|
|
1431
|
-
errors.push(`${key}[${index}]: > max=${element.max}`);
|
|
1432
|
-
markValidity(input, `> max=${element.max}`);
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
const d = Number.isInteger(element.decimals ?? 0)
|
|
1436
|
-
? element.decimals
|
|
1437
|
-
: 0;
|
|
1438
|
-
markValidity(input, null);
|
|
1439
|
-
values.push(Number(v.toFixed(d)));
|
|
1440
|
-
});
|
|
1441
|
-
|
|
1442
|
-
// Validate minCount/maxCount constraints
|
|
1443
|
-
if (!skipValidation) {
|
|
1444
|
-
const minCount = element.minCount ?? 1;
|
|
1445
|
-
const maxCount = element.maxCount ?? 10;
|
|
1446
|
-
const nonNullValues = values.filter((v) => v !== null);
|
|
1447
|
-
|
|
1448
|
-
if (element.required && nonNullValues.length === 0) {
|
|
1449
|
-
errors.push(`${key}: required`);
|
|
1450
|
-
}
|
|
1451
|
-
if (nonNullValues.length < minCount) {
|
|
1452
|
-
errors.push(`${key}: minimum ${minCount} items required`);
|
|
1453
|
-
}
|
|
1454
|
-
if (nonNullValues.length > maxCount) {
|
|
1455
|
-
errors.push(`${key}: maximum ${maxCount} items allowed`);
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
return values;
|
|
1460
|
-
} else {
|
|
1461
|
-
// Handle single number field
|
|
1462
|
-
const input = scopeRoot.querySelector(`[name$="${key}"]`);
|
|
1463
|
-
const raw = input?.value ?? "";
|
|
1464
|
-
if (!skipValidation && element.required && raw === "") {
|
|
1465
|
-
errors.push(`${key}: required`);
|
|
1466
|
-
markValidity(input, "required");
|
|
1467
|
-
return null;
|
|
1468
|
-
}
|
|
1469
|
-
if (raw === "") {
|
|
1470
|
-
markValidity(input, null);
|
|
1471
|
-
return null;
|
|
1472
|
-
}
|
|
1473
|
-
const v = parseFloat(raw);
|
|
1474
|
-
if (!skipValidation && !Number.isFinite(v)) {
|
|
1475
|
-
errors.push(`${key}: not a number`);
|
|
1476
|
-
markValidity(input, "not a number");
|
|
1477
|
-
return null;
|
|
1478
|
-
}
|
|
1479
|
-
if (!skipValidation && element.min !== null && v < element.min) {
|
|
1480
|
-
errors.push(`${key}: < min=${element.min}`);
|
|
1481
|
-
markValidity(input, `< min=${element.min}`);
|
|
1482
|
-
}
|
|
1483
|
-
if (!skipValidation && element.max !== null && v > element.max) {
|
|
1484
|
-
errors.push(`${key}: > max=${element.max}`);
|
|
1485
|
-
markValidity(input, `> max=${element.max}`);
|
|
1486
|
-
}
|
|
1487
|
-
const d = Number.isInteger(element.decimals ?? 0)
|
|
1488
|
-
? element.decimals
|
|
1489
|
-
: 0;
|
|
1490
|
-
markValidity(input, null);
|
|
1491
|
-
return Number(v.toFixed(d));
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
case "select": {
|
|
1495
|
-
if (element.multiple) {
|
|
1496
|
-
// Handle multiple select fields
|
|
1497
|
-
const inputs = scopeRoot.querySelectorAll(`[name^="${key}["]`);
|
|
1498
|
-
const values = [];
|
|
1499
|
-
|
|
1500
|
-
inputs.forEach((input) => {
|
|
1501
|
-
const val = input?.value ?? "";
|
|
1502
|
-
values.push(val);
|
|
1503
|
-
markValidity(input, null);
|
|
1504
|
-
});
|
|
1505
|
-
|
|
1506
|
-
// Validate minCount/maxCount constraints
|
|
1507
|
-
if (!skipValidation) {
|
|
1508
|
-
const minCount = element.minCount ?? 1;
|
|
1509
|
-
const maxCount = element.maxCount ?? 10;
|
|
1510
|
-
const nonEmptyValues = values.filter((v) => v !== "");
|
|
1511
|
-
|
|
1512
|
-
if (element.required && nonEmptyValues.length === 0) {
|
|
1513
|
-
errors.push(`${key}: required`);
|
|
1514
|
-
}
|
|
1515
|
-
if (nonEmptyValues.length < minCount) {
|
|
1516
|
-
errors.push(`${key}: minimum ${minCount} items required`);
|
|
1517
|
-
}
|
|
1518
|
-
if (nonEmptyValues.length > maxCount) {
|
|
1519
|
-
errors.push(`${key}: maximum ${maxCount} items allowed`);
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
return values;
|
|
1524
|
-
} else {
|
|
1525
|
-
// Handle single select field
|
|
1526
|
-
const input = scopeRoot.querySelector(`[name$="${key}"]`);
|
|
1527
|
-
const val = input?.value ?? "";
|
|
1528
|
-
if (!skipValidation && element.required && val === "") {
|
|
1529
|
-
errors.push(`${key}: required`);
|
|
1530
|
-
markValidity(input, "required");
|
|
1531
|
-
return "";
|
|
1532
|
-
}
|
|
1533
|
-
markValidity(input, null);
|
|
1534
|
-
return val;
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
case "file": {
|
|
1538
|
-
if (element.multiple) {
|
|
1539
|
-
// Handle file with multiple property like files type
|
|
1540
|
-
// Find the files list by locating the specific file input for this field
|
|
1541
|
-
const fullKey = pathJoin(ctx.path, key);
|
|
1542
|
-
const pickerInput = scopeRoot.querySelector(
|
|
1543
|
-
`input[type="file"][name="${fullKey}"]`,
|
|
1544
|
-
);
|
|
1545
|
-
const filesWrapper = pickerInput?.closest(".space-y-2");
|
|
1546
|
-
const container = filesWrapper?.querySelector(".files-list") || null;
|
|
1547
|
-
|
|
1548
|
-
const resourceIds = [];
|
|
1549
|
-
if (container) {
|
|
1550
|
-
const pills = container.querySelectorAll(".resource-pill");
|
|
1551
|
-
pills.forEach((pill) => {
|
|
1552
|
-
const resourceId = pill.dataset.resourceId;
|
|
1553
|
-
if (resourceId) {
|
|
1554
|
-
resourceIds.push(resourceId);
|
|
1555
|
-
}
|
|
1556
|
-
});
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
// Validate minCount/maxCount constraints
|
|
1560
|
-
if (!skipValidation) {
|
|
1561
|
-
const minFiles = element.minCount ?? 0;
|
|
1562
|
-
const maxFiles = element.maxCount ?? Infinity;
|
|
1563
|
-
|
|
1564
|
-
if (element.required && resourceIds.length === 0) {
|
|
1565
|
-
errors.push(`${key}: required`);
|
|
1566
|
-
}
|
|
1567
|
-
if (resourceIds.length < minFiles) {
|
|
1568
|
-
errors.push(`${key}: minimum ${minFiles} files required`);
|
|
1569
|
-
}
|
|
1570
|
-
if (resourceIds.length > maxFiles) {
|
|
1571
|
-
errors.push(`${key}: maximum ${maxFiles} files allowed`);
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
return resourceIds;
|
|
1576
|
-
} else {
|
|
1577
|
-
// Handle single file
|
|
1578
|
-
const input = scopeRoot.querySelector(
|
|
1579
|
-
`input[name$="${key}"][type="hidden"]`,
|
|
1580
|
-
);
|
|
1581
|
-
const rid = input?.value ?? "";
|
|
1582
|
-
if (!skipValidation && element.required && rid === "") {
|
|
1583
|
-
errors.push(`${key}: required`);
|
|
1584
|
-
return null;
|
|
1585
|
-
}
|
|
1586
|
-
return rid || null;
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
case "files": {
|
|
1590
|
-
// For files, we need to collect all resource IDs
|
|
1591
|
-
// Find the correct .files-list by looking for one that's associated with this field
|
|
1592
|
-
let container = null;
|
|
1593
|
-
|
|
1594
|
-
// Strategy 1: Try to find via the input element
|
|
1595
|
-
const nameElement = scopeRoot.querySelector(`[name="${key}"]`);
|
|
1596
|
-
if (nameElement) {
|
|
1597
|
-
// Look for .files-list in the input's parent container
|
|
1598
|
-
container = nameElement.parentElement?.querySelector(".files-list");
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
// Strategy 2: If we have multiple .files-list elements, this gets tricky
|
|
1602
|
-
// For now, let's use a simpler approach for the demo: just find the first .files-list
|
|
1603
|
-
// that has resource pills (since our demo only has one files field)
|
|
1604
|
-
if (!container) {
|
|
1605
|
-
const allFilesLists = scopeRoot.querySelectorAll(".files-list");
|
|
1606
|
-
for (const filesList of allFilesLists) {
|
|
1607
|
-
const pillCount =
|
|
1608
|
-
filesList.querySelectorAll(".resource-pill").length;
|
|
1609
|
-
if (pillCount > 0) {
|
|
1610
|
-
container = filesList;
|
|
1611
|
-
break;
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
const rids = [];
|
|
1617
|
-
if (container) {
|
|
1618
|
-
const resourcePills = container.querySelectorAll(".resource-pill");
|
|
1619
|
-
resourcePills.forEach((pill, _index) => {
|
|
1620
|
-
const resourceId = pill.dataset.resourceId;
|
|
1621
|
-
if (resourceId) {
|
|
1622
|
-
rids.push(resourceId);
|
|
1623
|
-
}
|
|
1624
|
-
});
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
return rids;
|
|
1628
|
-
}
|
|
1629
|
-
case "group": {
|
|
1630
|
-
if (element.repeat && isPlainObject(element.repeat)) {
|
|
1631
|
-
const items = [];
|
|
1632
|
-
// Use full path for nested group element search
|
|
1633
|
-
const fullKey = pathJoin(ctx.path, key);
|
|
1634
|
-
const itemElements = scopeRoot.querySelectorAll(
|
|
1635
|
-
`[name^="${fullKey}["]`,
|
|
1636
|
-
);
|
|
1637
|
-
|
|
1638
|
-
// Extract actual indices from DOM element names instead of assuming sequential numbering
|
|
1639
|
-
const actualIndices = new Set();
|
|
1640
|
-
itemElements.forEach((el) => {
|
|
1641
|
-
const match = el.name.match(
|
|
1642
|
-
new RegExp(
|
|
1643
|
-
`^${fullKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\[(\\d+)\\]`,
|
|
1644
|
-
),
|
|
1645
|
-
);
|
|
1646
|
-
if (match) {
|
|
1647
|
-
actualIndices.add(parseInt(match[1]));
|
|
1648
|
-
}
|
|
1649
|
-
});
|
|
1650
|
-
|
|
1651
|
-
const sortedIndices = Array.from(actualIndices).sort((a, b) => a - b);
|
|
1652
|
-
|
|
1653
|
-
sortedIndices.forEach((actualIndex) => {
|
|
1654
|
-
const itemData = {};
|
|
1655
|
-
// Find the specific group item container for scoped queries - use full path
|
|
1656
|
-
const fullItemPath = `${fullKey}[${actualIndex}]`;
|
|
1657
|
-
const itemContainer =
|
|
1658
|
-
scopeRoot.querySelector(`[data-group-item="${fullItemPath}"]`) ||
|
|
1659
|
-
scopeRoot;
|
|
1660
|
-
element.elements.forEach((child) => {
|
|
1661
|
-
if (child.hidden) {
|
|
1662
|
-
// For hidden child elements, use their default value
|
|
1663
|
-
itemData[child.key] =
|
|
1664
|
-
child.default !== undefined ? child.default : null;
|
|
1665
|
-
} else {
|
|
1666
|
-
const childKey = `${fullKey}[${actualIndex}].${child.key}`;
|
|
1667
|
-
itemData[child.key] = validateElement(
|
|
1668
|
-
{ ...child, key: childKey },
|
|
1669
|
-
ctx,
|
|
1670
|
-
itemContainer,
|
|
1671
|
-
);
|
|
1672
|
-
}
|
|
1673
|
-
});
|
|
1674
|
-
items.push(itemData);
|
|
1675
|
-
});
|
|
1676
|
-
return items;
|
|
1677
|
-
} else {
|
|
1678
|
-
const groupData = {};
|
|
1679
|
-
// Find the specific group container for scoped queries
|
|
1680
|
-
const groupContainer =
|
|
1681
|
-
scopeRoot.querySelector(`[data-group="${key}"]`) || scopeRoot;
|
|
1682
|
-
element.elements.forEach((child) => {
|
|
1683
|
-
if (child.hidden) {
|
|
1684
|
-
// For hidden child elements, use their default value
|
|
1685
|
-
groupData[child.key] =
|
|
1686
|
-
child.default !== undefined ? child.default : null;
|
|
1687
|
-
} else {
|
|
1688
|
-
const childKey = `${key}.${child.key}`;
|
|
1689
|
-
groupData[child.key] = validateElement(
|
|
1690
|
-
{ ...child, key: childKey },
|
|
1691
|
-
ctx,
|
|
1692
|
-
groupContainer,
|
|
1693
|
-
);
|
|
1694
|
-
}
|
|
1695
|
-
});
|
|
1696
|
-
return groupData;
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
case "container": {
|
|
1700
|
-
if (element.multiple) {
|
|
1701
|
-
// Handle multiple containers like repeating groups
|
|
1702
|
-
const items = [];
|
|
1703
|
-
const itemElements = scopeRoot.querySelectorAll(`[name^="${key}["]`);
|
|
1704
|
-
const itemCount = Math.max(
|
|
1705
|
-
0,
|
|
1706
|
-
Math.floor(itemElements.length / element.elements.length),
|
|
1707
|
-
);
|
|
1708
|
-
|
|
1709
|
-
for (let i = 0; i < itemCount; i++) {
|
|
1710
|
-
const itemData = {};
|
|
1711
|
-
// Find the specific container item container for scoped queries
|
|
1712
|
-
const itemContainer =
|
|
1713
|
-
scopeRoot.querySelector(`[data-container-item="${key}[${i}]"]`) ||
|
|
1714
|
-
scopeRoot;
|
|
1715
|
-
element.elements.forEach((child) => {
|
|
1716
|
-
if (child.hidden) {
|
|
1717
|
-
// For hidden child elements, use their default value
|
|
1718
|
-
itemData[child.key] =
|
|
1719
|
-
child.default !== undefined ? child.default : null;
|
|
1720
|
-
} else {
|
|
1721
|
-
const childKey = `${key}[${i}].${child.key}`;
|
|
1722
|
-
itemData[child.key] = validateElement(
|
|
1723
|
-
{ ...child, key: childKey },
|
|
1724
|
-
ctx,
|
|
1725
|
-
itemContainer,
|
|
1726
|
-
);
|
|
1727
|
-
}
|
|
1728
|
-
});
|
|
1729
|
-
items.push(itemData);
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
// Validate minCount/maxCount constraints
|
|
1733
|
-
if (!skipValidation) {
|
|
1734
|
-
const minItems = element.minCount ?? 0;
|
|
1735
|
-
const maxItems = element.maxCount ?? Infinity;
|
|
1736
|
-
|
|
1737
|
-
if (element.required && items.length === 0) {
|
|
1738
|
-
errors.push(`${key}: required`);
|
|
1739
|
-
}
|
|
1740
|
-
if (items.length < minItems) {
|
|
1741
|
-
errors.push(`${key}: minimum ${minItems} items required`);
|
|
1742
|
-
}
|
|
1743
|
-
if (items.length > maxItems) {
|
|
1744
|
-
errors.push(`${key}: maximum ${maxItems} items allowed`);
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
return items;
|
|
1749
|
-
} else {
|
|
1750
|
-
const containerData = {};
|
|
1751
|
-
// Find the specific container container for scoped queries
|
|
1752
|
-
const containerContainer =
|
|
1753
|
-
scopeRoot.querySelector(`[data-container="${key}"]`) || scopeRoot;
|
|
1754
|
-
element.elements.forEach((child) => {
|
|
1755
|
-
if (child.hidden) {
|
|
1756
|
-
// For hidden child elements, use their default value
|
|
1757
|
-
containerData[child.key] =
|
|
1758
|
-
child.default !== undefined ? child.default : null;
|
|
1759
|
-
} else {
|
|
1760
|
-
const childKey = `${key}.${child.key}`;
|
|
1761
|
-
containerData[child.key] = validateElement(
|
|
1762
|
-
{ ...child, key: childKey },
|
|
1763
|
-
ctx,
|
|
1764
|
-
containerContainer,
|
|
1765
|
-
);
|
|
1766
|
-
}
|
|
1767
|
-
});
|
|
1768
|
-
return containerData;
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
default:
|
|
1772
|
-
return null;
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
state.schema.elements.forEach((element) => {
|
|
1777
|
-
// Handle hidden elements - use their default value instead of reading from DOM
|
|
1778
|
-
if (element.hidden) {
|
|
1779
|
-
// Use null as default for consistency with empty field behavior
|
|
1780
|
-
// File fields, number fields, and other non-text fields return null when empty
|
|
1781
|
-
data[element.key] = element.default !== undefined ? element.default : null;
|
|
1782
|
-
} else {
|
|
1783
|
-
data[element.key] = validateElement(element, { path: "" });
|
|
1784
|
-
}
|
|
1785
|
-
});
|
|
1786
|
-
|
|
1787
|
-
return {
|
|
1788
|
-
valid: errors.length === 0,
|
|
1789
|
-
errors,
|
|
1790
|
-
data,
|
|
1791
|
-
};
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
// Element rendering functions
|
|
1795
|
-
function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
1796
|
-
const textInput = document.createElement("input");
|
|
1797
|
-
textInput.type = "text";
|
|
1798
|
-
textInput.className =
|
|
1799
|
-
"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1800
|
-
textInput.name = pathKey;
|
|
1801
|
-
textInput.placeholder = element.placeholder || "Введите текст";
|
|
1802
|
-
textInput.value = ctx.prefill[element.key] || element.default || "";
|
|
1803
|
-
textInput.readOnly = state.config.readonly;
|
|
1804
|
-
wrapper.appendChild(textInput);
|
|
1805
|
-
|
|
1806
|
-
// Add hint
|
|
1807
|
-
const textHint = document.createElement("p");
|
|
1808
|
-
textHint.className = "text-xs text-gray-500 mt-1";
|
|
1809
|
-
textHint.textContent = makeFieldHint(element);
|
|
1810
|
-
wrapper.appendChild(textHint);
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
1814
|
-
const prefillValues = ctx.prefill[element.key] || [];
|
|
1815
|
-
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
1816
|
-
|
|
1817
|
-
// Ensure minimum count
|
|
1818
|
-
const minCount = element.minCount ?? 1;
|
|
1819
|
-
const maxCount = element.maxCount ?? 10;
|
|
1820
|
-
|
|
1821
|
-
while (values.length < minCount) {
|
|
1822
|
-
values.push(element.default || "");
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
const container = document.createElement("div");
|
|
1826
|
-
container.className = "space-y-2";
|
|
1827
|
-
wrapper.appendChild(container);
|
|
1828
|
-
|
|
1829
|
-
function updateIndices() {
|
|
1830
|
-
const items = container.querySelectorAll(".multiple-text-item");
|
|
1831
|
-
items.forEach((item, index) => {
|
|
1832
|
-
const input = item.querySelector("input");
|
|
1833
|
-
if (input) {
|
|
1834
|
-
input.name = `${pathKey}[${index}]`;
|
|
1835
|
-
}
|
|
1836
|
-
});
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
function addTextItem(value = "", index = -1) {
|
|
1840
|
-
const itemWrapper = document.createElement("div");
|
|
1841
|
-
itemWrapper.className = "multiple-text-item flex items-center gap-2";
|
|
1842
|
-
|
|
1843
|
-
const textInput = document.createElement("input");
|
|
1844
|
-
textInput.type = "text";
|
|
1845
|
-
textInput.className =
|
|
1846
|
-
"flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1847
|
-
textInput.placeholder = element.placeholder || "Enter text";
|
|
1848
|
-
textInput.value = value;
|
|
1849
|
-
textInput.readOnly = state.config.readonly;
|
|
1850
|
-
|
|
1851
|
-
itemWrapper.appendChild(textInput);
|
|
1852
|
-
|
|
1853
|
-
// Remove buttons are managed centrally via updateRemoveButtons()
|
|
1854
|
-
|
|
1855
|
-
if (index === -1) {
|
|
1856
|
-
container.appendChild(itemWrapper);
|
|
1857
|
-
} else {
|
|
1858
|
-
container.insertBefore(itemWrapper, container.children[index]);
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
updateIndices();
|
|
1862
|
-
return itemWrapper;
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
function updateRemoveButtons() {
|
|
1866
|
-
if (state.config.readonly) return;
|
|
1867
|
-
const items = container.querySelectorAll(".multiple-text-item");
|
|
1868
|
-
const currentCount = items.length;
|
|
1869
|
-
items.forEach((item) => {
|
|
1870
|
-
let removeBtn = item.querySelector(".remove-item-btn");
|
|
1871
|
-
if (!removeBtn) {
|
|
1872
|
-
removeBtn = document.createElement("button");
|
|
1873
|
-
removeBtn.type = "button";
|
|
1874
|
-
removeBtn.className =
|
|
1875
|
-
"remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
|
|
1876
|
-
removeBtn.innerHTML = "✕";
|
|
1877
|
-
removeBtn.onclick = () => {
|
|
1878
|
-
const currentIndex = Array.from(container.children).indexOf(item);
|
|
1879
|
-
if (container.children.length > minCount) {
|
|
1880
|
-
values.splice(currentIndex, 1);
|
|
1881
|
-
item.remove();
|
|
1882
|
-
updateIndices();
|
|
1883
|
-
updateAddButton();
|
|
1884
|
-
updateRemoveButtons();
|
|
1885
|
-
}
|
|
1886
|
-
};
|
|
1887
|
-
item.appendChild(removeBtn);
|
|
1888
|
-
}
|
|
1889
|
-
const disabled = currentCount <= minCount;
|
|
1890
|
-
removeBtn.disabled = disabled;
|
|
1891
|
-
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
1892
|
-
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1893
|
-
});
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
function updateAddButton() {
|
|
1897
|
-
const existingAddBtn = wrapper.querySelector(".add-text-btn");
|
|
1898
|
-
if (existingAddBtn) existingAddBtn.remove();
|
|
1899
|
-
|
|
1900
|
-
if (!state.config.readonly && values.length < maxCount) {
|
|
1901
|
-
const addBtn = document.createElement("button");
|
|
1902
|
-
addBtn.type = "button";
|
|
1903
|
-
addBtn.className =
|
|
1904
|
-
"add-text-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
|
|
1905
|
-
addBtn.textContent = `+ Add ${element.label || "Text"}`;
|
|
1906
|
-
addBtn.onclick = () => {
|
|
1907
|
-
values.push(element.default || "");
|
|
1908
|
-
addTextItem(element.default || "");
|
|
1909
|
-
updateAddButton();
|
|
1910
|
-
updateRemoveButtons();
|
|
1911
|
-
};
|
|
1912
|
-
wrapper.appendChild(addBtn);
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
// Render initial items
|
|
1917
|
-
values.forEach((value) => addTextItem(value));
|
|
1918
|
-
updateAddButton();
|
|
1919
|
-
updateRemoveButtons();
|
|
1920
|
-
|
|
1921
|
-
// Add hint
|
|
1922
|
-
const hint = document.createElement("p");
|
|
1923
|
-
hint.className = "text-xs text-gray-500 mt-1";
|
|
1924
|
-
hint.textContent = makeFieldHint(element);
|
|
1925
|
-
wrapper.appendChild(hint);
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
1929
|
-
const textareaInput = document.createElement("textarea");
|
|
1930
|
-
textareaInput.className =
|
|
1931
|
-
"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
|
|
1932
|
-
textareaInput.name = pathKey;
|
|
1933
|
-
textareaInput.placeholder = element.placeholder || "Введите текст";
|
|
1934
|
-
textareaInput.rows = element.rows || 4;
|
|
1935
|
-
textareaInput.value = ctx.prefill[element.key] || element.default || "";
|
|
1936
|
-
textareaInput.readOnly = state.config.readonly;
|
|
1937
|
-
wrapper.appendChild(textareaInput);
|
|
1938
|
-
|
|
1939
|
-
// Add hint
|
|
1940
|
-
const textareaHint = document.createElement("p");
|
|
1941
|
-
textareaHint.className = "text-xs text-gray-500 mt-1";
|
|
1942
|
-
textareaHint.textContent = makeFieldHint(element);
|
|
1943
|
-
wrapper.appendChild(textareaHint);
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
1947
|
-
const prefillValues = ctx.prefill[element.key] || [];
|
|
1948
|
-
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
1949
|
-
|
|
1950
|
-
// Ensure minimum count
|
|
1951
|
-
const minCount = element.minCount ?? 1;
|
|
1952
|
-
const maxCount = element.maxCount ?? 10;
|
|
1953
|
-
|
|
1954
|
-
while (values.length < minCount) {
|
|
1955
|
-
values.push(element.default || "");
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
const container = document.createElement("div");
|
|
1959
|
-
container.className = "space-y-2";
|
|
1960
|
-
wrapper.appendChild(container);
|
|
1961
|
-
|
|
1962
|
-
function updateIndices() {
|
|
1963
|
-
const items = container.querySelectorAll(".multiple-textarea-item");
|
|
1964
|
-
items.forEach((item, index) => {
|
|
1965
|
-
const textarea = item.querySelector("textarea");
|
|
1966
|
-
if (textarea) {
|
|
1967
|
-
textarea.name = `${pathKey}[${index}]`;
|
|
1968
|
-
}
|
|
1969
|
-
});
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
function addTextareaItem(value = "", index = -1) {
|
|
1973
|
-
const itemWrapper = document.createElement("div");
|
|
1974
|
-
itemWrapper.className = "multiple-textarea-item";
|
|
1975
|
-
|
|
1976
|
-
const textareaInput = document.createElement("textarea");
|
|
1977
|
-
textareaInput.className =
|
|
1978
|
-
"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
|
|
1979
|
-
textareaInput.placeholder = element.placeholder || "Enter text";
|
|
1980
|
-
textareaInput.rows = element.rows || 4;
|
|
1981
|
-
textareaInput.value = value;
|
|
1982
|
-
textareaInput.readOnly = state.config.readonly;
|
|
1983
|
-
|
|
1984
|
-
itemWrapper.appendChild(textareaInput);
|
|
1985
|
-
|
|
1986
|
-
// Remove buttons are managed centrally via updateRemoveButtons()
|
|
1987
|
-
|
|
1988
|
-
if (index === -1) {
|
|
1989
|
-
container.appendChild(itemWrapper);
|
|
1990
|
-
} else {
|
|
1991
|
-
container.insertBefore(itemWrapper, container.children[index]);
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
updateIndices();
|
|
1995
|
-
return itemWrapper;
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
function updateRemoveButtons() {
|
|
1999
|
-
if (state.config.readonly) return;
|
|
2000
|
-
const items = container.querySelectorAll(".multiple-textarea-item");
|
|
2001
|
-
const currentCount = items.length;
|
|
2002
|
-
items.forEach((item) => {
|
|
2003
|
-
let removeBtn = item.querySelector(".remove-item-btn");
|
|
2004
|
-
if (!removeBtn) {
|
|
2005
|
-
removeBtn = document.createElement("button");
|
|
2006
|
-
removeBtn.type = "button";
|
|
2007
|
-
removeBtn.className =
|
|
2008
|
-
"remove-item-btn mt-1 px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm";
|
|
2009
|
-
removeBtn.innerHTML = "✕ Remove";
|
|
2010
|
-
removeBtn.onclick = () => {
|
|
2011
|
-
const currentIndex = Array.from(container.children).indexOf(item);
|
|
2012
|
-
if (container.children.length > minCount) {
|
|
2013
|
-
values.splice(currentIndex, 1);
|
|
2014
|
-
item.remove();
|
|
2015
|
-
updateIndices();
|
|
2016
|
-
updateAddButton();
|
|
2017
|
-
updateRemoveButtons();
|
|
2018
|
-
}
|
|
2019
|
-
};
|
|
2020
|
-
item.appendChild(removeBtn);
|
|
2021
|
-
}
|
|
2022
|
-
const disabled = currentCount <= minCount;
|
|
2023
|
-
removeBtn.disabled = disabled;
|
|
2024
|
-
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
2025
|
-
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
2026
|
-
});
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
function updateAddButton() {
|
|
2030
|
-
const existingAddBtn = wrapper.querySelector(".add-textarea-btn");
|
|
2031
|
-
if (existingAddBtn) existingAddBtn.remove();
|
|
2032
|
-
|
|
2033
|
-
if (!state.config.readonly && values.length < maxCount) {
|
|
2034
|
-
const addBtn = document.createElement("button");
|
|
2035
|
-
addBtn.type = "button";
|
|
2036
|
-
addBtn.className =
|
|
2037
|
-
"add-textarea-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
|
|
2038
|
-
addBtn.textContent = `+ Add ${element.label || "Textarea"}`;
|
|
2039
|
-
addBtn.onclick = () => {
|
|
2040
|
-
values.push(element.default || "");
|
|
2041
|
-
addTextareaItem(element.default || "");
|
|
2042
|
-
updateAddButton();
|
|
2043
|
-
updateRemoveButtons();
|
|
2044
|
-
};
|
|
2045
|
-
wrapper.appendChild(addBtn);
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
// Render initial items
|
|
2050
|
-
values.forEach((value) => addTextareaItem(value));
|
|
2051
|
-
updateAddButton();
|
|
2052
|
-
updateRemoveButtons();
|
|
2053
|
-
|
|
2054
|
-
// Add hint
|
|
2055
|
-
const hint = document.createElement("p");
|
|
2056
|
-
hint.className = "text-xs text-gray-500 mt-1";
|
|
2057
|
-
hint.textContent = makeFieldHint(element);
|
|
2058
|
-
wrapper.appendChild(hint);
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
2062
|
-
const numberInput = document.createElement("input");
|
|
2063
|
-
numberInput.type = "number";
|
|
2064
|
-
numberInput.className =
|
|
2065
|
-
"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
2066
|
-
numberInput.name = pathKey;
|
|
2067
|
-
numberInput.placeholder = element.placeholder || "0";
|
|
2068
|
-
if (element.min !== undefined) numberInput.min = element.min;
|
|
2069
|
-
if (element.max !== undefined) numberInput.max = element.max;
|
|
2070
|
-
if (element.step !== undefined) numberInput.step = element.step;
|
|
2071
|
-
numberInput.value = ctx.prefill[element.key] || element.default || "";
|
|
2072
|
-
numberInput.readOnly = state.config.readonly;
|
|
2073
|
-
wrapper.appendChild(numberInput);
|
|
2074
|
-
|
|
2075
|
-
// Add hint
|
|
2076
|
-
const numberHint = document.createElement("p");
|
|
2077
|
-
numberHint.className = "text-xs text-gray-500 mt-1";
|
|
2078
|
-
numberHint.textContent = makeFieldHint(element);
|
|
2079
|
-
wrapper.appendChild(numberHint);
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
2083
|
-
const prefillValues = ctx.prefill[element.key] || [];
|
|
2084
|
-
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
2085
|
-
|
|
2086
|
-
// Ensure minimum count
|
|
2087
|
-
const minCount = element.minCount ?? 1;
|
|
2088
|
-
const maxCount = element.maxCount ?? 10;
|
|
2089
|
-
|
|
2090
|
-
while (values.length < minCount) {
|
|
2091
|
-
values.push(element.default || "");
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
const container = document.createElement("div");
|
|
2095
|
-
container.className = "space-y-2";
|
|
2096
|
-
wrapper.appendChild(container);
|
|
2097
|
-
|
|
2098
|
-
function updateIndices() {
|
|
2099
|
-
const items = container.querySelectorAll(".multiple-number-item");
|
|
2100
|
-
items.forEach((item, index) => {
|
|
2101
|
-
const input = item.querySelector("input");
|
|
2102
|
-
if (input) {
|
|
2103
|
-
input.name = `${pathKey}[${index}]`;
|
|
2104
|
-
}
|
|
2105
|
-
});
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
function addNumberItem(value = "", index = -1) {
|
|
2109
|
-
const itemWrapper = document.createElement("div");
|
|
2110
|
-
itemWrapper.className = "multiple-number-item flex items-center gap-2";
|
|
2111
|
-
|
|
2112
|
-
const numberInput = document.createElement("input");
|
|
2113
|
-
numberInput.type = "number";
|
|
2114
|
-
numberInput.className =
|
|
2115
|
-
"flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
2116
|
-
numberInput.placeholder = element.placeholder || "0";
|
|
2117
|
-
if (element.min !== undefined) numberInput.min = element.min;
|
|
2118
|
-
if (element.max !== undefined) numberInput.max = element.max;
|
|
2119
|
-
if (element.step !== undefined) numberInput.step = element.step;
|
|
2120
|
-
numberInput.value = value;
|
|
2121
|
-
numberInput.readOnly = state.config.readonly;
|
|
2122
|
-
|
|
2123
|
-
itemWrapper.appendChild(numberInput);
|
|
2124
|
-
|
|
2125
|
-
// Remove buttons are managed centrally via updateRemoveButtons()
|
|
2126
|
-
|
|
2127
|
-
if (index === -1) {
|
|
2128
|
-
container.appendChild(itemWrapper);
|
|
2129
|
-
} else {
|
|
2130
|
-
container.insertBefore(itemWrapper, container.children[index]);
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
updateIndices();
|
|
2134
|
-
return itemWrapper;
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
function updateRemoveButtons() {
|
|
2138
|
-
if (state.config.readonly) return;
|
|
2139
|
-
const items = container.querySelectorAll(".multiple-number-item");
|
|
2140
|
-
const currentCount = items.length;
|
|
2141
|
-
items.forEach((item) => {
|
|
2142
|
-
let removeBtn = item.querySelector(".remove-item-btn");
|
|
2143
|
-
if (!removeBtn) {
|
|
2144
|
-
removeBtn = document.createElement("button");
|
|
2145
|
-
removeBtn.type = "button";
|
|
2146
|
-
removeBtn.className =
|
|
2147
|
-
"remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
|
|
2148
|
-
removeBtn.innerHTML = "✕";
|
|
2149
|
-
removeBtn.onclick = () => {
|
|
2150
|
-
const currentIndex = Array.from(container.children).indexOf(item);
|
|
2151
|
-
if (container.children.length > minCount) {
|
|
2152
|
-
values.splice(currentIndex, 1);
|
|
2153
|
-
item.remove();
|
|
2154
|
-
updateIndices();
|
|
2155
|
-
updateAddButton();
|
|
2156
|
-
updateRemoveButtons();
|
|
2157
|
-
}
|
|
2158
|
-
};
|
|
2159
|
-
item.appendChild(removeBtn);
|
|
2160
|
-
}
|
|
2161
|
-
const disabled = currentCount <= minCount;
|
|
2162
|
-
removeBtn.disabled = disabled;
|
|
2163
|
-
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
2164
|
-
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
2165
|
-
});
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
|
-
function updateAddButton() {
|
|
2169
|
-
const existingAddBtn = wrapper.querySelector(".add-number-btn");
|
|
2170
|
-
if (existingAddBtn) existingAddBtn.remove();
|
|
2171
|
-
|
|
2172
|
-
if (!state.config.readonly && values.length < maxCount) {
|
|
2173
|
-
const addBtn = document.createElement("button");
|
|
2174
|
-
addBtn.type = "button";
|
|
2175
|
-
addBtn.className =
|
|
2176
|
-
"add-number-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
|
|
2177
|
-
addBtn.textContent = `+ Add ${element.label || "Number"}`;
|
|
2178
|
-
addBtn.onclick = () => {
|
|
2179
|
-
values.push(element.default || "");
|
|
2180
|
-
addNumberItem(element.default || "");
|
|
2181
|
-
updateAddButton();
|
|
2182
|
-
updateRemoveButtons();
|
|
2183
|
-
};
|
|
2184
|
-
wrapper.appendChild(addBtn);
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
// Render initial items
|
|
2189
|
-
values.forEach((value) => addNumberItem(value));
|
|
2190
|
-
updateAddButton();
|
|
2191
|
-
updateRemoveButtons();
|
|
2192
|
-
|
|
2193
|
-
// Add hint
|
|
2194
|
-
const hint = document.createElement("p");
|
|
2195
|
-
hint.className = "text-xs text-gray-500 mt-1";
|
|
2196
|
-
hint.textContent = makeFieldHint(element);
|
|
2197
|
-
wrapper.appendChild(hint);
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
2201
|
-
const selectInput = document.createElement("select");
|
|
2202
|
-
selectInput.className =
|
|
2203
|
-
"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
2204
|
-
selectInput.name = pathKey;
|
|
2205
|
-
selectInput.disabled = state.config.readonly;
|
|
2206
|
-
|
|
2207
|
-
(element.options || []).forEach((option) => {
|
|
2208
|
-
const optionEl = document.createElement("option");
|
|
2209
|
-
optionEl.value = option.value;
|
|
2210
|
-
optionEl.textContent = option.label;
|
|
2211
|
-
if ((ctx.prefill[element.key] || element.default) === option.value) {
|
|
2212
|
-
optionEl.selected = true;
|
|
2213
|
-
}
|
|
2214
|
-
selectInput.appendChild(optionEl);
|
|
2215
|
-
});
|
|
2216
|
-
|
|
2217
|
-
wrapper.appendChild(selectInput);
|
|
2218
|
-
|
|
2219
|
-
// Add hint
|
|
2220
|
-
const selectHint = document.createElement("p");
|
|
2221
|
-
selectHint.className = "text-xs text-gray-500 mt-1";
|
|
2222
|
-
selectHint.textContent = makeFieldHint(element);
|
|
2223
|
-
wrapper.appendChild(selectHint);
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
2227
|
-
const prefillValues = ctx.prefill[element.key] || [];
|
|
2228
|
-
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
2229
|
-
|
|
2230
|
-
// Ensure minimum count
|
|
2231
|
-
const minCount = element.minCount ?? 1;
|
|
2232
|
-
const maxCount = element.maxCount ?? 10;
|
|
2233
|
-
|
|
2234
|
-
while (values.length < minCount) {
|
|
2235
|
-
values.push(element.default || element.options?.[0]?.value || "");
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
const container = document.createElement("div");
|
|
2239
|
-
container.className = "space-y-2";
|
|
2240
|
-
wrapper.appendChild(container);
|
|
2241
|
-
|
|
2242
|
-
function updateIndices() {
|
|
2243
|
-
const items = container.querySelectorAll(".multiple-select-item");
|
|
2244
|
-
items.forEach((item, index) => {
|
|
2245
|
-
const select = item.querySelector("select");
|
|
2246
|
-
if (select) {
|
|
2247
|
-
select.name = `${pathKey}[${index}]`;
|
|
2248
|
-
}
|
|
2249
|
-
});
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
function addSelectItem(value = "", index = -1) {
|
|
2253
|
-
const itemWrapper = document.createElement("div");
|
|
2254
|
-
itemWrapper.className = "multiple-select-item flex items-center gap-2";
|
|
2255
|
-
|
|
2256
|
-
const selectInput = document.createElement("select");
|
|
2257
|
-
selectInput.className =
|
|
2258
|
-
"flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
2259
|
-
selectInput.disabled = state.config.readonly;
|
|
2260
|
-
|
|
2261
|
-
// Add options
|
|
2262
|
-
(element.options || []).forEach((option) => {
|
|
2263
|
-
const optionElement = document.createElement("option");
|
|
2264
|
-
optionElement.value = option.value;
|
|
2265
|
-
optionElement.textContent = option.label;
|
|
2266
|
-
if (value === option.value) {
|
|
2267
|
-
optionElement.selected = true;
|
|
2268
|
-
}
|
|
2269
|
-
selectInput.appendChild(optionElement);
|
|
2270
|
-
});
|
|
2271
|
-
|
|
2272
|
-
itemWrapper.appendChild(selectInput);
|
|
2273
|
-
|
|
2274
|
-
// Remove buttons are managed centrally via updateRemoveButtons()
|
|
2275
|
-
|
|
2276
|
-
if (index === -1) {
|
|
2277
|
-
container.appendChild(itemWrapper);
|
|
2278
|
-
} else {
|
|
2279
|
-
container.insertBefore(itemWrapper, container.children[index]);
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
updateIndices();
|
|
2283
|
-
return itemWrapper;
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
function updateRemoveButtons() {
|
|
2287
|
-
if (state.config.readonly) return;
|
|
2288
|
-
const items = container.querySelectorAll(".multiple-select-item");
|
|
2289
|
-
const currentCount = items.length;
|
|
2290
|
-
items.forEach((item) => {
|
|
2291
|
-
let removeBtn = item.querySelector(".remove-item-btn");
|
|
2292
|
-
if (!removeBtn) {
|
|
2293
|
-
removeBtn = document.createElement("button");
|
|
2294
|
-
removeBtn.type = "button";
|
|
2295
|
-
removeBtn.className =
|
|
2296
|
-
"remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
|
|
2297
|
-
removeBtn.innerHTML = "✕";
|
|
2298
|
-
removeBtn.onclick = () => {
|
|
2299
|
-
const currentIndex = Array.from(container.children).indexOf(item);
|
|
2300
|
-
if (container.children.length > minCount) {
|
|
2301
|
-
values.splice(currentIndex, 1);
|
|
2302
|
-
item.remove();
|
|
2303
|
-
updateIndices();
|
|
2304
|
-
updateAddButton();
|
|
2305
|
-
updateRemoveButtons();
|
|
2306
|
-
}
|
|
2307
|
-
};
|
|
2308
|
-
item.appendChild(removeBtn);
|
|
2309
|
-
}
|
|
2310
|
-
const disabled = currentCount <= minCount;
|
|
2311
|
-
removeBtn.disabled = disabled;
|
|
2312
|
-
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
2313
|
-
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
2314
|
-
});
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
function updateAddButton() {
|
|
2318
|
-
const existingAddBtn = wrapper.querySelector(".add-select-btn");
|
|
2319
|
-
if (existingAddBtn) existingAddBtn.remove();
|
|
2320
|
-
|
|
2321
|
-
if (!state.config.readonly && values.length < maxCount) {
|
|
2322
|
-
const addBtn = document.createElement("button");
|
|
2323
|
-
addBtn.type = "button";
|
|
2324
|
-
addBtn.className =
|
|
2325
|
-
"add-select-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
|
|
2326
|
-
addBtn.textContent = `+ Add ${element.label || "Selection"}`;
|
|
2327
|
-
addBtn.onclick = () => {
|
|
2328
|
-
const defaultValue =
|
|
2329
|
-
element.default || element.options?.[0]?.value || "";
|
|
2330
|
-
values.push(defaultValue);
|
|
2331
|
-
addSelectItem(defaultValue);
|
|
2332
|
-
updateAddButton();
|
|
2333
|
-
updateRemoveButtons();
|
|
2334
|
-
};
|
|
2335
|
-
wrapper.appendChild(addBtn);
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
// Render initial items
|
|
2340
|
-
values.forEach((value) => addSelectItem(value));
|
|
2341
|
-
updateAddButton();
|
|
2342
|
-
updateRemoveButtons();
|
|
2343
|
-
|
|
2344
|
-
// Add hint
|
|
2345
|
-
const hint = document.createElement("p");
|
|
2346
|
-
hint.className = "text-xs text-gray-500 mt-1";
|
|
2347
|
-
hint.textContent = makeFieldHint(element);
|
|
2348
|
-
wrapper.appendChild(hint);
|
|
2349
|
-
}
|
|
2350
|
-
|
|
2351
|
-
function renderFileElement(element, ctx, wrapper, pathKey) {
|
|
2352
|
-
if (state.config.readonly) {
|
|
2353
|
-
// Readonly mode: use common preview function
|
|
2354
|
-
const initial = ctx.prefill[element.key] || element.default;
|
|
2355
|
-
if (initial) {
|
|
2356
|
-
const filePreview = renderFilePreviewReadonly(initial);
|
|
2357
|
-
wrapper.appendChild(filePreview);
|
|
2358
|
-
} else {
|
|
2359
|
-
const emptyState = document.createElement("div");
|
|
2360
|
-
emptyState.className =
|
|
2361
|
-
"aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
|
|
2362
|
-
emptyState.innerHTML = `<div class="text-center">${t("noFileSelected")}</div>`;
|
|
2363
|
-
wrapper.appendChild(emptyState);
|
|
2364
|
-
}
|
|
2365
|
-
} else {
|
|
2366
|
-
// Edit mode: normal file input
|
|
2367
|
-
const fileWrapper = document.createElement("div");
|
|
2368
|
-
fileWrapper.className = "space-y-2";
|
|
2369
|
-
|
|
2370
|
-
const picker = document.createElement("input");
|
|
2371
|
-
picker.type = "file";
|
|
2372
|
-
picker.name = pathKey;
|
|
2373
|
-
picker.style.display = "none"; // Hide default input
|
|
2374
|
-
if (element.accept?.extensions) {
|
|
2375
|
-
picker.accept = element.accept.extensions
|
|
2376
|
-
.map((ext) => `.${ext}`)
|
|
2377
|
-
.join(",");
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
const fileContainer = document.createElement("div");
|
|
2381
|
-
fileContainer.className =
|
|
2382
|
-
"file-preview-container w-full aspect-square max-w-xs bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";
|
|
2383
|
-
|
|
2384
|
-
const initial = ctx.prefill[element.key] || element.default;
|
|
2385
|
-
|
|
2386
|
-
// Set up click and drag handlers
|
|
2387
|
-
const fileUploadHandler = () => picker.click();
|
|
2388
|
-
const dragHandler = (files) => {
|
|
2389
|
-
if (files.length > 0) {
|
|
2390
|
-
const deps = { picker, fileUploadHandler, dragHandler };
|
|
2391
|
-
handleFileSelect(files[0], fileContainer, pathKey, deps);
|
|
2392
|
-
}
|
|
2393
|
-
};
|
|
2394
|
-
|
|
2395
|
-
// Handle initial prefill data
|
|
2396
|
-
if (initial) {
|
|
2397
|
-
handleInitialFileData(initial, fileContainer, pathKey, fileWrapper, {
|
|
2398
|
-
picker,
|
|
2399
|
-
fileUploadHandler,
|
|
2400
|
-
dragHandler,
|
|
2401
|
-
});
|
|
2402
|
-
} else {
|
|
2403
|
-
setEmptyFileContainer(fileContainer);
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
fileContainer.onclick = fileUploadHandler;
|
|
2407
|
-
setupDragAndDrop(fileContainer, dragHandler);
|
|
2408
|
-
|
|
2409
|
-
picker.onchange = () => {
|
|
2410
|
-
if (picker.files.length > 0) {
|
|
2411
|
-
const deps = { picker, fileUploadHandler, dragHandler };
|
|
2412
|
-
handleFileSelect(picker.files[0], fileContainer, pathKey, deps);
|
|
2413
|
-
}
|
|
2414
|
-
};
|
|
2415
|
-
|
|
2416
|
-
fileWrapper.appendChild(fileContainer);
|
|
2417
|
-
fileWrapper.appendChild(picker);
|
|
2418
|
-
|
|
2419
|
-
// Add upload text
|
|
2420
|
-
const uploadText = document.createElement("p");
|
|
2421
|
-
uploadText.className = "text-xs text-gray-600 mt-2 text-center";
|
|
2422
|
-
uploadText.innerHTML = `<span class="underline cursor-pointer">${t("uploadText")}</span> ${t("dragDropTextSingle")}`;
|
|
2423
|
-
uploadText.querySelector("span").onclick = () => picker.click();
|
|
2424
|
-
fileWrapper.appendChild(uploadText);
|
|
2425
|
-
|
|
2426
|
-
// Add hint
|
|
2427
|
-
const fileHint = document.createElement("p");
|
|
2428
|
-
fileHint.className = "text-xs text-gray-500 mt-1 text-center";
|
|
2429
|
-
fileHint.textContent = makeFieldHint(element);
|
|
2430
|
-
fileWrapper.appendChild(fileHint);
|
|
2431
|
-
|
|
2432
|
-
wrapper.appendChild(fileWrapper);
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
function handleInitialFileData(
|
|
2437
|
-
initial,
|
|
2438
|
-
fileContainer,
|
|
2439
|
-
pathKey,
|
|
2440
|
-
fileWrapper,
|
|
2441
|
-
deps,
|
|
2442
|
-
) {
|
|
2443
|
-
// Add prefill data to resourceIndex so renderFilePreview can use it
|
|
2444
|
-
if (!state.resourceIndex.has(initial)) {
|
|
2445
|
-
// Extract filename from URL/path
|
|
2446
|
-
const filename = initial.split("/").pop() || "file";
|
|
2447
|
-
// Determine file type from extension
|
|
2448
|
-
const extension = filename.split(".").pop()?.toLowerCase();
|
|
2449
|
-
let fileType = "application/octet-stream";
|
|
2450
|
-
|
|
2451
|
-
if (extension) {
|
|
2452
|
-
if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension)) {
|
|
2453
|
-
fileType = `image/${extension === "jpg" ? "jpeg" : extension}`;
|
|
2454
|
-
} else if (["mp4", "webm", "mov", "avi"].includes(extension)) {
|
|
2455
|
-
fileType = `video/${extension === "mov" ? "quicktime" : extension}`;
|
|
2456
|
-
}
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
|
-
state.resourceIndex.set(initial, {
|
|
2460
|
-
name: filename,
|
|
2461
|
-
type: fileType,
|
|
2462
|
-
size: 0,
|
|
2463
|
-
file: null, // No local file for prefill data
|
|
2464
|
-
});
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
renderFilePreview(fileContainer, initial, {
|
|
2468
|
-
fileName: initial,
|
|
2469
|
-
isReadonly: false,
|
|
2470
|
-
deps,
|
|
2471
|
-
}).catch(console.error);
|
|
2472
|
-
|
|
2473
|
-
// Create hidden input to store the prefilled resource ID
|
|
2474
|
-
const hiddenInput = document.createElement("input");
|
|
2475
|
-
hiddenInput.type = "hidden";
|
|
2476
|
-
hiddenInput.name = pathKey;
|
|
2477
|
-
hiddenInput.value = initial;
|
|
2478
|
-
fileWrapper.appendChild(hiddenInput);
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
function setEmptyFileContainer(fileContainer) {
|
|
2482
|
-
fileContainer.innerHTML = `
|
|
2483
|
-
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
2484
|
-
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
2485
|
-
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
2486
|
-
</svg>
|
|
2487
|
-
<div class="text-sm text-center">${t("clickDragText")}</div>
|
|
2488
|
-
</div>
|
|
2489
|
-
`;
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
2493
|
-
if (state.config.readonly) {
|
|
2494
|
-
// Readonly mode: render as results list like in workflow-preview.html
|
|
2495
|
-
const resultsWrapper = document.createElement("div");
|
|
2496
|
-
resultsWrapper.className = "space-y-4";
|
|
2497
|
-
|
|
2498
|
-
const initialFiles = ctx.prefill[element.key] || [];
|
|
2499
|
-
|
|
2500
|
-
if (initialFiles.length > 0) {
|
|
2501
|
-
initialFiles.forEach((resourceId) => {
|
|
2502
|
-
const filePreview = renderFilePreviewReadonly(resourceId);
|
|
2503
|
-
resultsWrapper.appendChild(filePreview);
|
|
2504
|
-
});
|
|
2505
|
-
} else {
|
|
2506
|
-
resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${t("noFilesSelected")}</div></div>`;
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
|
-
wrapper.appendChild(resultsWrapper);
|
|
2510
|
-
} else {
|
|
2511
|
-
// Edit mode: normal files input
|
|
2512
|
-
const filesWrapper = document.createElement("div");
|
|
2513
|
-
filesWrapper.className = "space-y-2";
|
|
2514
|
-
|
|
2515
|
-
const filesPicker = document.createElement("input");
|
|
2516
|
-
filesPicker.type = "file";
|
|
2517
|
-
filesPicker.name = pathKey;
|
|
2518
|
-
filesPicker.multiple = true;
|
|
2519
|
-
filesPicker.style.display = "none"; // Hide default input
|
|
2520
|
-
if (element.accept?.extensions) {
|
|
2521
|
-
filesPicker.accept = element.accept.extensions
|
|
2522
|
-
.map((ext) => `.${ext}`)
|
|
2523
|
-
.join(",");
|
|
2524
|
-
}
|
|
2525
|
-
|
|
2526
|
-
// Create container with border like in workflow-preview
|
|
2527
|
-
const filesContainer = document.createElement("div");
|
|
2528
|
-
filesContainer.className =
|
|
2529
|
-
"border-2 border-dashed border-gray-300 rounded-lg p-3 hover:border-gray-400 transition-colors";
|
|
2530
|
-
|
|
2531
|
-
const list = document.createElement("div");
|
|
2532
|
-
list.className = "files-list";
|
|
2533
|
-
|
|
2534
|
-
const initialFiles = ctx.prefill[element.key] || [];
|
|
2535
|
-
|
|
2536
|
-
// Add prefill files to resourceIndex so renderResourcePills can use them
|
|
2537
|
-
addPrefillFilesToIndex(initialFiles);
|
|
2538
|
-
|
|
2539
|
-
function updateFilesList() {
|
|
2540
|
-
renderResourcePills(list, initialFiles, (ridToRemove) => {
|
|
2541
|
-
const index = initialFiles.indexOf(ridToRemove);
|
|
2542
|
-
if (index > -1) {
|
|
2543
|
-
initialFiles.splice(index, 1);
|
|
2544
|
-
}
|
|
2545
|
-
updateFilesList(); // Re-render after removal
|
|
2546
|
-
});
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
|
-
// Initial render
|
|
2550
|
-
updateFilesList();
|
|
2551
|
-
|
|
2552
|
-
setupFilesDropHandler(filesContainer, initialFiles, updateFilesList);
|
|
2553
|
-
setupFilesPickerHandler(filesPicker, initialFiles, updateFilesList);
|
|
2554
|
-
|
|
2555
|
-
filesContainer.appendChild(list);
|
|
2556
|
-
filesWrapper.appendChild(filesContainer);
|
|
2557
|
-
filesWrapper.appendChild(filesPicker);
|
|
2558
|
-
|
|
2559
|
-
// Add hint
|
|
2560
|
-
const filesHint = document.createElement("p");
|
|
2561
|
-
filesHint.className = "text-xs text-gray-500 mt-1 text-center";
|
|
2562
|
-
filesHint.textContent = makeFieldHint(element);
|
|
2563
|
-
filesWrapper.appendChild(filesHint);
|
|
2564
|
-
|
|
2565
|
-
wrapper.appendChild(filesWrapper);
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
|
|
2569
|
-
function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
|
|
2570
|
-
// Use the same logic as renderFilesElement but with minCount/maxCount from element properties
|
|
2571
|
-
const minFiles = element.minCount ?? 0;
|
|
2572
|
-
const maxFiles = element.maxCount ?? Infinity;
|
|
2573
|
-
|
|
2574
|
-
if (state.config.readonly) {
|
|
2575
|
-
// Readonly mode: render as results list
|
|
2576
|
-
const resultsWrapper = document.createElement("div");
|
|
2577
|
-
resultsWrapper.className = "space-y-4";
|
|
2578
|
-
|
|
2579
|
-
const initialFiles = ctx.prefill[element.key] || [];
|
|
2580
|
-
|
|
2581
|
-
if (initialFiles.length > 0) {
|
|
2582
|
-
initialFiles.forEach((resourceId) => {
|
|
2583
|
-
const filePreview = renderFilePreviewReadonly(resourceId);
|
|
2584
|
-
resultsWrapper.appendChild(filePreview);
|
|
2585
|
-
});
|
|
2586
|
-
} else {
|
|
2587
|
-
resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${t("noFilesSelected")}</div></div>`;
|
|
2588
|
-
}
|
|
2589
|
-
|
|
2590
|
-
wrapper.appendChild(resultsWrapper);
|
|
2591
|
-
} else {
|
|
2592
|
-
// Edit mode: multiple file input with min/max validation
|
|
2593
|
-
const filesWrapper = document.createElement("div");
|
|
2594
|
-
filesWrapper.className = "space-y-2";
|
|
2595
|
-
|
|
2596
|
-
const filesPicker = document.createElement("input");
|
|
2597
|
-
filesPicker.type = "file";
|
|
2598
|
-
filesPicker.name = pathKey;
|
|
2599
|
-
filesPicker.multiple = true;
|
|
2600
|
-
filesPicker.style.display = "none"; // Hide default input
|
|
2601
|
-
if (element.accept?.extensions) {
|
|
2602
|
-
filesPicker.accept = element.accept.extensions
|
|
2603
|
-
.map((ext) => `.${ext}`)
|
|
2604
|
-
.join(",");
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
const filesContainer = document.createElement("div");
|
|
2608
|
-
filesContainer.className = "files-list space-y-2";
|
|
2609
|
-
|
|
2610
|
-
filesWrapper.appendChild(filesPicker);
|
|
2611
|
-
filesWrapper.appendChild(filesContainer);
|
|
2612
|
-
|
|
2613
|
-
const initialFiles = Array.isArray(ctx.prefill[element.key])
|
|
2614
|
-
? [...ctx.prefill[element.key]]
|
|
2615
|
-
: [];
|
|
2616
|
-
|
|
2617
|
-
// Add initial files to resource index
|
|
2618
|
-
addPrefillFilesToIndex(initialFiles);
|
|
2619
|
-
|
|
2620
|
-
const updateFilesDisplay = () => {
|
|
2621
|
-
renderResourcePills(filesContainer, initialFiles, (index) => {
|
|
2622
|
-
initialFiles.splice(index, 1);
|
|
2623
|
-
updateFilesDisplay();
|
|
2624
|
-
});
|
|
2625
|
-
|
|
2626
|
-
// Show count and min/max info
|
|
2627
|
-
const countInfo = document.createElement("div");
|
|
2628
|
-
countInfo.className = "text-xs text-gray-500 mt-2";
|
|
2629
|
-
const countText = `${initialFiles.length} file${initialFiles.length !== 1 ? "s" : ""}`;
|
|
2630
|
-
const minMaxText =
|
|
2631
|
-
minFiles > 0 || maxFiles < Infinity
|
|
2632
|
-
? ` (${minFiles}-${maxFiles} allowed)`
|
|
2633
|
-
: "";
|
|
2634
|
-
countInfo.textContent = countText + minMaxText;
|
|
2635
|
-
|
|
2636
|
-
// Remove previous count info
|
|
2637
|
-
const existingCount = filesWrapper.querySelector(".file-count-info");
|
|
2638
|
-
if (existingCount) existingCount.remove();
|
|
2639
|
-
|
|
2640
|
-
countInfo.className += " file-count-info";
|
|
2641
|
-
filesWrapper.appendChild(countInfo);
|
|
2642
|
-
};
|
|
2643
|
-
|
|
2644
|
-
// Set up drag and drop
|
|
2645
|
-
setupFilesDropHandler(filesContainer, initialFiles, updateFilesDisplay);
|
|
2646
|
-
|
|
2647
|
-
// Set up file picker
|
|
2648
|
-
setupFilesPickerHandler(filesPicker, initialFiles, updateFilesDisplay);
|
|
2649
|
-
|
|
2650
|
-
// Initial display
|
|
2651
|
-
updateFilesDisplay();
|
|
2652
|
-
|
|
2653
|
-
wrapper.appendChild(filesWrapper);
|
|
2654
|
-
}
|
|
2655
|
-
}
|
|
2656
|
-
|
|
2657
|
-
function addPrefillFilesToIndex(initialFiles) {
|
|
2658
|
-
if (initialFiles.length > 0) {
|
|
2659
|
-
initialFiles.forEach((resourceId) => {
|
|
2660
|
-
if (!state.resourceIndex.has(resourceId)) {
|
|
2661
|
-
// Extract filename from URL/path
|
|
2662
|
-
const filename = resourceId.split("/").pop() || "file";
|
|
2663
|
-
// Determine file type from extension (excluding PSD from image types)
|
|
2664
|
-
const extension = filename.split(".").pop()?.toLowerCase();
|
|
2665
|
-
const fileType =
|
|
2666
|
-
extension && ["jpg", "jpeg", "png", "gif", "webp"].includes(extension)
|
|
2667
|
-
? `image/${extension === "jpg" ? "jpeg" : extension}`
|
|
2668
|
-
: "application/octet-stream";
|
|
2669
|
-
|
|
2670
|
-
state.resourceIndex.set(resourceId, {
|
|
2671
|
-
name: filename,
|
|
2672
|
-
type: fileType,
|
|
2673
|
-
size: 0,
|
|
2674
|
-
file: null, // No local file for prefill data
|
|
2675
|
-
});
|
|
2676
|
-
}
|
|
2677
|
-
});
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
|
|
2681
|
-
function setupFilesDropHandler(filesContainer, initialFiles, updateCallback) {
|
|
2682
|
-
setupDragAndDrop(filesContainer, async (files) => {
|
|
2683
|
-
const arr = Array.from(files);
|
|
2684
|
-
for (const file of arr) {
|
|
2685
|
-
const rid = await uploadSingleFile(file);
|
|
2686
|
-
state.resourceIndex.set(rid, {
|
|
2687
|
-
name: file.name,
|
|
2688
|
-
type: file.type,
|
|
2689
|
-
size: file.size,
|
|
2690
|
-
file: null, // Files are always uploaded, never stored locally
|
|
2691
|
-
});
|
|
2692
|
-
initialFiles.push(rid);
|
|
2693
|
-
}
|
|
2694
|
-
updateCallback();
|
|
2695
|
-
});
|
|
2696
|
-
}
|
|
2697
|
-
|
|
2698
|
-
function setupFilesPickerHandler(filesPicker, initialFiles, updateCallback) {
|
|
2699
|
-
filesPicker.onchange = async () => {
|
|
2700
|
-
for (const file of Array.from(filesPicker.files)) {
|
|
2701
|
-
const rid = await uploadSingleFile(file);
|
|
2702
|
-
state.resourceIndex.set(rid, {
|
|
2703
|
-
name: file.name,
|
|
2704
|
-
type: file.type,
|
|
2705
|
-
size: file.size,
|
|
2706
|
-
file: null, // Files are always uploaded, never stored locally
|
|
2707
|
-
});
|
|
2708
|
-
initialFiles.push(rid);
|
|
2709
|
-
}
|
|
2710
|
-
updateCallback();
|
|
2711
|
-
// Clear the file input
|
|
2712
|
-
filesPicker.value = "";
|
|
2713
|
-
};
|
|
2714
|
-
}
|
|
2715
|
-
|
|
2716
|
-
async function uploadSingleFile(file) {
|
|
2717
|
-
// If uploadHandler is configured, use it to upload the file
|
|
2718
|
-
if (state.config.uploadFile) {
|
|
2719
|
-
try {
|
|
2720
|
-
const rid = await state.config.uploadFile(file);
|
|
2721
|
-
if (typeof rid !== "string") {
|
|
2722
|
-
throw new Error("Upload handler must return a string resource ID");
|
|
2723
|
-
}
|
|
2724
|
-
return rid;
|
|
2725
|
-
} catch (error) {
|
|
2726
|
-
throw new Error(`File upload failed: ${error.message}`);
|
|
2727
|
-
}
|
|
2728
|
-
} else {
|
|
2729
|
-
throw new Error(
|
|
2730
|
-
"No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()",
|
|
2731
|
-
);
|
|
2732
|
-
}
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
|
-
function renderGroupElement(element, ctx, wrapper, _pathKey) {
|
|
2736
|
-
const groupWrap = document.createElement("div");
|
|
2737
|
-
groupWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
2738
|
-
|
|
2739
|
-
const header = document.createElement("div");
|
|
2740
|
-
header.className = "flex justify-between items-center mb-4";
|
|
2741
|
-
|
|
2742
|
-
const left = document.createElement("div");
|
|
2743
|
-
left.className = "flex-1";
|
|
2744
|
-
|
|
2745
|
-
const right = document.createElement("div");
|
|
2746
|
-
right.className = "flex gap-2";
|
|
2747
|
-
|
|
2748
|
-
const itemsWrap = document.createElement("div");
|
|
2749
|
-
itemsWrap.className = "space-y-4";
|
|
2750
|
-
|
|
2751
|
-
groupWrap.appendChild(header);
|
|
2752
|
-
header.appendChild(left);
|
|
2753
|
-
if (!state.config.readonly) {
|
|
2754
|
-
header.appendChild(right);
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
if (element.repeat && isPlainObject(element.repeat)) {
|
|
2758
|
-
renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap);
|
|
2759
|
-
} else {
|
|
2760
|
-
renderSingleGroup(element, ctx, itemsWrap, left, groupWrap);
|
|
2761
|
-
}
|
|
2762
|
-
|
|
2763
|
-
wrapper.appendChild(groupWrap);
|
|
2764
|
-
}
|
|
2765
|
-
|
|
2766
|
-
function renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap) {
|
|
2767
|
-
const min = element.repeat.min ?? 0;
|
|
2768
|
-
const max = element.repeat.max ?? Infinity;
|
|
2769
|
-
const pre = Array.isArray(ctx.prefill?.[element.key])
|
|
2770
|
-
? ctx.prefill[element.key]
|
|
2771
|
-
: null;
|
|
2772
|
-
|
|
2773
|
-
const countItems = () =>
|
|
2774
|
-
itemsWrap.querySelectorAll(":scope > .groupItem").length;
|
|
2775
|
-
|
|
2776
|
-
const addItem = (prefillObj) => {
|
|
2777
|
-
const item = document.createElement("div");
|
|
2778
|
-
item.className =
|
|
2779
|
-
"groupItem border border-dashed border-slate-300 dark:border-slate-600 rounded-lg p-3 mb-3 bg-blue-50/30 dark:bg-blue-900/10";
|
|
2780
|
-
const itemIndex = countItems();
|
|
2781
|
-
const fullPath = pathJoin(ctx.path, `${element.key}[${itemIndex}]`);
|
|
2782
|
-
// Add data-group-item attribute for validation scoping - use full path
|
|
2783
|
-
item.setAttribute("data-group-item", fullPath);
|
|
2784
|
-
const subCtx = {
|
|
2785
|
-
path: fullPath,
|
|
2786
|
-
prefill: prefillObj || {},
|
|
2787
|
-
};
|
|
2788
|
-
element.elements.forEach((child) => {
|
|
2789
|
-
// Skip rendering hidden child elements
|
|
2790
|
-
if (!child.hidden) {
|
|
2791
|
-
item.appendChild(renderElement(child, subCtx));
|
|
2792
|
-
}
|
|
2793
|
-
});
|
|
2794
|
-
|
|
2795
|
-
// Only add remove button in edit mode
|
|
2796
|
-
if (!state.config.readonly) {
|
|
2797
|
-
const rem = document.createElement("button");
|
|
2798
|
-
rem.type = "button";
|
|
2799
|
-
rem.className =
|
|
2800
|
-
"bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded text-xs font-medium transition-colors";
|
|
2801
|
-
rem.textContent = t("removeElement");
|
|
2802
|
-
rem.addEventListener("click", () => {
|
|
2803
|
-
if (countItems() <= (element.repeat.min ?? 0)) return;
|
|
2804
|
-
itemsWrap.removeChild(item);
|
|
2805
|
-
refreshControls();
|
|
2806
|
-
});
|
|
2807
|
-
item.appendChild(rem);
|
|
2808
|
-
}
|
|
2809
|
-
itemsWrap.appendChild(item);
|
|
2810
|
-
if (!state.config.readonly) {
|
|
2811
|
-
refreshControls();
|
|
2812
|
-
}
|
|
2813
|
-
};
|
|
2814
|
-
|
|
2815
|
-
groupWrap.appendChild(itemsWrap);
|
|
2816
|
-
|
|
2817
|
-
// Only add button in edit mode
|
|
2818
|
-
let addBtn;
|
|
2819
|
-
if (!state.config.readonly) {
|
|
2820
|
-
addBtn = document.createElement("button");
|
|
2821
|
-
addBtn.type = "button";
|
|
2822
|
-
addBtn.className =
|
|
2823
|
-
"w-full py-2 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 flex items-center justify-center mt-3";
|
|
2824
|
-
addBtn.innerHTML = `<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>${t("addElement")}`;
|
|
2825
|
-
groupWrap.appendChild(addBtn);
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
|
-
const refreshControls = () => {
|
|
2829
|
-
if (state.config.readonly) return;
|
|
2830
|
-
const n = countItems();
|
|
2831
|
-
if (addBtn) addBtn.disabled = n >= max;
|
|
2832
|
-
left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-slate-500 dark:text-slate-400 text-xs">[${n} / ${max === Infinity ? "∞" : max}, min=${min}]</span>`;
|
|
2833
|
-
};
|
|
2834
|
-
|
|
2835
|
-
if (pre && pre.length) {
|
|
2836
|
-
const n = Math.min(max, Math.max(min, pre.length));
|
|
2837
|
-
for (let i = 0; i < n; i++) addItem(pre[i]);
|
|
2838
|
-
} else {
|
|
2839
|
-
const n = Math.max(min, 0);
|
|
2840
|
-
for (let i = 0; i < n; i++) addItem(null);
|
|
2841
|
-
}
|
|
2842
|
-
|
|
2843
|
-
if (!state.config.readonly) {
|
|
2844
|
-
addBtn.addEventListener("click", () => addItem(null));
|
|
2845
|
-
} else {
|
|
2846
|
-
// In readonly mode, just show the label without count controls
|
|
2847
|
-
left.innerHTML = `<span>${element.label || element.key}</span>`;
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
|
|
2851
|
-
function renderSingleGroup(element, ctx, itemsWrap, left, groupWrap) {
|
|
2852
|
-
// Single object group
|
|
2853
|
-
const subCtx = {
|
|
2854
|
-
path: pathJoin(ctx.path, element.key),
|
|
2855
|
-
prefill: ctx.prefill?.[element.key] || {},
|
|
2856
|
-
};
|
|
2857
|
-
element.elements.forEach((child) => {
|
|
2858
|
-
// Skip rendering hidden child elements
|
|
2859
|
-
if (!child.hidden) {
|
|
2860
|
-
itemsWrap.appendChild(renderElement(child, subCtx));
|
|
2861
|
-
}
|
|
2862
|
-
});
|
|
2863
|
-
groupWrap.appendChild(itemsWrap);
|
|
2864
|
-
left.innerHTML = `<span>${element.label || element.key}</span>`;
|
|
2865
|
-
}
|
|
2866
|
-
|
|
2867
|
-
function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
2868
|
-
// Same as renderSingleGroup but with updated naming
|
|
2869
|
-
const containerWrap = document.createElement("div");
|
|
2870
|
-
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
2871
|
-
containerWrap.setAttribute("data-container", pathKey);
|
|
2872
|
-
|
|
2873
|
-
const header = document.createElement("div");
|
|
2874
|
-
header.className = "flex justify-between items-center mb-4";
|
|
2875
|
-
|
|
2876
|
-
const left = document.createElement("div");
|
|
2877
|
-
left.className = "flex-1";
|
|
2878
|
-
|
|
2879
|
-
const itemsWrap = document.createElement("div");
|
|
2880
|
-
itemsWrap.className = "space-y-4";
|
|
2881
|
-
|
|
2882
|
-
containerWrap.appendChild(header);
|
|
2883
|
-
header.appendChild(left);
|
|
2884
|
-
|
|
2885
|
-
// Single object container
|
|
2886
|
-
const subCtx = {
|
|
2887
|
-
path: pathJoin(ctx.path, element.key),
|
|
2888
|
-
prefill: ctx.prefill?.[element.key] || {},
|
|
2889
|
-
};
|
|
2890
|
-
element.elements.forEach((child) => {
|
|
2891
|
-
// Skip rendering hidden child elements
|
|
2892
|
-
if (!child.hidden) {
|
|
2893
|
-
itemsWrap.appendChild(renderElement(child, subCtx));
|
|
2894
|
-
}
|
|
2895
|
-
});
|
|
2896
|
-
containerWrap.appendChild(itemsWrap);
|
|
2897
|
-
left.innerHTML = `<span>${element.label || element.key}</span>`;
|
|
2898
|
-
|
|
2899
|
-
wrapper.appendChild(containerWrap);
|
|
2900
|
-
}
|
|
2901
|
-
|
|
2902
|
-
function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
2903
|
-
// Same as renderRepeatableGroup but with minCount/maxCount from element properties
|
|
2904
|
-
const containerWrap = document.createElement("div");
|
|
2905
|
-
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
2906
|
-
|
|
2907
|
-
const header = document.createElement("div");
|
|
2908
|
-
header.className = "flex justify-between items-center mb-4";
|
|
2909
|
-
|
|
2910
|
-
const left = document.createElement("div");
|
|
2911
|
-
left.className = "flex-1";
|
|
2912
|
-
|
|
2913
|
-
const right = document.createElement("div");
|
|
2914
|
-
right.className = "flex gap-2";
|
|
2915
|
-
|
|
2916
|
-
const itemsWrap = document.createElement("div");
|
|
2917
|
-
itemsWrap.className = "space-y-4";
|
|
2918
|
-
|
|
2919
|
-
containerWrap.appendChild(header);
|
|
2920
|
-
header.appendChild(left);
|
|
2921
|
-
if (!state.config.readonly) {
|
|
2922
|
-
header.appendChild(right);
|
|
2923
|
-
}
|
|
2924
|
-
|
|
2925
|
-
const min = element.minCount ?? 0;
|
|
2926
|
-
const max = element.maxCount ?? Infinity;
|
|
2927
|
-
const pre = Array.isArray(ctx.prefill?.[element.key])
|
|
2928
|
-
? ctx.prefill[element.key]
|
|
2929
|
-
: null;
|
|
2930
|
-
|
|
2931
|
-
const countItems = () =>
|
|
2932
|
-
itemsWrap.querySelectorAll(":scope > .containerItem").length;
|
|
2933
|
-
|
|
2934
|
-
const createAddButton = () => {
|
|
2935
|
-
const add = document.createElement("button");
|
|
2936
|
-
add.type = "button";
|
|
2937
|
-
add.className =
|
|
2938
|
-
"px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors";
|
|
2939
|
-
add.textContent = t("addElement");
|
|
2940
|
-
add.onclick = () => {
|
|
2941
|
-
if (countItems() < max) {
|
|
2942
|
-
const idx = countItems();
|
|
2943
|
-
const subCtx = {
|
|
2944
|
-
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
2945
|
-
prefill: {},
|
|
2946
|
-
};
|
|
2947
|
-
const item = document.createElement("div");
|
|
2948
|
-
item.className =
|
|
2949
|
-
"containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
2950
|
-
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
2951
|
-
|
|
2952
|
-
element.elements.forEach((child) => {
|
|
2953
|
-
// Skip rendering hidden child elements
|
|
2954
|
-
if (!child.hidden) {
|
|
2955
|
-
item.appendChild(renderElement(child, subCtx));
|
|
2956
|
-
}
|
|
2957
|
-
});
|
|
2958
|
-
|
|
2959
|
-
// Only add remove button in edit mode
|
|
2960
|
-
if (!state.config.readonly) {
|
|
2961
|
-
const rem = document.createElement("button");
|
|
2962
|
-
rem.type = "button";
|
|
2963
|
-
rem.className =
|
|
2964
|
-
"absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors";
|
|
2965
|
-
rem.textContent = "×";
|
|
2966
|
-
rem.onclick = () => {
|
|
2967
|
-
item.remove();
|
|
2968
|
-
updateAddButton();
|
|
2969
|
-
};
|
|
2970
|
-
item.style.position = "relative";
|
|
2971
|
-
item.appendChild(rem);
|
|
2972
|
-
}
|
|
2973
|
-
|
|
2974
|
-
itemsWrap.appendChild(item);
|
|
2975
|
-
updateAddButton();
|
|
2976
|
-
}
|
|
2977
|
-
};
|
|
2978
|
-
return add;
|
|
2979
|
-
};
|
|
2980
|
-
|
|
2981
|
-
const updateAddButton = () => {
|
|
2982
|
-
const currentCount = countItems();
|
|
2983
|
-
const addBtn = right.querySelector("button");
|
|
2984
|
-
if (addBtn) {
|
|
2985
|
-
addBtn.disabled = currentCount >= max;
|
|
2986
|
-
addBtn.style.opacity = currentCount >= max ? "0.5" : "1";
|
|
2987
|
-
}
|
|
2988
|
-
left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-sm text-gray-500">(${currentCount}/${max === Infinity ? "∞" : max})</span>`;
|
|
2989
|
-
};
|
|
2990
|
-
|
|
2991
|
-
if (!state.config.readonly) {
|
|
2992
|
-
right.appendChild(createAddButton());
|
|
2993
|
-
}
|
|
2994
|
-
|
|
2995
|
-
// Pre-fill initial items
|
|
2996
|
-
if (pre && Array.isArray(pre)) {
|
|
2997
|
-
pre.forEach((prefillObj, idx) => {
|
|
2998
|
-
const subCtx = {
|
|
2999
|
-
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
3000
|
-
prefill: prefillObj || {},
|
|
3001
|
-
};
|
|
3002
|
-
const item = document.createElement("div");
|
|
3003
|
-
item.className =
|
|
3004
|
-
"containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
3005
|
-
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
3006
|
-
|
|
3007
|
-
element.elements.forEach((child) => {
|
|
3008
|
-
// Skip rendering hidden child elements
|
|
3009
|
-
if (!child.hidden) {
|
|
3010
|
-
item.appendChild(renderElement(child, subCtx));
|
|
3011
|
-
}
|
|
3012
|
-
});
|
|
3013
|
-
|
|
3014
|
-
// Only add remove button in edit mode
|
|
3015
|
-
if (!state.config.readonly) {
|
|
3016
|
-
const rem = document.createElement("button");
|
|
3017
|
-
rem.type = "button";
|
|
3018
|
-
rem.className =
|
|
3019
|
-
"absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors";
|
|
3020
|
-
rem.textContent = "×";
|
|
3021
|
-
rem.onclick = () => {
|
|
3022
|
-
item.remove();
|
|
3023
|
-
updateAddButton();
|
|
3024
|
-
};
|
|
3025
|
-
item.style.position = "relative";
|
|
3026
|
-
item.appendChild(rem);
|
|
3027
|
-
}
|
|
3028
|
-
|
|
3029
|
-
itemsWrap.appendChild(item);
|
|
3030
|
-
});
|
|
3031
|
-
}
|
|
3032
|
-
|
|
3033
|
-
// Ensure minimum items
|
|
3034
|
-
if (!state.config.readonly) {
|
|
3035
|
-
while (countItems() < min) {
|
|
3036
|
-
const idx = countItems();
|
|
3037
|
-
const subCtx = {
|
|
3038
|
-
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
3039
|
-
prefill: {},
|
|
3040
|
-
};
|
|
3041
|
-
const item = document.createElement("div");
|
|
3042
|
-
item.className =
|
|
3043
|
-
"containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
3044
|
-
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
3045
|
-
|
|
3046
|
-
element.elements.forEach((child) => {
|
|
3047
|
-
// Skip rendering hidden child elements
|
|
3048
|
-
if (!child.hidden) {
|
|
3049
|
-
item.appendChild(renderElement(child, subCtx));
|
|
3050
|
-
}
|
|
3051
|
-
});
|
|
3052
|
-
|
|
3053
|
-
// Remove button - but disabled if we're at minimum
|
|
3054
|
-
const rem = document.createElement("button");
|
|
3055
|
-
rem.type = "button";
|
|
3056
|
-
rem.className =
|
|
3057
|
-
"absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors";
|
|
3058
|
-
rem.textContent = "×";
|
|
3059
|
-
rem.onclick = () => {
|
|
3060
|
-
if (countItems() > min) {
|
|
3061
|
-
item.remove();
|
|
3062
|
-
updateAddButton();
|
|
3063
|
-
}
|
|
3064
|
-
};
|
|
3065
|
-
item.style.position = "relative";
|
|
3066
|
-
item.appendChild(rem);
|
|
3067
|
-
|
|
3068
|
-
itemsWrap.appendChild(item);
|
|
3069
|
-
}
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
|
-
containerWrap.appendChild(itemsWrap);
|
|
3073
|
-
updateAddButton();
|
|
3074
|
-
|
|
3075
|
-
wrapper.appendChild(containerWrap);
|
|
3076
|
-
}
|
|
3077
|
-
|
|
3078
|
-
// Common file preview rendering function for readonly mode
|
|
3079
|
-
function renderFilePreviewReadonly(resourceId, fileName) {
|
|
3080
|
-
const meta = state.resourceIndex.get(resourceId);
|
|
3081
|
-
const actualFileName =
|
|
3082
|
-
fileName || meta?.name || resourceId.split("/").pop() || "file";
|
|
3083
|
-
|
|
3084
|
-
// Determine if this looks like a PSD file (should be treated as download, not preview)
|
|
3085
|
-
const isPSD = actualFileName.toLowerCase().match(/\.psd$/);
|
|
3086
|
-
|
|
3087
|
-
// Individual file result container
|
|
3088
|
-
const fileResult = document.createElement("div");
|
|
3089
|
-
fileResult.className = isPSD ? "space-y-2" : "space-y-3";
|
|
3090
|
-
|
|
3091
|
-
// Preview container - compact for PSD files, large for others
|
|
3092
|
-
const previewContainer = document.createElement("div");
|
|
3093
|
-
if (isPSD) {
|
|
3094
|
-
// Compact container for PSD files
|
|
3095
|
-
previewContainer.className =
|
|
3096
|
-
"bg-gray-100 rounded-lg overflow-hidden cursor-pointer hover:opacity-90 transition-opacity flex items-center p-3 max-w-sm";
|
|
3097
|
-
} else {
|
|
3098
|
-
// Large container for images/videos
|
|
3099
|
-
previewContainer.className =
|
|
3100
|
-
"bg-gray-100 rounded-lg overflow-hidden cursor-pointer hover:opacity-90 transition-opacity";
|
|
3101
|
-
}
|
|
3102
|
-
|
|
3103
|
-
// Determine if this looks like an image file (excluding PSD files)
|
|
3104
|
-
const isImage =
|
|
3105
|
-
!isPSD && (
|
|
3106
|
-
meta?.type?.startsWith("image/") ||
|
|
3107
|
-
actualFileName.toLowerCase().match(/\.(jpg|jpeg|png|gif|webp)$/)
|
|
3108
|
-
);
|
|
3109
|
-
|
|
3110
|
-
// Determine if this looks like a video file
|
|
3111
|
-
const isVideo =
|
|
3112
|
-
meta?.type?.startsWith("video/") ||
|
|
3113
|
-
actualFileName.toLowerCase().match(/\.(mp4|webm|avi|mov)$/);
|
|
3114
|
-
|
|
3115
|
-
if (isImage) {
|
|
3116
|
-
// Image preview - try getThumbnail first
|
|
3117
|
-
if (state.config.getThumbnail) {
|
|
3118
|
-
try {
|
|
3119
|
-
const thumbnailUrl = state.config.getThumbnail(resourceId);
|
|
3120
|
-
if (thumbnailUrl) {
|
|
3121
|
-
previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${actualFileName}" class="w-full h-auto">`;
|
|
3122
|
-
} else {
|
|
3123
|
-
// Fallback to icon if getThumbnail returns null/undefined
|
|
3124
|
-
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🖼️</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
3125
|
-
}
|
|
3126
|
-
} catch (error) {
|
|
3127
|
-
console.warn("getThumbnail failed for", resourceId, error);
|
|
3128
|
-
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🖼️</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
3129
|
-
}
|
|
3130
|
-
} else {
|
|
3131
|
-
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🖼️</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
3132
|
-
}
|
|
3133
|
-
} else if (isVideo) {
|
|
3134
|
-
// Video preview - try getThumbnail for video URL
|
|
3135
|
-
if (state.config.getThumbnail) {
|
|
3136
|
-
try {
|
|
3137
|
-
const videoUrl = state.config.getThumbnail(resourceId);
|
|
3138
|
-
if (videoUrl) {
|
|
3139
|
-
previewContainer.innerHTML = `
|
|
3140
|
-
<div class="relative group">
|
|
3141
|
-
<video class="w-full h-auto" controls preload="auto" muted>
|
|
3142
|
-
<source src="${videoUrl}" type="${meta?.type || "video/mp4"}">
|
|
3143
|
-
Ваш браузер не поддерживает видео.
|
|
3144
|
-
</video>
|
|
3145
|
-
<div class="absolute inset-0 bg-black bg-opacity-20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
|
|
3146
|
-
<div class="bg-white bg-opacity-90 rounded-full p-3">
|
|
3147
|
-
<svg class="w-8 h-8 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
|
|
3148
|
-
<path d="M8 5v14l11-7z"/>
|
|
3149
|
-
</svg>
|
|
3150
|
-
</div>
|
|
3151
|
-
</div>
|
|
3152
|
-
</div>
|
|
3153
|
-
`;
|
|
3154
|
-
} else {
|
|
3155
|
-
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🎥</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
3156
|
-
}
|
|
3157
|
-
} catch (error) {
|
|
3158
|
-
console.warn("getThumbnail failed for video", resourceId, error);
|
|
3159
|
-
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🎥</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
3160
|
-
}
|
|
3161
|
-
} else {
|
|
3162
|
-
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🎥</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
3163
|
-
}
|
|
3164
|
-
} else {
|
|
3165
|
-
// Other file types - special handling for PSD files
|
|
3166
|
-
const fileIcon = isPSD ? "🎨" : "📁";
|
|
3167
|
-
const fileDescription = isPSD ? "PSD File" : "Document";
|
|
3168
|
-
|
|
3169
|
-
if (isPSD) {
|
|
3170
|
-
// Compact horizontal layout for PSD files
|
|
3171
|
-
previewContainer.innerHTML = `
|
|
97
|
+
`:s.innerHTML=`<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${l}</div></div></div>`}catch(m){console.warn("getThumbnail failed for video",e,m),s.innerHTML=`<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${l}</div></div></div>`}else s.innerHTML=`<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">\u{1F3A5}</div><div class="text-sm">${l}</div></div></div>`;else{const m=a?"\u{1F3A8}":"\u{1F4C1}",v=a?"PSD File":"Document";a?s.innerHTML=`
|
|
3172
98
|
<div class="flex items-center space-x-3">
|
|
3173
|
-
<div class="text-3xl text-gray-400">${
|
|
99
|
+
<div class="text-3xl text-gray-400">${m}</div>
|
|
3174
100
|
<div class="flex-1 min-w-0">
|
|
3175
|
-
<div class="text-sm font-medium text-gray-900 truncate">${
|
|
3176
|
-
<div class="text-xs text-gray-500">${
|
|
101
|
+
<div class="text-sm font-medium text-gray-900 truncate">${l}</div>
|
|
102
|
+
<div class="text-xs text-gray-500">${v}</div>
|
|
3177
103
|
</div>
|
|
3178
104
|
</div>
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
const blobUrl = URL.createObjectURL(blob);
|
|
3255
|
-
const link = document.createElement("a");
|
|
3256
|
-
link.href = blobUrl;
|
|
3257
|
-
link.download = fileName;
|
|
3258
|
-
link.style.display = "none";
|
|
3259
|
-
|
|
3260
|
-
// Important: add to DOM, click, then remove
|
|
3261
|
-
document.body.appendChild(link);
|
|
3262
|
-
link.click();
|
|
3263
|
-
document.body.removeChild(link);
|
|
3264
|
-
|
|
3265
|
-
// Clean up blob URL after download
|
|
3266
|
-
setTimeout(() => {
|
|
3267
|
-
URL.revokeObjectURL(blobUrl);
|
|
3268
|
-
}, 100);
|
|
3269
|
-
} catch (error) {
|
|
3270
|
-
throw new Error(`Blob download failed: ${error.message}`);
|
|
3271
|
-
}
|
|
3272
|
-
}
|
|
3273
|
-
|
|
3274
|
-
// Public API
|
|
3275
|
-
function setFormRoot(element) {
|
|
3276
|
-
state.formRoot = element;
|
|
3277
|
-
}
|
|
3278
|
-
|
|
3279
|
-
function configure(config) {
|
|
3280
|
-
Object.assign(state.config, config);
|
|
3281
|
-
}
|
|
3282
|
-
|
|
3283
|
-
function setUploadHandler(uploadFn) {
|
|
3284
|
-
state.config.uploadFile = uploadFn;
|
|
3285
|
-
}
|
|
3286
|
-
|
|
3287
|
-
function setDownloadHandler(downloadFn) {
|
|
3288
|
-
state.config.downloadFile = downloadFn;
|
|
3289
|
-
}
|
|
3290
|
-
|
|
3291
|
-
function setThumbnailHandler(thumbnailFn) {
|
|
3292
|
-
state.config.getThumbnail = thumbnailFn;
|
|
3293
|
-
}
|
|
3294
|
-
|
|
3295
|
-
function setActionHandler(actionFn) {
|
|
3296
|
-
state.config.actionHandler = actionFn;
|
|
3297
|
-
}
|
|
3298
|
-
|
|
3299
|
-
function setMode(mode) {
|
|
3300
|
-
state.config.readonly = mode === "readonly";
|
|
3301
|
-
}
|
|
3302
|
-
|
|
3303
|
-
function setLocale(locale) {
|
|
3304
|
-
if (state.config.translations[locale]) {
|
|
3305
|
-
state.config.locale = locale;
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
|
-
function getFormData() {
|
|
3310
|
-
return validateForm(false);
|
|
3311
|
-
}
|
|
3312
|
-
|
|
3313
|
-
function submitForm() {
|
|
3314
|
-
const result = validateForm(false);
|
|
3315
|
-
if (result.valid) {
|
|
3316
|
-
// Emit event for successful submission
|
|
3317
|
-
if (typeof window !== "undefined" && window.parent) {
|
|
3318
|
-
window.parent.postMessage(
|
|
3319
|
-
{
|
|
3320
|
-
type: "formSubmit",
|
|
3321
|
-
data: result.data,
|
|
3322
|
-
schema: state.schema,
|
|
3323
|
-
},
|
|
3324
|
-
"*",
|
|
3325
|
-
);
|
|
3326
|
-
}
|
|
3327
|
-
}
|
|
3328
|
-
return result;
|
|
3329
|
-
}
|
|
3330
|
-
|
|
3331
|
-
function saveDraft() {
|
|
3332
|
-
const result = validateForm(true); // Skip validation for drafts
|
|
3333
|
-
// Emit event for draft save
|
|
3334
|
-
if (typeof window !== "undefined" && window.parent) {
|
|
3335
|
-
window.parent.postMessage(
|
|
3336
|
-
{
|
|
3337
|
-
type: "formDraft",
|
|
3338
|
-
data: result.data,
|
|
3339
|
-
schema: state.schema,
|
|
3340
|
-
},
|
|
3341
|
-
"*",
|
|
3342
|
-
);
|
|
3343
|
-
}
|
|
3344
|
-
return result;
|
|
3345
|
-
}
|
|
3346
|
-
|
|
3347
|
-
function clearForm() {
|
|
3348
|
-
if (state.formRoot) {
|
|
3349
|
-
clear(state.formRoot);
|
|
3350
|
-
}
|
|
3351
|
-
}
|
|
3352
|
-
|
|
3353
|
-
// Create the API object
|
|
3354
|
-
const formBuilderAPI = {
|
|
3355
|
-
setFormRoot,
|
|
3356
|
-
renderForm,
|
|
3357
|
-
configure,
|
|
3358
|
-
setUploadHandler,
|
|
3359
|
-
setDownloadHandler,
|
|
3360
|
-
setThumbnailHandler,
|
|
3361
|
-
setActionHandler,
|
|
3362
|
-
setMode,
|
|
3363
|
-
setLocale,
|
|
3364
|
-
getFormData,
|
|
3365
|
-
submitForm,
|
|
3366
|
-
saveDraft,
|
|
3367
|
-
clearForm,
|
|
3368
|
-
validateSchema,
|
|
3369
|
-
pretty,
|
|
3370
|
-
state,
|
|
3371
|
-
};
|
|
3372
|
-
|
|
3373
|
-
// Browser global export (existing behavior)
|
|
3374
|
-
if (typeof window !== "undefined") {
|
|
3375
|
-
window.FormBuilder = formBuilderAPI;
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
|
-
// ES6 Module exports for modern bundlers
|
|
3379
|
-
export {
|
|
3380
|
-
setFormRoot,
|
|
3381
|
-
renderForm,
|
|
3382
|
-
configure,
|
|
3383
|
-
setUploadHandler,
|
|
3384
|
-
setDownloadHandler,
|
|
3385
|
-
setThumbnailHandler,
|
|
3386
|
-
setActionHandler,
|
|
3387
|
-
setMode,
|
|
3388
|
-
setLocale,
|
|
3389
|
-
getFormData,
|
|
3390
|
-
submitForm,
|
|
3391
|
-
saveDraft,
|
|
3392
|
-
clearForm,
|
|
3393
|
-
validateSchema,
|
|
3394
|
-
pretty,
|
|
3395
|
-
state,
|
|
3396
|
-
};
|
|
3397
|
-
|
|
3398
|
-
// Default export
|
|
3399
|
-
export default formBuilderAPI;
|
|
105
|
+
`:s.innerHTML=`<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">${m}</div><div class="text-sm">${l}</div><div class="text-xs text-gray-500 mt-1">${v}</div></div></div>`}const h=document.createElement("p");h.className=a?"hidden":"text-sm font-medium text-gray-900 text-center",h.textContent=l;const p=document.createElement("button");return p.className="w-full px-3 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors",p.textContent=$("downloadButton",t),p.onclick=m=>{m.preventDefault(),m.stopPropagation(),t.config.downloadFile?t.config.downloadFile(e,l):Se(e,l,t)},i.appendChild(s),i.appendChild(h),i.appendChild(p),i}function P(e,t,n,o){N(e);const c=!e.classList.contains("grid");if((!t||t.length===0)&&c){const a=document.createElement("div");a.className="grid grid-cols-4 gap-3 mb-3";for(let u=0;u<4;u++){const d=document.createElement("div");d.className="aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors";const h=document.createElementNS("http://www.w3.org/2000/svg","svg");h.setAttribute("class","w-12 h-12 text-gray-400"),h.setAttribute("fill","currentColor"),h.setAttribute("viewBox","0 0 24 24");const p=document.createElementNS("http://www.w3.org/2000/svg","path");p.setAttribute("d","M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"),h.appendChild(p),d.appendChild(h),d.onclick=()=>{let m=e.parentElement;for(;m&&!m.classList.contains("space-y-2");)m=m.parentElement;!m&&e.classList.contains("space-y-2")&&(m=e);const v=m==null?void 0:m.querySelector('input[type="file"]');v&&v.click()},a.appendChild(d)}const i=document.createElement("div");i.className="text-center text-xs text-gray-600";const s=document.createElement("span");s.className="underline cursor-pointer",s.textContent=$("uploadText",n),s.onclick=u=>{u.stopPropagation();let d=e.parentElement;for(;d&&!d.classList.contains("space-y-2");)d=d.parentElement;!d&&e.classList.contains("space-y-2")&&(d=e);const h=d==null?void 0:d.querySelector('input[type="file"]');h&&h.click()},i.appendChild(s),i.appendChild(document.createTextNode(` ${$("dragDropText",n)}`)),e.appendChild(a),e.appendChild(i);return}e.className="files-list grid grid-cols-4 gap-3 mt-2";const r=t?t.length:0,l=(Math.floor(r/4)+1)*4;for(let a=0;a<l;a++){const i=document.createElement("div");if(t&&a<t.length){const s=t[a],u=n.resourceIndex.get(s);if(i.className="resource-pill aspect-square bg-gray-100 rounded-lg overflow-hidden relative group border border-gray-300",i.dataset.resourceId=s,Fe(i,s,u,n).catch(d=>{console.error("Failed to render thumbnail:",d),i.innerHTML=`<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
106
|
+
<div class="text-2xl mb-1">\u{1F4C1}</div>
|
|
107
|
+
<div class="text-xs">Preview error</div>
|
|
108
|
+
</div>`}),o){const d=document.createElement("div");d.className="absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center";const h=document.createElement("button");h.className="bg-red-600 text-white px-2 py-1 rounded text-xs",h.textContent=$("removeElement",n),h.onclick=p=>{p.stopPropagation(),o(s)},d.appendChild(h),i.appendChild(d)}}else i.className="aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors",i.innerHTML='<svg class="w-12 h-12 text-gray-400" fill="currentColor" viewBox="0 0 24 24"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>',i.onclick=()=>{let s=e.parentElement;for(;s&&!s.classList.contains("space-y-2");)s=s.parentElement;!s&&e.classList.contains("space-y-2")&&(s=e);const u=s==null?void 0:s.querySelector('input[type="file"]');u&&u.click()};e.appendChild(i)}}async function Fe(e,t,n,o){var c,r;if(n&&(c=n.type)!=null&&c.startsWith("image/"))if(n.file&&n.file instanceof File){const l=document.createElement("img");l.className="w-full h-full object-contain",l.alt=n.name;const a=new FileReader;a.onload=i=>{var s;l.src=((s=i.target)==null?void 0:s.result)||""},a.readAsDataURL(n.file),e.appendChild(l)}else if(o.config.getThumbnail){const l=await o.config.getThumbnail(t);if(l){const a=document.createElement("img");a.className="w-full h-full object-contain",a.alt=n.name,a.src=l,e.appendChild(a)}else e.innerHTML=`<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
109
|
+
<svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
|
110
|
+
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
111
|
+
</svg>
|
|
112
|
+
</div>`}else e.innerHTML=`<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
113
|
+
<svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
|
114
|
+
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
115
|
+
</svg>
|
|
116
|
+
</div>`;else if(n&&(r=n.type)!=null&&r.startsWith("video/"))if(n.file&&n.file instanceof File){const l=URL.createObjectURL(n.file);e.innerHTML=`
|
|
117
|
+
<div class="relative group h-full w-full">
|
|
118
|
+
<video class="w-full h-full object-contain" preload="metadata" muted>
|
|
119
|
+
<source src="${l}" type="${n.type}">
|
|
120
|
+
</video>
|
|
121
|
+
<div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
|
122
|
+
<div class="bg-white bg-opacity-90 rounded-full p-1">
|
|
123
|
+
<svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
|
|
124
|
+
<path d="M8 5v14l11-7z"/>
|
|
125
|
+
</svg>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
`}else if(o.config.getThumbnail){const l=await o.config.getThumbnail(t);l?e.innerHTML=`
|
|
130
|
+
<div class="relative group h-full w-full">
|
|
131
|
+
<video class="w-full h-full object-contain" preload="metadata" muted>
|
|
132
|
+
<source src="${l}" type="${n.type}">
|
|
133
|
+
</video>
|
|
134
|
+
<div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
|
135
|
+
<div class="bg-white bg-opacity-90 rounded-full p-1">
|
|
136
|
+
<svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
|
|
137
|
+
<path d="M8 5v14l11-7z"/>
|
|
138
|
+
</svg>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
`:e.innerHTML=`<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
143
|
+
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
|
144
|
+
<path d="M8 5v14l11-7z"/>
|
|
145
|
+
</svg>
|
|
146
|
+
<div class="text-xs mt-1">${(n==null?void 0:n.name)||"Video"}</div>
|
|
147
|
+
</div>`}else e.innerHTML=`<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
148
|
+
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
|
149
|
+
<path d="M8 5v14l11-7z"/>
|
|
150
|
+
</svg>
|
|
151
|
+
<div class="text-xs mt-1">${(n==null?void 0:n.name)||"Video"}</div>
|
|
152
|
+
</div>`;else e.innerHTML=`<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
153
|
+
<div class="text-2xl mb-1">\u{1F4C1}</div>
|
|
154
|
+
<div class="text-xs">${(n==null?void 0:n.name)||"File"}</div>
|
|
155
|
+
</div>`}function j(e,t){e.innerHTML=`
|
|
156
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
157
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
158
|
+
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
159
|
+
</svg>
|
|
160
|
+
<div class="text-sm text-center">${$("clickDragText",t)}</div>
|
|
161
|
+
</div>
|
|
162
|
+
`}async function V(e,t,n,o,c=null,r){var l,a;let i;if(o.config.uploadFile)try{if(i=await o.config.uploadFile(e),typeof i!="string")throw new Error("Upload handler must return a string resource ID")}catch(u){throw new Error(`File upload failed: ${u.message}`)}else throw new Error("No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()");o.resourceIndex.set(i,{name:e.name,type:e.type,size:e.size,uploadedAt:new Date,file:e});let s=(l=t.parentElement)==null?void 0:l.querySelector('input[type="hidden"]');s||(s=document.createElement("input"),s.type="hidden",s.name=n,(a=t.parentElement)==null||a.appendChild(s)),s.value=i,U(t,i,o,{fileName:e.name,isReadonly:!1,deps:c}).catch(console.error),r&&!o.config.readonly&&r.triggerOnChange(n,i)}function z(e,t){e.addEventListener("dragover",n=>{n.preventDefault(),e.classList.add("border-blue-500","bg-blue-50")}),e.addEventListener("dragleave",n=>{n.preventDefault(),e.classList.remove("border-blue-500","bg-blue-50")}),e.addEventListener("drop",n=>{var o;n.preventDefault(),e.classList.remove("border-blue-500","bg-blue-50"),(o=n.dataTransfer)!=null&&o.files&&t(n.dataTransfer.files)})}function Le(e,t,n){const o=e.querySelector(".delete-overlay");o&&o.remove();const c=document.createElement("div");c.className="delete-overlay absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center";const r=document.createElement("button");r.className="bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700 transition-colors",r.textContent=$("removeElement",t),r.onclick=l=>{l.stopPropagation(),n()},c.appendChild(r),e.appendChild(c)}async function W(e,t){if(t.config.uploadFile)try{const n=await t.config.uploadFile(e);if(typeof n!="string")throw new Error("Upload handler must return a string resource ID");return n}catch(n){throw new Error(`File upload failed: ${n.message}`)}else throw new Error("No upload handler configured. Set uploadHandler via FormBuilder.setUploadHandler()")}async function Se(e,t,n){let o=null;if(n.config.getDownloadUrl?o=n.config.getDownloadUrl(e):n.config.getThumbnail&&(o=await n.config.getThumbnail(e)),o){const c=o.startsWith("http")?o:new URL(o,window.location.href).href;fetch(c).then(r=>{if(!r.ok)throw new Error(`HTTP error! status: ${r.status}`);return r.blob()}).then(r=>{Ne(r,t)}).catch(r=>{throw new Error(`File download failed: ${r.message}`)})}else console.warn("No download URL available for resource:",e)}function Ne(e,t){try{const n=URL.createObjectURL(e),o=document.createElement("a");o.href=n,o.download=t,o.style.display="none",document.body.appendChild(o),o.click(),document.body.removeChild(o),setTimeout(()=>{URL.revokeObjectURL(n)},100)}catch(n){throw new Error(`Blob download failed: ${n.message}`)}}function _(e,t){e.length>0&&e.forEach(n=>{var o;if(!t.resourceIndex.has(n)){const c=n.split("/").pop()||"file",r=(o=c.split(".").pop())==null?void 0:o.toLowerCase(),l=r&&["jpg","jpeg","png","gif","webp"].includes(r)?`image/${r==="jpg"?"jpeg":r}`:"application/octet-stream";t.resourceIndex.set(n,{name:c,type:l,size:0,uploadedAt:new Date,file:void 0})}})}function Ae(e,t,n,o,c,r){var l;if(!c.resourceIndex.has(e)){const i=e.split("/").pop()||"file",s=(l=i.split(".").pop())==null?void 0:l.toLowerCase();let u="application/octet-stream";s&&(["jpg","jpeg","png","gif","webp"].includes(s)?u=`image/${s==="jpg"?"jpeg":s}`:["mp4","webm","mov","avi"].includes(s)&&(u=`video/${s==="mov"?"quicktime":s}`)),c.resourceIndex.set(e,{name:i,type:u,size:0,uploadedAt:new Date,file:void 0})}U(t,e,c,{fileName:e,isReadonly:!1,deps:r}).catch(console.error);const a=document.createElement("input");a.type="hidden",a.name=n,a.value=e,o.appendChild(a)}function G(e,t,n,o,c,r){z(e,async l=>{const a=Array.from(l);for(const i of a){const s=await W(i,n);n.resourceIndex.set(s,{name:i.name,type:i.type,size:i.size,uploadedAt:new Date,file:void 0}),t.push(s)}o(),r&&c&&!n.config.readonly&&r.triggerOnChange(c,t)})}function Y(e,t,n,o,c,r){e.onchange=async()=>{if(e.files)for(const l of Array.from(e.files)){const a=await W(l,n);n.resourceIndex.set(a,{name:l.name,type:l.type,size:l.size,uploadedAt:new Date,file:void 0}),t.push(a)}o(),e.value="",r&&c&&!n.config.readonly&&r.triggerOnChange(c,t)}}function Te(e,t,n,o){var c;const r=t.state;if(r.config.readonly){const l=t.prefill[e.key];if(l)B(l,r).then(a=>{n.appendChild(a)}).catch(a=>{console.error("Failed to render file preview:",a);const i=document.createElement("div");i.className="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500",i.innerHTML='<div class="text-center">Preview unavailable</div>',n.appendChild(i)});else{const a=document.createElement("div");a.className="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500",a.innerHTML=`<div class="text-center">${$("noFileSelected",r)}</div>`,n.appendChild(a)}}else{const l=document.createElement("div");l.className="space-y-2";const a=document.createElement("input");a.type="file",a.name=o,a.style.display="none",e.accept&&(a.accept=typeof e.accept=="string"?e.accept:((c=e.accept.extensions)==null?void 0:c.map(v=>`.${v}`).join(","))||"");const i=document.createElement("div");i.className="file-preview-container w-full aspect-square max-w-xs bg-gray-100 rounded-lg overflow-hidden relative group cursor-pointer";const s=t.prefill[e.key],u=()=>a.click(),d=v=>{if(v.length>0){const b={picker:a,fileUploadHandler:u,dragHandler:d};V(v[0],i,o,r,b,t.instance)}};s?Ae(s,i,o,l,r,{picker:a,fileUploadHandler:u,dragHandler:d}):j(i,r),i.onclick=u,z(i,d),a.onchange=()=>{if(a.files&&a.files.length>0){const v={picker:a,fileUploadHandler:u,dragHandler:d};V(a.files[0],i,o,r,v,t.instance)}},l.appendChild(i),l.appendChild(a);const h=document.createElement("p");h.className="text-xs text-gray-600 mt-2 text-center",h.innerHTML=`<span class="underline cursor-pointer">${$("uploadText",r)}</span> ${$("dragDropTextSingle",r)}`;const p=h.querySelector("span");p&&(p.onclick=()=>a.click()),l.appendChild(h);const m=document.createElement("p");m.className="text-xs text-gray-500 mt-1 text-center",m.textContent=k(e),l.appendChild(m),n.appendChild(l)}}function qe(e,t,n,o){var c;const r=t.state;if(r.config.readonly){const l=document.createElement("div");l.className="space-y-4";const a=t.prefill[e.key]||[];a.length>0?a.forEach(i=>{B(i,r).then(s=>{l.appendChild(s)}).catch(s=>{console.error("Failed to render file preview:",s)})}):l.innerHTML=`<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${$("noFilesSelected",r)}</div></div>`,n.appendChild(l)}else{let l=function(){P(u,d,r,p=>{const m=d.indexOf(p);m>-1&&d.splice(m,1),l()})};const a=document.createElement("div");a.className="space-y-2";const i=document.createElement("input");i.type="file",i.name=o,i.multiple=!0,i.style.display="none",e.accept&&(i.accept=typeof e.accept=="string"?e.accept:((c=e.accept.extensions)==null?void 0:c.map(p=>`.${p}`).join(","))||"");const s=document.createElement("div");s.className="border-2 border-dashed border-gray-300 rounded-lg p-3 hover:border-gray-400 transition-colors";const u=document.createElement("div");u.className="files-list";const d=t.prefill[e.key]||[];_(d,r),l(),G(s,d,r,l,o,t.instance),Y(i,d,r,l,o,t.instance),s.appendChild(u),a.appendChild(s),a.appendChild(i);const h=document.createElement("p");h.className="text-xs text-gray-500 mt-1 text-center",h.textContent=k(e),a.appendChild(h),n.appendChild(a)}}function He(e,t,n,o){var c,r,l;const a=t.state,i=(c=e.minCount)!=null?c:0,s=(r=e.maxCount)!=null?r:1/0;if(a.config.readonly){const u=document.createElement("div");u.className="space-y-4";const d=t.prefill[e.key]||[];d.length>0?d.forEach(h=>{B(h,a).then(p=>{u.appendChild(p)}).catch(p=>{console.error("Failed to render file preview:",p)})}):u.innerHTML=`<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${$("noFilesSelected",a)}</div></div>`,n.appendChild(u)}else{const u=document.createElement("div");u.className="space-y-2";const d=document.createElement("input");d.type="file",d.name=o,d.multiple=!0,d.style.display="none",e.accept&&(d.accept=typeof e.accept=="string"?e.accept:((l=e.accept.extensions)==null?void 0:l.map(v=>`.${v}`).join(","))||"");const h=document.createElement("div");h.className="files-list space-y-2",u.appendChild(d),u.appendChild(h);const p=Array.isArray(t.prefill[e.key])?[...t.prefill[e.key]]:[];_(p,a);const m=()=>{P(h,p,a,y=>{p.splice(p.indexOf(y),1),m()});const v=document.createElement("div");v.className="text-xs text-gray-500 mt-2 file-count-info";const b=`${p.length} file${p.length!==1?"s":""}`,x=i>0||s<1/0?` (${i}-${s} allowed)`:"";v.textContent=b+x;const g=u.querySelector(".file-count-info");g&&g.remove(),u.appendChild(v)};G(h,p,a,m,o,t.instance),Y(d,p,a,m,o,t.instance),m(),n.appendChild(u)}}function J(e,t,n){var o;const c=[],{scopeRoot:r,skipValidation:l,path:a}=n,i=(s,u,d)=>{var h,p;if(l)return;const m="minCount"in d&&(h=d.minCount)!=null?h:0,v="maxCount"in d&&(p=d.maxCount)!=null?p:1/0;d.required&&u.length===0&&c.push(`${s}: required`),u.length<m&&c.push(`${s}: minimum ${m} files required`),u.length>v&&c.push(`${s}: maximum ${v} files allowed`)};if("multiple"in e&&e.multiple){const s=L(a,t),u=r.querySelector(`input[type="file"][name="${s}"]`),d=u==null?void 0:u.closest(".space-y-2"),h=(d==null?void 0:d.querySelector(".files-list"))||null,p=[];return h&&h.querySelectorAll(".resource-pill").forEach(m=>{const v=m.dataset.resourceId;v&&p.push(v)}),i(t,p,e),{value:p,errors:c}}else{const s=r.querySelector(`input[name$="${t}"][type="hidden"]`),u=(o=s==null?void 0:s.value)!=null?o:"";return!l&&e.required&&u===""?(c.push(`${t}: required`),{value:null,errors:c}):{value:u||null,errors:c}}}function X(e,t,n,o){var c;const{scopeRoot:r,state:l}=o;if("multiple"in e&&e.multiple){if(!Array.isArray(n)){console.warn(`updateFileField: Expected array for multiple file field "${t}", got ${typeof n}`);return}n.forEach(a=>{var i;if(a&&typeof a=="string"&&!l.resourceIndex.has(a)){const s=a.split("/").pop()||"file",u=(i=s.split(".").pop())==null?void 0:i.toLowerCase();let d="application/octet-stream";u&&(["jpg","jpeg","png","gif","webp"].includes(u)?d=`image/${u==="jpg"?"jpeg":u}`:["mp4","webm","mov","avi"].includes(u)&&(d=`video/${u==="mov"?"quicktime":u}`)),l.resourceIndex.set(a,{name:s,type:d,size:0,uploadedAt:new Date,file:void 0})}}),console.info(`updateFileField: Multiple file field "${t}" updated. Preview update requires re-render.`)}else{const a=r.querySelector(`input[name="${t}"][type="hidden"]`);if(!a){console.warn(`updateFileField: Hidden input not found for file field "${t}"`);return}if(a.value=n!=null?String(n):"",n&&typeof n=="string"){if(!l.resourceIndex.has(n)){const i=n.split("/").pop()||"file",s=(c=i.split(".").pop())==null?void 0:c.toLowerCase();let u="application/octet-stream";s&&(["jpg","jpeg","png","gif","webp"].includes(s)?u=`image/${s==="jpg"?"jpeg":s}`:["mp4","webm","mov","avi"].includes(s)&&(u=`video/${s==="mov"?"quicktime":s}`)),l.resourceIndex.set(n,{name:i,type:u,size:0,uploadedAt:new Date,file:void 0})}console.info(`updateFileField: File field "${t}" updated. Preview update requires re-render.`)}}}let R=null;function Me(e){R=e}function q(e,t){if(!R)throw new Error("renderElement not initialized. Import from components/index.ts");return R(e,t)}function Z(e,t,n,o){var c;const r=document.createElement("div");r.className="border border-gray-200 rounded-lg p-4 bg-gray-50",r.setAttribute("data-container",o);const l=document.createElement("div");l.className="flex justify-between items-center mb-4";const a=document.createElement("div");a.className="flex-1";const i=document.createElement("div");i.className="space-y-4",r.appendChild(l),l.appendChild(a);const s={path:L(t.path,e.key),prefill:((c=t.prefill)==null?void 0:c[e.key])||{},state:t.state};e.elements.forEach(u=>{u.hidden||i.appendChild(q(u,s))}),r.appendChild(i),a.innerHTML=`<span>${e.label||e.key}</span>`,n.appendChild(r)}function K(e,t,n,o){var c,r,l;const a=t.state,i=document.createElement("div");i.className="border border-gray-200 rounded-lg p-4 bg-gray-50";const s=document.createElement("div");s.className="flex justify-between items-center mb-4";const u=document.createElement("div");u.className="flex-1";const d=document.createElement("div");d.className="flex gap-2";const h=document.createElement("div");h.className="space-y-4",i.appendChild(s),s.appendChild(u),a.config.readonly||s.appendChild(d);const p=(c=e.minCount)!=null?c:0,m=(r=e.maxCount)!=null?r:1/0,v=Array.isArray((l=t.prefill)==null?void 0:l[e.key])?t.prefill[e.key]:null,b=()=>h.querySelectorAll(":scope > .containerItem").length,x=()=>{const y=document.createElement("button");return y.type="button",y.className="px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors",y.textContent=$("addElement",a),y.onclick=()=>{if(b()<m){const f=b(),C={state:t.state,path:L(t.path,`${e.key}[${f}]`),prefill:{}},E=document.createElement("div");if(E.className="containerItem border border-gray-300 rounded-lg p-4 bg-white",E.setAttribute("data-container-item",`${e.key}[${f}]`),e.elements.forEach(w=>{w.hidden||E.appendChild(q(w,C))}),!a.config.readonly){const w=document.createElement("button");w.type="button",w.className="absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors",w.textContent="\xD7",w.onclick=()=>{E.remove(),g()},E.style.position="relative",E.appendChild(w)}h.appendChild(E),g()}},y},g=()=>{const y=b(),f=d.querySelector("button");f&&(f.disabled=y>=m,f.style.opacity=y>=m?"0.5":"1"),u.innerHTML=`<span>${e.label||e.key}</span> <span class="text-sm text-gray-500">(${y}/${m===1/0?"\u221E":m})</span>`};if(a.config.readonly||d.appendChild(x()),v&&Array.isArray(v)&&v.forEach((y,f)=>{const C={state:t.state,path:L(t.path,`${e.key}[${f}]`),prefill:y||{}},E=document.createElement("div");if(E.className="containerItem border border-gray-300 rounded-lg p-4 bg-white",E.setAttribute("data-container-item",`${e.key}[${f}]`),e.elements.forEach(w=>{w.hidden||E.appendChild(q(w,C))}),!a.config.readonly){const w=document.createElement("button");w.type="button",w.className="absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors",w.textContent="\xD7",w.onclick=()=>{E.remove(),g()},E.style.position="relative",E.appendChild(w)}h.appendChild(E)}),!a.config.readonly)for(;b()<p;){const y=b(),f={state:t.state,path:L(t.path,`${e.key}[${y}]`),prefill:{}},C=document.createElement("div");C.className="containerItem border border-gray-300 rounded-lg p-4 bg-white",C.setAttribute("data-container-item",`${e.key}[${y}]`),e.elements.forEach(w=>{w.hidden||C.appendChild(q(w,f))});const E=document.createElement("button");E.type="button",E.className="absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors",E.textContent="\xD7",E.onclick=()=>{b()>p&&(C.remove(),g())},C.style.position="relative",C.appendChild(E),h.appendChild(C)}i.appendChild(h),g(),n.appendChild(i)}let D=null;function Be(e){D=e}function Q(e,t,n){if(!D)throw new Error("validateElement not initialized. Should be set from FormBuilderInstance");return D(e,t,n)}function ee(e,t,n){const o=[],{scopeRoot:c,skipValidation:r,path:l}=n;if(!("elements"in e))return{value:null,errors:o};const a=(i,s,u)=>{var d,h;if(r)return;const p="minCount"in u&&(d=u.minCount)!=null?d:0,m="maxCount"in u&&(h=u.maxCount)!=null?h:1/0;u.required&&s.length===0&&o.push(`${i}: required`),s.length<p&&o.push(`${i}: minimum ${p} items required`),s.length>m&&o.push(`${i}: maximum ${m} items allowed`)};if("multiple"in e&&e.multiple){const i=[],s=c.querySelectorAll("[data-container-item]"),u=Array.from(s).filter(d=>{const h=d.getAttribute("data-container-item");return h==null?void 0:h.startsWith(`${t}[`)}).length;for(let d=0;d<u;d++){const h={},p=c.querySelector(`[data-container-item="${t}[${d}]"]`)||c;e.elements.forEach(m=>{if(m.hidden||m.type==="hidden")h[m.key]=m.default!==void 0?m.default:null;else{const v=`${t}[${d}].${m.key}`;h[m.key]=Q({...m,key:v},{path:l},p)}}),i.push(h)}return a(t,i,e),{value:i,errors:o}}else{const i={},s=c.querySelector(`[data-container="${t}"]`)||c;return e.elements.forEach(u=>{if(u.hidden||u.type==="hidden")i[u.key]=u.default!==void 0?u.default:null;else{const d=`${t}.${u.key}`;i[u.key]=Q({...u,key:d},{path:l},s)}}),{value:i,errors:o}}}function te(e,t,n,o){const{instance:c,scopeRoot:r}=o;if("elements"in e)if("multiple"in e&&e.multiple){if(!Array.isArray(n)){console.warn(`updateContainerField: Expected array for multiple container field "${t}", got ${typeof n}`);return}n.forEach((a,i)=>{T(a)&&e.elements.forEach(s=>{const u=s.key,d=`${t}[${i}].${u}`,h=a[u];h!==void 0&&c.updateField(d,h)})});const l=r.querySelectorAll(`[data-container-item^="${t}["]`);n.length!==l.length&&console.warn(`updateContainerField: Multiple container field "${t}" item count mismatch. Consider re-rendering for add/remove.`)}else{if(!T(n)){console.warn(`updateContainerField: Expected object for container field "${t}", got ${typeof n}`);return}e.elements.forEach(l=>{const a=l.key,i=`${t}.${a}`,s=n[a];s!==void 0&&c.updateField(i,s)})}}function je(e,t,n,o){var c,r;typeof console!="undefined"&&console.warn&&console.warn(`[Form Builder] The "group" field type is deprecated and will be removed in a future version. Please use type: "container" with multiple: true instead. Field key: "${e.key}"`);const l={key:e.key,label:e.label,description:e.description,hint:e.hint,required:e.required,hidden:e.hidden,default:e.default,actions:e.actions,elements:e.elements,multiple:!!(e.repeat&&T(e.repeat)),minCount:(c=e.repeat)==null?void 0:c.min,maxCount:(r=e.repeat)==null?void 0:r.max};l.multiple?K(l,t,n):Z(l,t,n,o)}function ne(e){var t,n;const o=e;return{type:"container",key:o.key,label:o.label,description:o.description,hint:o.hint,required:o.required,hidden:o.hidden,default:o.default,actions:o.actions,elements:o.elements,multiple:!!(o.repeat&&T(o.repeat)),minCount:(t=o.repeat)==null?void 0:t.min,maxCount:(n=o.repeat)==null?void 0:n.max}}function ze(e,t,n){typeof console!="undefined"&&console.warn&&console.warn(`[Form Builder] The "group" field type is deprecated. Please use type: "container" instead. Field key: "${t}"`);const o=ne(e);return ee(o,t,n)}function Re(e,t,n,o){typeof console!="undefined"&&console.warn&&console.warn(`[Form Builder] The "group" field type is deprecated. Please use type: "container" instead. Field path: "${t}"`);const c=ne(e);return te(c,t,n,o)}function De(e,t){const n=document.getElementById(e);if(!n)return;const o=!n.classList.contains("hidden");if(document.querySelectorAll('[id^="tooltip-"]').forEach(u=>{u.classList.add("hidden")}),o)return;const c=t.getBoundingClientRect(),r=window.innerWidth,l=window.innerHeight;n&&n.parentElement!==document.body&&document.body.appendChild(n),n.style.visibility="hidden",n.style.position="fixed",n.classList.remove("hidden");const a=n.getBoundingClientRect();n.classList.add("hidden"),n.style.visibility="visible";let i=c.left,s=c.bottom+5;i+a.width>r&&(i=c.right-a.width),s+a.height>l&&(s=c.top-a.height-5),i<10&&(i=10),s<10&&(s=c.bottom+5),n.style.left=`${i}px`,n.style.top=`${s}px`,n.classList.remove("hidden"),setTimeout(()=>{n.classList.add("hidden")},25e3)}typeof document!="undefined"&&document.addEventListener("click",e=>{const t=e.target,n=t.closest("button")&&t.closest("button").onclick,o=t.closest('[id^="tooltip-"]');!n&&!o&&document.querySelectorAll('[id^="tooltip-"]').forEach(c=>{c.classList.add("hidden")})});function re(e,t){const n=document.createElement("div");n.className="mb-6 fb-field-wrapper";const o=document.createElement("div");o.className="flex items-center mb-2";const c=document.createElement("label");if(c.className="text-sm font-medium text-gray-900",c.textContent=e.label||e.key,e.required){const l=document.createElement("span");l.className="text-red-500 ml-1",l.textContent="*",c.appendChild(l)}if(o.appendChild(c),e.description||e.hint){const l=document.createElement("button");l.type="button",l.className="ml-2 text-gray-400 hover:text-gray-600",l.innerHTML='<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>';const a=`tooltip-${e.key}-${Math.random().toString(36).substr(2,9)}`,i=document.createElement("div");i.id=a,i.className="hidden absolute z-50 bg-gray-200 text-gray-900 text-sm rounded-lg p-3 max-w-sm border border-gray-300 shadow-lg",i.style.position="fixed",i.textContent=e.description||e.hint||"Field information",document.body.appendChild(i),l.onclick=s=>{s.preventDefault(),s.stopPropagation(),De(a,l)},o.appendChild(l)}n.appendChild(o);const r=L(t.path,e.key);switch(e.type){case"text":"multiple"in e&&e.multiple?pe(e,t,n,r):me(e,t,n,r);break;case"textarea":"multiple"in e&&e.multiple?he(e,t,n,r):fe(e,t,n,r);break;case"number":"multiple"in e&&e.multiple?be(e,t,n,r):ye(e,t,n,r);break;case"select":"multiple"in e&&e.multiple?we(e,t,n,r):Ee(e,t,n,r);break;case"file":"multiple"in e&&e.multiple?He(e,t,n,r):Te(e,t,n,r);break;case"files":qe(e,t,n,r);break;case"group":je(e,t,n,r);break;case"container":"multiple"in e&&e.multiple?K(e,t,n):Z(e,t,n,r);break;default:{const l=document.createElement("div");l.className="text-red-500 text-sm",l.textContent=`Unsupported field type: ${e.type}`,n.appendChild(l)}}return n}Me(re);const Ie={uploadFile:null,downloadFile:null,getThumbnail:null,getDownloadUrl:null,actionHandler:null,onChange:null,onFieldChange:null,debounceMs:300,enableFilePreview:!0,maxPreviewSize:"200px",readonly:!1,locale:"en",translations:{en:{addElement:"Add Element",removeElement:"Remove",uploadText:"Upload",dragDropText:"or drag and drop files",dragDropTextSingle:"or drag and drop file",clickDragText:"Click or drag file",noFileSelected:"No file selected",noFilesSelected:"No files selected",downloadButton:"Download"},ru:{addElement:"\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u044D\u043B\u0435\u043C\u0435\u043D\u0442",removeElement:"\u0423\u0434\u0430\u043B\u0438\u0442\u044C",uploadText:"\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u0435",dragDropText:"\u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B\u044B",dragDropTextSingle:"\u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B",clickDragText:"\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B",noFileSelected:"\u0424\u0430\u0439\u043B \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D",noFilesSelected:"\u041D\u0435\u0442 \u0444\u0430\u0439\u043B\u043E\u0432",downloadButton:"\u0421\u043A\u0430\u0447\u0430\u0442\u044C"}},theme:{}};function Oe(e){return{schema:null,formRoot:null,resourceIndex:new Map,externalActions:null,version:"1.0.0",config:{...Ie,...e},debounceTimer:null}}function Ue(){const e=Date.now().toString(36),t=Math.random().toString(36).substring(2,9);return`inst-${e}-${t}`}const A={primaryColor:"#3b82f6",primaryHoverColor:"#2563eb",errorColor:"#ef4444",errorHoverColor:"#dc2626",successColor:"#10b981",borderColor:"#d1d5db",borderHoverColor:"#9ca3af",borderFocusColor:"#3b82f6",backgroundColor:"#ffffff",backgroundHoverColor:"#f9fafb",backgroundReadonlyColor:"#f3f4f6",textColor:"#1f2937",textSecondaryColor:"#6b7280",textPlaceholderColor:"#9ca3af",textDisabledColor:"#d1d5db",buttonBgColor:"#3b82f6",buttonTextColor:"#ffffff",buttonBorderColor:"#2563eb",buttonHoverBgColor:"#2563eb",buttonHoverBorderColor:"#1d4ed8",actionBgColor:"#ffffff",actionTextColor:"#374151",actionBorderColor:"#e5e7eb",actionHoverBgColor:"#f9fafb",actionHoverBorderColor:"#d1d5db",fileUploadBgColor:"#f3f4f6",fileUploadBorderColor:"#d1d5db",fileUploadTextColor:"#9ca3af",fileUploadHoverBorderColor:"#3b82f6",inputPaddingX:"0.75rem",inputPaddingY:"0.5rem",borderRadius:"0.5rem",borderWidth:"1px",fontSize:"0.875rem",fontSizeSmall:"0.75rem",fontSizeExtraSmall:"0.625rem",fontFamily:'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',fontWeightNormal:"400",fontWeightMedium:"500",focusRingWidth:"2px",focusRingColor:"#3b82f6",focusRingOpacity:"0.5",transitionDuration:"200ms"};function Pe(e){const t={...A,...e},n=[];return Object.entries(t).forEach(([o,c])=>{const r=o.replace(/([A-Z])/g,"-$1").toLowerCase();n.push(` --fb-${r}: ${c};`)}),n.join(`
|
|
163
|
+
`)}function Ve(e,t){const n=Pe(t);let o=e.querySelector("style[data-fb-theme]");o||(o=document.createElement("style"),o.setAttribute("data-fb-theme","true"),e.appendChild(o)),o.textContent=`
|
|
164
|
+
[data-fb-root="true"] {
|
|
165
|
+
${n}
|
|
166
|
+
}
|
|
167
|
+
`}const We={default:A,dark:{...A,primaryColor:"#60a5fa",primaryHoverColor:"#3b82f6",borderColor:"#4b5563",borderHoverColor:"#6b7280",borderFocusColor:"#60a5fa",backgroundColor:"#1f2937",backgroundHoverColor:"#374151",backgroundReadonlyColor:"#111827",textColor:"#f9fafb",textSecondaryColor:"#9ca3af",textPlaceholderColor:"#6b7280",fileUploadBgColor:"#374151",fileUploadBorderColor:"#4b5563",fileUploadTextColor:"#9ca3af"},klein:{...A,primaryColor:"#0066cc",primaryHoverColor:"#0052a3",errorColor:"#d32f2f",errorHoverColor:"#c62828",successColor:"#388e3c",borderColor:"#e0e0e0",borderHoverColor:"#bdbdbd",borderFocusColor:"#0066cc",borderRadius:"4px",fontSize:"16px",fontSizeSmall:"14px",fontFamily:'"Roboto", "Helvetica", "Arial", sans-serif'}};function le(e,t=!1){e.style.cssText=`
|
|
168
|
+
background-color: var(--fb-action-bg-color);
|
|
169
|
+
color: var(--fb-action-text-color);
|
|
170
|
+
border: var(--fb-border-width) solid var(--fb-action-border-color);
|
|
171
|
+
padding: ${t?"0.5rem 1rem":"0.5rem 0.75rem"};
|
|
172
|
+
font-size: var(--fb-font-size);
|
|
173
|
+
font-weight: var(--fb-font-weight-medium);
|
|
174
|
+
border-radius: var(--fb-border-radius);
|
|
175
|
+
transition: all var(--fb-transition-duration);
|
|
176
|
+
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
177
|
+
`,e.addEventListener("mouseenter",()=>{e.style.backgroundColor="var(--fb-action-hover-bg-color)",e.style.borderColor="var(--fb-action-hover-border-color)"}),e.addEventListener("mouseleave",()=>{e.style.backgroundColor="var(--fb-action-bg-color)",e.style.borderColor="var(--fb-action-border-color)"})}const _e={text:{validate:I,update:O},textarea:{validate:ve,update:ge},number:{validate:xe,update:Ce},select:{validate:$e,update:ke},file:{validate:J,update:X},files:{validate:J,update:X},container:{validate:ee,update:te},group:{validate:ze,update:Re}};function oe(e){return _e[e]||null}function Ge(e,t,n){const o=oe(e.type);return o&&o.validate?o.validate(e,t,n):null}function Ye(e,t,n,o){const c=oe(e.type);return c&&c.update?(c.update(e,t,n,o),!0):!1}class H{constructor(t){this.instanceId=Ue(),this.state=Oe(t)}getInstanceId(){return this.instanceId}getState(){return this.state}setFormRoot(t){this.state.formRoot=t}configure(t){Object.assign(this.state.config,t)}setUploadHandler(t){this.state.config.uploadFile=t}setDownloadHandler(t){this.state.config.downloadFile=t}setThumbnailHandler(t){this.state.config.getThumbnail=t}setActionHandler(t){this.state.config.actionHandler=t}setMode(t){this.state.config.readonly=t==="readonly"}setLocale(t){this.state.config.translations[t]&&(this.state.config.locale=t)}triggerOnChange(t,n){this.state.config.readonly||(this.state.debounceTimer!==null&&clearTimeout(this.state.debounceTimer),this.state.debounceTimer=setTimeout(()=>{const o=this.validateForm(!0);this.state.config.onChange&&this.state.config.onChange(o),this.state.config.onFieldChange&&t!==void 0&&n!==void 0&&this.state.config.onFieldChange(t,n,o),this.state.debounceTimer=null},this.state.config.debounceMs))}registerAction(t){if(!t||!t.value)throw new Error("Action must have a value property");this.state.externalActions||(this.state.externalActions=[]);const n=this.state.externalActions.findIndex(o=>o.value===t.value&&o.related_field===t.related_field);n>=0?this.state.externalActions[n]=t:this.state.externalActions.push(t)}findFormElementByFieldPath(t){if(!this.state.formRoot)return null;if(!this.state.config.readonly){let c=this.state.formRoot.querySelector(`[name="${t}"]`);if(c)return c;const r=[t,t.replace(/\[(\d+)\]/g,"[$1]"),t.replace(/\./g,"[")+"]".repeat((t.match(/\./g)||[]).length)];for(const l of r)if(c=this.state.formRoot.querySelector(`[name="${l}"]`),c)return c}const n=this.findSchemaElement(t);if(!n)return null;const o=this.state.formRoot.querySelectorAll(".fb-field-wrapper");for(const c of o){const r=n.label||n.key,l=c.querySelector("label");if(l&&(l.textContent===r||l.textContent===`${r}*`)){let a=c.querySelector(".field-placeholder");return a||(a=document.createElement("div"),a.className="field-placeholder",a.style.display="none",c.appendChild(a)),a}}return null}findSchemaElement(t){if(!this.state.schema||!this.state.schema.elements)return null;let n=this.state.schema.elements,o=null;const c=t.replace(/\[\d+\]/g,"").split(".").filter(Boolean);for(const r of c){if(o=n.find(l=>l.key===r)||null,!o)return null;"elements"in o&&o.elements&&(n=o.elements)}return o}resolveActionLabel(t,n,o,c=!1){if(o&&"actions"in o&&o.actions){const r=o.actions.find(l=>l.key===t);if(r&&r.label)return r.label}if(c&&this.state.schema&&"actions"in this.state.schema&&this.state.schema.actions){const r=this.state.schema.actions.find(l=>l.key===t);if(r&&r.label)return r.label}return n||t}renderFormLevelActions(t,n=[]){if(!this.state.formRoot)return;const o=this.state.formRoot.querySelector(".form-level-actions-container");o&&o.remove();const c=document.createElement("div");c.className="form-level-actions-container mt-6 pt-4 flex flex-wrap gap-3 justify-center",c.style.cssText=`
|
|
178
|
+
border-top: var(--fb-border-width) solid var(--fb-border-color);
|
|
179
|
+
`,t.forEach(r=>{const l=document.createElement("button");l.type="button",le(l,!0);const a=n.includes(r),i=this.resolveActionLabel(r.key,r.label,null,a);l.textContent=i,l.addEventListener("click",s=>{s.preventDefault(),s.stopPropagation(),this.state.config.actionHandler&&typeof this.state.config.actionHandler=="function"&&this.state.config.actionHandler(r.value,r.key,null)}),c.appendChild(l)}),this.state.formRoot.appendChild(c)}renderExternalActions(){if(!this.state.externalActions||!Array.isArray(this.state.externalActions))return;const t=new Map,n=[],o=[];this.state.externalActions.forEach(r=>{!r.key||!r.value||(r.related_field?(t.has(r.related_field)||t.set(r.related_field,[]),t.get(r.related_field).push(r)):n.push(r))}),t.forEach((r,l)=>{const a=this.findFormElementByFieldPath(l);if(!a){console.warn(`External action: Could not find form element for field "${l}", treating as form-level actions`),o.push(...r);return}let i=a.closest(".fb-field-wrapper");if(i||(i=a.parentElement),!i){console.warn(`External action: Could not find wrapper for field "${l}"`);return}const s=i.querySelector(".external-actions-container");s&&s.remove();const u=document.createElement("div");u.className="external-actions-container mt-3 flex flex-wrap gap-2";const d=this.findSchemaElement(l);r.forEach(h=>{const p=document.createElement("button");p.type="button",le(p,!1);const m=this.resolveActionLabel(h.key,h.label,d);p.textContent=m,p.addEventListener("click",v=>{v.preventDefault(),v.stopPropagation(),this.state.config.actionHandler&&typeof this.state.config.actionHandler=="function"&&this.state.config.actionHandler(h.value,h.key,h.related_field)}),u.appendChild(p)}),i.appendChild(u)});const c=[...n,...o];c.length>0&&this.renderFormLevelActions(c,n)}renderForm(t,n,o,c){const r=M(n);if(r.length>0){console.error("Schema validation errors:",r);return}this.state.formRoot=t,this.state.schema=n,this.state.externalActions=c||null,N(t),t.setAttribute("data-fb-root","true"),Ve(t,this.state.config.theme);const l=document.createElement("div");l.className="space-y-6",n.elements.forEach(a=>{if(a.hidden)return;const i=re(a,{path:"",prefill:o||{},state:this.state,instance:this});l.appendChild(i)}),t.appendChild(l),this.state.config.readonly&&this.state.externalActions&&Array.isArray(this.state.externalActions)&&this.renderExternalActions()}validateForm(t=!1){if(!this.state.schema||!this.state.formRoot)return{valid:!0,errors:[],data:{}};const n=[],o={},c=(r,l,a=null)=>{const i=r.key,s={scopeRoot:a||this.state.formRoot,state:this.state,instance:this,path:l.path,skipValidation:t},u=Ge(r,i,s);return u!==null?(n.push(...u.errors),u.value):(console.warn(`Unknown field type "${r.type}" for key "${i}"`),null)};return Be(c),this.state.schema.elements.forEach(r=>{r.hidden?o[r.key]=r.default!==void 0?r.default:null:o[r.key]=c(r,{path:""})}),{valid:n.length===0,errors:n,data:o}}getFormData(){return this.validateForm(!1)}submitForm(){const t=this.validateForm(!1);return t.valid&&typeof window!="undefined"&&window.parent&&window.parent.postMessage({type:"formSubmit",data:t.data,schema:this.state.schema},"*"),t}saveDraft(){const t=this.validateForm(!0);return typeof window!="undefined"&&window.parent&&window.parent.postMessage({type:"formDraft",data:t.data,schema:this.state.schema},"*"),t}clearForm(){if(!this.state.schema||!this.state.formRoot){console.warn("clearForm: Form not initialized. Call renderForm() first.");return}const t=this.state.schema,n=this.state.formRoot,o=this.buildHiddenFieldsData(t.elements);this.renderForm(n,t,o)}buildHiddenFieldsData(t,n=""){const o={};for(const c of t){const r=c.key,l=n?`${n}.${r}`:r;if(c.hidden&&c.default!==void 0&&(o[l]=c.default),c.type==="container"||c.type==="group"){const a=c,i=this.buildHiddenFieldsData(a.elements,l);Object.assign(o,i)}}return o}setFormData(t){if(!this.state.schema||!this.state.formRoot){console.warn("setFormData: Form not initialized. Call renderForm() first.");return}for(const n in t)this.updateField(n,t[n])}updateField(t,n){if(!this.state.schema||!this.state.formRoot){console.warn("updateField: Form not initialized. Call renderForm() first.");return}const o=this.findSchemaElement(t);if(!o){console.warn(`updateField: Schema element not found for path "${t}"`);return}const c=this.findFormElementByFieldPath(t);if(!c){console.warn(`updateField: DOM element not found for path "${t}"`);return}this.updateFieldValue(c,o,t,n),(this.state.config.onChange||this.state.config.onFieldChange)&&this.triggerOnChange(t,n)}updateFieldValue(t,n,o,c){const r={scopeRoot:this.state.formRoot,state:this.state,instance:this,path:""};Ye(n,o,c,r)||console.warn(`updateField: No updater found for field type "${n.type}" at path "${o}"`)}destroy(){this.state.debounceTimer!==null&&(clearTimeout(this.state.debounceTimer),this.state.debounceTimer=null),this.state.resourceIndex.clear(),this.state.formRoot&&N(this.state.formRoot),this.state.formRoot=null,this.state.schema=null,this.state.externalActions=null}}function ae(e){return new H(e)}return typeof window!="undefined"&&(window.FormBuilder=H,window.createFormBuilder=ae,window.validateSchema=M),F.FormBuilderInstance=H,F.createFormBuilder=ae,F.default=H,F.defaultTheme=A,F.exampleThemes=We,F.validateSchema=M,Object.defineProperty(F,"__esModule",{value:!0}),F})({});
|