vgapp 1.0.2 → 1.0.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.
|
@@ -138,7 +138,7 @@ class VGFilesBase extends BaseModule {
|
|
|
138
138
|
isSingle;
|
|
139
139
|
|
|
140
140
|
if (shouldReplaceOnSingle) {
|
|
141
|
-
this.clear();
|
|
141
|
+
this.clear(false);
|
|
142
142
|
this.append(filesArray, true);
|
|
143
143
|
this._revokeUrls();
|
|
144
144
|
this._cleanupFakeInputs();
|
|
@@ -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);
|
|
@@ -398,6 +454,12 @@ class VGFilesBase extends BaseModule {
|
|
|
398
454
|
if ($message) {
|
|
399
455
|
if (files.length) Classes.add($message, 'has-files');
|
|
400
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
|
+
}
|
|
401
463
|
}
|
|
402
464
|
|
|
403
465
|
this._nodes.drop.appendChild($list);
|
|
@@ -429,14 +491,21 @@ class VGFilesBase extends BaseModule {
|
|
|
429
491
|
if (this._params.detach) classes.push('with-remove')
|
|
430
492
|
if (this._params.sortable.enabled) classes.push('with-sortable');
|
|
431
493
|
|
|
432
|
-
let parts = [];
|
|
433
|
-
$itemsTemplate.forEach(tmpl => {
|
|
434
|
-
const part = this._renderTemplatePart(tmpl.element, file, i);
|
|
435
|
-
if (part) parts.push(part);
|
|
436
|
-
});
|
|
494
|
+
let parts = [];
|
|
495
|
+
$itemsTemplate.forEach(tmpl => {
|
|
496
|
+
const part = this._renderTemplatePart(tmpl.element, file, i);
|
|
497
|
+
if (part) parts.push(part);
|
|
498
|
+
});
|
|
437
499
|
|
|
438
500
|
const $li = this._tpl.li(
|
|
439
|
-
|
|
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
|
|
440
509
|
);
|
|
441
510
|
fragment.appendChild($li);
|
|
442
511
|
});
|
|
@@ -445,38 +514,38 @@ class VGFilesBase extends BaseModule {
|
|
|
445
514
|
Classes.add(this._nodes.info, 'show')
|
|
446
515
|
}
|
|
447
516
|
|
|
448
|
-
_renderTemplatePart(element, file, index = null, options = {}) {
|
|
449
|
-
if (!element) return null;
|
|
450
|
-
const { isDrop = false } = options;
|
|
451
|
-
|
|
452
|
-
const classList = element?.classList;
|
|
453
|
-
|
|
454
|
-
if (classList?.contains('file-image')) return this._renderUIImage(file);
|
|
455
|
-
if (classList?.contains('file-info')) {
|
|
456
|
-
if (isDrop) return null;
|
|
457
|
-
return this._renderUIInfo(file, index);
|
|
458
|
-
}
|
|
459
|
-
if (classList?.contains('file-remove')) return this._wrapInFileCustom(this._renderUIDetach(file));
|
|
460
|
-
|
|
461
|
-
const $part = element.cloneNode(true);
|
|
462
|
-
|
|
463
|
-
this._replaceTemplateSlot($part, '.file-image', () => this._renderUIImage(file));
|
|
464
|
-
this._replaceTemplateSlot($part, '.file-info', () => isDrop ? null : this._renderUIInfo(file, index));
|
|
465
|
-
this._replaceTemplateSlot($part, '.file-remove', () => this._renderUIDetach(file));
|
|
466
|
-
|
|
467
|
-
return this._wrapInFileCustom($part);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
_wrapInFileCustom(node) {
|
|
471
|
-
if (!node) return null;
|
|
472
|
-
if (node.classList?.contains('file-custom')) return node;
|
|
473
|
-
|
|
474
|
-
const wrapper = document.createElement('div');
|
|
475
|
-
wrapper.className = 'file-custom';
|
|
476
|
-
wrapper.appendChild(node);
|
|
477
|
-
|
|
478
|
-
return wrapper;
|
|
479
|
-
}
|
|
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
|
+
}
|
|
480
549
|
|
|
481
550
|
_replaceTemplateSlot(container, selector, renderer) {
|
|
482
551
|
const isMatchSelf = container?.matches && container.matches(selector);
|
|
@@ -580,9 +649,11 @@ class VGFilesBase extends BaseModule {
|
|
|
580
649
|
Selectors.findAll('[data-vg-files="generated"]', this._element).forEach(el => el.remove());
|
|
581
650
|
}
|
|
582
651
|
|
|
583
|
-
clear() {
|
|
652
|
+
clear(resetInput = true) {
|
|
584
653
|
this._revokeUrls();
|
|
585
|
-
|
|
654
|
+
if (resetInput) {
|
|
655
|
+
this._resetFileInput();
|
|
656
|
+
}
|
|
586
657
|
this._cleanupFakeInputs();
|
|
587
658
|
this._cleanupErrors();
|
|
588
659
|
this._files = [];
|
|
@@ -597,7 +668,14 @@ class VGFilesBase extends BaseModule {
|
|
|
597
668
|
if ($list) $list.innerHTML = '';
|
|
598
669
|
|
|
599
670
|
const $message = Selectors.find(`.${this._getClass('drop-message')}`, this._element);
|
|
600
|
-
if ($message)
|
|
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
|
+
}
|
|
601
679
|
|
|
602
680
|
Classes.remove(this._nodes.drop, 'active');
|
|
603
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,12 +41,12 @@ 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,
|
|
@@ -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);
|
|
113
|
+
}
|
|
114
|
+
|
|
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();
|
|
120
|
+
|
|
121
|
+
VGFilesDroppable.getOrCreateInstance(this._nodes.drop, this._params).init();
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const activeText = (this._nodes.drop.getAttribute('data-drop-active-text') || '').trim() || messages['drop-active'] || 'Release to upload';
|
|
132
|
-
this._nodes.drop.setAttribute('data-drop-active-text', activeText);
|
|
133
|
-
|
|
134
|
-
const title = Selectors.find('.vg-files-drop-message .title', this._nodes.drop);
|
|
135
|
-
if (!title) return;
|
|
136
|
-
|
|
137
|
-
const originalText = (title.getAttribute('data-drop-original-text') || '').trim() || (title.textContent || '').trim();
|
|
138
|
-
title.setAttribute('data-drop-original-text', originalText);
|
|
139
|
-
}
|
|
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
|
|
|
@@ -209,14 +233,29 @@ class VGFiles extends VGFilesBase {
|
|
|
209
233
|
|
|
210
234
|
_handleChange(e) {
|
|
211
235
|
const input = e?.target;
|
|
236
|
+
const inputFiles = this._snapshotInputFiles(input);
|
|
212
237
|
this.change(input);
|
|
213
238
|
|
|
214
239
|
if (this._params.ajax) this.uploadAll(this._files);
|
|
215
240
|
|
|
216
|
-
const payload = {
|
|
241
|
+
const payload = {
|
|
242
|
+
files: this._files,
|
|
243
|
+
input: e?.target || e?.src || '',
|
|
244
|
+
inputFiles
|
|
245
|
+
};
|
|
217
246
|
|
|
218
247
|
this._triggerCallback('onChange', payload);
|
|
219
|
-
this._triggerEvent('change', {
|
|
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);
|
|
220
259
|
}
|
|
221
260
|
|
|
222
261
|
async uploadAll(files) {
|
|
@@ -667,15 +706,15 @@ class VGFiles extends VGFilesBase {
|
|
|
667
706
|
|
|
668
707
|
return this._tpl.button([
|
|
669
708
|
this._tpl.i({}, icon, { isHTML: true })
|
|
670
|
-
], 'button', {
|
|
709
|
+
], 'button', this._buildFileDataAttributes(file, {
|
|
671
710
|
type: 'button',
|
|
672
711
|
[action]: 'file',
|
|
673
712
|
'data-name': file.name,
|
|
674
|
-
'data-size': file.size,
|
|
675
|
-
'data-type': file.type,
|
|
676
|
-
'data-last-modified': file.lastModified,
|
|
713
|
+
'data-size': file.size ?? 0,
|
|
714
|
+
'data-type': file.type || '',
|
|
715
|
+
'data-last-modified': file.lastModified || '',
|
|
677
716
|
'data-id': file.id || ''
|
|
678
|
-
});
|
|
717
|
+
}));
|
|
679
718
|
}
|
|
680
719
|
|
|
681
720
|
_renderStat() {
|
|
@@ -859,4 +898,4 @@ EventHandler.on(document, `click.${NAME_KEY}.data.api`, SELECTOR_DATA_DISMISS_AL
|
|
|
859
898
|
}
|
|
860
899
|
});
|
|
861
900
|
|
|
862
|
-
export default VGFiles;
|
|
901
|
+
export default VGFiles;
|
|
@@ -84,7 +84,7 @@ const files = new VGFiles(document.querySelector('.vg-files'), {
|
|
|
84
84
|
| Колбэк | Параметры | Описание |
|
|
85
85
|
|-------|----------|--------|
|
|
86
86
|
| `onInit` | `(data)` | Инициализация завершена |
|
|
87
|
-
| `onChange` | `{ files, input }` | Изменён список файлов |
|
|
87
|
+
| `onChange` | `{ files, input, inputFiles }` | Изменён список файлов (`inputFiles` — снимок исходных `input.files`) |
|
|
88
88
|
| `onUploadStart` | `{ files, total }` | Началась загрузка |
|
|
89
89
|
| `onUploadProgress` | `{ file, progress, bytesSent, totalBytes }` | Прогресс загрузки |
|
|
90
90
|
| `onUploadComplete` | `{ file, response, status, id }` | Файл успешно загружен |
|
|
@@ -190,4 +190,4 @@ MIT. Свободно использовать и модифицировать.
|
|
|
190
190
|
|
|
191
191
|
📌 *Разработано в рамках фронтенд-системы VG Modules.*
|
|
192
192
|
> 🚀 Автор: VEGAS STUDIO (vegas-dev.com)
|
|
193
|
-
> 📍 Поддерживается в проектах VEGAS
|
|
193
|
+
> 📍 Поддерживается в проектах VEGAS
|
|
@@ -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
|
}
|