vgapp 1.1.4 → 1.1.6

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.
@@ -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,150 @@ 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
+ VGFilePreview.stopActiveInlineAudioIfDetached([existingNode]);
533
+ existingNode.remove();
534
+ }
535
+
536
+ const nextNode = this._createInfoListItem(file, i, $itemsTemplate, $itemsTemplateClasses, fileKey, signature);
537
+ nextNodes.push(nextNode);
538
+ existingByKey.delete(fileKey);
539
+ });
540
+
541
+ if (existingByKey.size) {
542
+ const removedNodes = Array.from(existingByKey.values());
543
+ VGFilePreview.stopActiveInlineAudioIfDetached(removedNodes);
544
+ removedNodes.forEach((node) => node.remove());
545
+ }
546
+
547
+ nextNodes.forEach((node) => {
548
+ $list.appendChild(node);
549
+ });
550
+
551
+ this._initFilePreviewInInfo($list);
552
+
553
+ Classes.add(this._nodes.info, 'show')
554
+ }
555
+
556
+ _getInfoItemSignature(file, index) {
557
+ const displayName = this._resolveDisplayName(file);
558
+ const previewPath = this._resolveFilePreviewPath(file);
559
+ const id = file?.id || '';
560
+ const name = file?.name || '';
561
+ const size = file?.size ?? 0;
562
+ const type = file?.type || '';
563
+
564
+ return `${id}|${name}|${size}|${type}|${displayName}|${previewPath}`;
565
+ }
566
+
567
+ _getInfoNodeKey(node) {
568
+ if (!node || typeof node.getAttribute !== 'function') {
569
+ return '';
570
+ }
571
+
572
+ const fromAttr = String(node.getAttribute('data-file-key') || '').trim();
573
+ if (fromAttr) {
574
+ return fromAttr;
575
+ }
576
+
577
+ const name = String(node.getAttribute('data-name') || '').trim();
578
+ const size = String(node.getAttribute('data-size') || '').trim();
579
+ const type = String(node.getAttribute('data-type') || '').trim();
580
+ if (name && size && type) {
581
+ return `${name}-${size}-${type}`;
582
+ }
583
+
584
+ const raw = String(node.getAttribute('data-file') || '').trim();
585
+ if (!raw) {
586
+ return '';
587
+ }
588
+
589
+ try {
590
+ const parsed = JSON.parse(raw);
591
+ const pName = String(parsed?.name || '').trim();
592
+ const pSize = String(parsed?.size ?? '').trim();
593
+ const pType = String(parsed?.type || '').trim();
594
+ if (pName && pSize && pType) {
595
+ return `${pName}-${pSize}-${pType}`;
596
+ }
597
+ } catch {
598
+ return '';
599
+ }
600
+
601
+ return '';
602
+ }
603
+
604
+ _createInfoListItem(file, i, itemsTemplate, itemTemplateClasses, fileKey, signature) {
605
+ let classes = [...itemTemplateClasses];
606
+
607
+ if (this._params.image) classes.push('with-image');
608
+ if (this._params.info) classes.push('with-info');
609
+ if (this._params.detach) classes.push('with-remove')
610
+ if (this._params.sortable.enabled) classes.push('with-sortable');
611
+ const previewPath = this._resolveFilePreviewPath(file);
612
+ const displayName = this._resolveDisplayName(file);
613
+
614
+ let parts = [];
615
+ itemsTemplate.forEach(tmpl => {
616
+ const part = this._renderTemplatePart(tmpl.element, file, i);
617
+ if (part) parts.push(part);
618
+ });
619
+
620
+ const liAttrs = {
621
+ 'data-name': file.name,
622
+ 'data-size': file.size ?? 0,
623
+ 'data-type': file.type || '',
624
+ 'data-id': file.id || '',
625
+ 'data-file-key': fileKey,
626
+ 'data-vg-info-signature': signature,
627
+ class: 'file ' + classes.join(' ') + ' '
628
+ };
629
+
630
+ if (previewPath) {
631
+ liAttrs['data-vg-filepreview'] = previewPath;
632
+ liAttrs['data-fields'] = 'name,size,download';
633
+ liAttrs['data-original-name'] = file.name || '';
634
+ liAttrs['data-vg-filepreview-display-name'] = displayName || file.name || '';
635
+ }
636
+
637
+ return this._tpl.li(
638
+ this._buildFileDataAttributes(file, liAttrs),
639
+ parts
640
+ );
641
+ }
641
642
 
