@dmitryvim/form-builder 0.1.33 → 0.1.34

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.
@@ -1,4 +1,56 @@
1
1
  // Form Builder Library - Core API
2
+
3
+ // Object URL management for memory leak prevention
4
+ const objectUrlIndex = new Map();
5
+
6
+ function createObjectURL(file, resourceIdParam) {
7
+ // Revoke previous URL for this resource to prevent memory leaks
8
+ revokeObjectURL(resourceIdParam);
9
+
10
+ const url = URL.createObjectURL(file);
11
+ objectUrlIndex.set(resourceIdParam, url);
12
+ return url;
13
+ }
14
+
15
+ function revokeObjectURL(resourceIdParam) {
16
+ const url = objectUrlIndex.get(resourceIdParam);
17
+ if (url) {
18
+ URL.revokeObjectURL(url);
19
+ objectUrlIndex.delete(resourceIdParam);
20
+ }
21
+ }
22
+
23
+ function revokeAllObjectURLs() {
24
+ for (const [, url] of objectUrlIndex.entries()) {
25
+ URL.revokeObjectURL(url);
26
+ }
27
+ objectUrlIndex.clear();
28
+ }
29
+
30
+ // Helper function to create safe preview elements
31
+ function createPreviewElement(icon, fileName) {
32
+ const wrapper = document.createElement("div");
33
+ wrapper.className =
34
+ "aspect-video flex items-center justify-center text-gray-400";
35
+
36
+ const textCenter = document.createElement("div");
37
+ textCenter.className = "text-center";
38
+
39
+ const iconEl = document.createElement("div");
40
+ iconEl.className = "text-4xl mb-2";
41
+ iconEl.textContent = icon;
42
+
43
+ const nameEl = document.createElement("div");
44
+ nameEl.className = "text-sm";
45
+ nameEl.textContent = fileName || "";
46
+
47
+ textCenter.appendChild(iconEl);
48
+ textCenter.appendChild(nameEl);
49
+ wrapper.appendChild(textCenter);
50
+
51
+ return wrapper;
52
+ }
53
+
2
54
  // State management
