vgapp 1.1.3 → 1.1.5

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +48 -48
  3. package/agents.md +7 -0
  4. package/app/langs/en/buttons.json +17 -17
  5. package/app/langs/en/messages.json +36 -36
  6. package/app/langs/ru/buttons.json +17 -17
  7. package/app/langs/ru/messages.json +36 -36
  8. package/app/modules/vgfilepreview/js/i18n.js +56 -56
  9. package/app/modules/vgfilepreview/js/renderers/image-modal.js +145 -145
  10. package/app/modules/vgfilepreview/js/renderers/image.js +92 -92
  11. package/app/modules/vgfilepreview/js/renderers/index.js +19 -19
  12. package/app/modules/vgfilepreview/js/renderers/office-modal.js +168 -168
  13. package/app/modules/vgfilepreview/js/renderers/office.js +79 -79
  14. package/app/modules/vgfilepreview/js/renderers/pdf-modal.js +260 -260
  15. package/app/modules/vgfilepreview/js/renderers/pdf.js +76 -76
  16. package/app/modules/vgfilepreview/js/renderers/playlist.js +71 -71
  17. package/app/modules/vgfilepreview/js/renderers/text-modal.js +343 -343
  18. package/app/modules/vgfilepreview/js/renderers/text.js +83 -83
  19. package/app/modules/vgfilepreview/js/renderers/video-modal.js +272 -272
  20. package/app/modules/vgfilepreview/js/renderers/video.js +80 -80
  21. package/app/modules/vgfilepreview/js/renderers/zip-modal.js +522 -522
  22. package/app/modules/vgfilepreview/js/renderers/zip.js +89 -89
  23. package/app/modules/vgfilepreview/js/vgfilepreview.js +965 -530
  24. package/app/modules/vgfilepreview/readme.md +68 -68
  25. package/app/modules/vgfilepreview/scss/_variables.scss +113 -113
  26. package/app/modules/vgfilepreview/scss/vgfilepreview.scss +464 -460
  27. package/app/modules/vgfiles/js/base.js +463 -463
  28. package/app/modules/vgfiles/js/droppable.js +260 -260
  29. package/app/modules/vgfiles/js/render.js +153 -153
  30. package/app/modules/vgfiles/js/vgfiles.js +41 -41
  31. package/app/modules/vgfiles/readme.md +123 -123
  32. package/app/modules/vgfiles/scss/_variables.scss +18 -18
  33. package/app/modules/vgfiles/scss/vgfiles.scss +148 -148
  34. package/app/modules/vgformsender/js/vgformsender.js +13 -13
  35. package/app/modules/vgmodal/js/vgmodal.drag.js +332 -0
  36. package/app/modules/vgmodal/js/vgmodal.js +116 -14
  37. package/app/modules/vgmodal/js/vgmodal.resize.js +435 -0
  38. package/app/modules/vgnav/js/vgnav.js +135 -135
  39. package/app/modules/vgnav/readme.md +67 -67
  40. package/app/modules/vgnestable/README.md +307 -307
  41. package/app/modules/vgnestable/scss/_variables.scss +60 -60
  42. package/app/modules/vgnestable/scss/vgnestable.scss +163 -163
  43. package/app/modules/vgselect/js/vgselect.js +39 -39
  44. package/app/modules/vgselect/scss/vgselect.scss +22 -22
  45. package/app/modules/vgspy/readme.md +28 -28
  46. package/app/utils/js/components/audio-metadata.js +240 -240
  47. package/app/utils/js/components/file-icon.js +109 -109
  48. package/app/utils/js/components/file-preview.js +304 -304
  49. package/app/utils/js/components/sanitize.js +150 -150
  50. package/app/utils/js/components/video-metadata.js +140 -0
  51. package/build/vgapp.css +1 -1
  52. package/build/vgapp.css.map +1 -1
  53. package/index.scss +9 -9
  54. package/package.json +1 -1
@@ -1,12 +1,12 @@
1
1
  import BaseModule from "../../base-module";
2
2
  import {mergeDeepObject} from "../../../utils/js/functions";
3
3
  import Html from "../../../utils/js/components/templater";
4
- import {lang_messages} from "../../../utils/js/components/lang";
5
- import {Classes, Manipulator} from "../../../utils/js/dom/manipulator";
6
- import Selectors from "../../../utils/js/dom/selectors";
7
- import {getSVG} from "../../module-fn";
8
- import VGFilePreview from "../../vgfilepreview";
9
- import {extractAudioMetadata} from "../../../utils/js/components/audio-metadata";
4
+ import {lang_messages} from "../../../utils/js/components/lang";
5
+ import {Classes, Manipulator} from "../../../utils/js/dom/manipulator";
6
+ import Selectors from "../../../utils/js/dom/selectors";
7
+ import {getSVG} from "../../module-fn";
8
+ import VGFilePreview from "../../vgfilepreview";
9
+ import {extractAudioMetadata} from "../../../utils/js/components/audio-metadata";
10
10
 
