vgapp 0.7.6 → 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.
@@ -1,9 +1,46 @@
1
+ /**
2
+ * VGFormSender Module
3
+ *
4
+ * Этот модуль отвечает за отправку форм с поддержкой AJAX, валидации, отображения уведомлений
5
+ * (модальные окна или collapse), обработки ошибок, спиннеров для кнопок и многое другое.
6
+ * Поддерживает кастомизацию через параметры и data-атрибуты.
7
+ *
8
+ * @class VGFormSender
9
+ * @extends BaseModule
10
+ *
11
+ * @param {HTMLElement} element - DOM-элемент формы
12
+ * @param {Object} params - Параметры инициализации
13
+ *
14
+ * @example
15
+ * VGFormSender.init(document.getElementById('myForm'), {
16
+ * ajax: {
17
+ * route: '/submit',
18
+ * method: 'post'
19
+ * },
20
+ * alert: {
21
+ * type: 'modal',
22
+ * enabled: true
23
+ * },
24
+ * callback: {
25
+ * afterSuccess: (form, instance, event, data) => {
26
+ * console.log('Форма успешно отправлена');
27
+ * }
28
+ * }
29
+ * });
30
+ */
31
+
1
32
  import BaseModule from "../../base-module";
33
+ import VGModal from "../../vgmodal/js/vgmodal";
34
+ import VGCollapse from "../../vgcollapse/js/vgcollapse";
35
+ import VGHideShowPass from "./hideshowpass";
36
+ import {lang_titles, lang_messages} from "../../../utils/js/components/lang";
37
+ import Html from "../../../utils/js/components/templater";
2
38
  import {Manipulator} from "../../../utils/js/dom/manipulator";
3
39
  import EventHandler from "../../../utils/js/dom/event";
