@dmitryvim/form-builder 0.1.34 → 0.1.37

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,61 +1,10 @@
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
-
54
2
  // State management
55
3
  const state = {
56
4
  schema: null,
57
5
  formRoot: null,
58
6
  resourceIndex: new Map(),
7
+ externalActions: null, // Store external actions for the current form
59
8
  version: "1.0.0",
60
9
  config: {
61
10
  // File upload configuration
@@ -181,7 +130,7 @@ function validateSchema(schema) {
181
130
  }
182
131
 
183
132
  // Form rendering
184
- function renderForm(schema, prefill) {
133
+ function renderForm(schema, prefill, actions) {
185
134
  const errors = validateSchema(schema);
186
135
  if (errors.length > 0) {
187
136
  console.error("Schema validation errors:", errors);
@@ -189,13 +138,13 @@ function renderForm(schema, prefill) {
189
138
  }
190
139
 
191
140
  state.schema = schema;
141
+ state.externalActions = actions || null;
142
+
192
143
  if (!state.formRoot) {
193
144
  console.error("No form root element set. Call setFormRoot() first.");
194
145
  return;
195
146
  }
196
147
 
197
- // Clean up any existing object URLs before clearing form
198
- revokeAllObjectURLs();
199
148
  clear(state.formRoot);
200
149
 
201
150
  const formEl = document.createElement("div");
@@ -214,11 +163,20 @@ function renderForm(schema, prefill) {
214
163
  });
215
164
 
216
165
  state.formRoot.appendChild(formEl);
166
+
167
+ // Render external actions after form is built (only in readonly mode)
168
+ if (
169
+ state.config.readonly &&
170
+ state.externalActions &&
171
+ Array.isArray(state.externalActions)
172
+ ) {
173
+ renderExternalActions();
174
+ }
217
175
  }
218
176
 
219
177
  function renderElement(element, ctx) {
220
178
  const wrapper = document.createElement("div");
221
- wrapper.className = "mb-6";
179
+ wrapper.className = "mb-6 fb-field-wrapper";
222
180
 
223
181
  const label = document.createElement("div");
224
182
  label.className = "flex items-center mb-2";
@@ -238,17 +196,8 @@ function renderElement(element, ctx) {
238
196
  const infoBtn = document.createElement("button");
239
197
  infoBtn.type = "button";
240
198
  infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
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);
199
+ infoBtn.innerHTML =
200
+ '<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>';
252
201
 
253
202
  // Create tooltip
254
203
  const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
@@ -356,7 +305,7 @@ function renderElement(element, ctx) {
356
305
  const actionBtn = document.createElement("button");
357
306
  actionBtn.type = "button";
358
307
  actionBtn.className =
359
- "px-3 py-1.5 text-sm bg-blue-50 border border-blue-200 text-blue-700 rounded-lg hover:bg-blue-100 transition-colors";
308
+ "px-3 py-2 text-sm border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors";
360
309
  actionBtn.textContent = action.label;
361
310
 
362
311
  actionBtn.addEventListener("click", (e) => {
@@ -367,7 +316,15 @@ function renderElement(element, ctx) {
367
316
  state.config.actionHandler &&
368
317
  typeof state.config.actionHandler === "function"
369
318
  ) {
370
- state.config.actionHandler(action.value);
319
+ // For schema-based actions (old system), call with just the value for backward compatibility
320
+ // Check if the handler expects 2 parameters (new system) or 1 parameter (old system)
321
+ if (state.config.actionHandler.length > 1) {
322
+ // New system: pass related_field (element key) and value
323
+ state.config.actionHandler(element.key, action.value);
324
+ } else {
325
+ // Old system: pass only value for backward compatibility
326
+ state.config.actionHandler(action.value);
327
+ }
371
328
  }
372
329
  });
373
330
 
@@ -479,8 +436,8 @@ async function renderFilePreview(container, resourceId, options = {}) {
479
436
  reader.readAsDataURL(meta.file);
480
437
  container.appendChild(img);
481
438
  } else if (meta.type && meta.type.startsWith("video/")) {
482
- // Video file - use managed object URL for preview
483
- const videoUrl = createObjectURL(meta.file, resourceId);
439
+ // Video file - use object URL for preview
440
+ const videoUrl = URL.createObjectURL(meta.file);
484
441
 
485
442
  // Remove all conflicting handlers to prevent interference with video controls
486
443
  container.onclick = null;
@@ -490,55 +447,39 @@ async function renderFilePreview(container, resourceId, options = {}) {
490
447
  container.parentNode.replaceChild(newContainer, container);
491
448
  container = newContainer;
492
449
 
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);
450
+ container.innerHTML = `
451
+ <div class="relative group h-full">
452
+ <video class="w-full h-full object-contain" controls preload="auto" muted>
453
+ <source src="${videoUrl}" type="${meta.type}">
454
+ Your browser does not support the video tag.
455
+ </video>
456
+ <div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
457
+ <button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
458
+ ${t("removeElement")}
459
+ </button>
460
+ <button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
461
+ Change
462
+ </button>
463
+ </div>
464
+ </div>
465
+ `;
529
466
 
530
467
  // Add click handlers to the custom buttons
531
- changeBtn.onclick = (e) => {
532
- e.stopPropagation();
533
- if (deps?.picker) {
534
- deps.picker.click();
535
- }
536
- };
468
+ const changeBtn = container.querySelector(".change-file-btn");
469
+ if (changeBtn) {
470
+ changeBtn.onclick = (e) => {
471
+ e.stopPropagation();
472
+ if (deps?.picker) {
473
+ deps.picker.click();
474
+ }
475
+ };
476
+ }
537
477
 
538
- deleteBtn.onclick = (e) => {
478
+ const deleteBtn = container.querySelector(".delete-file-btn");
479
+ if (deleteBtn) {
480
+ deleteBtn.onclick = (e) => {
539
481
  e.stopPropagation();
540
- // Clear the file and revoke object URL
541
- revokeObjectURL(resourceId);
482
+ // Clear the file
542
483
  state.resourceIndex.delete(resourceId);
543
484
  // Update hidden input
544
485
  const hiddenInput = container.parentElement.querySelector(
@@ -554,55 +495,27 @@ async function renderFilePreview(container, resourceId, options = {}) {
554
495
  if (deps?.dragHandler) {
555
496
  setupDragAndDrop(container, deps.dragHandler);
556
497
  }
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);
498
+ container.innerHTML = `
499
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
500
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
501
+ <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"/>
502
+ </svg>
503
+ <div class="text-sm text-center">${t("clickDragText")}</div>
504
+ </div>
505
+ `;
580
506
  };
507
+ }
581
508
  } else {
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);
509
+ // Non-image, non-video file
510
+ 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">${
511
+ fileName
512
+ }</div></div>`;
599
513
  }
600
514
 
601
515
  // Add delete button for edit mode (except for videos which have custom buttons)
602
516
  if (!isReadonly && !(meta && meta.type && meta.type.startsWith("video/"))) {
603
517
  addDeleteButton(container, () => {
604
- // Clear the file and revoke object URL
605
- revokeObjectURL(resourceId);
518
+ // Clear the file
606
519
  state.resourceIndex.delete(resourceId);
607
520
  // Update hidden input
608
521
  const hiddenInput = container.parentElement.querySelector(
@@ -611,29 +524,15 @@ async function renderFilePreview(container, resourceId, options = {}) {
611
524
  if (hiddenInput) {
612
525
  hiddenInput.value = "";
613
526
  }
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);
527
+ // Clear preview and show placeholder
528
+ container.innerHTML = `
529
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
530
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
531
+ <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"/>
532
+ </svg>
533
+ <div class="text-sm text-center">${t("clickDragText")}</div>
534
+ </div>
535
+ `;
637
536
  });
638
537
  }
639
538
  } else if (state.config.getThumbnail) {
@@ -646,50 +545,21 @@ async function renderFilePreview(container, resourceId, options = {}) {
646
545
  container.appendChild(img);
647
546
  } else {
648
547
  // Fallback to file icon
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);
548
+ 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">${
549
+ fileName
550
+ }</div></div>`;
666
551
  }
667
552
  } catch (error) {
668
553
  console.warn("Thumbnail loading failed:", error);
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);
554
+ 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">${
555
+ fileName
556
+ }</div></div>`;
686
557
  }
687
558
 
688
559
  // Add delete button for edit mode
689
560
  if (!isReadonly) {
690
561
  addDeleteButton(container, () => {
691
- // Clear the file and revoke object URL
692
- revokeObjectURL(resourceId);
562
+ // Clear the file
693
563
  state.resourceIndex.delete(resourceId);
694
564
  // Update hidden input
695
565
  const hiddenInput = container.parentElement.querySelector(
@@ -698,35 +568,22 @@ async function renderFilePreview(container, resourceId, options = {}) {
698
568
  if (hiddenInput) {
699
569
  hiddenInput.value = "";
700
570
  }
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);
571
+ // Clear preview and show placeholder
572
+ container.innerHTML = `
573
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
574
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
575
+ <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"/>
576
+ </svg>
577
+ <div class="text-sm text-center">${t("clickDragText")}</div>
578
+ </div>
579
+ `;
724
580
  });
725
581
  }
726
582
  } else {
727
- // No file and no getThumbnail config - fallback - create elements safely
728
- clear(container);
729
- container.appendChild(createPreviewElement("🖼️", fileName));
583
+ // No file and no getThumbnail config - fallback
584
+ 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">${
585
+ fileName
586
+ }</div></div>`;
730
587
  }
731
588
 
732
589
  // Add click handler for download in readonly mode
@@ -873,179 +730,76 @@ function renderResourcePills(container, rids, onRemove) {
873
730
  img.src = url;
874
731
  slot.appendChild(img);
875
732
  } else {
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);
733
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
734
+ <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
735
+ <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"/>
736
+ </svg>
737
+ </div>`;
891
738
  }
892
739
  } else {
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);
740
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
741
+ <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
742
+ <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"/>
743
+ </svg>
744
+ </div>`;
908
745
  }
909
746
  } else if (meta && meta.type?.startsWith("video/")) {
910
747
  if (meta.file && meta.file instanceof File) {
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);
748
+ // Video file - use object URL for preview in thumbnail format
749
+ const videoUrl = URL.createObjectURL(meta.file);
750
+ slot.innerHTML = `
751
+ <div class="relative group h-full w-full">
752
+ <video class="w-full h-full object-contain" preload="metadata" muted>
753
+ <source src="${videoUrl}" type="${meta.type}">
754
+ </video>
755
+ <div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
756
+ <div class="bg-white bg-opacity-90 rounded-full p-1">
757
+ <svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
758
+ <path d="M8 5v14l11-7z"/>
759
+ </svg>
760
+ </div>
761
+ </div>
762
+ </div>
763
+ `;
948
764
  } else if (state.config.getThumbnail) {
949
765
  // Use getThumbnail for uploaded video files
950
766
  const videoUrl = state.config.getThumbnail(rid);
951
767
  if (videoUrl) {
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);
768
+ slot.innerHTML = `
769
+ <div class="relative group h-full w-full">
770
+ <video class="w-full h-full object-contain" preload="metadata" muted>
771
+ <source src="${videoUrl}" type="${meta.type}">
772
+ </video>
773
+ <div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
774
+ <div class="bg-white bg-opacity-90 rounded-full p-1">
775
+ <svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
776
+ <path d="M8 5v14l11-7z"/>
777
+ </svg>
778
+ </div>
779
+ </div>
780
+ </div>
781
+ `;
987
782
  } else {
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);
783
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
784
+ <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
785
+ <path d="M8 5v14l11-7z"/>
786
+ </svg>
787
+ <div class="text-xs mt-1">${meta?.name || "Video"}</div>
788
+ </div>`;
1009
789
  }
1010
790
  } else {
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);
791
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
792
+ <svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
793
+ <path d="M8 5v14l11-7z"/>
794
+ </svg>
795
+ <div class="text-xs mt-1">${meta?.name || "Video"}</div>
796
+ </div>`;
1032
797
  }
