vgapp 1.1.2 → 1.1.4

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 (55) hide show
  1. package/CHANGELOG.md +23 -5
  2. package/README.md +19 -16
  3. package/agents.md +6 -0
  4. package/app/langs/en/buttons.json +17 -2
  5. package/app/langs/en/messages.json +36 -1
  6. package/app/langs/ru/buttons.json +17 -2
  7. package/app/langs/ru/messages.json +69 -34
  8. package/app/modules/module-fn.js +15 -9
  9. package/app/modules/vgfilepreview/index.js +3 -0
  10. package/app/modules/vgfilepreview/js/i18n.js +56 -0
  11. package/app/modules/vgfilepreview/js/renderers/image-modal.js +145 -0
  12. package/app/modules/vgfilepreview/js/renderers/image.js +92 -0
  13. package/app/modules/vgfilepreview/js/renderers/index.js +19 -0
  14. package/app/modules/vgfilepreview/js/renderers/office-modal.js +168 -0
  15. package/app/modules/vgfilepreview/js/renderers/office.js +79 -0
  16. package/app/modules/vgfilepreview/js/renderers/pdf-modal.js +260 -0
  17. package/app/modules/vgfilepreview/js/renderers/pdf.js +76 -0
  18. package/app/modules/vgfilepreview/js/renderers/playlist.js +71 -0
  19. package/app/modules/vgfilepreview/js/renderers/text-modal.js +343 -0
  20. package/app/modules/vgfilepreview/js/renderers/text.js +83 -0
  21. package/app/modules/vgfilepreview/js/renderers/video-modal.js +272 -0
  22. package/app/modules/vgfilepreview/js/renderers/video.js +80 -0
  23. package/app/modules/vgfilepreview/js/renderers/zip-modal.js +522 -0
  24. package/app/modules/vgfilepreview/js/renderers/zip.js +89 -0
  25. package/app/modules/vgfilepreview/js/vgfilepreview.js +594 -0
  26. package/app/modules/vgfilepreview/readme.md +68 -0
  27. package/app/modules/vgfilepreview/scss/_variables.scss +113 -0
  28. package/app/modules/vgfilepreview/scss/vgfilepreview.scss +460 -0
  29. package/app/modules/vgfiles/js/base.js +463 -175
  30. package/app/modules/vgfiles/js/droppable.js +260 -260
  31. package/app/modules/vgfiles/js/render.js +153 -153
  32. package/app/modules/vgfiles/js/vgfiles.js +41 -29
  33. package/app/modules/vgfiles/readme.md +116 -217
  34. package/app/modules/vgfiles/scss/_variables.scss +18 -10
  35. package/app/modules/vgfiles/scss/vgfiles.scss +153 -59
  36. package/app/modules/vgformsender/js/vgformsender.js +13 -13
  37. package/app/modules/vgmodal/js/vgmodal.js +12 -0
  38. package/app/modules/vgnav/js/vgnav.js +135 -135
  39. package/app/modules/vgnav/readme.md +67 -67
  40. package/app/modules/vgnestable/README.md +307 -307
  41. package/app/modules/vgnestable/scss/_variables.scss +60 -60
  42. package/app/modules/vgnestable/scss/vgnestable.scss +163 -163
  43. package/app/modules/vgselect/js/vgselect.js +39 -39
  44. package/app/modules/vgselect/scss/vgselect.scss +22 -22
  45. package/app/modules/vgspy/readme.md +28 -28
  46. package/app/utils/js/components/audio-metadata.js +240 -0
  47. package/app/utils/js/components/file-icon.js +109 -0
  48. package/app/utils/js/components/file-preview.js +304 -0
  49. package/app/utils/js/components/sanitize.js +150 -150
  50. package/build/vgapp.css +1 -1
  51. package/build/vgapp.css.map +1 -1
  52. package/build/vgapp.js.map +1 -1
  53. package/index.js +1 -0
  54. package/index.scss +9 -6
  55. package/package.json +1 -1