4
- import VGModal from "../../vgmodal/js/vgmodal";
40
+ import Selectors from "../../../utils/js/dom/selectors";
41
+ import {getSVG} from "../../module-fn";
5
42
  import {
6
- execute, getDeepestLastChild,
43
+ execute,
7
44
  isObject,
8
45
  isVisible,
9
46
  makeRandomString,
@@ -11,32 +48,66 @@ import {
11
48
  noop,
12
49
  normalizeData
13
50
  } from "../../../utils/js/functions";
14
- import Selectors from "../../../utils/js/dom/selectors";
15
- import VGCollapse from "../../vgcollapse/js/vgcollapse";
16
- import {getSVG} from "../../module-fn";
17
- import VGHideShowPass from "./hideshowpass";
18
51
 
19
52
  /**
20
- * Constants
53
+ * Константа: имя модуля
54
+ * @type {string}
21
55
  */
22
56
  const NAME = 'form-sender';
57
+
58
+ /**
59
+ * Константа: ключ модуля для использования в data-атрибутах и событиях
60
+ * @type {string}
61
+ */
23
62
  const NAME_KEY = 'vg.fs';
24
63
 
25
64
  /**
26
- * Constants Events
65
+ * CSS-класс для алертов
66
+ * @type {string}
27
67
  */
28
68
  const CLASS_NAME_ALERT = 'vg-form-sender-alert';
29
69
 
70
+ /**
71
+ * Событие: успешная отправка формы
72
+ * @type {string}
73
+ */
30
74
  const EVENT_KEY_SUCCESS = 'vg.fs.success';
75
+
76
+ /**
77
+ * Событие: ошибка при отправке формы
78
+ * @type {string}
79
+ */
31
80
  const EVENT_KEY_ERROR = 'vg.fs.error';
81
+
82
+ /**
83
+ * Событие: перед отправкой формы
84
+ * @type {string}
85
+ */
32
86
  const EVENT_KEY_BEFORE = 'vg.fs.before';
33
87
 
88
+ /**
89
+ * Событие: обработка нативной отправки формы
90
+ * @type {string}
91
+ */
34
92
  const EVENT_SUBMIT_DATA_API = `submit.${NAME_KEY}.data.api`;
35
93
 
94
+ /**
95
+ * Основной класс модуля формы
96
+ */
36
97
  class VGFormSender extends BaseModule {
98
+ /**
99
+ * Создаёт экземпляр VGFormSender
100
+ * @param {HTMLElement} element - Элемент формы
101
+ * @param {Object} params - Параметры конфигурации
102
+ */
37
103
  constructor(element, params = {}) {
38
104
  super(element, params);
39
105
 
106
+ /**
107
+ * Объединённые параметры с дефолтными значениями
108
+ * @type {Object}
109
+ * @private
110
+ */
40
111
  this._params = this._getParams(element, mergeDeepObject({
41
112
  response: {
42
113
  enabled: false,
@@ -69,6 +140,8 @@ class VGFormSender extends BaseModule {
69
140
  target: '',
70
141
  method: 'get',
71
142
  timeout: 1000,
143
+ loader: false,
144
+ output: true
72
145
  },
73
146
  classes: {
74
147
  general: 'vg-form-sender',
@@ -82,6 +155,7 @@ class VGFormSender extends BaseModule {
82
155
  afterInit: noop,
83
156
  afterSuccess: noop,
84
157
  afterError: noop,
158
+ afterSend: noop,
85
159
  },
86
160
  interceptors: {
87
161
  beforeSend: () => new Promise((resolve, reject) => resolve()),
@@ -99,37 +173,84 @@ class VGFormSender extends BaseModule {
99
173
  },
100
174
  click: noop,
101
175
  },
176
+ lang: 'ru'
102
177
  }, params));
103
178
 
104
- this._button = Selectors.find('[type="submit"]', this._element) || Selectors.find('[form="' + this._element.id + '"]') || null;
105
-
106
- this._params.ajax.route = Manipulator.get(this._element, 'action').toLowerCase();
107
- this._params.ajax.method = Manipulator.get(this._element, 'method').toLowerCase();
108
-
109
- this._params.isBtnText = Manipulator.get(this._element, 'data-btn-text') !== 'false';
110
- this._params.isJsonParse = Manipulator.get(this._element, 'data-json-parse') !== 'false';
111
- this._params.isShowPass = Manipulator.get(this._element, 'data-show-pass') === 'true';
112
-
113
- this._params = this._getParams(this._button, this._params);
114
- this._params.button.initial = this._button.innerHTML || this._params.button.initial;
179
+ /**
180
+ * Кнопка отправки формы
181
+ * @type {HTMLElement|null}
182
+ * @private
183
+ */
184
+ this._button = null;
185
+
186
+ /**
187
+ * Кэш для часто используемых элементов
188
+ * @type {Map<string, any>}
189
+ * @private
190
+ */
191
+ this._cachedElements = new Map();
192
+
193
+ this._initElements();
115
194
  }
116
195
 
196
+ /**
197
+ * Возвращает имя модуля
198
+ * @returns {string}
199
+ * @static
200
+ */
117
201
  static get NAME() {
118
202
  return NAME;
119
203
  }
120
204
 
205
+ /**
206
+ * Возвращает ключ модуля
207
+ * @returns {string}
208
+ * @static
209
+ */
121
210
  static get NAME_KEY() {
122
211
  return NAME_KEY;
123
212
  }
124
213
 
214
+ /**
215
+ * Инициализация внутренних элементов: кнопка, поля, параметры
216
+ * @private
217
+ */
218
+ _initElements() {
219
+ this._button = Selectors.find('[type="submit"]', this._element) ||
220
+ Selectors.find('[form="' + this._element.id + '"]') ||
221
+ null;
222
+
223
+ const fields = Selectors.findAll('input, textarea, select', this._element);
224
+ this._cachedElements.set('fields', fields);
225
+
226
+ this._params.ajax.route = Manipulator.get(this._element, 'action') || this._params.ajax.route;
227
+ this._params.ajax.method = Manipulator.get(this._element, 'method') || this._params.ajax.method;
228
+
229
+ this._params.isBtnText = Manipulator.get(this._element, 'data-btn-text') !== 'false';
230
+ this._params.isJsonParse = Manipulator.get(this._element, 'data-json-parse') !== 'false';
231
+ this._params.isShowPass = Manipulator.get(this._element, 'data-show-pass') === 'true';
232
+
233
+ if (this._button) {
234
+ this._params = this._getParams(this._button, this._params);
235
+ this._params.button.initial = this._button.innerHTML.trim() || this._params.button.initial;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Построение формы: добавление классов, инициализация паролей, валидации
241
+ * @returns {VGFormSender}
242
+ */
125
243
  build() {
126
244
  this._element.classList.add(this._params.classes.general);
127
245
 
128
- [... Selectors.findAll('input, textarea, select', this._element)].forEach((el) => {
129
- if (isVisible(el)) {
130
- el.parentElement.classList.add(this._params.classes.content)
131
- }
132
- });
246
+ const fields = this._cachedElements.get('fields');
247
+ if (fields) {
248
+ fields.forEach((el) => {
249
+ if (isVisible(el) && el.parentElement) {
250
+ el.parentElement.classList.add(this._params.classes.content);
251
+ }
252
+ });
253
+ }
133
254
 
134
255
  if (this._params.validate) {
135
256
  Manipulator.set(this._element, 'novalidate', '');
@@ -147,6 +268,11 @@ class VGFormSender extends BaseModule {
147
268
  return this
148
269
  }
149
270
 
271
+ /**
272
+ * Отправка формы (AJAX или нативная)
273
+ * @param {Event} event - DOM-событие отправки
274
+ * @param {FormData|null} data - Дополнительные данные для отправки
275
+ */
150
276
  request(event, data = null) {
151
277
  const _this = this;
152
278
  const mergeFormData = (target, source) => {
@@ -165,6 +291,8 @@ class VGFormSender extends BaseModule {
165
291
  else _this._params.ajax.data = formData;
166
292
 
167
293
  _this._route(function (status, data) {
294
+ execute(_this._params.callback.afterSend, [_this._element, _this, status, data]);
295
+
168
296
  _this._element.classList.remove('was-validated');
169
297
 
170
298
  if (_this._params.response.enabled) {
@@ -201,50 +329,81 @@ class VGFormSender extends BaseModule {
201
329
 
202
330
  _this._params.interceptors.beforeSend().then(() => {
203
331
  submit();
332
+ }).catch(err => {
333
+ console.error(err);
204
334
  });
205
335
  }
206
336
 
337
+ /**
338
+ * Действия перед отправкой формы: блокировка кнопки, триггер события
339
+ * @private
340
+ */
207
341
  _alertBefore() {
208
342
  const _this = this;
209
343
 
210
344
  if (_this._params.alert.type === 'collapse') {
211
- [...document.getElementsByClassName(_this._params.classes.alertCollapse)].forEach(function (element) {
212
- if (element && element.classList.contains('show')) {
213
- VGCollapse.getOrCreateInstance(element, {toggle: false}).hide();
214
- }
215
- });
345
+ const collapseClass = _this._params.classes.alertCollapse;
346
+ if (!_this._cachedElements.has('collapses') || document.querySelector('.' + collapseClass + '.show')) {
347
+ const collapses = document.getElementsByClassName(collapseClass);
348
+ _this._cachedElements.set('collapses', collapses);
349
+
350
+ [...collapses].forEach(function (element) {
351
+ if (element.classList.contains('show')) {
352
+ VGCollapse.getOrCreateInstance(element, { toggle: false }).hide();
353
+ }
354
+ });
355
+ }
216
356
  }
217
357
 
218
358
  _this._statusButton('before');
219
359
  EventHandler.trigger(_this._element, EVENT_KEY_BEFORE, _this);
220
360
  }
221
361
 
362
+ /**
363
+ * Обработка ошибки: отображение алерта, вызов колбэка
364
+ * @param {Event} event - DOM-событие
365
+ * @param {Object} data - Данные ответа
366
+ * @private
367
+ */
222
368
  _alertError(event, data) {
223
369
  const _this = this;
224
370
 
225
371
  _this._statusButton('after');
226
372
  _this._jsonParse(data, 'error');
227
- EventHandler.trigger(_this._element, EVENT_KEY_ERROR, [event, _this, data]);
373
+ EventHandler.trigger(_this._element, EVENT_KEY_ERROR, {
374
+ vgformsender: {
375
+ event: event,
376
+ self: _this,
377
+ data: data
378
+ }
379
+ });
228
380
  }
229
381
 
382
+ /**
383
+ * Обработка успеха: отображение алерта, вызов колбэка
384
+ * @param {Event} event - DOM-событие
385
+ * @param {Object} data - Данные ответа
386
+ * @private
387
+ */
230
388
  _alertSuccess(event, data) {
231
389
  const _this = this;
232
390
 
233
391
  _this._statusButton('after');
234
392
  _this._jsonParse(data, 'success');
235
- EventHandler.trigger(_this._element, EVENT_KEY_SUCCESS, [event, _this, data]);
236
- }
237
-
238
- static buttonClick(formID, callback, status = 'before') {
239
- const form = Selectors.find(formID);
240
- if (form) {
241
- const instance = VGFormSender.getOrCreateInstance(formID);
242
- form.addEventListener('vg.fs.' + status, e => {
243
- execute(callback, [form, instance])
244
- })
245
- }
393
+ EventHandler.trigger(_this._element, EVENT_KEY_SUCCESS, {
394
+ vgformsender: {
395
+ event: event,
396
+ self: _this,
397
+ data: data
398
+ }
399
+ });
246
400
  }
247
401
 
402
+ /**
403
+ * Управление состоянием кнопки отправки
404
+ * @param {'before'|'after'} status - Статус: до/после отправки
405
+ * @private
406
+ */
248
407
  _statusButton(status) {
249
408
  if (!this._button) return;
250
409
 
@@ -286,39 +445,40 @@ class VGFormSender extends BaseModule {
286
445
  }
287
446
  }
288
447
 
448
+ /**
449
+ * Парсинг JSON-ответа и вызов алерта
450
+ * @param {Object} data - Данные ответа
451
+ * @param {'success'|'error'} status - Статус ответа
452
+ * @private
453
+ */
289
454
  _jsonParse(data, status) {
290
455
  const _this = this;
291
456
 
292
- if (_this._params.isJsonParse && typeof data === 'string') {
293
- let parserData = {};
294
-
295
- try {
296
- parserData = JSON.parse(data);
297
- _this.alert(parserData, status);
298
- } catch (e) {
299
- _this.alert(data, status);
300
- }
457
+ if (_this._params.isJsonParse) {
458
+ _this.alert(normalizeData(data), status);
301
459
  } else {
302
460
  _this.alert(data, status);
303
461
  }
304
462
  }
305
463
 
464
+ /**
465
+ * Отображение алерта в зависимости от типа (modal/collapse)
466
+ * @param {Object|string} data - Данные для отображения
467
+ * @param {string} status - Статус: success, error, danger и т.д.
468
+ */
306
469
  alert(data, status) {
307
- const _this = this;
308
-
309
470
  if (isObject(data)) {
310
471
  if (('code' in data) && data.code && data.code === 200) {
311
472
  if ('response' in data && data.response) {
312
473
  let response = normalizeData(data.response);
313
474
  if (typeof response === 'string') {
314
475
  if (response.indexOf("Parse error") !== -1 || response.indexOf("syntax error") !== -1) {
315
- status = 'error';
476
+ status = 'danger';
316
477
  data = {
317
478
  response: {
318
- title: 'Error',
319
- message: 'Something went wrong, please repeat later'
320
- },
321
- text: 'Something went wrong, please repeat later'
479
+ title: lang_titles(this._params.lang, 'errors').title,
480
+ message: lang_messages(this._params.lang, 'errors').went_wrong
481
+ }
322
482
  }
323
483
  }
324
484
  } else {
@@ -327,30 +487,40 @@ class VGFormSender extends BaseModule {
327
487
  }
328
488
  }
329
489
  }
490
+ } else {
491
+ status = 'danger'
330
492
  }
331
493
  }
332
494
 
333
- if (!_this._params.alert.enabled) {
334
- return;
335
- }
495
+ if (!this._params.alert.enabled) return;
336
496
 
337
- if (_this._params.alert.type === 'modal') {
338
- _this._alertModal(data, status)
497
+ if (this._params.alert.type === 'modal') {
498
+ this._alertModal(data, status)
339
499
  }
340
500
 
341
- if (_this._params.alert.type === 'collapse') {
342
- _this._alertCollapse(data, status)
501
+ if (this._params.alert.type === 'collapse') {
502
+ this._alertCollapse(data, status)
343
503
  }
344
504
  }
345
505
 
506
+ /**
507
+ * Показ алерта в виде модального окна
508
+ * @param {Object} data - Данные для отображения
509
+ * @param {string} status - Статус (success/error)
510
+ * @private
511
+ */
346
512
  _alertModal(data, status) {
347
513
  const _this = this;
348
514
 
349
- // Есть ли открытые модалки, закрываем
515
+ // Закрытие всех открытых модальных окон
350
516
  [...document.getElementsByClassName('modal')].forEach(function (element) {
351
517
  if (element && element.classList.contains('show')) {
352
- let mBS = bootstrap.Modal.getOrCreateInstance(element);
353
- mBS.hide();
518
+ if (typeof bootstrap !== 'undefined') {
519
+ let mBS = bootstrap.Modal?.getOrCreateInstance(element);
520
+ mBS.hide();
521
+ } else {
522
+ console.warn(lang_messages(_this._params.lang, NAME).bootstrap_not_found)
523
+ }
354
524
  }
355
525
  });
356
526
 
@@ -368,6 +538,7 @@ class VGFormSender extends BaseModule {
368
538
 
369
539
  setTimeout(() => {
370
540
  VGModal.init(id, {
541
+ dismiss: true,
371
542
  classes: {
372
543
  alert: _this._params.classes.alertModal
373
544
  }
@@ -375,6 +546,9 @@ class VGFormSender extends BaseModule {
375
546
  let element = self._element;
376
547
  element.classList.add(_this._params.classes.alertModal);
377
548
 
549
+ let $content = Selectors.find('.vg-modal-content', element);
550
+ if ($content) $content.classList.add(CLASS_NAME_ALERT, CLASS_NAME_ALERT + '-' + status);
551
+
378
552
  let $body = Selectors.find('.vg-modal-body', element);
379
553
  if ($body) $body.append(_this.setDataRelationStatus(element, status, data, 'modal'));
380
554
 
@@ -389,6 +563,12 @@ class VGFormSender extends BaseModule {
389
563
  }, _this._params.timeout);
390
564
  }
391
565
 
566
+ /**
567
+ * Показ алерта в виде collapse
568
+ * @param {Object} data - Данные для отображения
569
+ * @param {string} status - Статус (success/error)
570
+ * @private
571
+ */
392
572
  _alertCollapse(data, status) {
393
573
  const _this = this;
394
574
 
@@ -413,129 +593,132 @@ class VGFormSender extends BaseModule {
413
593
  }
414
594
  }
415
595
 
596
+ /**
597
+ * Формирование содержимого алерта (заголовок, текст, иконка)
598
+ * @param {HTMLElement} $element - Родительский элемент
599
+ * @param {string} status - Статус (success/danger и т.д.)
600
+ * @param {Object} data - Данные ответа
601
+ * @param {'modal'|'collapse'} type - Тип алерта
602
+ * @returns {HTMLElement} - DOM-элемент с контентом
603
+ */
416
604
  setDataRelationStatus($element, status, data, type) {
417
- if (status === 'error') status = 'danger';
418
-
419
- let $alert = Selectors.find('.'+ CLASS_NAME_ALERT +'-' + status, $element);
605
+ let response = normalizeData(data.response) || data,
606
+ $alert = Selectors.find('.'+ CLASS_NAME_ALERT +'-content', $element);
420
607
 
421
608
  if (isObject(data)) {
422
- if (status === 'error') {
423
- if ('code' in data && data.code !== 200) {
424
- if ('text' in data && !data.text) {
425
- data.text = 'Something went wrong, please repeat later';
426
-
427
- switch (data.code) {
428
- case 400:
429
- data.text = 'Bad Request'
430
- break;
431
- case 401:
432
- data.text = 'Unauthorized'
433
- break;
434
- case 403:
435
- data.text = 'Unauthorized'
436
- break;
437
- case 413:
438
- data.text = 'Forbidden'
439
- break;
440
- case 404:
441
- data.text = 'Not Found'
442
- break;
443
- case 422:
444
- data.text = 'Unprocessable Entity'
445
- break;
446
- case 500:
447
- data.text = 'Internal Server Error'
448
- break;
449
- case 504:
450
- data.text = 'Gateway Timeout'
451
- break;
452
- }
609
+ let view = '';
610
+
611
+ if ('view' in data.response) {
612
+ response = data.response.view
613
+ } else if (typeof response !== 'string') {
614
+ if (status === 'danger') {
615
+ response.title = lang_titles(this._params.lang, 'errors').title;
616
+
617
+ if ('code' in data && data.code !== 200) {
618
+ const messages = {
619
+ 400: lang_messages(this._params.lang, 'errors')[400],
620
+ 401: lang_messages(this._params.lang, 'errors')[401],
621
+ 403: lang_messages(this._params.lang, 'errors')[403],
622
+ 404: lang_messages(this._params.lang, 'errors')[404],
623
+ 413: lang_messages(this._params.lang, 'errors')[413],
624
+ 419: lang_messages(this._params.lang, 'errors')[419],
625
+ 422: lang_messages(this._params.lang, 'errors')[422],
626
+ 500: lang_messages(this._params.lang, 'errors')[500],
627
+ 504: lang_messages(this._params.lang, 'errors')[504],
628
+ };
629
+ response.message = messages[data.code] || lang_messages(this._params.lang, 'errors').went_wrong;
630
+ response.title += ' (' + data.code + ')';
453
631
  }
454
- }
455
- }
456
632
 
457
- if ('response' in data) {
458
- let response = normalizeData(data.response), title = '', txt = '', code = '';
459
- if (typeof response !== 'string') {
460
- if (!('view' in response)) {
461
- if ('title' in response) title = response.title;
462
- if (status === 'error' && data.code !== 200 && this._params.alert.errors) {
463
- code = ' ' + data.text + ' (' + data.code + ')';
464
- }
633
+ if ('errors' in response && this._params.alert.errors) {
634
+ let errors = normalizeData(response.errors) || null;
635
+ response.message = [];
465
636
 
466
- if (!title) txt += '<h4 class="'+ CLASS_NAME_ALERT +'-content--title">' + code + '</h4>';
467
- else txt += '<h4 class="'+ CLASS_NAME_ALERT +'-content--title">' + title + '</h4>';
468
-
469
- if ('message' in response) {
470
- txt += '<div class="'+ CLASS_NAME_ALERT +'-content--message">' + response.message + '</div>'
471
- }
472
-
473
- if ('errors' in response && this._params.alert.errors) {
474
- let errors = normalizeData(response.errors) || null;
475
- if (isObject(errors)) {
476
- for (const error in errors) {
477
- if (Array.isArray(errors[error])) {
478
- errors[error].forEach(function (t) {
479
- txt += '<div>'+ t +'</div>';
480
- })
481
- } else {
482
- txt = '<div>'+ errors[error] +'</div>';
483
- }
637
+ if (isObject(errors)) {
638
+ for (const error in errors) {
639
+ if (Array.isArray(errors[error])) {
640
+ errors[error].forEach((t) => response.message.push(t))
641
+ } else {
642
+ response.message.push(errors[error]);
484
643
  }
485
644
  }
486
645
  }
487
-
488
- data = {
489
- view: txt
490
- }
491
646
  }
492
- } else {
493
- data.view = response;
494
647
  }
495
- }
496
- }
497
648
 
498
- if (!$alert) {
499
- $alert = document.createElement('div');
500
- $alert.classList.add(CLASS_NAME_ALERT, CLASS_NAME_ALERT + '-' + status, CLASS_NAME_ALERT + '-' + type);
501
-
502
- let content = document.createElement('div');
503
- content.classList.add(CLASS_NAME_ALERT + '-content');
649
+ const elm = Html('string');
504
650
 
505
- let icon = document.createElement('div');
506
- icon.classList.add(CLASS_NAME_ALERT + '-content--icon');
651
+ view = elm.h4({class: CLASS_NAME_ALERT +'-content--title'}, response.title);
507
652
 
508
- let i = document.createElement('i');
509
- i.innerHTML = getSVG(status);
510
-
511
- icon.append(i);
512
- content.append(icon);
653
+ if (Array.isArray(response.message)) {
654
+ response.message.forEach(message => {
655
+ view += elm.div({
656
+ class: CLASS_NAME_ALERT +'-content--message'
657
+ }, message);
658
+ })
659
+ } else {
660
+ view += elm.div({
661
+ class: CLASS_NAME_ALERT +'-content--message'
662
+ }, response.message);
663
+ }
513
664
 
514
- let text = document.createElement('div');
515
- text.classList.add(CLASS_NAME_ALERT + '-content--text');
516
- text.innerHTML = data.view;
665
+ response = view;
666
+ }
667
+ }
517
668
 
518
- content.append(text);
519
- $alert.append(content);
669
+ if (!$alert) {
670
+ const elm = Html('dom');
671
+
672
+ $alert = elm.div({
673
+ class: CLASS_NAME_ALERT + '-' + type
674
+ }, [
675
+ elm.div({class: CLASS_NAME_ALERT + '-content'}, [
676
+ elm.i({class: CLASS_NAME_ALERT + '-content--icon'}, getSVG(status), {isHTML: true}),
677
+ elm.div({class: CLASS_NAME_ALERT + '-content--text'}, response, {isHTML: true})
678
+ ]),
679
+ ]);
520
680
  } else {
521
- let text = Selectors.find('.'+ CLASS_NAME_ALERT +'-content--text', $alert);
522
- text.innerHTML = data.view;
681
+ let text = Selectors.find('.vg-modal-body', $element);
682
+ text.innerHTML = response;
523
683
  }
524
684
 
525
685
  return $alert;
526
686
  }
527
687
 
528
688
  /**
529
- * Инициализация
530
- * @param element
531
- * @param params
689
+ * Статический метод инициализации формы
690
+ * @param {HTMLElement} element - Форма
691
+ * @param {Object} params - Параметры
692
+ * @example
693
+ * VGFormSender.init(formElement, { validate: true });
532
694
  */
533
695
  static init(element, params = {}) {
534
696
  const instance = VGFormSender.getOrCreateInstance(element, params);
535
697
  instance.build();
536
698
  }
699
+
700
+ /**
701
+ * Подписка на события кнопки формы
702
+ * @param {string} formID - CSS-селектор формы
703
+ * @param {Function} callback - Колбэк-функция
704
+ * @param {'before'|'after'} status - Статус события
705
+ * @example
706
+ * VGFormSender.buttonClick('#myForm', (form, instance) => { ... }, 'before');
707
+ */
708
+ static buttonClick(formID, callback, status = 'before') {
709
+ const form = Selectors.find(formID);
710
+ if (form) {
711
+ const instance = VGFormSender.getOrCreateInstance(form);
712
+ form.addEventListener('vg.fs.' + status, e => {
713
+ execute(callback, [form, instance])
714
+ })
715
+ }
716
+ }
537
717
  }
538
718
 
719
+ /**
720
+ * Обработчик события отправки формы
721
+ */
539
722
  EventHandler.on(document, EVENT_SUBMIT_DATA_API, function (event) {
540
723
  if (!Manipulator.has(event.target, 'data-vgformsender')) {
541
724
  return;