1033
798
  } else {
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);
799
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
800
+ <div class="text-2xl mb-1">📁</div>
801
+ <div class="text-xs">${meta?.name || "File"}</div>
802
+ </div>`;
1049
803
  }
1050
804
 
1051
805
  // Add remove button overlay (similar to file field)
@@ -1069,17 +823,8 @@ function renderResourcePills(container, rids, onRemove) {
1069
823
  // Empty slot placeholder
1070
824
  slot.className =
1071
825
  "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";
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);
826
+ slot.innerHTML =
827
+ '<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>';
1083
828
  slot.onclick = () => {
1084
829
  // Look for file input - check parent containers that have space-y-2 class
1085
830
  let filesWrapper = container.parentElement;
@@ -1141,37 +886,14 @@ async function handleFileSelect(file, container, fieldName, deps = null) {
1141
886
  });
1142
887
 
1143
888
  // Create hidden input to store the resource ID
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(
889
+ let hiddenInput = container.parentElement.querySelector(
1168
890
  'input[type="hidden"]',
1169
891
  );
1170
892
  if (!hiddenInput) {
1171
893
  hiddenInput = document.createElement("input");
1172
894
  hiddenInput.type = "hidden";
1173
895
  hiddenInput.name = fieldName;
1174
- parentElement.appendChild(hiddenInput);
896
+ container.parentElement.appendChild(hiddenInput);
1175
897
  }
1176
898
  hiddenInput.value = rid;
1177
899
 
@@ -1225,6 +947,125 @@ function addDeleteButton(container, onDelete) {
1225
947
  container.appendChild(overlay);
1226
948
  }
1227
949
 
950
+ // JSON path resolution for external actions (currently unused but kept for future use)
951
+ // eslint-disable-next-line no-unused-vars
952
+ function resolveFieldPath(path, formData) {
953
+ // Remove leading $input_data. prefix if present
954
+ const cleanPath = path.replace(/^\$input_data\./, "");
955
+
956
+ // Split path into segments, handling array notation
957
+ const segments = cleanPath.split(/[.[\]]/).filter(Boolean);
958
+
959
+ // Try to find the corresponding form element
960
+ return findElementByPath(segments, formData);
961
+ }
962
+
963
+ function findElementByPath(segments, data, currentPath = "") {
964
+ if (segments.length === 0) return currentPath;
965
+
966
+ const [head, ...tail] = segments;
967
+
968
+ // Check if this is an array index
969
+ const isArrayIndex = /^\d+$/.test(head);
970
+
971
+ if (isArrayIndex) {
972
+ // Array index case: build path like "fieldName[index]"
973
+ const newPath = currentPath ? `${currentPath}[${head}]` : `[${head}]`;
974
+ return findElementByPath(tail, data, newPath);
975
+ } else {
976
+ // Regular field name
977
+ const newPath = currentPath ? `${currentPath}.${head}` : head;
978
+ return findElementByPath(tail, data, newPath);
979
+ }
980
+ }
981
+
982
+ function findFormElementByFieldPath(fieldPath) {
983
+ // Try to find the form element that corresponds to the field path
984
+ // This looks for elements with name attributes that match the path pattern
985
+
986
+ if (!state.formRoot) return null;
987
+
988
+ // Try exact match first
989
+ let element = state.formRoot.querySelector(`[name="${fieldPath}"]`);
990
+ if (element) return element;
991
+
992
+ // Try with array notation variations
993
+ const variations = [
994
+ fieldPath,
995
+ fieldPath.replace(/\[(\d+)\]/g, "[$1]"), // normalize array notation
996
+ fieldPath.replace(/\./g, "[") +
997
+ "]".repeat((fieldPath.match(/\./g) || []).length), // convert dots to brackets
998
+ ];
999
+
1000
+ for (const variation of variations) {
1001
+ element = state.formRoot.querySelector(`[name="${variation}"]`);
1002
+ if (element) return element;
1003
+ }
1004
+
1005
+ return null;
1006
+ }
1007
+
1008
+ function renderExternalActions() {
1009
+ if (!state.externalActions || !Array.isArray(state.externalActions)) return;
1010
+
1011
+ state.externalActions.forEach((action) => {
1012
+ if (!action.related_field || !action.value || !action.label) return;
1013
+
1014
+ // Find the form element for this related field
1015
+ const fieldElement = findFormElementByFieldPath(action.related_field);
1016
+ if (!fieldElement) {
1017
+ console.warn(
1018
+ `External action: Could not find form element for field "${action.related_field}"`,
1019
+ );
1020
+ return;
1021
+ }
1022
+
1023
+ // Find the wrapper element that contains the field using stable class
1024
+ let wrapper = fieldElement.closest(".fb-field-wrapper");
1025
+ if (!wrapper) {
1026
+ wrapper = fieldElement.parentElement;
1027
+ }
1028
+
1029
+ if (!wrapper) {
1030
+ console.warn(
1031
+ `External action: Could not find wrapper for field "${action.related_field}"`,
1032
+ );
1033
+ return;
1034
+ }
1035
+
1036
+ // Check if we already added external actions to this wrapper
1037
+ if (wrapper.querySelector(".external-actions-container")) return;
1038
+
1039
+ // Create actions container
1040
+ const actionsContainer = document.createElement("div");
1041
+ actionsContainer.className =
1042
+ "external-actions-container mt-4 flex flex-wrap gap-2";
1043
+
1044
+ // Create action button
1045
+ const actionBtn = document.createElement("button");
1046
+ actionBtn.type = "button";
1047
+ actionBtn.className =
1048
+ "px-3 py-2 text-sm border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors";
1049
+ actionBtn.textContent = action.label;
1050
+
1051
+ actionBtn.addEventListener("click", (e) => {
1052
+ e.preventDefault();
1053
+ e.stopPropagation();
1054
+
1055
+ if (
1056
+ state.config.actionHandler &&
1057
+ typeof state.config.actionHandler === "function"
1058
+ ) {
1059
+ // Call with both related_field and value for the new actions system
1060
+ state.config.actionHandler(action.related_field, action.value);
1061
+ }
1062
+ });
1063
+
1064
+ actionsContainer.appendChild(actionBtn);
1065
+ wrapper.appendChild(actionsContainer);
1066
+ });
1067
+ }
1068
+
1228
1069
  function showTooltip(tooltipId, button) {
1229
1070
  const tooltip = document.getElementById(tooltipId);
1230
1071
  const isCurrentlyVisible = !tooltip.classList.contains("hidden");
@@ -1899,7 +1740,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1899
1740
  removeBtn.type = "button";
1900
1741
  removeBtn.className =
1901
1742
  "remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
1902
- removeBtn.textContent = "✕";
1743
+ removeBtn.innerHTML = "✕";
1903
1744
  removeBtn.onclick = () => {
1904
1745
  const currentIndex = Array.from(container.children).indexOf(item);
1905
1746
  if (container.children.length > minCount) {
@@ -2032,7 +1873,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
2032
1873
  removeBtn.type = "button";
2033
1874
  removeBtn.className =
2034
1875
  "remove-item-btn mt-1 px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm";
2035
- removeBtn.textContent = "✕ Remove";
1876
+ removeBtn.innerHTML = "✕ Remove";
2036
1877
  removeBtn.onclick = () => {
2037
1878
  const currentIndex = Array.from(container.children).indexOf(item);
2038
1879
  if (container.children.length > minCount) {
@@ -2171,7 +2012,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
2171
2012
  removeBtn.type = "button";
2172
2013
  removeBtn.className =
2173
2014
  "remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
2174
- removeBtn.textContent = "✕";
2015
+ removeBtn.innerHTML = "✕";
2175
2016
  removeBtn.onclick = () => {
2176
2017
  const currentIndex = Array.from(container.children).indexOf(item);
2177
2018
  if (container.children.length > minCount) {
@@ -2320,7 +2161,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
2320
2161
  removeBtn.type = "button";
2321
2162
  removeBtn.className =
2322
2163
  "remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
2323
- removeBtn.textContent = "✕";
2164
+ removeBtn.innerHTML = "✕";
2324
2165
  removeBtn.onclick = () => {
2325
2166
  const currentIndex = Array.from(container.children).indexOf(item);
2326
2167
  if (container.children.length > minCount) {
@@ -2385,11 +2226,7 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2385
2226
  const emptyState = document.createElement("div");
2386
2227
  emptyState.className =
2387
2228
  "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
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);
2229
+ emptyState.innerHTML = `<div class="text-center">${t("noFileSelected")}</div>`;
2393
2230
  wrapper.appendChild(emptyState);
2394
2231
  }
2395
2232
  } else {
@@ -2449,14 +2286,8 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
2449
2286
  // Add upload text
2450
2287
  const uploadText = document.createElement("p");
2451
2288
  uploadText.className = "text-xs text-gray-600 mt-2 text-center";
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")));
2289
+ uploadText.innerHTML = `<span class="underline cursor-pointer">${t("uploadText")}</span> ${t("dragDropTextSingle")}`;
2290
+ uploadText.querySelector("span").onclick = () => picker.click();
2460
2291
  fileWrapper.appendChild(uploadText);
2461
2292
 
2462
2293
  // Add hint
@@ -2515,29 +2346,14 @@ function handleInitialFileData(
2515
2346
  }
2516
2347
 
2517
2348
  function setEmptyFileContainer(fileContainer) {
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);
2349
+ fileContainer.innerHTML = `
2350
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
2351
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
2352
+ <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"/>
2353
+ </svg>
2354
+ <div class="text-sm text-center">${t("clickDragText")}</div>
2355
+ </div>
2356
+ `;
2541
2357
  }
2542
2358
 
2543
2359
  function renderFilesElement(element, ctx, wrapper, pathKey) {
@@ -2554,16 +2370,7 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
2554
2370
  resultsWrapper.appendChild(filePreview);
2555
2371
  });
2556
2372
  } else {
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);
2373
+ 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>`;
2567
2374
  }
