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