642
643
  _renderTemplatePart(element, file, index = null, options = {}) {
643
644
  if (!element) return null;
@@ -691,205 +692,205 @@ class VGFilesBase extends BaseModule {
691
692
  }
692
693
  }
693
694
 
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
- }
695
+ _renderUIInfo(file, i) {
696
+ if (this._params.info) {
697
+ const displayName = this._resolveDisplayName(file);
698
+ return this._tpl.div({ class: 'file-info' }, [
699
+ this._tpl.span({ class: 'iteration' }, `${i + 1}.`),
700
+ this._tpl.span({ class: 'name' }, displayName),
701
+ this._tpl.span({ class: 'size' }, `[${this._getSizes(file.size)}]`),
702
+ this._tpl.span({ class: 'download' }, '')
703
+ ]);
704
+ }
705
+ }
706
+
707
+ _renderUIImage(file) {
708
+ const $container = this._tpl.div({ class: 'file-image' });
709
+
710
+ const src = file?.src || file?.image;
711
+ if (src) {
712
+ $container.appendChild(this._tpl.img(src, file.name || '', { class: 'file-preview' }));
713
+ return $container;
714
+ }
715
+
716
+ const customData = this._getFileCustomData(file);
717
+ const audioCover = String(customData.audioCover || '').trim();
718
+ if (audioCover) {
719
+ $container.appendChild(this._tpl.img(audioCover, this._resolveDisplayName(file), { class: 'file-preview' }));
720
+ return $container;
721
+ }
722
+
723
+ if (file?.type && file.type.startsWith('image/')) {
724
+ const objectUrl = this._getFileObjectUrl(file);
725
+ if (!objectUrl) {
726
+ return $container;
727
+ }
728
+ $container.appendChild(this._tpl.img(objectUrl, file.name, { class: 'file-preview' }));
729
+ return $container;
730
+ }
731
+
732
+ const icon = this._getIconByFileType(file);
733
+ $container.appendChild(this._tpl.i({}, icon, { isHTML: true }));
734
+ return $container;
735
+ }
736
+
737
+ _resolveDisplayName(file) {
738
+ const customData = this._getFileCustomData(file);
739
+ const metaTitle = String(customData.audioTitle || '').trim();
740
+ if (metaTitle) {
741
+ return metaTitle;
742
+ }
743
+
744
+ return String(file?.name || '').trim();
745
+ }
746
+
747
+ _getIconByFileType(file) {
748
+ return getSVG(file);
749
+ }
750
+
751
+ _resolveFilePreviewPath(file) {
752
+ const src = String(file?.src || file?.image || '').trim();
753
+ if (src) {
754
+ return src;
755
+ }
756
+
757
+ return this._getFileObjectUrl(file);
758
+ }
759
+
760
+ _getFileObjectUrl(file) {
761
+ if (!file || typeof File === 'undefined' || !(file instanceof File)) {
762
+ return '';
763
+ }
764
+
765
+ const key = this._getFileKey(file);
766
+ if (this._fileObjectUrls.has(key)) {
767
+ return this._fileObjectUrls.get(key);
768
+ }
769
+
770
+ const objectUrl = URL.createObjectURL(file);
771
+ this._fileObjectUrls.set(key, objectUrl);
772
+ return objectUrl;
773
+ }
774
+
775
+ _isAudioFile(file) {
776
+ if (!file) {
777
+ return false;
778
+ }
779
+
780
+ const fileType = String(file.type || '').toLowerCase();
781
+ if (fileType.startsWith('audio/')) {
782
+ return true;
783
+ }
784
+
785
+ const name = String(file.name || '').toLowerCase();
786
+ return /\.(mp3|m4a|aac|wav|ogg|flac|opus|wma)$/.test(name);
787
+ }
788
+
789
+ _setFileCustomDataValue(file, key, value) {
790
+ if (!file || !key) {
791
+ return;
792
+ }
793
+
794
+ if (!file.customData || typeof file.customData !== 'object' || Array.isArray(file.customData)) {
795
+ Object.defineProperty(file, 'customData', {
796
+ value: {},
797
+ writable: true,
798
+ enumerable: true
799
+ });
800
+ }
801
+
802
+ file.customData[key] = value;
803
+ }
804
+
805
+ _enrichAudioMetadata(files = []) {
806
+ if (!Array.isArray(files) || !files.length) {
807
+ return;
808
+ }
809
+
810
+ const pending = [];
811
+
812
+ files.forEach((file) => {
813
+ if (!this._isAudioFile(file)) {
814
+ return;
815
+ }
816
+
817
+ const fileKey = this._getFileKey(file);
818
+ if (this._audioMetaPromises.has(fileKey)) {
819
+ return;
820
+ }
821
+
822
+ const customData = this._getFileCustomData(file);
823
+ if (customData.audioTitle || customData.audioCover) {
824
+ return;
825
+ }
826
+
827
+ const task = extractAudioMetadata(file)
828
+ .then((meta) => {
829
+ if (!meta) {
830
+ return false;
831
+ }
832
+
833
+ let changed = false;
834
+
835
+ if (meta.title) {
836
+ this._setFileCustomDataValue(file, 'audioTitle', meta.title);
837
+ changed = true;
838
+ }
839
+
840
+ if (meta.pictureBlob) {
841
+ const coverKey = `cover:${fileKey}`;
842
+ let coverUrl = this._fileObjectUrls.get(coverKey);
843
+ if (!coverUrl) {
844
+ coverUrl = URL.createObjectURL(meta.pictureBlob);
845
+ this._fileObjectUrls.set(coverKey, coverUrl);
846
+ }
847
+ this._setFileCustomDataValue(file, 'audioCover', coverUrl);
848
+ changed = true;
849
+ }
850
+
851
+ return changed;
852
+ })
853
+ .catch(() => false)
854
+ .then((result) => {
855
+ this._audioMetaPromises.delete(fileKey);
856
+ return result;
857
+ });
858
+
859
+ this._audioMetaPromises.set(fileKey, task);
860
+ pending.push(task);
861
+ });
862
+
863
+ if (!pending.length) {
864
+ return;
865
+ }
866
+
867
+ Promise.allSettled(pending).then((results) => {
868
+ const hasChanges = results.some((item) => item.status === 'fulfilled' && item.value === true);
869
+ if (!hasChanges || !this._files.length) {
870
+ return;
871
+ }
872
+
873
+ this._renderUI(this._files);
874
+ });
875
+ }
876
+
877
+ _initFilePreviewInInfo(root) {
878
+ if (!this._nodes.info || !root) {
879
+ return;
880
+ }
881
+
882
+ const lang = this._params?.lang || document.documentElement.lang || 'ru';
883
+ const nodes = Selectors.findAll('[data-vg-filepreview]', root) || [];
884
+
885
+ nodes.forEach((node) => {
886
+ VGFilePreview.getOrCreateInstance(node, {
887
+ lang,
888
+ ui: {
889
+ nameOnly: true
890
+ }
891
+ });
892
+ });
893
+ }
893
894
 
