form-father 0.2.8 → 0.2.10

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.
@@ -73,6 +73,103 @@ function closest($el, selector) {
73
73
  }
74
74
  return null;
75
75
  }
76
+ /** Включает блокировку скролла страницы */
77
+ function blockScrollBody() {
78
+ window.lastBlockScrollPosition = window.pageYOffset;
79
+ document.body.style.top = `${-window.lastBlockScrollPosition}px`;
80
+ document.body.classList.add('body--block-scroll');
81
+ }
82
+ /** Отменяет блокировку скролла страницы */
83
+ function unblockScrollBody() {
84
+ document.body.classList.remove('body--block-scroll');
85
+ document.body.style.top = '';
86
+ window.scrollTo(0, window.lastBlockScrollPosition || 0);
87
+ }
88
+ /**
89
+ * Проверяет является ли строка валидным адресом электронной почты.
90
+ *
91
+ * @param {string} value - Строка, которую нужно проверить.
92
+ *
93
+ * @returns {boolean} Является ли строка валидным адресом электронной почты true/false.
94
+ */
95
+ function isEmailValid(value) {
96
+ return /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu.test(value);
97
+ }
98
+ const HTTP_PROTOCOLS = ['http:', 'https:'];
99
+ function getNormalizedUrlString(originalUrlString) {
100
+ const trimmedUrl = originalUrlString.trim();
101
+ if (trimmedUrl.length === 0)
102
+ return '';
103
+ if (trimmedUrl.startsWith('//')) {
104
+ return `https:${trimmedUrl}`;
105
+ }
106
+ return /^[a-z][a-z\d+.-]*:\/\//i.test(trimmedUrl) ? trimmedUrl : `https://${trimmedUrl}`;
107
+ }
108
+ function isValidIpv4(hostname) {
109
+ const parts = hostname.split('.');
110
+ if (parts.length !== 4)
111
+ return false;
112
+ return parts.every(part => {
113
+ if (!/^\d{1,3}$/.test(part))
114
+ return false;
115
+ const num = Number(part);
116
+ return num >= 0 && num <= 255;
117
+ });
118
+ }
119
+ function isValidIpv6(hostname) {
120
+ if (!hostname.startsWith('[') || !hostname.endsWith(']'))
121
+ return false;
122
+ const normalizedHostname = hostname.slice(1, -1);
123
+ return normalizedHostname.includes(':') && /^[\da-f:.]+$/i.test(normalizedHostname);
124
+ }
125
+ function isValidDomainLabel(label) {
126
+ return label.length > 0 && label.length <= 63 && /^[a-z\d-]+$/i.test(label) && !label.startsWith('-') && !label.endsWith('-');
127
+ }
128
+ function isValidDomainHostname(hostname) {
129
+ const labels = hostname.split('.');
130
+ return labels.length >= 2 && labels.every(isValidDomainLabel);
131
+ }
132
+ function isAllowedHostname(hostname) {
133
+ if (!hostname)
134
+ return false;
135
+ return (hostname === 'localhost' || isValidIpv4(hostname) || isValidIpv6(hostname) || isValidDomainHostname(hostname));
136
+ }
137
+ /**
138
+ * Проверяет является ли строка валидным url.
139
+ *
140
+ * @param {string} originalUrlString - Строка, которую нужно проверить.
141
+ *
142
+ * @returns {boolean} Является ли строка валидным url адресом true/false.
143
+ */
144
+ function isUrlValid(originalUrlString) {
145
+ const urlString = getNormalizedUrlString(originalUrlString);
146
+ if (urlString.length === 0)
147
+ return false;
148
+ try {
149
+ const url = new URL(urlString);
150
+ return HTTP_PROTOCOLS.includes(url.protocol) && isAllowedHostname(url.hostname);
151
+ }
152
+ catch (_a) {
153
+ return false;
154
+ }
155
+ }
156
+ /**
157
+ * Проверяет является ли строка валидным номером телефона.
158
+ *
159
+ * @param {string} value - Строка, которую нужно проверить.
160
+ *
161
+ * @returns {boolean} Является ли строка валидным номером телефона true/false.
162
+ */
163
+ function isPhoneValid(value) {
164
+ const numbersArray = [];
165
+ Array.from(value).forEach(char => {
166
+ if (!!Number(char) || Number(char) === 0)
167
+ numbersArray.push(char);
168
+ });
169
+ if (numbersArray.length < 11 || (numbersArray.length < 12 && value[1] === '+'))
170
+ return false;
171
+ return true;
172
+ }
76
173
  /**
77
174
  * Кастомная сериализация формы в объект FormData.
78
175
  *
@@ -92,12 +189,15 @@ function serializeToFormData($element) {
92
189
  element instanceof HTMLButtonElement)) {
93
190
  return;
94
191
  }
95
- if (!element.name || element.hasAttribute('data-no-serialize')) {
192
+ if (!element.name || element.disabled || element.hasAttribute('data-no-serialize')) {
193
+ return;
194
+ }
195
+ if (element instanceof HTMLButtonElement) {
96
196
  return;
97
197
  }
98
198
  const { name, type } = element;
99
199
  if (type === 'file') {
100
- if (element.getAttribute('multiple') === 'true') {
200
+ if (element.multiple) {
101
201
  const files = element.files;
102
202
  if (files) {
103
203
  for (let i = 0; i < files.length; i++) {
@@ -199,6 +299,29 @@ function parseCommonResponseProperties(responseBody) {
199
299
  }
200
300
  }
201
301
  }
302
+ /**
303
+ * Сериализует данные формы в объект JSON.
304
+ *
305
+ * @param {HTMLFormElement} form - Элемент формы.
306
+ *
307
+ * @returns {Object} Сериализованные данные формы.
308
+ */
309
+ function serializeFormToJSON(form) {
310
+ const obj = {};
311
+ const formData = new FormData(form);
312
+ formData.forEach((value, key) => {
313
+ if (obj.hasOwnProperty(key)) {
314
+ if (!Array.isArray(obj[key])) {
315
+ obj[key] = [obj[key]];
316
+ }
317
+ obj[key].push(value);
318
+ }
319
+ else {
320
+ obj[key] = value;
321
+ }
322
+ });
323
+ return obj;
324
+ }
202
325
 