3
55
  const state = {
4
56
  schema: null,
@@ -142,6 +194,8 @@ function renderForm(schema, prefill) {
142
194
  return;
143
195
  }
144
196
 
197
+ // Clean up any existing object URLs before clearing form
198
+ revokeAllObjectURLs();
145
199
  clear(state.formRoot);
146
200
 
147
201
  const formEl = document.createElement("div");
@@ -184,8 +238,17 @@ function renderElement(element, ctx) {
184
238
  const infoBtn = document.createElement("button");
185
239
  infoBtn.type = "button";
186
240
  infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
187
- infoBtn.innerHTML =
188
- '<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>';
241
+ // Create SVG icon safely
242
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
243
+ svg.setAttribute("class", "w-4 h-4");
244
+ svg.setAttribute("fill", "currentColor");
245
+ svg.setAttribute("viewBox", "0 0 24 24");
246
+
247
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
248
+ path.setAttribute("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");
249
+
250
+ svg.appendChild(path);
251
+ infoBtn.appendChild(svg);
189
252
 
190
253
  // Create tooltip
191
254
  const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
@@ -416,8 +479,8 @@ async function renderFilePreview(container, resourceId, options = {}) {
416
479
  reader.readAsDataURL(meta.file);
417
480
  container.appendChild(img);
418
481
  } else if (meta.type && meta.type.startsWith("video/")) {
419
- // Video file - use object URL for preview
420
- const videoUrl = URL.createObjectURL(meta.file);
482
+ // Video file - use managed object URL for preview
483
+ const videoUrl = createObjectURL(meta.file, resourceId);
421
484
 
422
485
  // Remove all conflicting handlers to prevent interference with video controls
423
486
  container.onclick = null;
@@ -427,39 +490,55 @@ async function renderFilePreview(container, resourceId, options = {}) {
427
490
  container.parentNode.replaceChild(newContainer, container);
428
491
  container = newContainer;
429
492
 
430
- container.innerHTML = `
431
- <div class="relative group h-full">
432
- <video class="w-full h-full object-contain" controls preload="auto" muted>
433
- <source src="${videoUrl}" type="${meta.type}">
434
- Your browser does not support the video tag.
435
- </video>
436
- <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
437
- <button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
438
- ${t("removeElement")}
439
- </button>
440
- <button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
441
- Change
442
- </button>
443
- </div>
444
- </div>
445
- `;
493
+ // Create video container safely
494
+ clear(container);
495
+
496
+ const wrapper = document.createElement("div");
497
+ wrapper.className = "relative group h-full";
498
+
499
+ const video = document.createElement("video");
500
+ video.className = "w-full h-full object-contain";
501
+ video.controls = true;
502
+ video.preload = "auto";
503
+ video.muted = true;
504
+
505
+ const source = document.createElement("source");
506
+ source.src = videoUrl;
507
+ source.type = meta.type;
508
+
509
+ const fallback = document.createTextNode("Your browser does not support the video tag.");
510
+ video.appendChild(source);
511
+ video.appendChild(fallback);
512
+
513
+ const buttonsContainer = document.createElement("div");
514
+ buttonsContainer.className = "absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1";
515
+
516
+ const deleteBtn = document.createElement("button");
517
+ deleteBtn.className = "bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn";
518
+ deleteBtn.textContent = t("removeElement");
519
+
520
+ const changeBtn = document.createElement("button");
521
+ changeBtn.className = "bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn";
522
+ changeBtn.textContent = "Change";
523
+
524
+ buttonsContainer.appendChild(deleteBtn);
525
+ buttonsContainer.appendChild(changeBtn);
526
+ wrapper.appendChild(video);
527
+ wrapper.appendChild(buttonsContainer);
528
+ container.appendChild(wrapper);
446
529
 
447
530
  // Add click handlers to the custom buttons
448
- const changeBtn = container.querySelector(".change-file-btn");
449
- if (changeBtn) {
450
- changeBtn.onclick = (e) => {
451
- e.stopPropagation();
452
- if (deps?.picker) {
453
- deps.picker.click();
454
- }
455
- };
456
- }
531
+ changeBtn.onclick = (e) => {
532
+ e.stopPropagation();
533
+ if (deps?.picker) {
534
+ deps.picker.click();
535
+ }
536
+ };
457
537
 
458
- const deleteBtn = container.querySelector(".delete-file-btn");
459
- if (deleteBtn) {
460
- deleteBtn.onclick = (e) => {
538
+ deleteBtn.onclick = (e) => {
461
539
  e.stopPropagation();
462
- // Clear the file
540
+ // Clear the file and revoke object URL
541
+ revokeObjectURL(resourceId);
463
542
  state.resourceIndex.delete(resourceId);
464
543
  // Update hidden input
465
544
  const hiddenInput = container.parentElement.querySelector(
@@ -475,27 +554,55 @@ async function renderFilePreview(container, resourceId, options = {}) {
475
554
  if (deps?.dragHandler) {
476
555
  setupDragAndDrop(container, deps.dragHandler);
477
556
  }
478
- container.innerHTML = `
479
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
480
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
481
- <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"/>
482
- </svg>
483
- <div class="text-sm text-center">${t("clickDragText")}</div>
484
- </div>
485
- `;
557
+ // Create placeholder content safely
558
+ clear(container);
559
+
560
+ const wrapper = document.createElement("div");
561
+ wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
562
+
563
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
564
+ svg.setAttribute("class", "w-6 h-6 mb-2");
565
+ svg.setAttribute("fill", "currentColor");
566
+ svg.setAttribute("viewBox", "0 0 24 24");
567
+
568
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
569
+ path.setAttribute("d", "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z");
570
+
571
+ svg.appendChild(path);
572
+
573
+ const textDiv = document.createElement("div");
574
+ textDiv.className = "text-sm text-center";
575
+ textDiv.textContent = t("clickDragText");
576
+
577
+ wrapper.appendChild(svg);
578
+ wrapper.appendChild(textDiv);
579
+ container.appendChild(wrapper);
486
580
  };
487
- }
488
581
  } else {
489
- // Non-image, non-video file
490
- container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">📁</div><div class="text-sm">${
491
- fileName
492
- }</div></div>`;
582
+ // Non-image, non-video file - create elements safely
583
+ const wrapper = document.createElement("div");
584
+ wrapper.className =
585
+ "flex flex-col items-center justify-center h-full text-gray-400";
586
+
587
+ const icon = document.createElement("div");
588
+ icon.className = "text-2xl mb-2";
589
+ icon.textContent = "📁";
590
+
591
+ const nameEl = document.createElement("div");
592
+ nameEl.className = "text-sm";
593
+ nameEl.textContent = fileName || "";
594
+
595
+ wrapper.appendChild(icon);
596
+ wrapper.appendChild(nameEl);
597
+ clear(container);
598
+ container.appendChild(wrapper);
493
599
  }
494
600
 
495
601
  // Add delete button for edit mode (except for videos which have custom buttons)
496
602
  if (!isReadonly && !(meta && meta.type && meta.type.startsWith("video/"))) {
497
603
  addDeleteButton(container, () => {
498
- // Clear the file
604
+ // Clear the file and revoke object URL
605
+ revokeObjectURL(resourceId);
499
606
  state.resourceIndex.delete(resourceId);
500
607
  // Update hidden input
501
608
  const hiddenInput = container.parentElement.querySelector(
@@ -504,15 +611,29 @@ async function renderFilePreview(container, resourceId, options = {}) {
504
611
  if (hiddenInput) {
505
612
  hiddenInput.value = "";
506
613
  }
507
- // Clear preview and show placeholder
508
- container.innerHTML = `
509
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
510
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
511
- <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"/>
512
- </svg>
513
- <div class="text-sm text-center">${t("clickDragText")}</div>
514
- </div>
515
- `;
614
+ // Clear preview and show placeholder safely
615
+ clear(container);
616
+
617
+ const wrapper = document.createElement("div");
618
+ wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
619
+
620
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
621
+ svg.setAttribute("class", "w-6 h-6 mb-2");
622
+ svg.setAttribute("fill", "currentColor");
623
+ svg.setAttribute("viewBox", "0 0 24 24");
624
+
625
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
626
+ path.setAttribute("d", "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z");
627
+
628
+ svg.appendChild(path);
629
+
630
+ const textDiv = document.createElement("div");
631
+ textDiv.className = "text-sm text-center";
632
+ textDiv.textContent = t("clickDragText");
633
+
634
+ wrapper.appendChild(svg);
635
+ wrapper.appendChild(textDiv);
636
+ container.appendChild(wrapper);
516
637
  });
517
638
  }
518
639
  } else if (state.config.getThumbnail) {
@@ -525,21 +646,50 @@ async function renderFilePreview(container, resourceId, options = {}) {
525
646
  container.appendChild(img);
526
647
  } else {
527
648
  // Fallback to file icon
528
- container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">🖼️</div><div class="text-sm">${
529
- fileName
530
- }</div></div>`;
649
+ // Create elements safely
650
+ const wrapper = document.createElement("div");
651
+ wrapper.className =
652
+ "flex flex-col items-center justify-center h-full text-gray-400";
653
+
654
+ const icon = document.createElement("div");
655
+ icon.className = "text-2xl mb-2";
656
+ icon.textContent = "🖼️";
657
+
658
+ const nameEl = document.createElement("div");
659
+ nameEl.className = "text-sm";
660
+ nameEl.textContent = fileName || "";
661
+
662
+ wrapper.appendChild(icon);
663
+ wrapper.appendChild(nameEl);
664
+ clear(container);
665
+ container.appendChild(wrapper);
531
666
  }
532
667
  } catch (error) {
533
668
  console.warn("Thumbnail loading failed:", error);
534
- container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">📁</div><div class="text-sm">${
535
- fileName
536
- }</div></div>`;
669
+ // Create elements safely
670
+ const wrapper = document.createElement("div");
671
+ wrapper.className =
672
+ "flex flex-col items-center justify-center h-full text-gray-400";
673
+
674
+ const icon = document.createElement("div");
675
+ icon.className = "text-2xl mb-2";
676
+ icon.textContent = "📁";
677
+
678
+ const nameEl = document.createElement("div");
679
+ nameEl.className = "text-sm";
680
+ nameEl.textContent = fileName || "";
681
+
682
+ wrapper.appendChild(icon);
683
+ wrapper.appendChild(nameEl);
684
+ clear(container);
685
+ container.appendChild(wrapper);
537
686
  }
538
687
 
539
688
  // Add delete button for edit mode
540
689
  if (!isReadonly) {
541
690
  addDeleteButton(container, () => {
542
- // Clear the file
691
+ // Clear the file and revoke object URL
692
+ revokeObjectURL(resourceId);
543
693
  state.resourceIndex.delete(resourceId);
544
694
  // Update hidden input
545
695
  const hiddenInput = container.parentElement.querySelector(
@@ -548,22 +698,35 @@ async function renderFilePreview(container, resourceId, options = {}) {
548
698
  if (hiddenInput) {
549
699
  hiddenInput.value = "";
550
700
  }
551
- // Clear preview and show placeholder
552
- container.innerHTML = `
553
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
554
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
555
- <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"/>
556
- </svg>
557
- <div class="text-sm text-center">${t("clickDragText")}</div>
558
- </div>
559
- `;
701
+ // Clear preview and show placeholder safely
702
+ clear(container);
703
+
704
+ const wrapper = document.createElement("div");
705
+ wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
706
+
707
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
708
+ svg.setAttribute("class", "w-6 h-6 mb-2");
709
+ svg.setAttribute("fill", "currentColor");
710
+ svg.setAttribute("viewBox", "0 0 24 24");
711
+
712
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
713
+ path.setAttribute("d", "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z");
714
+
715
+ svg.appendChild(path);
716
+
717
+ const textDiv = document.createElement("div");
718
+ textDiv.className = "text-sm text-center";
719
+ textDiv.textContent = t("clickDragText");
720
+
721
+ wrapper.appendChild(svg);
722
+ wrapper.appendChild(textDiv);
723
+ container.appendChild(wrapper);
560
724
  });
561
725
  }
562
726
  } else {
563
- // No file and no getThumbnail config - fallback
564
- container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">🖼️</div><div class="text-sm">${
565
- fileName
566
- }</div></div>`;
727
+ // No file and no getThumbnail config - fallback - create elements safely
728
+ clear(container);
729
+ container.appendChild(createPreviewElement("🖼️", fileName));
567
730
  }
568
731
 
569
732
  // Add click handler for download in readonly mode
@@ -616,11 +779,11 @@ function renderResourcePills(container, rids, onRemove) {
616
779
  slot.onclick = () => {
617
780
  // Look for file input - check parent containers that have space-y-2 class
618
781
  let filesWrapper = container.parentElement;
619
- while (filesWrapper && !filesWrapper.classList.contains('space-y-2')) {
782
+ while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
620
783
  filesWrapper = filesWrapper.parentElement;
621
784
  }
622
785
  // If no parent with space-y-2, container itself might be the wrapper
623
- if (!filesWrapper && container.classList.contains('space-y-2')) {
786
+ if (!filesWrapper && container.classList.contains("space-y-2")) {
624
787
  filesWrapper = container;
625
788
  }
626
789
  const fileInput = filesWrapper?.querySelector('input[type="file"]');
@@ -641,11 +804,11 @@ function renderResourcePills(container, rids, onRemove) {
641
804
  e.stopPropagation();
642
805
  // Look for file input - check parent containers that have space-y-2 class
643
806
  let filesWrapper = container.parentElement;
644
- while (filesWrapper && !filesWrapper.classList.contains('space-y-2')) {
807
+ while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
645
808
  filesWrapper = filesWrapper.parentElement;
646
809
  }
647
810
  // If no parent with space-y-2, container itself might be the wrapper
648
- if (!filesWrapper && container.classList.contains('space-y-2')) {
811
+ if (!filesWrapper && container.classList.contains("space-y-2")) {
649
812
  filesWrapper = container;
650
813
  }
651
814
  const fileInput = filesWrapper?.querySelector('input[type="file"]');
@@ -710,76 +873,179 @@ function renderResourcePills(container, rids, onRemove) {
710
873
  img.src = url;
711
874
  slot.appendChild(img);
712
875
  } else {
713
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
714
- <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
715
- <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"/>
716
- </svg>
717
- </div>`;
876
+ // Create fallback placeholder safely
877
+ const wrapper = document.createElement("div");
878
+ wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
879
+
880
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
881
+ svg.setAttribute("class", "w-12 h-12");
882
+ svg.setAttribute("fill", "currentColor");
883
+ svg.setAttribute("viewBox", "0 0 24 24");
884
+
885
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
886
+ path.setAttribute("d", "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z");
887
+
888
+ svg.appendChild(path);
889
+ wrapper.appendChild(svg);
890
+ slot.appendChild(wrapper);
718
891
  }
719
892
  } else {
720
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
721
- <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
722
- <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"/>
723
- </svg>
724
- </div>`;
893
+ // Create fallback placeholder safely
894
+ const wrapper = document.createElement("div");
895
+ wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
896
+
897
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
898
+ svg.setAttribute("class", "w-12 h-12");
899
+ svg.setAttribute("fill", "currentColor");
900
+ svg.setAttribute("viewBox", "0 0 24 24");
901
+
902
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
903
+ path.setAttribute("d", "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z");
904
+
905
+ svg.appendChild(path);
906
+ wrapper.appendChild(svg);
907
+ slot.appendChild(wrapper);
725
908
  }
726
909
  } else if (meta && meta.type?.startsWith("video/")) {
727
910
  if (meta.file && meta.file instanceof File) {
728
- // Video file - use object URL for preview in thumbnail format
729
- const videoUrl = URL.createObjectURL(meta.file);
730
- slot.innerHTML = `
731
- <div class="relative group h-full w-full">
732
- <video class="w-full h-full object-contain" preload="metadata" muted>
733
- <source src="${videoUrl}" type="${meta.type}">
734
- </video>
735
- <div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
736
- <div class="bg-white bg-opacity-90 rounded-full p-1">
737
- <svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
738
- <path d="M8 5v14l11-7z"/>
739
- </svg>
740
- </div>
741
- </div>
742
- </div>
743
- `;
911
+ // Video file - use managed object URL for preview in thumbnail format
912
+ const videoUrl = createObjectURL(meta.file, rid);
913
+ // Create video thumbnail safely
914
+ const wrapper = document.createElement("div");
915
+ wrapper.className = "relative group h-full w-full";
916
+
917
+ const video = document.createElement("video");
918
+ video.className = "w-full h-full object-contain";
919
+ video.preload = "metadata";
920
+ video.muted = true;
921
+
922
+ const source = document.createElement("source");
923
+ source.src = videoUrl;
924
+ source.type = meta.type;
925
+
926
+ video.appendChild(source);
927
+
928
+ const overlay = document.createElement("div");
929
+ overlay.className = "absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center";
930
+
931
+ const playButton = document.createElement("div");
932
+ playButton.className = "bg-white bg-opacity-90 rounded-full p-1";
933
+
934
+ const playIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
935
+ playIcon.setAttribute("class", "w-4 h-4 text-gray-800");
936
+ playIcon.setAttribute("fill", "currentColor");
937
+ playIcon.setAttribute("viewBox", "0 0 24 24");
938
+
939
+ const playPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
940
+ playPath.setAttribute("d", "M8 5v14l11-7z");
941
+
942
+ playIcon.appendChild(playPath);
943
+ playButton.appendChild(playIcon);
944
+ overlay.appendChild(playButton);
945
+ wrapper.appendChild(video);
946
+ wrapper.appendChild(overlay);
947
+ slot.appendChild(wrapper);
744
948
  } else if (state.config.getThumbnail) {
745
949
  // Use getThumbnail for uploaded video files
746
950
  const videoUrl = state.config.getThumbnail(rid);
747
951
  if (videoUrl) {
748
- slot.innerHTML = `
749
- <div class="relative group h-full w-full">
750
- <video class="w-full h-full object-contain" preload="metadata" muted>
751
- <source src="${videoUrl}" type="${meta.type}">
752
- </video>
753
- <div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
754
- <div class="bg-white bg-opacity-90 rounded-full p-1">
755
- <svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
756
- <path d="M8 5v14l11-7z"/>
757
- </svg>
758
- </div>
759
- </div>
760
- </div>
761
- `;
952
+ // Create video thumbnail safely
953
+ const wrapper = document.createElement("div");
954
+ wrapper.className = "relative group h-full w-full";
955
+
956
+ const video = document.createElement("video");
957
+ video.className = "w-full h-full object-contain";
958
+ video.preload = "metadata";
959
+ video.muted = true;
960
+
961
+ const source = document.createElement("source");
962
+ source.src = videoUrl;
963
+ source.type = meta.type;
964
+
965
+ video.appendChild(source);
966
+
967
+ const overlay = document.createElement("div");
968
+ overlay.className = "absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center";
969
+
970
+ const playButton = document.createElement("div");
971
+ playButton.className = "bg-white bg-opacity-90 rounded-full p-1";
972
+
973
+ const playIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
974
+ playIcon.setAttribute("class", "w-4 h-4 text-gray-800");
975
+ playIcon.setAttribute("fill", "currentColor");
976
+ playIcon.setAttribute("viewBox", "0 0 24 24");
977
+
978
+ const playPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
979
+ playPath.setAttribute("d", "M8 5v14l11-7z");
980
+
981
+ playIcon.appendChild(playPath);
982
+ playButton.appendChild(playIcon);
983
+ overlay.appendChild(playButton);
984
+ wrapper.appendChild(video);
985
+ wrapper.appendChild(overlay);
986
+ slot.appendChild(wrapper);
762
987
  } else {
763
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
764
- <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
765
- <path d="M8 5v14l11-7z"/>
766
- </svg>
767
- <div class="text-xs mt-1">${meta?.name || "Video"}</div>
768
- </div>`;
988
+ // Create video placeholder safely
989
+ const wrapper = document.createElement("div");
990
+ wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
991
+
992
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
993
+ svg.setAttribute("class", "w-8 h-8");
994
+ svg.setAttribute("fill", "currentColor");
995
+ svg.setAttribute("viewBox", "0 0 24 24");
996
+
997
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
998
+ path.setAttribute("d", "M8 5v14l11-7z");
999
+
1000
+ svg.appendChild(path);
1001
+
1002
+ const nameDiv = document.createElement("div");
1003
+ nameDiv.className = "text-xs mt-1";
1004
+ nameDiv.textContent = meta?.name || "Video";
1005
+
1006
+ wrapper.appendChild(svg);
1007
+ wrapper.appendChild(nameDiv);
1008
+ slot.appendChild(wrapper);
769
1009
  }
770
1010
  } else {
771
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
772
- <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
773
- <path d="M8 5v14l11-7z"/>
774
- </svg>
775
- <div class="text-xs mt-1">${meta?.name || "Video"}</div>
776
- </div>`;
1011
+ // Create video placeholder safely
1012
+ const wrapper = document.createElement("div");
1013
+ wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
1014
+
1015
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1016
+ svg.setAttribute("class", "w-8 h-8");
1017
+ svg.setAttribute("fill", "currentColor");
1018
+ svg.setAttribute("viewBox", "0 0 24 24");
1019
+
1020
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1021
+ path.setAttribute("d", "M8 5v14l11-7z");
1022
+
1023
+ svg.appendChild(path);
1024
+
1025
+ const nameDiv = document.createElement("div");
1026
+ nameDiv.className = "text-xs mt-1";
1027
+ nameDiv.textContent = meta?.name || "Video";
1028
+
1029
+ wrapper.appendChild(svg);
1030
+ wrapper.appendChild(nameDiv);
1031
+ slot.appendChild(wrapper);
777
1032
  }
778
1033
  } else {
779
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
780
- <div class="text-2xl mb-1">📁</div>
781
- <div class="text-xs">${meta?.name || "File"}</div>
782
- </div>`;
1034
+ // Create file placeholder safely
1035
+ const wrapper = document.createElement("div");
1036
+ wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
1037
+
1038
+ const iconDiv = document.createElement("div");
1039
+ iconDiv.className = "text-2xl mb-1";
1040
+ iconDiv.textContent = "📁";
1041
+
1042
+ const nameDiv = document.createElement("div");
1043
+ nameDiv.className = "text-xs";
1044
+ nameDiv.textContent = meta?.name || "File";
1045
+
1046
+ wrapper.appendChild(iconDiv);
1047
+ wrapper.appendChild(nameDiv);
1048
+ slot.appendChild(wrapper);
783
1049
  }
784
1050
 
785
1051
  // Add remove button overlay (similar to file field)
@@ -803,16 +1069,25 @@ function renderResourcePills(container, rids, onRemove) {
803
1069
  // Empty slot placeholder
804
1070
  slot.className =
805
1071
  "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";
806
- slot.innerHTML =
807
- '<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>';
1072
+ // Create empty slot SVG safely
1073
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1074
+ svg.setAttribute("class", "w-12 h-12 text-gray-400");
1075
+ svg.setAttribute("fill", "currentColor");
1076
+ svg.setAttribute("viewBox", "0 0 24 24");
1077
+
1078
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
1079
+ path.setAttribute("d", "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z");
1080
+
1081
+ svg.appendChild(path);
1082
+ slot.appendChild(svg);
808
1083
  slot.onclick = () => {
809
1084
  // Look for file input - check parent containers that have space-y-2 class
810
1085
  let filesWrapper = container.parentElement;
811
- while (filesWrapper && !filesWrapper.classList.contains('space-y-2')) {
1086
+ while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
812
1087
  filesWrapper = filesWrapper.parentElement;
813
1088
  }
814
1089
  // If no parent with space-y-2, container itself might be the wrapper
815
- if (!filesWrapper && container.classList.contains('space-y-2')) {
1090
+ if (!filesWrapper && container.classList.contains("space-y-2")) {
816
1091
  filesWrapper = container;
817
1092
  }
818
1093
  const fileInput = filesWrapper?.querySelector('input[type="file"]');
@@ -866,14 +1141,37 @@ async function handleFileSelect(file, container, fieldName, deps = null) {
866
1141
  });
867
1142
 
868
1143
  // Create hidden input to store the resource ID
869
- let hiddenInput = container.parentElement.querySelector(
1144
+ // Handle case where container might be detached due to video replacement
1145
+ let parentElement = container.parentElement;
1146
+ if (!parentElement) {
1147
+ // Container is detached - find the current container by field name
1148
+ const form = document.querySelector('[data-form-builder]');
1149
+ if (form) {
1150
+ const existingInput = form.querySelector(`input[name="${fieldName}"]`);
1151
+ if (existingInput) {
1152
+ parentElement = existingInput.parentElement;
1153
+ // Update container reference to the current one in DOM
1154
+ const currentContainer = parentElement.querySelector('.file-preview-container');
1155
+ if (currentContainer) {
1156
+ container = currentContainer;
1157
+ }
1158
+ }
1159
+ }
1160
+ }
1161
+
1162
+ if (!parentElement) {
1163
+ console.warn('Could not find parent element for file field:', fieldName);
1164
+ return;
1165
+ }
1166
+
1167
+ let hiddenInput = parentElement.querySelector(
870
1168
  'input[type="hidden"]',
871
1169
  );
872
1170
  if (!hiddenInput) {
873
1171
  hiddenInput = document.createElement("input");
874
1172
  hiddenInput.type = "hidden";
875
1173
  hiddenInput.name = fieldName;
876
- container.parentElement.appendChild(hiddenInput);
1174
+ parentElement.appendChild(hiddenInput);
877
1175
  }
878
1176
  hiddenInput.value = rid;
879
1177
 
@@ -1044,11 +1342,17 @@ function validateForm(skipValidation = false) {
1044
1342
  values.push(val);
1045
1343
 
1046
1344
  if (!skipValidation && val) {
1047
- if (element.minLength !== null && val.length < element.minLength) {
1345
+ if (
1346
+ element.minLength !== null &&
1347
+ val.length < element.minLength
1348
+ ) {
1048
1349
  errors.push(`${key}[${index}]: minLength=${element.minLength}`);
1049
1350
  markValidity(input, `minLength=${element.minLength}`);
1050
1351
  }
1051
- if (element.maxLength !== null && val.length > element.maxLength) {
1352
+ if (
1353
+ element.maxLength !== null &&
1354
+ val.length > element.maxLength
1355
+ ) {
1052
1356
  errors.push(`${key}[${index}]: maxLength=${element.maxLength}`);
1053
1357
  markValidity(input, `maxLength=${element.maxLength}`);
1054
1358
  }
@@ -1073,7 +1377,7 @@ function validateForm(skipValidation = false) {
1073
1377
  if (!skipValidation) {
1074
1378
  const minCount = element.minCount ?? 1;
1075
1379
  const maxCount = element.maxCount ?? 10;
1076
- const nonEmptyValues = values.filter(v => v.trim() !== "");
1380
+ const nonEmptyValues = values.filter((v) => v.trim() !== "");
1077
1381
 
1078
1382
  if (element.required && nonEmptyValues.length === 0) {
1079
1383
  errors.push(`${key}: required`);
@@ -1156,7 +1460,9 @@ function validateForm(skipValidation = false) {
1156
1460
  markValidity(input, `> max=${element.max}`);
1157
1461
  }
1158
1462
 
1159
- const d = Number.isInteger(element.decimals ?? 0) ? element.decimals : 0;
1463
+ const d = Number.isInteger(element.decimals ?? 0)
1464
+ ? element.decimals
1465
+ : 0;
1160
1466
  markValidity(input, null);
1161
1467
  values.push(Number(v.toFixed(d)));
1162
1468
  });
@@ -1165,7 +1471,7 @@ function validateForm(skipValidation = false) {
1165
1471
  if (!skipValidation) {
1166
1472
  const minCount = element.minCount ?? 1;
1167
1473
  const maxCount = element.maxCount ?? 10;
1168
- const nonNullValues = values.filter(v => v !== null);
1474
+ const nonNullValues = values.filter((v) => v !== null);
1169
1475
 
1170
1476
  if (element.required && nonNullValues.length === 0) {
1171
1477
  errors.push(`${key}: required`);
@@ -1229,7 +1535,7 @@ function validateForm(skipValidation = false) {
1229
1535
  if (!skipValidation) {
1230
1536
  const minCount = element.minCount ?? 1;
1231
1537
  const maxCount = element.maxCount ?? 10;
1232
- const nonEmptyValues = values.filter(v => v !== "");
1538
+ const nonEmptyValues = values.filter((v) => v !== "");
1233
1539
 
1234
1540
  if (element.required && nonEmptyValues.length === 0) {
1235
1541
  errors.push(`${key}: required`);
@@ -1261,9 +1567,11 @@ function validateForm(skipValidation = false) {
1261
1567
  // Handle file with multiple property like files type
1262
1568
  // Find the files list by locating the specific file input for this field
1263
1569
  const fullKey = pathJoin(ctx.path, key);
1264
- const pickerInput = scopeRoot.querySelector(`input[type="file"][name="${fullKey}"]`);
1265
- const filesWrapper = pickerInput?.closest('.space-y-2');
1266
- const container = filesWrapper?.querySelector('.files-list') || null;
1570
+ const pickerInput = scopeRoot.querySelector(
1571
+ `input[type="file"][name="${fullKey}"]`,
1572
+ );
1573
+ const filesWrapper = pickerInput?.closest(".space-y-2");
1574
+ const container = filesWrapper?.querySelector(".files-list") || null;
1267
1575
 
1268
1576
  const resourceIds = [];
1269
1577
  if (container) {
@@ -1351,12 +1659,18 @@ function validateForm(skipValidation = false) {
1351
1659
  const items = [];
1352
1660
  // Use full path for nested group element search
1353
1661
  const fullKey = pathJoin(ctx.path, key);
1354
- const itemElements = scopeRoot.querySelectorAll(`[name^="${fullKey}["]`);
1662
+ const itemElements = scopeRoot.querySelectorAll(
1663
+ `[name^="${fullKey}["]`,
1664
+ );
1355
1665
 
1356
1666
  // Extract actual indices from DOM element names instead of assuming sequential numbering
1357
1667
  const actualIndices = new Set();
1358
1668
  itemElements.forEach((el) => {
1359
- const match = el.name.match(new RegExp(`^${fullKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\[(\\d+)\\]`));
1669
+ const match = el.name.match(
1670
+ new RegExp(
1671
+ `^${fullKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\[(\\d+)\\]`,
1672
+ ),
1673
+ );
1360
1674
  if (match) {
1361
1675
  actualIndices.add(parseInt(match[1]));
1362
1676
  }
@@ -1374,7 +1688,8 @@ function validateForm(skipValidation = false) {
1374
1688
  element.elements.forEach((child) => {
1375
1689
  if (child.hidden) {
1376
1690
  // For hidden child elements, use their default value
1377
- itemData[child.key] = child.default !== undefined ? child.default : "";
1691
+ itemData[child.key] =
1692
+ child.default !== undefined ? child.default : "";
1378
1693
  } else {
1379
1694
  const childKey = `${fullKey}[${actualIndex}].${child.key}`;
1380
1695
  itemData[child.key] = validateElement(
@@ -1395,7 +1710,8 @@ function validateForm(skipValidation = false) {
1395
1710
  element.elements.forEach((child) => {
1396
1711
  if (child.hidden) {
1397
1712
  // For hidden child elements, use their default value
1398
- groupData[child.key] = child.default !== undefined ? child.default : "";
1713
+ groupData[child.key] =
1714
+ child.default !== undefined ? child.default : "";
1399
1715
  } else {
1400
1716
  const childKey = `${key}.${child.key}`;
1401
1717
  groupData[child.key] = validateElement(
@@ -1427,7 +1743,8 @@ function validateForm(skipValidation = false) {
1427
1743
  element.elements.forEach((child) => {
1428
1744
  if (child.hidden) {
1429
1745
  // For hidden child elements, use their default value
1430
- itemData[child.key] = child.default !== undefined ? child.default : "";
1746
+ itemData[child.key] =
1747
+ child.default !== undefined ? child.default : "";
1431
1748
  } else {
1432
1749
  const childKey = `${key}[${i}].${child.key}`;
1433
1750
  itemData[child.key] = validateElement(
@@ -1465,7 +1782,8 @@ function validateForm(skipValidation = false) {
1465
1782
  element.elements.forEach((child) => {
1466
1783
  if (child.hidden) {
1467
1784
  // For hidden child elements, use their default value
1468
- containerData[child.key] = child.default !== undefined ? child.default : "";
1785
+ containerData[child.key] =
1786
+ child.default !== undefined ? child.default : "";
1469
1787
  } else {
1470
1788
  const childKey = `${key}.${child.key}`;
1471
1789
  containerData[child.key] = validateElement(
@@ -1550,7 +1868,8 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1550
1868
 
1551
1869
  const textInput = document.createElement("input");
1552
1870
  textInput.type = "text";
1553
- textInput.className = "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1871
+ textInput.className =
1872
+ "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1554
1873
  textInput.placeholder = element.placeholder || "Enter text";
1555
1874
  textInput.value = value;
1556
1875
  textInput.readOnly = state.config.readonly;
@@ -1571,15 +1890,16 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1571
1890
 
1572
1891
  function updateRemoveButtons() {
1573
1892
  if (state.config.readonly) return;
1574
- const items = container.querySelectorAll('.multiple-text-item');
1893
+ const items = container.querySelectorAll(".multiple-text-item");
1575
1894
  const currentCount = items.length;
1576
1895
  items.forEach((item) => {
1577
- let removeBtn = item.querySelector('.remove-item-btn');
1896
+ let removeBtn = item.querySelector(".remove-item-btn");
1578
1897
  if (!removeBtn) {
1579
- removeBtn = document.createElement('button');
1580
- removeBtn.type = 'button';
1581
- removeBtn.className = 'remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded';
1582
- removeBtn.innerHTML = '✕';
1898
+ removeBtn = document.createElement("button");
1899
+ removeBtn.type = "button";
1900
+ removeBtn.className =
1901
+ "remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
1902
+ removeBtn.textContent = "✕";
1583
1903
  removeBtn.onclick = () => {
1584
1904
  const currentIndex = Array.from(container.children).indexOf(item);
1585
1905
  if (container.children.length > minCount) {
@@ -1594,8 +1914,8 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1594
1914
  }
1595
1915
  const disabled = currentCount <= minCount;
1596
1916
  removeBtn.disabled = disabled;
1597
- removeBtn.style.opacity = disabled ? '0.5' : '1';
1598
- removeBtn.style.pointerEvents = disabled ? 'none' : 'auto';
1917
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
1918
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1599
1919
  });
1600
1920
  }
1601
1921
 
@@ -1606,8 +1926,9 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1606
1926
  if (!state.config.readonly && values.length < maxCount) {
1607
1927
  const addBtn = document.createElement("button");
1608
1928
  addBtn.type = "button";
1609
- addBtn.className = "add-text-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
1610
- addBtn.textContent = `+ Add ${element.label || 'Text'}`;
1929
+ addBtn.className =
1930
+ "add-text-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
1931
+ addBtn.textContent = `+ Add ${element.label || "Text"}`;
1611
1932
  addBtn.onclick = () => {
1612
1933
  values.push(element.default || "");
1613
1934
  addTextItem(element.default || "");
@@ -1619,7 +1940,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1619
1940
  }
1620
1941
 
1621
1942
  // Render initial items
1622
- values.forEach(value => addTextItem(value));
1943
+ values.forEach((value) => addTextItem(value));
1623
1944
  updateAddButton();
1624
1945
  updateRemoveButtons();
1625
1946
 
@@ -1679,7 +2000,8 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1679
2000
  itemWrapper.className = "multiple-textarea-item";
1680
2001
 
1681
2002
  const textareaInput = document.createElement("textarea");
1682
- 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";
2003
+ textareaInput.className =
2004
+ "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";
1683
2005
  textareaInput.placeholder = element.placeholder || "Enter text";
1684
2006
  textareaInput.rows = element.rows || 4;
1685
2007
  textareaInput.value = value;
@@ -1701,15 +2023,16 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1701
2023
 
1702
2024
  function updateRemoveButtons() {
1703
2025
  if (state.config.readonly) return;
1704
- const items = container.querySelectorAll('.multiple-textarea-item');
2026
+ const items = container.querySelectorAll(".multiple-textarea-item");
1705
2027
  const currentCount = items.length;
1706
2028
  items.forEach((item) => {
1707
- let removeBtn = item.querySelector('.remove-item-btn');
2029
+ let removeBtn = item.querySelector(".remove-item-btn");
1708
2030
  if (!removeBtn) {
1709
- removeBtn = document.createElement('button');
1710
- removeBtn.type = 'button';
1711
- removeBtn.className = 'remove-item-btn mt-1 px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm';
1712
- removeBtn.innerHTML = '✕ Remove';
2031
+ removeBtn = document.createElement("button");
2032
+ removeBtn.type = "button";
2033
+ removeBtn.className =
2034
+ "remove-item-btn mt-1 px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm";
2035
+ removeBtn.textContent = "✕ Remove";
1713
2036
  removeBtn.onclick = () => {
1714
2037
  const currentIndex = Array.from(container.children).indexOf(item);
1715
2038
  if (container.children.length > minCount) {
@@ -1724,8 +2047,8 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1724
2047
  }
1725
2048
  const disabled = currentCount <= minCount;
1726
2049
  removeBtn.disabled = disabled;
1727
- removeBtn.style.opacity = disabled ? '0.5' : '1';
1728
- removeBtn.style.pointerEvents = disabled ? 'none' : 'auto';
2050
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
2051
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1729
2052
  });
1730
2053
  }
1731
2054
 
@@ -1736,8 +2059,9 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1736
2059
  if (!state.config.readonly && values.length < maxCount) {
1737
2060
  const addBtn = document.createElement("button");
1738
2061
  addBtn.type = "button";
1739
- 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";
1740
- addBtn.textContent = `+ Add ${element.label || 'Textarea'}`;
2062
+ addBtn.className =
2063
+ "add-textarea-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
2064
+ addBtn.textContent = `+ Add ${element.label || "Textarea"}`;
1741
2065
  addBtn.onclick = () => {
1742
2066
  values.push(element.default || "");
1743
2067
  addTextareaItem(element.default || "");
@@ -1749,7 +2073,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1749
2073
  }
1750
2074
 
1751
2075
  // Render initial items
1752
- values.forEach(value => addTextareaItem(value));
2076
+ values.forEach((value) => addTextareaItem(value));
1753
2077
  updateAddButton();
1754
2078
  updateRemoveButtons();
1755
2079
 
@@ -1813,7 +2137,8 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1813
2137
 
1814
2138
  const numberInput = document.createElement("input");
1815
2139
  numberInput.type = "number";
1816
- 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";
2140
+ numberInput.className =
2141
+ "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1817
2142
  numberInput.placeholder = element.placeholder || "0";
1818
2143
  if (element.min !== undefined) numberInput.min = element.min;
1819
2144
  if (element.max !== undefined) numberInput.max = element.max;
@@ -1837,15 +2162,16 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1837
2162
 
1838
2163
  function updateRemoveButtons() {
1839
2164
  if (state.config.readonly) return;
1840
- const items = container.querySelectorAll('.multiple-number-item');
2165
+ const items = container.querySelectorAll(".multiple-number-item");
1841
2166
  const currentCount = items.length;
1842
2167
  items.forEach((item) => {
1843
- let removeBtn = item.querySelector('.remove-item-btn');
2168
+ let removeBtn = item.querySelector(".remove-item-btn");
1844
2169
  if (!removeBtn) {
1845
- removeBtn = document.createElement('button');
1846
- removeBtn.type = 'button';
1847
- removeBtn.className = 'remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded';
1848
- removeBtn.innerHTML = '✕';
2170
+ removeBtn = document.createElement("button");
2171
+ removeBtn.type = "button";
2172
+ removeBtn.className =
2173
+ "remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
2174
+ removeBtn.textContent = "✕";
1849
2175
  removeBtn.onclick = () => {
1850
2176
  const currentIndex = Array.from(container.children).indexOf(item);
1851
2177
  if (container.children.length > minCount) {
@@ -1860,8 +2186,8 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1860
2186
  }
1861
2187
  const disabled = currentCount <= minCount;
1862
2188
  removeBtn.disabled = disabled;
1863
- removeBtn.style.opacity = disabled ? '0.5' : '1';
1864
- removeBtn.style.pointerEvents = disabled ? 'none' : 'auto';
2189
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
2190
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1865
2191
  });
1866
2192
  }
1867
2193
 
@@ -1872,8 +2198,9 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1872
2198
  if (!state.config.readonly && values.length < maxCount) {
1873
2199
  const addBtn = document.createElement("button");
1874
2200
  addBtn.type = "button";
1875
- 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";
1876
- addBtn.textContent = `+ Add ${element.label || 'Number'}`;
2201
+ addBtn.className =
2202
+ "add-number-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
2203
+ addBtn.textContent = `+ Add ${element.label || "Number"}`;
1877
2204
  addBtn.onclick = () => {
1878
2205
  values.push(element.default || "");
1879
2206
  addNumberItem(element.default || "");
@@ -1885,7 +2212,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1885
2212
  }
1886
2213
 
1887
2214
  // Render initial items
1888
- values.forEach(value => addNumberItem(value));
2215
+ values.forEach((value) => addNumberItem(value));
1889
2216
  updateAddButton();
1890
2217
  updateRemoveButtons();
1891
2218
 
@@ -1931,7 +2258,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1931
2258
  const maxCount = element.maxCount ?? 10;
1932
2259
 
1933
2260
  while (values.length < minCount) {
1934
- values.push(element.default || (element.options?.[0]?.value || ""));
2261
+ values.push(element.default || element.options?.[0]?.value || "");
1935
2262
  }
1936
2263
 
1937
2264
  const container = document.createElement("div");
@@ -1953,7 +2280,8 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1953
2280
  itemWrapper.className = "multiple-select-item flex items-center gap-2";
1954
2281
 
1955
2282
  const selectInput = document.createElement("select");
1956
- 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";
2283
+ selectInput.className =
2284
+ "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1957
2285
  selectInput.disabled = state.config.readonly;
1958
2286
 
1959
2287
  // Add options
@@ -1983,15 +2311,16 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1983
2311
 
1984
2312
  function updateRemoveButtons() {
1985
2313
  if (state.config.readonly) return;
1986
- const items = container.querySelectorAll('.multiple-select-item');
2314
+ const items = container.querySelectorAll(".multiple-select-item");
1987
2315
  const currentCount = items.length;
1988
2316
  items.forEach((item) => {
1989
- let removeBtn = item.querySelector('.remove-item-btn');
2317
+ let removeBtn = item.querySelector(".remove-item-btn");
1990
2318
  if (!removeBtn) {
1991
- removeBtn = document.createElement('button');
1992
- removeBtn.type = 'button';
1993
- removeBtn.className = 'remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded';
1994
- removeBtn.innerHTML = '✕';
2319
+ removeBtn = document.createElement("button");
2320
+ removeBtn.type = "button";
2321
+ removeBtn.className =
2322
+ "remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
2323
+ removeBtn.textContent = "✕";
1995
2324
  removeBtn.onclick = () => {
1996
2325
  const currentIndex = Array.from(container.children).indexOf(item);
1997
2326
  if (container.children.length > minCount) {
@@ -2006,8 +2335,8 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
2006
2335
  }
2007
2336
  const disabled = currentCount <= minCount;
2008
2337
  removeBtn.disabled = disabled;
2009
- removeBtn.style.opacity = disabled ? '0.5' : '1';
2010
- removeBtn.style.pointerEvents = disabled ? 'none' : 'auto';
2338
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
2339
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
2011
2340
  });
2012
2341
  }
2013
2342
 
@@ -2018,10 +2347,12 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
2018
2347
  if (!state.config.readonly && values.length < maxCount) {
2019
2348
  const addBtn = document.createElement("button");
2020
2349
  addBtn.type = "button";
2021
- 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";
2022
- addBtn.textContent = `+ Add ${element.label || 'Selection'}`;
2350
+ addBtn.className =
2351
+ "add-select-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
2352
+ addBtn.textContent = `+ Add ${element.label || "Selection"}`;
2023
2353
  addBtn.onclick = () => {
2024
- const defaultValue = element.default || (element.options?.[0]?.value || "");
2354
+ const defaultValue =
2355
+ element.default || element.options?.[0]?.value || "";
2025
2356
  values.push(defaultValue);
2026
2357
  addSelectItem(defaultValue);
2027
2358
  updateAddButton();
@@ -2032,7 +2363,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
2032
2363
  }
2033
2364
 
2034
2365
  // Render initial items
2035
- values.forEach(value => addSelectItem(value));
2366
+ values.forEach((value) => addSelectItem(value));
2036
2367
  updateAddButton();
2037
2368
  updateRemoveButtons();
2038
2369
 
@@ -2054,7 +2385,11 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2054
2385
  const emptyState = document.createElement("div");
2055
2386
  emptyState.className =
2056
2387
  "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
2057
- emptyState.innerHTML = `<div class="text-center">${t("noFileSelected")}</div>`;
2388
+ // Create empty state content safely
2389
+ const textDiv = document.createElement("div");
2390
+ textDiv.className = "text-center";
2391
+ textDiv.textContent = t("noFileSelected");
2392
+ emptyState.appendChild(textDiv);
2058
2393
  wrapper.appendChild(emptyState);
2059
2394
  }
2060
2395
  } else {
@@ -2114,8 +2449,14 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2114
2449
  // Add upload text
2115
2450
  const uploadText = document.createElement("p");
2116
2451
  uploadText.className = "text-xs text-gray-600 mt-2 text-center";
2117
- uploadText.innerHTML = `<span class="underline cursor-pointer">${t("uploadText")}</span> ${t("dragDropTextSingle")}`;
2118
- uploadText.querySelector("span").onclick = () => picker.click();
2452
+ // Create upload text content safely
2453
+ const uploadSpan = document.createElement("span");
2454
+ uploadSpan.className = "underline cursor-pointer";
2455
+ uploadSpan.textContent = t("uploadText");
2456
+ uploadSpan.onclick = () => picker.click();
2457
+
2458
+ uploadText.appendChild(uploadSpan);
2459
+ uploadText.appendChild(document.createTextNode(" " + t("dragDropTextSingle")));
2119
2460
  fileWrapper.appendChild(uploadText);
2120
2461
 
2121
2462
  // Add hint
@@ -2174,14 +2515,29 @@ function handleInitialFileData(
2174
2515
  }
2175
2516
 
2176
2517
  function setEmptyFileContainer(fileContainer) {
2177
- fileContainer.innerHTML = `
2178
- <div class="flex flex-col items-center justify-center h-full text-gray-400">
2179
- <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
2180
- <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"/>
2181
- </svg>
2182
- <div class="text-sm text-center">${t("clickDragText")}</div>
2183
- </div>
2184
- `;
2518
+ // Create empty file container content safely
2519
+ clear(fileContainer);
2520
+
2521
+ const wrapper = document.createElement("div");
2522
+ wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
2523
+
2524
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2525
+ svg.setAttribute("class", "w-6 h-6 mb-2");
2526
+ svg.setAttribute("fill", "currentColor");
2527
+ svg.setAttribute("viewBox", "0 0 24 24");
2528
+
2529
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
2530
+ path.setAttribute("d", "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z");
2531
+
2532
+ svg.appendChild(path);
2533
+
2534
+ const textDiv = document.createElement("div");
2535
+ textDiv.className = "text-sm text-center";
2536
+ textDiv.textContent = t("clickDragText");
2537
+
2538
+ wrapper.appendChild(svg);
2539
+ wrapper.appendChild(textDiv);
2540
+ fileContainer.appendChild(wrapper);
2185
2541
  }
2186
2542
 
2187
2543
  function renderFilesElement(element, ctx, wrapper, pathKey) {
@@ -2198,7 +2554,16 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2198
2554
  resultsWrapper.appendChild(filePreview);
2199
2555
  });
2200
2556
  } else {
2201
- resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${t("noFilesSelected")}</div></div>`;
2557
+ // Create empty state safely
2558
+ const emptyDiv = document.createElement("div");
2559
+ emptyDiv.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
2560
+
2561
+ const textDiv = document.createElement("div");
2562
+ textDiv.className = "text-center";
2563
+ textDiv.textContent = t("noFilesSelected");
2564
+
2565
+ emptyDiv.appendChild(textDiv);
2566
+ resultsWrapper.appendChild(emptyDiv);
2202
2567
  }
2203
2568
 
2204
2569
  wrapper.appendChild(resultsWrapper);
@@ -2279,7 +2644,16 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2279
2644
  resultsWrapper.appendChild(filePreview);
2280
2645
  });
2281
2646
  } else {
2282
- resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${t("noFilesSelected")}</div></div>`;
2647
+ // Create empty state safely
2648
+ const emptyDiv = document.createElement("div");
2649
+ emptyDiv.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
2650
+
2651
+ const textDiv = document.createElement("div");
2652
+ textDiv.className = "text-center";
2653
+ textDiv.textContent = t("noFilesSelected");
2654
+
2655
+ emptyDiv.appendChild(textDiv);
2656
+ resultsWrapper.appendChild(emptyDiv);
2283
2657
  }
2284
2658
 
2285
2659
  wrapper.appendChild(resultsWrapper);
@@ -2321,17 +2695,18 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2321
2695
  // Show count and min/max info
2322
2696
  const countInfo = document.createElement("div");
2323
2697
  countInfo.className = "text-xs text-gray-500 mt-2";
2324
- const countText = `${initialFiles.length} file${initialFiles.length !== 1 ? 's' : ''}`;
2325
- const minMaxText = minFiles > 0 || maxFiles < Infinity
2326
- ? ` (${minFiles}-${maxFiles} allowed)`
2327
- : '';
2698
+ const countText = `${initialFiles.length} file${initialFiles.length !== 1 ? "s" : ""}`;
2699
+ const minMaxText =
2700
+ minFiles > 0 || maxFiles < Infinity
2701
+ ? ` (${minFiles}-${maxFiles} allowed)`
2702
+ : "";
2328
2703
  countInfo.textContent = countText + minMaxText;
2329
2704
 
2330
2705
  // Remove previous count info
2331
- const existingCount = filesWrapper.querySelector('.file-count-info');
2706
+ const existingCount = filesWrapper.querySelector(".file-count-info");
2332
2707
  if (existingCount) existingCount.remove();
2333
2708
 
2334
- countInfo.className += ' file-count-info';
2709
+ countInfo.className += " file-count-info";
2335
2710
  filesWrapper.appendChild(countInfo);
2336
2711
  };
2337
2712
 
@@ -2515,7 +2890,18 @@ function renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap) {
2515
2890
  addBtn.type = "button";
2516
2891
  addBtn.className =
2517
2892
  "w-full py-2 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 flex items-center justify-center mt-3";
2518
- addBtn.innerHTML = `<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>${t("addElement")}`;
2893
+ // Create add button content safely
2894
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2895
+ svg.setAttribute("class", "w-4 h-4 mr-2");
2896
+ svg.setAttribute("fill", "currentColor");
2897
+ svg.setAttribute("viewBox", "0 0 24 24");
2898
+
2899
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
2900
+ path.setAttribute("d", "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z");
2901
+
2902
+ svg.appendChild(path);
2903
+ addBtn.appendChild(svg);
2904
+ addBtn.appendChild(document.createTextNode(t("addElement")));
2519
2905
  groupWrap.appendChild(addBtn);
2520
2906
  }
2521
2907
 
@@ -2523,7 +2909,16 @@ function renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap) {
2523
2909
  if (state.config.readonly) return;
2524
2910
  const n = countItems();
2525
2911
  if (addBtn) addBtn.disabled = n >= max;
2526
- left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-slate-500 dark:text-slate-400 text-xs">[${n} / ${max === Infinity ? "∞" : max}, min=${min}]</span>`;
2912
+ // Create elements safely
2913
+ clear(left);
2914
+ const labelSpan = document.createElement("span");
2915
+ labelSpan.textContent = element.label || element.key;
2916
+ const countSpan = document.createElement("span");
2917
+ countSpan.className = "text-slate-500 dark:text-slate-400 text-xs";
2918
+ countSpan.textContent = `[${n} / ${max === Infinity ? "∞" : max}, min=${min}]`;
2919
+ left.appendChild(labelSpan);
2920
+ left.appendChild(document.createTextNode(" "));
2921
+ left.appendChild(countSpan);
2527
2922
  };
2528
2923
 
2529
2924
  if (pre && pre.length) {
@@ -2538,7 +2933,10 @@ function renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap) {
2538
2933
  addBtn.addEventListener("click", () => addItem(null));
2539
2934
  } else {
2540
2935
  // In readonly mode, just show the label without count controls
2541
- left.innerHTML = `<span>${element.label || element.key}</span>`;
2936
+ clear(left);
2937
+ const labelSpan = document.createElement("span");
2938
+ labelSpan.textContent = element.label || element.key;
2939
+ left.appendChild(labelSpan);
2542
2940
  }
2543
2941
  }
2544
2942
 
@@ -2555,7 +2953,10 @@ function renderSingleGroup(element, ctx, itemsWrap, left, groupWrap) {
2555
2953
  }
2556
2954
  });
2557
2955
  groupWrap.appendChild(itemsWrap);
2558
- left.innerHTML = `<span>${element.label || element.key}</span>`;
2956
+ clear(left);
2957
+ const labelSpan = document.createElement("span");
2958
+ labelSpan.textContent = element.label || element.key;
2959
+ left.appendChild(labelSpan);
2559
2960
  }
2560
2961
 
2561
2962
  function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
@@ -2588,7 +2989,10 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
2588
2989
  }
2589
2990
  });
