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,594 +1,972 @@
1
- import BaseModule from "../../base-module";
2
- import {mergeDeepObject} from "../../../utils/js/functions";
3
- import FilePreviewHelper from "../../../utils/js/components/file-preview";
4
- import {getSVG} from "../../module-fn";
5
- import createFilePreviewRenderers from "./renderers";
6
- import {createFilePreviewI18n, resolveFilePreviewLang} from "./i18n";
7
-
8
- const NAME = 'filepreview';
9
- const NAME_KEY = 'vg.filepreview';
10
-
11
- class VGFilePreview extends BaseModule {
12
- constructor(el, params = {}) {
13
- super(el, params);
14
-
15
- this._params = this._getParams(el, mergeDeepObject({
16
- validate: true,
17
- lang: 'ru',
18
- ui: {
19
- nameOnly: false,
20
- preview: false
21
- }
22
- }, params));
23
-
24
- this._filePath = '';
25
- this._fileUrl = null;
26
- this._isValid = false;
27
- this._fileMeta = {};
28
- this._fields = [];
29
- this._editableFields = {};
30
- this._helper = new FilePreviewHelper(this._element);
31
- this._renderers = createFilePreviewRenderers();
32
- this._lang = resolveFilePreviewLang(this._params.lang, this._element);
33
- this._i18n = createFilePreviewI18n(this._lang);
34
- this._inlineAudio = null;
35
- this._inlineAudioSrc = '';
36
- this._inlineAudioButton = null;
37
- this._inlineAudioIcon = null;
38
- this._inlineAudioContainer = null;
39
-
40
- this.init();
41
- }
42
-
43
- static get NAME() {
44
- return NAME;
45
- }
46
-
47
- static get NAME_KEY() {
48
- return NAME_KEY;
49
- }
50
-
51
- get filePath() {
52
- return this._filePath;
53
- }
54
-
55
- get fileUrl() {
56
- return this._fileUrl;
57
- }
58
-
59
- get isValid() {
60
- return this._isValid;
61
- }
62
-
63
- get fileMeta() {
64
- return this._fileMeta;
65
- }
66
-
67
- get fields() {
68
- return this._fields;
69
- }
70
-
71
- get editableFields() {
72
- return this._editableFields;
73
- }
74
-
75
- init() {
76
- const filePath = this._helper.getFilePath();
77
- this._filePath = filePath;
78
-
79
- if (this._params.validate) {
80
- const validation = this._helper.validateFilePath(filePath);
81
- this._isValid = validation.isValid;
82
- this._fileUrl = validation.fileUrl;
83
- } else {
84
- this._isValid = true;
85
- this._fileUrl = null;
86
- }
87
-
88
- this._helper.syncState(this._isValid);
89
-
90
- if (!this._isValid) {
91
- return false;
92
- }
93
-
94
- this._fileMeta = this._helper.getFileMeta(this._filePath);
95
- return this.preview();
96
- }
97
-
98
- preview() {
99
- this._setState('loading');
100
- this._fields = this._helper.getFields();
101
- this._editableFields = this._helper.resolveEditableFields(this._fields);
102
- this._helper.syncEditableFields(this._editableFields);
103
- this._renderIcon();
104
- this._renderTextFields();
105
- this._renderDownloadField();
106
-
107
- if (this._shouldRenderPreview()) {
108
- this._renderPreview();
109
- return this._editableFields;
110
- }
111
-
112
- const previewField = this._editableFields.preview;
113
- if (previewField) {
114
- previewField.innerHTML = '';
115
- }
116
-
117
- this._element.removeAttribute('data-vg-filepreview-renderer');
118
- this._setState('ready');
119
-
120
- return this._editableFields;
121
- }
122
-
123
- _renderIcon() {
124
- const iconField = this._editableFields.icon;
125
- if (!iconField) {
126
- return;
127
- }
128
-
129
- const imageSrc = this._getImageIconSrc();
130
- if (imageSrc) {
131
- iconField.innerHTML = '';
132
- const image = document.createElement('img');
133
- image.src = imageSrc;
134
- image.alt = this._fileMeta?.originalName || this._fileMeta?.name || '';
135
- image.className = 'vg-filepreview-icon-image';
136
- image.loading = 'lazy';
137
- image.addEventListener('error', () => {
138
- this._renderDefaultIcon(iconField);
139
- });
140
- iconField.appendChild(image);
141
- return;
142
- }
143
-
144
- this._renderDefaultIcon(iconField);
145
- }
146
-
147
- _renderTextFields() {
148
- const nameField = this._editableFields.name;
149
- if (nameField) {
150
- if (this._isAudioFile()) {
151
- this._renderAudioNameField(nameField);
152
- this._element.setAttribute('data-vg-filepreview-renderer', 'audio');
153
- } else if (this._fileMeta.name) {
154
- nameField.classList.remove('vg-filepreview-audio-inline');
155
- nameField.textContent = this._fileMeta.name;
156
- this._applyNameClampStyles(nameField);
157
- }
158
- }
159
-
160
- const extField = this._editableFields.ext;
161
- if (extField && this._fileMeta.ext) {
162
- extField.textContent = this._fileMeta.ext;
163
- }
164
-
165
- const sizeField = this._editableFields.size;
166
- if (sizeField && this._fileMeta.sizeText) {
167
- sizeField.textContent = this._fileMeta.sizeText;
168
- }
169
-
170
- const originalNameField = this._editableFields.original_name;
171
- if (!originalNameField) {
172
- return;
173
- }
174
-
175
- if (this._fileMeta.originalName) {
176
- originalNameField.textContent = this._fileMeta.originalName;
177
- this._applyNameClampStyles(originalNameField);
178
- return;
179
- }
180
-
181
- if (this._fileMeta.isMedia) {
182
- originalNameField.textContent = '';
183
- }
184
- }
185
-
186
- _renderAudioNameField(nameField) {
187
- const displayName = String(
188
- this._element?.getAttribute('data-vg-filepreview-display-name')
189
- || this._fileMeta?.originalName
190
- || this._fileMeta?.name
191
- || ''
192
- ).trim();
193
-
194
- const fileName = displayName;
195
- if (!fileName) {
196
- return;
197
- }
198
-
199
- nameField.innerHTML = '';
200
- nameField.classList.add('vg-filepreview-audio-inline');
201
-
202
- const button = document.createElement('button');
203
- button.type = 'button';
204
- button.className = 'vg-filepreview-audio-inline__toggle';
205
- button.setAttribute('aria-label', 'Play/Pause');
206
-
207
- const icon = document.createElement('span');
208
- icon.className = 'vg-filepreview-audio-inline__icon';
209
- button.appendChild(icon);
210
-
211
- const text = document.createElement('span');
212
- text.className = 'vg-filepreview-audio-inline__name';
213
- text.textContent = fileName;
214
- this._applyNameClampStyles(text);
215
-
216
- button.addEventListener('click', (event) => {
217
- event.preventDefault();
218
- event.stopPropagation();
219
- this._toggleInlineAudio();
220
- });
221
-
222
- nameField.appendChild(button);
223
- nameField.appendChild(text);
224
-
225
- this._inlineAudioButton = button;
226
- this._inlineAudioIcon = icon;
227
- const rootFile = this._element?.classList?.contains('file')
228
- ? this._element
229
- : this._element?.closest?.('.file');
230
- this._inlineAudioContainer = rootFile || this._element || nameField;
231
- this._setInlineAudioProgress(0);
232
- const isCurrentAudioPlaying = VGFilePreview._activeAudioOwner === this && this._inlineAudio && !this._inlineAudio.paused;
233
- this._syncInlineAudioIcon(isCurrentAudioPlaying);
234
- }
235
-
236
- _toggleInlineAudio() {
237
- const src = this._fileUrl?.href || this._filePath || '';
238
- if (!src || !this._inlineAudioButton) {
239
- return;
240
- }
241
-
242
- if (VGFilePreview._activeAudioOwner && VGFilePreview._activeAudioOwner !== this) {
243
- VGFilePreview._activeAudioOwner._stopInlineAudio();
244
- }
245
-
246
- if (!this._inlineAudio || this._inlineAudioSrc !== src) {
247
- this._stopInlineAudio();
248
- this._inlineAudio = new Audio(src);
249
- this._inlineAudioSrc = src;
250
- this._inlineAudio.addEventListener('ended', () => {
251
- this._syncInlineAudioIcon(false);
252
- this._setInlineAudioProgress(0);
253
- });
254
- this._inlineAudio.addEventListener('timeupdate', () => this._syncInlineAudioProgress());
255
- this._inlineAudio.addEventListener('loadedmetadata', () => this._syncInlineAudioProgress());
256
- }
257
-
258
- if (this._inlineAudio.paused) {
259
- this._inlineAudio.play().then(() => {
260
- VGFilePreview._activeAudioOwner = this;
261
- this._syncInlineAudioIcon(true);
262
- }).catch(() => {
263
- this._syncInlineAudioIcon(false);
264
- });
265
- return;
266
- }
267
-
268
- this._inlineAudio.pause();
269
- this._syncInlineAudioIcon(false);
270
- if (VGFilePreview._activeAudioOwner === this) {
271
- VGFilePreview._activeAudioOwner = null;
272
- }
273
- }
274
-
275
- _stopInlineAudio() {
276
- if (!this._inlineAudio) {
277
- this._syncInlineAudioIcon(false);
278
- this._setInlineAudioProgress(0);
279
- return;
280
- }
281
-
282
- this._inlineAudio.pause();
283
- this._inlineAudio.currentTime = 0;
284
- this._syncInlineAudioIcon(false);
285
- this._setInlineAudioProgress(0);
286
- if (VGFilePreview._activeAudioOwner === this) {
287
- VGFilePreview._activeAudioOwner = null;
288
- }
289
- }
290
-
291
- _syncInlineAudioIcon(isPlaying) {
292
- if (!this._inlineAudioIcon) {
293
- return;
294
- }
295
-
296
- this._inlineAudioIcon.innerHTML = isPlaying ? (getSVG('pause') || '') : (getSVG('play') || '');
297
- }
298
-
299
- _syncInlineAudioProgress() {
300
- if (!this._inlineAudio) {
301
- this._setInlineAudioProgress(0);
302
- return;
303
- }
304
-
305
- const duration = Number(this._inlineAudio.duration || 0);
306
- const currentTime = Number(this._inlineAudio.currentTime || 0);
307
- if (!duration || !Number.isFinite(duration) || duration <= 0) {
308
- this._setInlineAudioProgress(0);
309
- return;
310
- }
311
-
312
- const progress = Math.max(0, Math.min(100, (currentTime / duration) * 100));
313
- this._setInlineAudioProgress(progress);
314
- }
315
-
316
- _setInlineAudioProgress(percent) {
317
- if (!this._inlineAudioContainer) {
318
- return;
319
- }
320
-
321
- const normalized = Math.max(0, Math.min(100, Number(percent) || 0));
322
- this._inlineAudioContainer.style.setProperty('--vg-filepreview-audio-inline-progress', `${normalized}%`);
323
- }
324
-
325
- _renderDownloadField() {
326
- const downloadField = this._editableFields.download;
327
- if (!downloadField) {
328
- return;
329
- }
330
-
331
- const downloadLabel = this._i18n?.button('download') || '';
332
- const downloadIcon = getSVG('download') || '';
333
- const fieldTag = String(downloadField.tagName || '').toUpperCase();
334
- let control = null;
335
-
336
- if (fieldTag === 'A' || fieldTag === 'BUTTON') {
337
- control = downloadField;
338
- } else {
339
- control = downloadField.querySelector('[data-vg-filepreview-download-control]');
340
- if (!control) {
341
- control = document.createElement('button');
342
- control.type = 'button';
343
- downloadField.innerHTML = '';
344
- downloadField.appendChild(control);
345
- }
346
- }
347
-
348
- control.classList.add('vg-filepreview-download-trigger');
349
- control.setAttribute('data-vg-filepreview-download-control', 'true');
350
-
351
- if (fieldTag === 'A' && control === downloadField) {
352
- control.setAttribute('href', this._fileUrl?.href || this._filePath || '#');
353
- if (this._fileMeta?.name) {
354
- control.setAttribute('download', this._fileMeta.name);
355
- }
356
- } else if (String(control.tagName || '').toUpperCase() === 'BUTTON') {
357
- control.setAttribute('type', 'button');
358
- }
359
-
360
- if (!control.hasAttribute('data-vg-filepreview-download-content-init')) {
361
- control.setAttribute('data-vg-filepreview-download-content-init', 'true');
362
- control.innerHTML = '';
363
-
364
- if (downloadIcon) {
365
- const icon = document.createElement('span');
366
- icon.className = 'vg-filepreview-download-trigger__icon';
367
- icon.innerHTML = downloadIcon;
368
- control.appendChild(icon);
369
- }
370
-
371
- if (downloadLabel) {
372
- const text = document.createElement('span');
373
- text.className = 'vg-filepreview-download-trigger__text';
374
- text.textContent = downloadLabel;
375
- control.appendChild(text);
376
- }
377
- }
378
-
379
- if (!control.hasAttribute('data-vg-filepreview-download-bind')) {
380
- control.setAttribute('data-vg-filepreview-download-bind', 'true');
381
- control.addEventListener('click', (event) => {
382
- event.preventDefault();
383
- this._downloadFile();
384
- });
385
- }
386
- }
387
-
388
- _downloadFile() {
389
- const src = this._fileUrl?.href || this._filePath || '';
390
- if (!src) {
391
- return;
392
- }
393
-
394
- const fileName = this._fileMeta?.originalName || this._fileMeta?.name || 'file';
395
-
396
- fetch(src, {
397
- method: 'GET',
398
- credentials: 'same-origin'
399
- })
400
- .then((response) => {
401
- if (!response.ok) {
402
- throw new Error(`HTTP ${response.status}`);
403
- }
404
- return response.blob();
405
- })
406
- .then((blob) => {
407
- const objectUrl = URL.createObjectURL(blob);
408
- this._downloadByLink(objectUrl, fileName);
409
- setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
410
- })
411
- .catch(() => {
412
- this._downloadByLink(src, fileName);
413
- });
414
- }
415
-
416
- _downloadByLink(href, fileName) {
417
- const link = document.createElement('a');
418
- link.href = href;
419
- link.style.display = 'none';
420
- link.rel = 'noopener noreferrer';
421
- if (fileName) {
422
- link.setAttribute('download', fileName);
423
- }
424
-
425
- document.body.appendChild(link);
426
- link.click();
427
- document.body.removeChild(link);
428
- }
429
-
430
- _renderPreview() {
431
- const isNameOnly = Boolean(this._params?.ui?.nameOnly);
432
- const previewContainer = this._resolvePreviewContainer({
433
- autoCreate: !isNameOnly
434
- });
435
- if (!previewContainer && !isNameOnly) {
436
- this._setState('error');
437
- return;
438
- }
439
-
440
- if (previewContainer) {
441
- previewContainer.innerHTML = '';
442
- }
443
-
444
- const context = {
445
- element: this._element,
446
- filePath: this._filePath,
447
- fileUrl: this._fileUrl,
448
- fileMeta: this._fileMeta,
449
- previewContainer,
450
- lang: this._lang,
451
- i18n: this._i18n,
452
- ui: this._params?.ui || {}
453
- };
454
-
455
- let rendered = false;
456
- this._renderers.forEach((renderer) => {
457
- if (rendered || typeof renderer?.canRender !== 'function' || typeof renderer?.render !== 'function') {
458
- return;
459
- }
460
-
461
- if (!renderer.canRender(context)) {
462
- return;
463
- }
464
-
465
- try {
466
- rendered = renderer.render(context) === true;
467
- } catch (error) {
468
- rendered = false;
469
- }
470
- if (rendered) {
471
- this._element.setAttribute('data-vg-filepreview-renderer', renderer.name || 'custom');
472
- }
473
- });
474
-
475
- if (!rendered && !isNameOnly) {
476
- this._element.removeAttribute('data-vg-filepreview-renderer');
477
- this._setState('empty');
478
- return;
479
- }
480
- this._setState('ready');
481
- }
482
-
483
- _resolvePreviewContainer(params = {}) {
484
- const autoCreate = !Object.prototype.hasOwnProperty.call(params, 'autoCreate') || Boolean(params.autoCreate);
485
- const editablePreview = this._editableFields.preview;
486
- if (editablePreview) {
487
- editablePreview.setAttribute('data-vg-filepreview-slot', 'preview');
488
- return editablePreview;
489
- }
490
-
491
- const existedPreview = this._element.querySelector('[data-vg-filepreview-slot="preview"]');
492
- if (existedPreview) {
493
- this._editableFields.preview = existedPreview;
494
- return existedPreview;
495
- }
496
-
497
- if (!autoCreate) {
498
- return null;
499
- }
500
-
501
- const container = document.createElement('div');
502
- container.className = 'preview';
503
- container.setAttribute('data-vg-filepreview-slot', 'preview');
504
- this._element.appendChild(container);
505
- this._editableFields.preview = container;
506
-
507
- return container;
508
- }
509
-
510
- _shouldRenderPreview() {
511
- return Boolean(this._params?.ui?.preview);
512
- }
513
-
514
- _getImageIconSrc() {
515
- const ext = String(this._fileMeta?.ext || '').toLowerCase();
516
- const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.avif', '.ico'];
517
- if (!imageExts.includes(ext)) {
518
- return '';
519
- }
520
-
521
- return this._fileUrl?.href || this._filePath || '';
522
- }
523
-
524
- _renderDefaultIcon(iconField) {
525
- const icon = getSVG(this._filePath);
526
- if (!icon) {
527
- iconField.innerHTML = '';
528
- return;
529
- }
530
-
531
- iconField.innerHTML = icon;
532
- }
533
-
534
- _applyNameClampStyles(field) {
535
- if (!field || !field.style) {
536
- return;
537
- }
538
-
539
- field.style.minWidth = '60px';
540
- field.style.maxWidth = '100%';
541
- field.style.overflow = 'hidden';
542
- field.style.textOverflow = 'ellipsis';
543
- field.style.whiteSpace = 'nowrap';
544
- }
545
-
546
- static init(element, params = {}) {
547
- return VGFilePreview.getOrCreateInstance(element, params);
548
- }
549
-
550
- static stopActiveInlineAudio() {
551
- const owner = VGFilePreview._activeAudioOwner;
552
- if (owner && typeof owner._stopInlineAudio === 'function') {
553
- owner._stopInlineAudio();
554
- }
555
-
556
- VGFilePreview._activeAudioOwner = null;
557
- }
558
-
559
- static stopActiveInlineAudioIfDetached(nodes = []) {
560
- const owner = VGFilePreview._activeAudioOwner;
561
- if (!owner || !owner._element || !Array.isArray(nodes) || !nodes.length) {
562
- return;
563
- }
564
-
565
- const shouldStop = nodes.some((node) => {
566
- if (!node || typeof node.contains !== 'function') {
567
- return false;
568
- }
569
-
570
- return node === owner._element || node.contains(owner._element);
571
- });
572
-
573
- if (shouldStop) {
574
- owner._stopInlineAudio();
575
- VGFilePreview._activeAudioOwner = null;
576
- }
577
- }
578
-
579
- _setState(state = '') {
580
- const value = String(state || '').trim();
581
- if (!value) {
582
- this._element.removeAttribute('data-vg-filepreview-state');
583
- return;
584
- }
585
- this._element.setAttribute('data-vg-filepreview-state', value);
586
- }
587
-
588
- _isAudioFile() {
589
- const ext = String(this._fileMeta?.ext || '').toLowerCase();
590
- return ['.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a', '.opus', '.wma'].includes(ext);
591
- }
592
- }
593
-
594
- export default VGFilePreview;
1
+ import BaseModule from "../../base-module";
2
+ import {mergeDeepObject} from "../../../utils/js/functions";
3
+ import FilePreviewHelper from "../../../utils/js/components/file-preview";
4
+ import {extractAudioMetadata} from "../../../utils/js/components/audio-metadata";
5
+ import {extractVideoMetadata} from "../../../utils/js/components/video-metadata";
6
+ import {getSVG} from "../../module-fn";
7
+ import createFilePreviewRenderers from "./renderers";
8
+ import ImageModal from "./renderers/image-modal";
9
+ import {createFilePreviewI18n, resolveFilePreviewLang} from "./i18n";
10
+
11
+ const NAME = 'filepreview';
12
+ const NAME_KEY = 'vg.filepreview';
13
+
14
+ class VGFilePreview extends BaseModule {
15
+ constructor(el, params = {}) {
16
+ super(el, params);
17
+
18
+ this._params = this._getParams(el, mergeDeepObject({
19
+ validate: true,
20
+ lang: 'ru',
21
+ ui: {
22
+ nameOnly: false
23
+ },
24
+ preview: {
25
+ audio: {enable: true},
26
+ video: {enable: true},
27
+ image: {enable: true},
28
+ archive: {enable: true},
29
+ text: {enable: true},
30
+ office: {enable: true},
31
+ pdf: {enable: true}
32
+ }
33
+ }, params));
34
+
35
+ this._filePath = '';
36
+ this._fileUrl = null;
37
+ this._isValid = false;
38
+ this._fileMeta = {};
39
+ this._fields = [];
40
+ this._editableFields = {};
41
+ this._helper = new FilePreviewHelper(this._element);
42
+ this._renderers = createFilePreviewRenderers();
43
+ this._lang = resolveFilePreviewLang(this._params.lang, this._element);
44
+ this._i18n = createFilePreviewI18n(this._lang);
45
+ this._inlineAudio = null;
46
+ this._inlineAudioSrc = '';
47
+ this._inlineAudioButton = null;
48
+ this._inlineAudioIcon = null;
49
+ this._inlineAudioContainer = null;
50
+ this._audioMetaPromise = null;
51
+ this._audioMetaApplied = false;
52
+ this._audioCoverObjectUrl = '';
53
+ this._videoMetaPromise = null;
54
+ this._videoMetaApplied = false;
55
+ this._videoCoverObjectUrl = '';
56
+ this._imageModal = null;
57
+
58
+ this.init();
59
+ }
60
+
61
+ static get NAME() {
62
+ return NAME;
63
+ }
64
+
65
+ static get NAME_KEY() {
66
+ return NAME_KEY;
67
+ }
68
+
69
+ get filePath() {
70
+ return this._filePath;
71
+ }
72
+
73
+ get fileUrl() {
74
+ return this._fileUrl;
75
+ }
76
+
77
+ get isValid() {
78
+ return this._isValid;
79
+ }
80
+
81
+ get fileMeta() {
82
+ return this._fileMeta;
83
+ }
84
+
85
+ get fields() {
86
+ return this._fields;
87
+ }
88
+
89
+ get editableFields() {
90
+ return this._editableFields;
91
+ }
92
+
93
+ init() {
94
+ const filePath = this._helper.getFilePath();
95
+ this._filePath = filePath;
96
+
97
+ if (this._params.validate) {
98
+ const validation = this._helper.validateFilePath(filePath);
99
+ this._isValid = validation.isValid;
100
+ this._fileUrl = validation.fileUrl;
101
+ } else {
102
+ this._isValid = true;
103
+ this._fileUrl = null;
104
+ }
105
+
106
+ this._helper.syncState(this._isValid);
107
+
108
+ if (!this._isValid) {
109
+ return false;
110
+ }
111
+
112
+ this._fileMeta = this._helper.getFileMeta(this._filePath);
113
+ return this.preview();
114
+ }
115
+
116
+ preview() {
117
+ this._setState('loading');
118
+ this._fields = this._helper.getFields();
119
+ this._editableFields = this._helper.resolveEditableFields(this._fields);
120
+ this._helper.syncEditableFields(this._editableFields);
121
+ this._renderIcon();
122
+ this._renderTextFields();
123
+ this._renderDownloadField();
124
+ this._enrichVideoMetadata();
125
+ this._renderPreview();
126
+
127
+ return this._editableFields;
128
+ }
129
+
130
+ _renderIcon() {
131
+ const iconField = this._editableFields.icon;
132
+ if (!iconField) {
133
+ return;
134
+ }
135
+
136
+ const imageSrc = this._getImageIconSrc();
137
+ if (imageSrc) {
138
+ iconField.innerHTML = '';
139
+ const image = document.createElement('img');
140
+ image.src = imageSrc;
141
+ image.alt = this._fileMeta?.originalName || this._fileMeta?.name || '';
142
+ image.className = 'vg-filepreview-icon-image';
143
+ image.loading = 'lazy';
144
+ image.addEventListener('error', () => {
145
+ this._renderDefaultIcon(iconField);
146
+ });
147
+ if (this._isPreviewGroupEnabled('image')) {
148
+ this._bindIconImageToModal(image, imageSrc);
149
+ }
150
+ iconField.appendChild(image);
151
+ return;
152
+ }
153
+
154
+ this._renderDefaultIcon(iconField);
155
+ }
156
+
157
+ _renderTextFields() {
158
+ const displayName = this._getDisplayName();
159
+ const fileName = this._getFileName();
160
+ const hasDataTitle = this._hasDataDrivenDisplayName();
161
+
162
+ const nameField = this._editableFields.name;
163
+ if (nameField) {
164
+ if (this._isAudioFile() && this._isPreviewGroupEnabled('audio')) {
165
+ this._renderAudioNameField(nameField);
166
+ this._element.setAttribute('data-vg-filepreview-renderer', 'audio');
167
+ } else if (displayName) {
168
+ nameField.classList.remove('vg-filepreview-audio-inline');
169
+ nameField.textContent = displayName;
170
+ this._applyNameClampStyles(nameField);
171
+ }
172
+ }
173
+
174
+ const extField = this._editableFields.ext;
175
+ if (extField && this._fileMeta.ext) {
176
+ extField.textContent = this._fileMeta.ext;
177
+ }
178
+
179
+ const sizeField = this._editableFields.size;
180
+ if (sizeField && this._fileMeta.sizeText) {
181
+ sizeField.textContent = this._fileMeta.sizeText;
182
+ }
183
+
184
+ const originalNameField = this._editableFields.original_name;
185
+ if (!originalNameField) {
186
+ return;
187
+ }
188
+
189
+ if (hasDataTitle && fileName) {
190
+ originalNameField.textContent = fileName;
191
+ this._applyNameClampStyles(originalNameField);
192
+ return;
193
+ }
194
+
195
+ if (this._fileMeta.originalName) {
196
+ originalNameField.textContent = this._fileMeta.originalName;
197
+ this._applyNameClampStyles(originalNameField);
198
+ return;
199
+ }
200
+
201
+ if (this._fileMeta.isMedia) {
202
+ originalNameField.textContent = '';
203
+ }
204
+ }
205
+
206
+ _renderAudioNameField(nameField) {
207
+ const fileName = this._getDisplayName();
208
+ if (!fileName) {
209
+ return;
210
+ }
211
+
212
+ nameField.innerHTML = '';
213
+ nameField.classList.add('vg-filepreview-audio-inline');
214
+
215
+ const button = document.createElement('button');
216
+ button.type = 'button';
217
+ button.className = 'vg-filepreview-audio-inline__toggle';
218
+ button.setAttribute('aria-label', 'Play/Pause');
219
+
220
+ const icon = document.createElement('span');
221
+ icon.className = 'vg-filepreview-audio-inline__icon';
222
+ button.appendChild(icon);
223
+
224
+ const text = document.createElement('span');
225
+ text.className = 'vg-filepreview-audio-inline__name';
226
+ text.textContent = fileName;
227
+ this._applyNameClampStyles(text);
228
+ text.addEventListener('click', (event) => {
229
+ event.preventDefault();
230
+ event.stopPropagation();
231
+ this._toggleInlineAudio();
232
+ });
233
+
234
+ button.addEventListener('click', (event) => {
235
+ event.preventDefault();
236
+ event.stopPropagation();
237
+ this._toggleInlineAudio();
238
+ });
239
+
240
+ nameField.appendChild(button);
241
+ nameField.appendChild(text);
242
+
243
+ this._inlineAudioButton = button;
244
+ this._inlineAudioIcon = icon;
245
+ const rootFile = this._element?.classList?.contains('file')
246
+ ? this._element
247
+ : this._element?.closest?.('.file');
248
+ this._inlineAudioContainer = rootFile || this._element || nameField;
249
+ this._setInlineAudioProgress(0);
250
+ const isCurrentAudioPlaying = VGFilePreview._activeAudioOwner === this && this._inlineAudio && !this._inlineAudio.paused;
251
+ this._syncInlineAudioIcon(isCurrentAudioPlaying);
252
+ this._enrichAudioMetadata(text);
253
+ }
254
+
255
+ _toggleInlineAudio() {
256
+ if (!this._isPreviewGroupEnabled('audio')) {
257
+ return;
258
+ }
259
+
260
+ const src = this._fileUrl?.href || this._filePath || '';
261
+ if (!src || !this._inlineAudioButton) {
262
+ return;
263
+ }
264
+
265
+ if (VGFilePreview._activeAudioOwner && VGFilePreview._activeAudioOwner !== this) {
266
+ VGFilePreview._activeAudioOwner._stopInlineAudio();
267
+ }
268
+
269
+ if (!this._inlineAudio || this._inlineAudioSrc !== src) {
270
+ this._stopInlineAudio();
271
+ this._inlineAudio = new Audio(src);
272
+ this._inlineAudioSrc = src;
273
+ this._inlineAudio.addEventListener('ended', () => {
274
+ this._syncInlineAudioIcon(false);
275
+ this._setInlineAudioProgress(0);
276
+ });
277
+ this._inlineAudio.addEventListener('timeupdate', () => this._syncInlineAudioProgress());
278
+ this._inlineAudio.addEventListener('loadedmetadata', () => this._syncInlineAudioProgress());
279
+ }
280
+
281
+ if (this._inlineAudio.paused) {
282
+ this._inlineAudio.play().then(() => {
283
+ VGFilePreview._activeAudioOwner = this;
284
+ this._syncInlineAudioIcon(true);
285
+ }).catch(() => {
286
+ this._syncInlineAudioIcon(false);
287
+ });
288
+ return;
289
+ }
290
+
291
+ this._inlineAudio.pause();
292
+ this._syncInlineAudioIcon(false);
293
+ if (VGFilePreview._activeAudioOwner === this) {
294
+ VGFilePreview._activeAudioOwner = null;
295
+ }
296
+ }
297
+
298
+ _stopInlineAudio() {
299
+ if (!this._inlineAudio) {
300
+ this._syncInlineAudioIcon(false);
301
+ this._setInlineAudioProgress(0);
302
+ return;
303
+ }
304
+
305
+ this._inlineAudio.pause();
306
+ this._inlineAudio.currentTime = 0;
307
+ this._syncInlineAudioIcon(false);
308
+ this._setInlineAudioProgress(0);
309
+ if (VGFilePreview._activeAudioOwner === this) {
310
+ VGFilePreview._activeAudioOwner = null;
311
+ }
312
+ }
313
+
314
+ _syncInlineAudioIcon(isPlaying) {
315
+ if (!this._inlineAudioIcon) {
316
+ return;
317
+ }
318
+
319
+ this._inlineAudioIcon.innerHTML = isPlaying ? (getSVG('pause') || '') : (getSVG('play') || '');
320
+ }
321
+
322
+ _syncInlineAudioProgress() {
323
+ if (!this._inlineAudio) {
324
+ this._setInlineAudioProgress(0);
325
+ return;
326
+ }
327
+
328
+ const duration = Number(this._inlineAudio.duration || 0);
329
+ const currentTime = Number(this._inlineAudio.currentTime || 0);
330
+ if (!duration || !Number.isFinite(duration) || duration <= 0) {
331
+ this._setInlineAudioProgress(0);
332
+ return;
333
+ }
334
+
335
+ const progress = Math.max(0, Math.min(100, (currentTime / duration) * 100));
336
+ this._setInlineAudioProgress(progress);
337
+ }
338
+
339
+ _setInlineAudioProgress(percent) {
340
+ if (!this._inlineAudioContainer) {
341
+ return;
342
+ }
343
+
344
+ const normalized = Math.max(0, Math.min(100, Number(percent) || 0));
345
+ this._inlineAudioContainer.style.setProperty('--vg-filepreview-audio-inline-progress', `${normalized}%`);
346
+ }
347
+
348
+ _renderDownloadField() {
349
+ const downloadField = this._editableFields.download;
350
+ if (!downloadField) {
351
+ return;
352
+ }
353
+
354
+ const downloadLabel = this._i18n?.button('download') || '';
355
+ const downloadIcon = getSVG('download') || '';
356
+ const fieldTag = String(downloadField.tagName || '').toUpperCase();
357
+ let control = null;
358
+
359
+ if (fieldTag === 'A' || fieldTag === 'BUTTON') {
360
+ control = downloadField;
361
+ } else {
362
+ control = downloadField.querySelector('[data-vg-filepreview-download-control]');
363
+ if (!control) {
364
+ control = document.createElement('button');
365
+ control.type = 'button';
366
+ downloadField.innerHTML = '';
367
+ downloadField.appendChild(control);
368
+ }
369
+ }
370
+
371
+ control.classList.add('vg-filepreview-download-trigger');
372
+ control.setAttribute('data-vg-filepreview-download-control', 'true');
373
+
374
+ if (fieldTag === 'A' && control === downloadField) {
375
+ control.setAttribute('href', this._fileUrl?.href || this._filePath || '#');
376
+ if (this._fileMeta?.name) {
377
+ control.setAttribute('download', this._fileMeta.name);
378
+ }
379
+ } else if (String(control.tagName || '').toUpperCase() === 'BUTTON') {
380
+ control.setAttribute('type', 'button');
381
+ }
382
+
383
+ if (!control.hasAttribute('data-vg-filepreview-download-content-init')) {
384
+ control.setAttribute('data-vg-filepreview-download-content-init', 'true');
385
+ control.innerHTML = '';
386
+
387
+ if (downloadIcon) {
388
+ const icon = document.createElement('span');
389
+ icon.className = 'vg-filepreview-download-trigger__icon';
390
+ icon.innerHTML = downloadIcon;
391
+ control.appendChild(icon);
392
+ }
393
+
394
+ if (downloadLabel) {
395
+ const text = document.createElement('span');
396
+ text.className = 'vg-filepreview-download-trigger__text';
397
+ text.textContent = downloadLabel;
398
+ control.appendChild(text);
399
+ }
400
+ }
401
+
402
+ if (!control.hasAttribute('data-vg-filepreview-download-bind')) {
403
+ control.setAttribute('data-vg-filepreview-download-bind', 'true');
404
+ control.addEventListener('click', (event) => {
405
+ event.preventDefault();
406
+ this._downloadFile();
407
+ });
408
+ }
409
+ }
410
+
411
+ _downloadFile() {
412
+ const src = this._fileUrl?.href || this._filePath || '';
413
+ if (!src) {
414
+ return;
415
+ }
416
+
417
+ const fileName = this._fileMeta?.originalName || this._fileMeta?.name || 'file';
418
+
419
+ fetch(src, {
420
+ method: 'GET',
421
+ credentials: 'same-origin'
422
+ })
423
+ .then((response) => {
424
+ if (!response.ok) {
425
+ throw new Error(`HTTP ${response.status}`);
426
+ }
427
+ return response.blob();
428
+ })
429
+ .then((blob) => {
430
+ const objectUrl = URL.createObjectURL(blob);
431
+ this._downloadByLink(objectUrl, fileName);
432
+ setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
433
+ })
434
+ .catch(() => {
435
+ this._downloadByLink(src, fileName);
436
+ });
437
+ }
438
+
439
+ _downloadByLink(href, fileName) {
440
+ const link = document.createElement('a');
441
+ link.href = href;
442
+ link.style.display = 'none';
443
+ link.rel = 'noopener noreferrer';
444
+ if (fileName) {
445
+ link.setAttribute('download', fileName);
446
+ }
447
+
448
+ document.body.appendChild(link);
449
+ link.click();
450
+ document.body.removeChild(link);
451
+ }
452
+
453
+ _enrichAudioMetadata(nameField = null) {
454
+ if (this._audioMetaApplied || this._audioMetaPromise || !this._isAudioFile()) {
455
+ return;
456
+ }
457
+
458
+ const src = this._fileUrl?.href || this._filePath || '';
459
+ if (!src) {
460
+ return;
461
+ }
462
+
463
+ this._audioMetaPromise = this._createAudioFileFromSource(src)
464
+ .then((file) => {
465
+ if (!file) {
466
+ return null;
467
+ }
468
+ return extractAudioMetadata(file);
469
+ })
470
+ .then((meta) => {
471
+ if (!meta) {
472
+ return false;
473
+ }
474
+
475
+ let changed = false;
476
+ const title = String(meta.title || '').trim();
477
+ if (title) {
478
+ this._element.setAttribute('data-vg-filepreview-display-name', title);
479
+
480
+ if (nameField) {
481
+ nameField.textContent = title;
482
+ this._applyNameClampStyles(nameField);
483
+ }
484
+
485
+ const originalNameField = this._editableFields?.original_name;
486
+ if (originalNameField) {
487
+ originalNameField.textContent = this._getFileName();
488
+ this._applyNameClampStyles(originalNameField);
489
+ }
490
+
491
+ changed = true;
492
+ }
493
+
494
+ if (meta.pictureBlob instanceof Blob && this._editableFields?.icon) {
495
+ if (this._audioCoverObjectUrl) {
496
+ URL.revokeObjectURL(this._audioCoverObjectUrl);
497
+ }
498
+ this._audioCoverObjectUrl = URL.createObjectURL(meta.pictureBlob);
499
+
500
+ const iconField = this._editableFields.icon;
501
+ iconField.innerHTML = '';
502
+
503
+ const image = document.createElement('img');
504
+ image.src = this._audioCoverObjectUrl;
505
+ image.alt = title || this._fileMeta?.originalName || this._fileMeta?.name || '';
506
+ image.className = 'vg-filepreview-icon-image';
507
+ image.loading = 'lazy';
508
+ image.addEventListener('error', () => {
509
+ this._renderDefaultIcon(iconField);
510
+ });
511
+ this._bindIconImageToModal(image, this._audioCoverObjectUrl, title);
512
+ iconField.appendChild(image);
513
+
514
+ changed = true;
515
+ }
516
+
517
+ this._audioMetaApplied = true;
518
+ return changed;
519
+ })
520
+ .catch(() => false)
521
+ .finally(() => {
522
+ this._audioMetaPromise = null;
523
+ });
524
+ }
525
+
526
+ _enrichVideoMetadata() {
527
+ if (!this._isPreviewGroupEnabled('video')) {
528
+ return;
529
+ }
530
+
531
+ if (this._videoMetaApplied || this._videoMetaPromise || !this._isVideoFile()) {
532
+ return;
533
+ }
534
+
535
+ const src = this._fileUrl?.href || this._filePath || '';
536
+ if (!src) {
537
+ return;
538
+ }
539
+
540
+ this._videoMetaPromise = this._createMediaFileFromSource(src, this._fileMeta?.originalName || this._fileMeta?.name || 'video.mp4', 'video/mp4')
541
+ .then((file) => {
542
+ if (!file) {
543
+ return null;
544
+ }
545
+ return extractVideoMetadata(file);
546
+ })
547
+ .then((meta) => {
548
+ if (!meta || !(meta.posterBlob instanceof Blob) || !this._editableFields?.icon) {
549
+ return false;
550
+ }
551
+
552
+ if (this._videoCoverObjectUrl) {
553
+ URL.revokeObjectURL(this._videoCoverObjectUrl);
554
+ }
555
+ this._videoCoverObjectUrl = URL.createObjectURL(meta.posterBlob);
556
+
557
+ const iconField = this._editableFields.icon;
558
+ iconField.innerHTML = '';
559
+
560
+ const image = document.createElement('img');
561
+ image.src = this._videoCoverObjectUrl;
562
+ image.alt = this._fileMeta?.originalName || this._fileMeta?.name || '';
563
+ image.className = 'vg-filepreview-icon-image';
564
+ image.loading = 'lazy';
565
+ image.addEventListener('error', () => {
566
+ this._renderDefaultIcon(iconField);
567
+ });
568
+ if (this._isPreviewGroupEnabled('video')) {
569
+ this._bindIconImageToModal(image, this._videoCoverObjectUrl, image.alt);
570
+ }
571
+ iconField.appendChild(image);
572
+
573
+ this._videoMetaApplied = true;
574
+ return true;
575
+ })
576
+ .catch(() => false)
577
+ .finally(() => {
578
+ this._videoMetaPromise = null;
579
+ });
580
+ }
581
+
582
+ async _createAudioFileFromSource(src = '') {
583
+ const name = this._fileMeta?.originalName || this._fileMeta?.name || 'audio.mp3';
584
+ return this._createMediaFileFromSource(src, name, 'audio/mpeg');
585
+ }
586
+
587
+ async _createMediaFileFromSource(src = '', name = 'file', defaultType = 'application/octet-stream') {
588
+ try {
589
+ const response = await fetch(src, {
590
+ method: 'GET',
591
+ credentials: 'same-origin'
592
+ });
593
+ if (!response.ok) {
594
+ return null;
595
+ }
596
+
597
+ const blob = await response.blob();
598
+ if (!blob || !blob.size) {
599
+ return null;
600
+ }
601
+
602
+ return new File([blob], name, {
603
+ type: blob.type || defaultType,
604
+ lastModified: Date.now()
605
+ });
606
+ } catch {
607
+ return null;
608
+ }
609
+ }
610
+
611
+ _bindIconImageToModal(imageNode, src = '', title = '') {
612
+ if (!imageNode) {
613
+ return;
614
+ }
615
+
616
+ const modalSrc = String(src || '').trim();
617
+ if (!modalSrc) {
618
+ return;
619
+ }
620
+
621
+ const modalTitle = String(title || this._fileMeta?.originalName || this._fileMeta?.name || '').trim();
622
+ imageNode.setAttribute('data-vg-filepreview-image-modal-src', modalSrc);
623
+ imageNode.setAttribute('data-vg-filepreview-image-modal-title', modalTitle);
624
+ imageNode.classList.add('is-preview-action');
625
+
626
+ if (imageNode.hasAttribute('data-vg-filepreview-image-modal-bind')) {
627
+ return;
628
+ }
629
+
630
+ imageNode.setAttribute('data-vg-filepreview-image-modal-bind', 'true');
631
+ imageNode.addEventListener('click', (event) => {
632
+ event.preventDefault();
633
+ event.stopPropagation();
634
+
635
+ const nodeSrc = String(imageNode.getAttribute('data-vg-filepreview-image-modal-src') || '').trim();
636
+ if (!nodeSrc) {
637
+ return;
638
+ }
639
+
640
+ const nodeTitle = String(imageNode.getAttribute('data-vg-filepreview-image-modal-title') || '').trim();
641
+ if (!this._imageModal) {
642
+ this._imageModal = ImageModal.getInstance();
643
+ }
644
+
645
+ this._imageModal.open({
646
+ src: nodeSrc,
647
+ title: nodeTitle,
648
+ defaultTitle: this._i18n?.message('image_title') || ''
649
+ });
650
+ });
651
+ }
652
+
653
+ _renderPreview() {
654
+ const isNameOnly = Boolean(this._params?.ui?.nameOnly) || !this._shouldRenderPreviewForCurrentFile();
655
+ const previewContainer = this._resolvePreviewContainer({
656
+ autoCreate: !isNameOnly
657
+ });
658
+ if (!previewContainer && !isNameOnly) {
659
+ this._setState('error');
660
+ return;
661
+ }
662
+
663
+ if (previewContainer) {
664
+ previewContainer.innerHTML = '';
665
+ }
666
+
667
+ const context = {
668
+ element: this._element,
669
+ filePath: this._filePath,
670
+ fileUrl: this._fileUrl,
671
+ fileMeta: this._fileMeta,
672
+ previewContainer,
673
+ lang: this._lang,
674
+ i18n: this._i18n,
675
+ ui: this._params?.ui || {}
676
+ };
677
+
678
+ let rendered = false;
679
+ this._renderers.forEach((renderer) => {
680
+ if (rendered || typeof renderer?.canRender !== 'function' || typeof renderer?.render !== 'function') {
681
+ return;
682
+ }
683
+ if (!this._isRendererEnabled(renderer?.name)) {
684
+ return;
685
+ }
686
+
687
+ if (!renderer.canRender(context)) {
688
+ return;
689
+ }
690
+
691
+ try {
692
+ rendered = renderer.render(context) === true;
693
+ } catch (error) {
694
+ rendered = false;
695
+ }
696
+ if (rendered) {
697
+ this._element.setAttribute('data-vg-filepreview-renderer', renderer.name || 'custom');
698
+ }
699
+ });
700
+
701
+ if (!rendered && !isNameOnly) {
702
+ this._element.removeAttribute('data-vg-filepreview-renderer');
703
+ this._setState('empty');
704
+ return;
705
+ }
706
+ this._setState('ready');
707
+ }
708
+
709
+ _resolvePreviewContainer(params = {}) {
710
+ const autoCreate = !Object.prototype.hasOwnProperty.call(params, 'autoCreate') || Boolean(params.autoCreate);
711
+ if (!autoCreate) {
712
+ const disabledPreview = this._editableFields.preview || this._element.querySelector('[data-vg-filepreview-slot="preview"]');
713
+ if (disabledPreview) {
714
+ disabledPreview.innerHTML = '';
715
+ disabledPreview.classList.remove('preview');
716
+ disabledPreview.setAttribute('hidden', 'hidden');
717
+ disabledPreview.setAttribute('aria-hidden', 'true');
718
+ }
719
+ return null;
720
+ }
721
+
722
+ const editablePreview = this._editableFields.preview;
723
+ if (editablePreview) {
724
+ editablePreview.classList.add('preview');
725
+ editablePreview.removeAttribute('hidden');
726
+ editablePreview.removeAttribute('aria-hidden');
727
+ editablePreview.setAttribute('data-vg-filepreview-slot', 'preview');
728
+ return editablePreview;
729
+ }
730
+
731
+ const existedPreview = this._element.querySelector('[data-vg-filepreview-slot="preview"]');
732
+ if (existedPreview) {
733
+ existedPreview.classList.add('preview');
734
+ existedPreview.removeAttribute('hidden');
735
+ existedPreview.removeAttribute('aria-hidden');
736
+ this._editableFields.preview = existedPreview;
737
+ return existedPreview;
738
+ }
739
+
740
+ const container = document.createElement('div');
741
+ container.className = 'preview';
742
+ container.setAttribute('data-vg-filepreview-slot', 'preview');
743
+ this._element.appendChild(container);
744
+ this._editableFields.preview = container;
745
+
746
+ return container;
747
+ }
748
+
749
+ _shouldRenderPreviewForCurrentFile() {
750
+ if (this._isAudioFile()) {
751
+ return this._isPreviewGroupEnabled('audio');
752
+ }
753
+
754
+ const ext = String(this._fileMeta?.ext || '').toLowerCase();
755
+ if (ext === '.pdf') {
756
+ return this._isPreviewGroupEnabled('pdf');
757
+ }
758
+
759
+ if (this._isVideoFile()) {
760
+ return this._isPreviewGroupEnabled('video');
761
+ }
762
+
763
+ if (this._isImageFile()) {
764
+ return false;
765
+ }
766
+
767
+ if (this._isOfficeFile()) {
768
+ return this._isPreviewGroupEnabled('office');
769
+ }
770
+
771
+ if (this._isArchiveFile()) {
772
+ return this._isPreviewGroupEnabled('archive');
773
+ }
774
+
775
+ if (this._isTextFile()) {
776
+ return this._isPreviewGroupEnabled('text');
777
+ }
778
+
779
+ return false;
780
+ }
781
+
782
+ _isRendererEnabled(rendererName = '') {
783
+ const name = String(rendererName || '').trim();
784
+ const map = {
785
+ image: 'image',
786
+ video: 'video',
787
+ pdf: 'pdf',
788
+ office: 'office',
789
+ zip: 'archive',
790
+ text: 'text'
791
+ };
792
+ const group = map[name];
793
+ if (!group) {
794
+ return true;
795
+ }
796
+
797
+ return this._isPreviewGroupEnabled(group);
798
+ }
799
+
800
+ _isPreviewGroupEnabled(groupName = '') {
801
+ const group = String(groupName || '').trim();
802
+ if (!group) {
803
+ return false;
804
+ }
805
+
806
+ const preview = this._params?.preview;
807
+ if (!preview || typeof preview !== 'object' || Array.isArray(preview)) {
808
+ return false;
809
+ }
810
+
811
+ if (!Object.prototype.hasOwnProperty.call(preview, group)) {
812
+ return false;
813
+ }
814
+
815
+ const config = preview[group];
816
+ if (typeof config === 'boolean') {
817
+ return config;
818
+ }
819
+
820
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
821
+ return false;
822
+ }
823
+
824
+ if (!Object.prototype.hasOwnProperty.call(config, 'enable')) {
825
+ return false;
826
+ }
827
+
828
+ return Boolean(config.enable);
829
+ }
830
+
831
+ _getImageIconSrc() {
832
+ const ext = String(this._fileMeta?.ext || '').toLowerCase();
833
+ const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.avif', '.ico'];
834
+ if (!imageExts.includes(ext)) {
835
+ return '';
836
+ }
837
+
838
+ return this._fileUrl?.href || this._filePath || '';
839
+ }
840
+
841
+ _isImageFile() {
842
+ const ext = String(this._fileMeta?.ext || '').toLowerCase();
843
+ return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.avif', '.ico', '.tif', '.tiff', '.heic', '.heif'].includes(ext);
844
+ }
845
+
846
+ _isOfficeFile() {
847
+ const ext = String(this._fileMeta?.ext || '').toLowerCase();
848
+ return ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.odt', '.ods', '.odp'].includes(ext);
849
+ }
850
+
851
+ _isArchiveFile() {
852
+ const ext = String(this._fileMeta?.ext || '').toLowerCase();
853
+ return ['.zip'].includes(ext);
854
+ }
855
+
856
+ _isTextFile() {
857
+ const ext = String(this._fileMeta?.ext || '').toLowerCase();
858
+ return ['.txt', '.md', '.csv', '.json', '.xml', '.yml', '.yaml', '.log', '.ini', '.conf', '.env'].includes(ext);
859
+ }
860
+
861
+ _renderDefaultIcon(iconField) {
862
+ const icon = getSVG(this._filePath);
863
+ if (!icon) {
864
+ iconField.innerHTML = '';
865
+ return;
866
+ }
867
+
868
+ iconField.innerHTML = icon;
869
+ }
870
+
871
+ _applyNameClampStyles(field) {
872
+ if (!field || !field.style) {
873
+ return;
874
+ }
875
+
876
+ field.style.minWidth = '60px';
877
+ field.style.maxWidth = '100%';
878
+ field.style.overflow = 'hidden';
879
+ field.style.textOverflow = 'ellipsis';
880
+ field.style.whiteSpace = 'nowrap';
881
+ }
882
+
883
+ static init(element, params = {}) {
884
+ return VGFilePreview.getOrCreateInstance(element, params);
885
+ }
886
+
887
+ static stopActiveInlineAudio() {
888
+ const owner = VGFilePreview._activeAudioOwner;
889
+ if (owner && typeof owner._stopInlineAudio === 'function') {
890
+ owner._stopInlineAudio();
891
+ }
892
+
893
+ VGFilePreview._activeAudioOwner = null;
894
+ }
895
+
896
+ static stopActiveInlineAudioIfDetached(nodes = []) {
897
+ const owner = VGFilePreview._activeAudioOwner;
898
+ if (!owner || !owner._element || !Array.isArray(nodes) || !nodes.length) {
899
+ return;
900
+ }
901
+
902
+ const shouldStop = nodes.some((node) => {
903
+ if (!node || typeof node.contains !== 'function') {
904
+ return false;
905
+ }
906
+
907
+ return node === owner._element || node.contains(owner._element);
908
+ });
909
+
910
+ if (shouldStop) {
911
+ owner._stopInlineAudio();
912
+ VGFilePreview._activeAudioOwner = null;
913
+ }
914
+ }
915
+
916
+ _setState(state = '') {
917
+ const value = String(state || '').trim();
918
+ if (!value) {
919
+ this._element.removeAttribute('data-vg-filepreview-state');
920
+ return;
921
+ }
922
+ this._element.setAttribute('data-vg-filepreview-state', value);
923
+ }
924
+
925
+ _isAudioFile() {
926
+ const ext = String(this._fileMeta?.ext || '').toLowerCase();
927
+ return ['.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a', '.opus', '.wma'].includes(ext);
928
+ }
929
+
930
+ _isVideoFile() {
931
+ const ext = String(this._fileMeta?.ext || '').toLowerCase();
932
+ return ['.mp4', '.webm', '.mov', '.mkv', '.avi', '.m4v', '.ogv'].includes(ext);
933
+ }
934
+
935
+ _getFileName() {
936
+ return String(this._fileMeta?.originalName || this._fileMeta?.name || '').trim();
937
+ }
938
+
939
+ _getDataDisplayName() {
940
+ const dataName = String(this._element?.getAttribute('data-name') || '').trim();
941
+ if (dataName) {
942
+ return dataName;
943
+ }
944
+
945
+ return String(this._element?.getAttribute('data-vg-filepreview-display-name') || '').trim();
946
+ }
947
+
948
+ _hasDataDrivenDisplayName() {
949
+ const dataName = this._getDataDisplayName();
950
+ if (!dataName) {
951
+ return false;
952
+ }
953
+
954
+ const fileName = this._getFileName();
955
+ if (!fileName) {
956
+ return true;
957
+ }
958
+
959
+ return dataName !== fileName;
960
+ }
961
+
962
+ _getDisplayName() {
963
+ const dataName = this._getDataDisplayName();
964
+ if (dataName) {
965
+ return dataName;
966
+ }
967
+
968
+ return this._getFileName();
969
+ }
970
+ }
971
+
972
+ export default VGFilePreview;