vgapp 0.7.7 → 0.7.8

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.
@@ -0,0 +1,221 @@
1
+ # VGFormSender — Модуль отправки форм
2
+
3
+ Модуль `VGFormSender` позволяет легко и гибко управлять отправкой форм на сайте. Он поддерживает как нативную, так и **AJAX-отправку**, валидацию, отображение уведомлений (через **VGModal** или **VGCollapse**), работу с паролями, спиннеры на кнопках и многое другое.
4
+
5
+ ---
6
+
7
+ ## ✅ Основные возможности
8
+
9
+ - 📤 **Отправка форм через AJAX или нативно**
10
+ - ✅ **HTML5-валидация** с подсветкой ошибок
11
+ - 🔔 **Уведомления** — в виде модального окна или collapse-блока
12
+ - 🔐 **Показ/скрытие пароля** (с иконкой "глаз")
13
+ - 🔁 **Спиннеры на кнопке отправки**
14
+ - 🔄 **Редиректы** после успешной или неудачной отправки
15
+ - 🧩 **Интерцепторы** (`beforeSend`, `success`, `error`) — полный контроль
16
+ - 🌐 **Мультиязычность** (поддержка `ru`, легко расширяемо)
17
+ - 📢 **События** (`before`, `success`, `error`) для внешнего контроля
18
+ - ⚙️ **Кастомизация** через параметры и `data-*` атрибуты
19
+
20
+ ---
21
+
22
+ ## 🧱 Подключение
23
+
24
+ ### Через JavaScript
25
+ ```js
26
+ import VGFormSender from './app/modules/vgformsender/js/vgformsender.js';
27
+ ```
28
+ Или подключите как часть сборки.
29
+
30
+ ---
31
+ ## 🛠️ Инициализация
32
+
33
+ ### 1. Через JavaScript
34
+ ```js
35
+ VGFormSender.init(document.getElementById('contactForm'), {
36
+ validate: true, // включить валидацию
37
+ ajax: {
38
+ route: '/api/send', // URL для отправки
39
+ method: 'post'
40
+ },
41
+ alert: {
42
+ type: 'modal', // 'modal' или 'collapse'
43
+ enabled: true,
44
+ delay: 5000 // ожидание открытия, через 5 секунд
45
+ },
46
+ callback: {
47
+ afterSuccess: (form, instance, event, data) => {
48
+ console.log('Форма отправлена!', data);
49
+ },
50
+ afterError: (form, instance, event, data) => {
51
+ console.error('Ошибка:', data);
52
+ }
53
+ }
54
+ });
55
+ ```
56
+
57
+ ### 2. Через HTML (`data-*` атрибуты)
58
+ ```html
59
+ <form action="/api/send" method="post" id="contactForm" data-vgformsender
60
+ data-validate="true"
61
+ data-alert-type="modal"
62
+ >
63
+ <!-- ... -->
64
+ <button type="submit" data-button-send="Отправляем..." data-button-spinner-enabled="true">Отправить</button>
65
+ </form>
66
+ ```
67
+
68
+ ---
69
+
70
+ ## ⚙️ Параметры модуля
71
+
72
+ | Параметр | Тип | По умолчанию | Описание |
73
+ |---------------------------|-------------------------|------------------------------------|----------------------------------------------------------------------|
74
+ | `validate` | `boolean` | `false` | Включить HTML5-валидацию |
75
+ | `response.enabled` | `boolean` | `false` | Нативная обработка ответа (без AJAX) |
76
+ | `submit` | `boolean` | `false` | Отправлять нативно (без AJAX) |
77
+ | `fields` | `Array` | `[]` | Доп. данные для отправки |
78
+ | `pass.enabled` | `boolean` | `true` | Показывать иконку глаза у паролей |
79
+ | `alert.enabled` | `boolean` | `true` | Показывать уведомления |
80
+ | `alert.type` | `'modal' \| 'collapse'` | `'modal'` | Тип уведомления |
81
+ | `alert.errors` | `boolean` | `true` | Показывать детали ошибок |
82
+ | `alert.delay` | `number` | `0` | Задержка закрытия (мс), `0` — не закрывать |
83
+ | `ajax.route` | `string` | `''` | URL отправки (переопределяет `action`) |
84
+ | `ajax.method` | `string` | `'get'` | HTTP-метод (`post`, `put`, и т.д.) |
85
+ | `ajax.target` | `string` | `''` | Указывается ID целевого блока, для AJAX-ответа |
86
+ | `ajax.output` | `boolean` | `false` | Разрешает или запрещает добавление контента с сервера в целевой блок |
87
+ | `button.spinner.enabled` | `boolean` | `false` | Показывать спиннер на кнопке |
88
+ | `button.spinner.element` | `string` | `<span class="spinner-border...">` | HTML спиннера |
89
+ | `button.send` | `string` | `'Отправляем...'` | Текст кнопки при отправке |
90
+ | `button.initial` | `string` | `'Отправить'` | Исходный текст кнопки |
91
+ | `redirect.success` | `string` | `''` | Редирект после успеха |
92
+ | `redirect.error` | `string` | `''` | Редирект после ошибки |
93
+ | `lang` | `string` | `'ru'` | Язык сообщений |
94
+ | `interceptors.beforeSend` | `Function` | `Promise.resolve()` | Выполняется перед отправкой |
95
+ | `interceptors.success` | `Function\|false` | `false` | Кастомная обработка успеха |
96
+ | `interceptors.error` | `Function\|false` | `false` | Кастомная обработка ошибки |
97
+ | `callback.afterInit` | `Function` | `noop` | После инициализации |
98
+ | `callback.afterSuccess` | `Function` | `noop` | После успеха |
99
+ | `callback.afterError` | `Function` | `noop` | После ошибки |
100
+ | `callback.afterSend` | `Function` | `noop` | После любого ответа |
101
+
102
+ ---
103
+
104
+ ## 🔔 События
105
+
106
+ Модуль генерирует события на DOM-элементе формы.
107
+
108
+ ### Доступные события
109
+
110
+ | Событие | Данные | Описание |
111
+ |--------|--------|---------|
112
+ | `vg.fs.before` | `{ instance }` | Перед отправкой |
113
+ | `vg.fs.success` | `{ event, self, data }` | Успешная отправка |
114
+ | `vg.fs.error` | `{ event, self, data }` | Ошибка при отправке |
115
+
116
+ ### Пример прослушивания
117
+
118
+ ```js
119
+ document.getElementById('contactForm').addEventListener('vg.fs.success', (e) => {
120
+ const { data } = e.vgformsender;
121
+ console.log('Ответ сервера:', data);
122
+ });
123
+ ```
124
+ ---
125
+
126
+ ## 💬 Языки
127
+
128
+ Поддерживается русский (`ru`) по умолчанию. Вы можете расширить поддержку:
129
+ ```js
130
+ // В lang.js lang_messages('en', 'errors').went_wrong = 'Something went wrong'; lang_titles('en', 'errors').title = 'Error';
131
+ ```
132
+ ---
133
+
134
+ ## 🖼️ Типы уведомлений
135
+
136
+ ### 1. Модальное окно (`modal`)
137
+ ```js
138
+ alert: {
139
+ type: 'modal',
140
+ enabled: true
141
+ }
142
+ ```
143
+ Автоматически закрывает другие модальные окна (включая Bootstrap и VGModal).
144
+
145
+ ### 2. Collapse-блок
146
+ ```js
147
+ alert: {
148
+ type: 'collapse',
149
+ enabled: true
150
+ }
151
+ ```
152
+ Уведомление появляется в начале формы.
153
+
154
+ ---
155
+
156
+ ## 🔄 Интерцепторы
157
+
158
+ Полный контроль над процессом:
159
+ ```js
160
+ interceptors: {
161
+ beforeSend: () => {
162
+ return new Promise((resolve, reject) => {
163
+ if (confirm('Отправить форму?')) {
164
+ resolve();
165
+ } else {
166
+ reject();
167
+ }
168
+ });
169
+ },
170
+ success: (form, instance, data) => { // кастомная логика, отключает стандартное поведение
171
+ console.log('Кастомный успех');
172
+ return false; // важно: вернуть false, чтобы отключить стандартный alert
173
+ }
174
+ }
175
+ ```
176
+ ---
177
+
178
+ ## 🖱️ Статические методы
179
+
180
+ ### `VGFormSender.init(element, params)`
181
+ Инициализирует форму.
182
+
183
+ ### `VGFormSender.buttonClick(formID, callback, status)`
184
+ Подписывается на нажатие кнопки.
185
+
186
+ ```js
187
+ VGFormSender.buttonClick('#contactForm', (form, instance) => {
188
+ console.log('Кнопка нажата до отправки');
189
+ }, 'before');
190
+ ```
191
+ ---
192
+
193
+ ## 🎨 CSS-классы
194
+
195
+ | Класс | Назначение |
196
+ |------|-----------|
197
+ | `.vg-form-sender` | Базовый класс формы |
198
+ | `.vg-form-sender--content` | Обёртка полей |
199
+ | `.vg-form-sender-alert` | Блок уведомления |
200
+ | `.vg-form-sender-modal` | Модальное уведомление |
201
+ | `.vg-form-sender-collapse` | Collapse-уведомление |
202
+
203
+ ---
204
+
205
+ ## 📦 Зависимости
206
+ - `BaseModule` — базовый класс
207
+ - `VGModal`, `VGCollapse` — компоненты интерфейса
208
+ - `VGHideShowPass` — показ/скрытие пароля
209
+ - `utils/js/dom/*` — утилиты DOM
210
+ - `utils/js/functions` — вспомогательные функции
211
+
212
+ ---
213
+
214
+ ## 📄 Лицензия
215
+
216
+ MIT — свободно используйте и модифицируйте.
217
+
218
+ ---
219
+
220
+ > 🚀 Автор: VEGAS STUDIO (vegas-dev.com)
221
+ > 📍 Поддерживается в проектах на VEGAS / SberTech
@@ -49,13 +49,20 @@
49
49
 
