@dmitryvim/form-builder 0.1.42 → 0.2.1

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