2568
2375
 
2569
2376
  wrapper.appendChild(resultsWrapper);
@@ -2644,16 +2451,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2644
2451
  resultsWrapper.appendChild(filePreview);
2645
2452
  });
2646
2453
  } else {
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);
2454
+ 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>`;
2657
2455
  }
2658
2456
 
2659
2457
  wrapper.appendChild(resultsWrapper);
@@ -2890,18 +2688,7 @@ function renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap) {
2890
2688
  addBtn.type = "button";
2891
2689
  addBtn.className =
2892
2690
  "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";
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")));
2691
+ 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")}`;
2905
2692
  groupWrap.appendChild(addBtn);
2906
2693
  }
2907
2694
 
@@ -2909,16 +2696,7 @@ function renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap) {
2909
2696
  if (state.config.readonly) return;
2910
2697
  const n = countItems();
2911
2698
  if (addBtn) addBtn.disabled = n >= max;
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);
2699
+ 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>`;
2922
2700
  };
2923
2701
 
2924
2702
  if (pre && pre.length) {
@@ -2933,10 +2711,7 @@ function renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap) {
2933
2711
  addBtn.addEventListener("click", () => addItem(null));
2934
2712
  } else {
2935
2713
  // In readonly mode, just show the label without count controls
2936
- clear(left);
2937
- const labelSpan = document.createElement("span");
2938
- labelSpan.textContent = element.label || element.key;
2939
- left.appendChild(labelSpan);
2714
+ left.innerHTML = `<span>${element.label || element.key}</span>`;
2940
2715
  }
2941
2716
  }
2942
2717
 
@@ -2953,10 +2728,7 @@ function renderSingleGroup(element, ctx, itemsWrap, left, groupWrap) {
2953
2728
  }
2954
2729
  });
2955
2730
  groupWrap.appendChild(itemsWrap);
2956
- clear(left);
2957
- const labelSpan = document.createElement("span");
2958
- labelSpan.textContent = element.label || element.key;
2959
- left.appendChild(labelSpan);
2731
+ left.innerHTML = `<span>${element.label || element.key}</span>`;
2960
2732
  }
2961
2733
 
2962
2734
  function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
@@ -2989,10 +2761,7 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
2989
2761
  }
2990
2762
  });
2991
2763
  containerWrap.appendChild(itemsWrap);
2992
- clear(left);
2993
- const labelSpan = document.createElement("span");
2994
- labelSpan.textContent = element.label || element.key;
2995
- left.appendChild(labelSpan);
2764
+ left.innerHTML = `<span>${element.label || element.key}</span>`;
2996
2765
 
2997
2766
  wrapper.appendChild(containerWrap);
2998
2767
  }
@@ -3083,16 +2852,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
3083
2852
  addBtn.disabled = currentCount >= max;
3084
2853
  addBtn.style.opacity = currentCount >= max ? "0.5" : "1";
3085
2854
  }
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);
2855
+ left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-sm text-gray-500">(${currentCount}/${max === Infinity ? "∞" : max})</span>`;
3096
2856
  };