894
895
  _updateStat() {
895
896
  if (!this._nodes.stat) return;
@@ -935,6 +936,23 @@ class VGFilesBase extends BaseModule {
935
936
  }
936
937
 
937
938
  clear(resetInput = true) {
939
+ const detachedNodes = [];
940
+ if (this._nodes.info) {
941
+ const $infoList = Selectors.find(`.${this._getClass('info-list')}`, this._element);
942
+ if ($infoList?.children?.length) {
943
+ detachedNodes.push(...Array.from($infoList.children));
944
+ }
945
+ }
946
+ if (this._nodes.drop) {
947
+ const $dropList = Selectors.find(`.${this._getClass('drop-list')}`, this._element);
948
+ if ($dropList?.children?.length) {
949
+ detachedNodes.push(...Array.from($dropList.children));
950
+ }
951
+ }
952
+ if (detachedNodes.length) {
953
+ VGFilePreview.stopActiveInlineAudioIfDetached(detachedNodes);
954
+ }
955
+
938
956
  this._revokeUrls();
939
957
  if (resetInput) {
940
958
  this._resetFileInput();
@@ -969,13 +987,13 @@ class VGFilesBase extends BaseModule {
969
987
  }
970
988
  }
971
989
 
972
- _revokeUrls() {
973
- this._fileObjectUrls.forEach((url) => {
974
- URL.revokeObjectURL(url);
975
- });
976
- this._fileObjectUrls.clear();
977
- this._audioMetaPromises.clear();
978
- }
990
+ _revokeUrls() {
991
+ this._fileObjectUrls.forEach((url) => {
992
+ URL.revokeObjectURL(url);
993
+ });
994
+ this._fileObjectUrls.clear();
995
+ this._audioMetaPromises.clear();
996
+ }
979
997
 
980
998
  _resetFileInput() {
981
999
  Selectors.findAll('[data-vg-toggle="files"]', this._element).forEach(input => input.value = '');