@dmitryvim/form-builder 0.1.6 → 0.1.8
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 +165 -4
- package/package.json +1 -1
package/dist/form-builder.js
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
if (seen.has(el.key)) errors.push(`${path}: duplicate key "${el.key}"`);
|
|
72
72
|
seen.add(el.key);
|
|
73
73
|
}
|
|
74
|
-
if (el.default !== undefined && (el.type === 'file' || el.type === 'files')) {
|
|
74
|
+
if (el.default !== undefined && (el.type === 'file' || el.type === 'files' || el.type === 'videos')) {
|
|
75
75
|
errors.push(`${here}: default forbidden for "${el.type}"`);
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -112,6 +112,11 @@
|
|
|
112
112
|
errors.push(`${here}: minCount > maxCount`);
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
if (el.type === 'videos') {
|
|
116
|
+
if (el.minCount != null && el.maxCount != null && el.minCount > el.maxCount) {
|
|
117
|
+
errors.push(`${here}: minCount > maxCount`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
115
120
|
if (el.type === 'group') {
|
|
116
121
|
if (!Array.isArray(el.elements)) errors.push(`${here}: group.elements must be array`);
|
|
117
122
|
if (el.repeat) {
|
|
@@ -224,6 +229,10 @@
|
|
|
224
229
|
if (element.minCount != null) bits.push(`minCount=${element.minCount}`);
|
|
225
230
|
if (element.maxCount != null) bits.push(`maxCount=${element.maxCount}`);
|
|
226
231
|
}
|
|
232
|
+
if (element.type === 'videos') {
|
|
233
|
+
if (element.minCount != null) bits.push(`minCount=${element.minCount}`);
|
|
234
|
+
if (element.maxCount != null) bits.push(`maxCount=${element.maxCount}`);
|
|
235
|
+
}
|
|
227
236
|
|
|
228
237
|
hint.textContent = [bits.join(' • '), extra].filter(Boolean).join(' | ');
|
|
229
238
|
return hint;
|
|
@@ -555,9 +564,19 @@
|
|
|
555
564
|
}
|
|
556
565
|
|
|
557
566
|
for (const file of picker.files) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
567
|
+
try {
|
|
568
|
+
let resourceId;
|
|
569
|
+
if (config.uploadFile && typeof config.uploadFile === 'function') {
|
|
570
|
+
resourceId = await config.uploadFile(file);
|
|
571
|
+
} else {
|
|
572
|
+
resourceId = await makeResourceIdFromFile(file);
|
|
573
|
+
}
|
|
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
|
+
}
|
|
561
580
|
}
|
|
562
581
|
|
|
563
582
|
hid.value = JSON.stringify(arr);
|
|
@@ -589,6 +608,131 @@
|
|
|
589
608
|
wrapper.appendChild(makeFieldHint(element, 'Multiple files return resource ID array'));
|
|
590
609
|
break;
|
|
591
610
|
}
|
|
611
|
+
case 'videos': {
|
|
612
|
+
const hid = document.createElement('input');
|
|
613
|
+
hid.type = 'hidden';
|
|
614
|
+
hid.name = pathKey;
|
|
615
|
+
hid.dataset.type = 'videos';
|
|
616
|
+
|
|
617
|
+
const list = document.createElement('div');
|
|
618
|
+
list.className = 'flex flex-col gap-3 mt-2';
|
|
619
|
+
|
|
620
|
+
const picker = document.createElement('input');
|
|
621
|
+
picker.type = 'file';
|
|
622
|
+
picker.multiple = true;
|
|
623
|
+
{
|
|
624
|
+
const acc = [];
|
|
625
|
+
if (element.accept?.mime && Array.isArray(element.accept.mime) && element.accept.mime.length) {
|
|
626
|
+
acc.push(...element.accept.mime);
|
|
627
|
+
}
|
|
628
|
+
if (element.accept?.extensions && Array.isArray(element.accept.extensions) && element.accept.extensions.length) {
|
|
629
|
+
acc.push(...element.accept.extensions.map(ext => `.${ext}`));
|
|
630
|
+
}
|
|
631
|
+
picker.accept = acc.length ? acc.join(',') : 'video/*';
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const renderVideos = (rids) => {
|
|
635
|
+
list.innerHTML = '';
|
|
636
|
+
rids.forEach(rid => {
|
|
637
|
+
const meta = resourceIndex.get(rid) || {};
|
|
638
|
+
const row = document.createElement('div');
|
|
639
|
+
row.className = 'flex items-start gap-3';
|
|
640
|
+
const video = document.createElement('video');
|
|
641
|
+
video.controls = true;
|
|
642
|
+
video.className = 'w-48 max-w-full rounded border border-gray-300';
|
|
643
|
+
// Use thumbnail as poster instead of loading video src
|
|
644
|
+
if (config.getThumbnail) {
|
|
645
|
+
Promise.resolve(config.getThumbnail(rid)).then(url => {
|
|
646
|
+
if (url) {
|
|
647
|
+
video.poster = url;
|
|
648
|
+
}
|
|
649
|
+
}).catch(() => {});
|
|
650
|
+
}
|
|
651
|
+
const info = document.createElement('div');
|
|
652
|
+
info.className = 'flex-1 text-sm text-gray-700';
|
|
653
|
+
info.textContent = `${meta.name || 'video'} (${formatFileSize(meta.size || 0)})`;
|
|
654
|
+
const actions = document.createElement('div');
|
|
655
|
+
actions.className = 'flex items-center gap-2';
|
|
656
|
+
const downloadBtn = document.createElement('button');
|
|
657
|
+
downloadBtn.type = 'button';
|
|
658
|
+
downloadBtn.className = 'px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded';
|
|
659
|
+
downloadBtn.textContent = 'Download';
|
|
660
|
+
downloadBtn.addEventListener('click', async () => {
|
|
661
|
+
if (config.downloadFile && typeof config.downloadFile === 'function') {
|
|
662
|
+
try { await config.downloadFile(rid, meta.name || 'video'); } catch(_) {}
|
|
663
|
+
} else {
|
|
664
|
+
console.log('Download simulated:', rid, meta.name || 'video');
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
const remove = document.createElement('button');
|
|
668
|
+
remove.type = 'button';
|
|
669
|
+
remove.className = 'px-2 py-1 text-xs bg-red-500 hover:bg-red-600 text-white rounded';
|
|
670
|
+
remove.textContent = 'Remove';
|
|
671
|
+
remove.addEventListener('click', () => {
|
|
672
|
+
const arr = parseJSONSafe(hid.value, []);
|
|
673
|
+
const next = Array.isArray(arr) ? arr.filter(x => x !== rid) : [];
|
|
674
|
+
hid.value = JSON.stringify(next);
|
|
675
|
+
renderVideos(next);
|
|
676
|
+
});
|
|
677
|
+
row.appendChild(video);
|
|
678
|
+
row.appendChild(info);
|
|
679
|
+
actions.appendChild(downloadBtn);
|
|
680
|
+
actions.appendChild(remove);
|
|
681
|
+
row.appendChild(actions);
|
|
682
|
+
list.appendChild(row);
|
|
683
|
+
});
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
picker.addEventListener('change', async () => {
|
|
687
|
+
let arr = parseJSONSafe(hid.value, []);
|
|
688
|
+
if (!Array.isArray(arr)) arr = [];
|
|
689
|
+
if (picker.files && picker.files.length) {
|
|
690
|
+
for (const file of picker.files) {
|
|
691
|
+
const err = fileValidationError(element, file);
|
|
692
|
+
if (err) {
|
|
693
|
+
markValidity(picker, err);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
// additionally ensure it's a video
|
|
697
|
+
if (!file.type.startsWith('video/')) {
|
|
698
|
+
markValidity(picker, 'mime not allowed: ' + file.type);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
for (const file of picker.files) {
|
|
703
|
+
try {
|
|
704
|
+
let resourceId;
|
|
705
|
+
if (config.uploadFile && typeof config.uploadFile === 'function') {
|
|
706
|
+
resourceId = await config.uploadFile(file);
|
|
707
|
+
} else {
|
|
708
|
+
resourceId = await makeResourceIdFromFile(file);
|
|
709
|
+
}
|
|
710
|
+
resourceIndex.set(resourceId, { name: file.name, type: file.type, size: file.size });
|
|
711
|
+
arr.push(resourceId);
|
|
712
|
+
} catch (error) {
|
|
713
|
+
markValidity(picker, `Upload failed: ${error.message}`);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
hid.value = JSON.stringify(arr);
|
|
718
|
+
renderVideos(arr);
|
|
719
|
+
markValidity(picker, null);
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
const pv = ctx.prefill && ctx.prefill[element.key];
|
|
724
|
+
let initial = Array.isArray(pv) ? pv.filter(Boolean) : [];
|
|
725
|
+
if (initial.length) {
|
|
726
|
+
hid.value = JSON.stringify(initial);
|
|
727
|
+
renderVideos(initial);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
wrapper.appendChild(picker);
|
|
731
|
+
wrapper.appendChild(list);
|
|
732
|
+
wrapper.appendChild(hid);
|
|
733
|
+
wrapper.appendChild(makeFieldHint(element, 'Multiple videos return resource ID array'));
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
592
736
|
case 'group': {
|
|
593
737
|
wrapper.dataset.group = element.key;
|
|
594
738
|
wrapper.dataset.groupPath = pathKey;
|
|
@@ -846,6 +990,20 @@
|
|
|
846
990
|
if (hid?.previousElementSibling) markValidity(hid.previousElementSibling, null);
|
|
847
991
|
return Array.isArray(arr) ? arr : [];
|
|
848
992
|
}
|
|
993
|
+
case 'videos': {
|
|
994
|
+
const hid = scopeRoot.querySelector(`input[type="hidden"][name$="${key}"]`);
|
|
995
|
+
const arr = parseJSONSafe(hid?.value ?? '[]', []);
|
|
996
|
+
const count = Array.isArray(arr) ? arr.length : 0;
|
|
997
|
+
if (!skipValidation && !Array.isArray(arr)) errors.push(`${key}: internal value corrupted`);
|
|
998
|
+
if (!skipValidation && element.minCount != null && count < element.minCount) {
|
|
999
|
+
errors.push(`${key}: < minCount=${element.minCount}`);
|
|
1000
|
+
}
|
|
1001
|
+
if (!skipValidation && element.maxCount != null && count > element.maxCount) {
|
|
1002
|
+
errors.push(`${key}: > maxCount=${element.maxCount}`);
|
|
1003
|
+
}
|
|
1004
|
+
if (hid?.previousElementSibling) markValidity(hid.previousElementSibling, null);
|
|
1005
|
+
return Array.isArray(arr) ? arr : [];
|
|
1006
|
+
}
|
|
849
1007
|
case 'group': {
|
|
850
1008
|
const groupWrapper = scopeRoot.querySelector(`[data-group="${key}"]`);
|
|
851
1009
|
if (!groupWrapper) {
|
|
@@ -1008,6 +1166,9 @@
|
|
|
1008
1166
|
case 'files':
|
|
1009
1167
|
obj[el.key] = [];
|
|
1010
1168
|
break;
|
|
1169
|
+
case 'videos':
|
|
1170
|
+
obj[el.key] = [];
|
|
1171
|
+
break;
|
|
1011
1172
|
case 'group':
|
|
1012
1173
|
if (el.repeat && isPlainObject(el.repeat)) {
|
|
1013
1174
|
const sample = walk(el.elements);
|