50
50
  .vg-form-sender-alert {
51
51
  @include mix-alert-color-mode($class: vg-form-sender-alert);
52
-
53
52
  background-color: var(--vg-form-sender-alert-background-color);
54
53
  border: 1px solid var(--vg-form-sender-alert-border-color) ;
55
54
  border-radius: var(--vg-border-radius);
56
55
 
57
- &.vg-form-sender-alert-modal {
58
- height: 260px;
56
+ .vg-btn-close {
57
+ svg {
58
+ path {
59
+ fill: var(--vg-form-sender-alert-close)
60
+ }
61
+ }
62
+ }
63
+
64
+ .vg-form-sender-alert-modal {
65
+ min-height: 260px;
59
66
  display: flex;
60
67
  align-items: center;
61
68
  justify-content: center;
@@ -65,6 +72,7 @@
65
72
  flex-direction: column;
66
73
  align-items: center;
67
74
  gap: 1.5rem;
75
+ padding: 1rem;
68
76
  }
69
77
 
70
78
  .vg-form-sender-alert-content--text {
@@ -23,7 +23,7 @@ const CLASS_NAME_ACTIVE = 'active';
23
23
  /**
24
24
  * Constants toggle
25
25
  */
26
- const SELECTOR_DATA_TOGGLE = '.'+ CLASS_NAME +' a';
26
+ const SELECTOR_DATA_TOGGLE = '.' + CLASS_NAME + ' a';
27
27
 
28
28
  /**
29
29
  * Constants Events
@@ -55,7 +55,8 @@ class VGNav extends BaseModule {
55
55
  enable: true,
56
56
  always: false,
57
57
  title: '',
58
- body: null
58
+ body: null,
59
+ target: '#sidebar-nav'
59
60
  },
60
61
  callbacks: {
61
62
  afterInit: noop,
@@ -87,8 +88,12 @@ class VGNav extends BaseModule {
87
88
  this.navigation = '.' + this._classes.wrapper;
88
89
 
89
90
  if (this._params.animation.enable === false) {
90
- this._params.animation.timeout = 10
91
+ this._params.animation.timeout = 10;
91
92
  }
93
+
94
+ this._openDrops = new Map();
95
+ this._handleScroll = this._handleScroll.bind(this);
96
+ this._handleResize = this._handleResize.bind(this);
92
97
  }
93
98
 
94
99
  static get NAME() {
@@ -138,7 +143,7 @@ class VGNav extends BaseModule {
138
143
  hamburger = '<span class="' + classes.hamburger + '--lines"><span></span><span></span><span></span></span>';
139
144
 
140
145
  if (params.hamburger.title) {
141
- mobileNavTitle = '<span class="' + classes.hamburger + '--title">'+ params.hamburger.title +'</span>';
146
+ mobileNavTitle = '<span class="' + classes.hamburger + '--title">' + params.hamburger.title + '</span>';
142
147
  }
143
148
 
144
149
  if (params.hamburger.body !== null) {
@@ -163,8 +168,8 @@ class VGNav extends BaseModule {
163
168
  if ($dropdown_a.length) {
164
169
  $dropdown_a.forEach(function (elem) {
165
170
  if (!elem.querySelector('.toggle') && !elem.closest('.dots')) {
166
- elem.setAttribute('aria-expanded', 'false')
167
- elem.insertAdjacentHTML('beforeend', toggle)
171
+ elem.setAttribute('aria-expanded', 'false');
172
+ elem.insertAdjacentHTML('beforeend', toggle);
168
173
  }
169
174
  });
170
175
  }
@@ -195,15 +200,32 @@ class VGNav extends BaseModule {
195
200
  target.classList.add(CLASS_NAME_ACTIVE);
196
201
 
197
202
  const $placement = new Placement({
198
- drop: drop
199
- })
203
+ reference: target,
204
+ drop: drop,
205
+ placement: 'bottom-start',
206
+ fallbackPlacements: ['top-start', 'bottom-end', 'top-end'],
207
+ offset: [0, 6],
208
+ boundary: 'clippingParents',
209
+ autoFlip: true,
210
+ overflowProtection: true
211
+ });
200
212
 
201
213
  $placement._setPlacement();
202
214
 
215
+ this._openDrops.set(drop, {
216
+ reference: target,
217
+ placement: $placement,
218
+ scrollHandler: this._handleScroll,
219
+ resizeHandler: this._handleResize
220
+ });
221
+
222
+ window.addEventListener('scroll', this._handleScroll, { passive: true, capture: true });
223
+ window.addEventListener('resize', this._handleResize);
224
+
203
225
  const completeCallBack = () => {
204
226
  drop.classList.add(CLASS_NAME_FADE);
205
- EventHandler.trigger(target, EVENT_KEY_SHOWN, relatedTarget)
206
- }
227
+ EventHandler.trigger(target, EVENT_KEY_SHOWN, relatedTarget);
228
+ };
207
229
  this._queueCallback(completeCallBack, drop, true, 10);
208
230
  }
209
231
 
@@ -218,7 +240,7 @@ class VGNav extends BaseModule {
218
240
  let element = relatedTarget.relatedTarget;
219
241
 
220
242
  if ('elm' in relatedTarget && relatedTarget.elm) {
221
- element = relatedTarget.elm
243
+ element = relatedTarget.elm;
222
244
  }
223
245
 
224
246
  if (element) {
@@ -245,15 +267,65 @@ class VGNav extends BaseModule {
245
267
  if (index === 0) {
246
268
  const completeCallback = () => {
247
269
  el.classList.remove(CLASS_NAME_SHOW);
248
- EventHandler.trigger(el, EVENT_KEY_HIDDEN, relatedTarget)
249
- }
270
+ EventHandler.trigger(el, EVENT_KEY_HIDDEN, relatedTarget);
271
+ };
250
272
 
251
273
  _this._queueCallback(completeCallback, el, true, 500);
252
274
  }
275
+
276
+ const dropData = _this._openDrops.get(el);
277
+ if (dropData) {
278
+ window.removeEventListener('scroll', dropData.scrollHandler, { capture: true });
279
+ window.removeEventListener('resize', dropData.resizeHandler);
280
+ _this._openDrops.delete(el);
281
+ }
253
282
  });
254
283
  }
255
284
  }
256
285
 
286
+ _handleScroll() {
287
+ for (const [drop, data] of this._openDrops.entries()) {
288
+ if (drop.offsetParent === null) {
289
+ this._cleanupDrop(drop);
290
+ continue;
291
+ }
292
+
293
+ data.placement._setPlacement();
294
+
295
+ if (!this._isElementInViewport(drop)) {
296
+ const target = data.reference;
297
+ this.hide({ relatedTarget: target });
298
+ }
299
+ }
300
+ }
301
+
302
+ _handleResize() {
303
+ for (const [drop, data] of this._openDrops.entries()) {
304
+ if (drop.offsetParent === null) continue;
305
+ data.placement._setPlacement();
306
+ }
307
+ }
308
+
309
+ _cleanupDrop(drop) {
310
+ const dropData = this._openDrops.get(drop);
311
+ if (dropData) {
312
+ window.removeEventListener('scroll', dropData.scrollHandler, { capture: true });
313
+ window.removeEventListener('resize', dropData.resizeHandler);
314
+ this._openDrops.delete(drop);
315
+ }
316
+ }
317
+
318
+ _isElementInViewport(el) {
319
+ const rect = el.getBoundingClientRect();
320
+ const viewHeight = window.innerHeight || document.documentElement.clientHeight;
321
+ const viewWidth = window.innerWidth || document.documentElement.clientWidth;
322
+
323
+ const vertInView = (rect.top <= viewHeight) && ((rect.top + rect.height) >= 0);
324
+ const horInView = (rect.left <= viewWidth) && ((rect.left + rect.width) >= 0);
325
+
326
+ return vertInView && horInView;
327
+ }
328
+
257
329
  static init(element, params = {}) {
258
330
  const instance = VGNav.getOrCreateInstance(element, params);
259
331
  instance.build();
@@ -276,7 +348,7 @@ class VGNav extends BaseModule {
276
348
 
277
349
  let relatedTarget = {
278
350
  relatedTarget: target
279
- }
351
+ };
280
352
 
281
353
  instance.show(relatedTarget);
282
354
  });
@@ -293,9 +365,9 @@ class VGNav extends BaseModule {
293
365
  }
294
366
 
295
367
  currentElem = null;
296
- instance.hide({relatedTarget: relatedTarget, elm: elm});
297
- })
298
- })
368
+ instance.hide({ relatedTarget: relatedTarget, elm: elm });
369
+ });
370
+ });
299
371
  }
300
372
 
301
373
  const vgNavSidebar = document.getElementById('sidebar-nav');
@@ -314,16 +386,16 @@ class VGNav extends BaseModule {
314
386
 
315
387
  static clearDrops(event) {
316
388
  if (event.button === 2 || (event.type === 'keyup' && event.key !== 'Tab')) {
317
- return
389
+ return;
318
390
  }
319
391
 
320
- VGNav.hideOpenDrops(event)
392
+ VGNav.hideOpenDrops(event);
321
393
  }
322
394
 
323
395
  static hideOpenDrops(event) {
324
- [... Selectors.findAll('.dropdown:not(.disabled):not(:disabled).active')].forEach((el) => {
396
+ [...Selectors.findAll('.dropdown:not(.disabled):not(:disabled).active')].forEach((el) => {
325
397
  let target = event.target,
326
- drop = target.closest('.dropdown');
398
+ drop = target.closest('.dropdown');
327
399
 
328
400
  if (el !== drop) {
329
401
  const nav = el.closest('.vg-nav');
@@ -337,9 +409,9 @@ class VGNav extends BaseModule {
337
409
  return;
338
410
  }
339
411
 
340
- const relatedTarget = { relatedTarget: el }
412
+ const relatedTarget = { relatedTarget: el };
341
413
 
342
- context.hide(relatedTarget)
414
+ context.hide(relatedTarget);
343
415
  }
344
416
  });
345
417
  }
@@ -375,16 +447,16 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (
375
447
 
376
448
  if (dropContent && isFirst) {
377
449
  if (drop.classList.contains('active')) {
378
- instance.hide({relatedTarget: drop});
450
+ instance.hide({ relatedTarget: drop });
379
451
  return;
380
452
  }
381
453
  } else {
382
454
  [...Selectors.findAll('.active', nav)].forEach(function (el) {
383
- instance.hide({relatedTarget: el})
455
+ instance.hide({ relatedTarget: el });
384
456
  });
385
457
  }
386
458
 
387
- instance.show({relatedTarget: drop});
459
+ instance.show({ relatedTarget: drop });
388
460
  });
389
461
 
390
462
  export default VGNav;
@@ -1,44 +1,6 @@
1
1
  &.vg-nav-horizontal {
2
2
  .vg-nav-wrapper {
3
3
  flex-direction: row;
4
-
5
- .dropdown {
6
- > .dropdown-content {
7
- &.left, .left {
8
- left: 0;
9
- }
10
-
11
- &.right, .right {
12
- right: 0;
13
- }
14
-
15
- &.top, .top {
16
- top: 0;
17
- }
18
-
19
- &.bottom, .bottom {
20
- bottom: 0;
21
- }
22
-
23
- .dropdown {
24
- ul.left {
25
- left: 100%;
26
- }
27
-
28
- ul.right {
29
- right: 100%;
30
- }
31
-
32
- ul.top {
33
- top: 100%;
34
- }
35
-
36
- ul.bottom {
37
- bottom: 100%;
38
- }
39
- }
40
- }
41
- }
42
4
  }
43
5
  }
44
6
 
@@ -46,64 +8,17 @@
46
8
  .vg-nav-wrapper {
47
9
  flex-direction: column;
48
10
 
49
- .dropdown {
50
- > ul {
51
- &.left, .left {
52
- left: 100%;
53
- }
54
-
55
- &.right, .right {
56
- right: 100%;
57
- }
58
-
59
- &.top, .top {
60
- top: 100%;
61
- }
62
-
63
- &.bottom, .bottom {
64
- bottom: 100%;
65
- }
66
-
67
- &.fade {
68
- &.top {
69
- top: 0;
70
- }
71
- &.bottom {
72
- bottom: 0;
73
- }
74
- }
75
- }
76
-
77
- &.show {
78
- &.top {
79
- > ul {
80
- top: 0;
81
- }
82
- }
83
- &.bottom {
84
- > ul {
85
- bottom: 0;
86
- }
87
- }
88
- }
11
+ [data-vg-placement=top-start], [data-vg-placement=bottom-start] {
12
+ left: 100%;
89
13
  }
90
14
 
91
- .dropdown-mega {
92
- position: relative;
93
-
94
- .dropdown-mega-container {
95
- left: 100%;
96
- top: 100%;
97
-
98
- &.fade {
99
- &.top {
100
- top: 0;
101
- }
15
+ [data-vg-placement^=bottom] {
16
+ top: 0;
17
+ }
102
18
 
103
- &.bottom {
104
- bottom: 0;
105
- }
106
- }
19
+ [data-vg-placement^=bottom] {
20
+ [data-vg-placement=top-start] {
21
+ bottom: 0;
107
22
  }
108
23
  }
109
24
  }
@@ -115,7 +115,6 @@
115
115
 
116
116
  .dropdown-content {
117
117
  position: absolute;
118
- left: 0;
119
118
  transition: var(--vg-nav-drop-mega-transition);
120
119
  width: var(--vg-nav-drop-mega-width);
121
120
  min-height: var(--vg-nav-drop-mega-min-height);