2590
2991
  containerWrap.appendChild(itemsWrap);
2591
- left.innerHTML = `<span>${element.label || element.key}</span>`;
2992
+ clear(left);
2993
+ const labelSpan = document.createElement("span");
2994
+ labelSpan.textContent = element.label || element.key;
2995
+ left.appendChild(labelSpan);
2592
2996
 
2593
2997
  wrapper.appendChild(containerWrap);
2594
2998
  }
@@ -2639,7 +3043,8 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2639
3043
  prefill: {},
2640
3044
  };
2641
3045
  const item = document.createElement("div");
2642
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
3046
+ item.className =
3047
+ "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2643
3048
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
2644
3049
 
2645
3050
  element.elements.forEach((child) => {
@@ -2678,7 +3083,16 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2678
3083
  addBtn.disabled = currentCount >= max;
2679
3084
  addBtn.style.opacity = currentCount >= max ? "0.5" : "1";
2680
3085
  }
2681
- left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-sm text-gray-500">(${currentCount}/${max === Infinity ? '∞' : max})</span>`;
3086
+ // Create elements safely
3087
+ clear(left);
3088
+ const labelSpan = document.createElement("span");
3089
+ labelSpan.textContent = element.label || element.key;
3090
+ const countSpan = document.createElement("span");
3091
+ countSpan.className = "text-sm text-gray-500";
3092
+ countSpan.textContent = `(${currentCount}/${max === Infinity ? "∞" : max})`;
3093
+ left.appendChild(labelSpan);
3094
+ left.appendChild(document.createTextNode(" "));
3095
+ left.appendChild(countSpan);
2682
3096
  };
