@dmitryvim/form-builder 0.1.8 → 0.1.9
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 +209 -83
- package/dist/sample.html +107 -19
- package/package.json +1 -1
package/dist/form-builder.js
CHANGED
|
@@ -143,10 +143,89 @@
|
|
|
143
143
|
uploadFile: null,
|
|
144
144
|
downloadFile: null,
|
|
145
145
|
getThumbnail: null,
|
|
146
|
+
getPreviewUrl: null,
|
|
146
147
|
enableFilePreview: true,
|
|
147
148
|
maxPreviewSize: '200px'
|
|
148
149
|
};
|
|
149
150
|
|
|
151
|
+
// Lightweight preview modal
|
|
152
|
+
function ensurePreviewModal() {
|
|
153
|
+
let modal = document.getElementById('fb-preview-modal');
|
|
154
|
+
if (modal) return modal;
|
|
155
|
+
modal = document.createElement('div');
|
|
156
|
+
modal.id = 'fb-preview-modal';
|
|
157
|
+
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.75);display:none;align-items:center;justify-content:center;z-index:9999;padding:24px;';
|
|
158
|
+
const inner = document.createElement('div');
|
|
159
|
+
inner.style.cssText = 'position:relative;max-width:90vw;max-height:90vh;background:#0b0b0b;border-radius:12px;padding:16px;display:flex;flex-direction:column;gap:12px;';
|
|
160
|
+
const close = document.createElement('button');
|
|
161
|
+
close.textContent = '✕';
|
|
162
|
+
close.title = 'Close';
|
|
163
|
+
close.style.cssText = 'position:absolute;top:8px;right:8px;background:#111;color:#fff;border:1px solid #333;border-radius:6px;padding:4px 8px;cursor:pointer;';
|
|
164
|
+
close.addEventListener('click', () => hidePreviewModal());
|
|
165
|
+
const mediaWrap = document.createElement('div');
|
|
166
|
+
mediaWrap.style.cssText = 'display:flex;align-items:center;justify-content:center;max-height:70vh;';
|
|
167
|
+
const actions = document.createElement('div');
|
|
168
|
+
actions.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;';
|
|
169
|
+
const downloadBtn = document.createElement('button');
|
|
170
|
+
downloadBtn.textContent = 'Download';
|
|
171
|
+
downloadBtn.style.cssText = 'background:#e5e7eb;color:#111;border-radius:8px;padding:6px 10px;border:0;cursor:pointer;';
|
|
172
|
+
actions.appendChild(downloadBtn);
|
|
173
|
+
inner.appendChild(close);
|
|
174
|
+
inner.appendChild(mediaWrap);
|
|
175
|
+
inner.appendChild(actions);
|
|
176
|
+
modal.appendChild(inner);
|
|
177
|
+
modal.addEventListener('click', (e) => { if (e.target === modal) hidePreviewModal(); });
|
|
178
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hidePreviewModal(); });
|
|
179
|
+
document.body.appendChild(modal);
|
|
180
|
+
modal._mediaWrap = mediaWrap;
|
|
181
|
+
modal._downloadBtn = downloadBtn;
|
|
182
|
+
return modal;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function showPreviewModal(resourceId, fileName, fileType) {
|
|
186
|
+
const modal = ensurePreviewModal();
|
|
187
|
+
const wrap = modal._mediaWrap;
|
|
188
|
+
wrap.innerHTML = '';
|
|
189
|
+
let src = null;
|
|
190
|
+
if (config.getPreviewUrl && typeof config.getPreviewUrl === 'function') {
|
|
191
|
+
try { src = await config.getPreviewUrl(resourceId); } catch {}
|
|
192
|
+
}
|
|
193
|
+
if (!src && config.getThumbnail && typeof config.getThumbnail === 'function') {
|
|
194
|
+
try { src = await config.getThumbnail(resourceId); } catch {}
|
|
195
|
+
}
|
|
196
|
+
if (fileType?.startsWith?.('image/')) {
|
|
197
|
+
const img = document.createElement('img');
|
|
198
|
+
if (src) img.src = src;
|
|
199
|
+
img.alt = fileName || resourceId;
|
|
200
|
+
img.style.cssText = 'max-width:85vw;max-height:80vh;border-radius:8px;object-fit:contain;background:#111';
|
|
201
|
+
wrap.appendChild(img);
|
|
202
|
+
} else if (fileType?.startsWith?.('video/')) {
|
|
203
|
+
const video = document.createElement('video');
|
|
204
|
+
video.controls = true;
|
|
205
|
+
if (src) video.src = src;
|
|
206
|
+
video.style.cssText = 'max-width:85vw;max-height:80vh;border-radius:8px;background:#000';
|
|
207
|
+
wrap.appendChild(video);
|
|
208
|
+
} else {
|
|
209
|
+
const box = document.createElement('div');
|
|
210
|
+
box.style.cssText = 'color:#e5e7eb;padding:24px;';
|
|
211
|
+
box.textContent = fileName || resourceId;
|
|
212
|
+
wrap.appendChild(box);
|
|
213
|
+
}
|
|
214
|
+
modal._downloadBtn.onclick = async () => {
|
|
215
|
+
if (config.downloadFile && typeof config.downloadFile === 'function') {
|
|
216
|
+
try { await config.downloadFile(resourceId, fileName || 'file'); } catch {}
|
|
217
|
+
} else {
|
|
218
|
+
console.log('Download simulated:', resourceId, fileName || 'file');
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
modal.style.display = 'flex';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function hidePreviewModal() {
|
|
225
|
+
const modal = document.getElementById('fb-preview-modal');
|
|
226
|
+
if (modal) modal.style.display = 'none';
|
|
227
|
+
}
|
|
228
|
+
|
|
150
229
|
function setTextValueFromPrefill(input, element, prefillObj, key) {
|
|
151
230
|
let v = undefined;
|
|
152
231
|
if (prefillObj && Object.prototype.hasOwnProperty.call(prefillObj, key)) v = prefillObj[key];
|
|
@@ -243,6 +322,7 @@
|
|
|
243
322
|
|
|
244
323
|
const preview = document.createElement('div');
|
|
245
324
|
preview.className = 'flex items-center gap-3 p-2';
|
|
325
|
+
const isReadonly = container.closest('.file-container')?.dataset.readonly === 'true';
|
|
246
326
|
|
|
247
327
|
// File icon/thumbnail
|
|
248
328
|
const iconContainer = document.createElement('div');
|
|
@@ -313,21 +393,31 @@
|
|
|
313
393
|
}
|
|
314
394
|
});
|
|
315
395
|
|
|
316
|
-
// Remove button
|
|
317
|
-
const removeBtn = document.createElement('button');
|
|
318
|
-
removeBtn.className = 'px-2 py-1 text-xs bg-red-500 hover:bg-red-600 text-white rounded transition-colors';
|
|
319
|
-
removeBtn.textContent = '✕';
|
|
320
|
-
removeBtn.title = 'Remove';
|
|
321
|
-
removeBtn.addEventListener('click', () => {
|
|
322
|
-
const hiddenInput = container.closest('.file-container')?.querySelector('input[type="hidden"]');
|
|
323
|
-
if (hiddenInput) {
|
|
324
|
-
hiddenInput.value = '';
|
|
325
|
-
}
|
|
326
|
-
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 to upload</div></div>';
|
|
327
|
-
});
|
|
328
|
-
|
|
329
396
|
actions.appendChild(downloadBtn);
|
|
330
|
-
|
|
397
|
+
// Open button (preview modal)
|
|
398
|
+
const openBtn = document.createElement('button');
|
|
399
|
+
openBtn.className = 'px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded transition-colors';
|
|
400
|
+
openBtn.textContent = 'Open';
|
|
401
|
+
openBtn.title = 'Open preview';
|
|
402
|
+
openBtn.addEventListener('click', async () => {
|
|
403
|
+
await showPreviewModal(resourceId, fileName, fileType);
|
|
404
|
+
});
|
|
405
|
+
actions.appendChild(openBtn);
|
|
406
|
+
if (!isReadonly) {
|
|
407
|
+
// Remove button (editable only)
|
|
408
|
+
const removeBtn = document.createElement('button');
|
|
409
|
+
removeBtn.className = 'px-2 py-1 text-xs bg-red-500 hover:bg-red-600 text-white rounded transition-colors';
|
|
410
|
+
removeBtn.textContent = '✕';
|
|
411
|
+
removeBtn.title = 'Remove';
|
|
412
|
+
removeBtn.addEventListener('click', () => {
|
|
413
|
+
const hiddenInput = container.closest('.file-container')?.querySelector('input[type="hidden"]');
|
|
414
|
+
if (hiddenInput) {
|
|
415
|
+
hiddenInput.value = '';
|
|
416
|
+
}
|
|
417
|
+
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 to upload</div></div>';
|
|
418
|
+
});
|
|
419
|
+
actions.appendChild(removeBtn);
|
|
420
|
+
}
|
|
331
421
|
preview.appendChild(actions);
|
|
332
422
|
|
|
333
423
|
container.appendChild(preview);
|
|
@@ -466,13 +556,21 @@
|
|
|
466
556
|
|
|
467
557
|
// Preview container
|
|
468
558
|
const previewContainer = document.createElement('div');
|
|
469
|
-
|
|
470
|
-
|
|
559
|
+
if (options.readonly) {
|
|
560
|
+
container.dataset.readonly = 'true';
|
|
561
|
+
previewContainer.className = 'aspect-square w-full max-w-xs bg-gray-100 rounded-lg overflow-hidden border-2 border-gray-300 transition-colors relative mb-3';
|
|
562
|
+
} else {
|
|
563
|
+
previewContainer.className = 'aspect-square w-full max-w-xs bg-gray-100 rounded-lg overflow-hidden border-2 border-dashed border-gray-300 hover:border-gray-400 transition-colors relative group cursor-pointer mb-3';
|
|
564
|
+
}
|
|
471
565
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
picker.
|
|
566
|
+
let picker = null;
|
|
567
|
+
if (!options.readonly) {
|
|
568
|
+
picker = document.createElement('input');
|
|
569
|
+
picker.type = 'file';
|
|
570
|
+
if (element.accept?.extensions) {
|
|
571
|
+
picker.accept = element.accept.extensions.map(ext => `.${ext}`).join(',');
|
|
572
|
+
}
|
|
573
|
+
previewContainer.onclick = () => picker.click();
|
|
476
574
|
}
|
|
477
575
|
|
|
478
576
|
const handleFileSelect = async (file) => {
|
|
@@ -502,11 +600,13 @@
|
|
|
502
600
|
}
|
|
503
601
|
};
|
|
504
602
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
603
|
+
if (!options.readonly) {
|
|
604
|
+
picker.addEventListener('change', async () => {
|
|
605
|
+
if (picker.files && picker.files[0]) {
|
|
606
|
+
await handleFileSelect(picker.files[0]);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}
|
|
510
610
|
|
|
511
611
|
// Handle prefilled values
|
|
512
612
|
const pv = ctx.prefill && ctx.prefill[element.key];
|
|
@@ -516,22 +616,29 @@
|
|
|
516
616
|
const fileName = `file_${pv.slice(-8)}`;
|
|
517
617
|
renderFilePreview(previewContainer, pv, fileName, 'application/octet-stream');
|
|
518
618
|
} else {
|
|
519
|
-
// Show upload prompt
|
|
520
|
-
|
|
619
|
+
// Show upload prompt (editable only)
|
|
620
|
+
if (!options.readonly) {
|
|
621
|
+
previewContainer.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 to upload</div></div>';
|
|
622
|
+
} else {
|
|
623
|
+
previewContainer.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">No file</div></div>';
|
|
624
|
+
}
|
|
521
625
|
}
|
|
522
626
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
627
|
+
let helpText = null;
|
|
628
|
+
if (!options.readonly) {
|
|
629
|
+
helpText = document.createElement('p');
|
|
630
|
+
helpText.className = 'text-xs text-gray-600 mt-2 text-center';
|
|
631
|
+
helpText.innerHTML = '<span class="underline cursor-pointer">Upload</span> or drag and drop file';
|
|
632
|
+
helpText.onclick = () => picker.click();
|
|
633
|
+
}
|
|
527
634
|
|
|
528
635
|
container.appendChild(previewContainer);
|
|
529
|
-
container.appendChild(helpText);
|
|
530
|
-
container.appendChild(picker);
|
|
636
|
+
if (helpText) container.appendChild(helpText);
|
|
637
|
+
if (picker) container.appendChild(picker);
|
|
531
638
|
container.appendChild(hid);
|
|
532
639
|
|
|
533
640
|
wrapper.appendChild(container);
|
|
534
|
-
wrapper.appendChild(makeFieldHint(element, 'Returns resource ID for download/submission'));
|
|
641
|
+
wrapper.appendChild(makeFieldHint(element, options.readonly ? 'Read-only' : 'Returns resource ID for download/submission'));
|
|
535
642
|
break;
|
|
536
643
|
}
|
|
537
644
|
case 'files': {
|
|
@@ -541,71 +648,90 @@
|
|
|
541
648
|
hid.dataset.type = 'files';
|
|
542
649
|
|
|
543
650
|
const list = document.createElement('div');
|
|
544
|
-
list.className = 'flex flex-wrap gap-1.5 mt-2';
|
|
651
|
+
list.className = options.readonly ? 'flex flex-col gap-2 mt-2' : 'flex flex-wrap gap-1.5 mt-2';
|
|
545
652
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
picker.
|
|
653
|
+
let picker = null;
|
|
654
|
+
if (!options.readonly) {
|
|
655
|
+
picker = document.createElement('input');
|
|
656
|
+
picker.type = 'file';
|
|
657
|
+
picker.multiple = true;
|
|
658
|
+
if (element.accept?.extensions) {
|
|
659
|
+
picker.accept = element.accept.extensions.map(ext => `.${ext}`).join(',');
|
|
660
|
+
}
|
|
551
661
|
}
|
|
552
662
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if (picker.files && picker.files.length) {
|
|
558
|
-
for (const file of picker.files) {
|
|
559
|
-
const err = fileValidationError(element, file);
|
|
560
|
-
if (err) {
|
|
561
|
-
markValidity(picker, err);
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
663
|
+
if (picker) {
|
|
664
|
+
picker.addEventListener('change', async () => {
|
|
665
|
+
let arr = parseJSONSafe(hid.value, []);
|
|
666
|
+
if (!Array.isArray(arr)) arr = [];
|
|
565
667
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
resourceId = await makeResourceIdFromFile(file);
|
|
668
|
+
if (picker.files && picker.files.length) {
|
|
669
|
+
for (const file of picker.files) {
|
|
670
|
+
const err = fileValidationError(element, file);
|
|
671
|
+
if (err) {
|
|
672
|
+
markValidity(picker, err);
|
|
673
|
+
return;
|
|
573
674
|
}
|
|
574
|
-
resourceIndex.set(resourceId, { name: file.name, type: file.type, size: file.size });
|
|
575
|
-
arr.push(resourceId);
|
|
576
|
-
} catch (error) {
|
|
577
|
-
markValidity(picker, `Upload failed: ${error.message}`);
|
|
578
|
-
return;
|
|
579
675
|
}
|
|
676
|
+
|
|
677
|
+
for (const file of picker.files) {
|
|
678
|
+
try {
|
|
679
|
+
let resourceId;
|
|
680
|
+
if (config.uploadFile && typeof config.uploadFile === 'function') {
|
|
681
|
+
resourceId = await config.uploadFile(file);
|
|
682
|
+
} else {
|
|
683
|
+
resourceId = await makeResourceIdFromFile(file);
|
|
684
|
+
}
|
|
685
|
+
resourceIndex.set(resourceId, { name: file.name, type: file.type, size: file.size });
|
|
686
|
+
arr.push(resourceId);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
markValidity(picker, `Upload failed: ${error.message}`);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
hid.value = JSON.stringify(arr);
|
|
694
|
+
renderResourcePills(list, arr, (ridToRemove) => {
|
|
695
|
+
const next = arr.filter(x => x !== ridToRemove);
|
|
696
|
+
hid.value = JSON.stringify(next);
|
|
697
|
+
arr = next;
|
|
698
|
+
renderResourcePills(list, next, arguments.callee);
|
|
699
|
+
});
|
|
700
|
+
markValidity(picker, null);
|
|
580
701
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
renderResourcePills(list, arr, (ridToRemove) => {
|
|
584
|
-
const next = arr.filter(x => x !== ridToRemove);
|
|
585
|
-
hid.value = JSON.stringify(next);
|
|
586
|
-
arr = next;
|
|
587
|
-
renderResourcePills(list, next, arguments.callee);
|
|
588
|
-
});
|
|
589
|
-
markValidity(picker, null);
|
|
590
|
-
}
|
|
591
|
-
});
|
|
702
|
+
});
|
|
703
|
+
}
|
|
592
704
|
|
|
593
705
|
const pv = ctx.prefill && ctx.prefill[element.key];
|
|
594
706
|
let initial = Array.isArray(pv) ? pv.filter(Boolean) : [];
|
|
595
707
|
if (initial.length) {
|
|
596
708
|
hid.value = JSON.stringify(initial);
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
initial
|
|
601
|
-
|
|
602
|
-
|
|
709
|
+
if (options.readonly) {
|
|
710
|
+
// Render each as small preview with download
|
|
711
|
+
list.innerHTML = '';
|
|
712
|
+
for (const rid of initial) {
|
|
713
|
+
const row = document.createElement('div');
|
|
714
|
+
row.className = 'file-container';
|
|
715
|
+
row.dataset.readonly = 'true';
|
|
716
|
+
const itemPreview = document.createElement('div');
|
|
717
|
+
row.appendChild(itemPreview);
|
|
718
|
+
renderFilePreview(itemPreview, rid, `file_${rid.slice(-8)}`, 'application/octet-stream');
|
|
719
|
+
list.appendChild(row);
|
|
720
|
+
}
|
|
721
|
+
} else {
|
|
722
|
+
renderResourcePills(list, initial, (ridToRemove) => {
|
|
723
|
+
const next = initial.filter(x => x !== ridToRemove);
|
|
724
|
+
hid.value = JSON.stringify(next);
|
|
725
|
+
initial = next;
|
|
726
|
+
renderResourcePills(list, next, arguments.callee);
|
|
727
|
+
});
|
|
728
|
+
}
|
|
603
729
|
}
|
|
604
730
|
|
|
605
|
-
wrapper.appendChild(picker);
|
|
731
|
+
if (picker) wrapper.appendChild(picker);
|
|
606
732
|
wrapper.appendChild(list);
|
|
607
733
|
wrapper.appendChild(hid);
|
|
608
|
-
wrapper.appendChild(makeFieldHint(element, 'Multiple files return resource ID array'));
|
|
734
|
+
wrapper.appendChild(makeFieldHint(element, options.readonly ? 'Read-only' : 'Multiple files return resource ID array'));
|
|
609
735
|
break;
|
|
610
736
|
}
|
|
611
737
|
case 'videos': {
|
package/dist/sample.html
CHANGED
|
@@ -420,7 +420,7 @@
|
|
|
420
420
|
formEl.id = 'dynamicForm';
|
|
421
421
|
formEl.addEventListener('submit', (e) => e.preventDefault());
|
|
422
422
|
|
|
423
|
-
const ctx = { path: '', prefill: prefill || {} };
|
|
423
|
+
const ctx = { path: '', prefill: prefill || {}, readonly: state.config.readonly || false };
|
|
424
424
|
schema.elements.forEach(element => {
|
|
425
425
|
const block = renderElement(element, ctx);
|
|
426
426
|
formEl.appendChild(block);
|
|
@@ -531,14 +531,34 @@
|
|
|
531
531
|
|
|
532
532
|
// Preview container
|
|
533
533
|
const previewContainer = document.createElement('div');
|
|
534
|
-
previewContainer.className = 'aspect-square w-full max-w-xs bg-gray-100 rounded-lg overflow-hidden border-2 border-
|
|
535
|
-
previewContainer.onclick = () => picker.click();
|
|
534
|
+
previewContainer.className = 'aspect-square w-full max-w-xs bg-gray-100 rounded-lg overflow-hidden border-2 border-gray-300 transition-colors relative mb-3';
|
|
536
535
|
|
|
536
|
+
// Read-only rendering: show preview + download only
|
|
537
|
+
if (ctx.readonly === true) {
|
|
538
|
+
previewContainer.dataset.readonly = 'true';
|
|
539
|
+
const pv = ctx.prefill && ctx.prefill[element.key];
|
|
540
|
+
if (typeof pv === 'string' && pv) {
|
|
541
|
+
hid.value = pv;
|
|
542
|
+
const fileName = `file_${pv.slice(-8)}`;
|
|
543
|
+
renderFilePreview(previewContainer, pv, fileName, 'application/octet-stream');
|
|
544
|
+
} else {
|
|
545
|
+
previewContainer.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">No file</div></div>';
|
|
546
|
+
}
|
|
547
|
+
container.appendChild(previewContainer);
|
|
548
|
+
container.appendChild(hid);
|
|
549
|
+
wrapper.appendChild(container);
|
|
550
|
+
wrapper.appendChild(makeFieldHint(element, 'Read-only'));
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Editable mode
|
|
555
|
+
previewContainer.className += ' border-dashed hover:border-gray-400 cursor-pointer group';
|
|
537
556
|
const picker = document.createElement('input');
|
|
538
557
|
picker.type = 'file';
|
|
539
558
|
if (element.accept?.extensions) {
|
|
540
559
|
picker.accept = element.accept.extensions.map(ext => `.${ext}`).join(',');
|
|
541
560
|
}
|
|
561
|
+
previewContainer.onclick = () => picker.click();
|
|
542
562
|
|
|
543
563
|
const handleFileSelect = async (file) => {
|
|
544
564
|
const err = fileValidationError(element, file);
|
|
@@ -607,7 +627,73 @@
|
|
|
607
627
|
|
|
608
628
|
const list = document.createElement('div');
|
|
609
629
|
list.className = 'list';
|
|
610
|
-
|
|
630
|
+
|
|
631
|
+
// Read-only rendering: show list of previews with download buttons
|
|
632
|
+
if (ctx.readonly === true) {
|
|
633
|
+
const pv = ctx.prefill && ctx.prefill[element.key];
|
|
634
|
+
const rids = Array.isArray(pv) ? pv.filter(Boolean) : [];
|
|
635
|
+
const renderReadonlyList = async (ridsArr) => {
|
|
636
|
+
list.innerHTML = '';
|
|
637
|
+
ridsArr.forEach(async (rid) => {
|
|
638
|
+
const row = document.createElement('div');
|
|
639
|
+
row.className = 'flex items-center gap-3 p-2 border border-gray-200 rounded';
|
|
640
|
+
const icon = document.createElement('div');
|
|
641
|
+
icon.className = 'w-12 h-12 rounded-lg flex items-center justify-center bg-blue-600 text-white text-xl flex-shrink-0';
|
|
642
|
+
// Try thumbnail
|
|
643
|
+
if (state.config.getThumbnail && typeof state.config.getThumbnail === 'function') {
|
|
644
|
+
try {
|
|
645
|
+
const thumb = await state.config.getThumbnail(rid);
|
|
646
|
+
if (thumb) {
|
|
647
|
+
const img = document.createElement('img');
|
|
648
|
+
img.className = 'w-12 h-12 object-cover rounded-lg';
|
|
649
|
+
img.src = thumb;
|
|
650
|
+
icon.innerHTML = '';
|
|
651
|
+
icon.appendChild(img);
|
|
652
|
+
} else {
|
|
653
|
+
icon.textContent = '📎';
|
|
654
|
+
}
|
|
655
|
+
} catch {
|
|
656
|
+
icon.textContent = '📎';
|
|
657
|
+
}
|
|
658
|
+
} else {
|
|
659
|
+
icon.textContent = '📎';
|
|
660
|
+
}
|
|
661
|
+
const info = document.createElement('div');
|
|
662
|
+
info.className = 'flex-1 text-sm text-gray-700';
|
|
663
|
+
info.textContent = rid;
|
|
664
|
+
const actions = document.createElement('div');
|
|
665
|
+
actions.className = 'flex items-center gap-2';
|
|
666
|
+
const downloadBtn = document.createElement('button');
|
|
667
|
+
downloadBtn.type = 'button';
|
|
668
|
+
downloadBtn.className = 'px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded';
|
|
669
|
+
downloadBtn.textContent = 'Download';
|
|
670
|
+
downloadBtn.addEventListener('click', async () => {
|
|
671
|
+
if (state.config.downloadFile && typeof state.config.downloadFile === 'function') {
|
|
672
|
+
try { await state.config.downloadFile(rid, rid); } catch(_) {}
|
|
673
|
+
} else {
|
|
674
|
+
console.log('Download simulated:', rid);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
actions.appendChild(downloadBtn);
|
|
678
|
+
row.appendChild(icon);
|
|
679
|
+
row.appendChild(info);
|
|
680
|
+
row.appendChild(actions);
|
|
681
|
+
list.appendChild(row);
|
|
682
|
+
});
|
|
683
|
+
};
|
|
684
|
+
if (rids.length) {
|
|
685
|
+
hid.value = JSON.stringify(rids);
|
|
686
|
+
renderReadonlyList(rids);
|
|
687
|
+
} else {
|
|
688
|
+
list.innerHTML = '<div class="text-gray-400 text-sm">No files</div>';
|
|
689
|
+
}
|
|
690
|
+
wrapper.appendChild(list);
|
|
691
|
+
wrapper.appendChild(hid);
|
|
692
|
+
wrapper.appendChild(makeFieldHint(element, 'Read-only'));
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Editable mode
|
|
611
697
|
const picker = document.createElement('input');
|
|
612
698
|
picker.type = 'file';
|
|
613
699
|
picker.multiple = true;
|
|
@@ -789,6 +875,7 @@
|
|
|
789
875
|
|
|
790
876
|
const preview = document.createElement('div');
|
|
791
877
|
preview.style.cssText = 'display: flex; align-items: center; gap: 12px; padding: 8px;';
|
|
878
|
+
const isReadonly = state.config.readonly || container.dataset.readonly === 'true';
|
|
792
879
|
|
|
793
880
|
// File icon/thumbnail
|
|
794
881
|
const iconContainer = document.createElement('div');
|
|
@@ -860,22 +947,23 @@
|
|
|
860
947
|
}
|
|
861
948
|
});
|
|
862
949
|
|
|
863
|
-
// Remove button
|
|
864
|
-
const removeBtn = document.createElement('button');
|
|
865
|
-
removeBtn.className = 'btn bad';
|
|
866
|
-
removeBtn.style.cssText = 'padding: 6px 10px; font-size: 12px;';
|
|
867
|
-
removeBtn.textContent = '✕';
|
|
868
|
-
removeBtn.title = 'Remove';
|
|
869
|
-
removeBtn.addEventListener('click', () => {
|
|
870
|
-
const hiddenInput = container.parentElement.querySelector('input[type="hidden"]');
|
|
871
|
-
if (hiddenInput) {
|
|
872
|
-
hiddenInput.value = '';
|
|
873
|
-
}
|
|
874
|
-
container.innerHTML = '<div style="color: var(--muted); font-size: 14px;">📁 Click "Choose File" to upload</div>';
|
|
875
|
-
});
|
|
876
|
-
|
|
877
950
|
actions.appendChild(downloadBtn);
|
|
878
|
-
|
|
951
|
+
if (!isReadonly) {
|
|
952
|
+
// Remove button (editable only)
|
|
953
|
+
const removeBtn = document.createElement('button');
|
|
954
|
+
removeBtn.className = 'btn bad';
|
|
955
|
+
removeBtn.style.cssText = 'padding: 6px 10px; font-size: 12px;';
|
|
956
|
+
removeBtn.textContent = '✕';
|
|
957
|
+
removeBtn.title = 'Remove';
|
|
958
|
+
removeBtn.addEventListener('click', () => {
|
|
959
|
+
const hiddenInput = container.parentElement.querySelector('input[type="hidden"]');
|
|
960
|
+
if (hiddenInput) {
|
|
961
|
+
hiddenInput.value = '';
|
|
962
|
+
}
|
|
963
|
+
container.innerHTML = '<div style="color: var(--muted); font-size: 14px;">📁 Click "Choose File" to upload</div>';
|
|
964
|
+
});
|
|
965
|
+
actions.appendChild(removeBtn);
|
|
966
|
+
}
|
|
879
967
|
preview.appendChild(actions);
|
|
880
968
|
|
|
881
969
|
container.appendChild(preview);
|