@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.
@@ -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
- const rid = await makeResourceIdFromFile(file);
559
- resourceIndex.set(rid, { name: file.name, type: file.type, size: file.size });
560
- arr.push(rid);
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);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.6",
6
+ "version": "0.1.8",
7
7
  "description": "A reusable JSON schema form builder library",
8
8
  "main": "dist/form-builder.js",
9
9
  "files": [