@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.
@@ -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);
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.7",
7
7
  "description": "A reusable JSON schema form builder library",
8
8
  "main": "dist/form-builder.js",
9
9
  "files": [