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
- { '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);
@@ -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
- { '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
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
- this._resetFileInput();
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) Classes.remove($message, 'has-files');
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 { 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
 
@@ -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 = { files: this._files, input: e?.target || e?.src || '' };
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', { files: this._files, input: payload.input });
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 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.2",
3
+ "version": "1.0.4",
4
4
  "description": "",
5
5
  "author": {
6
6
  "name": "Vegas Studio",