@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.
- package/dist/demo.js +13 -12
- package/dist/elements.html +610 -161
- package/dist/elements.js +269 -231
- package/dist/form-builder.js +730 -258
- package/dist/index.html +27 -2
- package/package.json +8 -3
package/dist/form-builder.js
CHANGED
|
@@ -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
|
-
|
|
188
|
-
|
|
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 =
|
|
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
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
}
|
|
531
|
+
changeBtn.onclick = (e) => {
|
|
532
|
+
e.stopPropagation();
|
|
533
|
+
if (deps?.picker) {
|
|
534
|
+
deps.picker.click();
|
|
535
|
+
}
|
|
536
|
+
};
|
|
457
537
|
|
|
458
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
565
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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 =
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
|
807
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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)
|
|
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(
|
|
1265
|
-
|
|
1266
|
-
|
|
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(
|
|
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(
|
|
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] =
|
|
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] =
|
|
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] =
|
|
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] =
|
|
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 =
|
|
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(
|
|
1893
|
+
const items = container.querySelectorAll(".multiple-text-item");
|
|
1575
1894
|
const currentCount = items.length;
|
|
1576
1895
|
items.forEach((item) => {
|
|
1577
|
-
let removeBtn = item.querySelector(
|
|
1896
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
1578
1897
|
if (!removeBtn) {
|
|
1579
|
-
removeBtn = document.createElement(
|
|
1580
|
-
removeBtn.type =
|
|
1581
|
-
removeBtn.className =
|
|
1582
|
-
|
|
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 ?
|
|
1598
|
-
removeBtn.style.pointerEvents = disabled ?
|
|
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 =
|
|
1610
|
-
|
|
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 =
|
|
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(
|
|
2026
|
+
const items = container.querySelectorAll(".multiple-textarea-item");
|
|
1705
2027
|
const currentCount = items.length;
|
|
1706
2028
|
items.forEach((item) => {
|
|
1707
|
-
let removeBtn = item.querySelector(
|
|
2029
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
1708
2030
|
if (!removeBtn) {
|
|
1709
|
-
removeBtn = document.createElement(
|
|
1710
|
-
removeBtn.type =
|
|
1711
|
-
removeBtn.className =
|
|
1712
|
-
|
|
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 ?
|
|
1728
|
-
removeBtn.style.pointerEvents = disabled ?
|
|
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 =
|
|
1740
|
-
|
|
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 =
|
|
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(
|
|
2165
|
+
const items = container.querySelectorAll(".multiple-number-item");
|
|
1841
2166
|
const currentCount = items.length;
|
|
1842
2167
|
items.forEach((item) => {
|
|
1843
|
-
let removeBtn = item.querySelector(
|
|
2168
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
1844
2169
|
if (!removeBtn) {
|
|
1845
|
-
removeBtn = document.createElement(
|
|
1846
|
-
removeBtn.type =
|
|
1847
|
-
removeBtn.className =
|
|
1848
|
-
|
|
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 ?
|
|
1864
|
-
removeBtn.style.pointerEvents = disabled ?
|
|
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 =
|
|
1876
|
-
|
|
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 ||
|
|
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 =
|
|
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(
|
|
2314
|
+
const items = container.querySelectorAll(".multiple-select-item");
|
|
1987
2315
|
const currentCount = items.length;
|
|
1988
2316
|
items.forEach((item) => {
|
|
1989
|
-
let removeBtn = item.querySelector(
|
|
2317
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
1990
2318
|
if (!removeBtn) {
|
|
1991
|
-
removeBtn = document.createElement(
|
|
1992
|
-
removeBtn.type =
|
|
1993
|
-
removeBtn.className =
|
|
1994
|
-
|
|
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 ?
|
|
2010
|
-
removeBtn.style.pointerEvents = disabled ?
|
|
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 =
|
|
2022
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
2118
|
-
|
|
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
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ?
|
|
2325
|
-
const minMaxText =
|
|
2326
|
-
|
|
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(
|
|
2706
|
+
const existingCount = filesWrapper.querySelector(".file-count-info");
|
|
2332
2707
|
if (existingCount) existingCount.remove();
|
|
2333
2708
|
|
|
2334
|
-
countInfo.className +=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
3231
|
+
clear(previewContainer);
|
|
3232
|
+
previewContainer.appendChild(
|
|
3233
|
+
createPreviewElement("🖼️", actualFileName),
|
|
3234
|
+
);
|
|
2808
3235
|
}
|
|
2809
3236
|
} else {
|
|
2810
|
-
previewContainer
|
|
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
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
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
|
|
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
|
|
3303
|
+
clear(previewContainer);
|
|
3304
|
+
previewContainer.appendChild(
|
|
3305
|
+
createPreviewElement("🎥", actualFileName),
|
|
3306
|
+
);
|
|
2839
3307
|
}
|
|
2840
3308
|
} else {
|
|
2841
|
-
previewContainer
|
|
3309
|
+
clear(previewContainer);
|
|
3310
|
+
previewContainer.appendChild(createPreviewElement("🎥", actualFileName));
|
|
2842
3311
|
}
|
|
2843
3312
|
} else {
|
|
2844
3313
|
// Other file types
|
|
2845
|
-
previewContainer
|
|
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
|
}
|