2683
3097
 
2684
3098
  if (!state.config.readonly) {
@@ -2693,7 +3107,8 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2693
3107
  prefill: prefillObj || {},
2694
3108
  };
2695
3109
  const item = document.createElement("div");
2696
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
3110
+ item.className =
3111
+ "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2697
3112
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
2698
3113
 
2699
3114
  element.elements.forEach((child) => {
@@ -2731,7 +3146,8 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2731
3146
  prefill: {},
2732
3147
  };
2733
3148
  const item = document.createElement("div");
2734
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
3149
+ item.className =
3150
+ "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2735
3151
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
2736
3152
 
2737
3153
  element.elements.forEach((child) => {
@@ -2797,17 +3213,29 @@ function renderFilePreviewReadonly(resourceId, fileName) {
2797
3213
  try {
2798
3214
  const thumbnailUrl = state.config.getThumbnail(resourceId);
2799
3215
  if (thumbnailUrl) {
2800
- previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${actualFileName}" class="w-full h-auto">`;
3216
+ const img = document.createElement("img");
3217
+ img.src = thumbnailUrl;
3218
+ img.alt = actualFileName || "";
3219
+ img.className = "w-full h-auto";
3220
+ clear(previewContainer);
3221
+ previewContainer.appendChild(img);
2801
3222
  } else {
2802
3223
  // Fallback to icon if getThumbnail returns null/undefined
2803
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🖼️</div><div class="text-sm">${actualFileName}</div></div></div>`;
3224
+ clear(previewContainer);
3225
+ previewContainer.appendChild(
3226
+ createPreviewElement("🖼️", actualFileName),
3227
+ );
2804
3228
  }
2805
3229
  } catch (error) {
2806
3230
  console.warn("getThumbnail failed for", resourceId, error);
2807
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🖼️</div><div class="text-sm">${actualFileName}</div></div></div>`;
3231
+ clear(previewContainer);
3232
+ previewContainer.appendChild(
3233
+ createPreviewElement("🖼️", actualFileName),
3234
+ );
2808
3235
  }
2809
3236
  } else {
2810
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🖼️</div><div class="text-sm">${actualFileName}</div></div></div>`;
3237
+ clear(previewContainer);
3238
+ previewContainer.appendChild(createPreviewElement("🖼️", actualFileName));
2811
3239
  }
2812
3240
  } else if (isVideo) {
2813
3241
  // Video preview - try getThumbnail for video URL
@@ -2815,34 +3243,76 @@ function renderFilePreviewReadonly(resourceId, fileName) {
2815
3243
  try {
2816
3244
  const videoUrl = state.config.getThumbnail(resourceId);
2817
3245
  if (videoUrl) {
2818
- previewContainer.innerHTML = `
2819
- <div class="relative group">
2820
- <video class="w-full h-auto" controls preload="auto" muted>
2821
- <source src="${videoUrl}" type="${meta?.type || "video/mp4"}">
2822
- Ваш браузер не поддерживает видео.
2823
- </video>
2824
- <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">
2825
- <div class="bg-white bg-opacity-90 rounded-full p-3">
2826
- <svg class="w-8 h-8 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
2827
- <path d="M8 5v14l11-7z"/>
2828
- </svg>
2829
- </div>
2830
- </div>
2831
- </div>
2832
- `;
3246
+ // Create video elements safely
3247
+ const wrapper = document.createElement("div");
3248
+ wrapper.className = "relative group";
3249
+
3250
+ const video = document.createElement("video");
3251
+ video.className = "w-full h-auto";
3252
+ video.controls = true;
3253
+ video.preload = "auto";
3254
+ video.muted = true;
3255
+
3256
+ const source = document.createElement("source");
3257
+ source.src = videoUrl;
3258
+ source.type = meta?.type || "video/mp4";
3259
+
3260
+ video.appendChild(source);
3261
+ video.appendChild(
3262
+ document.createTextNode("Ваш браузер не поддерживает видео."),
3263
+ );
3264
+
3265
+ const overlay = document.createElement("div");
3266
+ overlay.className =
3267
+ "absolute inset-0 bg-black bg-opacity-20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none";
3268
+
3269
+ const overlayContent = document.createElement("div");
3270
+ overlayContent.className = "bg-white bg-opacity-90 rounded-full p-3";
3271
+
3272
+ const svg = document.createElementNS(
3273
+ "http://www.w3.org/2000/svg",
3274
+ "svg",
3275
+ );
3276
+ svg.setAttribute("class", "w-8 h-8 text-gray-800");
3277
+ svg.setAttribute("fill", "currentColor");
3278
+ svg.setAttribute("viewBox", "0 0 24 24");
3279
+
3280
+ const path = document.createElementNS(
3281
+ "http://www.w3.org/2000/svg",
3282
+ "path",
3283
+ );
3284
+ path.setAttribute("d", "M8 5v14l11-7z");
3285
+
3286
+ svg.appendChild(path);
3287
+ overlayContent.appendChild(svg);
3288
+ overlay.appendChild(overlayContent);
3289
+
3290
+ wrapper.appendChild(video);
3291
+ wrapper.appendChild(overlay);
3292
+
3293
+ clear(previewContainer);
3294
+ previewContainer.appendChild(wrapper);
2833
3295
  } else {
2834
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🎥</div><div class="text-sm">${actualFileName}</div></div></div>`;
3296
+ clear(previewContainer);
3297
+ previewContainer.appendChild(
3298
+ createPreviewElement("🎥", actualFileName),
3299
+ );
2835
3300
  }
2836
3301
  } catch (error) {
2837
3302
  console.warn("getThumbnail failed for video", resourceId, error);
2838
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🎥</div><div class="text-sm">${actualFileName}</div></div></div>`;
3303
+ clear(previewContainer);
3304
+ previewContainer.appendChild(
3305
+ createPreviewElement("🎥", actualFileName),
3306
+ );
2839
3307
  }
2840
3308
  } else {
2841
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🎥</div><div class="text-sm">${actualFileName}</div></div></div>`;
3309
+ clear(previewContainer);
3310
+ previewContainer.appendChild(createPreviewElement("🎥", actualFileName));
2842
3311
  }
2843
3312
  } else {
2844
3313
  // Other file types
2845
- previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">📁</div><div class="text-sm">${actualFileName}</div></div></div>`;
3314
+ clear(previewContainer);
3315
+ previewContainer.appendChild(createPreviewElement("📁", actualFileName));
2846
3316
  }
2847
3317
 
2848
3318
  // File name
@@ -3008,6 +3478,8 @@ function saveDraft() {
3008
3478
 
3009
3479
  function clearForm() {
3010
3480
  if (state.formRoot) {
3481
+ // Clean up any existing object URLs before clearing form
3482
+ revokeAllObjectURLs();
3011
3483
  clear(state.formRoot);
3012
3484
  }
3013
3485
  }