11
11
  class VGFilesBase extends BaseModule {
12
12
  constructor(element, params = {}, defaults = {}) {
@@ -21,11 +21,11 @@ class VGFilesBase extends BaseModule {
21
21
  }
22
22
  this._isInitialized = true;
23
23
 
24
- this._tpl = Html('dom');
25
- this._files = [];
26
- this._errors = new Set();
27
- this._fileObjectUrls = new Map();
28
- this._audioMetaPromises = new Map();
24
+ this._tpl = Html('dom');
25
+ this._files = [];
26
+ this._errors = new Set();
27
+ this._fileObjectUrls = new Map();
28
+ this._audioMetaPromises = new Map();
29
29
 
30
30
  this._nodes = {
31
31
  stat: Selectors.find(`.${this._getClass('stat')}`, this._element),
@@ -186,98 +186,98 @@ class VGFilesBase extends BaseModule {
186
186
  }
187
187
  }
188
188
 
189
- append(values, replace = true) {
190
- const incoming = this._applyRenameToIncomingFiles(Array.from(values));
191
- let filesToProcess;
192
-
193
- if (replace) {
194
- filesToProcess = incoming;
195
- } else {
196
- filesToProcess = this._mergeFilesByOrder(incoming, this._files, Boolean(this._params.prepend));
197
- }
198
-
199
- this._files = this._filterFiles(filesToProcess);
200
-
201
- this._renderErrors();
202
- this._enrichAudioMetadata(this._files);
203
-
204
- return this._files;
205
- }
206
-
207
- _mergeFilesByOrder(incoming = [], existing = [], prepend = false) {
208
- const ordered = prepend
209
- ? [...incoming, ...existing]
210
- : [...existing, ...incoming];
211
-
212
- const seen = new Set();
213
- const result = [];
214
-
215
- ordered.forEach((file) => {
216
- const key = this._getFileKey(file);
217
- if (seen.has(key)) {
218
- return;
219
- }
220
-
221
- seen.add(key);
222
- result.push(file);
223
- });
224
-
225
- return result;
226
- }
227
-
228
- _getFileKey(file) {
229
- return `${file.name}-${file.size}-${file.type}`;
230
- }
231
-
232
- _getFileCustomData(file) {
233
- const customData = file?.customData;
234
- if (!customData || typeof customData !== 'object' || Array.isArray(customData)) return {};
235
- return customData;
236
- }
237
-
238
- _toDataAttributeKey(key) {
239
- if (!key) return '';
240
-
241
- return String(key)
242
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
243
- .replace(/[_\s]+/g, '-')
244
- .replace(/[^a-zA-Z0-9-]/g, '')
245
- .replace(/-+/g, '-')
246
- .replace(/^-|-$/g, '')
247
- .toLowerCase();
248
- }
249
-
250
- _toDataAttributeValue(value) {
251
- if (value === undefined || value === null || value === '') return null;
252
- if (typeof value === 'object') {
253
- try {
254
- return JSON.stringify(value);
255
- } catch (e) {
256
- return String(value);
257
- }
258
- }
259
- return value;
260
- }
261
-
262
- _buildFileDataAttributes(file, baseAttrs = {}) {
263
- const attrs = { ...baseAttrs };
264
- const customData = this._getFileCustomData(file);
265
-
266
- Object.entries(customData).forEach(([key, value]) => {
267
- const attrKey = this._toDataAttributeKey(key);
268
- if (!attrKey) return;
269
-
270
- const attrName = `data-${attrKey}`;
271
- if (Object.prototype.hasOwnProperty.call(attrs, attrName)) return;
272
-
273
- const attrValue = this._toDataAttributeValue(value);
274
- if (attrValue === null) return;
275
-
276
- attrs[attrName] = attrValue;
277
- });
278
-
279
- return attrs;
280
- }
189
+ append(values, replace = true) {
190
+ const incoming = this._applyRenameToIncomingFiles(Array.from(values));
191
+ let filesToProcess;
192
+
193
+ if (replace) {
194
+ filesToProcess = incoming;
195
+ } else {
196
+ filesToProcess = this._mergeFilesByOrder(incoming, this._files, Boolean(this._params.prepend));
197
+ }
198
+
199
+ this._files = this._filterFiles(filesToProcess);
200
+
201
+ this._renderErrors();
202
+ this._enrichAudioMetadata(this._files);
203
+
204
+ return this._files;
205
+ }
206
+
207
+ _mergeFilesByOrder(incoming = [], existing = [], prepend = false) {
208
+ const ordered = prepend
209
+ ? [...incoming, ...existing]
210
+ : [...existing, ...incoming];
211
+
212
+ const seen = new Set();
213
+ const result = [];
214
+
215
+ ordered.forEach((file) => {
216
+ const key = this._getFileKey(file);
217
+ if (seen.has(key)) {
218
+ return;
219
+ }
220
+
221
+ seen.add(key);
222
+ result.push(file);
223
+ });
224
+
225
+ return result;
226
+ }
227
+
228
+ _getFileKey(file) {
229
+ return `${file.name}-${file.size}-${file.type}`;
230
+ }
231
+
232
+ _getFileCustomData(file) {
233
+ const customData = file?.customData;
234
+ if (!customData || typeof customData !== 'object' || Array.isArray(customData)) return {};
235
+ return customData;
236
+ }
237
+
238
+ _toDataAttributeKey(key) {
239
+ if (!key) return '';
240
+
241
+ return String(key)
242
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
243
+ .replace(/[_\s]+/g, '-')
244
+ .replace(/[^a-zA-Z0-9-]/g, '')
245
+ .replace(/-+/g, '-')
246
+ .replace(/^-|-$/g, '')
247
+ .toLowerCase();
248
+ }
249
+
250
+ _toDataAttributeValue(value) {
251
+ if (value === undefined || value === null || value === '') return null;
252
+ if (typeof value === 'object') {
253
+ try {
254
+ return JSON.stringify(value);
255
+ } catch (e) {
256
+ return String(value);
257
+ }
258
+ }
259
+ return value;
260
+ }
261
+
262
+ _buildFileDataAttributes(file, baseAttrs = {}) {
263
+ const attrs = { ...baseAttrs };
264
+ const customData = this._getFileCustomData(file);
265
+
266
+ Object.entries(customData).forEach(([key, value]) => {
267
+ const attrKey = this._toDataAttributeKey(key);
268
+ if (!attrKey) return;
269
+
270
+ const attrName = `data-${attrKey}`;
271
+ if (Object.prototype.hasOwnProperty.call(attrs, attrName)) return;
272
+
273
+ const attrValue = this._toDataAttributeValue(value);
274
+ if (attrValue === null) return;
275
+
276
+ attrs[attrName] = attrValue;
277
+ });
278
+
279
+ return attrs;
280
+ }
281
281
 
282
282
  _filterFiles(files) {
283
283
  this._errors.clear();
@@ -454,15 +454,15 @@ class VGFilesBase extends BaseModule {
454
454
  if (part) parts.push(part);
455
455
  });
456
456
 
457
- const $li = this._tpl.li(
458
- this._buildFileDataAttributes(file, {
459
- 'data-name': file.name,
460
- 'data-size': file.size ?? 0,
461
- 'data-id': file.id || '',
462
- class: 'file ' + classes.join(' ')
463
- }),
464
- parts
465
- );
457
+ const $li = this._tpl.li(
458
+ this._buildFileDataAttributes(file, {
459
+ 'data-name': file.name,
460
+ 'data-size': file.size ?? 0,
461
+ 'data-id': file.id || '',
462
+ class: 'file ' + classes.join(' ')
463
+ }),
464
+ parts
465
+ );
466
466
 
467
467
  fragment.appendChild($li);
468
468
  });
