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.
- package/README.md +223 -9
- package/RESPONSE_API.md +233 -0
- package/dist/FormFather.min.js +2 -0
- package/{index.d.ts → dist/index.d.ts} +97 -12
- package/{index.js → dist/index.js} +334 -74
- package/{index.js.map → dist/index.js.map} +1 -1
- package/{types → dist/types}/index.d.ts +37 -13
- package/package.json +20 -3
- package/FormFather.min.js +0 -2
- /package/{FormFather.min.js.map → dist/FormFather.min.js.map} +0 -0
- /package/{types → dist/types}/helpers.d.ts +0 -0
- /package/{types → dist/types}/validators.d.ts +0 -0
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
635
|
-
(_a =
|
|
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
|
|
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 =
|
|
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 =
|
|
1145
|
+
body = requestFormData;
|
|
876
1146
|
// ВАЖНО: Не указываем Content-Type, т.к. браузер сам добавит с корректной границей!
|
|
877
1147
|
break;
|
|
878
1148
|
case 'text/plain':
|
|
879
|
-
body =
|
|
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 =
|
|
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
|
-
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
this.
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|