vgapp 1.0.1 → 1.0.3

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.
@@ -22,7 +22,8 @@
22
22
  "titles": "Deleting files",
23
23
  "title": "Deleting file",
24
24
  "descriptions": "Do you really want to delete all uploaded files from the server?",
25
- "description": "It will not be possible to return it"
25
+ "description": "It will not be possible to return it",
26
+ "drop-active": "Release to upload"
26
27
  },
27
28
  "alert": {
28
29
  "title": "Default header",
@@ -32,4 +33,4 @@
32
33
  "select": {
33
34
  "loading": "Loading"
34
35
  }
35
- }
36
+ }
@@ -1,35 +1,36 @@
1
- {
2
- "errors": {
3
- "went_wrong": "Что-то пошло не так, повторите позже",
4
- "400": "Неверный запрос",
5
- "401": "Не авторизован",
6
- "403": "Запрещено",
7
- "404": "Не найдено",
8
- "413": "Слишком большой запрос",
9
- "419": "Проблемы с токеном CSRF",
10
- "422": "Неверный запрос",
11
- "500": "Внутренняя ошибка сервера",
12
- "504": "Превышено время ожидания"
13
- },
14
- "form-sender": {
15
- "bootstrap_not_found": "VGApp не удалось найти bootstrap, модалки не будут закрыты, попробуйте сделать это через коллбек afterSend."
16
- },
17
- "files": {
18
- "is-count": "Превышен лимит по количеству файлов",
19
- "is-sizes": "Превышен размер файл",
20
- "is-types": "Недопустимый тип файла",
21
- "is-total-size": "Превышен максимально разрешённый размер для выбранных файлов",
22
- "titles": "Удаление файлов",
23
- "title": "Удаление файлa",
24
- "descriptions": "Вы действительно хотите удалить все загруженные файлы с сервера?",
25
- "description": "Вернуть будет невозможно"
26
- },
27
- "alert": {
28
- "title": "Заголовок по умолчанию",
29
- "description": "Описание текущего действия",
30
- "reason": "Алерт уже открыт"
31
- },
32
- "select": {
33
- "loading": "Загрузка"
34
- }
35
- }
1
+ {
2
+ "errors": {
3
+ "went_wrong": "Что-то пошло не так, повторите позже",
4
+ "400": "Неверный запрос",
5
+ "401": "Не авторизован",
6
+ "403": "Запрещено",
7
+ "404": "Не найдено",
8
+ "413": "Слишком большой запрос",
9
+ "419": "Проблемы с токеном CSRF",
10
+ "422": "Неверный запрос",
11
+ "500": "Внутренняя ошибка сервера",
12
+ "504": "Превышено время ожидания"
13
+ },
14
+ "form-sender": {
15
+ "bootstrap_not_found": "VGApp не удалось найти bootstrap, модалки не будут закрыты, попробуйте сделать это через коллбек afterSend."
16
+ },
17
+ "files": {
18
+ "is-count": "Превышен лимит по количеству файлов",
19
+ "is-sizes": "Превышен размер файл",
20
+ "is-types": "Недопустимый тип файла",
21
+ "is-total-size": "Превышен максимально разрешённый размер для выбранных файлов",
22
+ "titles": "Удаление файлов",
23
+ "title": "Удаление файлa",
24
+ "descriptions": "Вы действительно хотите удалить все загруженные файлы с сервера?",
25
+ "description": "Вернуть будет невозможно",
26
+ "drop-active": "Отпустите, чтобы загрузить"
27
+ },
28
+ "alert": {
29
+ "title": "Заголовок по умолчанию",
30
+ "description": "Описание текущего действия",
31
+ "reason": "Алерт уже открыт"
32
+ },
33
+ "select": {
34
+ "loading": "Загрузка"
35
+ }
36
+ }
@@ -137,12 +137,12 @@ class VGFilesBase extends BaseModule {
137
137
  Boolean(this._params?.replace) &&
138
138
  isSingle;
139
139
 
140
- if (shouldReplaceOnSingle) {
141
- this.clear();
142
- this.append(filesArray, true);
143
- this._revokeUrls();
144
- this._cleanupFakeInputs();
145
- this._cleanupErrors();
140
+ if (shouldReplaceOnSingle) {
141
+ this.clear(false);
142
+ this.append(filesArray, true);
143
+ this._revokeUrls();
144
+ this._cleanupFakeInputs();
145
+ this._cleanupErrors();
146
146
 
147
147
  this._files = this._filterFiles(filesArray);
148
148
  if (this._params.prepend) this._files.reverse();
@@ -314,7 +314,8 @@ class VGFilesBase extends BaseModule {
314
314
 
315
315
  _parseTemplate() {
316
316
  const render = this._render;
317
- let tmpl = this.template;
317
+ const fallbackTemplate = '<li data-file="" class="file"><div class="file-image"></div><div class="file-custom"><div class="file-info"></div><div class="file-remove"></div></div></li>';
318
+ let tmpl = this.template || fallbackTemplate;
318
319
 
319
320
  if (render) {
320
321
  if (render.bufferTemplate) tmpl = render.bufferTemplate;
@@ -322,7 +323,12 @@ class VGFilesBase extends BaseModule {
322
323
 
323
324
  const temp = document.createElement('div');
324
325
  temp.innerHTML = tmpl;
325
- const liElement = temp.firstElementChild;
326
+ let liElement = temp.firstElementChild;
327
+
328
+ if (!liElement) {
329
+ temp.innerHTML = fallbackTemplate;
330
+ liElement = temp.firstElementChild;
331
+ }
326
332
  const liClasses = liElement.className || '';
327
333
  const liClassList = liClasses ? liClasses.split(' ').filter(cls => cls.trim() !== '') : [];
328
334
 
@@ -372,16 +378,11 @@ class VGFilesBase extends BaseModule {
372
378
  }
373
379
  }
374
380
 
375
- let parts = [];
376
- $itemsTemplate.forEach(tmpl => {
377
- if (tmpl.className === 'file-image') {
378
- parts.push(this._renderUIImage(file))
379
- } else if (tmpl.className === 'file-remove') {
380
- parts.push(this._renderUIDetach(file))
381
- } else {
382
- parts.push(tmpl.element.cloneNode(true));
383
- }
384
- });
381
+ let parts = [];
382
+ $itemsTemplate.forEach(tmpl => {
383
+ const part = this._renderTemplatePart(tmpl.element, file, null, { isDrop: true });
384
+ if (part) parts.push(part);
385
+ });
385
386
 
386
387
  const $li = this._tpl.li(
387
388
  { 'data-name': file.name, 'data-size': file.size, 'data-id': file.id || '', class: 'file ' + classes.join(' ') }, parts
@@ -393,8 +394,17 @@ class VGFilesBase extends BaseModule {
393
394
  $list.innerHTML = '';
394
395
  $list.appendChild(fragment);
395
396
 
396
- const $message = Selectors.find(`.${this._getClass('drop-message')}`, this._nodes.drop);
397
- if ($message) Classes.remove($message, 'show');
397
+ const $message = Selectors.find(`.${this._getClass('drop-message')}`, this._nodes.drop);
398
+ if ($message) {
399
+ if (files.length) Classes.add($message, 'has-files');
400
+ else Classes.remove($message, 'has-files');
401
+
402
+ const isSingle = Number(this._params?.limits?.count) === 1;
403
+ if (isSingle) {
404
+ if (files.length) Classes.remove($message, 'show');
405
+ else Classes.add($message, 'show');
406
+ }
407
+ }
398
408
 
399
409
  this._nodes.drop.appendChild($list);
400
410
  Classes.add(this._nodes.drop, 'active');
@@ -425,18 +435,11 @@ class VGFilesBase extends BaseModule {
425
435
  if (this._params.detach) classes.push('with-remove')
426
436
  if (this._params.sortable.enabled) classes.push('with-sortable');
427
437
 
428
- let parts = [];
429
- $itemsTemplate.forEach(tmpl => {
430
- if (tmpl.className === 'file-image') {
431
- parts.push(this._renderUIImage(file))
432
- } else if (tmpl.className === 'file-info') {
433
- parts.push(this._renderUIInfo(file, i))
434
- } else if (tmpl.className === 'file-remove') {
435
- parts.push(this._renderUIDetach(file))
436
- } else {
437
- parts.push(tmpl.element.cloneNode(true));
438
- }
439
- });
438
+ let parts = [];
439
+ $itemsTemplate.forEach(tmpl => {
440
+ const part = this._renderTemplatePart(tmpl.element, file, i);
441
+ if (part) parts.push(part);
442
+ });
440
443
 
441
444
  const $li = this._tpl.li(
442
445
  { 'data-name': file.name, 'data-size': file.size, 'data-type': file.type, 'data-id': file.id || '', class: 'file ' + classes.join(' ') + ' ' }, parts
@@ -448,6 +451,50 @@ class VGFilesBase extends BaseModule {
448
451
  Classes.add(this._nodes.info, 'show')
449
452
  }
450
453
 
454
+ _renderTemplatePart(element, file, index = null, options = {}) {
455
+ if (!element) return null;
456
+ const { isDrop = false } = options;
457
+
458
+ const classList = element?.classList;
459
+
460
+ if (classList?.contains('file-image')) return this._renderUIImage(file);
461
+ if (classList?.contains('file-info')) {
462
+ if (isDrop) return null;
463
+ return this._renderUIInfo(file, index);
464
+ }
465
+ if (classList?.contains('file-remove')) return this._wrapInFileCustom(this._renderUIDetach(file));
466
+
467
+ const $part = element.cloneNode(true);
468
+
469
+ this._replaceTemplateSlot($part, '.file-image', () => this._renderUIImage(file));
470
+ this._replaceTemplateSlot($part, '.file-info', () => isDrop ? null : this._renderUIInfo(file, index));
471
+ this._replaceTemplateSlot($part, '.file-remove', () => this._renderUIDetach(file));
472
+
473
+ return this._wrapInFileCustom($part);
474
+ }
475
+
476
+ _wrapInFileCustom(node) {
477
+ if (!node) return null;
478
+ if (node.classList?.contains('file-custom')) return node;
479
+
480
+ const wrapper = document.createElement('div');
481
+ wrapper.className = 'file-custom';
482
+ wrapper.appendChild(node);
483
+
484
+ return wrapper;
485
+ }
486
+
487
+ _replaceTemplateSlot(container, selector, renderer) {
488
+ const isMatchSelf = container?.matches && container.matches(selector);
489
+ const matched = isMatchSelf ? [container] : Array.from(container.querySelectorAll(selector));
490
+
491
+ matched.forEach((node) => {
492
+ const replacement = renderer();
493
+ if (replacement) node.replaceWith(replacement);
494
+ else node.remove();
495
+ });
496
+ }
497
+
451
498
  _renderUIDetach(file) {
452
499
  if (this._params.detach) {
453
500
  return this._tpl.div({ class: 'file-remove' }, [
@@ -539,12 +586,14 @@ class VGFilesBase extends BaseModule {
539
586
  Selectors.findAll('[data-vg-files="generated"]', this._element).forEach(el => el.remove());
540
587
  }
541
588
 
542
- clear() {
543
- this._revokeUrls();
544
- this._resetFileInput();
545
- this._cleanupFakeInputs();
546
- this._cleanupErrors();
547
- this._files = [];
589
+ clear(resetInput = true) {
590
+ this._revokeUrls();
591
+ if (resetInput) {
592
+ this._resetFileInput();
593
+ }
594
+ this._cleanupFakeInputs();
595
+ this._cleanupErrors();
596
+ this._files = [];
548
597
 
549
598
  if (this._nodes.info) {
550
599
  Classes.remove(this._nodes.info, 'show');
@@ -555,8 +604,15 @@ class VGFilesBase extends BaseModule {
555
604
  const $list = Selectors.find(`.${this._getClass('drop-list')}`, this._element);
556
605
  if ($list) $list.innerHTML = '';
557
606
 
558
- const $message = Selectors.find(`.${this._getClass('drop-message')}`, this._element);
559
- if ($message) Classes.add($message, 'show');
607
+ const $message = Selectors.find(`.${this._getClass('drop-message')}`, this._element);
608
+ if ($message) {
609
+ Classes.remove($message, 'has-files');
610
+
611
+ const isSingle = Number(this._params?.limits?.count) === 1;
612
+ if (isSingle) {
613
+ Classes.add($message, 'show');
614
+ }
615
+ }
560
616
 
561
617
  Classes.remove(this._nodes.drop, 'active');
562
618
  }
@@ -599,4 +655,4 @@ class VGFilesBase extends BaseModule {
599
655
  }
600
656
  }
601
657
 
602
- export default VGFilesBase;
658
+ export default VGFilesBase;
@@ -13,11 +13,11 @@ const CLASS_NAME_DROP_HOVER = 'drop-hover';
13
13
  const NAME = 'drop-and-drop';
14
14
  const NAME_KEY = 'vg.drop-and-drop';
15
15
 
16
- class VGFilesDroppable extends BaseModule {
17
- /**
18
- * @param {HTMLElement} element
19
- * @param {Object} params
20
- */
16
+ class VGFilesDroppable extends BaseModule {
17
+ /**
18
+ * @param {HTMLElement} element
19
+ * @param {Object} params
20
+ */
21
21
  constructor(element, params = {}) {
22
22
  super(element, params);
23
23
  this._element = element;
@@ -31,10 +31,15 @@ class VGFilesDroppable extends BaseModule {
31
31
  static get NAME() { return NAME; }
32
32
  static get NAME_KEY() { return NAME_KEY; }
33
33
 
34
- _init() {
35
- this._findInput();
36
- this._setupEvents();
37
- }
34
+ _init() {
35
+ VGFilesDroppable._instances.add(this);
36
+ if (this._params?.smartdrop) {
37
+ VGFilesDroppable._smartInstances.add(this);
38
+ this._bindGlobalEvents();
39
+ }
40
+ this._findInput();
41
+ this._setupEvents();
42
+ }
38
43
 
39
44
  /**
40
45
  * Поиск связанного input[type="file"] внутри или рядом с drop-зоной
@@ -53,8 +58,8 @@ class VGFilesDroppable extends BaseModule {
53
58
  /**
54
59
  * Настройка событий перетаскивания
55
60
  */
56
- _setupEvents() {
57
- if (!isElement(this._element)) return;
61
+ _setupEvents() {
62
+ if (!isElement(this._element)) return;
58
63
 
59
64
  const isSortableDrag = (e) => {
60
65
  // 1) если sortable реально активен — на элементе есть класс dragging
@@ -64,70 +69,76 @@ class VGFilesDroppable extends BaseModule {
64
69
  let plain = '';
65
70
  try { plain = e.dataTransfer?.getData?.('text/plain') || ''; } catch (_) {}
66
71
  return plain === 'vgsortable';
67
- };
72
+ };
68
73
 
69
- EventHandler.on(this._element, 'dragover', (e) => {
70
- e.preventDefault();
71
- e.stopPropagation();
74
+ EventHandler.on(this._element, 'dragover', (e) => {
75
+ e.preventDefault();
76
+ e.stopPropagation();
72
77
 
73
78
  // ✅ Сортировка: НЕ подсвечиваем dropzone
74
- if (isSortableDrag(e)) {
75
- Classes.remove(this._element, [CLASS_NAME_DROP_ACTIVE, CLASS_NAME_DROP_HOVER]);
76
- e.dataTransfer.dropEffect = 'none';
77
- return;
78
- }
79
+ if (isSortableDrag(e)) {
80
+ Classes.remove(this._element, [CLASS_NAME_DROP_ACTIVE, CLASS_NAME_DROP_HOVER]);
81
+ VGFilesDroppable._setDropMessageTitleState(this._element, false);
82
+ e.dataTransfer.dropEffect = 'none';
83
+ return;
84
+ }
79
85
 
80
86
  if (!Classes.has(this._element, CLASS_NAME_DROP_ACTIVE)) {
81
87
  Classes.add(this._element, CLASS_NAME_DROP_HOVER);
82
88
  }
83
89
  });
84
90
 
85
- EventHandler.on(this._element, 'dragenter', (e) => {
86
- e.preventDefault();
87
- e.stopPropagation();
91
+ EventHandler.on(this._element, 'dragenter', (e) => {
92
+ e.preventDefault();
93
+ e.stopPropagation();
88
94
 
89
95
  // ✅ Сортировка: НЕ подсвечиваем dropzone
90
- if (isSortableDrag(e)) {
91
- Classes.remove(this._element, [CLASS_NAME_DROP_ACTIVE, CLASS_NAME_DROP_HOVER]);
92
- return;
93
- }
94
-
95
- Classes.add(this._element, [CLASS_NAME_DROP_ACTIVE, CLASS_NAME_DROP_HOVER]);
96
- });
96
+ if (isSortableDrag(e)) {
97
+ Classes.remove(this._element, [CLASS_NAME_DROP_ACTIVE, CLASS_NAME_DROP_HOVER]);
98
+ VGFilesDroppable._setDropMessageTitleState(this._element, false);
99
+ return;
100
+ }
101
+
102
+ Classes.add(this._element, [CLASS_NAME_DROP_ACTIVE, CLASS_NAME_DROP_HOVER]);
103
+ VGFilesDroppable._setDropMessageTitleState(this._element, true);
104
+ });
97
105
 
98
106
  EventHandler.on(this._element, 'dragleave', (e) => {
99
107
  e.preventDefault();
100
108
  e.stopPropagation();
101
109
 
102
110
  // ✅ Сортировка: гарантированно без подсветки
103
- if (isSortableDrag(e)) {
104
- Classes.remove(this._element, [CLASS_NAME_DROP_ACTIVE, CLASS_NAME_DROP_HOVER]);
105
- return;
106
- }
111
+ if (isSortableDrag(e)) {
112
+ Classes.remove(this._element, [CLASS_NAME_DROP_ACTIVE, CLASS_NAME_DROP_HOVER]);
113
+ VGFilesDroppable._setDropMessageTitleState(this._element, false);
114
+ return;
115
+ }
107
116
 
108
117
  if (e.target === this._element || e.target.closest('.' + CLASS_NAME_DROP) === this._element) {
109
118
  Classes.remove(this._element, CLASS_NAME_DROP_HOVER);
110
119
  setTimeout(() => {
111
- if (!this._element.matches(':hover')) {
112
- Classes.remove(this._element, CLASS_NAME_DROP_ACTIVE);
113
- }
114
- }, 50);
115
- }
116
- });
117
-
118
- EventHandler.on(this._element, 'drop', (e) => {
119
- e.preventDefault();
120
- e.stopPropagation();
120
+ if (!this._element.matches(':hover')) {
121
+ Classes.remove(this._element, CLASS_NAME_DROP_ACTIVE);
122
+ VGFilesDroppable._setDropMessageTitleState(this._element, false);
123
+ }
124
+ }, 50);
125
+ }
126
+ });
121
127
 
122
- Classes.remove(this._element, [CLASS_NAME_DROP_ACTIVE, CLASS_NAME_DROP_HOVER]);
128
+ EventHandler.on(this._element, 'drop', (e) => {
129
+ e.preventDefault();
130
+ e.stopPropagation();
131
+
132
+ Classes.remove(this._element, [CLASS_NAME_DROP_ACTIVE, CLASS_NAME_DROP_HOVER]);
133
+ VGFilesDroppable._setDropMessageTitleState(this._element, false);
123
134
 
124
135
  // ✅ сортировка: не трогаем input
125
- if (isSortableDrag(e)) {
126
- return;
127
- }
128
-
129
- const files = e.dataTransfer?.files;
130
- if (!files || !files.length) return;
136
+ if (isSortableDrag(e)) {
137
+ return;
138
+ }
139
+
140
+ const files = e.dataTransfer?.files;
141
+ if (!files || !files.length) return;
131
142
 
132
143
  this._files = files;
133
144
 
@@ -135,25 +146,209 @@ class VGFilesDroppable extends BaseModule {
135
146
  this._input.files = files;
136
147
  EventHandler.trigger(this._input, 'change');
137
148
  }
138
- });
139
- }
149
+ });
150
+ }
151
+
152
+ _bindGlobalEvents() {
153
+ if (VGFilesDroppable._isGlobalEventsBound) return;
154
+
155
+ VGFilesDroppable._globalHandlers = {
156
+ dragenter: (e) => this._updateSuggestedDrop(e),
157
+ dragover: (e) => this._updateSuggestedDrop(e),
158
+ dragleave: (e) => {
159
+ if (!this._isFileDrag(e)) return;
160
+ if (e.relatedTarget === null || e.target === document || e.target === document.documentElement) {
161
+ VGFilesDroppable._clearSuggestedDrop();
162
+ }
163
+ },
164
+ drop: (e) => {
165
+ if (!this._isFileDrag(e)) {
166
+ VGFilesDroppable._clearSuggestedDrop();
167
+ return;
168
+ }
169
+
170
+ const activeDrop = VGFilesDroppable._activeSuggestedDrop;
171
+ if (activeDrop) {
172
+ const instance = Array.from(VGFilesDroppable._smartInstances).find(i => i._element === activeDrop);
173
+ const files = e.dataTransfer?.files;
174
+
175
+ if (instance && files && files.length && isElement(instance._input)) {
176
+ e.preventDefault();
177
+ e.stopPropagation();
178
+ instance._files = files;
179
+ instance._input.files = files;
180
+ EventHandler.trigger(instance._input, 'change');
181
+ }
182
+ }
183
+
184
+ VGFilesDroppable._clearSuggestedDrop();
185
+ },
186
+ dragend: () => VGFilesDroppable._clearSuggestedDrop(),
187
+ };
188
+
189
+ EventHandler.on(document, 'dragenter', VGFilesDroppable._globalHandlers.dragenter);
190
+ EventHandler.on(document, 'dragover', VGFilesDroppable._globalHandlers.dragover);
191
+ EventHandler.on(document, 'dragleave', VGFilesDroppable._globalHandlers.dragleave);
192
+ EventHandler.on(document, 'drop', VGFilesDroppable._globalHandlers.drop);
193
+ EventHandler.on(document, 'dragend', VGFilesDroppable._globalHandlers.dragend);
194
+
195
+ EventHandler.on(window, 'dragenter', VGFilesDroppable._globalHandlers.dragenter);
196
+ EventHandler.on(window, 'dragover', VGFilesDroppable._globalHandlers.dragover);
197
+ EventHandler.on(window, 'dragleave', VGFilesDroppable._globalHandlers.dragleave);
198
+ EventHandler.on(window, 'drop', VGFilesDroppable._globalHandlers.drop);
199
+ EventHandler.on(window, 'dragend', VGFilesDroppable._globalHandlers.dragend);
200
+
201
+ VGFilesDroppable._isGlobalEventsBound = true;
202
+ }
203
+
204
+ _updateSuggestedDrop(e) {
205
+ if (!this._isFileDrag(e)) {
206
+ VGFilesDroppable._clearSuggestedDrop();
207
+ return;
208
+ }
209
+
210
+ const visibleDrops = this._getVisibleDropZonesInViewport();
211
+
212
+ if (visibleDrops.length === 1) {
213
+ const [dropZone] = visibleDrops;
214
+ if (VGFilesDroppable._activeSuggestedDrop !== dropZone) {
215
+ VGFilesDroppable._clearSuggestedDrop();
216
+ Classes.add(dropZone, CLASS_NAME_DROP_ACTIVE);
217
+ VGFilesDroppable._setDropMessageTitleState(dropZone, true);
218
+ VGFilesDroppable._activeSuggestedDrop = dropZone;
219
+ }
220
+
221
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
222
+ e.preventDefault();
223
+ return;
224
+ }
225
+
226
+ VGFilesDroppable._clearSuggestedDrop();
227
+ }
228
+
229
+ _isFileDrag(e) {
230
+ try {
231
+ const dt = e?.dataTransfer;
232
+ if (!dt) return false;
233
+
234
+ if (dt.files && dt.files.length > 0) return true;
235
+
236
+ if (dt.items && dt.items.length > 0) {
237
+ return Array.from(dt.items).some(item => item.kind === 'file');
238
+ }
239
+
240
+ if (dt.types) {
241
+ return Array.from(dt.types).includes('Files');
242
+ }
243
+
244
+ return false;
245
+ } catch (_) {
246
+ return false;
247
+ }
248
+ }
249
+
250
+ _getVisibleDropZonesInViewport() {
251
+ const dropZones = Array.from(Selectors.findAll(`.${CLASS_NAME_DROP}`) || []);
252
+ return dropZones.filter((el) => this._isVisibleInViewport(el));
253
+ }
254
+
255
+ _isVisibleInViewport(el) {
256
+ if (!isElement(el) || !el.isConnected) return false;
257
+
258
+ const style = window.getComputedStyle(el);
259
+ if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
260
+ return false;
261
+ }
262
+
263
+ const rect = el.getBoundingClientRect();
264
+ if (!rect.width || !rect.height) return false;
265
+
266
+ const inViewport =
267
+ rect.bottom > 0 &&
268
+ rect.right > 0 &&
269
+ rect.top < window.innerHeight &&
270
+ rect.left < window.innerWidth;
271
+
272
+ return inViewport;
273
+ }
274
+
275
+ static _clearSuggestedDrop() {
276
+ if (!VGFilesDroppable._activeSuggestedDrop) return;
277
+
278
+ VGFilesDroppable._setDropMessageTitleState(VGFilesDroppable._activeSuggestedDrop, false);
279
+ Classes.remove(VGFilesDroppable._activeSuggestedDrop, CLASS_NAME_DROP_ACTIVE);
280
+ VGFilesDroppable._activeSuggestedDrop = null;
281
+ }
282
+
283
+ static _setDropMessageTitleState(dropElement, isActive) {
284
+ if (!isElement(dropElement)) return;
285
+
286
+ const title = Selectors.find('.vg-files-drop-message .title', dropElement);
287
+ if (!title) return;
288
+
289
+ const originalText = (title.getAttribute('data-drop-original-text') || '').trim() || (title.textContent || '').trim();
290
+ title.setAttribute('data-drop-original-text', originalText);
291
+
292
+ const activeText = (dropElement.getAttribute('data-drop-active-text') || '').trim();
293
+ if (isActive && activeText) {
294
+ title.textContent = activeText;
295
+ return;
296
+ }
297
+
298
+ title.textContent = originalText;
299
+ }
300
+
301
+ static _unbindGlobalEvents() {
302
+ if (!VGFilesDroppable._isGlobalEventsBound) return;
303
+
304
+ const handlers = VGFilesDroppable._globalHandlers || {};
305
+
306
+ EventHandler.off(document, 'dragenter', handlers.dragenter);
307
+ EventHandler.off(document, 'dragover', handlers.dragover);
308
+ EventHandler.off(document, 'dragleave', handlers.dragleave);
309
+ EventHandler.off(document, 'drop', handlers.drop);
310
+ EventHandler.off(document, 'dragend', handlers.dragend);
311
+
312
+ EventHandler.off(window, 'dragenter', handlers.dragenter);
313
+ EventHandler.off(window, 'dragover', handlers.dragover);
314
+ EventHandler.off(window, 'dragleave', handlers.dragleave);
315
+ EventHandler.off(window, 'drop', handlers.drop);
316
+ EventHandler.off(window, 'dragend', handlers.dragend);
317
+
318
+ VGFilesDroppable._isGlobalEventsBound = false;
319
+ VGFilesDroppable._globalHandlers = null;
320
+ VGFilesDroppable._clearSuggestedDrop();
321
+ }
140
322
 
141
323
  getFiles() {
142
324
  return this._files;
143
325
  }
144
326
 
145
- dispose() {
146
- EventHandler.off(this._element, 'dragover');
147
- EventHandler.off(this._element, 'dragenter');
148
- EventHandler.off(this._element, 'dragleave');
149
- EventHandler.off(this._element, 'drop');
150
- this._input = null;
151
- this._files = null;
152
- }
327
+ dispose() {
328
+ EventHandler.off(this._element, 'dragover');
329
+ EventHandler.off(this._element, 'dragenter');
330
+ EventHandler.off(this._element, 'dragleave');
331
+ EventHandler.off(this._element, 'drop');
332
+
333
+ VGFilesDroppable._instances.delete(this);
334
+ VGFilesDroppable._smartInstances.delete(this);
335
+ if (!VGFilesDroppable._smartInstances.size) {
336
+ VGFilesDroppable._unbindGlobalEvents();
337
+ }
338
+
339
+ this._input = null;
340
+ this._files = null;
341
+ }
153
342
 
154
343
  init() {
155
344
  return this;
156
345
  }
157
- }
158
-
159
- export default VGFilesDroppable;
346
+ }
347
+
348
+ VGFilesDroppable._instances = new Set();
349
+ VGFilesDroppable._smartInstances = new Set();
350
+ VGFilesDroppable._isGlobalEventsBound = false;
351
+ VGFilesDroppable._activeSuggestedDrop = null;
352
+ VGFilesDroppable._globalHandlers = null;
353
+
354
+ export default VGFilesDroppable;