@@ -0,0 +1,343 @@
1
+ import VGModal from "../../../vgmodal";
2
+
3
+ class TextModal {
4
+ constructor() {
5
+ this._modalId = 'vg-filepreview-text-modal';
6
+ this._abortController = null;
7
+ this._labels = {};
8
+ }
9
+
10
+ static getInstance() {
11
+ if (!TextModal._instance) {
12
+ TextModal._instance = new TextModal();
13
+ }
14
+
15
+ return TextModal._instance;
16
+ }
17
+
18
+ open(payload = {}) {
19
+ const src = String(payload.src || '').trim();
20
+ if (!src) {
21
+ return;
22
+ }
23
+
24
+ this._ensureModal();
25
+ if (!this._modal || !this._content) {
26
+ return;
27
+ }
28
+
29
+ this._labels = payload?.labels && typeof payload.labels === 'object' ? payload.labels : {};
30
+ const defaultTitle = String(payload.defaultTitle || '').trim();
31
+ const title = String(payload.title || '').trim();
32
+ const ext = String(payload.ext || '').toLowerCase();
33
+ this._title.textContent = title || defaultTitle;
34
+ this._content.textContent = this._label('loading');
35
+ this._content.classList.remove('is-markdown');
36
+ this._modal.show();
37
+
38
+ this._loadText(src, {ext});
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: '600px',
65
+ height: '',
66
+ },
67
+ animation: {
68
+ enable: false
69
+ }
70
+ };
71
+
72
+ const existed = document.getElementById(this._modalId);
73
+ if (existed) {
74
+ this._root = existed;
75
+ this._modal = VGModal.getOrCreateInstance(existed, params);
76
+ this._bindElements(existed);
77
+ this._bindLifecycle(existed);
78
+ return;
79
+ }
80
+
81
+ this._modal = VGModal.build(this._modalId, params, (modalInstance) => {
82
+ const element = modalInstance._element;
83
+ this._root = element;
84
+ element.classList.add('vg-filepreview-text-modal');
85
+
86
+ const body = element.querySelector('.vg-modal-body');
87
+ const content = element.querySelector('.vg-modal-content');
88
+ if (!body || !content) {
89
+ return;
90
+ }
91
+
92
+ body.classList.add('vg-filepreview-text-modal__body');
93
+ body.innerHTML = '';
94
+
95
+ let header = element.querySelector('.vg-modal-header');
96
+ if (!header) {
97
+ header = document.createElement('div');
98
+ header.className = 'vg-modal-header';
99
+ content.prepend(header);
100
+ }
101
+
102
+ this._title = document.createElement('div');
103
+ this._title.className = 'vg-modal-title';
104
+ this._title.textContent = '';
105
+
106
+ this._content = document.createElement('pre');
107
+ this._content.className = 'vg-filepreview-text-modal__content';
108
+ this._content.textContent = '';
109
+
110
+ header.appendChild(this._title);
111
+ body.appendChild(this._content);
112
+
113
+ this._bindLifecycle(element);
114
+ });
115
+ }
116
+
117
+ _bindElements(root) {
118
+ this._title = root.querySelector('.vg-filepreview-text-modal__title');
119
+ this._content = root.querySelector('.vg-filepreview-text-modal__content');
120
+ }
121
+
122
+ _bindLifecycle(root) {
123
+ if (!root || root.hasAttribute('data-vg-filepreview-text-lifecycle-bind')) {
124
+ return;
125
+ }
126
+
127
+ root.setAttribute('data-vg-filepreview-text-lifecycle-bind', 'true');
128
+ root.addEventListener('vg.modal.hidden', () => {
129
+ this._destroyModal();
130
+ });
131
+ }
132
+
133
+ _loadText(src, options = {}) {
134
+ const ext = String(options.ext || '').toLowerCase();
135
+ const cacheKey = `${src}|${ext}`;
136
+
137
+ if (TextModal._cache.has(cacheKey)) {
138
+ const cached = TextModal._cache.get(cacheKey);
139
+ this._renderLoadedText(cached, ext);
140
+ return;
141
+ }
142
+
143
+ if (this._abortController) {
144
+ this._abortController.abort();
145
+ }
146
+
147
+ this._abortController = new AbortController();
148
+
149
+ fetch(src, {
150
+ method: 'GET',
151
+ signal: this._abortController.signal
152
+ })
153
+ .then((response) => {
154
+ if (!response.ok) {
155
+ throw new Error(`HTTP ${response.status}`);
156
+ }
157
+
158
+ return response.text();
159
+ })
160
+ .then((text) => {
161
+ const normalized = text || '';
162
+ TextModal._cache.set(cacheKey, normalized);
163
+ this._renderLoadedText(normalized, ext);
164
+ })
165
+ .catch((error) => {
166
+ if (!this._content) {
167
+ return;
168
+ }
169
+
170
+ if (error?.name === 'AbortError') {
171
+ return;
172
+ }
173
+
174
+ this._content.classList.remove('is-markdown');
175
+ this._content.textContent = `${this._label('cannotOpen')}: ${error?.message || this._label('unknownError')}`;
176
+ });
177
+ }
178
+
179
+ _renderLoadedText(text, ext) {
180
+ if (!this._content) {
181
+ return;
182
+ }
183
+
184
+ const normalized = String(text || '');
185
+ if (ext === '.md') {
186
+ this._content.classList.add('is-markdown');
187
+ this._content.innerHTML = this._renderMarkdown(normalized);
188
+ } else {
189
+ this._content.classList.remove('is-markdown');
190
+ this._content.textContent = normalized || this._label('emptyFile');
191
+ }
192
+ }
193
+
194
+ _destroyModal() {
195
+ if (this._abortController) {
196
+ this._abortController.abort();
197
+ this._abortController = null;
198
+ }
199
+
200
+ if (this._modal && typeof this._modal.dispose === 'function') {
201
+ this._modal.dispose();
202
+ }
203
+
204
+ if (this._root && this._root.parentNode) {
205
+ this._root.parentNode.removeChild(this._root);
206
+ }
207
+
208
+ this._root = null;
209
+ this._modal = null;
210
+ this._title = null;
211
+ this._content = null;
212
+ this._labels = null;
213
+ }
214
+
215
+ _label(key, fallback = '') {
216
+ const value = this._labels?.[key];
217
+ return typeof value === 'string' && value.trim() ? value : fallback;
218
+ }
219
+
220
+ _renderMarkdown(markdownText) {
221
+ const escapeHtml = (value) => String(value)
222
+ .replace(/&/g, '&')
223
+ .replace(/</g, '&lt;')
224
+ .replace(/>/g, '&gt;')
225
+ .replace(/"/g, '&quot;')
226
+ .replace(/'/g, '&#39;');
227
+
228
+ const inline = (line) => {
229
+ let result = escapeHtml(line);
230
+ result = result.replace(/`([^`]+)`/g, '<code>$1</code>');
231
+ result = result.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
232
+ result = result.replace(/\*([^*]+)\*/g, '<em>$1</em>');
233
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
234
+ const safeHref = this._sanitizeUrl(href);
235
+ return `<a href="${safeHref}" target="_blank" rel="noopener noreferrer">${text}</a>`;
236
+ });
237
+ return result;
238
+ };
239
+
240
+ const lines = String(markdownText || '').split(/\r?\n/);
241
+ const html = [];
242
+ let inList = false;
243
+ let inCode = false;
244
+
245
+ lines.forEach((rawLine) => {
246
+ const line = rawLine || '';
247
+
248
+ if (line.trim().startsWith('```')) {
249
+ if (!inCode) {
250
+ inCode = true;
251
+ html.push('<pre><code>');
252
+ } else {
253
+ inCode = false;
254
+ html.push('</code></pre>');
255
+ }
256
+ return;
257
+ }
258
+
259
+ if (inCode) {
260
+ html.push(`${escapeHtml(line)}\n`);
261
+ return;
262
+ }
263
+
264
+ if (!line.trim()) {
265
+ if (inList) {
266
+ inList = false;
267
+ html.push('</ul>');
268
+ }
269
+ return;
270
+ }
271
+
272
+ const heading = line.match(/^(#{1,6})\s+(.+)$/);
273
+ if (heading) {
274
+ if (inList) {
275
+ inList = false;
276
+ html.push('</ul>');
277
+ }
278
+
279
+ const level = heading[1].length;
280
+ html.push(`<h${level}>${inline(heading[2])}</h${level}>`);
281
+ return;
282
+ }
283
+
284
+ const listItem = line.match(/^[-*]\s+(.+)$/);
285
+ if (listItem) {
286
+ if (!inList) {
287
+ inList = true;
288
+ html.push('<ul>');
289
+ }
290
+ html.push(`<li>${inline(listItem[1])}</li>`);
291
+ return;
292
+ }
293
+
294
+ if (inList) {
295
+ inList = false;
296
+ html.push('</ul>');
297
+ }
298
+
299
+ html.push(`<p>${inline(line)}</p>`);
300
+ });
301
+
302
+ if (inList) {
303
+ html.push('</ul>');
304
+ }
305
+ if (inCode) {
306
+ html.push('</code></pre>');
307
+ }
308
+
309
+ return html.join('') || `<p>${this._escapeHtml(this._label('emptyFile'))}</p>`;
310
+ }
311
+
312
+ _escapeHtml(value) {
313
+ return String(value)
314
+ .replace(/&/g, '&amp;')
315
+ .replace(/</g, '&lt;')
316
+ .replace(/>/g, '&gt;')
317
+ .replace(/"/g, '&quot;')
318
+ .replace(/'/g, '&#39;');
319
+ }
320
+
321
+ _sanitizeUrl(url) {
322
+ const value = String(url || '').trim();
323
+ if (!value) {
324
+ return '#';
325
+ }
326
+
327
+ try {
328
+ const parsed = new URL(value, window.location.origin);
329
+ if (['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
330
+ return parsed.href;
331
+ }
332
+ } catch {
333
+ return '#';
334
+ }
335
+
336
+ return '#';
337
+ }
338
+
339
+ }
340
+
341
+ TextModal._cache = new Map();
342
+
343
+ export default TextModal;
@@ -0,0 +1,83 @@
1
+ import TextModal from "./text-modal";
2
+
3
+ const TEXT_EXTENSIONS = new Set([
4
+ '.txt',
5
+ '.md',
6
+ '.csv',
7
+ '.json',
8
+ '.xml',
9
+ '.yml',
10
+ '.yaml',
11
+ '.log',
12
+ '.ini',
13
+ '.conf',
14
+ '.env'
15
+ ]);
16
+
17
+ class TextFilePreviewRenderer {
18
+ constructor() {
19
+ this.name = 'text';
20
+ this._modal = TextModal.getInstance();
21
+ }
22
+
23
+ canRender(context = {}) {
24
+ const ext = String(context?.fileMeta?.ext || '').toLowerCase();
25
+ return TEXT_EXTENSIONS.has(ext);
26
+ }
27
+
28
+ render(context = {}) {
29
+ const container = context?.previewContainer;
30
+ const nameOnly = Boolean(context?.ui?.nameOnly);
31
+ const i18n = context?.i18n;
32
+
33
+ const src = context?.fileUrl?.href || context?.filePath || '';
34
+ if (!src) {
35
+ return false;
36
+ }
37
+
38
+ const openText = (event) => {
39
+ if (event) {
40
+ event.preventDefault();
41
+ }
42
+
43
+ this._modal.open({
44
+ src,
45
+ title: context?.fileMeta?.name || i18n?.message('text_title') || '',
46
+ defaultTitle: i18n?.message('text_title') || '',
47
+ ext: context?.fileMeta?.ext || '',
48
+ labels: {
49
+ loading: i18n?.message('loading_text') || '',
50
+ cannotOpen: i18n?.message('cannot_open_file') || '',
51
+ unknownError: i18n?.message('unknown_error') || '',
52
+ emptyFile: i18n?.message('empty_file') || ''
53
+ }
54
+ });
55
+ };
56
+
57
+ const titleLink = context?.element?.querySelector('.name');
58
+ if (titleLink && !titleLink.hasAttribute('data-vg-filepreview-text-bind')) {
59
+ titleLink.setAttribute('data-vg-filepreview-text-bind', 'true');
60
+ titleLink.classList.add('is-preview-action');
61
+ titleLink.addEventListener('click', openText);
62
+ }
63
+
64
+ if (nameOnly) {
65
+ return Boolean(titleLink);
66
+ }
67
+
68
+ if (!container) {
69
+ return false;
70
+ }
71
+
72
+ const trigger = document.createElement('button');
73
+ trigger.type = 'button';
74
+ trigger.className = 'vg-filepreview-text-trigger';
75
+ trigger.textContent = i18n?.button('open_text') || '';
76
+ trigger.addEventListener('click', openText);
77
+ container.appendChild(trigger);
78
+
79
+ return true;
80
+ }
81
+ }
82
+
83
+ export default TextFilePreviewRenderer;
@@ -0,0 +1,272 @@
1
+ import VGModal from "../../../vgmodal";
2
+
3
+ class VideoModal {
4
+ constructor() {
5
+ this._modalId = 'vg-filepreview-video-modal';
6
+ this._playlist = [];
7
+ this._currentIndex = -1;
8
+ this._labels = {};
9
+ this._onKeyDown = (event) => this._handleHotkeys(event);
10
+ }
11
+
12
+ static getInstance() {
13
+ if (!VideoModal._instance) {
14
+ VideoModal._instance = new VideoModal();
15
+ }
16
+
17
+ return VideoModal._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._video) {
28
+ return;
29
+ }
30
+
31
+ const playlist = payload?.playlist && typeof payload.playlist === 'object' ? payload.playlist : null;
32
+ this._labels = payload?.labels && typeof payload.labels === 'object' ? payload.labels : {};
33
+ if (playlist?.tracks?.length) {
34
+ this._playlist = playlist.tracks;
35
+ const currentBySrc = this._playlist.findIndex((track) => String(track.src || '').trim() === src);
36
+ this._currentIndex = currentBySrc >= 0 ? currentBySrc : Number(playlist.currentIndex || 0);
37
+ } else {
38
+ this._playlist = [{ src, title: String(payload.title || '').trim() }];
39
+ this._currentIndex = 0;
40
+ }
41
+
42
+ const defaultTitle = String(payload.defaultTitle || '').trim();
43
+ const title = String(payload.title || '').trim();
44
+ this._title.textContent = title || defaultTitle;
45
+ this._syncNavigation();
46
+ this._video.src = src;
47
+ this._video.load();
48
+
49
+ this._modal.show();
50
+ this._video.play().catch(() => {});
51
+ }
52
+
53
+ close() {
54
+ if (!this._modal) {
55
+ return;
56
+ }
57
+
58
+ this._modal.hide();
59
+ this._stop();
60
+ }
61
+
62
+ _ensureModal() {
63
+ if (this._modal && this._root) {
64
+ return;
65
+ }
66
+
67
+ this._initModal();
68
+ }
69
+
70
+ _initModal() {
71
+ const params = {
72
+ centered: true,
73
+ dismiss: true,
74
+ backdrop: true,
75
+ keyboard: true,
76
+ sizes: {
77
+ width: 'fit-content',
78
+ },
79
+ animation: {
80
+ enable: false
81
+ }
82
+ };
83
+
84
+ const existed = document.getElementById(this._modalId);
85
+ if (existed) {
86
+ this._root = existed;
87
+ this._modal = VGModal.getOrCreateInstance(existed, params);
88
+ this._bindElements(existed);
89
+ this._bindEvents(existed);
90
+ return;
91
+ }
92
+
93
+ this._modal = VGModal.build(this._modalId, params, (modalInstance) => {
94
+ const element = modalInstance._element;
95
+ this._root = element;
96
+ element.classList.add('vg-filepreview-video-modal');
97
+
98
+ const body = element.querySelector('.vg-modal-body');
99
+ const content = element.querySelector('.vg-modal-content');
100
+ if (!body || !content) {
101
+ return;
102
+ }
103
+
104
+ body.classList.add('vg-filepreview-image-modal__body');
105
+ body.innerHTML = '';
106
+
107
+ let header = element.querySelector('.vg-modal-header');
108
+ if (!header) {
109
+ header = document.createElement('div');
110
+ header.className = 'vg-modal-header';
111
+ content.prepend(header);
112
+ }
113
+
114
+ this._title = document.createElement('div');
115
+ this._title.className = 'vg-modal-title';
116
+ this._title.textContent = '';
117
+
118
+ const navigation = document.createElement('div');
119
+ navigation.className = 'vg-filepreview-video-modal__navigation';
120
+
121
+ this._prevButton = document.createElement('button');
122
+ this._prevButton.type = 'button';
123
+ this._prevButton.className = 'vg-filepreview-video-modal__nav-btn';
124
+ this._prevButton.addEventListener('click', () => this._goPrev());
125
+
126
+ this._nextButton = document.createElement('button');
127
+ this._nextButton.type = 'button';
128
+ this._nextButton.className = 'vg-filepreview-video-modal__nav-btn';
129
+ this._nextButton.addEventListener('click', () => this._goNext());
130
+
131
+ navigation.appendChild(this._prevButton);
132
+ navigation.appendChild(this._nextButton);
133
+
134
+ this._video = document.createElement('video');
135
+ this._video.className = 'vg-filepreview-video-modal__video';
136
+ this._video.controls = true;
137
+ this._video.preload = 'metadata';
138
+ this._video.playsInline = true;
139
+ this._video.addEventListener('ended', () => this._goNext(true));
140
+
141
+ header.appendChild(this._title);
142
+ //header.appendChild(navigation);
143
+ body.appendChild(this._video);
144
+
145
+ this._bindEvents(element);
146
+ });
147
+ }
148
+
149
+ _bindElements(root) {
150
+ this._title = root.querySelector('.vg-filepreview-video-modal__title');
151
+ this._video = root.querySelector('.vg-filepreview-video-modal__video');
152
+ }
153
+
154
+ _bindEvents(root) {
155
+ if (!root || root.hasAttribute('data-vg-filepreview-video-bind')) {
156
+ return;
157
+ }
158
+
159
+ root.setAttribute('data-vg-filepreview-video-bind', 'true');
160
+ document.addEventListener('keydown', this._onKeyDown);
161
+ root.addEventListener('vg.modal.hidden', () => {
162
+ this._stop();
163
+ this._destroyModal();
164
+ });
165
+ }
166
+
167
+ _stop() {
168
+ if (!this._video) {
169
+ return;
170
+ }
171
+
172
+ this._video.pause();
173
+ this._video.currentTime = 0;
174
+ }
175
+
176
+ _destroyModal() {
177
+ document.removeEventListener('keydown', this._onKeyDown);
178
+ if (this._modal && typeof this._modal.dispose === 'function') {
179
+ this._modal.dispose();
180
+ }
181
+
182
+ if (this._root && this._root.parentNode) {
183
+ this._root.parentNode.removeChild(this._root);
184
+ }
185
+
186
+ this._root = null;
187
+ this._modal = null;
188
+ this._title = null;
189
+ this._prevButton = null;
190
+ this._nextButton = null;
191
+ this._video = null;
192
+ this._playlist = [];
193
+ this._currentIndex = -1;
194
+ this._labels = {};
195
+ }
196
+
197
+ _syncNavigation() {
198
+ if (!this._prevButton || !this._nextButton) {
199
+ return;
200
+ }
201
+
202
+ this._prevButton.textContent = this._labels.prev || '';
203
+ this._nextButton.textContent = this._labels.next || '';
204
+ const hasPlaylist = this._playlist.length > 1;
205
+ this._prevButton.disabled = !hasPlaylist;
206
+ this._nextButton.disabled = !hasPlaylist;
207
+ }
208
+
209
+ _goPrev() {
210
+ if (!this._playlist.length) {
211
+ return;
212
+ }
213
+
214
+ this._currentIndex = this._currentIndex <= 0 ? this._playlist.length - 1 : this._currentIndex - 1;
215
+ this._openCurrentTrack();
216
+ }
217
+
218
+ _goNext(fromEnded = false) {
219
+ if (!this._playlist.length) {
220
+ return;
221
+ }
222
+
223
+ if (this._playlist.length === 1 && !fromEnded) {
224
+ return;
225
+ }
226
+
227
+ this._currentIndex = (this._currentIndex + 1) % this._playlist.length;
228
+ this._openCurrentTrack();
229
+ }
230
+
231
+ _openCurrentTrack() {
232
+ const current = this._playlist[this._currentIndex];
233
+ if (!current?.src) {
234
+ return;
235
+ }
236
+
237
+ this.open({
238
+ src: current.src,
239
+ title: current.title || '',
240
+ defaultTitle: current.title || '',
241
+ playlist: {
242
+ tracks: this._playlist,
243
+ currentIndex: this._currentIndex
244
+ },
245
+ labels: this._labels
246
+ });
247
+ }
248
+
249
+ _handleHotkeys(event) {
250
+ if (!this._root || !this._root.classList.contains('show')) {
251
+ return;
252
+ }
253
+
254
+ const tag = String(event?.target?.tagName || '').toUpperCase();
255
+ if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) {
256
+ return;
257
+ }
258
+
259
+ if (event.key === 'ArrowLeft') {
260
+ event.preventDefault();
261
+ this._goPrev();
262
+ }
263
+
264
+ if (event.key === 'ArrowRight') {
265
+ event.preventDefault();
266
+ this._goNext();
267
+ }
268
+ }
269
+
270
+ }
271
+
272
+ export default VideoModal;