@dmitryvim/form-builder 0.1.14 → 0.1.16
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/form-builder.js +139 -73
- package/package.json +1 -1
package/dist/form-builder.js
CHANGED
|
@@ -212,7 +212,7 @@ function renderElement(element, ctx) {
|
|
|
212
212
|
|
|
213
213
|
const initial = ctx.prefill[element.key] || element.default;
|
|
214
214
|
if (initial) {
|
|
215
|
-
renderFilePreview(fileContainer, initial, initial, '');
|
|
215
|
+
renderFilePreview(fileContainer, initial, initial, '', false).catch(console.error);
|
|
216
216
|
} else {
|
|
217
217
|
fileContainer.innerHTML = `
|
|
218
218
|
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
@@ -566,6 +566,28 @@ async function renderFilePreview(container, resourceId, fileName, fileType, isRe
|
|
|
566
566
|
// Non-image file
|
|
567
567
|
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">' + fileName + '</div></div>';
|
|
568
568
|
}
|
|
569
|
+
|
|
570
|
+
// Add delete button for edit mode
|
|
571
|
+
if (!isReadonly) {
|
|
572
|
+
addDeleteButton(container, () => {
|
|
573
|
+
// Clear the file
|
|
574
|
+
state.resourceIndex.delete(resourceId);
|
|
575
|
+
// Update hidden input
|
|
576
|
+
const hiddenInput = container.parentElement.querySelector('input[type="hidden"]');
|
|
577
|
+
if (hiddenInput) {
|
|
578
|
+
hiddenInput.value = '';
|
|
579
|
+
}
|
|
580
|
+
// Clear preview and show placeholder
|
|
581
|
+
container.innerHTML = `
|
|
582
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
583
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
584
|
+
<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"/>
|
|
585
|
+
</svg>
|
|
586
|
+
<div class="text-sm text-center">Нажмите или перетащите файл</div>
|
|
587
|
+
</div>
|
|
588
|
+
`;
|
|
589
|
+
});
|
|
590
|
+
}
|
|
569
591
|
} else if (state.config.getThumbnail) {
|
|
570
592
|
// Try to get thumbnail from config for uploaded files
|
|
571
593
|
try {
|
|
@@ -581,33 +603,35 @@ async function renderFilePreview(container, resourceId, fileName, fileType, isRe
|
|
|
581
603
|
console.warn('Thumbnail loading failed:', error);
|
|
582
604
|
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">' + fileName + '</div></div>';
|
|
583
605
|
}
|
|
606
|
+
|
|
607
|
+
// Add delete button for edit mode
|
|
608
|
+
if (!isReadonly) {
|
|
609
|
+
addDeleteButton(container, () => {
|
|
610
|
+
// Clear the file
|
|
611
|
+
state.resourceIndex.delete(resourceId);
|
|
612
|
+
// Update hidden input
|
|
613
|
+
const hiddenInput = container.parentElement.querySelector('input[type="hidden"]');
|
|
614
|
+
if (hiddenInput) {
|
|
615
|
+
hiddenInput.value = '';
|
|
616
|
+
}
|
|
617
|
+
// Clear preview and show placeholder
|
|
618
|
+
container.innerHTML = `
|
|
619
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
620
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
621
|
+
<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"/>
|
|
622
|
+
</svg>
|
|
623
|
+
<div class="text-sm text-center">Нажмите или перетащите файл</div>
|
|
624
|
+
</div>
|
|
625
|
+
`;
|
|
626
|
+
});
|
|
627
|
+
}
|
|
584
628
|
} else {
|
|
585
629
|
// No file and no getThumbnail config - fallback
|
|
586
630
|
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">' + fileName + '</div></div>';
|
|
587
631
|
}
|
|
588
632
|
|
|
589
|
-
// Add
|
|
590
|
-
if (
|
|
591
|
-
const overlay = document.createElement('div');
|
|
592
|
-
overlay.className = 'absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center';
|
|
593
|
-
const buttonContainer = document.createElement('div');
|
|
594
|
-
buttonContainer.className = 'flex gap-2';
|
|
595
|
-
|
|
596
|
-
const removeBtn = document.createElement('button');
|
|
597
|
-
removeBtn.className = 'bg-red-600 text-white px-2 py-1 rounded text-xs';
|
|
598
|
-
removeBtn.textContent = 'Удалить';
|
|
599
|
-
removeBtn.onclick = (e) => {
|
|
600
|
-
e.stopPropagation();
|
|
601
|
-
const hiddenInput = container.parentElement.querySelector('input[type="hidden"]');
|
|
602
|
-
if (hiddenInput) hiddenInput.value = '';
|
|
603
|
-
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">Click or drop to upload</div></div>';
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
buttonContainer.appendChild(removeBtn);
|
|
607
|
-
overlay.appendChild(buttonContainer);
|
|
608
|
-
container.appendChild(overlay);
|
|
609
|
-
} else if (isReadonly && state.config.downloadFile) {
|
|
610
|
-
// Add click handler for download in readonly mode
|
|
633
|
+
// Add click handler for download in readonly mode
|
|
634
|
+
if (isReadonly && state.config.downloadFile) {
|
|
611
635
|
container.style.cursor = 'pointer';
|
|
612
636
|
container.onclick = () => {
|
|
613
637
|
if (state.config.downloadFile) {
|
|
@@ -620,66 +644,83 @@ async function renderFilePreview(container, resourceId, fileName, fileType, isRe
|
|
|
620
644
|
function renderResourcePills(container, rids, onRemove) {
|
|
621
645
|
clear(container);
|
|
622
646
|
|
|
623
|
-
// Show
|
|
624
|
-
if
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
</div>
|
|
632
|
-
<div class="aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors">
|
|
633
|
-
<svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
|
|
634
|
-
<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"/>
|
|
635
|
-
</svg>
|
|
636
|
-
</div>
|
|
637
|
-
<div class="aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors">
|
|
638
|
-
<svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
|
|
639
|
-
<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"/>
|
|
640
|
-
</svg>
|
|
641
|
-
</div>
|
|
642
|
-
<div class="aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors">
|
|
643
|
-
<svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
|
|
644
|
-
<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"/>
|
|
645
|
-
</svg>
|
|
646
|
-
</div>
|
|
647
|
-
</div>
|
|
648
|
-
<div class="text-center text-xs text-gray-600">
|
|
649
|
-
<span class="underline cursor-pointer">Загрузите</span> или перетащите файлы
|
|
650
|
-
</div>
|
|
651
|
-
`;
|
|
652
|
-
// Add click handler to the entire placeholder
|
|
653
|
-
container.onclick = () => {
|
|
654
|
-
const fileInput = container.parentElement?.querySelector('input[type="file"]');
|
|
655
|
-
if (fileInput) fileInput.click();
|
|
656
|
-
};
|
|
647
|
+
// Show initial placeholder only if this is the first render (no previous grid)
|
|
648
|
+
// Check if container already has grid class to determine if this is initial render
|
|
649
|
+
const isInitialRender = !container.classList.contains('grid');
|
|
650
|
+
|
|
651
|
+
if ((!rids || rids.length === 0) && isInitialRender) {
|
|
652
|
+
// Create grid container
|
|
653
|
+
const gridContainer = document.createElement('div');
|
|
654
|
+
gridContainer.className = 'grid grid-cols-4 gap-3 mb-3';
|
|
657
655
|
|
|
658
|
-
//
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
656
|
+
// Create 4 placeholder slots
|
|
657
|
+
for (let i = 0; i < 4; i++) {
|
|
658
|
+
const slot = document.createElement('div');
|
|
659
|
+
slot.className = 'aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors';
|
|
660
|
+
|
|
661
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
662
|
+
svg.setAttribute('class', 'w-12 h-12 text-gray-400');
|
|
663
|
+
svg.setAttribute('fill', 'currentColor');
|
|
664
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
665
|
+
|
|
666
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
667
|
+
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');
|
|
668
|
+
|
|
669
|
+
svg.appendChild(path);
|
|
670
|
+
slot.appendChild(svg);
|
|
671
|
+
|
|
672
|
+
// Add click handler to each slot
|
|
673
|
+
slot.onclick = () => {
|
|
674
|
+
// Look for file input in the files wrapper (go up from list -> filesContainer -> filesWrapper)
|
|
675
|
+
const filesWrapper = container.closest('.space-y-2');
|
|
676
|
+
const fileInput = filesWrapper?.querySelector('input[type="file"]');
|
|
664
677
|
if (fileInput) fileInput.click();
|
|
665
678
|
};
|
|
679
|
+
|
|
680
|
+
gridContainer.appendChild(slot);
|
|
666
681
|
}
|
|
682
|
+
|
|
683
|
+
// Create text container
|
|
684
|
+
const textContainer = document.createElement('div');
|
|
685
|
+
textContainer.className = 'text-center text-xs text-gray-600';
|
|
686
|
+
|
|
687
|
+
const uploadLink = document.createElement('span');
|
|
688
|
+
uploadLink.className = 'underline cursor-pointer';
|
|
689
|
+
uploadLink.textContent = 'Загрузите';
|
|
690
|
+
uploadLink.onclick = (e) => {
|
|
691
|
+
e.stopPropagation();
|
|
692
|
+
// Look for file input in the files wrapper (go up from list -> filesContainer -> filesWrapper)
|
|
693
|
+
const filesWrapper = container.closest('.space-y-2');
|
|
694
|
+
const fileInput = filesWrapper?.querySelector('input[type="file"]');
|
|
695
|
+
if (fileInput) fileInput.click();
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
textContainer.appendChild(uploadLink);
|
|
699
|
+
textContainer.appendChild(document.createTextNode(' или перетащите файлы'));
|
|
700
|
+
|
|
701
|
+
// Clear and append
|
|
702
|
+
container.appendChild(gridContainer);
|
|
703
|
+
container.appendChild(textContainer);
|
|
667
704
|
return;
|
|
668
705
|
}
|
|
669
706
|
|
|
670
|
-
//
|
|
707
|
+
// Always show files grid if we have files OR if this was already a grid
|
|
708
|
+
// This prevents shrinking when deleting the last file
|
|
671
709
|
container.className = 'grid grid-cols-4 gap-3 mt-2';
|
|
672
710
|
|
|
673
711
|
// Calculate how many slots we need (at least 4, then expand by rows of 4)
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
712
|
+
const currentImagesCount = rids ? rids.length : 0;
|
|
713
|
+
// Calculate rows needed: always show an extra slot for adding next file
|
|
714
|
+
// 0-3 files → 1 row (4 slots), 4-7 files → 2 rows (8 slots), 8-11 files → 3 rows (12 slots)
|
|
715
|
+
const rowsNeeded = Math.floor(currentImagesCount / 4) + 1;
|
|
716
|
+
const slotsNeeded = rowsNeeded * 4;
|
|
717
|
+
|
|
677
718
|
|
|
678
719
|
// Add all slots (filled and empty)
|
|
679
720
|
for (let i = 0; i < slotsNeeded; i++) {
|
|
680
721
|
const slot = document.createElement('div');
|
|
681
722
|
|
|
682
|
-
if (i < rids.length) {
|
|
723
|
+
if (rids && i < rids.length) {
|
|
683
724
|
// Filled slot with image preview
|
|
684
725
|
const rid = rids[i];
|
|
685
726
|
const meta = state.resourceIndex.get(rid);
|
|
@@ -711,14 +752,14 @@ function renderResourcePills(container, rids, onRemove) {
|
|
|
711
752
|
slot.appendChild(img);
|
|
712
753
|
} else {
|
|
713
754
|
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
714
|
-
<svg class="w-
|
|
755
|
+
<svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
|
715
756
|
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
716
757
|
</svg>
|
|
717
758
|
</div>`;
|
|
718
759
|
}
|
|
719
760
|
} else {
|
|
720
761
|
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
721
|
-
<svg class="w-
|
|
762
|
+
<svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
|
722
763
|
<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
|
|
723
764
|
</svg>
|
|
724
765
|
</div>`;
|
|
@@ -749,9 +790,11 @@ function renderResourcePills(container, rids, onRemove) {
|
|
|
749
790
|
} else {
|
|
750
791
|
// Empty slot placeholder
|
|
751
792
|
slot.className = 'aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors';
|
|
752
|
-
slot.innerHTML = '<svg class="w-
|
|
793
|
+
slot.innerHTML = '<svg class="w-12 h-12 text-gray-400" fill="currentColor" viewBox="0 0 24 24"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>';
|
|
753
794
|
slot.onclick = () => {
|
|
754
|
-
|
|
795
|
+
// Look for file input in the files wrapper (go up from list -> filesContainer -> filesWrapper)
|
|
796
|
+
const filesWrapper = container.closest('.space-y-2');
|
|
797
|
+
const fileInput = filesWrapper?.querySelector('input[type="file"]');
|
|
755
798
|
if (fileInput) fileInput.click();
|
|
756
799
|
};
|
|
757
800
|
}
|
|
@@ -806,7 +849,7 @@ async function handleFileSelect(file, container, fieldName) {
|
|
|
806
849
|
}
|
|
807
850
|
hiddenInput.value = rid;
|
|
808
851
|
|
|
809
|
-
renderFilePreview(container, rid, file.name, file.type);
|
|
852
|
+
renderFilePreview(container, rid, file.name, file.type, false).catch(console.error);
|
|
810
853
|
}
|
|
811
854
|
|
|
812
855
|
function setupDragAndDrop(element, dropHandler) {
|
|
@@ -827,6 +870,29 @@ function setupDragAndDrop(element, dropHandler) {
|
|
|
827
870
|
});
|
|
828
871
|
}
|
|
829
872
|
|
|
873
|
+
function addDeleteButton(container, onDelete) {
|
|
874
|
+
// Remove existing overlay if any
|
|
875
|
+
const existingOverlay = container.querySelector('.delete-overlay');
|
|
876
|
+
if (existingOverlay) {
|
|
877
|
+
existingOverlay.remove();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Create overlay with center delete button (like in files)
|
|
881
|
+
const overlay = document.createElement('div');
|
|
882
|
+
overlay.className = 'delete-overlay absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center';
|
|
883
|
+
|
|
884
|
+
const deleteBtn = document.createElement('button');
|
|
885
|
+
deleteBtn.className = 'bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700 transition-colors';
|
|
886
|
+
deleteBtn.textContent = 'Удалить';
|
|
887
|
+
deleteBtn.onclick = (e) => {
|
|
888
|
+
e.stopPropagation();
|
|
889
|
+
onDelete();
|
|
890
|
+
};
|
|
891
|
+
|
|
892
|
+
overlay.appendChild(deleteBtn);
|
|
893
|
+
container.appendChild(overlay);
|
|
894
|
+
}
|
|
895
|
+
|
|
830
896
|
function showTooltip(tooltipId, button) {
|
|
831
897
|
const tooltip = document.getElementById(tooltipId);
|
|
832
898
|
const isCurrentlyVisible = !tooltip.classList.contains('hidden');
|