@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.
Files changed (2) hide show
  1. package/dist/form-builder.js +139 -73
  2. package/package.json +1 -1
@@ -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 overlay with download/remove buttons if needed
590
- if (!isReadonly) {
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 empty placeholder if no files
624
- if (!rids || rids.length === 0) {
625
- container.innerHTML = `
626
- <div class="grid grid-cols-4 gap-3 mb-3">
627
- <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">
628
- <svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
629
- <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"/>
630
- </svg>
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
- // Also add specific handler to the "Загрузите" link
659
- const uploadLink = container.querySelector('span.underline');
660
- if (uploadLink) {
661
- uploadLink.onclick = (e) => {
662
- e.stopPropagation(); // Prevent double trigger
663
- const fileInput = container.parentElement?.querySelector('input[type="file"]');
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
- // Show files grid
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 minSlots = 4;
675
- const currentImagesCount = rids.length;
676
- const slotsNeeded = Math.max(minSlots, Math.ceil((currentImagesCount + 1) / 4) * 4);
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-6 h-6" fill="currentColor" viewBox="0 0 24 24">
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-6 h-6" fill="currentColor" viewBox="0 0 24 24">
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-6 h-6 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>';
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
- const fileInput = container.parentElement?.querySelector('input[type="file"]');
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');
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.14",
6
+ "version": "0.1.16",
7
7
  "description": "A reusable JSON schema form builder library",
8
8
  "main": "dist/form-builder.js",
9
9
  "files": [