@dmitryvim/form-builder 0.1.6 → 0.1.7
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 +142 -1
- 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;
|
|
@@ -589,6 +598,121 @@
|
|
|
589
598
|
wrapper.appendChild(makeFieldHint(element, 'Multiple files return resource ID array'));
|
|
590
599
|
break;
|
|
591
600
|
}
|
|
601
|
+
case 'videos': {
|
|
602
|
+
const hid = document.createElement('input');
|
|
603
|
+
hid.type = 'hidden';
|
|
604
|
+
hid.name = pathKey;
|
|
605
|
+
hid.dataset.type = 'videos';
|
|
606
|
+
|
|
607
|
+
const list = document.createElement('div');
|
|
608
|
+
list.className = 'flex flex-col gap-3 mt-2';
|
|
609
|
+
|
|
610
|
+
const picker = document.createElement('input');
|
|
611
|
+
picker.type = 'file';
|
|
612
|
+
picker.multiple = true;
|
|
613
|
+
{
|
|
614
|
+
const acc = [];
|
|
615
|
+
if (element.accept?.mime && Array.isArray(element.accept.mime) && element.accept.mime.length) {
|
|
616
|
+
acc.push(...element.accept.mime);
|
|
617
|
+
}
|
|
618
|
+
if (element.accept?.extensions && Array.isArray(element.accept.extensions) && element.accept.extensions.length) {
|
|
619
|
+
acc.push(...element.accept.extensions.map(ext => `.${ext}`));
|
|
620
|
+
}
|
|
621
|
+
picker.accept = acc.length ? acc.join(',') : 'video/*';
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const renderVideos = (rids) => {
|
|
625
|
+
list.innerHTML = '';
|
|
626
|
+
rids.forEach(rid => {
|
|
627
|
+
const meta = resourceIndex.get(rid) || {};
|
|
628
|
+
const row = document.createElement('div');
|
|
629
|
+
row.className = 'flex items-start gap-3';
|
|
630
|
+
const video = document.createElement('video');
|
|
631
|
+
video.controls = true;
|
|
632
|
+
video.className = 'w-48 max-w-full rounded border border-gray-300';
|
|
633
|
+
// Use thumbnail as poster instead of loading video src
|
|
634
|
+
if (config.getThumbnail) {
|
|
635
|
+
Promise.resolve(config.getThumbnail(rid)).then(url => {
|
|
636
|
+
if (url) {
|
|
637
|
+
video.poster = url;
|
|
638
|
+
}
|
|
639
|
+
}).catch(() => {});
|
|
640
|
+
}
|
|
641
|
+
const info = document.createElement('div');
|
|
642
|
+
info.className = 'flex-1 text-sm text-gray-700';
|
|
643
|
+
info.textContent = `${meta.name || 'video'} (${formatFileSize(meta.size || 0)})`;
|
|
644
|
+
const actions = document.createElement('div');
|
|
645
|
+
actions.className = 'flex items-center gap-2';
|
|
646
|
+
const downloadBtn = document.createElement('button');
|
|
647
|
+
downloadBtn.type = 'button';
|
|
648
|
+
downloadBtn.className = 'px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded';
|
|
649
|
+
downloadBtn.textContent = 'Download';
|
|
650
|
+
downloadBtn.addEventListener('click', async () => {
|
|
651
|
+
if (config.downloadFile && typeof config.downloadFile === 'function') {
|
|
652
|
+
try { await config.downloadFile(rid, meta.name || 'video'); } catch(_) {}
|
|
653
|
+
} else {
|
|
654
|
+
console.log('Download simulated:', rid, meta.name || 'video');
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
const remove = document.createElement('button');
|
|
658
|
+
remove.type = 'button';
|
|
659
|
+
remove.className = 'px-2 py-1 text-xs bg-red-500 hover:bg-red-600 text-white rounded';
|
|
660
|
+
remove.textContent = 'Remove';
|
|
661
|
+
remove.addEventListener('click', () => {
|
|
662
|
+
const arr = parseJSONSafe(hid.value, []);
|
|
663
|
+
const next = Array.isArray(arr) ? arr.filter(x => x !== rid) : [];
|
|
664
|
+
hid.value = JSON.stringify(next);
|
|
665
|
+
renderVideos(next);
|
|
666
|
+
});
|
|
667
|
+
row.appendChild(video);
|
|
668
|
+
row.appendChild(info);
|
|
669
|
+
actions.appendChild(downloadBtn);
|
|
670
|
+
actions.appendChild(remove);
|
|
671
|
+
row.appendChild(actions);
|
|
672
|
+
list.appendChild(row);
|
|
673
|
+
});
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
picker.addEventListener('change', async () => {
|
|
677
|
+
let arr = parseJSONSafe(hid.value, []);
|
|
678
|
+
if (!Array.isArray(arr)) arr = [];
|
|
679
|
+
if (picker.files && picker.files.length) {
|
|
680
|
+
for (const file of picker.files) {
|
|
681
|
+
const err = fileValidationError(element, file);
|
|
682
|
+
if (err) {
|
|
683
|
+
markValidity(picker, err);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
// additionally ensure it's a video
|
|
687
|
+
if (!file.type.startsWith('video/')) {
|
|
688
|
+
markValidity(picker, 'mime not allowed: ' + file.type);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
for (const file of picker.files) {
|
|
693
|
+
const rid = await makeResourceIdFromFile(file);
|
|
694
|
+
resourceIndex.set(rid, { name: file.name, type: file.type, size: file.size });
|
|
695
|
+
arr.push(rid);
|
|
696
|
+
}
|
|
697
|
+
hid.value = JSON.stringify(arr);
|
|
698
|
+
renderVideos(arr);
|
|
699
|
+
markValidity(picker, null);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
const pv = ctx.prefill && ctx.prefill[element.key];
|
|
704
|
+
let initial = Array.isArray(pv) ? pv.filter(Boolean) : [];
|
|
705
|
+
if (initial.length) {
|
|
706
|
+
hid.value = JSON.stringify(initial);
|
|
707
|
+
renderVideos(initial);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
wrapper.appendChild(picker);
|
|
711
|
+
wrapper.appendChild(list);
|
|
712
|
+
wrapper.appendChild(hid);
|
|
713
|
+
wrapper.appendChild(makeFieldHint(element, 'Multiple videos return resource ID array'));
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
592
716
|
case 'group': {
|
|
593
717
|
wrapper.dataset.group = element.key;
|
|
594
718
|
wrapper.dataset.groupPath = pathKey;
|
|
@@ -846,6 +970,20 @@
|
|
|
846
970
|
if (hid?.previousElementSibling) markValidity(hid.previousElementSibling, null);
|
|
847
971
|
return Array.isArray(arr) ? arr : [];
|
|
848
972
|
}
|
|
973
|
+
case 'videos': {
|
|
974
|
+
const hid = scopeRoot.querySelector(`input[type="hidden"][name$="${key}"]`);
|
|
975
|
+
const arr = parseJSONSafe(hid?.value ?? '[]', []);
|
|
976
|
+
const count = Array.isArray(arr) ? arr.length : 0;
|
|
977
|
+
if (!skipValidation && !Array.isArray(arr)) errors.push(`${key}: internal value corrupted`);
|
|
978
|
+
if (!skipValidation && element.minCount != null && count < element.minCount) {
|
|
979
|
+
errors.push(`${key}: < minCount=${element.minCount}`);
|
|
980
|
+
}
|
|
981
|
+
if (!skipValidation && element.maxCount != null && count > element.maxCount) {
|
|
982
|
+
errors.push(`${key}: > maxCount=${element.maxCount}`);
|
|
983
|
+
}
|
|
984
|
+
if (hid?.previousElementSibling) markValidity(hid.previousElementSibling, null);
|
|
985
|
+
return Array.isArray(arr) ? arr : [];
|
|
986
|
+
}
|
|
849
987
|
case 'group': {
|
|
850
988
|
const groupWrapper = scopeRoot.querySelector(`[data-group="${key}"]`);
|
|
851
989
|
if (!groupWrapper) {
|
|
@@ -1008,6 +1146,9 @@
|
|
|
1008
1146
|
case 'files':
|
|
1009
1147
|
obj[el.key] = [];
|
|
1010
1148
|
break;
|
|
1149
|
+
case 'videos':
|
|
1150
|
+
obj[el.key] = [];
|
|
1151
|
+
break;
|
|
1011
1152
|
case 'group':
|
|
1012
1153
|
if (el.repeat && isPlainObject(el.repeat)) {
|
|
1013
1154
|
const sample = walk(el.elements);
|