203
326
  var _a$1;
204
327
  /* ---------- singleton-storage на globalThis ---------- */
@@ -217,7 +340,12 @@ function getAllValidators() {
217
340
  return new Map(validators); // копию, чтобы никто не мутировал оригинал
218
341
  }
219
342
  /* ---------- built-in rules ---------- */
220
- registerValidator('required', (value, $input, $form) => {
343
+ function registerBuiltinValidator(name, fn, defaultMessage) {
344
+ if (!validators.has(name)) {
345
+ validators.set(name, { fn, defaultMessage });
346
+ }
347
+ }
348
+ registerBuiltinValidator('required', (value, $input, $form) => {
221
349
  if ($input && $input.type) {
222
350
  if ($input.type === 'checkbox') {
223
351
  return $input.checked;
@@ -230,10 +358,10 @@ registerValidator('required', (value, $input, $form) => {
230
358
  // fallback — если нет $input (например, в unit-тестах)
231
359
  return value.trim().length > 0;
232
360
  }, 'Пустое значение');
233
- registerValidator('email', v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), 'Неверный формат');
234
- registerValidator('tel', v => /^\+7\d{10}$/.test(v), 'Неверный формат');
235
- registerValidator('url', v => /^(https?:\/\/).+\..+/.test(v), 'Неверный формат');
236
- registerValidator('not-numbers', v => !/[0-9]/.test(v), 'Неверный формат');
361
+ registerBuiltinValidator('email', v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), 'Неверный формат');
362
+ registerBuiltinValidator('tel', v => /^\+7\d{10}$/.test(v), 'Неверный формат');
363
+ registerBuiltinValidator('url', v => isUrlValid(v), 'Неверный формат');
364
+ registerBuiltinValidator('not-numbers', v => !/[0-9]/.test(v), 'Неверный формат');
237
365
 
238
366
  var _a;
239
367
  // --- shared cross-file state (singleton via globalThis) ---
@@ -295,7 +423,7 @@ class Form {
295
423
  * @param {HTMLElement} $el - Элемент формы (тег form!).
296
424
  * @param {Object} options - Параметры формы.
297
425
  */
298
- constructor($el, options) {
426
+ constructor($el, options = {}) {
299
427
  this.$submits = [];
300
428
  this.waitResponse = false;
301
429
  this.inputs = null;
@@ -360,20 +488,10 @@ class Form {
360
488
  this.defaultParams = Object.assign(Object.assign({}, this.defaultParams), params);
361
489
  }
362
490
  findSubmitElements() {
363
- return Array.from(this.$el.querySelectorAll('input[type="submit"], button[type="submit"]'));
491
+ return Array.from(this.$el.querySelectorAll('input[type="submit"], input[type="image"], button[type="submit"], button:not([type])'));
364
492
  }
365
493
  initialization() {
366
494
  this.inputs = this.$el.querySelectorAll(this.config.inputSelector);
367
- /* TODO: Добавить параметр с возможностью отключать/включать кнопку, в зависимости от валидности поля. */
368
- // this.$licensesCheckbox = this.$el.querySelector('input[data-input-name="user-consent"]') as HTMLInputElement;
369
- // if (this.$licensesCheckbox) {
370
- // if (this.$licensesCheckbox.checked) this.enableSubmit();
371
- // else this.disableSubmit();
372
- // this.$licensesCheckbox.addEventListener('change', () => {
373
- // if (this.$licensesCheckbox!.checked) this.enableSubmit();
374
- // else this.disableSubmit();
375
- // });
376
- // }
377
495
  this._onSubmitHandler = (e) => __awaiter(this, void 0, void 0, function* () {
378
496
  var _a, _b, _c, _d;
379
497
  e.preventDefault();
@@ -392,7 +510,26 @@ class Form {
392
510
  * @param {String} text - Текст ошибки.
393
511
  */
394
512
  showError($input, text) {
395
- closest($input, this.config.inputWrapperSelector).showError(text);
513
+ const $inputWrapper = closest($input, this.config.inputWrapperSelector);
514
+ if (!$inputWrapper) {
515
+ console.warn(`Не найдена обёртка для поля с именем: ${$input.name || $input.type}`);
516
+ return;
517
+ }
518
+ const customShowError = $inputWrapper.showError;
519
+ if (typeof customShowError === 'function') {
520
+ customShowError.call($inputWrapper, text);
521
+ return;
522
+ }
523
+ $input.setAttribute('aria-invalid', 'true');
524
+ $inputWrapper.classList.add('input__wrapper--error');
525
+ let $error = $inputWrapper.querySelector('[data-form-father-error]');
526
+ if (!$error) {
527
+ $error = document.createElement('div');
528
+ $error.setAttribute('data-form-father-error', '');
529
+ $error.setAttribute('role', 'alert');
530
+ $input.insertAdjacentElement('afterend', $error);
531
+ }
532
+ $error.textContent = text;
396
533
  }
397
534
  /** Показывает лоадер */
398
535
  showLoader() {
@@ -447,10 +584,13 @@ class Form {
447
584
  * @param {HTMLInputElement} $input - Поле ввода.
448
585
  */
449
586
  hideError($input) {
450
- const $inputWrapper = $input.parentElement;
587
+ var _a;
588
+ const $inputWrapper = closest($input, this.config.inputWrapperSelector) || $input.parentElement;
589
+ $input.removeAttribute('aria-invalid');
590
+ (_a = $inputWrapper === null || $inputWrapper === void 0 ? void 0 : $inputWrapper.querySelector('[data-form-father-error]')) === null || _a === void 0 ? void 0 : _a.remove();
591
+ $inputWrapper === null || $inputWrapper === void 0 ? void 0 : $inputWrapper.classList.remove('input__wrapper--error');
451
592
  if ($inputWrapper && $inputWrapper.classList.contains('input__wrapper')) {
452
593
  $inputWrapper.getAttribute('data-type-error') || 'default';
453
- $inputWrapper.classList.remove('input__wrapper--error');
454
594
  }
455
595
  }
456
596
  /**
@@ -517,7 +657,10 @@ class Form {
517
657
  .split(',')
518
658
  .map(s => s.trim())
519
659
  .filter(Boolean);
520
- list.push(...custom); // 3
660
+ list.push(...custom.map(ruleString => {
661
+ const [rule, ...params] = ruleString.split(':').map(s => s.trim());
662
+ return params.length > 0 ? { rule, params: params.length === 1 ? params[0] : params } : rule;
663
+ })); // 3
521
664
  /* —–– предварительная фильтрация unknown ––– */
522
665
  const filtered = [];
523
666
  for (const r of list) {
@@ -533,7 +676,8 @@ class Form {
533
676
  if ($input.hasAttribute('data-no-validate'))
534
677
  return;
535
678
  const empty = $input.type === 'checkbox' || $input.type === 'radio' ? !$input.checked : $input.value.trim().length === 0;
536
- if (empty && !rules.includes('required')) {
679
+ const hasRequiredRule = rules.some(r => (typeof r === 'string' ? r === 'required' : r.rule === 'required'));
680
+ if (empty && !hasRequiredRule) {
537
681
  this.hideError($input);
538
682
  return;
539
683
  }
@@ -567,6 +711,11 @@ class Form {
567
711
  }
568
712
  this.hideError($input);
569
713
  });
714
+ const getRadioGroup = ($input) => {
715
+ if ($input.type !== 'radio' || !$input.name)
716
+ return [];
717
+ return Array.from($block.querySelectorAll('input[type="radio"]')).filter($radio => $radio.name === $input.name);
718
+ };
570
719
  /* ---------- 1. inputs из schema (config + defaults) ---------- */
571
720
  for (const [key, def] of Object.entries(schema)) {
572
721
  const { selector, rules = [], messages = {} } = def;
@@ -576,6 +725,12 @@ class Form {
576
725
  for (const $input of Array.from(nodeList)) {
577
726
  if (processed.has($input))
578
727
  continue;
728
+ const radioGroup = getRadioGroup($input);
729
+ if (radioGroup.length > 0) {
730
+ radioGroup.forEach($radio => processed.add($radio));
731
+ yield validateInput($input, collectRules($input, rules), messages);
732
+ continue;
733
+ }
579
734
  processed.add($input);
580
735
  yield validateInput($input, collectRules($input, rules), messages);
581
736
  }
@@ -631,8 +786,8 @@ class Form {
631
786
  _cancelAnimations(el) {
632
787
  var _a;
633
788
  // Отменяем все активные WAAPI-анимации у элемента
634
- // @ts-ignore
635
- (_a = el === null || el === void 0 ? void 0 : el.getAnimations) === null || _a === void 0 ? void 0 : _a.call(el).forEach((a) => a.cancel());
789
+ const animatedEl = el;
790
+ (_a = animatedEl === null || animatedEl === void 0 ? void 0 : animatedEl.getAnimations) === null || _a === void 0 ? void 0 : _a.call(animatedEl).forEach(animation => animation.cancel());
636
791
  }
637
792
  /**
638
793
  * Показывает ошибку под полями ввода - ошибку, относящуюся ко всей форме.
@@ -644,7 +799,7 @@ class Form {
644
799
  const inputWrappersList = this.$el.querySelectorAll(this.config.inputWrapperSelector);
645
800
  const $lastInputWrapper = inputWrappersList[inputWrappersList.length - 1];
646
801
  let $errorWrapper = this.$el.querySelector('.error-block-under-input__wrapper');
647
- if (!$errorWrapper || !($lastInputWrapper.nextElementSibling === $errorWrapper)) {
802
+ if (!$errorWrapper || ($lastInputWrapper && !($lastInputWrapper.nextElementSibling === $errorWrapper))) {
648
803
  $errorWrapper = document.createElement('div');
649
804
  $errorWrapper.className = 'error-block-under-input__wrapper';
650
805
  $errorWrapper.innerHTML = `
@@ -667,7 +822,12 @@ class Form {
667
822
  paddingTop: '0px',
668
823
  opacity: '0',
669
824
  });
670
- $lastInputWrapper.insertAdjacentElement('afterend', $errorWrapper);
825
+ if ($lastInputWrapper) {
826
+ $lastInputWrapper.insertAdjacentElement('afterend', $errorWrapper);
827
+ }
828
+ else {
829
+ this.$el.appendChild($errorWrapper);
830
+ }
671
831
  }
672
832
  else {
673
833
  // убедимся, что нужные стартовые inline-стили есть
@@ -682,7 +842,6 @@ class Form {
682
842
  // Сброс success-класса, если он устанавливался где-то в другом коде
683
843
  (_a = $errorWrapper.querySelector('.error-block-under-input')) === null || _a === void 0 ? void 0 : _a.classList.remove('error-block-under-input--success');
684
844
  // Посчитать целевую высоту
685
- $errorWrapper.querySelector('.error-block-under-input');
686
845
  // Чтобы корректно мерить, временно выставим auto
687
846
  const prevHeight = $errorWrapper.style.height;
688
847
  const prevPadding = $errorWrapper.style.paddingTop;
@@ -735,7 +894,7 @@ class Form {
735
894
  hideErrorForm() {
736
895
  const inputWrappersList = this.$el.querySelectorAll(this.config.inputWrapperSelector);
737
896
  const $lastInputWrapper = inputWrappersList[inputWrappersList.length - 1];
738
- const $errorWrapper = $lastInputWrapper.nextElementSibling;
897
+ const $errorWrapper = (($lastInputWrapper === null || $lastInputWrapper === void 0 ? void 0 : $lastInputWrapper.nextElementSibling) || this.$el.querySelector('.error-block-under-input__wrapper'));
739
898
  if (!$errorWrapper || !$errorWrapper.classList.contains('error-block-under-input__wrapper'))
740
899
  return;
741
900
  // Текущая высота (если была auto) — измерим и зафиксируем
@@ -816,11 +975,8 @@ class Form {
816
975
  btnLoader.remove();
817
976
  });
818
977
  // 4) Сбросить ошибки у инпутов (если разметка позволяет)
819
- const inputs = this.$el.querySelectorAll('input, textarea');
820
- inputs.forEach($i => {
821
- if ($i instanceof HTMLInputElement)
822
- this.hideError($i);
823
- });
978
+ const inputs = this.$el.querySelectorAll('input, textarea, select');
979
+ inputs.forEach($i => this.hideError($i));
824
980
  // 5) Убрать возможный флаг скролла
825
981
  document.documentElement.removeAttribute('data-now-scrolling');
826
982
  // 6) Очистить ссылки на элементы, чтобы помочь GC
@@ -844,6 +1000,115 @@ class Form {
844
1000
  }
845
1001
  return true;
846
1002
  }
1003
+ formDataToObject(formData) {
1004
+ const data = {};
1005
+ formData.forEach((value, key) => {
1006
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
1007
+ if (!Array.isArray(data[key])) {
1008
+ data[key] = [data[key]];
1009
+ }
1010
+ data[key].push(value);
1011
+ }
1012
+ else {
1013
+ data[key] = value;
1014
+ }
1015
+ });
1016
+ return data;
1017
+ }
1018
+ appendFormDataValue(formData, key, value) {
1019
+ if (value === undefined || value === null)
1020
+ return;
1021
+ if (Array.isArray(value)) {
1022
+ value.forEach((item, index) => {
1023
+ this.appendFormDataValue(formData, `${key}[${index}]`, item);
1024
+ });
1025
+ return;
1026
+ }
1027
+ if (value instanceof Blob) {
1028
+ formData.append(key, value);
1029
+ return;
1030
+ }
1031
+ formData.append(key, typeof value === 'object' ? JSON.stringify(value) : String(value));
1032
+ }
1033
+ objectToFormData(data) {
1034
+ const formData = new FormData();
1035
+ Object.entries(data).forEach(([key, value]) => {
1036
+ this.appendFormDataValue(formData, key, value);
1037
+ });
1038
+ return formData;
1039
+ }
1040
+ stringifyFormDataValue(value) {
1041
+ return typeof value === 'string' ? value : value.name;
1042
+ }
1043
+ formDataToUrlSearchParams(formData) {
1044
+ const params = new URLSearchParams();
1045
+ formData.forEach((value, key) => {
1046
+ params.append(key, this.stringifyFormDataValue(value));
1047
+ });
1048
+ return params;
1049
+ }
1050
+ formDataToTextBody(formData) {
1051
+ return Array.from(formData)
1052
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(this.stringifyFormDataValue(value))}`)
1053
+ .join('\n');
1054
+ }
1055
+ isBodylessMethod(method) {
1056
+ return method === 'GET' || method === 'HEAD';
1057
+ }
1058
+ appendQueryString(action, params) {
1059
+ const query = params.toString();
1060
+ if (!query)
1061
+ return action;
1062
+ const separator = action.includes('?') ? '&' : '?';
1063
+ return `${action}${separator}${query}`;
1064
+ }
1065
+ findFieldByName(name) {
1066
+ const fields = Array.from(this.$el.querySelectorAll('input, textarea, select'));
1067
+ return fields.find($field => $field.name === name) || null;
1068
+ }
1069
+ parseResponseBody(response) {
1070
+ return __awaiter(this, void 0, void 0, function* () {
1071
+ try {
1072
+ const responseBody = yield response.json();
1073
+ return responseBody && typeof responseBody === 'object'
1074
+ ? responseBody
1075
+ : {
1076
+ success: false,
1077
+ error: true,
1078
+ 'error-msg': 'Некорректный ответ сервера',
1079
+ };
1080
+ }
1081
+ catch (_a) {
1082
+ return {
1083
+ success: false,
1084
+ error: true,
1085
+ 'error-msg': 'Некорректный ответ сервера',
1086
+ };
1087
+ }
1088
+ });
1089
+ }
1090
+ handleUnsuccessfulResponse(responseBody) {
1091
+ if (responseBody.error !== true)
1092
+ return;
1093
+ if (responseBody['error-msg']) {
1094
+ this.showErrorForm(responseBody['error-msg']);
1095
+ }
1096
+ if (responseBody.errors) {
1097
+ const fieldsList = [];
1098
+ responseBody.errors.forEach(error => {
1099
+ const $field = this.findFieldByName(error.name);
1100
+ if ($field) {
1101
+ fieldsList.push($field);
1102
+ this.showError($field, error['error-msg']);
1103
+ }
1104
+ else {
1105
+ console.warn(`Не найдено поле с именем: ${error.name}, для вывода ошибки: ${error['error-msg']}`);
1106
+ }
1107
+ });
1108
+ if (this.config.scrollToFirstErroredInput === true)
1109
+ this.scrollToFirstErroredInput(fieldsList);
1110
+ }
1111
+ }
847
1112
  sendData() {
848
1113
  return __awaiter(this, void 0, void 0, function* () {
849
1114
  var _a;
@@ -854,31 +1119,34 @@ class Form {
854
1119
  let headers = {};
855
1120
  // Сбор данных из формы
856
1121
  const formData = serializeToFormData(this.$el);
857
- let data = {};
858
- formData.forEach((value, key) => {
859
- data[key] = value;
860
- });
1122
+ let data = this.formDataToObject(formData);
861
1123
  if (this.config.logging) {
862
1124
  for (const key of formData.keys())
863
1125
  console.log(formData.get(key));
864
1126
  }
865
1127
  // Применение обёртки, если указано в настройках
1128
+ const hasDataWrapper = typeof this.config.wrapData === 'function';
866
1129
  if (typeof this.config.wrapData === 'function') {
867
1130
  data = this.config.wrapData(data);
868
1131
  }
1132
+ const requestFormData = hasDataWrapper ? this.objectToFormData(data) : formData;
1133
+ if (this.isBodylessMethod(method)) {
1134
+ return fetch(this.appendQueryString(action, this.formDataToUrlSearchParams(requestFormData)), {
1135
+ method,
1136
+ headers,
1137
+ });
1138
+ }
869
1139
  switch (enctype) {
870
1140
  case 'application/x-www-form-urlencoded':
871
- body = new URLSearchParams(serializeToFormData(this.$el)).toString();
1141
+ body = this.formDataToUrlSearchParams(requestFormData).toString();
872
1142
  headers['Content-Type'] = 'application/x-www-form-urlencoded';
873
1143
  break;
874
1144
  case 'multipart/form-data':
875
- body = serializeToFormData(this.$el);
1145
+ body = requestFormData;
876
1146
  // ВАЖНО: Не указываем Content-Type, т.к. браузер сам добавит с корректной границей!
877
1147
  break;
878
1148
  case 'text/plain':
879
- body = Array.from(serializeToFormData(this.$el))
880
- .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
881
- .join('\n');
1149
+ body = this.formDataToTextBody(requestFormData);
882
1150
  headers['Content-Type'] = 'text/plain';
883
1151
  break;
884
1152
  case 'application/json':
@@ -887,7 +1155,7 @@ class Form {
887
1155
  break;
888
1156
  default:
889
1157
  console.warn(`Неизвестный enctype: ${enctype}. Используется application/x-www-form-urlencoded.`);
890
- body = new URLSearchParams(serializeToFormData(this.$el)).toString();
1158
+ body = this.formDataToUrlSearchParams(requestFormData).toString();
891
1159
  headers['Content-Type'] = 'application/x-www-form-urlencoded';
892
1160
  }
893
1161
  return fetch(action, {
@@ -905,45 +1173,37 @@ class Form {
905
1173
  this.showLoader();
906
1174
  (_b = (_a = this.config).onSubmit) === null || _b === void 0 ? void 0 : _b.call(_a, this);
907
1175
  this.waitResponse = true;
908
- const response = yield this.sendData();
909
- const responseBody = yield response.json();
910
- this.waitResponse = false;
911
- if (this.config.showLoaderButton === true)
912
- this.hideLoader();
913
- (_d = (_c = this.config).onResponse) === null || _d === void 0 ? void 0 : _d.call(_c, responseBody, this);
914
- if (response.status === 200) {
915
- if (responseBody.success === true) {
1176
+ let loadingFinished = false;
1177
+ const finishLoading = () => {
1178
+ if (loadingFinished)
1179
+ return;
1180
+ this.waitResponse = false;
1181
+ if (this.config.showLoaderButton === true)
1182
+ this.hideLoader();
1183
+ loadingFinished = true;
1184
+ };
1185
+ try {
1186
+ const response = yield this.sendData();
1187
+ const responseBody = yield this.parseResponseBody(response);
1188
+ finishLoading();
1189
+ (_d = (_c = this.config).onResponse) === null || _d === void 0 ? void 0 : _d.call(_c, responseBody, this);
1190
+ if (response.ok && responseBody.success === true) {
916
1191
  (_f = (_e = this.config).onResponseSuccess) === null || _f === void 0 ? void 0 : _f.call(_e, responseBody, this);
917
1192
  }
918
1193
  else {
919
1194
  (_h = (_g = this.config).onResponseUnsuccess) === null || _h === void 0 ? void 0 : _h.call(_g, responseBody, this);
920
- if (responseBody.error === true) {
921
- if (responseBody['error-msg']) {
922
- this.showErrorForm(responseBody['error-msg']);
923
- }
924
- if (responseBody.errors) {
925
- const inputsList = [];
926
- responseBody.errors.forEach(error => {
927
- const $input = this.$el.querySelector(`input[name="${error.name}"]`);
928
- if ($input) {
929
- inputsList.push($input);
930
- this.showError($input, error['error-msg']);
931
- }
932
- else {
933
- console.warn(`Не найдено поле с именем: ${error.name}, для вывода ошибки: ${error['error-msg']}`);
934
- }
935
- });
936
- if (this.config.scrollToFirstErroredInput === true)
937
- this.scrollToFirstErroredInput(inputsList);
938
- }
939
- }
1195
+ this.handleUnsuccessfulResponse(responseBody);
940
1196
  }
941
1197
  parseCommonResponseProperties(responseBody);
942
1198
  }
1199
+ catch (error) {
1200
+ finishLoading();
1201
+ throw error;
1202
+ }
943
1203
  }
944
1204
  });
945
1205
  }
946
1206
  }
947
1207
 
948
- export { Form as default, getAllValidators, getValidator, registerValidator };
1208
+ export { blockScrollBody, closest, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
949
1209
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}