vgapp 0.7.9 → 0.8.0

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 (47) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/app/langs/en/buttons.json +10 -0
  3. package/app/langs/en/messages.json +32 -0
  4. package/app/langs/en/titles.json +6 -0
  5. package/app/langs/ru/buttons.json +10 -0
  6. package/app/langs/ru/messages.json +32 -0
  7. package/app/langs/ru/titles.json +6 -0
  8. package/app/modules/base-module.js +12 -1
  9. package/app/modules/module-fn.js +20 -9
  10. package/app/modules/vgalert/js/vgalert.js +12 -6
  11. package/app/modules/vgalert/readme.md +1 -1
  12. package/app/modules/vgcollapse/readme.md +1 -1
  13. package/app/modules/vgdropdown/js/vgdropdown.js +140 -38
  14. package/app/modules/vgdropdown/readme.md +225 -0
  15. package/app/modules/vgfiles/js/base.js +499 -0
  16. package/app/modules/vgfiles/js/droppable.js +159 -0
  17. package/app/modules/vgfiles/js/loader.js +389 -0
  18. package/app/modules/vgfiles/js/render.js +83 -0
  19. package/app/modules/vgfiles/js/sortable.js +155 -0
  20. package/app/modules/vgfiles/js/vgfiles.js +796 -280
  21. package/app/modules/vgfiles/readme.md +193 -0
  22. package/app/modules/vgfiles/scss/_animations.scss +18 -0
  23. package/app/modules/vgfiles/scss/_mixins.scss +73 -0
  24. package/app/modules/vgfiles/scss/_variables.scss +103 -26
  25. package/app/modules/vgfiles/scss/vgfiles.scss +573 -60
  26. package/app/modules/vgformsender/js/vgformsender.js +5 -1
  27. package/app/modules/vgformsender/readme.md +1 -1
  28. package/app/modules/vglawcookie/js/vglawcookie.js +96 -62
  29. package/app/modules/vglawcookie/readme.md +102 -0
  30. package/app/modules/vgsidebar/js/vgsidebar.js +6 -4
  31. package/app/utils/js/components/ajax.js +172 -122
  32. package/app/utils/js/components/animation.js +124 -39
  33. package/app/utils/js/components/backdrop.js +54 -31
  34. package/app/utils/js/components/lang.js +69 -88
  35. package/app/utils/js/components/params.js +34 -31
  36. package/app/utils/js/components/scrollbar.js +118 -67
  37. package/app/utils/js/components/templater.js +14 -4
  38. package/app/utils/js/dom/cookie.js +107 -64
  39. package/app/utils/js/dom/data.js +68 -20
  40. package/app/utils/js/dom/event.js +272 -239
  41. package/app/utils/js/dom/manipulator.js +135 -62
  42. package/app/utils/js/dom/selectors.js +134 -59
  43. package/app/utils/js/functions.js +183 -349
  44. package/build/vgapp.css +1 -1
  45. package/build/vgapp.css.map +1 -1
  46. package/package.json +1 -1
  47. package/app/utils/js/components/overflow.js +0 -28
