vgapp 1.0.3 → 1.0.5
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.
|
@@ -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(false);
|
|
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();
|
|
@@ -209,6 +209,56 @@ class VGFilesBase extends BaseModule {
|
|
|
209
209
|
return `${file.name}-${file.size}-${file.type}`;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
_getFileCustomData(file) {
|
|
213
|
+
const customData = file?.customData;
|
|
214
|
+
if (!customData || typeof customData !== 'object' || Array.isArray(customData)) return {};
|
|
215
|
+
return customData;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_toDataAttributeKey(key) {
|
|
219
|
+
if (!key) return '';
|
|
220
|
+
|
|
221
|
+
return String(key)
|
|
222
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
223
|
+
.replace(/[_\s]+/g, '-')
|
|
224
|
+
.replace(/[^a-zA-Z0-9-]/g, '')
|
|
225
|
+
.replace(/-+/g, '-')
|
|
226
|
+
.replace(/^-|-$/g, '')
|
|
227
|
+
.toLowerCase();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
_toDataAttributeValue(value) {
|
|
231
|
+
if (value === undefined || value === null || value === '') return null;
|
|
232
|
+
if (typeof value === 'object') {
|
|
233
|
+
try {
|
|
234
|
+
return JSON.stringify(value);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
return String(value);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
_buildFileDataAttributes(file, baseAttrs = {}) {
|
|
243
|
+
const attrs = { ...baseAttrs };
|
|
244
|
+
const customData = this._getFileCustomData(file);
|
|
245
|
+
|
|
246
|
+
Object.entries(customData).forEach(([key, value]) => {
|
|
247
|
+
const attrKey = this._toDataAttributeKey(key);
|
|
248
|
+
if (!attrKey) return;
|
|
249
|
+
|
|
250
|
+
const attrName = `data-${attrKey}`;
|
|
251
|
+
if (Object.prototype.hasOwnProperty.call(attrs, attrName)) return;
|
|
252
|
+
|
|
253
|
+
const attrValue = this._toDataAttributeValue(value);
|
|
254
|
+
if (attrValue === null) return;
|
|
255
|
+
|
|
256
|
+
attrs[attrName] = attrValue;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return attrs;
|
|
260
|
+
}
|
|
261
|
+
|
|
212
262
|
_filterFiles(files) {
|
|
213
263
|
this._errors.clear();
|
|
214
264
|
const { count, sizes, total } = this._params.limits;
|
|
@@ -378,14 +428,20 @@ class VGFilesBase extends BaseModule {
|
|
|
378
428
|
}
|
|
379
429
|
}
|
|
380
430
|
|
|
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
|
-
});
|
|
431
|
+
let parts = [];
|
|
432
|
+
$itemsTemplate.forEach(tmpl => {
|
|
433
|
+
const part = this._renderTemplatePart(tmpl.element, file, null, { isDrop: true });
|
|
434
|
+
if (part) parts.push(part);
|
|
435
|
+
});
|
|
386
436
|
|
|
387
437
|
const $li = this._tpl.li(
|
|
388
|
-
|
|
438
|
+
this._buildFileDataAttributes(file, {
|
|
439
|
+
'data-name': file.name,
|
|
440
|
+
'data-size': file.size ?? 0,
|
|
441
|
+
'data-id': file.id || '',
|
|
442
|
+
class: 'file ' + classes.join(' ')
|
|
443
|
+
}),
|
|
444
|
+
parts
|
|
389
445
|
);
|
|
390
446
|
|
|
391
447
|
fragment.appendChild($li);
|
|
@@ -394,17 +450,17 @@ class VGFilesBase extends BaseModule {
|
|
|
394
450
|
$list.innerHTML = '';
|
|
395
451
|
$list.appendChild(fragment);
|
|
396
452
|
|
|
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
|
-
}
|
|
453
|
+
const $message = Selectors.find(`.${this._getClass('drop-message')}`, this._nodes.drop);
|
|
454
|
+
if ($message) {
|
|
455
|
+
if (files.length) Classes.add($message, 'has-files');
|
|
456
|
+
else Classes.remove($message, 'has-files');
|
|
457
|
+
|
|
458
|
+
const isSingle = Number(this._params?.limits?.count) === 1;
|
|
459
|
+
if (isSingle) {
|
|
460
|
+
if (files.length) Classes.remove($message, 'show');
|
|
461
|
+
else Classes.add($message, 'show');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
408
464
|
|
|
409
465
|
this._nodes.drop.appendChild($list);
|
|
410
466
|
Classes.add(this._nodes.drop, 'active');
|
|
@@ -435,14 +491,21 @@ class VGFilesBase extends BaseModule {
|
|
|
435
491
|
if (this._params.detach) classes.push('with-remove')
|
|
436
492
|
if (this._params.sortable.enabled) classes.push('with-sortable');
|
|
437
493
|
|
|
438
|
-
let parts = [];
|
|
439
|
-
$itemsTemplate.forEach(tmpl => {
|
|
440
|
-
const part = this._renderTemplatePart(tmpl.element, file, i);
|
|
441
|
-
if (part) parts.push(part);
|
|
442
|
-
});
|
|
494
|
+
let parts = [];
|
|
495
|
+
$itemsTemplate.forEach(tmpl => {
|
|
496
|
+
const part = this._renderTemplatePart(tmpl.element, file, i);
|
|
497
|
+
if (part) parts.push(part);
|
|
498
|
+
});
|
|
443
499
|
|
|
444
500
|
const $li = this._tpl.li(
|
|
445
|
-
|
|
501
|
+
this._buildFileDataAttributes(file, {
|
|
502
|
+
'data-name': file.name,
|
|
503
|
+
'data-size': file.size ?? 0,
|
|
504
|
+
'data-type': file.type || '',
|
|
505
|
+
'data-id': file.id || '',
|
|
506
|
+
class: 'file ' + classes.join(' ') + ' '
|
|
507
|
+
}),
|
|
508
|
+
parts
|
|
446
509
|
);
|
|
447
510
|
fragment.appendChild($li);
|
|
448
511
|
});
|
|
@@ -451,38 +514,38 @@ class VGFilesBase extends BaseModule {
|
|
|
451
514
|
Classes.add(this._nodes.info, 'show')
|
|
452
515
|
}
|
|
453
516
|
|
|
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
|
-
}
|
|
517
|
+
_renderTemplatePart(element, file, index = null, options = {}) {
|
|
518
|
+
if (!element) return null;
|
|
519
|
+
const { isDrop = false } = options;
|
|
520
|
+
|
|
521
|
+
const classList = element?.classList;
|
|
522
|
+
|
|
523
|
+
if (classList?.contains('file-image')) return this._renderUIImage(file);
|
|
524
|
+
if (classList?.contains('file-info')) {
|
|
525
|
+
if (isDrop) return null;
|
|
526
|
+
return this._renderUIInfo(file, index);
|
|
527
|
+
}
|
|
528
|
+
if (classList?.contains('file-remove')) return this._wrapInFileCustom(this._renderUIDetach(file));
|
|
529
|
+
|
|
530
|
+
const $part = element.cloneNode(true);
|
|
531
|
+
|
|
532
|
+
this._replaceTemplateSlot($part, '.file-image', () => this._renderUIImage(file));
|
|
533
|
+
this._replaceTemplateSlot($part, '.file-info', () => isDrop ? null : this._renderUIInfo(file, index));
|
|
534
|
+
this._replaceTemplateSlot($part, '.file-remove', () => this._renderUIDetach(file));
|
|
535
|
+
|
|
536
|
+
return this._wrapInFileCustom($part);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
_wrapInFileCustom(node) {
|
|
540
|
+
if (!node) return null;
|
|
541
|
+
if (node.classList?.contains('file-custom')) return node;
|
|
542
|
+
|
|
543
|
+
const wrapper = document.createElement('div');
|
|
544
|
+
wrapper.className = 'file-custom';
|
|
545
|
+
wrapper.appendChild(node);
|
|
546
|
+
|
|
547
|
+
return wrapper;
|
|
548
|
+
}
|
|
486
549
|
|
|
487
550
|
_replaceTemplateSlot(container, selector, renderer) {
|
|
488
551
|
const isMatchSelf = container?.matches && container.matches(selector);
|
|
@@ -586,14 +649,14 @@ class VGFilesBase extends BaseModule {
|
|
|
586
649
|
Selectors.findAll('[data-vg-files="generated"]', this._element).forEach(el => el.remove());
|
|
587
650
|
}
|
|
588
651
|
|
|
589
|
-
clear(resetInput = true) {
|
|
590
|
-
this._revokeUrls();
|
|
591
|
-
if (resetInput) {
|
|
592
|
-
this._resetFileInput();
|
|
593
|
-
}
|
|
594
|
-
this._cleanupFakeInputs();
|
|
595
|
-
this._cleanupErrors();
|
|
596
|
-
this._files = [];
|
|
652
|
+
clear(resetInput = true) {
|
|
653
|
+
this._revokeUrls();
|
|
654
|
+
if (resetInput) {
|
|
655
|
+
this._resetFileInput();
|
|
656
|
+
}
|
|
657
|
+
this._cleanupFakeInputs();
|
|
658
|
+
this._cleanupErrors();
|
|
659
|
+
this._files = [];
|
|
597
660
|
|
|
598
661
|
if (this._nodes.info) {
|
|
599
662
|
Classes.remove(this._nodes.info, 'show');
|
|
@@ -604,15 +667,15 @@ class VGFilesBase extends BaseModule {
|
|
|
604
667
|
const $list = Selectors.find(`.${this._getClass('drop-list')}`, this._element);
|
|
605
668
|
if ($list) $list.innerHTML = '';
|
|
606
669
|
|
|
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
|
-
}
|
|
670
|
+
const $message = Selectors.find(`.${this._getClass('drop-message')}`, this._element);
|
|
671
|
+
if ($message) {
|
|
672
|
+
Classes.remove($message, 'has-files');
|
|
673
|
+
|
|
674
|
+
const isSingle = Number(this._params?.limits?.count) === 1;
|
|
675
|
+
if (isSingle) {
|
|
676
|
+
Classes.add($message, 'show');
|
|
677
|
+
}
|
|
678
|
+
}
|
|
616
679
|
|
|
617
680
|
Classes.remove(this._nodes.drop, 'active');
|
|
618
681
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isElement, normalizeData } from "../../../utils/js/functions";
|
|
2
2
|
import Params from "../../../utils/js/components/params";
|
|
3
3
|
import Selectors from "../../../utils/js/dom/selectors";
|
|
4
|
-
import {
|
|
4
|
+
import { Manipulator } from "../../../utils/js/dom/manipulator";
|
|
5
5
|
|
|
6
6
|
class VGFilesTemplateRender {
|
|
7
7
|
constructor(vgFilesInstance, element, params = {}) {
|
|
@@ -38,34 +38,105 @@ class VGFilesTemplateRender {
|
|
|
38
38
|
const $items = Array.from($list.children).filter(li => li.tagName === 'LI');
|
|
39
39
|
if ($items.length === 0) return false;
|
|
40
40
|
|
|
41
|
-
// Сохраняем шаблон только один раз
|
|
42
41
|
this._setTemplateInBuffer($items);
|
|
43
|
-
|
|
44
42
|
if (!this.bufferTemplate) return false;
|
|
45
43
|
|
|
46
|
-
// Парсим данные файлов
|
|
47
44
|
this.parsedFiles = $items
|
|
48
45
|
.map(li => {
|
|
49
46
|
const rawData = Manipulator.get(li, 'data-file');
|
|
50
47
|
if (!rawData) return null;
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
const requiredKeys = ['id', 'name', 'size', 'type', 'src'];
|
|
54
|
-
const isValid = requiredKeys.every(key => dataFile.hasOwnProperty(key));
|
|
55
|
-
|
|
56
|
-
return isValid ? dataFile : null;
|
|
49
|
+
return this._parseDataFile(rawData);
|
|
57
50
|
})
|
|
58
|
-
.filter(Boolean);
|
|
51
|
+
.filter(Boolean);
|
|
59
52
|
|
|
60
53
|
return true;
|
|
61
54
|
}
|
|
62
55
|
|
|
56
|
+
_parseDataFile(rawData) {
|
|
57
|
+
const dataFile = normalizeData(rawData);
|
|
58
|
+
if (!dataFile || typeof dataFile !== 'object' || Array.isArray(dataFile)) return null;
|
|
59
|
+
|
|
60
|
+
const binaryMeta = this._extractBinaryMeta(dataFile);
|
|
61
|
+
const name = this._toStringOrNull(dataFile.name);
|
|
62
|
+
const id = this._toStringOrNull(dataFile.id);
|
|
63
|
+
const src = this._toStringOrNull(dataFile.src);
|
|
64
|
+
const type = this._toStringOrNull(dataFile.type) || binaryMeta.type || 'application/octet-stream';
|
|
65
|
+
const size = this._toNumberOrNull(dataFile.size) ?? binaryMeta.size ?? 0;
|
|
66
|
+
const lastModified = this._toNumberOrNull(dataFile.lastModified ?? dataFile['last-modified']) ?? binaryMeta.lastModified ?? Date.now();
|
|
67
|
+
|
|
68
|
+
const result = {
|
|
69
|
+
id,
|
|
70
|
+
name,
|
|
71
|
+
size,
|
|
72
|
+
type,
|
|
73
|
+
src,
|
|
74
|
+
lastModified
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (dataFile.image !== undefined && dataFile.image !== null && dataFile.image !== '') {
|
|
78
|
+
result.image = dataFile.image;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const requiredKeys = ['id', 'name', 'size', 'type', 'src'];
|
|
82
|
+
const isValid = requiredKeys.every((key) => {
|
|
83
|
+
const value = result[key];
|
|
84
|
+
if (key === 'size') return Number.isFinite(value);
|
|
85
|
+
return value !== undefined && value !== null && value !== '';
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (!isValid) return null;
|
|
89
|
+
|
|
90
|
+
const customData = {};
|
|
91
|
+
const reserved = new Set(['id', 'name', 'size', 'type', 'src', 'image', 'lastModified', 'last-modified']);
|
|
92
|
+
|
|
93
|
+
Object.entries(dataFile).forEach(([key, value]) => {
|
|
94
|
+
if (reserved.has(key)) return;
|
|
95
|
+
if (value === undefined || value === null || value === '') return;
|
|
96
|
+
customData[key] = value;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (Object.keys(customData).length) {
|
|
100
|
+
result.customData = customData;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_extractBinaryMeta(dataFile) {
|
|
107
|
+
const binary = dataFile.file || dataFile.blob || dataFile.originFile || null;
|
|
108
|
+
if (!(binary instanceof Blob)) return {};
|
|
109
|
+
|
|
110
|
+
const size = this._toNumberOrNull(binary.size);
|
|
111
|
+
const type = this._toStringOrNull(binary.type);
|
|
112
|
+
const lastModified = (binary instanceof File)
|
|
113
|
+
? this._toNumberOrNull(binary.lastModified)
|
|
114
|
+
: null;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
size: size ?? null,
|
|
118
|
+
type: type || null,
|
|
119
|
+
lastModified: lastModified ?? null
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_toStringOrNull(value) {
|
|
124
|
+
if (value === undefined || value === null) return null;
|
|
125
|
+
const normalized = String(value).trim();
|
|
126
|
+
return normalized ? normalized : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_toNumberOrNull(value) {
|
|
130
|
+
if (value === undefined || value === null || value === '') return null;
|
|
131
|
+
const normalized = Number(value);
|
|
132
|
+
return Number.isFinite(normalized) ? normalized : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
63
135
|
_setTemplateInBuffer($items) {
|
|
64
136
|
if (this.bufferTemplate || $items.length === 0) return;
|
|
65
137
|
|
|
66
138
|
const firstItem = $items[0];
|
|
67
139
|
|
|
68
|
-
// Если нет data-file — шаблон извлекается и элемент удаляется
|
|
69
140
|
if (!Manipulator.has(firstItem, 'data-file')) {
|
|
70
141
|
this.bufferTemplate = firstItem.outerHTML;
|
|
71
142
|
firstItem.remove();
|
|
@@ -80,4 +151,4 @@ class VGFilesTemplateRender {
|
|
|
80
151
|
}
|
|
81
152
|
}
|
|
82
153
|
|
|
83
|
-
export default VGFilesTemplateRender;
|
|
154
|
+
export default VGFilesTemplateRender;
|
|
@@ -41,21 +41,21 @@ class VGFiles extends VGFilesBase {
|
|
|
41
41
|
types: [],
|
|
42
42
|
ajax: false,
|
|
43
43
|
prepend: true,
|
|
44
|
-
replace: true,
|
|
45
|
-
rename: false,
|
|
46
|
-
smartdrop: false,
|
|
47
|
-
uploads: {
|
|
48
|
-
mode: 'sequential',
|
|
49
|
-
route: '',
|
|
44
|
+
replace: true,
|
|
45
|
+
rename: false,
|
|
46
|
+
smartdrop: false,
|
|
47
|
+
uploads: {
|
|
48
|
+
mode: 'sequential',
|
|
49
|
+
route: '',
|
|
50
50
|
maxParallel: 3,
|
|
51
51
|
maxConcurrent: 1,
|
|
52
52
|
retryAttempts: 1,
|
|
53
53
|
retryDelay: 1000,
|
|
54
54
|
},
|
|
55
|
-
removes: {
|
|
56
|
-
all: { route: '', alert: true, toast: true },
|
|
57
|
-
single: { route: '', alert: true, toast: true }
|
|
58
|
-
},
|
|
55
|
+
removes: {
|
|
56
|
+
all: { route: '', alert: true, toast: true, confirm: null },
|
|
57
|
+
single: { route: '', alert: true, toast: true, confirm: null }
|
|
58
|
+
},
|
|
59
59
|
sortable: {
|
|
60
60
|
enabled: false,
|
|
61
61
|
route: '',
|
|
@@ -106,67 +106,91 @@ class VGFiles extends VGFilesBase {
|
|
|
106
106
|
if (this.isRenderNonInit) return;
|
|
107
107
|
this.isRenderNonInit = this._render.init();
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
const parsedFiles = this._render.parsedFiles;
|
|
110
|
+
|
|
111
|
+
if (parsedFiles.length) {
|
|
112
|
+
this._addExternalFiles(parsedFiles);
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
if (this._params.allowed && !this._params.ajax) this._params.detach = false;
|
|
114
|
-
if (this._nodes.drop) {
|
|
115
|
-
this._params.image = true;
|
|
116
|
-
this._params.detach = true;
|
|
117
|
-
this._setDropActiveText();
|
|
118
|
-
|
|
119
|
-
VGFilesDroppable.getOrCreateInstance(this._nodes.drop, this._params).init();
|
|
120
|
-
}
|
|
115
|
+
if (this._params.allowed && !this._params.ajax) this._params.detach = false;
|
|
116
|
+
if (this._nodes.drop) {
|
|
117
|
+
this._params.image = true;
|
|
118
|
+
this._params.detach = true;
|
|
119
|
+
this._setDropActiveText();
|
|
121
120
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
title
|
|
139
|
-
|
|
121
|
+
VGFilesDroppable.getOrCreateInstance(this._nodes.drop, this._params).init();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this._addEventListenerExtended();
|
|
125
|
+
this._renderStat();
|
|
126
|
+
this._triggerCallback('onInit', { element: this._element, files: parsedFiles || [] });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_setDropActiveText() {
|
|
130
|
+
if (!this._nodes.drop) return;
|
|
131
|
+
|
|
132
|
+
const messages = lang_messages(this._params.lang, NAME) || {};
|
|
133
|
+
const activeText = (this._nodes.drop.getAttribute('data-drop-active-text') || '').trim() || messages['drop-active'] || 'Release to upload';
|
|
134
|
+
this._nodes.drop.setAttribute('data-drop-active-text', activeText);
|
|
135
|
+
|
|
136
|
+
const title = Selectors.find('.vg-files-drop-message .title', this._nodes.drop);
|
|
137
|
+
if (!title) return;
|
|
138
|
+
|
|
139
|
+
const originalText = (title.getAttribute('data-drop-original-text') || '').trim() || (title.textContent || '').trim();
|
|
140
|
+
title.setAttribute('data-drop-original-text', originalText);
|
|
141
|
+
}
|
|
140
142
|
|
|
141
143
|
_addExternalFiles(files) {
|
|
142
144
|
if (!Array.isArray(files) || !files.length) return;
|
|
143
145
|
|
|
144
146
|
files.forEach(fileData => {
|
|
145
|
-
|
|
146
|
-
type: fileData.type || "application/octet-stream",
|
|
147
|
-
lastModified: fileData.lastModified || Date.now()
|
|
148
|
-
});
|
|
147
|
+
if (!fileData?.name) return;
|
|
149
148
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
writable: true,
|
|
154
|
-
enumerable: true
|
|
155
|
-
});
|
|
149
|
+
const fileOptions = {};
|
|
150
|
+
if (typeof fileData.type === 'string') fileOptions.type = fileData.type;
|
|
151
|
+
if (Number.isFinite(fileData.lastModified)) fileOptions.lastModified = fileData.lastModified;
|
|
156
152
|
|
|
157
|
-
|
|
158
|
-
Object.defineProperty(file, 'size', {
|
|
159
|
-
value: fileData.size,
|
|
160
|
-
writable: true,
|
|
161
|
-
enumerable: true
|
|
162
|
-
});
|
|
153
|
+
const file = new File([""], fileData.name, fileOptions);
|
|
163
154
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
155
|
+
if (fileData.id !== undefined && fileData.id !== null && fileData.id !== '') {
|
|
156
|
+
Object.defineProperty(file, 'id', {
|
|
157
|
+
value: fileData.id,
|
|
158
|
+
writable: true,
|
|
159
|
+
enumerable: true
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (Number.isFinite(fileData.size)) {
|
|
164
|
+
Object.defineProperty(file, 'size', {
|
|
165
|
+
value: fileData.size,
|
|
166
|
+
writable: true,
|
|
167
|
+
enumerable: true
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (fileData.src !== undefined && fileData.src !== null && fileData.src !== '') {
|
|
172
|
+
Object.defineProperty(file, 'src', {
|
|
173
|
+
value: fileData.src,
|
|
174
|
+
writable: true,
|
|
175
|
+
enumerable: true
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (fileData.image !== undefined && fileData.image !== null && fileData.image !== '') {
|
|
180
|
+
Object.defineProperty(file, 'image', {
|
|
181
|
+
value: fileData.image,
|
|
182
|
+
writable: true,
|
|
183
|
+
enumerable: true
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (fileData.customData && typeof fileData.customData === 'object' && !Array.isArray(fileData.customData)) {
|
|
188
|
+
Object.defineProperty(file, 'customData', {
|
|
189
|
+
value: fileData.customData,
|
|
190
|
+
writable: true,
|
|
191
|
+
enumerable: true
|
|
192
|
+
});
|
|
193
|
+
}
|
|
170
194
|
|
|
171
195
|
const fileKey = this._getFileKey(file);
|
|
172
196
|
|
|
@@ -207,32 +231,32 @@ class VGFiles extends VGFilesBase {
|
|
|
207
231
|
});
|
|
208
232
|
}
|
|
209
233
|
|
|
210
|
-
_handleChange(e) {
|
|
211
|
-
const input = e?.target;
|
|
212
|
-
const inputFiles = this._snapshotInputFiles(input);
|
|
213
|
-
this.change(input);
|
|
214
|
-
|
|
215
|
-
if (this._params.ajax) this.uploadAll(this._files);
|
|
216
|
-
|
|
217
|
-
const payload = {
|
|
218
|
-
files: this._files,
|
|
219
|
-
input: e?.target || e?.src || '',
|
|
220
|
-
inputFiles
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
this._triggerCallback('onChange', payload);
|
|
224
|
-
this._triggerEvent('change', {
|
|
225
|
-
files: this._files,
|
|
226
|
-
input: payload.input,
|
|
227
|
-
inputFiles: payload.inputFiles
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
_snapshotInputFiles(input) {
|
|
232
|
-
const files = input?.files;
|
|
233
|
-
if (!files?.length) return [];
|
|
234
|
-
return Array.from(files);
|
|
235
|
-
}
|
|
234
|
+
_handleChange(e) {
|
|
235
|
+
const input = e?.target;
|
|
236
|
+
const inputFiles = this._snapshotInputFiles(input);
|
|
237
|
+
this.change(input);
|
|
238
|
+
|
|
239
|
+
if (this._params.ajax) this.uploadAll(this._files);
|
|
240
|
+
|
|
241
|
+
const payload = {
|
|
242
|
+
files: this._files,
|
|
243
|
+
input: e?.target || e?.src || '',
|
|
244
|
+
inputFiles
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
this._triggerCallback('onChange', payload);
|
|
248
|
+
this._triggerEvent('change', {
|
|
249
|
+
files: this._files,
|
|
250
|
+
input: payload.input,
|
|
251
|
+
inputFiles: payload.inputFiles
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_snapshotInputFiles(input) {
|
|
256
|
+
const files = input?.files;
|
|
257
|
+
if (!files?.length) return [];
|
|
258
|
+
return Array.from(files);
|
|
259
|
+
}
|
|
236
260
|
|
|
237
261
|
async uploadAll(files) {
|
|
238
262
|
if (!this._params.ajax || !this._params.uploads.route) return;
|
|
@@ -501,8 +525,8 @@ class VGFiles extends VGFilesBase {
|
|
|
501
525
|
});
|
|
502
526
|
}
|
|
503
527
|
|
|
504
|
-
reload(button) {
|
|
505
|
-
if (!this._params.ajax || !this._params.uploads.route) return;
|
|
528
|
+
reload(button) {
|
|
529
|
+
if (!this._params.ajax || !this._params.uploads.route) return;
|
|
506
530
|
|
|
507
531
|
const dataButton = Manipulator.get(button, 'data');
|
|
508
532
|
const fileData = {
|
|
@@ -532,10 +556,85 @@ class VGFiles extends VGFilesBase {
|
|
|
532
556
|
this._triggerCallback('onReload', payload);
|
|
533
557
|
this._triggerEvent('reload', payload);
|
|
534
558
|
|
|
535
|
-
this.upload(fileToRetry);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
|
|
559
|
+
this.upload(fileToRetry);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
_runAjaxRequest(paramsAjax, callback) {
|
|
563
|
+
const previousAjax = this._params.ajax;
|
|
564
|
+
this._params.ajax = paramsAjax;
|
|
565
|
+
|
|
566
|
+
this._route((status, data) => {
|
|
567
|
+
this._params.ajax = previousAjax;
|
|
568
|
+
callback(status, data);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
_normalizeConfirmResult(result) {
|
|
573
|
+
if (result === true) return { accepted: true, data: null };
|
|
574
|
+
if (result === false || result == null) return { accepted: false, data: null };
|
|
575
|
+
|
|
576
|
+
if (typeof result === 'object') {
|
|
577
|
+
if (typeof result.accepted === 'boolean') {
|
|
578
|
+
return { accepted: result.accepted, data: result.data ?? null };
|
|
579
|
+
}
|
|
580
|
+
if (Object.prototype.hasOwnProperty.call(result, 'data')) {
|
|
581
|
+
return { accepted: true, data: result.data ?? null };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return { accepted: Boolean(result), data: null };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
_runDefaultRemoveConfirm(trigger, params) {
|
|
589
|
+
return new Promise((resolve) => {
|
|
590
|
+
VGAlert.confirm(trigger, {
|
|
591
|
+
lang: params.lang,
|
|
592
|
+
ajax: params.ajax,
|
|
593
|
+
buttons: params.buttons,
|
|
594
|
+
message: params.message
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
EventHandler.one(trigger, 'vg.alert.accept', (event) => {
|
|
598
|
+
resolve({ accepted: true, data: event?.vgalert?.data ?? null });
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
EventHandler.one(trigger, 'vg.alert.reject', () => {
|
|
602
|
+
resolve({ accepted: false, data: null });
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
_confirmRemove(type, trigger, ajax, message) {
|
|
608
|
+
const buttons = {
|
|
609
|
+
agree: {
|
|
610
|
+
text: lang_buttons(this._params.lang, NAME)['agree'],
|
|
611
|
+
class: ["btn-danger"],
|
|
612
|
+
},
|
|
613
|
+
cancel: {
|
|
614
|
+
text: lang_buttons(this._params.lang, NAME)['cancel'],
|
|
615
|
+
class: ["btn-outline-danger"],
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const confirmParams = {
|
|
620
|
+
type,
|
|
621
|
+
trigger,
|
|
622
|
+
lang: this._params.lang,
|
|
623
|
+
ajax,
|
|
624
|
+
buttons,
|
|
625
|
+
message
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const customConfirm = this._params?.removes?.[type]?.confirm;
|
|
629
|
+
if (typeof customConfirm === 'function') {
|
|
630
|
+
return Promise.resolve(customConfirm(confirmParams, this))
|
|
631
|
+
.then((result) => this._normalizeConfirmResult(result));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return this._runDefaultRemoveConfirm(trigger, confirmParams);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
removeFile(button) {
|
|
539
638
|
const name = normalizeData(Manipulator.get(button, 'data-name'));
|
|
540
639
|
const size = normalizeData(Manipulator.get(button, 'data-size'));
|
|
541
640
|
const id = normalizeData(Manipulator.get(button, 'data-id'));
|
|
@@ -569,7 +668,9 @@ class VGFiles extends VGFilesBase {
|
|
|
569
668
|
if (this._params.ajax && this._params.removes.single.route) {
|
|
570
669
|
if (!id) return;
|
|
571
670
|
|
|
572
|
-
const
|
|
671
|
+
const routeBase = this._params.removes.single.route;
|
|
672
|
+
const routeSeparator = routeBase.includes('?') ? '&' : '?';
|
|
673
|
+
const route = `${routeBase}${routeSeparator}id=${encodeURIComponent(id)}`;
|
|
573
674
|
const paramsAjax = {
|
|
574
675
|
route: route,
|
|
575
676
|
method: 'delete'
|
|
@@ -590,35 +691,31 @@ class VGFiles extends VGFilesBase {
|
|
|
590
691
|
}
|
|
591
692
|
};
|
|
592
693
|
|
|
593
|
-
if (this._params.removes.single.alert) {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
this._route((status, data) => {
|
|
619
|
-
_completeRemoveFile(data);
|
|
620
|
-
});
|
|
621
|
-
}
|
|
694
|
+
if (this._params.removes.single.alert) {
|
|
695
|
+
const message = {
|
|
696
|
+
title: lang_messages(this._params.lang, NAME)['title'],
|
|
697
|
+
description: lang_messages(this._params.lang, NAME)['description']
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
this._confirmRemove('single', button, paramsAjax, message)
|
|
701
|
+
.then((result) => {
|
|
702
|
+
if (!result.accepted) return;
|
|
703
|
+
|
|
704
|
+
if (result.data) {
|
|
705
|
+
_completeRemoveFile(result.data);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
this._runAjaxRequest(paramsAjax, (status, data) => {
|
|
710
|
+
_completeRemoveFile(data);
|
|
711
|
+
});
|
|
712
|
+
})
|
|
713
|
+
.catch(() => {});
|
|
714
|
+
} else {
|
|
715
|
+
this._runAjaxRequest(paramsAjax, (status, data) => {
|
|
716
|
+
_completeRemoveFile(data);
|
|
717
|
+
});
|
|
718
|
+
}
|
|
622
719
|
} else {
|
|
623
720
|
this._files = this._files.filter(f => !(f.name === name && f.size === size));
|
|
624
721
|
this._updateStatsAfterRemove();
|
|
@@ -682,15 +779,15 @@ class VGFiles extends VGFilesBase {
|
|
|
682
779
|
|
|
683
780
|
return this._tpl.button([
|
|
684
781
|
this._tpl.i({}, icon, { isHTML: true })
|
|
685
|
-
], 'button', {
|
|
782
|
+
], 'button', this._buildFileDataAttributes(file, {
|
|
686
783
|
type: 'button',
|
|
687
784
|
[action]: 'file',
|
|
688
785
|
'data-name': file.name,
|
|
689
|
-
'data-size': file.size,
|
|
690
|
-
'data-type': file.type,
|
|
691
|
-
'data-last-modified': file.lastModified,
|
|
786
|
+
'data-size': file.size ?? 0,
|
|
787
|
+
'data-type': file.type || '',
|
|
788
|
+
'data-last-modified': file.lastModified || '',
|
|
692
789
|
'data-id': file.id || ''
|
|
693
|
-
});
|
|
790
|
+
}));
|
|
694
791
|
}
|
|
695
792
|
|
|
696
793
|
_renderStat() {
|
|
@@ -835,37 +932,37 @@ EventHandler.on(document, `click.${NAME_KEY}.data.api`, SELECTOR_DATA_DISMISS_AL
|
|
|
835
932
|
instance.clear(true, true);
|
|
836
933
|
};
|
|
837
934
|
|
|
838
|
-
if (instance._params.removes.all.alert) {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
} else {
|
|
865
|
-
instance.
|
|
866
|
-
if (instance._params.removes.all.toast && data?.response?.message) {
|
|
867
|
-
VGToast.run(data.response.message);
|
|
868
|
-
}
|
|
935
|
+
if (instance._params.removes.all.alert) {
|
|
936
|
+
const message = {
|
|
937
|
+
title: lang_messages(instance._params.lang, NAME)['title'],
|
|
938
|
+
description: lang_messages(instance._params.lang, NAME)['descriptions']
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
instance._confirmRemove('all', e.target, paramsAjax, message)
|
|
942
|
+
.then((result) => {
|
|
943
|
+
if (!result.accepted) return;
|
|
944
|
+
|
|
945
|
+
if (result.data) {
|
|
946
|
+
if (instance._params.removes.all.toast) {
|
|
947
|
+
VGToast.run(result.data?.response?.message);
|
|
948
|
+
}
|
|
949
|
+
_completeClearAll();
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
instance._runAjaxRequest(paramsAjax, (status, data) => {
|
|
954
|
+
if (instance._params.removes.all.toast && data?.response?.message) {
|
|
955
|
+
VGToast.run(data.response.message);
|
|
956
|
+
}
|
|
957
|
+
_completeClearAll();
|
|
958
|
+
});
|
|
959
|
+
})
|
|
960
|
+
.catch(() => {});
|
|
961
|
+
} else {
|
|
962
|
+
instance._runAjaxRequest(paramsAjax, (status, data) => {
|
|
963
|
+
if (instance._params.removes.all.toast && data?.response?.message) {
|
|
964
|
+
VGToast.run(data.response.message);
|
|
965
|
+
}
|
|
869
966
|
_completeClearAll();
|
|
870
967
|
});
|
|
871
968
|
}
|
|
@@ -874,4 +971,4 @@ EventHandler.on(document, `click.${NAME_KEY}.data.api`, SELECTOR_DATA_DISMISS_AL
|
|
|
874
971
|
}
|
|
875
972
|
});
|
|
876
973
|
|
|
877
|
-
export default VGFiles;
|
|
974
|
+
export default VGFiles;
|
|
@@ -62,9 +62,13 @@ const files = new VGFiles(document.querySelector('.vg-files'), {
|
|
|
62
62
|
| `uploads.retryAttempts` | `number` | `1` | Кол-во попыток повтора при ошибке |
|
|
63
63
|
| `uploads.retryDelay` | `number` | `1000` | Задержка между попытками (мс) |
|
|
64
64
|
| `removes.single.route` | `string` | `''` | URL для удаления одного файла |
|
|
65
|
-
| `removes.all.route` | `string` | `''` | URL для удаления всех файлов |
|
|
66
|
-
| `removes.
|
|
67
|
-
| `removes.
|
|
65
|
+
| `removes.all.route` | `string` | `''` | URL для удаления всех файлов |
|
|
66
|
+
| `removes.all.alert` | `boolean` | `true` | Показывать подтверждение при удалении всех файлов |
|
|
67
|
+
| `removes.all.toast` | `boolean` | `true` | Показывать уведомление после удаления всех файлов |
|
|
68
|
+
| `removes.all.confirm` | `function \| null` | `null` | Кастомный confirm-обработчик для удаления всех файлов |
|
|
69
|
+
| `removes.single.alert` | `boolean` | `true` | Показывать подтверждение при удалении |
|
|
70
|
+
| `removes.single.toast` | `boolean` | `true` | Показывать уведомление после удаления |
|
|
71
|
+
| `removes.single.confirm` | `function \| null` | `null` | Кастомный confirm-обработчик для удаления одного файла |
|
|
68
72
|
| `sortable.enabled` | `boolean` | `false` | Включить сортировку |
|
|
69
73
|
| `sortable.route` | `string` | `''` | URL для сохранения порядка файлов |
|
|
70
74
|
| `callbacks` | `object` | `null` | Колбэки на события (см. ниже) |
|
|
@@ -116,7 +120,34 @@ const files = new VGFiles(document.querySelector('.vg-files'), {
|
|
|
116
120
|
- **Удаление одного файла**: кнопка с `data-vg-dismiss="file"` → вызов `removeFile()`.
|
|
117
121
|
- **Очистка всех**: кнопка с `data-vg-dismiss="vg-files"` → `clear()`.
|
|
118
122
|
|
|
119
|
-
Поддержка подтверждения через `VGAlert` и уведомлений через `VGToast`.
|
|
123
|
+
Поддержка подтверждения через `VGAlert` и уведомлений через `VGToast`.
|
|
124
|
+
|
|
125
|
+
### Кастомный confirm для удаления
|
|
126
|
+
|
|
127
|
+
Если задан `removes.single.confirm` или `removes.all.confirm`, модуль вызовет вашу функцию вместо дефолтного `VGAlert`.
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
new VGFiles(document.querySelector('.vg-files'), {
|
|
131
|
+
ajax: true,
|
|
132
|
+
removes: {
|
|
133
|
+
single: {
|
|
134
|
+
route: '/api/file/remove',
|
|
135
|
+
alert: true,
|
|
136
|
+
confirm: async ({ message }) => {
|
|
137
|
+
const accepted = window.confirm(`${message.title}\n${message.description}`);
|
|
138
|
+
return { accepted };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Контракт функции confirm:
|
|
146
|
+
- Вход: объект `{ type, trigger, lang, ajax, buttons, message }`.
|
|
147
|
+
- Выход:
|
|
148
|
+
`true` или `{ accepted: true }` — подтвердить;
|
|
149
|
+
`false` или `{ accepted: false }` — отменить;
|
|
150
|
+
`{ accepted: true, data }` — подтвердить и передать готовый ответ удаления (если запрос уже выполнен внутри confirm).
|
|
120
151
|
|
|
121
152
|
---
|
|
122
153
|
|
|
@@ -52,6 +52,10 @@ class VGNav extends BaseModule {
|
|
|
52
52
|
breakpoint: 'lg',
|
|
53
53
|
placement: 'horizontal',
|
|
54
54
|
hover: true,
|
|
55
|
+
hoversmoothfirstlevel: {
|
|
56
|
+
enable: false,
|
|
57
|
+
horizontalOnly: true
|
|
58
|
+
},
|
|
55
59
|
animation: {
|
|
56
60
|
enable: true,
|
|
57
61
|
timeout: 700
|
|
@@ -98,6 +102,7 @@ class VGNav extends BaseModule {
|
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
this._openDrops = new Map();
|
|
105
|
+
this._pointerPosition = null;
|
|
101
106
|
this._handleScroll = this._handleScroll.bind(this);
|
|
102
107
|
this._handleResize = this._handleResize.bind(this);
|
|
103
108
|
}
|
|
@@ -333,6 +338,50 @@ class VGNav extends BaseModule {
|
|
|
333
338
|
return vertInView && horInView;
|
|
334
339
|
}
|
|
335
340
|
|
|
341
|
+
_updatePointerPosition(event) {
|
|
342
|
+
if (!event || typeof event.clientX !== 'number' || typeof event.clientY !== 'number') return;
|
|
343
|
+
this._pointerPosition = {
|
|
344
|
+
x: event.clientX,
|
|
345
|
+
y: event.clientY
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_isHorizontalPointerMove(event) {
|
|
350
|
+
if (!this._pointerPosition || !event) return false;
|
|
351
|
+
|
|
352
|
+
const dx = event.clientX - this._pointerPosition.x;
|
|
353
|
+
const dy = event.clientY - this._pointerPosition.y;
|
|
354
|
+
|
|
355
|
+
return Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
_isFirstLevelDropdown(drop) {
|
|
359
|
+
return !!drop && !drop.closest('.dropdown-content');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
_hasDirectDropdownContent(drop) {
|
|
363
|
+
if (!drop || !drop.children) return false;
|
|
364
|
+
return [...drop.children].some((child) => child.classList && child.classList.contains('dropdown-content'));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_isAdjacentDropdown(drop, relatedDrop) {
|
|
368
|
+
if (!drop || !relatedDrop) return false;
|
|
369
|
+
if (drop.parentElement !== relatedDrop.parentElement) return false;
|
|
370
|
+
return drop.previousElementSibling === relatedDrop || drop.nextElementSibling === relatedDrop;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
_canSmoothSwitchFirstLevel(event, currentDrop, relatedDrop) {
|
|
374
|
+
const smoothParams = this._params.hoversmoothfirstlevel || {};
|
|
375
|
+
if (!smoothParams.enable) return false;
|
|
376
|
+
if (!currentDrop || !relatedDrop || currentDrop === relatedDrop) return false;
|
|
377
|
+
if (!this._isFirstLevelDropdown(currentDrop) || !this._isFirstLevelDropdown(relatedDrop)) return false;
|
|
378
|
+
if (!this._hasDirectDropdownContent(currentDrop) || !this._hasDirectDropdownContent(relatedDrop)) return false;
|
|
379
|
+
if (!this._isAdjacentDropdown(currentDrop, relatedDrop)) return false;
|
|
380
|
+
if (smoothParams.horizontalOnly && !this._isHorizontalPointerMove(event)) return false;
|
|
381
|
+
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
336
385
|
static init(element, params = {}) {
|
|
337
386
|
const instance = VGNav.getOrCreateInstance(element, params);
|
|
338
387
|
instance.build();
|
|
@@ -340,12 +389,15 @@ class VGNav extends BaseModule {
|
|
|
340
389
|
let drops = Selectors.findAll('.dropdown', instance.navigation);
|
|
341
390
|
|
|
342
391
|
if (instance._params.hover && !isMobileDevice()) {
|
|
392
|
+
EventHandler.on(instance.navigation, `mousemove.${NAME_KEY}.data.api`, function (event) {
|
|
393
|
+
instance._updatePointerPosition(event);
|
|
394
|
+
});
|
|
395
|
+
|
|
343
396
|
[...drops].forEach(function (el) {
|
|
344
397
|
let currentElem = null;
|
|
345
398
|
|
|
346
399
|
EventHandler.on(el, EVENT_MOUSEOVER_DATA_API, function (event) {
|
|
347
400
|
if (currentElem) return;
|
|
348
|
-
VGNav.hideOpenDrops(event);
|
|
349
401
|
|
|
350
402
|
let target = event.target.closest('.dropdown');
|
|
351
403
|
if (!target) return;
|
|
@@ -353,17 +405,31 @@ class VGNav extends BaseModule {
|
|
|
353
405
|
if (!instance.navigation.contains(target)) return;
|
|
354
406
|
currentElem = target;
|
|
355
407
|
|
|
408
|
+
const previousDrop = event.relatedTarget?.closest('.dropdown');
|
|
409
|
+
const useSmoothSwitch = instance._canSmoothSwitchFirstLevel(event, target, previousDrop);
|
|
410
|
+
|
|
411
|
+
if (!useSmoothSwitch) {
|
|
412
|
+
VGNav.hideOpenDrops(event);
|
|
413
|
+
}
|
|
414
|
+
|
|
356
415
|
let relatedTarget = {
|
|
357
416
|
relatedTarget: target
|
|
358
417
|
};
|
|
359
418
|
|
|
360
419
|
instance.show(relatedTarget);
|
|
420
|
+
|
|
421
|
+
if (useSmoothSwitch && previousDrop && previousDrop.classList.contains(CLASS_NAME_ACTIVE)) {
|
|
422
|
+
instance.hide({ relatedTarget: previousDrop });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
instance._updatePointerPosition(event);
|
|
361
426
|
});
|
|
362
427
|
|
|
363
428
|
EventHandler.on(el, EVENT_MOUSEOUT_DATA_API, function (event) {
|
|
364
429
|
if (!currentElem) return;
|
|
365
430
|
|
|
366
|
-
let
|
|
431
|
+
let nextDrop = event.relatedTarget?.closest('.dropdown'),
|
|
432
|
+
relatedTarget = nextDrop,
|
|
367
433
|
elm = currentElem;
|
|
368
434
|
|
|
369
435
|
while (relatedTarget) {
|
|
@@ -371,8 +437,15 @@ class VGNav extends BaseModule {
|
|
|
371
437
|
relatedTarget = relatedTarget.parentNode;
|
|
372
438
|
}
|
|
373
439
|
|
|
440
|
+
if (instance._canSmoothSwitchFirstLevel(event, elm, nextDrop)) {
|
|
441
|
+
currentElem = null;
|
|
442
|
+
instance._updatePointerPosition(event);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
374
446
|
currentElem = null;
|
|
375
447
|
instance.hide({ relatedTarget: relatedTarget, elm: elm });
|
|
448
|
+
instance._updatePointerPosition(event);
|
|
376
449
|
});
|
|
377
450
|
});
|
|
378
451
|
}
|