vgapp 1.5.7 → 1.5.9

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 (111) hide show
  1. package/app/langs/en/buttons.json +17 -17
  2. package/app/langs/en/messages.json +36 -36
  3. package/app/langs/ru/buttons.json +17 -17
  4. package/app/langs/ru/messages.json +1 -1
  5. package/app/modules/vgalert/js/vgalert.js +15 -12
  6. package/app/modules/vgalert/scss/_variables.scss +16 -16
  7. package/app/modules/vgalert/scss/vgalert.scss +13 -6
  8. package/app/modules/vgcollapse/scss/_variables.scss +7 -7
  9. package/app/modules/vgcollapse/scss/vgcollapse.scss +5 -5
  10. package/app/modules/vgdropdown/scss/_variables.scss +14 -14
  11. package/app/modules/vgdropdown/scss/vgdropdown.scss +5 -5
  12. package/app/modules/vgfilepreview/js/i18n.js +56 -56
  13. package/app/modules/vgfilepreview/js/renderers/image-modal.js +145 -145
  14. package/app/modules/vgfilepreview/js/renderers/image.js +92 -92
  15. package/app/modules/vgfilepreview/js/renderers/index.js +19 -19
  16. package/app/modules/vgfilepreview/js/renderers/office-modal.js +168 -168
  17. package/app/modules/vgfilepreview/js/renderers/office.js +79 -79
  18. package/app/modules/vgfilepreview/js/renderers/pdf-modal.js +260 -260
  19. package/app/modules/vgfilepreview/js/renderers/pdf.js +76 -76
  20. package/app/modules/vgfilepreview/js/renderers/playlist.js +71 -71
  21. package/app/modules/vgfilepreview/js/renderers/text-modal.js +343 -343
  22. package/app/modules/vgfilepreview/js/renderers/text.js +83 -83
  23. package/app/modules/vgfilepreview/js/renderers/zip-modal.js +522 -522
  24. package/app/modules/vgfilepreview/js/renderers/zip.js +89 -89
  25. package/app/modules/vgfilepreview/readme.md +68 -68
  26. package/app/modules/vgfiles/js/base.js +198 -198
  27. package/app/modules/vgfiles/js/droppable.js +17 -17
  28. package/app/modules/vgfiles/js/render.js +153 -153
  29. package/app/modules/vgfiles/js/sortable.js +52 -52
  30. package/app/modules/vgfiles/js/vgfiles.js +43 -43
  31. package/app/modules/vgfiles/readme.md +123 -123
  32. package/app/modules/vgfiles/scss/_mixins.scss +13 -13
  33. package/app/modules/vgfiles/scss/_variables.scss +10 -10
  34. package/app/modules/vgfiles/scss/vgfiles.scss +28 -28
  35. package/app/modules/vgformsender/js/hideshowpass.js +22 -22
  36. package/app/modules/vgformsender/readme.md +1 -1
  37. package/app/modules/vgformsender/scss/_variables.scss +5 -5
  38. package/app/modules/vgformsender/scss/vgformsender.scss +42 -42
  39. package/app/modules/vglawcookie/js/vglawcookie.js +19 -19
  40. package/app/modules/vglawcookie/scss/_variables.scss +13 -13
  41. package/app/modules/vglawcookie/scss/vglawcookie.scss +5 -5
  42. package/app/modules/vgmodal/js/vgmodal.interaction.js +216 -216
  43. package/app/modules/vgmodal/js/vgmodal.js +9 -9
  44. package/app/modules/vgmodal/js/vgmodal.minimized.js +227 -227
  45. package/app/modules/vgmodal/scss/_variables.scss +17 -17
  46. package/app/modules/vgmodal/scss/vgmodal.scss +77 -77
  47. package/app/modules/vgnav/js/vgnav.js +137 -84
  48. package/app/modules/vgnav/scss/_breakpoints.scss +36 -36
  49. package/app/modules/vgnav/scss/_hamburger.scss +5 -5
  50. package/app/modules/vgnav/scss/_placement.scss +5 -5
  51. package/app/modules/vgnav/scss/_toggle.scss +5 -5
  52. package/app/modules/vgnav/scss/_variables.scss +26 -26
  53. package/app/modules/vgnav/scss/vgnav.scss +41 -41
  54. package/app/modules/vgnestable/README.md +39 -39
  55. package/app/modules/vgrangeslider/js/skins.js +8 -8
  56. package/app/modules/vgrangeslider/js/vgrangeslider.js +29 -29
  57. package/app/modules/vgrangeslider/scss/_variables.scss +55 -55
  58. package/app/modules/vgrangeslider/scss/vgrangeslider.scss +239 -239
  59. package/app/modules/vgrollup/js/vgrollup.js +15 -15
  60. package/app/modules/vgrollup/scss/_variables.scss +15 -15
  61. package/app/modules/vgrollup/scss/vgrollup.scss +6 -6
  62. package/app/modules/vgselect/js/handlers.js +6 -6
  63. package/app/modules/vgselect/js/vgselect.js +167 -123
  64. package/app/modules/vgselect/readme.md +11 -11
  65. package/app/modules/vgselect/scss/_variables.scss +51 -51
  66. package/app/modules/vgselect/scss/vgselect.scss +1 -1
  67. package/app/modules/vgsidebar/js/vgsidebar.js +21 -21
  68. package/app/modules/vgsidebar/scss/_variables.scss +10 -10
  69. package/app/modules/vgsidebar/scss/vgsidebar.scss +5 -5
  70. package/app/modules/vgspy/js/vgspy.js +43 -43
  71. package/app/modules/vgtable/js/_filters.js +10 -6
  72. package/app/modules/vgtable/js/_options.js +16 -16
  73. package/app/modules/vgtable/js/_pagination.js +118 -118
  74. package/app/modules/vgtable/js/_responsive.js +79 -79
  75. package/app/modules/vgtable/js/_sticky-header.js +349 -280
  76. package/app/modules/vgtable/js/vgtable.js +98 -98
  77. package/app/modules/vgtable/scss/_fixed-columns.scss +23 -25
  78. package/app/modules/vgtable/scss/_sorting.scss +6 -6
  79. package/app/modules/vgtable/scss/_sticky-header.scss +31 -31
  80. package/app/modules/vgtable/scss/_variables.scss +6 -4
  81. package/app/modules/vgtable/scss/vgtable.scss +8 -4
  82. package/app/modules/vgtabs/js/vgtabs.js +91 -91
  83. package/app/modules/vgtabs/scss/_variables.scss +23 -23
  84. package/app/modules/vgtabs/scss/vgtabs.scss +5 -5
  85. package/app/modules/vgtoast/js/vgtoast.js +4 -4
  86. package/app/modules/vgtoast/scss/_variables.scss +13 -13
  87. package/app/modules/vgtoast/scss/vgtoast.scss +5 -5
  88. package/app/modules/vgtooltip/js/vgtooltip.js +127 -127
  89. package/app/modules/vgtooltip/scss/_variables.scss +10 -10
  90. package/app/modules/vgtooltip/scss/vgtooltip.scss +5 -5
  91. package/app/utils/js/components/audio-metadata.js +240 -240
  92. package/app/utils/js/components/backdrop.js +116 -116
  93. package/app/utils/js/components/file-icon.js +7 -7
  94. package/app/utils/js/components/file-preview.js +304 -304
  95. package/app/utils/js/components/responsive.js +165 -165
  96. package/app/utils/js/components/sanitize.js +150 -150
  97. package/app/utils/js/components/video-metadata.js +140 -140
  98. package/app/utils/scss/_backdrop.scss +5 -5
  99. package/app/utils/scss/_theme-default.scss +12 -12
  100. package/app/utils/scss/_theme.scss +7 -7
  101. package/app/utils/scss/default.scss +9 -9
  102. package/app/utils/scss/functions.scss +21 -21
  103. package/app/utils/scss/index.scss +4 -4
  104. package/app/utils/scss/mixin.scss +16 -16
  105. package/app/utils/scss/variables.scss +147 -147
  106. package/build/vgapp.css +1 -1
  107. package/build/vgapp.css.map +1 -1
  108. package/build/vgapp.js +1 -1
  109. package/build/vgapp.js.map +1 -1
  110. package/index.js +7 -7
  111. package/package.json +1 -1