@@ -0,0 +1,499 @@
1
+ import BaseModule from "../../base-module";
2
+ import {mergeDeepObject} from "../../../utils/js/functions";
3
+ import Html from "../../../utils/js/components/templater";
4
+ import {lang_messages} from "../../../utils/js/components/lang";
5
+ import {Classes, Manipulator} from "../../../utils/js/dom/manipulator";
6
+ import Selectors from "../../../utils/js/dom/selectors";
7
+ import {getSVG} from "../../module-fn";
8
+
9
+ class VGFilesBase extends BaseModule {
10
+ constructor(element, params = {}, defaults = {}) {
11
+ super(element, params);
12
+
13
+ this._params = this._getParams(element, mergeDeepObject(defaults, params));
14
+ this._params.init = ('init' in params) && params.init || this._params.init;
15
+
16
+ if (!this._params.init) {
17
+ this._isInitialized = false;
18
+ return;
19
+ }
20
+ this._isInitialized = true;
21
+
22
+ this._tpl = Html('dom');
23
+ this._files = [];
24
+ this._errors = new Set();
25
+ this._objectUrls = [];
26
+
27
+ this._nodes = {
28
+ stat: Selectors.find(`.${this._getClass('stat')}`, this._element),
29
+ info: Selectors.find(`.${this._getClass('info')}`, this._element),
30
+ drop: Selectors.find(`.${this._getClass('drop')}`, this._element),
31
+ };
32
+
33
+ this.template = '<li data-file="" class="file"><div class="file-image"></div><div class="file-info"></div><div class="file-remove"></div></li>';
34
+ this._init();
35
+ }
36
+
37
+ _getClass(name) {
38
+ const map = {
39
+ 'stat': 'vg-files-stat',
40
+ 'stat-progress': 'vg-files-stat-progress',
41
+ 'info': 'vg-files-info',
42
+ 'info-list': 'vg-files-info--list',
43
+ 'drop': 'vg-files-drop',
44
+ 'drop-list': 'vg-files-drop--list',
45
+ 'drop-message': 'vg-files-drop-message',
46
+ 'errors': 'vg-files-errors'
47
+ };
48
+ return map[name] || '';
49
+ }
50
+
51
+ _init() {
52
+ if (!this._isInitialized) return;
53
+
54
+ this._preventOriginalInputFromSubmit();
55
+ this._addEventListener();
56
+ }
57
+
58
+ change(input = null) {
59
+ const incomingFiles = input?.files;
60
+ if (!incomingFiles?.length) return;
61
+
62
+ const filesArray = Array.from(incomingFiles);
63
+
64
+ if (!this._params.allowed) {
65
+ this.append(filesArray, false);
66
+ this.build();
67
+ } else {
68
+ this.clear();
69
+ this.append(filesArray, true);
70
+ this.build();
71
+ }
72
+ }
73
+
74
+ build() {
75
+ this._updateStat();
76
+
77
+ if (this._params.ajax) {
78
+ if (this._render.init()) {
79
+ this._render.init()
80
+ } else {
81
+ this._renderUI(this._files);
82
+ }
83
+ this._renderUI(this._files);
84
+ } else {
85
+ this._renderUI(this._files);
86
+ this._generateHiddenInputs(this._files);
87
+ }
88
+ }
89
+
90
+ append(values, replace = true) {
91
+ const incoming = Array.from(values);
92
+ let filesToProcess;
93
+
94
+ if (replace) {
95
+ filesToProcess = incoming;
96
+ } else {
97
+ const fileMap = new Map(this._files.map(f => [this._getFileKey(f), f]));
98
+ incoming.forEach(file => {
99
+ fileMap.set(this._getFileKey(file), file);
100
+ });
101
+ filesToProcess = Array.from(fileMap.values());
102
+ }
103
+
104
+ this._files = this._filterFiles(filesToProcess);
105
+ if (this._params.prepend) this._files.reverse();
106
+
107
+ this._renderErrors();
108
+
109
+ return this._files;
110
+ }
111
+
112
+ _getFileKey(file) {
113
+ return `${file.name}-${file.size}-${file.type}`;
114
+ }
115
+
116
+ _filterFiles(files) {
117
+ this._errors.clear();
118
+ const { count, sizes, total } = this._params.limits;
119
+ const maxSize = sizes * 1024 * 1024;
120
+ const maxTotalSize = total * 1024 * 1024;
121
+
122
+ let currentTotalSize = 0;
123
+ const filtered = [];
124
+
125
+ for (const file of files) {
126
+ if (count > 0 && filtered.length >= count) {
127
+ this._errors.add('is-count');
128
+ break;
129
+ }
130
+
131
+ let isValid = true;
132
+
133
+ if (this._params.types.length && !this._params.types.includes(file.type)) {
134
+ this._errors.add('is-types');
135
+ isValid = false;
136
+ }
137
+
138
+ if (file.size > maxSize) {
139
+ this._errors.add('is-sizes');
140
+ isValid = false;
141
+ }
142
+
143
+ if (isValid && maxTotalSize > 0) {
144
+ if (currentTotalSize + file.size > maxTotalSize) {
145
+ this._errors.add('is-total-size');
146
+ isValid = false;
147
+ } else {
148
+ currentTotalSize += file.size;
149
+ }
150
+ }
151
+
152
+ if (isValid) filtered.push(file);
153
+ }
154
+
155
+ return filtered;
156
+ }
157
+
158
+ _getSizes(size, isArray = false) {
159
+ const totalSize = isArray ? this._files.reduce((acc, f) => acc + f.size, 0) : size;
160
+ const units = ['byte', 'kilobyte', 'megabyte', 'gigabyte'];
161
+ const index = totalSize > 0 ? Math.min(Math.floor(Math.log(totalSize) / Math.log(1024)), units.length - 1) : 0;
162
+ const value = totalSize / Math.pow(1024, index);
163
+
164
+ return new Intl.NumberFormat(this._params.lang, {
165
+ style: 'unit',
166
+ unit: units[index],
167
+ unitDisplay: 'short',
168
+ maximumFractionDigits: 2
169
+ }).format(value);
170
+ }
171
+
172
+ _cleanupErrors() {
173
+ this._errors.clear();
174
+ const $errorCont = Selectors.find(`.${this._getClass('errors')}`, this._element);
175
+ if ($errorCont) {
176
+ $errorCont.remove()
177
+ }
178
+ }
179
+
180
+ _renderErrors() {
181
+ if (!this._errors.size) return;
182
+
183
+ const messages = lang_messages(this._params.lang, 'files') || this._getFallbackErrors();
184
+ let $errorCont = Selectors.find(`.${this._getClass('errors')}`, this._element);
185
+
186
+ if (!$errorCont) {
187
+ $errorCont = this._tpl.div({ class: this._getClass('errors') });
188
+ this._element.prepend($errorCont);
189
+ } else {
190
+ $errorCont.innerHTML = '';
191
+ }
192
+
193
+ this._errors.forEach(errKey => {
194
+ const msg = messages[errKey] || errKey;
195
+ $errorCont.append(this._tpl.span({ class: 'error-item' }, msg));
196
+ });
197
+ }
198
+
199
+ _getFallbackErrors() {
200
+ const { count, sizes, total } = this._params.limits;
201
+ return {
202
+ 'is-count': `Limit: ${count}`,
203
+ 'is-sizes': `Max size: ${sizes}MB`,
204
+ 'is-total-size': `Total max size: ${total}MB`,
205
+ 'is-types': `Allowed: ${this._params.types.join(', ')}`
206
+ };
207
+ }
208
+
209
+ _renderUI(files) {
210
+ if (this._nodes.drop) {
211
+ this._renderUIDropList(files);
212
+ } else if (this._nodes.info) {
213
+ this._renderInfoList(files);
214
+ }
215
+
216
+ this._renderUIStatusDropInfoAjax(this._files)
217
+ }
218
+
219
+ _parseTemplate() {
220
+ const render = this._render;
221
+ let tmpl = this.template;
222
+
223
+ if (render) {
224
+ if (render.bufferTemplate) tmpl = render.bufferTemplate;
225
+ }
226
+
227
+ const temp = document.createElement('div');
228
+ temp.innerHTML = tmpl;
229
+ const liElement = temp.firstElementChild;
230
+ const liClasses = liElement.className || '';
231
+ const liClassList = liClasses ? liClasses.split(' ').filter(cls => cls.trim() !== '') : [];
232
+
233
+ const childCount = liElement.childElementCount;
234
+
235
+ const children = [];
236
+ for (let i = 0; i < childCount; i++) {
237
+ children.push({
238
+ index: i,
239
+ element: liElement.children[i],
240
+ className: liElement.children[i].className
241
+ });
242
+ }
243
+
244
+ return {
245
+ children: children,
246
+ template: tmpl,
247
+ liClasses: liClassList,
248
+ liClassName: liClasses
249
+ };
250
+ }
251
+
252
+ _renderUIDropList(files) {
253
+ if (!this._nodes.drop) return;
254
+
255
+ let $list = Selectors.find(`.${this._getClass('drop-list')}`, this._nodes.drop);
256
+ if (!$list) {
257
+ $list = this._tpl.ul([], { class: this._getClass('drop-list') });
258
+ }
259
+
260
+ const $itemsTemplate = this._parseTemplate().children;
261
+ const $itemsTemplateClasses = this._parseTemplate().liClasses.filter(cls => cls !== 'file');
262
+ const fragment = document.createDocumentFragment();
263
+
264
+ files.forEach((file) => {
265
+ let classes = $itemsTemplateClasses;;
266
+
267
+ if (this._params.detach) classes.push('with-remove')
268
+ if (this._params.sortable.enabled) classes.push('with-sortable')
269
+ if (this._params.limits.count === 1) {
270
+ classes.push('single');
271
+
272
+ if (file.type.startsWith('image/')) {
273
+ classes.push('single-image')
274
+ } else {
275
+ classes.push('single-file')
276
+ }
277
+ }
278
+
279
+ let parts = [];
280
+ $itemsTemplate.forEach(tmpl => {
281
+ if (tmpl.className === 'file-image') {
282
+ parts.push(this._renderUIImage(file))
283
+ } else if (tmpl.className === 'file-remove') {
284
+ parts.push(this._renderUIDetach(file))
285
+ } else {
286
+ parts.push(tmpl.element.cloneNode(true));
287
+ }
288
+ });
289
+
290
+ const $li = this._tpl.li(
291
+ { 'data-name': file.name, 'data-size': file.size, 'data-id': file.id || '', class: 'file ' + classes.join(' ') }, parts
292
+ );
293
+
294
+ fragment.appendChild($li);
295
+ });
296
+
297
+ $list.innerHTML = '';
298
+ $list.appendChild(fragment);
299
+
300
+ const $message = Selectors.find(`.${this._getClass('drop-message')}`, this._nodes.drop);
301
+ if ($message) Classes.remove($message, 'show');
302
+
303
+ this._nodes.drop.appendChild($list);
304
+ Classes.add(this._nodes.drop, 'active');
305
+ }
306
+
307
+ _renderInfoList(files) {
308
+ if (!this._nodes.info) return;
309
+
310
+ let $list = Selectors.find(`.${this._getClass('info-list')}`, this._element);
311
+ if (!$list) {
312
+ $list = this._tpl.ul([], { class: this._getClass('info-list') });
313
+ this._nodes.info.appendChild($list);
314
+ }
315
+
316
+ if (!this._params.info) Classes.add($list, 'list-row');
317
+
318
+ $list.innerHTML = '';
319
+
320
+ const $itemsTemplate = this._parseTemplate().children;
321
+ const $itemsTemplateClasses = this._parseTemplate().liClasses.filter(cls => cls !== 'file');
322
+ const fragment = document.createDocumentFragment();
323
+
324
+ files.forEach((file, i) => {
325
+ let classes = $itemsTemplateClasses;
326
+
327
+ if (this._params.image) classes.push('with-image');
328
+ if (this._params.info) classes.push('with-info');
329
+ if (this._params.detach) classes.push('with-remove')
330
+ if (this._params.sortable.enabled) classes.push('with-sortable');
331
+
332
+ let parts = [];
333
+ $itemsTemplate.forEach(tmpl => {
334
+ if (tmpl.className === 'file-image') {
335
+ parts.push(this._renderUIImage(file))
336
+ } else if (tmpl.className === 'file-info') {
337
+ parts.push(this._renderUIInfo(file, i))
338
+ } else if (tmpl.className === 'file-remove') {
339
+ parts.push(this._renderUIDetach(file))
340
+ } else {
341
+ parts.push(tmpl.element.cloneNode(true));
342
+ }
343
+ });
344
+
345
+ const $li = this._tpl.li(
346
+ { 'data-name': file.name, 'data-size': file.size, 'data-type': file.type, 'data-id': file.id || '', class: 'file ' + classes.join(' ') + ' ' }, parts
347
+ );
348
+ fragment.appendChild($li);
349
+ });
350
+ $list.appendChild(fragment);
351
+
352
+ Classes.add(this._nodes.info, 'show')
353
+ }
354
+
355
+ _renderUIDetach(file) {
356
+ if (this._params.detach) {
357
+ return this._tpl.div({ class: 'file-remove' }, [
358
+ this._setButtonElement(file)
359
+ ])
360
+ }
361
+ }
362
+
363
+ _renderUIInfo(file, i) {
364
+ if (this._params.info) {
365
+ return this._tpl.div({ class: 'file-info' }, [
366
+ this._tpl.span({ class: 'iteration' }, `${i + 1}.`),
367
+ this._tpl.span({ class: 'name' }, file.name),
368
+ this._tpl.span({ class: 'size' }, `[${this._getSizes(file.size)}]`)
369
+ ]);
370
+ }
371
+ }
372
+
373
+ _renderUIImage(file) {
374
+ const $container = this._tpl.div({ class: 'file-image' });
375
+
376
+ const src = file?.src || file?.image;
377
+ if (src) {
378
+ $container.appendChild(this._tpl.img(src, file.name || '', { class: 'file-preview' }));
379
+ return $container;
380
+ }
381
+
382
+ if (file?.type && file.type.startsWith('image/')) {
383
+ const objectUrl = URL.createObjectURL(file);
384
+ this._objectUrls.push(objectUrl);
385
+ $container.appendChild(this._tpl.img(objectUrl, file.name, { class: 'file-preview' }));
386
+ return $container;
387
+ }
388
+
389
+ const icon = this._getIconByFileType(file);
390
+ $container.appendChild(this._tpl.i({}, icon, { isHTML: true }));
391
+ return $container;
392
+ }
393
+
394
+ _getIconByFileType(file) {
395
+ if (file.type === 'application/pdf') return getSVG('file-pdf');
396
+ if (file.type.includes('word') || file.name.endsWith('.doc') || file.name.endsWith('.docx')) return getSVG('file-word');
397
+ if (file.type.includes('excel') || file.name.endsWith('.xls') || file.name.endsWith('.xlsx')) return getSVG('file-exel');
398
+ if (file.type === 'application/zip' || file.name.endsWith('.zip')) return getSVG('file-zip');
399
+ if (file.name.endsWith('.txt')) return getSVG('file-text');
400
+ return getSVG('file-generic');
401
+ }
402
+
403
+ _updateStat() {
404
+ if (!this._nodes.stat) return;
405
+
406
+ const totalSize = this._getSizes(this._files, true);
407
+ const $count = Selectors.find(`.${this._getClass('stat')}-count`, this._nodes.stat);
408
+ if ($count) {
409
+ $count.innerHTML = this._files.length ? `${this._files.length}<span>[${totalSize}]</span>` : '';
410
+ }
411
+
412
+ Classes.add(this._nodes.stat, 'show');
413
+ }
414
+
415
+ _generateHiddenInputs(files) {
416
+ this._cleanupFakeInputs();
417
+ const fragment = document.createDocumentFragment();
418
+ const name = this._element.querySelector('[data-vg-toggle]')?.name || 'files[]';
419
+
420
+ files.forEach((file, index) => {
421
+ const input = document.createElement('input');
422
+ input.type = 'file';
423
+ input.name = `${name.replace('[]', '')}[${index}]`;
424
+ input.dataset.vgFiles = 'generated';
425
+ Manipulator.hide(input);
426
+
427
+ const dataTransfer = new DataTransfer();
428
+ dataTransfer.items.add(file);
429
+ input.files = dataTransfer.files;
430
+ fragment.appendChild(input);
431
+ });
432
+
433
+ this._element.appendChild(fragment);
434
+ }
435
+
436
+ _cleanupFakeInputs() {
437
+ Selectors.findAll('[data-vg-files="generated"]', this._element).forEach(el => el.remove());
438
+ }
439
+
440
+ clear() {
441
+ this._revokeUrls();
442
+ this._resetFileInput();
443
+ this._cleanupFakeInputs();
444
+ this._cleanupErrors();
445
+ this._files = [];
446
+
447
+ if (this._nodes.info) {
448
+ Classes.remove(this._nodes.info, 'show');
449
+ const $list = Selectors.find(`.${this._getClass('info-list')}`, this._element);
450
+ if ($list) $list.innerHTML = '';
451
+ }
452
+ if (this._nodes.drop) {
453
+ const $list = Selectors.find(`.${this._getClass('drop-list')}`, this._element);
454
+ if ($list) $list.innerHTML = '';
455
+
456
+ const $message = Selectors.find(`.${this._getClass('drop-message')}`, this._element);
457
+ if ($message) Classes.add($message, 'show');
458
+
459
+ Classes.remove(this._nodes.drop, 'active');
460
+ }
461
+ if (this._nodes.stat) {
462
+ Classes.remove(this._nodes.stat, 'show');
463
+ }
464
+ }
465
+
466
+ _revokeUrls() {
467
+ this._objectUrls.forEach(url => URL.revokeObjectURL(url));
468
+ this._objectUrls = [];
469
+ }
470
+
471
+ _resetFileInput() {
472
+ Selectors.findAll('[data-vg-toggle="files"]', this._element).forEach(input => input.value = '');
473
+ }
474
+
475
+ _preventOriginalInputFromSubmit(isRestore = false) {
476
+ const el = Selectors.find('[data-vg-toggle="files"]', this._element);
477
+ if (!el) return;
478
+ if (!isRestore && !el.dataset.originalName) {
479
+ el.dataset.originalName = el.name;
480
+ el.removeAttribute('name');
481
+ } else if (isRestore && el.dataset.originalName) {
482
+ el.name = el.dataset.originalName;
483
+ delete el.dataset.originalName;
484
+ }
485
+ }
486
+
487
+ _addEventListener() {
488
+ Selectors.findAll('[data-vg-toggle="files"]', this._element).forEach(el => {
489
+ el.addEventListener('change', () => this.change(el));
490
+ });
491
+ }
492
+
493
+ dispose() {
494
+ this.clear();
495
+ super.dispose();
496
+ }
497
+ }
498
+
499
+ export default VGFilesBase;
@@ -0,0 +1,159 @@
1
+ import { isElement } from "../../../utils/js/functions";
2
+ import EventHandler from "../../../utils/js/dom/event";
3
+ import Selectors from "../../../utils/js/dom/selectors";
4
+ import { Classes } from "../../../utils/js/dom/manipulator";
5
+ import BaseModule from "../../base-module";
6
+
7
+ const CLASS_NAME_CONTAINER = 'vg-files';
8
+ const CLASS_NAME_DROP = `${CLASS_NAME_CONTAINER}-drop`;
9
+
10
+ const CLASS_NAME_DROP_ACTIVE = 'drop-active';
11
+ const CLASS_NAME_DROP_HOVER = 'drop-hover';
12
+
13
+ const NAME = 'drop-and-drop';
14
+ const NAME_KEY = 'vg.drop-and-drop';
15
+
16
+ class VGFilesDroppable extends BaseModule {
17
+ /**
18
+ * @param {HTMLElement} element
19
+ * @param {Object} params
20
+ */
21
+ constructor(element, params = {}) {
22
+ super(element, params);
23
+ this._element = element;
24
+ this._params = params;
25
+
26
+ this._files = null;
27
+ this._input = null;
28
+ this._init();
29
+ }
30
+
31
+ static get NAME() { return NAME; }
32
+ static get NAME_KEY() { return NAME_KEY; }
33
+
34
+ _init() {
35
+ this._findInput();
36
+ this._setupEvents();
37
+ }
38
+
39
+ /**
40
+ * Поиск связанного input[type="file"] внутри или рядом с drop-зоной
41
+ */
42
+ _findInput() {
43
+ const { id, name } = this._element.dataset;
44
+ if (id) this._input = document.getElementById(id);
45
+ if (!this._input && name) {
46
+ this._input = Selectors.find(`[name="${name}"]`);
47
+ }
48
+ if (!this._input) {
49
+ this._input = Selectors.find('input[type="file"]', this._element) || Selectors.find('input[type="file"]', this._element.parentNode);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Настройка событий перетаскивания
55
+ */
56
+ _setupEvents() {
57
+ if (!isElement(this._element)) return;
58
+
59
+ const isSortableDrag = (e) => {
60
+ // 1) если sortable реально активен — на элементе есть класс dragging
61
+ if (document.querySelector('.dragging')) return true;
62
+
63
+ // 2) маркер, который ставит sortable
64
+ let plain = '';
65
+ try { plain = e.dataTransfer?.getData?.('text/plain') || ''; } catch (_) {}
66
+ return plain === 'vgsortable';
67
+ };
68
+
69
+ EventHandler.on(this._element, 'dragover', (e) => {
70
+ e.preventDefault();
71
+ e.stopPropagation();
72
+
73
+ // ✅ Сортировка: НЕ подсвечиваем 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
+
80
+ if (!Classes.has(this._element, CLASS_NAME_DROP_ACTIVE)) {
81
+ Classes.add(this._element, CLASS_NAME_DROP_HOVER);
82
+ }
83
+ });
84
+
85
+ EventHandler.on(this._element, 'dragenter', (e) => {
86
+ e.preventDefault();
87
+ e.stopPropagation();
88
+
89
+ // ✅ Сортировка: НЕ подсвечиваем 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
+ });
97
+
98
+ EventHandler.on(this._element, 'dragleave', (e) => {
99
+ e.preventDefault();
100
+ e.stopPropagation();
101
+
102
+ // ✅ Сортировка: гарантированно без подсветки
103
+ if (isSortableDrag(e)) {
104
+ Classes.remove(this._element, [CLASS_NAME_DROP_ACTIVE, CLASS_NAME_DROP_HOVER]);
105
+ return;
106
+ }
107
+
108
+ if (e.target === this._element || e.target.closest('.' + CLASS_NAME_DROP) === this._element) {
109
+ Classes.remove(this._element, CLASS_NAME_DROP_HOVER);
110
+ 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();
121
+
122
+ Classes.remove(this._element, [CLASS_NAME_DROP_ACTIVE, CLASS_NAME_DROP_HOVER]);
123
+
124
+ // ✅ сортировка: не трогаем input
125
+ if (isSortableDrag(e)) {
126
+ return;
127
+ }
128
+
129
+ const files = e.dataTransfer?.files;
130
+ if (!files || !files.length) return;
131
+
132
+ this._files = files;
133
+
134
+ if (isElement(this._input)) {
135
+ this._input.files = files;
136
+ EventHandler.trigger(this._input, 'change');
137
+ }
138
+ });
139
+ }
140
+
141
+ getFiles() {
142
+ return this._files;
143
+ }
144
+
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
+ }
153
+
154
+ init() {
155
+ return this;
156
+ }
157
+ }
158
+
159
+ export default VGFilesDroppable;