@dmitryvim/form-builder 0.1.15 → 0.1.17

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 +154 -74
  2. package/package.json +1 -1
@@ -87,35 +87,45 @@ function validateSchema(schema) {
87
87
 
88
88
  // Form rendering
89
89
  function renderForm(schema, prefill) {
90
+ console.log('🔧 FormBuilder.renderForm called with:', { schema, prefill });
91
+
90
92
  const errors = validateSchema(schema);
91
93
  if (errors.length > 0) {
92
94
  console.error('Schema validation errors:', errors);
93
95
  return;
94
96
  }
97
+ console.log('✅ Schema validation passed');
95
98
 
96
99
  state.schema = schema;
97
100
  if (!state.formRoot) {
98
101
  console.error('No form root element set. Call setFormRoot() first.');
99
102
  return;
100
103
  }
104
+ console.log('✅ FormRoot is set:', state.formRoot);
101
105
 
102
106
  clear(state.formRoot);
103
107
 
104
108
  const formEl = document.createElement('div');
105
109
  formEl.className = 'space-y-6';
106
110
 
107
- schema.elements.forEach(element => {
111
+ console.log(`🔧 Processing ${schema.elements.length} schema elements`);
112
+ schema.elements.forEach((element, index) => {
113
+ console.log(`🔧 Rendering element ${index}:`, element);
108
114
  const block = renderElement(element, {
109
115
  path: '',
110
116
  prefill: prefill || {}
111
117
  });
118
+ console.log(`✅ Element ${index} rendered:`, block);
112
119
  formEl.appendChild(block);
113
120
  });
114
121
 
115
122
  state.formRoot.appendChild(formEl);
123
+ console.log(`✅ Form rendered with ${schema.elements.length} elements`);
116
124
  }
117
125
 
118
126
  function renderElement(element, ctx) {
127
+ console.log(`🔧 renderElement called:`, { element, ctx });
128
+
119
129
  const wrapper = document.createElement('div');
120
130
  wrapper.className = 'mb-6';
121
131
 
@@ -180,17 +190,21 @@ function renderElement(element, ctx) {
180
190
 
181
191
  case 'file':
182
192
  // TODO: Extract to renderFileElement() function
193
+ console.log(`🔧 Rendering file element '${element.key}':`, { readonly: state.config.readonly, prefill: ctx.prefill[element.key], element });
183
194
  if (state.config.readonly) {
184
195
  // Readonly mode: use common preview function
185
196
  const initial = ctx.prefill[element.key] || element.default;
197
+ console.log(`🔧 File initial value:`, initial);
186
198
  if (initial) {
187
199
  const filePreview = renderFilePreviewReadonly(initial);
188
200
  wrapper.appendChild(filePreview);
201
+ console.log(`✅ File preview rendered for '${element.key}'`);
189
202
  } else {
190
203
  const emptyState = document.createElement('div');
191
204
  emptyState.className = 'aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500';
192
205
  emptyState.innerHTML = '<div class="text-center">Нет файла</div>';
193
206
  wrapper.appendChild(emptyState);
207
+ console.log(`❌ No file data for '${element.key}' - showing empty state`);
194
208
  }
195
209
  } else {
196
210
  // Edit mode: normal file input
@@ -212,7 +226,7 @@ function renderElement(element, ctx) {
212
226
 
213
227
  const initial = ctx.prefill[element.key] || element.default;
214
228
  if (initial) {
215
- renderFilePreview(fileContainer, initial, initial, '');
229
+ renderFilePreview(fileContainer, initial, initial, '', false).catch(console.error);
216
230
  } else {
217
231
  fileContainer.innerHTML = `
218
232
  <div class="flex flex-col items-center justify-center h-full text-gray-400">
@@ -566,6 +580,28 @@ async function renderFilePreview(container, resourceId, fileName, fileType, isRe
566
580
  // Non-image file
567
581
  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
582
  }
583
+
584
+ // Add delete button for edit mode
585
+ if (!isReadonly) {
586
+ addDeleteButton(container, () => {
587
+ // Clear the file
588
+ state.resourceIndex.delete(resourceId);
589
+ // Update hidden input
590
+ const hiddenInput = container.parentElement.querySelector('input[type="hidden"]');
591
+ if (hiddenInput) {
592
+ hiddenInput.value = '';
593
+ }
594
+ // Clear preview and show placeholder
595
+ container.innerHTML = `
596
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
597
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
598
+ <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"/>
599
+ </svg>
600
+ <div class="text-sm text-center">Нажмите или перетащите файл</div>
601
+ </div>
602
+ `;
603
+ });
604
+ }
569
605
  } else if (state.config.getThumbnail) {
570
606
  // Try to get thumbnail from config for uploaded files
571
607
  try {
@@ -581,33 +617,35 @@ async function renderFilePreview(container, resourceId, fileName, fileType, isRe
581
617
  console.warn('Thumbnail loading failed:', error);
582
618
  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
619
  }
620
+
621
+ // Add delete button for edit mode
622
+ if (!isReadonly) {
623
+ addDeleteButton(container, () => {
624
+ // Clear the file
625
+ state.resourceIndex.delete(resourceId);
626
+ // Update hidden input
627
+ const hiddenInput = container.parentElement.querySelector('input[type="hidden"]');
628
+ if (hiddenInput) {
629
+ hiddenInput.value = '';
630
+ }
631
+ // Clear preview and show placeholder
632
+ container.innerHTML = `
633
+ <div class="flex flex-col items-center justify-center h-full text-gray-400">
634
+ <svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
635
+ <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"/>
636
+ </svg>
637
+ <div class="text-sm text-center">Нажмите или перетащите файл</div>
638
+ </div>
639
+ `;
640
+ });
641
+ }
584
642
  } else {
585
643
  // No file and no getThumbnail config - fallback
586
644
  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
645
  }
588
646
 
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
647
+ // Add click handler for download in readonly mode
648
+ if (isReadonly && state.config.downloadFile) {
611
649
  container.style.cursor = 'pointer';
612
650
  container.onclick = () => {
613
651
  if (state.config.downloadFile) {
@@ -620,66 +658,83 @@ async function renderFilePreview(container, resourceId, fileName, fileType, isRe
620
658
  function renderResourcePills(container, rids, onRemove) {
621
659
  clear(container);
622
660
 
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
- };
661
+ // Show initial placeholder only if this is the first render (no previous grid)
662
+ // Check if container already has grid class to determine if this is initial render
663
+ const isInitialRender = !container.classList.contains('grid');
664
+
665
+ if ((!rids || rids.length === 0) && isInitialRender) {
666
+ // Create grid container
667
+ const gridContainer = document.createElement('div');
668
+ gridContainer.className = 'grid grid-cols-4 gap-3 mb-3';
657
669
 
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"]');
670
+ // Create 4 placeholder slots
671
+ for (let i = 0; i < 4; i++) {
672
+ const slot = document.createElement('div');
673
+ 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';
674
+
675
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
676
+ svg.setAttribute('class', 'w-12 h-12 text-gray-400');
677
+ svg.setAttribute('fill', 'currentColor');
678
+ svg.setAttribute('viewBox', '0 0 24 24');
679
+
680
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
681
+ 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');
682
+
683
+ svg.appendChild(path);
684
+ slot.appendChild(svg);
685
+
686
+ // Add click handler to each slot
687
+ slot.onclick = () => {
688
+ // Look for file input in the files wrapper (go up from list -> filesContainer -> filesWrapper)
689
+ const filesWrapper = container.closest('.space-y-2');
690
+ const fileInput = filesWrapper?.querySelector('input[type="file"]');
664
691
  if (fileInput) fileInput.click();
665
692
  };
693
+
694
+ gridContainer.appendChild(slot);
666
695
  }
696
+
697
+ // Create text container
698
+ const textContainer = document.createElement('div');
699
+ textContainer.className = 'text-center text-xs text-gray-600';
700
+
701
+ const uploadLink = document.createElement('span');
702
+ uploadLink.className = 'underline cursor-pointer';
703
+ uploadLink.textContent = 'Загрузите';
704
+ uploadLink.onclick = (e) => {
705
+ e.stopPropagation();
706
+ // Look for file input in the files wrapper (go up from list -> filesContainer -> filesWrapper)
707
+ const filesWrapper = container.closest('.space-y-2');
708
+ const fileInput = filesWrapper?.querySelector('input[type="file"]');
709
+ if (fileInput) fileInput.click();
710
+ };
711
+
712
+ textContainer.appendChild(uploadLink);
713
+ textContainer.appendChild(document.createTextNode(' или перетащите файлы'));
714
+
715
+ // Clear and append
716
+ container.appendChild(gridContainer);
717
+ container.appendChild(textContainer);
667
718
  return;
668
719
  }
669
720
 
670
- // Show files grid
721
+ // Always show files grid if we have files OR if this was already a grid
722
+ // This prevents shrinking when deleting the last file
671
723
  container.className = 'grid grid-cols-4 gap-3 mt-2';
672
724
 
673
725
  // 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);
726
+ const currentImagesCount = rids ? rids.length : 0;
727
+ // Calculate rows needed: always show an extra slot for adding next file
728
+ // 0-3 files → 1 row (4 slots), 4-7 files 2 rows (8 slots), 8-11 files → 3 rows (12 slots)
729
+ const rowsNeeded = Math.floor(currentImagesCount / 4) + 1;
730
+ const slotsNeeded = rowsNeeded * 4;
731
+
677
732
 
678
733
  // Add all slots (filled and empty)
679
734
  for (let i = 0; i < slotsNeeded; i++) {
680
735
  const slot = document.createElement('div');
681
736
 
682
- if (i < rids.length) {
737
+ if (rids && i < rids.length) {
683
738
  // Filled slot with image preview
684
739
  const rid = rids[i];
685
740
  const meta = state.resourceIndex.get(rid);
@@ -711,14 +766,14 @@ function renderResourcePills(container, rids, onRemove) {
711
766
  slot.appendChild(img);
712
767
  } else {
713
768
  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">
769
+ <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
715
770
  <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
771
  </svg>
717
772
  </div>`;
718
773
  }
719
774
  } else {
720
775
  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">
776
+ <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
722
777
  <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
778
  </svg>
724
779
  </div>`;
@@ -749,9 +804,11 @@ function renderResourcePills(container, rids, onRemove) {
749
804
  } else {
750
805
  // Empty slot placeholder
751
806
  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>';
807
+ 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
808
  slot.onclick = () => {
754
- const fileInput = container.parentElement?.querySelector('input[type="file"]');
809
+ // Look for file input in the files wrapper (go up from list -> filesContainer -> filesWrapper)
810
+ const filesWrapper = container.closest('.space-y-2');
811
+ const fileInput = filesWrapper?.querySelector('input[type="file"]');
755
812
  if (fileInput) fileInput.click();
756
813
  };
757
814
  }
@@ -806,7 +863,7 @@ async function handleFileSelect(file, container, fieldName) {
806
863
  }
807
864
  hiddenInput.value = rid;
808
865
 
809
- renderFilePreview(container, rid, file.name, file.type);
866
+ renderFilePreview(container, rid, file.name, file.type, false).catch(console.error);
810
867
  }
811
868
 
812
869
  function setupDragAndDrop(element, dropHandler) {
@@ -827,6 +884,29 @@ function setupDragAndDrop(element, dropHandler) {
827
884
  });
828
885
  }
829
886
 
887
+ function addDeleteButton(container, onDelete) {
888
+ // Remove existing overlay if any
889
+ const existingOverlay = container.querySelector('.delete-overlay');
890
+ if (existingOverlay) {
891
+ existingOverlay.remove();
892
+ }
893
+
894
+ // Create overlay with center delete button (like in files)
895
+ const overlay = document.createElement('div');
896
+ 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';
897
+
898
+ const deleteBtn = document.createElement('button');
899
+ deleteBtn.className = 'bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700 transition-colors';
900
+ deleteBtn.textContent = 'Удалить';
901
+ deleteBtn.onclick = (e) => {
902
+ e.stopPropagation();
903
+ onDelete();
904
+ };
905
+
906
+ overlay.appendChild(deleteBtn);
907
+ container.appendChild(overlay);
908
+ }
909
+
830
910
  function showTooltip(tooltipId, button) {
831
911
  const tooltip = document.getElementById(tooltipId);
832
912
  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.15",
6
+ "version": "0.1.17",
7
7
  "description": "A reusable JSON schema form builder library",
8
8
  "main": "dist/form-builder.js",
9
9
  "files": [