@@ -1,522 +1,522 @@
1
- import VGModal from "../../../vgmodal";
2
-
3
- class ZipModal {
4
- constructor() {
5
- this._modalId = 'vg-filepreview-zip-modal';
6
- this._abortController = null;
7
- this._labels = {};
8
- this._entries = [];
9
- this._arrayBuffer = null;
10
- }
11
-
12
- static getInstance() {
13
- if (!ZipModal._instance) {
14
- ZipModal._instance = new ZipModal();
15
- }
16
-
17
- return ZipModal._instance;
18
- }
19
-
20
- open(payload = {}) {
21
- const src = String(payload.src || '').trim();
22
- if (!src) {
23
- return;
24
- }
25
-
26
- this._ensureModal();
27
- if (!this._modal || !this._content) {
28
- return;
29
- }
30
-
31
- this._labels = payload?.labels && typeof payload.labels === 'object' ? payload.labels : {};
32
- const defaultTitle = String(payload.defaultTitle || '').trim();
33
- const title = String(payload.title || '').trim();
34
- this._title.textContent = title || defaultTitle;
35
- this._content.innerHTML = `<div class="vg-filepreview-zip-modal__loading">${this._escapeHtml(this._label('loadingArchive'))}</div>`;
36
- this._modal.show();
37
-
38
- this._loadZipEntries(src);
39
- }
40
-
41
- close() {
42
- if (!this._modal) {
43
- return;
44
- }
45
-
46
- this._modal.hide();
47
- }
48
-
49
- _ensureModal() {
50
- if (this._modal && this._root) {
51
- return;
52
- }
53
-
54
- this._initModal();
55
- }
56
-
57
- _initModal() {
58
- const params = {
59
- centered: true,
60
- dismiss: true,
61
- backdrop: true,
62
- keyboard: true,
63
- sizes: {
64
- width: '800px',
65
- },
66
- animation: {
67
- enable: false
68
- }
69
- };
70
-
71
- const existed = document.getElementById(this._modalId);
72
- if (existed) {
73
- this._root = existed;
74
- this._modal = VGModal.getOrCreateInstance(existed, params);
75
- this._bindElements(existed);
76
- this._bindLifecycle(existed);
77
- return;
78
- }
79
-
80
- this._modal = VGModal.build(this._modalId, params, (modalInstance) => {
81
- const element = modalInstance._element;
82
- this._root = element;
83
- element.classList.add('vg-filepreview-zip-modal');
84
-
85
- const body = element.querySelector('.vg-modal-body');
86
- const content = element.querySelector('.vg-modal-content');
87
- if (!body || !content) {
88
- return;
89
- }
90
-
91
- body.classList.add('vg-filepreview-image-modal__body');
92
- body.innerHTML = '';
93
-
94
- let header = element.querySelector('.vg-modal-header');
95
- if (!header) {
96
- header = document.createElement('div');
97
- header.className = 'vg-modal-header';
98
- content.prepend(header);
99
- }
100
-
101
- this._title = document.createElement('div');
102
- this._title.className = 'vg-modal-title';
103
- this._title.textContent = '';
104
-
105
- this._content = document.createElement('div');
106
- this._content.className = 'vg-filepreview-zip-modal__content';
107
-
108
- header.appendChild(this._title);
109
- body.appendChild(this._content);
110
-
111
- this._bindLifecycle(element);
112
- });
113
- }
114
-
115
- _bindElements(root) {
116
- this._title = root.querySelector('.vg-filepreview-zip-modal__title');
117
- this._content = root.querySelector('.vg-filepreview-zip-modal__content');
118
- }
119
-
120
- _bindLifecycle(root) {
121
- if (!root || root.hasAttribute('data-vg-filepreview-zip-lifecycle-bind')) {
122
- return;
123
- }
124
-
125
- root.setAttribute('data-vg-filepreview-zip-lifecycle-bind', 'true');
126
- root.addEventListener('vg.modal.hidden', () => {
127
- this._destroyModal();
128
- });
129
- }
130
-
131
- _loadZipEntries(src) {
132
- if (this._abortController) {
133
- this._abortController.abort();
134
- }
135
- this._abortController = new AbortController();
136
-
137
- const cached = ZipModal._cache.get(src);
138
- if (cached?.entries && cached?.arrayBuffer) {
139
- this._entries = cached.entries;
140
- this._arrayBuffer = cached.arrayBuffer;
141
- this._renderEntries(this._entries);
142
- return;
143
- }
144
-
145
- fetch(src, {
146
- method: 'GET',
147
- signal: this._abortController.signal
148
- })
149
- .then((response) => {
150
- if (!response.ok) {
151
- throw new Error(`HTTP ${response.status}`);
152
- }
153
- return response.arrayBuffer();
154
- })
155
- .then((buffer) => {
156
- const entries = this._parseZipEntries(buffer);
157
- this._entries = entries;
158
- this._arrayBuffer = buffer;
159
- ZipModal._cache.set(src, {
160
- entries,
161
- arrayBuffer: buffer
162
- });
163
- this._renderEntries(entries);
164
- })
165
- .catch((error) => {
166
- if (error?.name === 'AbortError') {
167
- return;
168
- }
169
-
170
- if (this._content) {
171
- const errorText = this._resolveErrorText(error);
172
- this._content.innerHTML = `<div class="vg-filepreview-zip-modal__error">${this._escapeHtml(this._label('cannotOpenArchive'))}: ${this._escapeHtml(errorText)}</div>`;
173
- }
174
- });
175
- }
176
-
177
- _destroyModal() {
178
- if (this._abortController) {
179
- this._abortController.abort();
180
- this._abortController = null;
181
- }
182
-
183
- if (this._modal && typeof this._modal.dispose === 'function') {
184
- this._modal.dispose();
185
- }
186
-
187
- if (this._root && this._root.parentNode) {
188
- this._root.parentNode.removeChild(this._root);
189
- }
190
-
191
- this._root = null;
192
- this._modal = null;
193
- this._title = null;
194
- this._content = null;
195
- this._labels = null;
196
- this._entries = [];
197
- this._arrayBuffer = null;
198
- }
199
-
200
- _parseZipEntries(arrayBuffer) {
201
- const view = new DataView(arrayBuffer);
202
- const length = view.byteLength;
203
-
204
- if (length < 22) {
205
- throw new Error('invalid_zip');
206
- }
207
-
208
- const eocdOffset = this._findEndOfCentralDirectory(view);
209
- if (eocdOffset < 0) {
210
- throw new Error('central_directory_not_found');
211
- }
212
-
213
- const totalEntries = view.getUint16(eocdOffset + 10, true);
214
- const centralDirOffset = view.getUint32(eocdOffset + 16, true);
215
-
216
- const decoder = new TextDecoder('utf-8', { fatal: false });
217
- const entries = [];
218
- let ptr = centralDirOffset;
219
-
220
- for (let i = 0; i < totalEntries; i += 1) {
221
- if (ptr + 46 > length) {
222
- break;
223
- }
224
-
225
- const signature = view.getUint32(ptr, true);
226
- if (signature !== 0x02014b50) {
227
- break;
228
- }
229
-
230
- const compressedSize = view.getUint32(ptr + 20, true);
231
- const uncompressedSize = view.getUint32(ptr + 24, true);
232
- const compressionMethod = view.getUint16(ptr + 10, true);
233
- const fileNameLen = view.getUint16(ptr + 28, true);
234
- const extraLen = view.getUint16(ptr + 30, true);
235
- const commentLen = view.getUint16(ptr + 32, true);
236
- const localHeaderOffset = view.getUint32(ptr + 42, true);
237
-
238
- const nameStart = ptr + 46;
239
- const nameEnd = nameStart + fileNameLen;
240
- if (nameEnd > length) {
241
- break;
242
- }
243
-
244
- const fileNameBytes = new Uint8Array(arrayBuffer, nameStart, fileNameLen);
245
- const fileName = decoder.decode(fileNameBytes);
246
-
247
- entries.push({
248
- name: fileName,
249
- compressedSize,
250
- uncompressedSize,
251
- compressionMethod,
252
- localHeaderOffset,
253
- isDirectory: fileName.endsWith('/')
254
- });
255
-
256
- ptr = nameEnd + extraLen + commentLen;
257
- }
258
-
259
- return entries;
260
- }
261
-
262
- _findEndOfCentralDirectory(view) {
263
- const minOffset = Math.max(0, view.byteLength - 65557);
264
- for (let i = view.byteLength - 22; i >= minOffset; i -= 1) {
265
- if (view.getUint32(i, true) === 0x06054b50) {
266
- return i;
267
- }
268
- }
269
- return -1;
270
- }
271
-
272
- _renderEntries(entries) {
273
- if (!this._content) {
274
- return;
275
- }
276
-
277
- if (!entries.length) {
278
- this._content.innerHTML = `<div class="vg-filepreview-zip-modal__empty">${this._escapeHtml(this._label('archiveEmptyUnsupported'))}</div>`;
279
- return;
280
- }
281
-
282
- const rows = entries.map((entry, index) => {
283
- const type = entry.isDirectory
284
- ? this._label('typeDir')
285
- : this._label('typeFile');
286
- const compressed = entry.isDirectory ? '-' : this._formatSize(entry.compressedSize);
287
- const original = entry.isDirectory ? '-' : this._formatSize(entry.uncompressedSize);
288
- const previewAction = (!entry.isDirectory && this._canPreviewEntry(entry))
289
- ? `<button type="button" class="vg-filepreview-zip-modal__entry-btn" data-entry-index="${index}">${this._escapeHtml(this._label('previewEntry'))}</button>`
290
- : '-';
291
-
292
- return `
293
- <tr>
294
- <td>${index + 1}</td>
295
- <td class="name">${this._escapeHtml(entry.name)}</td>
296
- <td>${type}</td>
297
- <td>${compressed}</td>
298
- <td>${original}</td>
299
- <td>${previewAction}</td>
300
- </tr>`;
301
- }).join('');
302
-
303
- this._content.innerHTML = `
304
- <table class="vg-filepreview-zip-modal__table">
305
- <thead>
306
- <tr>
307
- <th>#</th>
308
- <th>${this._escapeHtml(this._label('tableName'))}</th>
309
- <th>${this._escapeHtml(this._label('tableType'))}</th>
310
- <th>${this._escapeHtml(this._label('tablePacked'))}</th>
311
- <th>${this._escapeHtml(this._label('tableSize'))}</th>
312
- <th>${this._escapeHtml(this._label('tablePreview'))}</th>
313
- </tr>
314
- </thead>
315
- <tbody>
316
- ${rows}
317
- </tbody>
318
- </table>`;
319
- this._bindEntriesPreview();
320
- }
321
-
322
- _label(key, fallback = '') {
323
- const value = this._labels?.[key];
324
- return typeof value === 'string' && value.trim() ? value : fallback;
325
- }
326
-
327
- _resolveErrorText(error) {
328
- const message = String(error?.message || '').trim();
329
- if (!message) {
330
- return this._label('unknownError');
331
- }
332
-
333
- if (message === 'invalid_zip') {
334
- return this._label('invalidZip');
335
- }
336
-
337
- if (message === 'central_directory_not_found') {
338
- return this._label('centralDirectoryNotFound');
339
- }
340
-
341
- return message;
342
- }
343
-
344
- _formatSize(bytes) {
345
- if (!Number.isFinite(bytes) || bytes < 0) {
346
- return '-';
347
- }
348
-
349
- const units = ['B', 'KB', 'MB', 'GB'];
350
- let value = bytes;
351
- let unitIndex = 0;
352
- while (value >= 1024 && unitIndex < units.length - 1) {
353
- value /= 1024;
354
- unitIndex += 1;
355
- }
356
-
357
- const digits = value >= 100 || unitIndex === 0 ? 0 : 2;
358
- return `${value.toFixed(digits)} ${units[unitIndex]}`;
359
- }
360
-
361
- _escapeHtml(value) {
362
- return String(value)
363
- .replace(/&/g, '&amp;')
364
- .replace(/</g, '&lt;')
365
- .replace(/>/g, '&gt;')
366
- .replace(/"/g, '&quot;')
367
- .replace(/'/g, '&#39;');
368
- }
369
-
370
- _bindEntriesPreview() {
371
- if (!this._content || this._content.hasAttribute('data-vg-filepreview-zip-preview-bind')) {
372
- return;
373
- }
374
-
375
- this._content.setAttribute('data-vg-filepreview-zip-preview-bind', 'true');
376
- this._content.addEventListener('click', (event) => {
377
- const button = event.target.closest('[data-entry-index]');
378
- if (!button) {
379
- return;
380
- }
381
-
382
- event.preventDefault();
383
- const index = Number(button.getAttribute('data-entry-index'));
384
- if (!Number.isFinite(index) || index < 0 || index >= this._entries.length) {
385
- return;
386
- }
387
- this._previewEntry(index);
388
- });
389
- }
390
-
391
- _canPreviewEntry(entry) {
392
- if (!entry || entry.isDirectory || !entry.name) {
393
- return false;
394
- }
395
-
396
- const ext = this._extractExtension(entry.name);
397
- const allowed = new Set([
398
- '.txt', '.md', '.json', '.xml', '.yml', '.yaml', '.csv', '.log',
399
- '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp'
400
- ]);
401
- return allowed.has(ext);
402
- }
403
-
404
- async _previewEntry(index) {
405
- if (!this._content) {
406
- return;
407
- }
408
-
409
- const entry = this._entries[index];
410
- if (!entry || !this._canPreviewEntry(entry)) {
411
- return;
412
- }
413
-
414
- let panel = this._content.querySelector('.vg-filepreview-zip-modal__entry-preview');
415
- if (!panel) {
416
- panel = document.createElement('div');
417
- panel.className = 'vg-filepreview-zip-modal__entry-preview';
418
- this._content.appendChild(panel);
419
- }
420
-
421
- panel.innerHTML = `<div class="vg-filepreview-zip-modal__entry-preview-loading">${this._escapeHtml(this._label('loadingEntry'))}</div>`;
422
-
423
- try {
424
- const uint8 = await this._extractEntryBytes(entry);
425
- const ext = this._extractExtension(entry.name);
426
-
427
- if (this._isImageExtension(ext)) {
428
- const blob = new Blob([uint8.buffer], { type: this._mimeByExtension(ext) });
429
- const url = URL.createObjectURL(blob);
430
- panel.innerHTML = `
431
- <div class="vg-filepreview-zip-modal__entry-preview-title">${this._escapeHtml(entry.name)}</div>
432
- <img class="vg-filepreview-zip-modal__entry-preview-image" src="${url}" alt="${this._escapeHtml(entry.name)}" />`;
433
- setTimeout(() => URL.revokeObjectURL(url), 2000);
434
- return;
435
- }
436
-
437
- const decoder = new TextDecoder('utf-8', { fatal: false });
438
- const text = decoder.decode(uint8);
439
- const safeText = this._escapeHtml(text || this._label('emptyFileInArchive'));
440
- panel.innerHTML = `
441
- <div class="vg-filepreview-zip-modal__entry-preview-title">${this._escapeHtml(entry.name)}</div>
442
- <pre class="vg-filepreview-zip-modal__entry-preview-text">${safeText}</pre>`;
443
- } catch (error) {
444
- panel.innerHTML = `<div class="vg-filepreview-zip-modal__entry-preview-error">${this._escapeHtml(this._label('cannotPreviewEntry'))}: ${this._escapeHtml(error?.message || this._label('unknownError'))}</div>`;
445
- }
446
- }
447
-
448
- async _extractEntryBytes(entry) {
449
- if (!this._arrayBuffer) {
450
- throw new Error(this._label('archiveBufferMissing'));
451
- }
452
-
453
- const view = new DataView(this._arrayBuffer);
454
- const offset = Number(entry.localHeaderOffset || 0);
455
- if (offset + 30 > view.byteLength) {
456
- throw new Error(this._label('entryCorrupted'));
457
- }
458
-
459
- const signature = view.getUint32(offset, true);
460
- if (signature !== 0x04034b50) {
461
- throw new Error(this._label('entryCorrupted'));
462
- }
463
-
464
- const nameLen = view.getUint16(offset + 26, true);
465
- const extraLen = view.getUint16(offset + 28, true);
466
- const dataStart = offset + 30 + nameLen + extraLen;
467
- const dataEnd = dataStart + Number(entry.compressedSize || 0);
468
- if (dataEnd > view.byteLength) {
469
- throw new Error(this._label('entryCorrupted'));
470
- }
471
-
472
- const compressed = new Uint8Array(this._arrayBuffer.slice(dataStart, dataEnd));
473
- const method = Number(entry.compressionMethod || 0);
474
-
475
- if (method === 0) {
476
- return compressed;
477
- }
478
-
479
- if (method === 8) {
480
- if (typeof DecompressionStream === 'undefined') {
481
- throw new Error(this._label('deflateNotSupported'));
482
- }
483
-
484
- const stream = new Blob([compressed]).stream().pipeThrough(new DecompressionStream('deflate-raw'));
485
- const decompressed = await new Response(stream).arrayBuffer();
486
- return new Uint8Array(decompressed);
487
- }
488
-
489
- throw new Error(this._label('compressionNotSupported'));
490
- }
491
-
492
- _extractExtension(name) {
493
- const value = String(name || '').toLowerCase();
494
- const clean = value.split('/').pop() || '';
495
- if (!clean.includes('.')) {
496
- return '';
497
- }
498
- return `.${clean.split('.').pop() || ''}`;
499
- }
500
-
501
- _isImageExtension(ext) {
502
- return new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp']).has(ext);
503
- }
504
-
505
- _mimeByExtension(ext) {
506
- const map = {
507
- '.png': 'image/png',
508
- '.jpg': 'image/jpeg',
509
- '.jpeg': 'image/jpeg',
510
- '.gif': 'image/gif',
511
- '.webp': 'image/webp',
512
- '.svg': 'image/svg+xml',
513
- '.bmp': 'image/bmp'
514
- };
515
- return map[ext] || 'application/octet-stream';
516
- }
517
-
518
- }
519
-
520
- ZipModal._cache = new Map();
521
-
522
- export default ZipModal;
1
+ import VGModal from "../../../vgmodal";
2
+
3
+ class ZipModal {
4
+ constructor() {
5
+ this._modalId = 'vg-filepreview-zip-modal';
6
+ this._abortController = null;
7
+ this._labels = {};
8
+ this._entries = [];
9
+ this._arrayBuffer = null;
10
+ }
11
+
12
+ static getInstance() {
13
+ if (!ZipModal._instance) {
14
+ ZipModal._instance = new ZipModal();
15
+ }
16
+
17
+ return ZipModal._instance;
18
+ }
19
+
20
+ open(payload = {}) {
21
+ const src = String(payload.src || '').trim();
22
+ if (!src) {
23
+ return;
24
+ }
25
+
26
+ this._ensureModal();
27
+ if (!this._modal || !this._content) {
28
+ return;
29
+ }
30
+
31
+ this._labels = payload?.labels && typeof payload.labels === 'object' ? payload.labels : {};
32
+ const defaultTitle = String(payload.defaultTitle || '').trim();
33
+ const title = String(payload.title || '').trim();
34
+ this._title.textContent = title || defaultTitle;
35
+ this._content.innerHTML = `<div class="vg-filepreview-zip-modal__loading">${this._escapeHtml(this._label('loadingArchive'))}</div>`;
36
+ this._modal.show();
37
+
38
+ this._loadZipEntries(src);
39
+ }
40
+
41
+ close() {
42
+ if (!this._modal) {
43
+ return;
44
+ }
45
+
46
+ this._modal.hide();
47
+ }
48
+
49
+ _ensureModal() {
50
+ if (this._modal && this._root) {
51
+ return;
52
+ }
53
+
54
+ this._initModal();
55
+ }
56
+
57
+ _initModal() {
58
+ const params = {
59
+ centered: true,
60
+ dismiss: true,
61
+ backdrop: true,
62
+ keyboard: true,
63
+ sizes: {
64
+ width: '800px',
65
+ },
66
+ animation: {
67
+ enable: false
68
+ }
69
+ };
70
+
71
+ const existed = document.getElementById(this._modalId);
72
+ if (existed) {
73
+ this._root = existed;
74
+ this._modal = VGModal.getOrCreateInstance(existed, params);
75
+ this._bindElements(existed);
76
+ this._bindLifecycle(existed);
77
+ return;
78
+ }
79
+
80
+ this._modal = VGModal.build(this._modalId, params, (modalInstance) => {
81
+ const element = modalInstance._element;
82
+ this._root = element;
83
+ element.classList.add('vg-filepreview-zip-modal');
84
+
85
+ const body = element.querySelector('.vg-modal-body');
86
+ const content = element.querySelector('.vg-modal-content');
87
+ if (!body || !content) {
88
+ return;
89
+ }
90
+
91
+ body.classList.add('vg-filepreview-image-modal__body');
92
+ body.innerHTML = '';
93
+
94
+ let header = element.querySelector('.vg-modal-header');
95
+ if (!header) {
96
+ header = document.createElement('div');
97
+ header.className = 'vg-modal-header';
98
+ content.prepend(header);
99
+ }
100
+
101
+ this._title = document.createElement('div');
102
+ this._title.className = 'vg-modal-title';
103
+ this._title.textContent = '';
104
+
105
+ this._content = document.createElement('div');
106
+ this._content.className = 'vg-filepreview-zip-modal__content';
107
+
108
+ header.appendChild(this._title);
109
+ body.appendChild(this._content);
110
+
111
+ this._bindLifecycle(element);
112
+ });
113
+ }
114
+
115
+ _bindElements(root) {
116
+ this._title = root.querySelector('.vg-filepreview-zip-modal__title');
117
+ this._content = root.querySelector('.vg-filepreview-zip-modal__content');
118
+ }
119
+
120
+ _bindLifecycle(root) {
121
+ if (!root || root.hasAttribute('data-vg-filepreview-zip-lifecycle-bind')) {
122
+ return;
123
+ }
124
+
125
+ root.setAttribute('data-vg-filepreview-zip-lifecycle-bind', 'true');
126
+ root.addEventListener('vg.modal.hidden', () => {
127
+ this._destroyModal();
128
+ });
129
+ }
130
+
131
+ _loadZipEntries(src) {
132
+ if (this._abortController) {
133
+ this._abortController.abort();
134
+ }
135
+ this._abortController = new AbortController();
136
+
137
+ const cached = ZipModal._cache.get(src);
138
+ if (cached?.entries && cached?.arrayBuffer) {
139
+ this._entries = cached.entries;
140
+ this._arrayBuffer = cached.arrayBuffer;
141
+ this._renderEntries(this._entries);
142
+ return;
143
+ }
144
+
145
+ fetch(src, {
146
+ method: 'GET',
147
+ signal: this._abortController.signal
148
+ })
149
+ .then((response) => {
150
+ if (!response.ok) {
151
+ throw new Error(`HTTP ${response.status}`);
152
+ }
153
+ return response.arrayBuffer();
154
+ })
155
+ .then((buffer) => {
156
+ const entries = this._parseZipEntries(buffer);
157
+ this._entries = entries;
158
+ this._arrayBuffer = buffer;
159
+ ZipModal._cache.set(src, {
160
+ entries,
161
+ arrayBuffer: buffer
162
+ });
163
+ this._renderEntries(entries);
164
+ })
165
+ .catch((error) => {
166
+ if (error?.name === 'AbortError') {
167
+ return;
168
+ }
169
+
170
+ if (this._content) {
171
+ const errorText = this._resolveErrorText(error);
172
+ this._content.innerHTML = `<div class="vg-filepreview-zip-modal__error">${this._escapeHtml(this._label('cannotOpenArchive'))}: ${this._escapeHtml(errorText)}</div>`;
173
+ }
174
+ });
175
+ }
176
+
177
+ _destroyModal() {
178
+ if (this._abortController) {
179
+ this._abortController.abort();
180
+ this._abortController = null;
181
+ }
182
+
183
+ if (this._modal && typeof this._modal.dispose === 'function') {
184
+ this._modal.dispose();
185
+ }
186
+
187
+ if (this._root && this._root.parentNode) {
188
+ this._root.parentNode.removeChild(this._root);
189
+ }
190
+
191
+ this._root = null;
192
+ this._modal = null;
193
+ this._title = null;
194
+ this._content = null;
195
+ this._labels = null;
196
+ this._entries = [];
197
+ this._arrayBuffer = null;
198
+ }
199
+
200
+ _parseZipEntries(arrayBuffer) {
201
+ const view = new DataView(arrayBuffer);
202
+ const length = view.byteLength;
203
+
204
+ if (length < 22) {
205
+ throw new Error('invalid_zip');
206
+ }
207
+
208
+ const eocdOffset = this._findEndOfCentralDirectory(view);
209
+ if (eocdOffset < 0) {
210
+ throw new Error('central_directory_not_found');
211
+ }
212
+
213
+ const totalEntries = view.getUint16(eocdOffset + 10, true);
214
+ const centralDirOffset = view.getUint32(eocdOffset + 16, true);
215
+
216
+ const decoder = new TextDecoder('utf-8', { fatal: false });
217
+ const entries = [];
218
+ let ptr = centralDirOffset;
219
+
220
+ for (let i = 0; i < totalEntries; i += 1) {
221
+ if (ptr + 46 > length) {
222
+ break;
223
+ }
224
+
225
+ const signature = view.getUint32(ptr, true);
226
+ if (signature !== 0x02014b50) {
227
+ break;
228
+ }
229
+
230
+ const compressedSize = view.getUint32(ptr + 20, true);
231
+ const uncompressedSize = view.getUint32(ptr + 24, true);
232
+ const compressionMethod = view.getUint16(ptr + 10, true);
233
+ const fileNameLen = view.getUint16(ptr + 28, true);
234
+ const extraLen = view.getUint16(ptr + 30, true);
235
+ const commentLen = view.getUint16(ptr + 32, true);
236
+ const localHeaderOffset = view.getUint32(ptr + 42, true);
237
+
238
+ const nameStart = ptr + 46;
239
+ const nameEnd = nameStart + fileNameLen;
240
+ if (nameEnd > length) {
241
+ break;
242
+ }
243
+
244
+ const fileNameBytes = new Uint8Array(arrayBuffer, nameStart, fileNameLen);
245
+ const fileName = decoder.decode(fileNameBytes);
246
+
247
+ entries.push({
248
+ name: fileName,
249
+ compressedSize,
250
+ uncompressedSize,
251
+ compressionMethod,
252
+ localHeaderOffset,
253
+ isDirectory: fileName.endsWith('/')
254
+ });
255
+
256
+ ptr = nameEnd + extraLen + commentLen;
257
+ }
258
+
259
+ return entries;
260
+ }
261
+
262
+ _findEndOfCentralDirectory(view) {
263
+ const minOffset = Math.max(0, view.byteLength - 65557);
264
+ for (let i = view.byteLength - 22; i >= minOffset; i -= 1) {
265
+ if (view.getUint32(i, true) === 0x06054b50) {
266
+ return i;
267
+ }
268
+ }
269
+ return -1;
270
+ }
271
+
272
+ _renderEntries(entries) {
273
+ if (!this._content) {
274
+ return;
275
+ }
276
+
277
+ if (!entries.length) {
278
+ this._content.innerHTML = `<div class="vg-filepreview-zip-modal__empty">${this._escapeHtml(this._label('archiveEmptyUnsupported'))}</div>`;
279
+ return;
280
+ }
281
+
282
+ const rows = entries.map((entry, index) => {
283
+ const type = entry.isDirectory
284
+ ? this._label('typeDir')
285
+ : this._label('typeFile');
286
+ const compressed = entry.isDirectory ? '-' : this._formatSize(entry.compressedSize);
287
+ const original = entry.isDirectory ? '-' : this._formatSize(entry.uncompressedSize);
288
+ const previewAction = (!entry.isDirectory && this._canPreviewEntry(entry))
289
+ ? `<button type="button" class="vg-filepreview-zip-modal__entry-btn" data-entry-index="${index}">${this._escapeHtml(this._label('previewEntry'))}</button>`
290
+ : '-';
291
+
292
+ return `
293
+ <tr>
294
+ <td>${index + 1}</td>
295
+ <td class="name">${this._escapeHtml(entry.name)}</td>
296
+ <td>${type}</td>
297
+ <td>${compressed}</td>
298
+ <td>${original}</td>
299
+ <td>${previewAction}</td>
300
+ </tr>`;
301
+ }).join('');
302
+
303
+ this._content.innerHTML = `
304
+ <table class="vg-filepreview-zip-modal__table">
305
+ <thead>
306
+ <tr>
307
+ <th>#</th>
308
+ <th>${this._escapeHtml(this._label('tableName'))}</th>
309
+ <th>${this._escapeHtml(this._label('tableType'))}</th>
310
+ <th>${this._escapeHtml(this._label('tablePacked'))}</th>
311
+ <th>${this._escapeHtml(this._label('tableSize'))}</th>
312
+ <th>${this._escapeHtml(this._label('tablePreview'))}</th>
313
+ </tr>
314
+ </thead>
315
+ <tbody>
316
+ ${rows}
317
+ </tbody>
318
+ </table>`;
319
+ this._bindEntriesPreview();
320
+ }
321
+
322
+ _label(key, fallback = '') {
323
+ const value = this._labels?.[key];
324
+ return typeof value === 'string' && value.trim() ? value : fallback;
325
+ }
326
+
327
+ _resolveErrorText(error) {
328
+ const message = String(error?.message || '').trim();
329
+ if (!message) {
330
+ return this._label('unknownError');
331
+ }
332
+
333
+ if (message === 'invalid_zip') {
334
+ return this._label('invalidZip');
335
+ }
336
+
337
+ if (message === 'central_directory_not_found') {
338
+ return this._label('centralDirectoryNotFound');
339
+ }
340
+
341
+ return message;
342
+ }
343
+
344
+ _formatSize(bytes) {
345
+ if (!Number.isFinite(bytes) || bytes < 0) {
346
+ return '-';
347
+ }
348
+
349
+ const units = ['B', 'KB', 'MB', 'GB'];
350
+ let value = bytes;
351
+ let unitIndex = 0;
352
+ while (value >= 1024 && unitIndex < units.length - 1) {
353
+ value /= 1024;
354
+ unitIndex += 1;
355
+ }
356
+
357
+ const digits = value >= 100 || unitIndex === 0 ? 0 : 2;
358
+ return `${value.toFixed(digits)} ${units[unitIndex]}`;
359
+ }
360
+
361
+ _escapeHtml(value) {
362
+ return String(value)
363
+ .replace(/&/g, '&amp;')
364
+ .replace(/</g, '&lt;')
365
+ .replace(/>/g, '&gt;')
366
+ .replace(/"/g, '&quot;')
367
+ .replace(/'/g, '&#39;');
368
+ }
369
+
370
+ _bindEntriesPreview() {
371
+ if (!this._content || this._content.hasAttribute('data-vg-filepreview-zip-preview-bind')) {
372
+ return;
373
+ }
374
+
375
+ this._content.setAttribute('data-vg-filepreview-zip-preview-bind', 'true');
376
+ this._content.addEventListener('click', (event) => {
377
+ const button = event.target.closest('[data-entry-index]');
378
+ if (!button) {
379
+ return;
380
+ }
381
+
382
+ event.preventDefault();
383
+ const index = Number(button.getAttribute('data-entry-index'));
384
+ if (!Number.isFinite(index) || index < 0 || index >= this._entries.length) {
385
+ return;
386
+ }
387
+ this._previewEntry(index);
388
+ });
389
+ }
390
+
391
+ _canPreviewEntry(entry) {
392
+ if (!entry || entry.isDirectory || !entry.name) {
393
+ return false;
394
+ }
395
+
396
+ const ext = this._extractExtension(entry.name);
397
+ const allowed = new Set([
398
+ '.txt', '.md', '.json', '.xml', '.yml', '.yaml', '.csv', '.log',
399
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp'
400
+ ]);
401
+ return allowed.has(ext);
402
+ }
403
+
404
+ async _previewEntry(index) {
405
+ if (!this._content) {
406
+ return;
407
+ }
408
+
409
+ const entry = this._entries[index];
410
+ if (!entry || !this._canPreviewEntry(entry)) {
411
+ return;
412
+ }
413
+
414
+ let panel = this._content.querySelector('.vg-filepreview-zip-modal__entry-preview');
415
+ if (!panel) {
416
+ panel = document.createElement('div');
417
+ panel.className = 'vg-filepreview-zip-modal__entry-preview';
418
+ this._content.appendChild(panel);
419
+ }
420
+
421
+ panel.innerHTML = `<div class="vg-filepreview-zip-modal__entry-preview-loading">${this._escapeHtml(this._label('loadingEntry'))}</div>`;
422
+
423
+ try {
424
+ const uint8 = await this._extractEntryBytes(entry);
425
+ const ext = this._extractExtension(entry.name);
426
+
427
+ if (this._isImageExtension(ext)) {
428
+ const blob = new Blob([uint8.buffer], { type: this._mimeByExtension(ext) });
429
+ const url = URL.createObjectURL(blob);
430
+ panel.innerHTML = `
431
+ <div class="vg-filepreview-zip-modal__entry-preview-title">${this._escapeHtml(entry.name)}</div>
432
+ <img class="vg-filepreview-zip-modal__entry-preview-image" src="${url}" alt="${this._escapeHtml(entry.name)}" />`;
433
+ setTimeout(() => URL.revokeObjectURL(url), 2000);
434
+ return;
435
+ }
436
+
437
+ const decoder = new TextDecoder('utf-8', { fatal: false });
438
+ const text = decoder.decode(uint8);
439
+ const safeText = this._escapeHtml(text || this._label('emptyFileInArchive'));
440
+ panel.innerHTML = `
441
+ <div class="vg-filepreview-zip-modal__entry-preview-title">${this._escapeHtml(entry.name)}</div>
442
+ <pre class="vg-filepreview-zip-modal__entry-preview-text">${safeText}</pre>`;
443
+ } catch (error) {
444
+ panel.innerHTML = `<div class="vg-filepreview-zip-modal__entry-preview-error">${this._escapeHtml(this._label('cannotPreviewEntry'))}: ${this._escapeHtml(error?.message || this._label('unknownError'))}</div>`;
445
+ }
446
+ }
447
+
448
+ async _extractEntryBytes(entry) {
449
+ if (!this._arrayBuffer) {
450
+ throw new Error(this._label('archiveBufferMissing'));
451
+ }
452
+
453
+ const view = new DataView(this._arrayBuffer);
454
+ const offset = Number(entry.localHeaderOffset || 0);
455
+ if (offset + 30 > view.byteLength) {
456
+ throw new Error(this._label('entryCorrupted'));
457
+ }
458
+
459
+ const signature = view.getUint32(offset, true);
460
+ if (signature !== 0x04034b50) {
461
+ throw new Error(this._label('entryCorrupted'));
462
+ }
463
+
464
+ const nameLen = view.getUint16(offset + 26, true);
465
+ const extraLen = view.getUint16(offset + 28, true);
466
+ const dataStart = offset + 30 + nameLen + extraLen;
467
+ const dataEnd = dataStart + Number(entry.compressedSize || 0);
468
+ if (dataEnd > view.byteLength) {
469
+ throw new Error(this._label('entryCorrupted'));
470
+ }
471
+
472
+ const compressed = new Uint8Array(this._arrayBuffer.slice(dataStart, dataEnd));
473
+ const method = Number(entry.compressionMethod || 0);
474
+
475
+ if (method === 0) {
476
+ return compressed;
477
+ }
478
+
479
+ if (method === 8) {
480
+ if (typeof DecompressionStream === 'undefined') {
481
+ throw new Error(this._label('deflateNotSupported'));
482
+ }
483
+
484
+ const stream = new Blob([compressed]).stream().pipeThrough(new DecompressionStream('deflate-raw'));
485
+ const decompressed = await new Response(stream).arrayBuffer();
486
+ return new Uint8Array(decompressed);
487
+ }
488
+
489
+ throw new Error(this._label('compressionNotSupported'));
490
+ }
491
+
492
+ _extractExtension(name) {
493
+ const value = String(name || '').toLowerCase();
494
+ const clean = value.split('/').pop() || '';
495
+ if (!clean.includes('.')) {
496
+ return '';
497
+ }
498
+ return `.${clean.split('.').pop() || ''}`;
499
+ }
500
+
501
+ _isImageExtension(ext) {
502
+ return new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp']).has(ext);
503
+ }
504
+
505
+ _mimeByExtension(ext) {
506
+ const map = {
507
+ '.png': 'image/png',
508
+ '.jpg': 'image/jpeg',
509
+ '.jpeg': 'image/jpeg',
510
+ '.gif': 'image/gif',
511
+ '.webp': 'image/webp',
512
+ '.svg': 'image/svg+xml',
513
+ '.bmp': 'image/bmp'
514
+ };
515
+ return map[ext] || 'application/octet-stream';
516
+ }
517
+
518
+ }
519
+
520
+ ZipModal._cache = new Map();
521
+
522
+ export default ZipModal;