@@ -486,8 +486,8 @@ class VGFilesBase extends BaseModule {
486
486
  Classes.add(this._nodes.drop, 'active');
487
487
  }
488
488
 
489
- _renderInfoList(files) {
490
- if (!this._nodes.info) return;
489
+ _renderInfoList(files) {
490
+ if (!this._nodes.info) return;
491
491
 
492
492
  let $list = Selectors.find(`.${this._getClass('info-list')}`, this._element);
493
493
  if (!$list) {
@@ -495,149 +495,149 @@ class VGFilesBase extends BaseModule {
495
495
  this._nodes.info.appendChild($list);
496
496
  }
497
497
 
498
- if (!this._params.info) Classes.add($list, 'list-row');
499
-
500
- const $itemsTemplate = this._parseTemplate().children;
501
- const $itemsTemplateClasses = this._parseTemplate().liClasses.filter(cls => cls !== 'file');
502
- const currentChildren = Array.from($list.children || []);
503
- const existingByKey = new Map();
504
- currentChildren.forEach((child) => {
505
- const key = this._getInfoNodeKey(child);
506
- if (key) {
507
- if (!child.hasAttribute('data-file-key')) {
508
- child.setAttribute('data-file-key', key);
509
- }
510
- existingByKey.set(key, child);
511
- }
512
- });
513
-
514
- const nextNodes = [];
515
-
516
- files.forEach((file, i) => {
517
- const fileKey = this._getFileKey(file);
518
- const signature = this._getInfoItemSignature(file, i);
519
- const existingNode = existingByKey.get(fileKey);
520
-
521
- if (existingNode && existingNode.getAttribute('data-vg-info-signature') === signature) {
522
- const iterationNode = existingNode.querySelector('.iteration');
523
- if (iterationNode) {
524
- iterationNode.textContent = `${i + 1}.`;
525
- }
526
- nextNodes.push(existingNode);
527
- existingByKey.delete(fileKey);
528
- return;
529
- }
530
-
531
- if (existingNode) {
532
- existingNode.remove();
533
- }
534
-
535
- const nextNode = this._createInfoListItem(file, i, $itemsTemplate, $itemsTemplateClasses, fileKey, signature);
536
- nextNodes.push(nextNode);
537
- existingByKey.delete(fileKey);
538
- });
539
-
540
- if (existingByKey.size) {
541
- const removedNodes = Array.from(existingByKey.values());
542
- VGFilePreview.stopActiveInlineAudioIfDetached(removedNodes);
543
- removedNodes.forEach((node) => node.remove());
544
- }
545
-
546
- nextNodes.forEach((node) => {
547
- $list.appendChild(node);
548
- });
549
-
550
- this._initFilePreviewInInfo($list);
551
-
552
- Classes.add(this._nodes.info, 'show')
553
- }
554
-
555
- _getInfoItemSignature(file, index) {
556
- const displayName = this._resolveDisplayName(file);
557
- const previewPath = this._resolveFilePreviewPath(file);
558
- const id = file?.id || '';
559
- const name = file?.name || '';
560
- const size = file?.size ?? 0;
561
- const type = file?.type || '';
562
-
563
- return `${id}|${name}|${size}|${type}|${displayName}|${previewPath}`;
564
- }
565
-
566
- _getInfoNodeKey(node) {
567
- if (!node || typeof node.getAttribute !== 'function') {
568
- return '';
569
- }
570
-
571
- const fromAttr = String(node.getAttribute('data-file-key') || '').trim();
572
- if (fromAttr) {
573
- return fromAttr;
574
- }
575
-
576
- const name = String(node.getAttribute('data-name') || '').trim();
577
- const size = String(node.getAttribute('data-size') || '').trim();
578
- const type = String(node.getAttribute('data-type') || '').trim();
579
- if (name && size && type) {
580
- return `${name}-${size}-${type}`;
581
- }
582
-
583
- const raw = String(node.getAttribute('data-file') || '').trim();
584
- if (!raw) {
585
- return '';
586
- }
587
-
588
- try {
589
- const parsed = JSON.parse(raw);
590
- const pName = String(parsed?.name || '').trim();
591
- const pSize = String(parsed?.size ?? '').trim();
592
- const pType = String(parsed?.type || '').trim();
593
- if (pName && pSize && pType) {
594
- return `${pName}-${pSize}-${pType}`;
595
- }
596
- } catch {
597
- return '';
598
- }
599
-
600
- return '';
601
- }
602
-
603
- _createInfoListItem(file, i, itemsTemplate, itemTemplateClasses, fileKey, signature) {
604
- let classes = [...itemTemplateClasses];
605
-
606
- if (this._params.image) classes.push('with-image');
607
- if (this._params.info) classes.push('with-info');
608
- if (this._params.detach) classes.push('with-remove')
609
- if (this._params.sortable.enabled) classes.push('with-sortable');
610
- const previewPath = this._resolveFilePreviewPath(file);
611
- const displayName = this._resolveDisplayName(file);
612
-
613
- let parts = [];
614
- itemsTemplate.forEach(tmpl => {
615
- const part = this._renderTemplatePart(tmpl.element, file, i);
616
- if (part) parts.push(part);
617
- });
618
-
619
- const liAttrs = {
620
- 'data-name': file.name,
621
- 'data-size': file.size ?? 0,
622
- 'data-type': file.type || '',
623
- 'data-id': file.id || '',
624
- 'data-file-key': fileKey,
625
- 'data-vg-info-signature': signature,
626
- class: 'file ' + classes.join(' ') + ' '
627
- };
628
-
629
- if (previewPath) {
630
- liAttrs['data-vg-filepreview'] = previewPath;
631
- liAttrs['data-fields'] = 'name,size,download';
632
- liAttrs['data-original-name'] = file.name || '';
633
- liAttrs['data-vg-filepreview-display-name'] = displayName || file.name || '';
634
- }
635
-
636
- return this._tpl.li(
637
- this._buildFileDataAttributes(file, liAttrs),
638
- parts
639
- );
640
- }
498
+ if (!this._params.info) Classes.add($list, 'list-row');
499
+
500
+ const $itemsTemplate = this._parseTemplate().children;
501
+ const $itemsTemplateClasses = this._parseTemplate().liClasses.filter(cls => cls !== 'file');
502
+ const currentChildren = Array.from($list.children || []);
503
+ const existingByKey = new Map();
504
+ currentChildren.forEach((child) => {
505
+ const key = this._getInfoNodeKey(child);
506
+ if (key) {
507
+ if (!child.hasAttribute('data-file-key')) {
508
+ child.setAttribute('data-file-key', key);
509
+ }
510
+ existingByKey.set(key, child);
511
+ }
512
+ });
513
+
514
+ const nextNodes = [];
515
+
516
+ files.forEach((file, i) => {
517
+ const fileKey = this._getFileKey(file);
518
+ const signature = this._getInfoItemSignature(file, i);
519
+ const existingNode = existingByKey.get(fileKey);
520
+
521
+ if (existingNode && existingNode.getAttribute('data-vg-info-signature') === signature) {
522
+ const iterationNode = existingNode.querySelector('.iteration');
523
+ if (iterationNode) {
524
+ iterationNode.textContent = `${i + 1}.`;
525
+ }
526
+ nextNodes.push(existingNode);
527
+ existingByKey.delete(fileKey);
528
+ return;
529
+ }
530
+
531
+ if (existingNode) {
532
+ existingNode.remove();
533
+ }
534
+
535
+ const nextNode = this._createInfoListItem(file, i, $itemsTemplate, $itemsTemplateClasses, fileKey, signature);
536
+ nextNodes.push(nextNode);
537
+ existingByKey.delete(fileKey);
538
+ });
539
+
540
+ if (existingByKey.size) {
541
+ const removedNodes = Array.from(existingByKey.values());
542
+ VGFilePreview.stopActiveInlineAudioIfDetached(removedNodes);
543
+ removedNodes.forEach((node) => node.remove());
544
+ }
545
+
546
+ nextNodes.forEach((node) => {
547
+ $list.appendChild(node);
548
+ });
549
+
550
+ this._initFilePreviewInInfo($list);
551
+
552
+ Classes.add(this._nodes.info, 'show')
553
+ }
554
+
555
+ _getInfoItemSignature(file, index) {
556
+ const displayName = this._resolveDisplayName(file);
557
+ const previewPath = this._resolveFilePreviewPath(file);
558
+ const id = file?.id || '';
559
+ const name = file?.name || '';
560
+ const size = file?.size ?? 0;
561
+ const type = file?.type || '';
562
+
563
+ return `${id}|${name}|${size}|${type}|${displayName}|${previewPath}`;
564
+ }
565
+
566
+ _getInfoNodeKey(node) {
567
+ if (!node || typeof node.getAttribute !== 'function') {
568
+ return '';
569
+ }
570
+
571
+ const fromAttr = String(node.getAttribute('data-file-key') || '').trim();
572
+ if (fromAttr) {
573
+ return fromAttr;
574
+ }
575
+
576
+ const name = String(node.getAttribute('data-name') || '').trim();
577
+ const size = String(node.getAttribute('data-size') || '').trim();
578
+ const type = String(node.getAttribute('data-type') || '').trim();
579
+ if (name && size && type) {
580
+ return `${name}-${size}-${type}`;
581
+ }
582
+
583
+ const raw = String(node.getAttribute('data-file') || '').trim();
584
+ if (!raw) {
585
+ return '';
586
+ }
587
+
588
+ try {
589
+ const parsed = JSON.parse(raw);
590
+ const pName = String(parsed?.name || '').trim();
591
+ const pSize = String(parsed?.size ?? '').trim();
592
+ const pType = String(parsed?.type || '').trim();
593
+ if (pName && pSize && pType) {
594
+ return `${pName}-${pSize}-${pType}`;
595
+ }
596
+ } catch {
597
+ return '';
598
+ }
599
+
600
+ return '';
601
+ }
602
+
603
+ _createInfoListItem(file, i, itemsTemplate, itemTemplateClasses, fileKey, signature) {
604
+ let classes = [...itemTemplateClasses];
605
+
606
+ if (this._params.image) classes.push('with-image');
607
+ if (this._params.info) classes.push('with-info');
608
+ if (this._params.detach) classes.push('with-remove')
609
+ if (this._params.sortable.enabled) classes.push('with-sortable');
610
+ const previewPath = this._resolveFilePreviewPath(file);
611
+ const displayName = this._resolveDisplayName(file);
612
+
613
+ let parts = [];
614
+ itemsTemplate.forEach(tmpl => {
615
+ const part = this._renderTemplatePart(tmpl.element, file, i);
616
+ if (part) parts.push(part);
617
+ });
618
+
619
+ const liAttrs = {
620
+ 'data-name': file.name,
621
+ 'data-size': file.size ?? 0,
622
+ 'data-type': file.type || '',
623
+ 'data-id': file.id || '',
624
+ 'data-file-key': fileKey,
625
+ 'data-vg-info-signature': signature,
626
+ class: 'file ' + classes.join(' ') + ' '
627
+ };
628
+
629
+ if (previewPath) {
630
+ liAttrs['data-vg-filepreview'] = previewPath;
631
+ liAttrs['data-fields'] = 'name,size,download';
632
+ liAttrs['data-original-name'] = file.name || '';
633
+ liAttrs['data-vg-filepreview-display-name'] = displayName || file.name || '';
634
+ }
635
+
636
+ return this._tpl.li(
637
+ this._buildFileDataAttributes(file, liAttrs),
638
+ parts
639
+ );
640
+ }
641
641
 
642
642
  _renderTemplatePart(element, file, index = null, options = {}) {
643
643
  if (!element) return null;
@@ -691,205 +691,205 @@ class VGFilesBase extends BaseModule {
691
691
  }
692
692
  }
693
693
 
694
- _renderUIInfo(file, i) {
695
- if (this._params.info) {
696
- const displayName = this._resolveDisplayName(file);
697
- return this._tpl.div({ class: 'file-info' }, [
698
- this._tpl.span({ class: 'iteration' }, `${i + 1}.`),
699
- this._tpl.span({ class: 'name' }, displayName),
700
- this._tpl.span({ class: 'size' }, `[${this._getSizes(file.size)}]`),
701
- this._tpl.span({ class: 'download' }, '')
702
- ]);
703
- }
704
- }
705
-
706
- _renderUIImage(file) {
707
- const $container = this._tpl.div({ class: 'file-image' });
708
-
709
- const src = file?.src || file?.image;
710
- if (src) {
711
- $container.appendChild(this._tpl.img(src, file.name || '', { class: 'file-preview' }));
712
- return $container;
713
- }
714
-
715
- const customData = this._getFileCustomData(file);
716
- const audioCover = String(customData.audioCover || '').trim();
717
- if (audioCover) {
718
- $container.appendChild(this._tpl.img(audioCover, this._resolveDisplayName(file), { class: 'file-preview' }));
719
- return $container;
720
- }
721
-
722
- if (file?.type && file.type.startsWith('image/')) {
723
- const objectUrl = this._getFileObjectUrl(file);
724
- if (!objectUrl) {
725
- return $container;
726
- }
727
- $container.appendChild(this._tpl.img(objectUrl, file.name, { class: 'file-preview' }));
728
- return $container;
729
- }
730
-
731
- const icon = this._getIconByFileType(file);
732
- $container.appendChild(this._tpl.i({}, icon, { isHTML: true }));
733
- return $container;
734
- }
735
-
736
- _resolveDisplayName(file) {
737
- const customData = this._getFileCustomData(file);
738
- const metaTitle = String(customData.audioTitle || '').trim();
739
- if (metaTitle) {
740
- return metaTitle;
741
- }
742
-
743
- return String(file?.name || '').trim();
744
- }
745
-
746
- _getIconByFileType(file) {
747
- return getSVG(file);
748
- }
749
-
750
- _resolveFilePreviewPath(file) {
751
- const src = String(file?.src || file?.image || '').trim();
752
- if (src) {
753
- return src;
754
- }
755
-
756
- return this._getFileObjectUrl(file);
757
- }
758
-
759
- _getFileObjectUrl(file) {
760
- if (!file || typeof File === 'undefined' || !(file instanceof File)) {
761
- return '';
762
- }
763
-
764
- const key = this._getFileKey(file);
765
- if (this._fileObjectUrls.has(key)) {
766
- return this._fileObjectUrls.get(key);
767
- }
768
-
769
- const objectUrl = URL.createObjectURL(file);
770
- this._fileObjectUrls.set(key, objectUrl);
771
- return objectUrl;
772
- }
773
-
774
- _isAudioFile(file) {
775
- if (!file) {
776
- return false;
777
- }
778
-
779
- const fileType = String(file.type || '').toLowerCase();
780
- if (fileType.startsWith('audio/')) {
781
- return true;
782
- }
783
-
784
- const name = String(file.name || '').toLowerCase();
785
- return /\.(mp3|m4a|aac|wav|ogg|flac|opus|wma)$/.test(name);
786
- }
787
-
788
- _setFileCustomDataValue(file, key, value) {
789
- if (!file || !key) {
790
- return;
791
- }
792
-
793
- if (!file.customData || typeof file.customData !== 'object' || Array.isArray(file.customData)) {
794
- Object.defineProperty(file, 'customData', {
795
- value: {},
796
- writable: true,
797
- enumerable: true
798
- });
799
- }
800
-
801
- file.customData[key] = value;
802
- }
803
-
804
- _enrichAudioMetadata(files = []) {
805
- if (!Array.isArray(files) || !files.length) {
806
- return;
807
- }
808
-
809
- const pending = [];
810
-
811
- files.forEach((file) => {
812
- if (!this._isAudioFile(file)) {
813
- return;
814
- }
815
-
816
- const fileKey = this._getFileKey(file);
817
- if (this._audioMetaPromises.has(fileKey)) {
818
- return;
819
- }
820
-
821
- const customData = this._getFileCustomData(file);
822
- if (customData.audioTitle || customData.audioCover) {
823
- return;
824
- }
825
-
826
- const task = extractAudioMetadata(file)
827
- .then((meta) => {
828
- if (!meta) {
829
- return false;
830
- }
831
-
832
- let changed = false;
833
-
834
- if (meta.title) {
835
- this._setFileCustomDataValue(file, 'audioTitle', meta.title);
836
- changed = true;
837
- }
838
-
839
- if (meta.pictureBlob) {
840
- const coverKey = `cover:${fileKey}`;
841
- let coverUrl = this._fileObjectUrls.get(coverKey);
842
- if (!coverUrl) {
843
- coverUrl = URL.createObjectURL(meta.pictureBlob);
844
- this._fileObjectUrls.set(coverKey, coverUrl);
845
- }
846
- this._setFileCustomDataValue(file, 'audioCover', coverUrl);
847
- changed = true;
848
- }
849
-
850
- return changed;
851
- })
852
- .catch(() => false)
853
- .then((result) => {
854
- this._audioMetaPromises.delete(fileKey);
855
- return result;
856
- });
857
-
858
- this._audioMetaPromises.set(fileKey, task);
859
- pending.push(task);
860
- });
861
-
862
- if (!pending.length) {
863
- return;
864
- }
865
-
866
- Promise.allSettled(pending).then((results) => {
867
- const hasChanges = results.some((item) => item.status === 'fulfilled' && item.value === true);
868
- if (!hasChanges || !this._files.length) {
869
- return;
870
- }
871
-
872
- this._renderUI(this._files);
873
- });
874
- }
875
-
876
- _initFilePreviewInInfo(root) {
877
- if (!this._nodes.info || !root) {
878
- return;
879
- }
880
-
881
- const lang = this._params?.lang || document.documentElement.lang || 'ru';
882
- const nodes = Selectors.findAll('[data-vg-filepreview]', root) || [];
883
-
884
- nodes.forEach((node) => {
885
- VGFilePreview.getOrCreateInstance(node, {
886
- lang,
887
- ui: {
888
- nameOnly: true
889
- }
890
- });
891
- });
892
- }
694
+ _renderUIInfo(file, i) {
695
+ if (this._params.info) {
696
+ const displayName = this._resolveDisplayName(file);
697
+ return this._tpl.div({ class: 'file-info' }, [
698
+ this._tpl.span({ class: 'iteration' }, `${i + 1}.`),
699
+ this._tpl.span({ class: 'name' }, displayName),
700
+ this._tpl.span({ class: 'size' }, `[${this._getSizes(file.size)}]`),
701
+ this._tpl.span({ class: 'download' }, '')
702
+ ]);
703
+ }
704
+ }
705
+
706
+ _renderUIImage(file) {
707
+ const $container = this._tpl.div({ class: 'file-image' });
708
+
709
+ const src = file?.src || file?.image;
710
+ if (src) {
711
+ $container.appendChild(this._tpl.img(src, file.name || '', { class: 'file-preview' }));
712
+ return $container;
713
+ }
714
+
715
+ const customData = this._getFileCustomData(file);
716
+ const audioCover = String(customData.audioCover || '').trim();
717
+ if (audioCover) {
718
+ $container.appendChild(this._tpl.img(audioCover, this._resolveDisplayName(file), { class: 'file-preview' }));
719
+ return $container;
720
+ }
721
+
722
+ if (file?.type && file.type.startsWith('image/')) {
723
+ const objectUrl = this._getFileObjectUrl(file);
724
+ if (!objectUrl) {
725
+ return $container;
726
+ }
727
+ $container.appendChild(this._tpl.img(objectUrl, file.name, { class: 'file-preview' }));
728
+ return $container;
729
+ }
730
+
731
+ const icon = this._getIconByFileType(file);
732
+ $container.appendChild(this._tpl.i({}, icon, { isHTML: true }));
733
+ return $container;
734
+ }
735
+
736
+ _resolveDisplayName(file) {
737
+ const customData = this._getFileCustomData(file);
738
+ const metaTitle = String(customData.audioTitle || '').trim();
739
+ if (metaTitle) {
740
+ return metaTitle;
741
+ }
742
+
743
+ return String(file?.name || '').trim();
744
+ }
745
+
746
+ _getIconByFileType(file) {
747
+ return getSVG(file);
748
+ }
749
+
750
+ _resolveFilePreviewPath(file) {
751
+ const src = String(file?.src || file?.image || '').trim();
752
+ if (src) {
753
+ return src;
754
+ }
755
+
756
+ return this._getFileObjectUrl(file);
757
+ }
758
+
759
+ _getFileObjectUrl(file) {
760
+ if (!file || typeof File === 'undefined' || !(file instanceof File)) {
761
+ return '';
762
+ }
763
+
764
+ const key = this._getFileKey(file);
765
+ if (this._fileObjectUrls.has(key)) {
766
+ return this._fileObjectUrls.get(key);
767
+ }
768
+
769
+ const objectUrl = URL.createObjectURL(file);
770
+ this._fileObjectUrls.set(key, objectUrl);
771
+ return objectUrl;
772
+ }
773
+
774
+ _isAudioFile(file) {
775
+ if (!file) {
776
+ return false;
777
+ }
778
+
779
+ const fileType = String(file.type || '').toLowerCase();
780
+ if (fileType.startsWith('audio/')) {
781
+ return true;
782
+ }
783
+
784
+ const name = String(file.name || '').toLowerCase();
785
+ return /\.(mp3|m4a|aac|wav|ogg|flac|opus|wma)$/.test(name);
786
+ }
787
+
788
+ _setFileCustomDataValue(file, key, value) {
789
+ if (!file || !key) {
790
+ return;
791
+ }
792
+
793
+ if (!file.customData || typeof file.customData !== 'object' || Array.isArray(file.customData)) {
794
+ Object.defineProperty(file, 'customData', {
795
+ value: {},
796
+ writable: true,
797
+ enumerable: true
798
+ });
799
+ }
800
+
801
+ file.customData[key] = value;
802
+ }
803
+
804
+ _enrichAudioMetadata(files = []) {
805
+ if (!Array.isArray(files) || !files.length) {
806
+ return;
807
+ }
808
+
809
+ const pending = [];
810
+
811
+ files.forEach((file) => {
812
+ if (!this._isAudioFile(file)) {
813
+ return;
814
+ }
815
+
816
+ const fileKey = this._getFileKey(file);
817
+ if (this._audioMetaPromises.has(fileKey)) {
818
+ return;
819
+ }
820
+
821
+ const customData = this._getFileCustomData(file);
822
+ if (customData.audioTitle || customData.audioCover) {
823
+ return;
824
+ }
825
+
826
+ const task = extractAudioMetadata(file)
827
+ .then((meta) => {
828
+ if (!meta) {
829
+ return false;
830
+ }
831
+
832
+ let changed = false;
833
+
834
+ if (meta.title) {
835
+ this._setFileCustomDataValue(file, 'audioTitle', meta.title);
836
+ changed = true;
837
+ }
838
+
839
+ if (meta.pictureBlob) {
840
+ const coverKey = `cover:${fileKey}`;
841
+ let coverUrl = this._fileObjectUrls.get(coverKey);
842
+ if (!coverUrl) {
843
+ coverUrl = URL.createObjectURL(meta.pictureBlob);
844
+ this._fileObjectUrls.set(coverKey, coverUrl);
845
+ }
846
+ this._setFileCustomDataValue(file, 'audioCover', coverUrl);
847
+ changed = true;
848
+ }
849
+
850
+ return changed;
851
+ })
852
+ .catch(() => false)
853
+ .then((result) => {
854
+ this._audioMetaPromises.delete(fileKey);
855
+ return result;
856
+ });
857
+
858
+ this._audioMetaPromises.set(fileKey, task);
859
+ pending.push(task);
860
+ });
861
+
862
+ if (!pending.length) {
863
+ return;
864
+ }
865
+
866
+ Promise.allSettled(pending).then((results) => {
867
+ const hasChanges = results.some((item) => item.status === 'fulfilled' && item.value === true);
868
+ if (!hasChanges || !this._files.length) {
869
+ return;
870
+ }
871
+
872
+ this._renderUI(this._files);
873
+ });
874
+ }
875
+
876
+ _initFilePreviewInInfo(root) {
877
+ if (!this._nodes.info || !root) {
878
+ return;
879
+ }
880
+
881
+ const lang = this._params?.lang || document.documentElement.lang || 'ru';
882
+ const nodes = Selectors.findAll('[data-vg-filepreview]', root) || [];
883
+
884
+ nodes.forEach((node) => {
885
+ VGFilePreview.getOrCreateInstance(node, {
886
+ lang,
887
+ ui: {
888
+ nameOnly: true
889
+ }
890
+ });
891
+ });
892
+ }
893
893
 
894
894
  _updateStat() {
895
895
  if (!this._nodes.stat) return;
@@ -969,13 +969,13 @@ class VGFilesBase extends BaseModule {
969
969
  }
970
970
  }
971
971
 
972
- _revokeUrls() {
973
- this._fileObjectUrls.forEach((url) => {
974
- URL.revokeObjectURL(url);
975
- });
976
- this._fileObjectUrls.clear();
977
- this._audioMetaPromises.clear();
978
- }
972
+ _revokeUrls() {
973
+ this._fileObjectUrls.forEach((url) => {
974
+ URL.revokeObjectURL(url);
975
+ });
976
+ this._fileObjectUrls.clear();
977
+ this._audioMetaPromises.clear();
978
+ }
979
979
 
980
980
  _resetFileInput() {
981
981
  Selectors.findAll('[data-vg-toggle="files"]', this._element).forEach(input => input.value = '');