vgapp 1.0.3 → 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.
@@ -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
- { 'data-name': file.name, 'data-size': file.size, 'data-id': file.id || '', class: 'file ' + classes.join(' ') }, parts
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
- { 'data-name': file.name, 'data-size': file.size, 'data-type': file.type, 'data-id': file.id || '', class: 'file ' + classes.join(' ') + ' ' }, parts
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 { Classes, Manipulator } from "../../../utils/js/dom/manipulator";
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
- const dataFile = normalizeData(rawData);
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); // Убираем null
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
- if (this._render.parsedFiles.length) {
110
- this._addExternalFiles(this._render.parsedFiles);
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
- 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
- }
121
-
122
- this._addEventListenerExtended();
123
- this._renderStat();
124
- this._triggerCallback('onInit', { element: this._element });
125
- }
126
-
127
- _setDropActiveText() {
128
- if (!this._nodes.drop) return;
129
-
130
- const messages = lang_messages(this._params.lang, NAME) || {};
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
- const file = new File([""], fileData.name, {
146
- type: fileData.type || "application/octet-stream",
147
- lastModified: fileData.lastModified || Date.now()
148
- });
147
+ if (!fileData?.name) return;
149
148
 
150
- // Добавляем ID, если он есть
151
- Object.defineProperty(file, 'id', {
152
- value: fileData.id,
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
- // Добавляем Size, если он есть
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
- // Добавляем Src, если он есть
165
- Object.defineProperty(file, 'src', {
166
- value: fileData.src,
167
- writable: true,
168
- enumerable: true
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;
@@ -682,15 +706,15 @@ class VGFiles extends VGFilesBase {
682
706
 
683
707
  return this._tpl.button([
684
708
  this._tpl.i({}, icon, { isHTML: true })
685
- ], 'button', {
709
+ ], 'button', this._buildFileDataAttributes(file, {
686
710
  type: 'button',
687
711
  [action]: 'file',
688
712
  'data-name': file.name,
689
- 'data-size': file.size,
690
- 'data-type': file.type,
691
- 'data-last-modified': file.lastModified,
713
+ 'data-size': file.size ?? 0,
714
+ 'data-type': file.type || '',
715
+ 'data-last-modified': file.lastModified || '',
692
716
  'data-id': file.id || ''
693
- });
717
+ }));
694
718
  }
695
719
 
696
720
  _renderStat() {
@@ -874,4 +898,4 @@ EventHandler.on(document, `click.${NAME_KEY}.data.api`, SELECTOR_DATA_DISMISS_AL
874
898
  }
875
899
  });
876
900
 
877
- export default VGFiles;
901
+ export default VGFiles;
@@ -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 relatedTarget = event.relatedTarget?.closest('.dropdown'),
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgapp",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "",
5
5
  "author": {
6
6
  "name": "Vegas Studio",