3097
2857
 
3098
2858
  if (!state.config.readonly) {
@@ -3213,29 +2973,17 @@ function renderFilePreviewReadonly(resourceId, fileName) {
3213
2973
  try {
3214
2974
  const thumbnailUrl = state.config.getThumbnail(resourceId);
3215
2975
  if (thumbnailUrl) {
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);
2976
+ previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${actualFileName}" class="w-full h-auto">`;
3222
2977
  } else {
3223
2978
  // Fallback to icon if getThumbnail returns null/undefined
3224
- clear(previewContainer);
3225
- previewContainer.appendChild(
3226
- createPreviewElement("🖼️", actualFileName),
3227
- );
2979
+ 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>`;
3228
2980
  }
3229
2981
  } catch (error) {
3230
2982
  console.warn("getThumbnail failed for", resourceId, error);
3231
- clear(previewContainer);
3232
- previewContainer.appendChild(
3233
- createPreviewElement("🖼️", actualFileName),
3234
- );
2983
+ 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>`;
3235
2984
  }
3236
2985
  } else {
3237
- clear(previewContainer);
3238
- previewContainer.appendChild(createPreviewElement("🖼️", actualFileName));
2986
+ 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>`;
3239
2987
  }
3240
2988
  } else if (isVideo) {
3241
2989
  // Video preview - try getThumbnail for video URL
@@ -3243,76 +2991,34 @@ function renderFilePreviewReadonly(resourceId, fileName) {
3243
2991
  try {
3244
2992
  const videoUrl = state.config.getThumbnail(resourceId);
3245
2993
  if (videoUrl) {
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);
2994
+ previewContainer.innerHTML = `
2995
+ <div class="relative group">
2996
+ <video class="w-full h-auto" controls preload="auto" muted>
2997
+ <source src="${videoUrl}" type="${meta?.type || "video/mp4"}">
2998
+ Ваш браузер не поддерживает видео.
2999
+ </video>
3000
+ <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">
3001
+ <div class="bg-white bg-opacity-90 rounded-full p-3">
3002
+ <svg class="w-8 h-8 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
3003
+ <path d="M8 5v14l11-7z"/>
3004
+ </svg>
3005
+ </div>
3006
+ </div>
3007
+ </div>
3008
+ `;
3295
3009
  } else {
3296
- clear(previewContainer);
3297
- previewContainer.appendChild(
3298
- createPreviewElement("🎥", actualFileName),
3299
- );
3010
+ 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>`;
3300
3011
  }
3301
3012
  } catch (error) {
3302
3013
  console.warn("getThumbnail failed for video", resourceId, error);
3303
- clear(previewContainer);
3304
- previewContainer.appendChild(
3305
- createPreviewElement("🎥", actualFileName),
3306
- );
3014
+ 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>`;
3307
3015
  }
3308
3016
  } else {
3309
- clear(previewContainer);
3310
- previewContainer.appendChild(createPreviewElement("🎥", actualFileName));
3017
+ 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>`;
3311
3018
  }
3312
3019
  } else {
3313
3020
  // Other file types
3314
- clear(previewContainer);
3315
- previewContainer.appendChild(createPreviewElement("📁", actualFileName));
3021
+ 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>`;
3316
3022
  }
3317
3023
 
3318
3024
  // File name
@@ -3478,8 +3184,6 @@ function saveDraft() {
3478
3184
 
3479
3185
  function clearForm() {
3480
3186
  if (state.formRoot) {
3481
- // Clean up any existing object URLs before clearing form
3482
- revokeAllObjectURLs();
3483
3187
  clear(state.formRoot);
3484
3188
  }
3485
3189
  }