form-father 0.7.0 → 0.8.2
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/CHANGELOG.md +69 -0
- package/dist/FormFather.min.js +1 -1
- package/dist/FormFather.min.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1665 -1950
- package/dist/index.js.map +1 -1
- package/package.json +38 -22
package/dist/index.js
CHANGED
|
@@ -1,1038 +1,852 @@
|
|
|
1
|
-
/******************************************************************************
|
|
2
|
-
Copyright (c) Microsoft Corporation.
|
|
3
|
-
|
|
4
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
5
|
-
purpose with or without fee is hereby granted.
|
|
6
|
-
|
|
7
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
8
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
9
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
10
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
11
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
12
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
13
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
14
|
-
***************************************************************************** */
|
|
15
|
-
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
function __rest(s, e) {
|
|
19
|
-
var t = {};
|
|
20
|
-
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
21
|
-
t[p] = s[p];
|
|
22
|
-
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
23
|
-
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
24
|
-
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
25
|
-
t[p[i]] = s[p[i]];
|
|
26
|
-
}
|
|
27
|
-
return t;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function __awaiter(thisArg, _arguments, P, generator) {
|
|
31
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
32
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
33
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
34
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
35
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
36
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
41
|
-
var e = new Error(message);
|
|
42
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Возвращает форматированную дату.
|
|
47
|
-
*
|
|
48
|
-
* @param {string} dateValue
|
|
49
|
-
* @param {string} format
|
|
50
|
-
*
|
|
51
|
-
* @returns {string}
|
|
52
|
-
*/
|
|
53
1
|
function formatDate(dateValue, format) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* @returns {string}
|
|
61
|
-
*/
|
|
62
|
-
const pad = (num) => num.toString().padStart(2, '0');
|
|
63
|
-
const day = pad(date.getDate());
|
|
64
|
-
const month = pad(date.getMonth() + 1);
|
|
65
|
-
const year = date.getFullYear().toString();
|
|
66
|
-
return format.replace('dd', day).replace('mm', month).replace('yyyy', year);
|
|
2
|
+
const date = new Date(dateValue);
|
|
3
|
+
const pad = (num) => num.toString().padStart(2, "0");
|
|
4
|
+
const day = pad(date.getDate());
|
|
5
|
+
const month = pad(date.getMonth() + 1);
|
|
6
|
+
const year = date.getFullYear().toString();
|
|
7
|
+
return format.replace("dd", day).replace("mm", month).replace("yyyy", year);
|
|
67
8
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Находит родительский элемент по заданому селектору.
|
|
70
|
-
*
|
|
71
|
-
* @param {HTMLElement | null} $el - Элемент, чего родителя нужно найти.
|
|
72
|
-
* @param {string} selector - Селектор родителя.
|
|
73
|
-
*
|
|
74
|
-
* @returns {HTMLElement | null} Элемент родителя. Но если родителя с таким селектори не было найдено возвращается null.
|
|
75
|
-
*/
|
|
76
9
|
function closest($el, selector) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return null;
|
|
10
|
+
if ($el == null) return null;
|
|
11
|
+
let parent = $el;
|
|
12
|
+
while (parent) {
|
|
13
|
+
if (parent.matches(selector)) {
|
|
14
|
+
return parent;
|
|
15
|
+
}
|
|
16
|
+
parent = parent.parentElement;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
87
19
|
}
|
|
88
|
-
/** Включает блокировку скролла страницы */
|
|
89
20
|
function blockScrollBody() {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
21
|
+
window.lastBlockScrollPosition = window.pageYOffset;
|
|
22
|
+
document.body.style.top = `${-window.lastBlockScrollPosition}px`;
|
|
23
|
+
document.body.classList.add("body--block-scroll");
|
|
93
24
|
}
|
|
94
|
-
/** Отменяет блокировку скролла страницы */
|
|
95
25
|
function unblockScrollBody() {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
26
|
+
document.body.classList.remove("body--block-scroll");
|
|
27
|
+
document.body.style.top = "";
|
|
28
|
+
window.scrollTo(0, window.lastBlockScrollPosition || 0);
|
|
99
29
|
}
|
|
100
|
-
/**
|
|
101
|
-
* Проверяет является ли строка валидным адресом электронной почты.
|
|
102
|
-
*
|
|
103
|
-
* @param {string} value - Строка, которую нужно проверить.
|
|
104
|
-
*
|
|
105
|
-
* @returns {boolean} Является ли строка валидным адресом электронной почты true/false.
|
|
106
|
-
*/
|
|
107
30
|
function isEmailValid(value) {
|
|
108
|
-
|
|
31
|
+
return /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu.test(
|
|
32
|
+
value
|
|
33
|
+
);
|
|
109
34
|
}
|
|
110
|
-
const HTTP_PROTOCOLS = [
|
|
35
|
+
const HTTP_PROTOCOLS = ["http:", "https:"];
|
|
111
36
|
function getNormalizedUrlString(originalUrlString) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return /^[a-z][a-z\d+.-]*:\/\//i.test(trimmedUrl) ? trimmedUrl : `https://${trimmedUrl}`;
|
|
37
|
+
const trimmedUrl = originalUrlString.trim();
|
|
38
|
+
if (trimmedUrl.length === 0) return "";
|
|
39
|
+
if (trimmedUrl.startsWith("//")) {
|
|
40
|
+
return `https:${trimmedUrl}`;
|
|
41
|
+
}
|
|
42
|
+
return /^[a-z][a-z\d+.-]*:\/\//i.test(trimmedUrl) ? trimmedUrl : `https://${trimmedUrl}`;
|
|
119
43
|
}
|
|
120
44
|
function isValidIpv4(hostname) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return num >= 0 && num <= 255;
|
|
129
|
-
});
|
|
45
|
+
const parts = hostname.split(".");
|
|
46
|
+
if (parts.length !== 4) return false;
|
|
47
|
+
return parts.every((part) => {
|
|
48
|
+
if (!/^\d{1,3}$/.test(part)) return false;
|
|
49
|
+
const num = Number(part);
|
|
50
|
+
return num >= 0 && num <= 255;
|
|
51
|
+
});
|
|
130
52
|
}
|
|
131
53
|
function isValidIpv6(hostname) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return normalizedHostname.includes(':') && /^[\da-f:.]+$/i.test(normalizedHostname);
|
|
54
|
+
if (!hostname.startsWith("[") || !hostname.endsWith("]")) return false;
|
|
55
|
+
const normalizedHostname = hostname.slice(1, -1);
|
|
56
|
+
return normalizedHostname.includes(":") && /^[\da-f:.]+$/i.test(normalizedHostname);
|
|
136
57
|
}
|
|
137
58
|
function isValidDomainLabel(label) {
|
|
138
|
-
|
|
59
|
+
return label.length > 0 && label.length <= 63 && /^[a-z\d-]+$/i.test(label) && !label.startsWith("-") && !label.endsWith("-");
|
|
139
60
|
}
|
|
140
61
|
function isValidDomainHostname(hostname) {
|
|
141
|
-
|
|
142
|
-
|
|
62
|
+
const labels = hostname.split(".");
|
|
63
|
+
return labels.length >= 2 && labels.every(isValidDomainLabel);
|
|
143
64
|
}
|
|
144
65
|
function isAllowedHostname(hostname) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return (hostname === 'localhost' || isValidIpv4(hostname) || isValidIpv6(hostname) || isValidDomainHostname(hostname));
|
|
66
|
+
if (!hostname) return false;
|
|
67
|
+
return hostname === "localhost" || isValidIpv4(hostname) || isValidIpv6(hostname) || isValidDomainHostname(hostname);
|
|
148
68
|
}
|
|
149
|
-
/**
|
|
150
|
-
* Проверяет является ли строка валидным url.
|
|
151
|
-
*
|
|
152
|
-
* @param {string} originalUrlString - Строка, которую нужно проверить.
|
|
153
|
-
*
|
|
154
|
-
* @returns {boolean} Является ли строка валидным url адресом true/false.
|
|
155
|
-
*/
|
|
156
69
|
function isUrlValid(originalUrlString) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
70
|
+
const urlString = getNormalizedUrlString(originalUrlString);
|
|
71
|
+
if (urlString.length === 0) return false;
|
|
72
|
+
try {
|
|
73
|
+
const url = new URL(urlString);
|
|
74
|
+
return HTTP_PROTOCOLS.includes(url.protocol) && isAllowedHostname(url.hostname);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
167
78
|
}
|
|
168
|
-
/**
|
|
169
|
-
* Проверяет является ли строка валидным номером телефона.
|
|
170
|
-
*
|
|
171
|
-
* @param {string} value - Строка, которую нужно проверить.
|
|
172
|
-
*
|
|
173
|
-
* @returns {boolean} Является ли строка валидным номером телефона true/false.
|
|
174
|
-
*/
|
|
175
79
|
function isPhoneValid(value) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return false;
|
|
183
|
-
return true;
|
|
80
|
+
const numbersArray = [];
|
|
81
|
+
Array.from(value).forEach((char) => {
|
|
82
|
+
if (!!Number(char) || Number(char) === 0) numbersArray.push(char);
|
|
83
|
+
});
|
|
84
|
+
if (numbersArray.length < 11 || numbersArray.length < 12 && value[1] === "+") return false;
|
|
85
|
+
return true;
|
|
184
86
|
}
|
|
185
|
-
/**
|
|
186
|
-
* Кастомная сериализация формы в объект FormData.
|
|
187
|
-
*
|
|
188
|
-
* @param {HTMLElement} $element - DOM элемент для сериализации.
|
|
189
|
-
*
|
|
190
|
-
* @returns {FormData} Сериализованные данные формы в объекте FormData.
|
|
191
|
-
*/
|
|
192
87
|
function serializeToFormData($element) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
else {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
.replace(/^8/, '+7');
|
|
249
|
-
}
|
|
250
|
-
else if (type === 'checkbox' || type === 'radio') {
|
|
251
|
-
if (!element.checked)
|
|
252
|
-
return;
|
|
253
|
-
value = element.value;
|
|
254
|
-
}
|
|
255
|
-
else if (type === 'date') {
|
|
256
|
-
const format = element.getAttribute('data-date-format');
|
|
257
|
-
value = format ? formatDate(element.value, format) : element.value;
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
value = element.value;
|
|
261
|
-
}
|
|
262
|
-
if (value && type !== 'select-one' && type !== 'select-multiple') {
|
|
263
|
-
data.append(name, value);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
return data;
|
|
88
|
+
const elements = $element.tagName === "FORM" ? Array.from($element.elements) : Array.from($element.querySelectorAll("input, select, textarea, button"));
|
|
89
|
+
const data = new FormData();
|
|
90
|
+
elements.forEach((element) => {
|
|
91
|
+
if (!(element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement || element instanceof HTMLButtonElement)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!element.name || element.disabled || element.hasAttribute("data-no-serialize")) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (element instanceof HTMLButtonElement) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const { name, type } = element;
|
|
101
|
+
if (type === "file") {
|
|
102
|
+
if (element.multiple) {
|
|
103
|
+
const files = element.files;
|
|
104
|
+
if (files) {
|
|
105
|
+
for (let i = 0; i < files.length; i++) {
|
|
106
|
+
data.append(`${name}[${i}]`, files[i]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
const files = element.files;
|
|
111
|
+
if (files && files[0]) data.append(name, files[0]);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
let value;
|
|
115
|
+
if (type === "select-one" || type === "select-multiple") {
|
|
116
|
+
const select = element;
|
|
117
|
+
if (type === "select-multiple") {
|
|
118
|
+
const selectedOptions = Array.from(select.selectedOptions).map((option) => option.value);
|
|
119
|
+
selectedOptions.forEach((val, index) => {
|
|
120
|
+
data.append(`${name}[${index}]`, val);
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
value = select.value;
|
|
124
|
+
if (value) data.append(name, value);
|
|
125
|
+
}
|
|
126
|
+
} else if (type === "tel") {
|
|
127
|
+
value = element.value.split(/[\s()-]/).join("").replace(/^8/, "+7");
|
|
128
|
+
} else if (type === "checkbox" || type === "radio") {
|
|
129
|
+
if (!element.checked) return;
|
|
130
|
+
value = element.value;
|
|
131
|
+
} else if (type === "date") {
|
|
132
|
+
const format = element.getAttribute("data-date-format");
|
|
133
|
+
value = format ? formatDate(element.value, format) : element.value;
|
|
134
|
+
} else {
|
|
135
|
+
value = element.value;
|
|
136
|
+
}
|
|
137
|
+
if (value && type !== "select-one" && type !== "select-multiple") {
|
|
138
|
+
data.append(name, value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
return data;
|
|
268
143
|
}
|
|
269
|
-
/**
|
|
270
|
-
* Функция для разбора общих свойств ответа.
|
|
271
|
-
*
|
|
272
|
-
* @param {any} responseBody - Тело ответа.
|
|
273
|
-
*/
|
|
274
144
|
function parseCommonResponseProperties(responseBody) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
else if (Object.prototype.hasOwnProperty.call(responseBody, 'error-toast')) {
|
|
309
|
-
/** @deprecated */
|
|
310
|
-
window.showToast(responseBody['error-toast']);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
145
|
+
if (Object.prototype.hasOwnProperty.call(responseBody, "redirect-url")) {
|
|
146
|
+
if (responseBody["redirect-url-delay"]) {
|
|
147
|
+
const redirectUrlDelay = Number(responseBody["redirect-url-delay"]);
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
window.location.href = responseBody["redirect-url"];
|
|
150
|
+
}, redirectUrlDelay);
|
|
151
|
+
} else {
|
|
152
|
+
window.location.href = responseBody["redirect-url"];
|
|
153
|
+
}
|
|
154
|
+
} else if (Object.prototype.hasOwnProperty.call(responseBody, "reload")) {
|
|
155
|
+
if (responseBody.reload === true) {
|
|
156
|
+
if (responseBody["reload-delay"]) {
|
|
157
|
+
const reloadDelay = Number(responseBody["reload-delay"]);
|
|
158
|
+
setTimeout(() => {
|
|
159
|
+
window.location.reload();
|
|
160
|
+
}, reloadDelay);
|
|
161
|
+
} else {
|
|
162
|
+
window.location.reload();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
if (Object.prototype.hasOwnProperty.call(responseBody, "toast")) {
|
|
167
|
+
const toasts = responseBody["toast"];
|
|
168
|
+
const normalizedToasts = Array.isArray(toasts) ? toasts : [toasts];
|
|
169
|
+
normalizedToasts.forEach((toast) => {
|
|
170
|
+
window.showToast(toast);
|
|
171
|
+
});
|
|
172
|
+
} else if (Object.prototype.hasOwnProperty.call(responseBody, "error-toast")) {
|
|
173
|
+
window.showToast(responseBody["error-toast"]);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
313
176
|
}
|
|
314
|
-
/**
|
|
315
|
-
* Сериализует данные формы в объект JSON.
|
|
316
|
-
*
|
|
317
|
-
* @param {HTMLFormElement} form - Элемент формы.
|
|
318
|
-
*
|
|
319
|
-
* @returns {Object} Сериализованные данные формы.
|
|
320
|
-
*/
|
|
321
177
|
function serializeFormToJSON(form) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return obj;
|
|
178
|
+
const obj = {};
|
|
179
|
+
const formData = new FormData(form);
|
|
180
|
+
formData.forEach((value, key) => {
|
|
181
|
+
if (obj.hasOwnProperty(key)) {
|
|
182
|
+
if (!Array.isArray(obj[key])) {
|
|
183
|
+
obj[key] = [obj[key]];
|
|
184
|
+
}
|
|
185
|
+
obj[key].push(value);
|
|
186
|
+
} else {
|
|
187
|
+
obj[key] = value;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return obj;
|
|
336
191
|
}
|
|
337
192
|
|
|
338
193
|
var _a$1;
|
|
339
|
-
|
|
340
|
-
const GLOBAL_KEY =
|
|
341
|
-
const validators = (_a$1 = globalThis[GLOBAL_KEY]) !== null && _a$1 !== void 0 ? _a$1 : (globalThis[GLOBAL_KEY] = new Map());
|
|
342
|
-
/* ---------- API ---------- */
|
|
194
|
+
const GLOBAL_KEY = /* @__PURE__ */ Symbol.for("FormFather.validators");
|
|
195
|
+
const validators = (_a$1 = globalThis[GLOBAL_KEY]) != null ? _a$1 : globalThis[GLOBAL_KEY] = /* @__PURE__ */ new Map();
|
|
343
196
|
function registerValidator(name, fn, defaultMessage, { override = false } = {}) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
197
|
+
if (validators.has(name) && !override) {
|
|
198
|
+
console.warn(`[FormFather] Validator "${name}" already exists; pass { override: true } to replace`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
validators.set(name, { fn, defaultMessage });
|
|
349
202
|
}
|
|
350
203
|
const getValidator = (name) => validators.get(name);
|
|
351
204
|
function getAllValidators() {
|
|
352
|
-
|
|
205
|
+
return new Map(validators);
|
|
353
206
|
}
|
|
354
|
-
/* ---------- built-in rules ---------- */
|
|
355
207
|
function registerBuiltinValidator(name, fn, defaultMessage) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
208
|
+
if (!validators.has(name)) {
|
|
209
|
+
validators.set(name, { fn, defaultMessage });
|
|
210
|
+
}
|
|
359
211
|
}
|
|
360
|
-
registerBuiltinValidator(
|
|
212
|
+
registerBuiltinValidator(
|
|
213
|
+
"required",
|
|
214
|
+
(value, $input, $form) => {
|
|
361
215
|
if ($input && $input.type) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
216
|
+
if ($input.type === "checkbox") {
|
|
217
|
+
return $input.checked;
|
|
218
|
+
}
|
|
219
|
+
if ($input.type === "radio") {
|
|
220
|
+
const group = $form.querySelectorAll(`input[type="radio"][name="${$input.name}"]`);
|
|
221
|
+
return Array.from(group).some((el) => el.checked);
|
|
222
|
+
}
|
|
369
223
|
}
|
|
370
|
-
// fallback — если нет $input (например, в unit-тестах)
|
|
371
224
|
return value.trim().length > 0;
|
|
372
|
-
},
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
registerBuiltinValidator(
|
|
376
|
-
registerBuiltinValidator(
|
|
225
|
+
},
|
|
226
|
+
"\u041F\u0443\u0441\u0442\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435"
|
|
227
|
+
);
|
|
228
|
+
registerBuiltinValidator("email", (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442");
|
|
229
|
+
registerBuiltinValidator("tel", (v) => /^\+7\d{10}$/.test(v), "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442");
|
|
230
|
+
registerBuiltinValidator("url", (v) => isUrlValid(v), "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442");
|
|
231
|
+
registerBuiltinValidator("not-numbers", (v) => !/[0-9]/.test(v), "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442");
|
|
377
232
|
|
|
378
233
|
function readFirstIssueMessage(error) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
return typeof error.message === 'string' ? error.message : undefined;
|
|
234
|
+
var _a;
|
|
235
|
+
if (!error || typeof error !== "object") return void 0;
|
|
236
|
+
const issues = error.issues || error.errors;
|
|
237
|
+
if (Array.isArray(issues)) {
|
|
238
|
+
const firstMessage = (_a = issues.find((issue) => typeof (issue == null ? void 0 : issue.message) === "string")) == null ? void 0 : _a.message;
|
|
239
|
+
if (firstMessage) return firstMessage;
|
|
240
|
+
}
|
|
241
|
+
return typeof error.message === "string" ? error.message : void 0;
|
|
389
242
|
}
|
|
390
243
|
function resolveMessage(error, message) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
return message || readFirstIssueMessage(error);
|
|
244
|
+
if (typeof message === "function") return message(error);
|
|
245
|
+
return message || readFirstIssueMessage(error);
|
|
394
246
|
}
|
|
395
247
|
function normalizeResult(result, message) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
return true;
|
|
400
|
-
return result ? true : { valid: false, message };
|
|
248
|
+
if (typeof result === "object" && result !== null) return result;
|
|
249
|
+
if (result === void 0) return true;
|
|
250
|
+
return result ? true : { valid: false, message };
|
|
401
251
|
}
|
|
402
252
|
function createSchemaValidator(schema, options = {}) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
return { valid: false, message: resolveMessage(undefined, options.message) };
|
|
424
|
-
});
|
|
253
|
+
return async (value, $input, $form, params) => {
|
|
254
|
+
const mappedValue = options.mapValue ? options.mapValue(value, $input, $form, params) : value;
|
|
255
|
+
try {
|
|
256
|
+
if (typeof schema === "function") {
|
|
257
|
+
return normalizeResult(await schema(mappedValue), resolveMessage(void 0, options.message));
|
|
258
|
+
}
|
|
259
|
+
if (typeof schema.safeParse === "function") {
|
|
260
|
+
const result = await schema.safeParse(mappedValue);
|
|
261
|
+
if (result.success) return true;
|
|
262
|
+
return { valid: false, message: resolveMessage(result.error || result, options.message) };
|
|
263
|
+
}
|
|
264
|
+
if (typeof schema.parse === "function") {
|
|
265
|
+
await schema.parse(mappedValue);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
return { valid: false, message: resolveMessage(error, options.message) };
|
|
270
|
+
}
|
|
271
|
+
return { valid: false, message: resolveMessage(void 0, options.message) };
|
|
272
|
+
};
|
|
425
273
|
}
|
|
426
274
|
function registerSchemaValidator(name, schema, defaultMessage, options = {}) {
|
|
427
|
-
|
|
428
|
-
|
|
275
|
+
const { override, ...adapterOptions } = options;
|
|
276
|
+
registerValidator(name, createSchemaValidator(schema, adapterOptions), defaultMessage, { override });
|
|
429
277
|
}
|
|
430
278
|
function registerFieldValidator(name, predicate, defaultMessage, options = {}) {
|
|
431
|
-
|
|
279
|
+
registerValidator(name, createFieldValidator(predicate), defaultMessage, options);
|
|
432
280
|
}
|
|
433
281
|
function createFieldValidator(predicate, message) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
282
|
+
return async (value, $input, $form, params) => {
|
|
283
|
+
const valid = await predicate(value, $input, $form, params);
|
|
284
|
+
return valid ? true : { valid: false, message };
|
|
285
|
+
};
|
|
438
286
|
}
|
|
439
287
|
function createPatternValidator(pattern, message) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
288
|
+
return createFieldValidator((value) => {
|
|
289
|
+
pattern.lastIndex = 0;
|
|
290
|
+
return pattern.test(value);
|
|
291
|
+
}, message);
|
|
444
292
|
}
|
|
445
293
|
function createLengthValidator(options, message) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
return false;
|
|
454
|
-
return true;
|
|
455
|
-
}, message);
|
|
294
|
+
return createFieldValidator((value) => {
|
|
295
|
+
const normalizedValue = options.trim === false ? value : value.trim();
|
|
296
|
+
if (normalizedValue.length === 0 && options.emptyIsValid === true) return true;
|
|
297
|
+
if (typeof options.min === "number" && normalizedValue.length < options.min) return false;
|
|
298
|
+
if (typeof options.max === "number" && normalizedValue.length > options.max) return false;
|
|
299
|
+
return true;
|
|
300
|
+
}, message);
|
|
456
301
|
}
|
|
457
302
|
|
|
458
303
|
var _a;
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
304
|
+
const FORM_GLOBAL_KEY = /* @__PURE__ */ Symbol.for("formfather.shared");
|
|
305
|
+
const __shared = (
|
|
306
|
+
// уже существует? используем его
|
|
307
|
+
(_a = globalThis[FORM_GLOBAL_KEY]) != null ? _a : (
|
|
308
|
+
// иначе создаём
|
|
309
|
+
globalThis[FORM_GLOBAL_KEY] = {
|
|
310
|
+
defaultParams: {},
|
|
311
|
+
defaultValidationSchema: void 0
|
|
312
|
+
}
|
|
313
|
+
)
|
|
314
|
+
);
|
|
315
|
+
const FORM_ERROR_FIELD = "_form";
|
|
470
316
|
function readFormValue(values, field) {
|
|
471
|
-
|
|
317
|
+
return values[field];
|
|
472
318
|
}
|
|
473
319
|
function isEmptyFormValue(value) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return value === undefined || value === null || String(value).trim().length === 0;
|
|
320
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
321
|
+
return value === void 0 || value === null || String(value).trim().length === 0;
|
|
477
322
|
}
|
|
478
|
-
function resolveFormValidatorIssue(context, issue, fallbackField, fallbackRule) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
323
|
+
async function resolveFormValidatorIssue(context, issue, fallbackField, fallbackRule) {
|
|
324
|
+
const resolvedIssue = typeof issue === "function" ? await issue(context) : issue;
|
|
325
|
+
if (typeof resolvedIssue === "string") {
|
|
326
|
+
return {
|
|
327
|
+
field: fallbackField,
|
|
328
|
+
rule: fallbackRule,
|
|
329
|
+
message: resolvedIssue
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
field: fallbackField,
|
|
334
|
+
rule: fallbackRule,
|
|
335
|
+
...resolvedIssue
|
|
336
|
+
};
|
|
490
337
|
}
|
|
491
338
|
function createFormValidator(predicate, issue) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
339
|
+
return async (context) => {
|
|
340
|
+
const valid = await predicate(context);
|
|
341
|
+
return valid ? true : resolveFormValidatorIssue(context, issue);
|
|
342
|
+
};
|
|
496
343
|
}
|
|
497
|
-
function sameAsField(field, otherField, message =
|
|
498
|
-
|
|
344
|
+
function sameAsField(field, otherField, message = "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u044F \u043D\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u044E\u0442", rule = "same-as-field") {
|
|
345
|
+
return createFormValidator(
|
|
346
|
+
({ values }) => readFormValue(values, field) === readFormValue(values, otherField),
|
|
347
|
+
{ field, rule, message }
|
|
348
|
+
);
|
|
499
349
|
}
|
|
500
|
-
function requiredIf(field, condition, message =
|
|
501
|
-
|
|
350
|
+
function requiredIf(field, condition, message = "\u041F\u043E\u043B\u0435 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E", rule = "conditional-required") {
|
|
351
|
+
return createFormValidator(
|
|
352
|
+
async (context) => !await condition(context) || !isEmptyFormValue(readFormValue(context.values, field)),
|
|
353
|
+
{ field, rule, message }
|
|
354
|
+
);
|
|
502
355
|
}
|
|
503
|
-
function dateOrder(startField, endField, message =
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
},
|
|
356
|
+
function dateOrder(startField, endField, message = "\u0414\u0430\u0442\u0430 \u043E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u044F \u0434\u043E\u043B\u0436\u043D\u0430 \u0431\u044B\u0442\u044C \u043F\u043E\u0437\u0436\u0435 \u0434\u0430\u0442\u044B \u043D\u0430\u0447\u0430\u043B\u0430", rule = "date-order") {
|
|
357
|
+
return createFormValidator(
|
|
358
|
+
({ values }) => {
|
|
359
|
+
const start = readFormValue(values, startField);
|
|
360
|
+
const end = readFormValue(values, endField);
|
|
361
|
+
if (isEmptyFormValue(start) || isEmptyFormValue(end)) return true;
|
|
362
|
+
return Date.parse(String(start)) <= Date.parse(String(end));
|
|
363
|
+
},
|
|
364
|
+
{ field: endField, rule, message }
|
|
365
|
+
);
|
|
511
366
|
}
|
|
512
367
|
let loaderIdCounter = 0;
|
|
513
368
|
let errorIdCounter = 0;
|
|
514
|
-
const FORM_INSTANCES = new WeakMap();
|
|
515
|
-
const ERROR_DESCRIBEDBY_ID_ATTRIBUTE =
|
|
516
|
-
const ADDED_DESCRIBEDBY_ID_ATTRIBUTE =
|
|
517
|
-
const ERROR_SUMMARY_ATTRIBUTE =
|
|
518
|
-
/** Изначальная дефолтная схема. */
|
|
369
|
+
const FORM_INSTANCES = /* @__PURE__ */ new WeakMap();
|
|
370
|
+
const ERROR_DESCRIBEDBY_ID_ATTRIBUTE = "data-form-father-describedby-id";
|
|
371
|
+
const ADDED_DESCRIBEDBY_ID_ATTRIBUTE = "data-form-father-added-describedby-id";
|
|
372
|
+
const ERROR_SUMMARY_ATTRIBUTE = "data-form-father-summary";
|
|
519
373
|
const INITIAL_DEFAULT_SCHEMA = {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
374
|
+
tel: {
|
|
375
|
+
selector: 'input[type="tel"]',
|
|
376
|
+
rules: ["tel"]
|
|
377
|
+
},
|
|
378
|
+
email: {
|
|
379
|
+
selector: 'input[type="email"]',
|
|
380
|
+
rules: ["email"]
|
|
381
|
+
},
|
|
382
|
+
required: {
|
|
383
|
+
selector: "[required]",
|
|
384
|
+
rules: ["required"]
|
|
385
|
+
},
|
|
386
|
+
url: {
|
|
387
|
+
selector: 'input[type="url"]',
|
|
388
|
+
rules: ["url"]
|
|
389
|
+
},
|
|
390
|
+
"not-numbers": {
|
|
391
|
+
selector: 'input[data-validate="not-numbers"]',
|
|
392
|
+
rules: ["not-numbers"]
|
|
393
|
+
}
|
|
540
394
|
};
|
|
541
|
-
/**
|
|
542
|
-
* Реализует форму отправки данных.
|
|
543
|
-
*
|
|
544
|
-
* @class
|
|
545
|
-
*/
|
|
546
395
|
class Form {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
ariaDescribeErrors: true,
|
|
603
|
-
errorIdPrefix: 'form-father-error',
|
|
604
|
-
errorSummary: false,
|
|
605
|
-
observeMutations: false,
|
|
606
|
-
};
|
|
607
|
-
/* Слияние параметров: глобальные параметры → пользовательские параметры */
|
|
608
|
-
this.config = Object.assign({}, defaultConfig, Form.defaultParams, options);
|
|
609
|
-
if (this.$el) {
|
|
610
|
-
this.$submits = this.findSubmitElements();
|
|
611
|
-
if (this.isCorrectArguments()) {
|
|
612
|
-
this.bindSubmitHandlers();
|
|
613
|
-
this.initialization();
|
|
614
|
-
this.bindFieldValidationEvents();
|
|
615
|
-
this.setupMutationObserver();
|
|
616
|
-
FORM_INSTANCES.set(this.$el, this);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
else {
|
|
620
|
-
console.warn('Empty $el');
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
getOptions() {
|
|
624
|
-
return this.options;
|
|
625
|
-
}
|
|
626
|
-
/**
|
|
627
|
-
* Инициализирует сразу несколько форм по CSS-селектору. Если форма уже инициализирована, её настройки обновятся.
|
|
628
|
-
*
|
|
629
|
-
* @param selector - CSS-селектор форм. По умолчанию `form[data-form-father]`.
|
|
630
|
-
* @param options - Общие параметры для найденных форм.
|
|
631
|
-
*/
|
|
632
|
-
static initAll(selector = 'form[data-form-father]', options = {}) {
|
|
633
|
-
/* istanbul ignore next -- SSR guard: jsdom does not allow document to be unset cleanly. */
|
|
634
|
-
if (typeof document === 'undefined')
|
|
635
|
-
return [];
|
|
636
|
-
return Array.from(document.querySelectorAll(selector))
|
|
637
|
-
.filter($form => $form.tagName === 'FORM')
|
|
638
|
-
.map($form => {
|
|
639
|
-
const existing = FORM_INSTANCES.get($form);
|
|
640
|
-
if (existing) {
|
|
641
|
-
existing.updateOptions(options);
|
|
642
|
-
return existing;
|
|
643
|
-
}
|
|
644
|
-
return new Form($form, options);
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
/** Обновляет настройки конкретного инстанса формы без пересоздания обработчиков вручную. */
|
|
648
|
-
updateOptions(options) {
|
|
649
|
-
this.options = Object.assign(Object.assign({}, this.options), options);
|
|
650
|
-
this.config = Object.assign(Object.assign({}, this.config), options);
|
|
651
|
-
this.refreshControls();
|
|
396
|
+
/**
|
|
397
|
+
* Создать форму.
|
|
398
|
+
*
|
|
399
|
+
* @param {HTMLElement} $el - Элемент формы (тег form!).
|
|
400
|
+
* @param {Object} options - Параметры формы.
|
|
401
|
+
*/
|
|
402
|
+
constructor($el, options = {}) {
|
|
403
|
+
this.$submits = [];
|
|
404
|
+
this.waitResponse = false;
|
|
405
|
+
this.inputs = null;
|
|
406
|
+
this.fieldEventHandlers = [];
|
|
407
|
+
this.liveValidationTimers = /* @__PURE__ */ new WeakMap();
|
|
408
|
+
this.fieldValidationTokens = /* @__PURE__ */ new WeakMap();
|
|
409
|
+
this.fieldValidationTokenCounter = 0;
|
|
410
|
+
this.errors = [];
|
|
411
|
+
/** Общие настройки анимаций (можно подправить под вкус) */
|
|
412
|
+
this._anim = {
|
|
413
|
+
duration: 220,
|
|
414
|
+
easing: "ease",
|
|
415
|
+
paddingTopOpened: 16,
|
|
416
|
+
iconDuration: 700
|
|
417
|
+
};
|
|
418
|
+
this.$el = $el;
|
|
419
|
+
this.options = options;
|
|
420
|
+
const defaultConfig = {
|
|
421
|
+
onSubmit: () => {
|
|
422
|
+
},
|
|
423
|
+
onResponse: () => {
|
|
424
|
+
},
|
|
425
|
+
onResponseSuccess: () => {
|
|
426
|
+
},
|
|
427
|
+
onResponseUnsuccess: () => {
|
|
428
|
+
},
|
|
429
|
+
customTypeError: null,
|
|
430
|
+
loaderColor: "#fff",
|
|
431
|
+
showLoaderButton: true,
|
|
432
|
+
scrollToFirstErroredInput: true,
|
|
433
|
+
focusFirstErroredInput: false,
|
|
434
|
+
logging: false,
|
|
435
|
+
inputSelector: ".input",
|
|
436
|
+
inputWrapperSelector: ".input-primary",
|
|
437
|
+
validateOn: "submit",
|
|
438
|
+
revalidateOn: ["input", "change"],
|
|
439
|
+
validationDebounce: 0,
|
|
440
|
+
errorContainerAttribute: "data-error-container",
|
|
441
|
+
validationStateAttribute: "data-form-father-state",
|
|
442
|
+
ariaDescribeErrors: true,
|
|
443
|
+
errorIdPrefix: "form-father-error",
|
|
444
|
+
errorSummary: false,
|
|
445
|
+
observeMutations: false
|
|
446
|
+
};
|
|
447
|
+
this.config = Object.assign({}, defaultConfig, Form.defaultParams, options);
|
|
448
|
+
if (this.$el) {
|
|
449
|
+
this.$submits = this.findSubmitElements();
|
|
450
|
+
if (this.isCorrectArguments()) {
|
|
652
451
|
this.bindSubmitHandlers();
|
|
452
|
+
this.initialization();
|
|
653
453
|
this.bindFieldValidationEvents();
|
|
654
454
|
this.setupMutationObserver();
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
message: issues['error-msg'],
|
|
734
|
-
source,
|
|
735
|
-
},
|
|
736
|
-
];
|
|
737
|
-
}
|
|
738
|
-
if (typeof issues === 'object' && 'message' in issues && typeof issues.message === 'string') {
|
|
739
|
-
return [Object.assign(Object.assign({}, issues), { source: issues.source || source })];
|
|
740
|
-
}
|
|
741
|
-
if (typeof issues === 'object') {
|
|
742
|
-
return Object.entries(issues).map(([field, message]) => ({
|
|
743
|
-
field,
|
|
744
|
-
rule: source,
|
|
745
|
-
message,
|
|
746
|
-
source,
|
|
747
|
-
}));
|
|
748
|
-
}
|
|
749
|
-
return [];
|
|
750
|
-
}
|
|
751
|
-
applyValidationIssue(issue, source) {
|
|
752
|
-
const issueSource = issue.source || source;
|
|
753
|
-
const rule = issue.rule || issueSource;
|
|
754
|
-
const field = issue.field || FORM_ERROR_FIELD;
|
|
755
|
-
if (field === FORM_ERROR_FIELD) {
|
|
756
|
-
this.setErrorRecord(FORM_ERROR_FIELD, rule, issue.message, issueSource);
|
|
757
|
-
this.showErrorForm(issue.message);
|
|
758
|
-
return null;
|
|
759
|
-
}
|
|
760
|
-
const $field = this.findFieldByName(field);
|
|
761
|
-
if (!$field) {
|
|
762
|
-
this.setErrorRecord(field, rule, issue.message, issueSource);
|
|
763
|
-
console.warn(`Не найдено поле с именем: ${field}, для вывода ошибки: ${issue.message}`);
|
|
764
|
-
return null;
|
|
765
|
-
}
|
|
766
|
-
this.showFieldError($field, issue.message, issueSource, rule);
|
|
767
|
-
return $field;
|
|
768
|
-
}
|
|
769
|
-
/** Показывает несколько ошибок формы или полей: из backend map/array, `ErrorResponse[]` или form validators. */
|
|
770
|
-
setErrors(issues, source = 'server') {
|
|
771
|
-
this.normalizeValidationIssues(issues, source).forEach(issue => {
|
|
772
|
-
this.applyValidationIssue(issue, source);
|
|
773
|
-
});
|
|
774
|
-
this.syncErrorSummary();
|
|
775
|
-
return this;
|
|
776
|
-
}
|
|
777
|
-
validateFormValidators() {
|
|
778
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
779
|
-
const validators = this.config.formValidators
|
|
780
|
-
? Array.isArray(this.config.formValidators)
|
|
781
|
-
? this.config.formValidators
|
|
782
|
-
: [this.config.formValidators]
|
|
783
|
-
: [];
|
|
784
|
-
const erroredInputs = [];
|
|
785
|
-
let ok = true;
|
|
786
|
-
for (const validator of validators) {
|
|
787
|
-
const result = yield validator({
|
|
788
|
-
form: this,
|
|
789
|
-
formElement: this.$el,
|
|
790
|
-
values: this.getValues(),
|
|
791
|
-
errors: this.getErrors(),
|
|
792
|
-
});
|
|
793
|
-
const issues = this.normalizeValidationIssues(result, 'client');
|
|
794
|
-
if (issues.length === 0)
|
|
795
|
-
continue;
|
|
796
|
-
ok = false;
|
|
797
|
-
issues.forEach(issue => {
|
|
798
|
-
const $field = this.applyValidationIssue(issue, 'client');
|
|
799
|
-
if ($field)
|
|
800
|
-
erroredInputs.push($field);
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
return { ok, erroredInputs };
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
/** Сбрасывает форму через native `reset()` и опционально очищает ошибки. */
|
|
807
|
-
reset(options = {}) {
|
|
808
|
-
this.$el.reset();
|
|
809
|
-
this.refreshControls();
|
|
810
|
-
if (options.clearErrors !== false) {
|
|
811
|
-
this.clearErrors();
|
|
812
|
-
}
|
|
813
|
-
return this;
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* Обновляет параметры по умолчанию для настроек формы. Метод объединяет переданные параметры с уже существующими
|
|
817
|
-
* параметрами по умолчанию.
|
|
818
|
-
*
|
|
819
|
-
* @param {Partial<FormOptions>} params - Объект, содержащий новые значения параметров. Можно передать только те
|
|
820
|
-
* свойства, которые необходимо обновить; остальные сохранятся без изменений.
|
|
821
|
-
*/
|
|
822
|
-
static setDefaultParams(params) {
|
|
823
|
-
this.defaultParams = Object.assign(Object.assign({}, this.defaultParams), params);
|
|
824
|
-
}
|
|
825
|
-
refreshControls() {
|
|
826
|
-
this.inputs = this.$el.querySelectorAll(this.config.inputSelector);
|
|
827
|
-
}
|
|
828
|
-
findSubmitElements() {
|
|
829
|
-
return Array.from(this.$el.querySelectorAll('input[type="submit"], input[type="image"], button[type="submit"], button:not([type])'));
|
|
830
|
-
}
|
|
831
|
-
bindSubmitHandlers() {
|
|
832
|
-
if (!this._onSubmitClickHandler) {
|
|
833
|
-
this._onSubmitClickHandler = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
834
|
-
yield this.handleSubmitEvent(e);
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
this.$submits.forEach($submit => {
|
|
838
|
-
$submit.removeEventListener('click', this._onSubmitClickHandler);
|
|
839
|
-
});
|
|
840
|
-
this.$submits = this.findSubmitElements();
|
|
841
|
-
this.$submits.forEach($submit => {
|
|
842
|
-
$submit.addEventListener('click', this._onSubmitClickHandler);
|
|
843
|
-
});
|
|
844
|
-
}
|
|
845
|
-
initialization() {
|
|
846
|
-
this.refreshControls();
|
|
847
|
-
this._onSubmitHandler = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
848
|
-
yield this.handleSubmitEvent(e);
|
|
849
|
-
});
|
|
850
|
-
this.$el.addEventListener('submit', this._onSubmitHandler);
|
|
851
|
-
}
|
|
852
|
-
handleSubmitEvent(e) {
|
|
853
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
854
|
-
var _a, _b, _c, _d, _e, _f;
|
|
855
|
-
e.preventDefault();
|
|
856
|
-
(_b = (_a = this.config).onBeforeValidate) === null || _b === void 0 ? void 0 : _b.call(_a, this);
|
|
857
|
-
const isValid = yield this.validate();
|
|
858
|
-
(_d = (_c = this.config).onAfterValidate) === null || _d === void 0 ? void 0 : _d.call(_c, isValid, this);
|
|
859
|
-
if (!isValid) {
|
|
860
|
-
(_f = (_e = this.config).onValidationError) === null || _f === void 0 ? void 0 : _f.call(_e, this.getErrors(), this);
|
|
861
|
-
return {
|
|
862
|
-
success: false,
|
|
863
|
-
error: this.getErrors(),
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
return this.submit();
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
normalizeTriggers(value, fallback) {
|
|
870
|
-
const triggers = value == null ? fallback : Array.isArray(value) ? value : [value];
|
|
871
|
-
return Array.from(new Set(triggers));
|
|
872
|
-
}
|
|
873
|
-
clearFieldValidationEvents() {
|
|
874
|
-
this.fieldEventHandlers.forEach(({ $input, event, handler }) => {
|
|
875
|
-
$input.removeEventListener(event, handler);
|
|
876
|
-
});
|
|
877
|
-
this.fieldEventHandlers = [];
|
|
878
|
-
}
|
|
879
|
-
bindFieldValidationEvents() {
|
|
880
|
-
this.clearFieldValidationEvents();
|
|
881
|
-
const validateOn = this.normalizeTriggers(this.config.validateOn, ['submit']).filter((event) => event !== 'submit');
|
|
882
|
-
const revalidateOn = this.normalizeTriggers(this.config.revalidateOn, ['input', 'change']).filter((event) => event !== 'submit');
|
|
883
|
-
const events = Array.from(new Set([...validateOn, ...revalidateOn]));
|
|
884
|
-
if (events.length === 0)
|
|
885
|
-
return;
|
|
886
|
-
const fields = this.getAllFields();
|
|
887
|
-
fields.forEach($input => {
|
|
888
|
-
events.forEach(event => {
|
|
889
|
-
const handler = () => {
|
|
890
|
-
const shouldValidate = validateOn.includes(event) || this.hasFieldError($input);
|
|
891
|
-
if (shouldValidate)
|
|
892
|
-
this.scheduleFieldValidation($input);
|
|
893
|
-
};
|
|
894
|
-
$input.addEventListener(event, handler);
|
|
895
|
-
this.fieldEventHandlers.push({ $input, event, handler });
|
|
896
|
-
});
|
|
455
|
+
FORM_INSTANCES.set(this.$el, this);
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
console.warn("Empty $el");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
static get defaultParams() {
|
|
462
|
+
return __shared.defaultParams;
|
|
463
|
+
}
|
|
464
|
+
static set defaultParams(v) {
|
|
465
|
+
__shared.defaultParams = v;
|
|
466
|
+
}
|
|
467
|
+
static get defaultValidationSchema() {
|
|
468
|
+
var _a2;
|
|
469
|
+
return (_a2 = __shared.defaultValidationSchema) != null ? _a2 : __shared.defaultValidationSchema = INITIAL_DEFAULT_SCHEMA;
|
|
470
|
+
}
|
|
471
|
+
static set defaultValidationSchema(v) {
|
|
472
|
+
__shared.defaultValidationSchema = v;
|
|
473
|
+
}
|
|
474
|
+
getOptions() {
|
|
475
|
+
return this.options;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Инициализирует сразу несколько форм по CSS-селектору. Если форма уже инициализирована, её настройки обновятся.
|
|
479
|
+
*
|
|
480
|
+
* @param selector - CSS-селектор форм. По умолчанию `form[data-form-father]`.
|
|
481
|
+
* @param options - Общие параметры для найденных форм.
|
|
482
|
+
*/
|
|
483
|
+
static initAll(selector = "form[data-form-father]", options = {}) {
|
|
484
|
+
if (typeof document === "undefined") return [];
|
|
485
|
+
return Array.from(document.querySelectorAll(selector)).filter(($form) => $form.tagName === "FORM").map(($form) => {
|
|
486
|
+
const existing = FORM_INSTANCES.get($form);
|
|
487
|
+
if (existing) {
|
|
488
|
+
existing.updateOptions(options);
|
|
489
|
+
return existing;
|
|
490
|
+
}
|
|
491
|
+
return new Form($form, options);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/** Обновляет настройки конкретного инстанса формы без пересоздания обработчиков вручную. */
|
|
495
|
+
updateOptions(options) {
|
|
496
|
+
this.options = { ...this.options, ...options };
|
|
497
|
+
this.config = { ...this.config, ...options };
|
|
498
|
+
this.refreshControls();
|
|
499
|
+
this.bindSubmitHandlers();
|
|
500
|
+
this.bindFieldValidationEvents();
|
|
501
|
+
this.setupMutationObserver();
|
|
502
|
+
return this;
|
|
503
|
+
}
|
|
504
|
+
/** Возвращает текущий список ошибок формы. */
|
|
505
|
+
getErrors() {
|
|
506
|
+
return this.errors.map((error) => ({ ...error }));
|
|
507
|
+
}
|
|
508
|
+
/** Возвращает значения формы обычным объектом. */
|
|
509
|
+
getValues() {
|
|
510
|
+
return serializeFormToJSON(this.$el);
|
|
511
|
+
}
|
|
512
|
+
normalizeValueList(value) {
|
|
513
|
+
return Array.isArray(value) ? value.map(String) : [String(value)];
|
|
514
|
+
}
|
|
515
|
+
/** Проставляет значения полей по `name` и уведомляет UI через `input`/`change` события. */
|
|
516
|
+
setValues(values) {
|
|
517
|
+
this.getAllFields().forEach(($field) => {
|
|
518
|
+
if (!$field.name || !Object.prototype.hasOwnProperty.call(values, $field.name)) return;
|
|
519
|
+
const value = values[$field.name];
|
|
520
|
+
if ($field instanceof HTMLInputElement && $field.type === "file") return;
|
|
521
|
+
if ($field instanceof HTMLInputElement && $field.type === "checkbox") {
|
|
522
|
+
if (typeof value === "boolean") {
|
|
523
|
+
$field.checked = value;
|
|
524
|
+
} else {
|
|
525
|
+
$field.checked = this.normalizeValueList(value).includes($field.value);
|
|
526
|
+
}
|
|
527
|
+
} else if ($field instanceof HTMLInputElement && $field.type === "radio") {
|
|
528
|
+
$field.checked = String(value) === $field.value;
|
|
529
|
+
} else if ($field instanceof HTMLSelectElement && $field.multiple) {
|
|
530
|
+
const valuesList = this.normalizeValueList(value);
|
|
531
|
+
Array.from($field.options).forEach((option) => {
|
|
532
|
+
option.selected = valuesList.includes(option.value);
|
|
897
533
|
});
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
534
|
+
} else {
|
|
535
|
+
$field.value = value == null ? "" : String(value);
|
|
536
|
+
}
|
|
537
|
+
$field.dispatchEvent(new Event("input", { bubbles: true }));
|
|
538
|
+
$field.dispatchEvent(new Event("change", { bubbles: true }));
|
|
539
|
+
});
|
|
540
|
+
return this;
|
|
541
|
+
}
|
|
542
|
+
/** Снимает все клиентские и серверные ошибки с формы, не меняя значения полей. */
|
|
543
|
+
clearErrors() {
|
|
544
|
+
this.errors = [];
|
|
545
|
+
this.getAllFields().forEach(($field) => {
|
|
546
|
+
this.hideError($field);
|
|
547
|
+
this.clearFieldValidationState($field);
|
|
548
|
+
});
|
|
549
|
+
this.hideErrorForm();
|
|
550
|
+
this.hideErrorSummary();
|
|
551
|
+
return this;
|
|
552
|
+
}
|
|
553
|
+
isErrorResponse(issue) {
|
|
554
|
+
return !!issue && typeof issue === "object" && typeof issue.name === "string" && typeof issue["error-msg"] === "string";
|
|
555
|
+
}
|
|
556
|
+
normalizeValidationIssues(issues, source) {
|
|
557
|
+
if (issues == null || issues === true) return [];
|
|
558
|
+
if (issues === false) return [{ message: "\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u0444\u043E\u0440\u043C\u044B", source }];
|
|
559
|
+
if (typeof issues === "string") return [{ message: issues, source }];
|
|
560
|
+
if (Array.isArray(issues)) {
|
|
561
|
+
return issues.flatMap((issue) => this.normalizeValidationIssues(issue, source));
|
|
562
|
+
}
|
|
563
|
+
if (this.isErrorResponse(issues)) {
|
|
564
|
+
return [
|
|
565
|
+
{
|
|
566
|
+
field: issues.name,
|
|
567
|
+
rule: source,
|
|
568
|
+
message: issues["error-msg"],
|
|
569
|
+
source
|
|
570
|
+
}
|
|
571
|
+
];
|
|
572
|
+
}
|
|
573
|
+
if (typeof issues === "object" && "message" in issues && typeof issues.message === "string") {
|
|
574
|
+
return [{ ...issues, source: issues.source || source }];
|
|
575
|
+
}
|
|
576
|
+
if (typeof issues === "object") {
|
|
577
|
+
return Object.entries(issues).map(([field, message]) => ({
|
|
578
|
+
field,
|
|
579
|
+
rule: source,
|
|
580
|
+
message,
|
|
581
|
+
source
|
|
582
|
+
}));
|
|
583
|
+
}
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
applyValidationIssue(issue, source) {
|
|
587
|
+
const issueSource = issue.source || source;
|
|
588
|
+
const rule = issue.rule || issueSource;
|
|
589
|
+
const field = issue.field || FORM_ERROR_FIELD;
|
|
590
|
+
if (field === FORM_ERROR_FIELD) {
|
|
591
|
+
this.setErrorRecord(FORM_ERROR_FIELD, rule, issue.message, issueSource);
|
|
592
|
+
this.showErrorForm(issue.message);
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
const $field = this.findFieldByName(field);
|
|
596
|
+
if (!$field) {
|
|
597
|
+
this.setErrorRecord(field, rule, issue.message, issueSource);
|
|
598
|
+
console.warn(`\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u043F\u043E\u043B\u0435 \u0441 \u0438\u043C\u0435\u043D\u0435\u043C: ${field}, \u0434\u043B\u044F \u0432\u044B\u0432\u043E\u0434\u0430 \u043E\u0448\u0438\u0431\u043A\u0438: ${issue.message}`);
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
this.showFieldError($field, issue.message, issueSource, rule);
|
|
602
|
+
return $field;
|
|
603
|
+
}
|
|
604
|
+
/** Показывает несколько ошибок формы или полей: из backend map/array, `ErrorResponse[]` или form validators. */
|
|
605
|
+
setErrors(issues, source = "server") {
|
|
606
|
+
this.normalizeValidationIssues(issues, source).forEach((issue) => {
|
|
607
|
+
this.applyValidationIssue(issue, source);
|
|
608
|
+
});
|
|
609
|
+
this.syncErrorSummary();
|
|
610
|
+
return this;
|
|
611
|
+
}
|
|
612
|
+
async validateFormValidators() {
|
|
613
|
+
const validators = this.config.formValidators ? Array.isArray(this.config.formValidators) ? this.config.formValidators : [this.config.formValidators] : [];
|
|
614
|
+
const erroredInputs = [];
|
|
615
|
+
let ok = true;
|
|
616
|
+
for (const validator of validators) {
|
|
617
|
+
const result = await validator({
|
|
618
|
+
form: this,
|
|
619
|
+
formElement: this.$el,
|
|
620
|
+
values: this.getValues(),
|
|
621
|
+
errors: this.getErrors()
|
|
622
|
+
});
|
|
623
|
+
const issues = this.normalizeValidationIssues(result, "client");
|
|
624
|
+
if (issues.length === 0) continue;
|
|
625
|
+
ok = false;
|
|
626
|
+
issues.forEach((issue) => {
|
|
627
|
+
const $field = this.applyValidationIssue(issue, "client");
|
|
628
|
+
if ($field) erroredInputs.push($field);
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return { ok, erroredInputs };
|
|
632
|
+
}
|
|
633
|
+
/** Сбрасывает форму через native `reset()` и опционально очищает ошибки. */
|
|
634
|
+
reset(options = {}) {
|
|
635
|
+
this.$el.reset();
|
|
636
|
+
this.refreshControls();
|
|
637
|
+
if (options.clearErrors !== false) {
|
|
638
|
+
this.clearErrors();
|
|
639
|
+
}
|
|
640
|
+
return this;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Обновляет параметры по умолчанию для настроек формы. Метод объединяет переданные параметры с уже существующими
|
|
644
|
+
* параметрами по умолчанию.
|
|
645
|
+
*
|
|
646
|
+
* @param {Partial<FormOptions>} params - Объект, содержащий новые значения параметров. Можно передать только те
|
|
647
|
+
* свойства, которые необходимо обновить; остальные сохранятся без изменений.
|
|
648
|
+
*/
|
|
649
|
+
static setDefaultParams(params) {
|
|
650
|
+
this.defaultParams = { ...this.defaultParams, ...params };
|
|
651
|
+
}
|
|
652
|
+
refreshControls() {
|
|
653
|
+
this.inputs = this.$el.querySelectorAll(this.config.inputSelector);
|
|
654
|
+
}
|
|
655
|
+
findSubmitElements() {
|
|
656
|
+
return Array.from(
|
|
657
|
+
this.$el.querySelectorAll('input[type="submit"], input[type="image"], button[type="submit"], button:not([type])')
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
bindSubmitHandlers() {
|
|
661
|
+
if (!this._onSubmitClickHandler) {
|
|
662
|
+
this._onSubmitClickHandler = async (e) => {
|
|
663
|
+
await this.handleSubmitEvent(e);
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
this.$submits.forEach(($submit) => {
|
|
667
|
+
$submit.removeEventListener("click", this._onSubmitClickHandler);
|
|
668
|
+
});
|
|
669
|
+
this.$submits = this.findSubmitElements();
|
|
670
|
+
this.$submits.forEach(($submit) => {
|
|
671
|
+
$submit.addEventListener("click", this._onSubmitClickHandler);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
initialization() {
|
|
675
|
+
this.refreshControls();
|
|
676
|
+
this._onSubmitHandler = async (e) => {
|
|
677
|
+
await this.handleSubmitEvent(e);
|
|
678
|
+
};
|
|
679
|
+
this.$el.addEventListener("submit", this._onSubmitHandler);
|
|
680
|
+
}
|
|
681
|
+
async handleSubmitEvent(e) {
|
|
682
|
+
var _a2, _b, _c, _d, _e, _f;
|
|
683
|
+
e.preventDefault();
|
|
684
|
+
(_b = (_a2 = this.config).onBeforeValidate) == null ? void 0 : _b.call(_a2, this);
|
|
685
|
+
const isValid = await this.validate();
|
|
686
|
+
(_d = (_c = this.config).onAfterValidate) == null ? void 0 : _d.call(_c, isValid, this);
|
|
687
|
+
if (!isValid) {
|
|
688
|
+
(_f = (_e = this.config).onValidationError) == null ? void 0 : _f.call(_e, this.getErrors(), this);
|
|
689
|
+
return {
|
|
690
|
+
success: false,
|
|
691
|
+
error: this.getErrors()
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
return this.submit();
|
|
695
|
+
}
|
|
696
|
+
normalizeTriggers(value, fallback) {
|
|
697
|
+
const triggers = value == null ? fallback : Array.isArray(value) ? value : [value];
|
|
698
|
+
return Array.from(new Set(triggers));
|
|
699
|
+
}
|
|
700
|
+
clearFieldValidationEvents() {
|
|
701
|
+
this.fieldEventHandlers.forEach(({ $input, event, handler }) => {
|
|
702
|
+
$input.removeEventListener(event, handler);
|
|
703
|
+
});
|
|
704
|
+
this.fieldEventHandlers = [];
|
|
705
|
+
}
|
|
706
|
+
bindFieldValidationEvents() {
|
|
707
|
+
this.clearFieldValidationEvents();
|
|
708
|
+
const validateOn = this.normalizeTriggers(this.config.validateOn, ["submit"]).filter(
|
|
709
|
+
(event) => event !== "submit"
|
|
710
|
+
);
|
|
711
|
+
const revalidateOn = this.normalizeTriggers(this.config.revalidateOn, ["input", "change"]).filter(
|
|
712
|
+
(event) => event !== "submit"
|
|
713
|
+
);
|
|
714
|
+
const events = Array.from(/* @__PURE__ */ new Set([...validateOn, ...revalidateOn]));
|
|
715
|
+
if (events.length === 0) return;
|
|
716
|
+
const fields = this.getAllFields();
|
|
717
|
+
fields.forEach(($input) => {
|
|
718
|
+
events.forEach((event) => {
|
|
719
|
+
const handler = () => {
|
|
720
|
+
const shouldValidate = validateOn.includes(event) || this.hasFieldError($input);
|
|
721
|
+
if (shouldValidate) this.scheduleFieldValidation($input);
|
|
906
722
|
};
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
const gradientId = `form-father-loader-${loaderIdCounter++}`;
|
|
1035
|
-
const loaderSvg = `
|
|
723
|
+
$input.addEventListener(event, handler);
|
|
724
|
+
this.fieldEventHandlers.push({ $input, event, handler });
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
scheduleFieldValidation($input) {
|
|
729
|
+
const currentTimer = this.liveValidationTimers.get($input);
|
|
730
|
+
if (currentTimer) clearTimeout(currentTimer);
|
|
731
|
+
const delay = Math.max(0, Number(this.config.validationDebounce) || 0);
|
|
732
|
+
const run = () => {
|
|
733
|
+
void this.validateField($input);
|
|
734
|
+
};
|
|
735
|
+
if (delay === 0) {
|
|
736
|
+
run();
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const timer = setTimeout(run, delay);
|
|
740
|
+
this.liveValidationTimers.set($input, timer);
|
|
741
|
+
}
|
|
742
|
+
setupMutationObserver() {
|
|
743
|
+
var _a2;
|
|
744
|
+
(_a2 = this.mutationObserver) == null ? void 0 : _a2.disconnect();
|
|
745
|
+
this.mutationObserver = void 0;
|
|
746
|
+
if (!this.config.observeMutations || typeof MutationObserver === "undefined") return;
|
|
747
|
+
this.mutationObserver = new MutationObserver(() => {
|
|
748
|
+
this.refreshControls();
|
|
749
|
+
this.bindSubmitHandlers();
|
|
750
|
+
this.bindFieldValidationEvents();
|
|
751
|
+
});
|
|
752
|
+
this.mutationObserver.observe(this.$el, {
|
|
753
|
+
childList: true,
|
|
754
|
+
subtree: true
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
getErrorContainer($input) {
|
|
758
|
+
const attrName = this.config.errorContainerAttribute || "data-error-container";
|
|
759
|
+
const selector = $input.getAttribute(attrName);
|
|
760
|
+
if (!selector) return null;
|
|
761
|
+
try {
|
|
762
|
+
return document.querySelector(selector);
|
|
763
|
+
} catch (e) {
|
|
764
|
+
console.warn(`[FormFather] Invalid error container selector "${selector}"`);
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
toSafeIdPart(value) {
|
|
769
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/gi, "-").replace(/^-+|-+$/g, "") || "field";
|
|
770
|
+
}
|
|
771
|
+
createErrorId($input) {
|
|
772
|
+
const prefix = this.config.errorIdPrefix || "form-father-error";
|
|
773
|
+
return `${this.toSafeIdPart(prefix)}-${this.toSafeIdPart(this.getFieldKey($input))}-${++errorIdCounter}`;
|
|
774
|
+
}
|
|
775
|
+
ensureElementId($input, $error) {
|
|
776
|
+
if (!$error.id) {
|
|
777
|
+
$error.id = $input.getAttribute(ERROR_DESCRIBEDBY_ID_ATTRIBUTE) || this.createErrorId($input);
|
|
778
|
+
}
|
|
779
|
+
return $error.id;
|
|
780
|
+
}
|
|
781
|
+
addErrorDescription($input, $error) {
|
|
782
|
+
if (!$error.hasAttribute("role")) $error.setAttribute("role", "alert");
|
|
783
|
+
const errorId = this.ensureElementId($input, $error);
|
|
784
|
+
$input.setAttribute(ERROR_DESCRIBEDBY_ID_ATTRIBUTE, errorId);
|
|
785
|
+
if (this.config.ariaDescribeErrors === false) return;
|
|
786
|
+
const currentIds = ($input.getAttribute("aria-describedby") || "").split(/\s+/).filter(Boolean);
|
|
787
|
+
if (!currentIds.includes(errorId)) {
|
|
788
|
+
$input.setAttribute("aria-describedby", [...currentIds, errorId].join(" "));
|
|
789
|
+
$input.setAttribute(ADDED_DESCRIBEDBY_ID_ATTRIBUTE, errorId);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
removeErrorDescription($input) {
|
|
793
|
+
const addedId = $input.getAttribute(ADDED_DESCRIBEDBY_ID_ATTRIBUTE);
|
|
794
|
+
if (addedId) {
|
|
795
|
+
const nextIds = ($input.getAttribute("aria-describedby") || "").split(/\s+/).filter((id) => id && id !== addedId);
|
|
796
|
+
if (nextIds.length > 0) {
|
|
797
|
+
$input.setAttribute("aria-describedby", nextIds.join(" "));
|
|
798
|
+
} else {
|
|
799
|
+
$input.removeAttribute("aria-describedby");
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
$input.removeAttribute(ERROR_DESCRIBEDBY_ID_ATTRIBUTE);
|
|
803
|
+
$input.removeAttribute(ADDED_DESCRIBEDBY_ID_ATTRIBUTE);
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Показывает ошибку для поля ввода.
|
|
807
|
+
*
|
|
808
|
+
* @param {HTMLInputElement} $input - Поле ввода.
|
|
809
|
+
* @param {String} text - Текст ошибки.
|
|
810
|
+
*/
|
|
811
|
+
showError($input, text) {
|
|
812
|
+
$input.setAttribute("aria-invalid", "true");
|
|
813
|
+
const $errorContainer = this.getErrorContainer($input);
|
|
814
|
+
if ($errorContainer) {
|
|
815
|
+
$errorContainer.textContent = text;
|
|
816
|
+
this.addErrorDescription($input, $errorContainer);
|
|
817
|
+
$errorContainer.removeAttribute("hidden");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const $inputWrapper = closest($input, this.config.inputWrapperSelector);
|
|
821
|
+
if (!$inputWrapper) {
|
|
822
|
+
console.warn(`\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u043E\u0431\u0451\u0440\u0442\u043A\u0430 \u0434\u043B\u044F \u043F\u043E\u043B\u044F \u0441 \u0438\u043C\u0435\u043D\u0435\u043C: ${$input.name || $input.type}`);
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const customShowError = $inputWrapper.showError;
|
|
826
|
+
if (typeof customShowError === "function") {
|
|
827
|
+
customShowError.call($inputWrapper, text);
|
|
828
|
+
const $customError = $inputWrapper.querySelector("[data-form-father-error]");
|
|
829
|
+
if ($customError) this.addErrorDescription($input, $customError);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
$inputWrapper.classList.add("input__wrapper--error");
|
|
833
|
+
let $error = $inputWrapper.querySelector("[data-form-father-error]");
|
|
834
|
+
if (!$error) {
|
|
835
|
+
$error = document.createElement("div");
|
|
836
|
+
$error.setAttribute("data-form-father-error", "");
|
|
837
|
+
$error.setAttribute("role", "alert");
|
|
838
|
+
$input.insertAdjacentElement("afterend", $error);
|
|
839
|
+
}
|
|
840
|
+
$error.textContent = text;
|
|
841
|
+
this.addErrorDescription($input, $error);
|
|
842
|
+
}
|
|
843
|
+
/** Показывает лоадер */
|
|
844
|
+
showLoader() {
|
|
845
|
+
this.$submits.forEach(($submit) => {
|
|
846
|
+
$submit.classList.add("button--loading");
|
|
847
|
+
if ($submit.querySelector(".button__loader")) return;
|
|
848
|
+
const gradientId = `form-father-loader-${loaderIdCounter++}`;
|
|
849
|
+
const loaderSvg = `
|
|
1036
850
|
<svg height="38" viewBox="0 0 38 38" width="38" xmlns="http://www.w3.org/2000/svg">
|
|
1037
851
|
<defs>
|
|
1038
852
|
<linearGradient id="${gradientId}" x1="8.042%" x2="65.682%" y1="0%" y2="23.865%">
|
|
@@ -1049,596 +863,552 @@ class Form {
|
|
|
1049
863
|
</g>
|
|
1050
864
|
</svg>
|
|
1051
865
|
`;
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
}
|
|
1128
|
-
focusErrorSummary($summary) {
|
|
1129
|
-
try {
|
|
1130
|
-
$summary.focus({ preventScroll: true });
|
|
1131
|
-
}
|
|
1132
|
-
catch (_a) {
|
|
1133
|
-
$summary.focus();
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
focusFieldFromSummary($field) {
|
|
1137
|
-
if (typeof $field.scrollIntoView === 'function') {
|
|
1138
|
-
$field.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1139
|
-
}
|
|
1140
|
-
try {
|
|
1141
|
-
$field.focus({ preventScroll: true });
|
|
1142
|
-
}
|
|
1143
|
-
catch (_a) {
|
|
1144
|
-
$field.focus();
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
renderDefaultErrorSummary($summary, errors, options) {
|
|
1148
|
-
$summary.textContent = '';
|
|
1149
|
-
const $title = document.createElement('p');
|
|
1150
|
-
$title.setAttribute('data-form-father-summary-title', '');
|
|
1151
|
-
$title.textContent = options.title || 'Проверьте поля формы';
|
|
1152
|
-
const $list = document.createElement('ul');
|
|
1153
|
-
$list.setAttribute('data-form-father-summary-list', '');
|
|
1154
|
-
errors.forEach(error => {
|
|
1155
|
-
const $item = document.createElement('li');
|
|
1156
|
-
const $field = error.field === FORM_ERROR_FIELD ? null : this.findFieldByName(error.field);
|
|
1157
|
-
if ($field) {
|
|
1158
|
-
const $button = document.createElement('button');
|
|
1159
|
-
$button.type = 'button';
|
|
1160
|
-
$button.setAttribute('data-form-father-summary-field', error.field);
|
|
1161
|
-
$button.textContent = error.message;
|
|
1162
|
-
$button.addEventListener('click', () => this.focusFieldFromSummary($field));
|
|
1163
|
-
$item.appendChild($button);
|
|
1164
|
-
}
|
|
1165
|
-
else {
|
|
1166
|
-
const $message = document.createElement('span');
|
|
1167
|
-
$message.textContent = error.message;
|
|
1168
|
-
$item.appendChild($message);
|
|
1169
|
-
}
|
|
1170
|
-
$list.appendChild($item);
|
|
1171
|
-
});
|
|
1172
|
-
$summary.append($title, $list);
|
|
1173
|
-
}
|
|
1174
|
-
renderErrorSummary(errors = this.getErrors()) {
|
|
1175
|
-
var _a;
|
|
1176
|
-
const options = this.getErrorSummaryOptions();
|
|
1177
|
-
if (!options)
|
|
1178
|
-
return;
|
|
1179
|
-
if (errors.length === 0) {
|
|
1180
|
-
this.hideErrorSummary();
|
|
1181
|
-
return;
|
|
1182
|
-
}
|
|
1183
|
-
const $summary = this.getErrorSummaryContainer(options, true);
|
|
1184
|
-
if (!$summary)
|
|
1185
|
-
return;
|
|
1186
|
-
this.prepareErrorSummaryContainer($summary);
|
|
1187
|
-
const rendered = (_a = options.render) === null || _a === void 0 ? void 0 : _a.call(options, errors.map(error => (Object.assign({}, error))), this);
|
|
1188
|
-
if (rendered instanceof HTMLElement) {
|
|
1189
|
-
$summary.replaceChildren(rendered);
|
|
1190
|
-
}
|
|
1191
|
-
else if (typeof rendered === 'string') {
|
|
1192
|
-
$summary.innerHTML = rendered;
|
|
1193
|
-
}
|
|
1194
|
-
else if (!options.render) {
|
|
1195
|
-
this.renderDefaultErrorSummary($summary, errors, options);
|
|
1196
|
-
}
|
|
1197
|
-
if (options.focus)
|
|
1198
|
-
this.focusErrorSummary($summary);
|
|
1199
|
-
}
|
|
1200
|
-
hideErrorSummary() {
|
|
1201
|
-
const options = this.getErrorSummaryOptions() || {};
|
|
1202
|
-
const $summary = this.getErrorSummaryContainer(options, false);
|
|
1203
|
-
if (!$summary)
|
|
1204
|
-
return;
|
|
1205
|
-
$summary.replaceChildren();
|
|
1206
|
-
$summary.setAttribute('hidden', '');
|
|
1207
|
-
}
|
|
1208
|
-
syncErrorSummary() {
|
|
1209
|
-
if (this.errors.length === 0) {
|
|
1210
|
-
this.hideErrorSummary();
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
this.renderErrorSummary();
|
|
1214
|
-
}
|
|
1215
|
-
/**
|
|
1216
|
-
* Проскроливает до первого ошибочного поля.
|
|
1217
|
-
*
|
|
1218
|
-
* @param {HTMLElement[]} inputsList - Массив элементов с ошибкой.
|
|
1219
|
-
*/
|
|
1220
|
-
scrollToFirstErroredInput(inputsList) {
|
|
1221
|
-
if (inputsList.length > 0) {
|
|
1222
|
-
const allInputInFormList = Array.from(this.$el.querySelectorAll('input, textarea, select'));
|
|
1223
|
-
let $firstInputOnForm = null;
|
|
1224
|
-
let minIndex = Infinity;
|
|
1225
|
-
inputsList.forEach($input => {
|
|
1226
|
-
const findIndex = allInputInFormList.findIndex($item => $item === $input);
|
|
1227
|
-
if (findIndex < minIndex) {
|
|
1228
|
-
minIndex = findIndex;
|
|
1229
|
-
$firstInputOnForm = $input;
|
|
1230
|
-
}
|
|
1231
|
-
});
|
|
1232
|
-
if ($firstInputOnForm) {
|
|
1233
|
-
const rect = $firstInputOnForm.getBoundingClientRect();
|
|
1234
|
-
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
1235
|
-
const scrollTargetY = rect.top + scrollTop - window.innerHeight / 2;
|
|
1236
|
-
document.documentElement.setAttribute('data-now-scrolling', '');
|
|
1237
|
-
window.scrollTo({
|
|
1238
|
-
top: scrollTargetY,
|
|
1239
|
-
behavior: 'smooth',
|
|
1240
|
-
});
|
|
1241
|
-
setTimeout(() => {
|
|
1242
|
-
document.documentElement.removeAttribute('data-now-scrolling');
|
|
1243
|
-
}, 800);
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
focusFirstErroredInput(inputsList) {
|
|
1248
|
-
var _a;
|
|
1249
|
-
if (inputsList.length === 0)
|
|
1250
|
-
return;
|
|
1251
|
-
const allInputInFormList = Array.from(this.$el.querySelectorAll('input, textarea, select'));
|
|
1252
|
-
const $firstInputOnForm = inputsList
|
|
1253
|
-
.slice()
|
|
1254
|
-
.sort((a, b) => allInputInFormList.indexOf(a) - allInputInFormList.indexOf(b))[0];
|
|
1255
|
-
(_a = $firstInputOnForm === null || $firstInputOnForm === void 0 ? void 0 : $firstInputOnForm.focus) === null || _a === void 0 ? void 0 : _a.call($firstInputOnForm, { preventScroll: true });
|
|
1256
|
-
}
|
|
1257
|
-
getValidationSchema() {
|
|
1258
|
-
return Object.assign(Object.assign({}, (Form.defaultValidationSchema || {})), (this.config.validationSchema || {}));
|
|
1259
|
-
}
|
|
1260
|
-
getAllFields($block = this.$el) {
|
|
1261
|
-
return Array.from($block.querySelectorAll('input, textarea, select'));
|
|
1262
|
-
}
|
|
1263
|
-
getFieldKey($input) {
|
|
1264
|
-
return $input.name || $input.id || $input.getAttribute('data-field') || $input.type || 'field';
|
|
1265
|
-
}
|
|
1266
|
-
hasFieldError($input) {
|
|
1267
|
-
const field = this.getFieldKey($input);
|
|
1268
|
-
return $input.getAttribute('aria-invalid') === 'true' || this.errors.some(error => error.field === field);
|
|
1269
|
-
}
|
|
1270
|
-
clearFieldErrorRecord($input) {
|
|
1271
|
-
const field = this.getFieldKey($input);
|
|
1272
|
-
this.errors = this.errors.filter(error => error.field !== field);
|
|
1273
|
-
}
|
|
1274
|
-
setErrorRecord(field, rule, message, source) {
|
|
1275
|
-
this.errors.push({
|
|
1276
|
-
field,
|
|
1277
|
-
rule,
|
|
1278
|
-
message,
|
|
1279
|
-
source,
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
1282
|
-
getValidationStateAttribute() {
|
|
1283
|
-
return this.config.validationStateAttribute || 'data-form-father-state';
|
|
1284
|
-
}
|
|
1285
|
-
setFieldValidationState($input, state) {
|
|
1286
|
-
const attr = this.getValidationStateAttribute();
|
|
1287
|
-
if (state) {
|
|
1288
|
-
$input.setAttribute(attr, state);
|
|
1289
|
-
}
|
|
1290
|
-
else {
|
|
1291
|
-
$input.removeAttribute(attr);
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
startFieldValidation($input) {
|
|
1295
|
-
const token = ++this.fieldValidationTokenCounter;
|
|
1296
|
-
this.fieldValidationTokens.set($input, token);
|
|
1297
|
-
$input.setAttribute('aria-busy', 'true');
|
|
1298
|
-
this.setFieldValidationState($input, 'validating');
|
|
1299
|
-
return token;
|
|
1300
|
-
}
|
|
1301
|
-
isLatestFieldValidation($input, token) {
|
|
1302
|
-
return token == null || this.fieldValidationTokens.get($input) === token;
|
|
1303
|
-
}
|
|
1304
|
-
finishFieldValidation($input, token, isValid) {
|
|
1305
|
-
if (!this.isLatestFieldValidation($input, token))
|
|
1306
|
-
return;
|
|
1307
|
-
$input.removeAttribute('aria-busy');
|
|
1308
|
-
this.setFieldValidationState($input, isValid ? 'valid' : 'invalid');
|
|
1309
|
-
}
|
|
1310
|
-
clearFieldValidationState($input) {
|
|
1311
|
-
this.fieldValidationTokens.delete($input);
|
|
1312
|
-
$input.removeAttribute('aria-busy');
|
|
1313
|
-
this.setFieldValidationState($input, null);
|
|
1314
|
-
}
|
|
1315
|
-
setValidationError($input, rule, message, source) {
|
|
1316
|
-
this.clearFieldErrorRecord($input);
|
|
1317
|
-
this.setErrorRecord(this.getFieldKey($input), rule, message, source);
|
|
1318
|
-
}
|
|
1319
|
-
resolveField(field) {
|
|
1320
|
-
if (typeof field !== 'string')
|
|
1321
|
-
return field;
|
|
1322
|
-
return this.findFieldByName(field);
|
|
1323
|
-
}
|
|
1324
|
-
parseRuleString(ruleString) {
|
|
1325
|
-
const [rule, ...params] = ruleString.split(':').map(s => s.trim());
|
|
1326
|
-
return params.length > 0 ? { rule, params: params.length === 1 ? params[0] : params } : rule;
|
|
1327
|
-
}
|
|
1328
|
-
parseValidationAttribute(value) {
|
|
1329
|
-
return (value !== null && value !== void 0 ? value : '')
|
|
1330
|
-
.split(/[,\s|]+/)
|
|
1331
|
-
.map(s => s.trim())
|
|
1332
|
-
.filter(Boolean)
|
|
1333
|
-
.map(ruleString => this.parseRuleString(ruleString));
|
|
1334
|
-
}
|
|
1335
|
-
getRuleName(rule) {
|
|
1336
|
-
return typeof rule === 'string' ? rule : rule.rule;
|
|
1337
|
-
}
|
|
1338
|
-
getRuleIdentity(rule) {
|
|
1339
|
-
return JSON.stringify(rule);
|
|
1340
|
-
}
|
|
1341
|
-
collectRules($input, fromSchema = [], schemaKeys = new Set()) {
|
|
1342
|
-
const list = [];
|
|
1343
|
-
if ($input.hasAttribute('required'))
|
|
1344
|
-
list.push('required');
|
|
1345
|
-
list.push(...fromSchema);
|
|
1346
|
-
const dataValidateRules = this.parseValidationAttribute($input.getAttribute('data-validate')).filter(rule => {
|
|
1347
|
-
const ruleName = this.getRuleName(rule);
|
|
1348
|
-
return getValidator(ruleName) || !schemaKeys.has(ruleName);
|
|
1349
|
-
});
|
|
1350
|
-
list.push(...dataValidateRules);
|
|
1351
|
-
list.push(...this.parseValidationAttribute($input.getAttribute('data-custom-validate')));
|
|
1352
|
-
const filtered = [];
|
|
1353
|
-
const seen = new Set();
|
|
1354
|
-
for (const rule of list) {
|
|
1355
|
-
const ruleName = this.getRuleName(rule);
|
|
1356
|
-
if (!getValidator(ruleName)) {
|
|
1357
|
-
console.warn(`[FormFather] Unknown validation rule "${ruleName}"`);
|
|
1358
|
-
continue;
|
|
1359
|
-
}
|
|
1360
|
-
const identity = this.getRuleIdentity(rule);
|
|
1361
|
-
if (seen.has(identity))
|
|
1362
|
-
continue;
|
|
1363
|
-
seen.add(identity);
|
|
1364
|
-
filtered.push(rule);
|
|
1365
|
-
}
|
|
1366
|
-
return filtered;
|
|
866
|
+
const $loaderEl = document.createElement("span");
|
|
867
|
+
$loaderEl.innerHTML = loaderSvg;
|
|
868
|
+
$loaderEl.className = "button__loader";
|
|
869
|
+
$submit.insertAdjacentElement("afterbegin", $loaderEl);
|
|
870
|
+
requestAnimationFrame(() => {
|
|
871
|
+
$loaderEl.setAttribute("data-showed", "");
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
/** Скрывает лоадер */
|
|
876
|
+
hideLoader() {
|
|
877
|
+
this.$submits.forEach(($submit) => {
|
|
878
|
+
$submit.classList.remove("button--loading");
|
|
879
|
+
const $loader = $submit.querySelector(".button__loader");
|
|
880
|
+
if ($loader) {
|
|
881
|
+
$loader.classList.remove("active");
|
|
882
|
+
$loader.removeAttribute("data-showed");
|
|
883
|
+
setTimeout(() => {
|
|
884
|
+
$loader.remove();
|
|
885
|
+
}, 250);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Скрывает ошибку.
|
|
891
|
+
*
|
|
892
|
+
* @param {HTMLInputElement} $input - Поле ввода.
|
|
893
|
+
*/
|
|
894
|
+
hideError($input) {
|
|
895
|
+
var _a2;
|
|
896
|
+
const $inputWrapper = closest($input, this.config.inputWrapperSelector) || $input.parentElement;
|
|
897
|
+
const $errorContainer = this.getErrorContainer($input);
|
|
898
|
+
$input.removeAttribute("aria-invalid");
|
|
899
|
+
this.removeErrorDescription($input);
|
|
900
|
+
if ($errorContainer) {
|
|
901
|
+
$errorContainer.textContent = "";
|
|
902
|
+
$errorContainer.setAttribute("hidden", "");
|
|
903
|
+
}
|
|
904
|
+
(_a2 = $inputWrapper == null ? void 0 : $inputWrapper.querySelector("[data-form-father-error]")) == null ? void 0 : _a2.remove();
|
|
905
|
+
$inputWrapper == null ? void 0 : $inputWrapper.classList.remove("input__wrapper--error");
|
|
906
|
+
if ($inputWrapper && $inputWrapper.classList.contains("input__wrapper")) {
|
|
907
|
+
$inputWrapper.getAttribute("data-type-error") || "default";
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
getErrorSummaryOptions() {
|
|
911
|
+
if (!this.config.errorSummary) return null;
|
|
912
|
+
return this.config.errorSummary === true ? {} : this.config.errorSummary;
|
|
913
|
+
}
|
|
914
|
+
getErrorSummaryContainer(options, create) {
|
|
915
|
+
if (options.selector) {
|
|
916
|
+
try {
|
|
917
|
+
return this.$el.querySelector(options.selector) || document.querySelector(options.selector);
|
|
918
|
+
} catch (e) {
|
|
919
|
+
console.warn(`[FormFather] Invalid error summary selector "${options.selector}"`);
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const existing = this.$el.querySelector(`[${ERROR_SUMMARY_ATTRIBUTE}]`);
|
|
924
|
+
if (existing || !create) return existing;
|
|
925
|
+
const $summary = document.createElement("div");
|
|
926
|
+
$summary.setAttribute(ERROR_SUMMARY_ATTRIBUTE, "");
|
|
927
|
+
this.$el.insertAdjacentElement("afterbegin", $summary);
|
|
928
|
+
return $summary;
|
|
929
|
+
}
|
|
930
|
+
prepareErrorSummaryContainer($summary) {
|
|
931
|
+
$summary.setAttribute(ERROR_SUMMARY_ATTRIBUTE, "");
|
|
932
|
+
if (!$summary.hasAttribute("role")) $summary.setAttribute("role", "alert");
|
|
933
|
+
if (!$summary.hasAttribute("tabindex")) $summary.setAttribute("tabindex", "-1");
|
|
934
|
+
$summary.removeAttribute("hidden");
|
|
935
|
+
}
|
|
936
|
+
focusErrorSummary($summary) {
|
|
937
|
+
try {
|
|
938
|
+
$summary.focus({ preventScroll: true });
|
|
939
|
+
} catch (e) {
|
|
940
|
+
$summary.focus();
|
|
1367
941
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
942
|
+
}
|
|
943
|
+
focusFieldFromSummary($field) {
|
|
944
|
+
if (typeof $field.scrollIntoView === "function") {
|
|
945
|
+
$field.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
1371
946
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
const message = this.getRuleMessage($input, rule, messages, validator.defaultMessage, isObjectResult && typeof result.message === 'string' ? result.message : undefined);
|
|
1465
|
-
this.setValidationError($input, rule, message, 'client');
|
|
1466
|
-
this.showError($input, message);
|
|
1467
|
-
return false;
|
|
1468
|
-
}
|
|
1469
|
-
if (isObjectResult && raw.stopOthers) {
|
|
1470
|
-
return true;
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
if (!this.isLatestFieldValidation($input, validationToken))
|
|
1474
|
-
return !this.hasFieldError($input);
|
|
1475
|
-
this.clearFieldErrorRecord($input);
|
|
1476
|
-
this.hideError($input);
|
|
1477
|
-
return true;
|
|
947
|
+
try {
|
|
948
|
+
$field.focus({ preventScroll: true });
|
|
949
|
+
} catch (e) {
|
|
950
|
+
$field.focus();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
renderDefaultErrorSummary($summary, errors, options) {
|
|
954
|
+
$summary.textContent = "";
|
|
955
|
+
const $title = document.createElement("p");
|
|
956
|
+
$title.setAttribute("data-form-father-summary-title", "");
|
|
957
|
+
$title.textContent = options.title || "\u041F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435 \u043F\u043E\u043B\u044F \u0444\u043E\u0440\u043C\u044B";
|
|
958
|
+
const $list = document.createElement("ul");
|
|
959
|
+
$list.setAttribute("data-form-father-summary-list", "");
|
|
960
|
+
errors.forEach((error) => {
|
|
961
|
+
const $item = document.createElement("li");
|
|
962
|
+
const $field = error.field === FORM_ERROR_FIELD ? null : this.findFieldByName(error.field);
|
|
963
|
+
if ($field) {
|
|
964
|
+
const $button = document.createElement("button");
|
|
965
|
+
$button.type = "button";
|
|
966
|
+
$button.setAttribute("data-form-father-summary-field", error.field);
|
|
967
|
+
$button.textContent = error.message;
|
|
968
|
+
$button.addEventListener("click", () => this.focusFieldFromSummary($field));
|
|
969
|
+
$item.appendChild($button);
|
|
970
|
+
} else {
|
|
971
|
+
const $message = document.createElement("span");
|
|
972
|
+
$message.textContent = error.message;
|
|
973
|
+
$item.appendChild($message);
|
|
974
|
+
}
|
|
975
|
+
$list.appendChild($item);
|
|
976
|
+
});
|
|
977
|
+
$summary.append($title, $list);
|
|
978
|
+
}
|
|
979
|
+
renderErrorSummary(errors = this.getErrors()) {
|
|
980
|
+
var _a2;
|
|
981
|
+
const options = this.getErrorSummaryOptions();
|
|
982
|
+
if (!options) return;
|
|
983
|
+
if (errors.length === 0) {
|
|
984
|
+
this.hideErrorSummary();
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
const $summary = this.getErrorSummaryContainer(options, true);
|
|
988
|
+
if (!$summary) return;
|
|
989
|
+
this.prepareErrorSummaryContainer($summary);
|
|
990
|
+
const rendered = (_a2 = options.render) == null ? void 0 : _a2.call(options, errors.map((error) => ({ ...error })), this);
|
|
991
|
+
if (rendered instanceof HTMLElement) {
|
|
992
|
+
$summary.replaceChildren(rendered);
|
|
993
|
+
} else if (typeof rendered === "string") {
|
|
994
|
+
$summary.innerHTML = rendered;
|
|
995
|
+
} else if (!options.render) {
|
|
996
|
+
this.renderDefaultErrorSummary($summary, errors, options);
|
|
997
|
+
}
|
|
998
|
+
if (options.focus) this.focusErrorSummary($summary);
|
|
999
|
+
}
|
|
1000
|
+
hideErrorSummary() {
|
|
1001
|
+
const options = this.getErrorSummaryOptions() || {};
|
|
1002
|
+
const $summary = this.getErrorSummaryContainer(options, false);
|
|
1003
|
+
if (!$summary) return;
|
|
1004
|
+
$summary.replaceChildren();
|
|
1005
|
+
$summary.setAttribute("hidden", "");
|
|
1006
|
+
}
|
|
1007
|
+
syncErrorSummary() {
|
|
1008
|
+
if (this.errors.length === 0) {
|
|
1009
|
+
this.hideErrorSummary();
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
this.renderErrorSummary();
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Проскроливает до первого ошибочного поля.
|
|
1016
|
+
*
|
|
1017
|
+
* @param {HTMLElement[]} inputsList - Массив элементов с ошибкой.
|
|
1018
|
+
*/
|
|
1019
|
+
scrollToFirstErroredInput(inputsList) {
|
|
1020
|
+
if (inputsList.length > 0) {
|
|
1021
|
+
const allInputInFormList = Array.from(this.$el.querySelectorAll("input, textarea, select"));
|
|
1022
|
+
let $firstInputOnForm = null;
|
|
1023
|
+
let minIndex = Infinity;
|
|
1024
|
+
inputsList.forEach(($input) => {
|
|
1025
|
+
const findIndex = allInputInFormList.findIndex(($item) => $item === $input);
|
|
1026
|
+
if (findIndex < minIndex) {
|
|
1027
|
+
minIndex = findIndex;
|
|
1028
|
+
$firstInputOnForm = $input;
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
if ($firstInputOnForm) {
|
|
1032
|
+
const rect = $firstInputOnForm.getBoundingClientRect();
|
|
1033
|
+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
1034
|
+
const scrollTargetY = rect.top + scrollTop - window.innerHeight / 2;
|
|
1035
|
+
document.documentElement.setAttribute("data-now-scrolling", "");
|
|
1036
|
+
window.scrollTo({
|
|
1037
|
+
top: scrollTargetY,
|
|
1038
|
+
behavior: "smooth"
|
|
1478
1039
|
});
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1040
|
+
setTimeout(() => {
|
|
1041
|
+
document.documentElement.removeAttribute("data-now-scrolling");
|
|
1042
|
+
}, 800);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
focusFirstErroredInput(inputsList) {
|
|
1047
|
+
var _a2;
|
|
1048
|
+
if (inputsList.length === 0) return;
|
|
1049
|
+
const allInputInFormList = Array.from(this.$el.querySelectorAll("input, textarea, select"));
|
|
1050
|
+
const $firstInputOnForm = inputsList.slice().sort((a, b) => allInputInFormList.indexOf(a) - allInputInFormList.indexOf(b))[0];
|
|
1051
|
+
(_a2 = $firstInputOnForm == null ? void 0 : $firstInputOnForm.focus) == null ? void 0 : _a2.call($firstInputOnForm, { preventScroll: true });
|
|
1052
|
+
}
|
|
1053
|
+
getValidationSchema() {
|
|
1054
|
+
return {
|
|
1055
|
+
...Form.defaultValidationSchema || {},
|
|
1056
|
+
...this.config.validationSchema || {}
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
getAllFields($block = this.$el) {
|
|
1060
|
+
return Array.from($block.querySelectorAll("input, textarea, select"));
|
|
1061
|
+
}
|
|
1062
|
+
getFieldKey($input) {
|
|
1063
|
+
return $input.name || $input.id || $input.getAttribute("data-field") || $input.type || "field";
|
|
1064
|
+
}
|
|
1065
|
+
hasFieldError($input) {
|
|
1066
|
+
const field = this.getFieldKey($input);
|
|
1067
|
+
return $input.getAttribute("aria-invalid") === "true" || this.errors.some((error) => error.field === field);
|
|
1068
|
+
}
|
|
1069
|
+
clearFieldErrorRecord($input) {
|
|
1070
|
+
const field = this.getFieldKey($input);
|
|
1071
|
+
this.errors = this.errors.filter((error) => error.field !== field);
|
|
1072
|
+
}
|
|
1073
|
+
setErrorRecord(field, rule, message, source) {
|
|
1074
|
+
this.errors.push({
|
|
1075
|
+
field,
|
|
1076
|
+
rule,
|
|
1077
|
+
message,
|
|
1078
|
+
source
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
getValidationStateAttribute() {
|
|
1082
|
+
return this.config.validationStateAttribute || "data-form-father-state";
|
|
1083
|
+
}
|
|
1084
|
+
setFieldValidationState($input, state) {
|
|
1085
|
+
const attr = this.getValidationStateAttribute();
|
|
1086
|
+
if (state) {
|
|
1087
|
+
$input.setAttribute(attr, state);
|
|
1088
|
+
} else {
|
|
1089
|
+
$input.removeAttribute(attr);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
startFieldValidation($input) {
|
|
1093
|
+
const token = ++this.fieldValidationTokenCounter;
|
|
1094
|
+
this.fieldValidationTokens.set($input, token);
|
|
1095
|
+
$input.setAttribute("aria-busy", "true");
|
|
1096
|
+
this.setFieldValidationState($input, "validating");
|
|
1097
|
+
return token;
|
|
1098
|
+
}
|
|
1099
|
+
isLatestFieldValidation($input, token) {
|
|
1100
|
+
return token == null || this.fieldValidationTokens.get($input) === token;
|
|
1101
|
+
}
|
|
1102
|
+
finishFieldValidation($input, token, isValid) {
|
|
1103
|
+
if (!this.isLatestFieldValidation($input, token)) return;
|
|
1104
|
+
$input.removeAttribute("aria-busy");
|
|
1105
|
+
this.setFieldValidationState($input, isValid ? "valid" : "invalid");
|
|
1106
|
+
}
|
|
1107
|
+
clearFieldValidationState($input) {
|
|
1108
|
+
this.fieldValidationTokens.delete($input);
|
|
1109
|
+
$input.removeAttribute("aria-busy");
|
|
1110
|
+
this.setFieldValidationState($input, null);
|
|
1111
|
+
}
|
|
1112
|
+
setValidationError($input, rule, message, source) {
|
|
1113
|
+
this.clearFieldErrorRecord($input);
|
|
1114
|
+
this.setErrorRecord(this.getFieldKey($input), rule, message, source);
|
|
1115
|
+
}
|
|
1116
|
+
resolveField(field) {
|
|
1117
|
+
if (typeof field !== "string") return field;
|
|
1118
|
+
return this.findFieldByName(field);
|
|
1119
|
+
}
|
|
1120
|
+
parseRuleString(ruleString) {
|
|
1121
|
+
const [rule, ...params] = ruleString.split(":").map((s) => s.trim());
|
|
1122
|
+
return params.length > 0 ? { rule, params: params.length === 1 ? params[0] : params } : rule;
|
|
1123
|
+
}
|
|
1124
|
+
parseValidationAttribute(value) {
|
|
1125
|
+
return (value != null ? value : "").split(/[,\s|]+/).map((s) => s.trim()).filter(Boolean).map((ruleString) => this.parseRuleString(ruleString));
|
|
1126
|
+
}
|
|
1127
|
+
getRuleName(rule) {
|
|
1128
|
+
return typeof rule === "string" ? rule : rule.rule;
|
|
1129
|
+
}
|
|
1130
|
+
getRuleIdentity(rule) {
|
|
1131
|
+
return JSON.stringify(rule);
|
|
1132
|
+
}
|
|
1133
|
+
collectRules($input, fromSchema = [], schemaKeys = /* @__PURE__ */ new Set()) {
|
|
1134
|
+
const list = [];
|
|
1135
|
+
if ($input.hasAttribute("required")) list.push("required");
|
|
1136
|
+
list.push(...fromSchema);
|
|
1137
|
+
const dataValidateRules = this.parseValidationAttribute($input.getAttribute("data-validate")).filter((rule) => {
|
|
1138
|
+
const ruleName = this.getRuleName(rule);
|
|
1139
|
+
return getValidator(ruleName) || !schemaKeys.has(ruleName);
|
|
1140
|
+
});
|
|
1141
|
+
list.push(...dataValidateRules);
|
|
1142
|
+
list.push(...this.parseValidationAttribute($input.getAttribute("data-custom-validate")));
|
|
1143
|
+
const filtered = [];
|
|
1144
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1145
|
+
for (const rule of list) {
|
|
1146
|
+
const ruleName = this.getRuleName(rule);
|
|
1147
|
+
if (!getValidator(ruleName)) {
|
|
1148
|
+
console.warn(`[FormFather] Unknown validation rule "${ruleName}"`);
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
const identity = this.getRuleIdentity(rule);
|
|
1152
|
+
if (seen.has(identity)) continue;
|
|
1153
|
+
seen.add(identity);
|
|
1154
|
+
filtered.push(rule);
|
|
1155
|
+
}
|
|
1156
|
+
return filtered;
|
|
1157
|
+
}
|
|
1158
|
+
getRuleMessage($input, rule, messages, defaultMessage, overrideMessage) {
|
|
1159
|
+
const attrMessage = $input.getAttribute(`data-error-${rule}`);
|
|
1160
|
+
return overrideMessage || attrMessage || messages[rule] || defaultMessage;
|
|
1161
|
+
}
|
|
1162
|
+
fieldMatchesSchemaDefinition($input, key, def) {
|
|
1163
|
+
if (def.selector) {
|
|
1164
|
+
try {
|
|
1165
|
+
return $input.matches(def.selector);
|
|
1166
|
+
} catch (e) {
|
|
1167
|
+
console.warn(`[FormFather] Invalid validation selector "${def.selector}"`);
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return this.parseValidationAttribute($input.getAttribute("data-validate")).some((rule) => this.getRuleName(rule) === key);
|
|
1172
|
+
}
|
|
1173
|
+
getFieldValidationConfig($input, schema = this.getValidationSchema()) {
|
|
1174
|
+
const rules = [];
|
|
1175
|
+
const messages = {};
|
|
1176
|
+
for (const [key, def] of Object.entries(schema)) {
|
|
1177
|
+
if (!this.fieldMatchesSchemaDefinition($input, key, def)) continue;
|
|
1178
|
+
rules.push(...def.rules);
|
|
1179
|
+
Object.assign(messages, def.messages || {});
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
rules: this.collectRules($input, rules, new Set(Object.keys(schema))),
|
|
1183
|
+
messages
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
getValidationFields($block, schema) {
|
|
1187
|
+
const fields = /* @__PURE__ */ new Set();
|
|
1188
|
+
const addFields = (nodeList) => {
|
|
1189
|
+
Array.from(nodeList).forEach(($input) => {
|
|
1190
|
+
if ($input instanceof HTMLInputElement || $input instanceof HTMLTextAreaElement || $input instanceof HTMLSelectElement) {
|
|
1191
|
+
fields.add($input);
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
};
|
|
1195
|
+
for (const [key, def] of Object.entries(schema)) {
|
|
1196
|
+
if (def.selector) {
|
|
1197
|
+
try {
|
|
1198
|
+
addFields($block.querySelectorAll(def.selector));
|
|
1199
|
+
} catch (e) {
|
|
1200
|
+
console.warn(`[FormFather] Invalid validation selector "${def.selector}"`);
|
|
1201
|
+
}
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
this.getAllFields($block).filter(
|
|
1205
|
+
($input) => this.parseValidationAttribute($input.getAttribute("data-validate")).some((rule) => this.getRuleName(rule) === key)
|
|
1206
|
+
).forEach(($input) => fields.add($input));
|
|
1207
|
+
}
|
|
1208
|
+
addFields($block.querySelectorAll("[required], [data-validate], [data-custom-validate]"));
|
|
1209
|
+
return Array.from(fields);
|
|
1210
|
+
}
|
|
1211
|
+
async validateInput($input, rules, messages = {}, $block = this.$el, validationToken) {
|
|
1212
|
+
if ($input.hasAttribute("data-no-validate")) return true;
|
|
1213
|
+
const isCheckable = $input instanceof HTMLInputElement && ($input.type === "checkbox" || $input.type === "radio");
|
|
1214
|
+
const empty = isCheckable ? !$input.checked : $input.value.trim().length === 0;
|
|
1215
|
+
const hasRequiredRule = rules.some((rule) => this.getRuleName(rule) === "required");
|
|
1216
|
+
if (empty && !hasRequiredRule) {
|
|
1217
|
+
if (!this.isLatestFieldValidation($input, validationToken)) return !this.hasFieldError($input);
|
|
1218
|
+
this.clearFieldErrorRecord($input);
|
|
1219
|
+
this.hideError($input);
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
for (const validationRule of rules) {
|
|
1223
|
+
const { rule, params } = typeof validationRule === "string" ? { rule: validationRule, params: void 0 } : { rule: validationRule.rule, params: validationRule.params };
|
|
1224
|
+
const validator = getValidator(rule);
|
|
1225
|
+
if (!validator) continue;
|
|
1226
|
+
const raw = await validator.fn($input.value, $input, $block, params);
|
|
1227
|
+
const isObjectResult = typeof raw === "object" && raw !== null;
|
|
1228
|
+
const passed = isObjectResult ? raw.valid === true : !!raw;
|
|
1229
|
+
if (!this.isLatestFieldValidation($input, validationToken)) return !this.hasFieldError($input);
|
|
1230
|
+
if (isObjectResult && raw.effect) {
|
|
1231
|
+
await raw.effect({
|
|
1232
|
+
value: $input.value,
|
|
1233
|
+
$input,
|
|
1234
|
+
$form: $block,
|
|
1235
|
+
params
|
|
1494
1236
|
});
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
this.setFieldValidationState($field, 'invalid');
|
|
1512
|
-
this.syncErrorSummary();
|
|
1237
|
+
}
|
|
1238
|
+
if (!this.isLatestFieldValidation($input, validationToken)) return !this.hasFieldError($input);
|
|
1239
|
+
if (!passed) {
|
|
1240
|
+
const result = raw;
|
|
1241
|
+
const message = this.getRuleMessage(
|
|
1242
|
+
$input,
|
|
1243
|
+
rule,
|
|
1244
|
+
messages,
|
|
1245
|
+
validator.defaultMessage,
|
|
1246
|
+
isObjectResult && typeof result.message === "string" ? result.message : void 0
|
|
1247
|
+
);
|
|
1248
|
+
this.setValidationError($input, rule, message, "client");
|
|
1249
|
+
this.showError($input, message);
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
if (isObjectResult && raw.stopOthers) {
|
|
1513
1253
|
return true;
|
|
1254
|
+
}
|
|
1514
1255
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1256
|
+
if (!this.isLatestFieldValidation($input, validationToken)) return !this.hasFieldError($input);
|
|
1257
|
+
this.clearFieldErrorRecord($input);
|
|
1258
|
+
this.hideError($input);
|
|
1259
|
+
return true;
|
|
1260
|
+
}
|
|
1261
|
+
async runFieldValidation($input, rules, messages = {}, $block = this.$el) {
|
|
1262
|
+
const validationToken = this.startFieldValidation($input);
|
|
1263
|
+
try {
|
|
1264
|
+
const isValid = await this.validateInput($input, rules, messages, $block, validationToken);
|
|
1265
|
+
this.finishFieldValidation($input, validationToken, isValid);
|
|
1266
|
+
return isValid;
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
if (this.isLatestFieldValidation($input, validationToken)) {
|
|
1269
|
+
this.clearFieldValidationState($input);
|
|
1270
|
+
}
|
|
1271
|
+
throw error;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
getRadioGroup($input, $block) {
|
|
1275
|
+
if (!($input instanceof HTMLInputElement) || $input.type !== "radio" || !$input.name) return [];
|
|
1276
|
+
return Array.from($block.querySelectorAll('input[type="radio"]')).filter(
|
|
1277
|
+
($radio) => $radio.name === $input.name
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
/** Показывает ошибку конкретного поля по имени или DOM-элементу. */
|
|
1281
|
+
showFieldError(field, message, source = "manual", rule = source) {
|
|
1282
|
+
const $field = this.resolveField(field);
|
|
1283
|
+
if (!$field) {
|
|
1284
|
+
console.warn(`[FormFather] Field "${String(field)}" not found`);
|
|
1285
|
+
return false;
|
|
1286
|
+
}
|
|
1287
|
+
this.setValidationError($field, rule, message, source);
|
|
1288
|
+
this.showError($field, message);
|
|
1289
|
+
$field.removeAttribute("aria-busy");
|
|
1290
|
+
this.setFieldValidationState($field, "invalid");
|
|
1291
|
+
this.syncErrorSummary();
|
|
1292
|
+
return true;
|
|
1293
|
+
}
|
|
1294
|
+
/** Проверяет одно поле по имени или DOM-элементу. */
|
|
1295
|
+
async validateField(field) {
|
|
1296
|
+
const $field = this.resolveField(field);
|
|
1297
|
+
if (!$field) {
|
|
1298
|
+
console.warn(`[FormFather] Field "${String(field)}" not found`);
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
const { rules, messages } = this.getFieldValidationConfig($field);
|
|
1302
|
+
const isValid = await this.runFieldValidation($field, rules, messages);
|
|
1303
|
+
this.syncErrorSummary();
|
|
1304
|
+
return isValid;
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Проверяет все поля ввода.
|
|
1308
|
+
*
|
|
1309
|
+
* Порядок для каждого поля:
|
|
1310
|
+
*
|
|
1311
|
+
* 1. `required` — если атрибут `required`
|
|
1312
|
+
* 2. правила из `validationSchema`
|
|
1313
|
+
* 3. правила из `data-validate`
|
|
1314
|
+
* 4. правила из `data-custom-validate`
|
|
1315
|
+
*
|
|
1316
|
+
* Для одного поля показывается только первая ошибка. Неизвестные правила фиксируются `console.warn` и исключаются ДО
|
|
1317
|
+
* валидации.
|
|
1318
|
+
*/
|
|
1319
|
+
async validate($block = this.$el) {
|
|
1320
|
+
const schema = this.getValidationSchema();
|
|
1321
|
+
const erroredInputs = [];
|
|
1322
|
+
const processed = /* @__PURE__ */ new WeakSet();
|
|
1323
|
+
let ok = true;
|
|
1324
|
+
this.errors = [];
|
|
1325
|
+
for (const $input of this.getValidationFields($block, schema)) {
|
|
1326
|
+
if (processed.has($input)) continue;
|
|
1327
|
+
const radioGroup = this.getRadioGroup($input, $block);
|
|
1328
|
+
if (radioGroup.length > 0) {
|
|
1329
|
+
radioGroup.forEach(($radio) => processed.add($radio));
|
|
1330
|
+
} else {
|
|
1331
|
+
processed.add($input);
|
|
1332
|
+
}
|
|
1333
|
+
const { rules, messages } = this.getFieldValidationConfig($input, schema);
|
|
1334
|
+
const fieldValid = await this.runFieldValidation($input, rules, messages, $block);
|
|
1335
|
+
if (!fieldValid) {
|
|
1336
|
+
ok = false;
|
|
1337
|
+
erroredInputs.push($input);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const formValidation = await this.validateFormValidators();
|
|
1341
|
+
if (!formValidation.ok) {
|
|
1342
|
+
ok = false;
|
|
1343
|
+
erroredInputs.push(...formValidation.erroredInputs);
|
|
1344
|
+
}
|
|
1345
|
+
const visibleErroredInputs = erroredInputs.filter((item) => !item.hasAttribute("data-no-error-scroll"));
|
|
1346
|
+
if (!ok && this.config.scrollToFirstErroredInput) {
|
|
1347
|
+
this.scrollToFirstErroredInput(visibleErroredInputs);
|
|
1348
|
+
}
|
|
1349
|
+
if (!ok && this.config.focusFirstErroredInput) {
|
|
1350
|
+
this.focusFirstErroredInput(visibleErroredInputs);
|
|
1351
|
+
}
|
|
1352
|
+
if (ok) {
|
|
1353
|
+
this.hideErrorSummary();
|
|
1354
|
+
} else {
|
|
1355
|
+
this.renderErrorSummary();
|
|
1356
|
+
}
|
|
1357
|
+
return ok;
|
|
1358
|
+
}
|
|
1359
|
+
/** Блокирует кнопку отправки данных. */
|
|
1360
|
+
disableSubmit() {
|
|
1361
|
+
this.$submits.forEach(($submit) => {
|
|
1362
|
+
$submit.setAttribute("disabled", "");
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
/** Снимает блокировку с кнопки отправки данных. */
|
|
1366
|
+
enableSubmit() {
|
|
1367
|
+
this.$submits.forEach(($submit) => {
|
|
1368
|
+
$submit.removeAttribute("disabled");
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
/** Очищает поля ввода формы. */
|
|
1372
|
+
clearInputs() {
|
|
1373
|
+
var _a2;
|
|
1374
|
+
(_a2 = this.inputs) == null ? void 0 : _a2.forEach(($inputEl) => {
|
|
1375
|
+
var _a3;
|
|
1376
|
+
const $input = $inputEl;
|
|
1377
|
+
if ($input.type === "radio" || $input.type === "checkbox") {
|
|
1378
|
+
$input.checked = false;
|
|
1379
|
+
} else {
|
|
1380
|
+
$input.value = "";
|
|
1381
|
+
(_a3 = $input.parentElement) == null ? void 0 : _a3.classList.remove("filled");
|
|
1382
|
+
$input.dispatchEvent(new Event("input"));
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
_prefersReducedMotion() {
|
|
1387
|
+
var _a2;
|
|
1388
|
+
return typeof window !== "undefined" && ((_a2 = window.matchMedia) == null ? void 0 : _a2.call(window, "(prefers-reduced-motion: reduce)").matches);
|
|
1389
|
+
}
|
|
1390
|
+
_canAnimate(el) {
|
|
1391
|
+
return !!(el && "animate" in el && !this._prefersReducedMotion());
|
|
1392
|
+
}
|
|
1393
|
+
_cancelAnimations(el) {
|
|
1394
|
+
var _a2;
|
|
1395
|
+
const animatedEl = el;
|
|
1396
|
+
(_a2 = animatedEl == null ? void 0 : animatedEl.getAnimations) == null ? void 0 : _a2.call(animatedEl).forEach((animation) => animation.cancel());
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Показывает ошибку под полями ввода - ошибку, относящуюся ко всей форме.
|
|
1400
|
+
*
|
|
1401
|
+
* @param {String} text - Текст ошибки.
|
|
1402
|
+
*/
|
|
1403
|
+
showErrorForm(text) {
|
|
1404
|
+
var _a2;
|
|
1405
|
+
const inputWrappersList = this.$el.querySelectorAll(this.config.inputWrapperSelector);
|
|
1406
|
+
const $lastInputWrapper = inputWrappersList[inputWrappersList.length - 1];
|
|
1407
|
+
let $errorWrapper = this.$el.querySelector(".error-block-under-input__wrapper");
|
|
1408
|
+
if (!$errorWrapper || $lastInputWrapper && !($lastInputWrapper.nextElementSibling === $errorWrapper)) {
|
|
1409
|
+
$errorWrapper = document.createElement("div");
|
|
1410
|
+
$errorWrapper.className = "error-block-under-input__wrapper";
|
|
1411
|
+
$errorWrapper.innerHTML = `
|
|
1642
1412
|
<div class="error-block-under-input error-block-under-input--warning" style="display:grid;grid-template-columns:auto 1fr;gap:8px;align-items:start;">
|
|
1643
1413
|
<div class="error-block-under-input__icon-wrapper" style="position:relative;width:20px;height:20px;">
|
|
1644
1414
|
<span class="error-block-under-input__icon" style="display:block;width:20px;height:20px;"></span>
|
|
@@ -1650,426 +1420,371 @@ class Form {
|
|
|
1650
1420
|
</p>
|
|
1651
1421
|
</div>
|
|
1652
1422
|
`;
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1423
|
+
Object.assign($errorWrapper.style, {
|
|
1424
|
+
boxSizing: "border-box",
|
|
1425
|
+
overflow: "hidden",
|
|
1426
|
+
height: "0px",
|
|
1427
|
+
paddingTop: "0px",
|
|
1428
|
+
opacity: "0"
|
|
1429
|
+
});
|
|
1430
|
+
if ($lastInputWrapper) {
|
|
1431
|
+
$lastInputWrapper.insertAdjacentElement("afterend", $errorWrapper);
|
|
1432
|
+
} else {
|
|
1433
|
+
this.$el.appendChild($errorWrapper);
|
|
1434
|
+
}
|
|
1435
|
+
} else {
|
|
1436
|
+
$errorWrapper.style.boxSizing = "border-box";
|
|
1437
|
+
$errorWrapper.style.overflow = "hidden";
|
|
1438
|
+
}
|
|
1439
|
+
$errorWrapper.querySelector(".error-block-under-input__main-text").textContent = text;
|
|
1440
|
+
const $secondaryText = $errorWrapper.querySelector(
|
|
1441
|
+
".error-block-under-input__secondary-text"
|
|
1442
|
+
);
|
|
1443
|
+
if ($secondaryText) $secondaryText.textContent = "";
|
|
1444
|
+
(_a2 = $errorWrapper.querySelector(".error-block-under-input")) == null ? void 0 : _a2.classList.remove("error-block-under-input--success");
|
|
1445
|
+
const prevHeight = $errorWrapper.style.height;
|
|
1446
|
+
const prevPadding = $errorWrapper.style.paddingTop;
|
|
1447
|
+
$errorWrapper.style.height = "auto";
|
|
1448
|
+
$errorWrapper.style.paddingTop = `${this._anim.paddingTopOpened}px`;
|
|
1449
|
+
const targetHeight = $errorWrapper.clientHeight;
|
|
1450
|
+
$errorWrapper.style.height = prevHeight || "0px";
|
|
1451
|
+
$errorWrapper.style.paddingTop = prevPadding || "0px";
|
|
1452
|
+
if (parseFloat($errorWrapper.style.height) === targetHeight && parseFloat($errorWrapper.style.opacity || "1") === 1) {
|
|
1453
|
+
this._ringIcon($errorWrapper);
|
|
1454
|
+
} else {
|
|
1455
|
+
this._cancelAnimations($errorWrapper);
|
|
1456
|
+
if (this._canAnimate($errorWrapper)) {
|
|
1457
|
+
const anim = $errorWrapper.animate(
|
|
1458
|
+
[
|
|
1459
|
+
{
|
|
1460
|
+
height: `${parseFloat($errorWrapper.style.height) || 0}px`,
|
|
1461
|
+
paddingTop: `${parseFloat($errorWrapper.style.paddingTop) || 0}px`,
|
|
1462
|
+
opacity: parseFloat($errorWrapper.style.opacity || "0")
|
|
1463
|
+
},
|
|
1464
|
+
{
|
|
1465
|
+
height: `${targetHeight}px`,
|
|
1466
|
+
paddingTop: `${this._anim.paddingTopOpened}px`,
|
|
1467
|
+
opacity: 1
|
|
1663
1468
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
$errorWrapper.querySelector('.error-block-under-input__main-text').textContent = text;
|
|
1675
|
-
const $secondaryText = $errorWrapper.querySelector('.error-block-under-input__secondary-text');
|
|
1676
|
-
if ($secondaryText)
|
|
1677
|
-
$secondaryText.textContent = '';
|
|
1678
|
-
// Сброс success-класса, если он устанавливался где-то в другом коде
|
|
1679
|
-
(_a = $errorWrapper.querySelector('.error-block-under-input')) === null || _a === void 0 ? void 0 : _a.classList.remove('error-block-under-input--success');
|
|
1680
|
-
// Посчитать целевую высоту
|
|
1681
|
-
// Чтобы корректно мерить, временно выставим auto
|
|
1682
|
-
const prevHeight = $errorWrapper.style.height;
|
|
1683
|
-
const prevPadding = $errorWrapper.style.paddingTop;
|
|
1684
|
-
$errorWrapper.style.height = 'auto';
|
|
1469
|
+
],
|
|
1470
|
+
{ duration: this._anim.duration, easing: this._anim.easing, fill: "forwards" }
|
|
1471
|
+
);
|
|
1472
|
+
anim.addEventListener("finish", () => {
|
|
1473
|
+
$errorWrapper.style.height = "auto";
|
|
1474
|
+
$errorWrapper.style.paddingTop = `${this._anim.paddingTopOpened}px`;
|
|
1475
|
+
$errorWrapper.style.opacity = "1";
|
|
1476
|
+
});
|
|
1477
|
+
} else {
|
|
1478
|
+
$errorWrapper.style.height = "auto";
|
|
1685
1479
|
$errorWrapper.style.paddingTop = `${this._anim.paddingTopOpened}px`;
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1480
|
+
$errorWrapper.style.opacity = "1";
|
|
1481
|
+
}
|
|
1482
|
+
this._ringIcon($errorWrapper);
|
|
1483
|
+
}
|
|
1484
|
+
this.$el.addEventListener("input", () => this.hideErrorForm(), { passive: true, once: true });
|
|
1485
|
+
}
|
|
1486
|
+
/** Скрывает ошибку под полями ввода - ошибку, относящуюся ко всей форме. */
|
|
1487
|
+
hideErrorForm() {
|
|
1488
|
+
const inputWrappersList = this.$el.querySelectorAll(this.config.inputWrapperSelector);
|
|
1489
|
+
const $lastInputWrapper = inputWrappersList[inputWrappersList.length - 1];
|
|
1490
|
+
const $errorWrapper = ($lastInputWrapper == null ? void 0 : $lastInputWrapper.nextElementSibling) || this.$el.querySelector(".error-block-under-input__wrapper");
|
|
1491
|
+
if (!$errorWrapper || !$errorWrapper.classList.contains("error-block-under-input__wrapper")) return;
|
|
1492
|
+
const wasAuto = $errorWrapper.style.height === "" || $errorWrapper.style.height === "auto";
|
|
1493
|
+
if (wasAuto) {
|
|
1494
|
+
$errorWrapper.style.height = `${$errorWrapper.clientHeight}px`;
|
|
1495
|
+
}
|
|
1496
|
+
$errorWrapper.style.paddingTop = $errorWrapper.style.paddingTop || `${this._anim.paddingTopOpened}px`;
|
|
1497
|
+
$errorWrapper.style.opacity = $errorWrapper.style.opacity || "1";
|
|
1498
|
+
this._cancelAnimations($errorWrapper);
|
|
1499
|
+
if (this._canAnimate($errorWrapper)) {
|
|
1500
|
+
const anim = $errorWrapper.animate(
|
|
1501
|
+
[
|
|
1502
|
+
{
|
|
1503
|
+
height: `${parseFloat($errorWrapper.style.height) || $errorWrapper.clientHeight}px`,
|
|
1504
|
+
paddingTop: `${parseFloat($errorWrapper.style.paddingTop) || this._anim.paddingTopOpened}px`,
|
|
1505
|
+
opacity: parseFloat($errorWrapper.style.opacity || "1")
|
|
1506
|
+
},
|
|
1507
|
+
{ height: "0px", paddingTop: "0px", opacity: 0 }
|
|
1508
|
+
],
|
|
1509
|
+
{ duration: this._anim.duration, easing: this._anim.easing, fill: "forwards" }
|
|
1510
|
+
);
|
|
1511
|
+
anim.addEventListener("finish", () => {
|
|
1512
|
+
$errorWrapper.remove();
|
|
1513
|
+
});
|
|
1514
|
+
} else {
|
|
1515
|
+
$errorWrapper.remove();
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
/** Короткая анимация иконки без CSS */
|
|
1519
|
+
_ringIcon($wrapper) {
|
|
1520
|
+
const $icon = $wrapper.querySelector(".error-block-under-input__icon--animated");
|
|
1521
|
+
if (!$icon) return;
|
|
1522
|
+
this._cancelAnimations($icon);
|
|
1523
|
+
if (!this._canAnimate($icon)) return;
|
|
1524
|
+
$icon.animate(
|
|
1525
|
+
[
|
|
1526
|
+
{ transform: "rotate(0deg) scale(1)", opacity: 0 },
|
|
1527
|
+
{ transform: "rotate(-8deg) scale(1.05)", opacity: 1, offset: 0.15 },
|
|
1528
|
+
{ transform: "rotate(8deg) scale(1.05)", opacity: 1, offset: 0.3 },
|
|
1529
|
+
{ transform: "rotate(-6deg) scale(1.03)", opacity: 1, offset: 0.45 },
|
|
1530
|
+
{ transform: "rotate(6deg) scale(1.03)", opacity: 1, offset: 0.6 },
|
|
1531
|
+
{ transform: "rotate(0deg) scale(1)", opacity: 0 }
|
|
1532
|
+
],
|
|
1533
|
+
{ duration: this._anim.iconDuration, easing: "ease" }
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
/** Полностью снимает слушатели и чистит артефакты формы. */
|
|
1537
|
+
destroy() {
|
|
1538
|
+
var _a2;
|
|
1539
|
+
if (this._onSubmitHandler) {
|
|
1540
|
+
this.$el.removeEventListener("submit", this._onSubmitHandler);
|
|
1541
|
+
this._onSubmitHandler = void 0;
|
|
1542
|
+
}
|
|
1543
|
+
if (this._onSubmitClickHandler) {
|
|
1544
|
+
this.$submits.forEach(($submit) => {
|
|
1545
|
+
$submit.removeEventListener("click", this._onSubmitClickHandler);
|
|
1546
|
+
});
|
|
1547
|
+
this._onSubmitClickHandler = void 0;
|
|
1548
|
+
}
|
|
1549
|
+
this.clearFieldValidationEvents();
|
|
1550
|
+
(_a2 = this.mutationObserver) == null ? void 0 : _a2.disconnect();
|
|
1551
|
+
this.mutationObserver = void 0;
|
|
1552
|
+
const formErrorWrapper = this.$el.querySelector(".error-block-under-input__wrapper");
|
|
1553
|
+
if (formErrorWrapper) {
|
|
1554
|
+
this._cancelAnimations(formErrorWrapper);
|
|
1555
|
+
formErrorWrapper.remove();
|
|
1556
|
+
}
|
|
1557
|
+
this.hideErrorSummary();
|
|
1558
|
+
this.$submits.forEach(($submit) => {
|
|
1559
|
+
$submit.classList.remove("button--loading");
|
|
1560
|
+
const btnLoader = $submit.querySelector(".button__loader");
|
|
1561
|
+
if (btnLoader) btnLoader.remove();
|
|
1562
|
+
});
|
|
1563
|
+
const inputs = this.$el.querySelectorAll("input, textarea, select");
|
|
1564
|
+
inputs.forEach(($i) => {
|
|
1565
|
+
this.hideError($i);
|
|
1566
|
+
this.clearFieldValidationState($i);
|
|
1567
|
+
});
|
|
1568
|
+
document.documentElement.removeAttribute("data-now-scrolling");
|
|
1569
|
+
this.inputs = null;
|
|
1570
|
+
this.$submits = [];
|
|
1571
|
+
this.errors = [];
|
|
1572
|
+
this.waitResponse = false;
|
|
1573
|
+
FORM_INSTANCES.delete(this.$el);
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Проверяет входные данные на их корректность.
|
|
1577
|
+
*
|
|
1578
|
+
* @returns {boolean} Корректны данные или нет (true/false).
|
|
1579
|
+
*/
|
|
1580
|
+
isCorrectArguments() {
|
|
1581
|
+
if (this.$el.tagName !== "FORM") {
|
|
1582
|
+
console.warn(`$el \u043D\u0435 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0442\u0435\u0433\u043E\u043C form`);
|
|
1583
|
+
return false;
|
|
1584
|
+
}
|
|
1585
|
+
if (this.$submits.length === 0) {
|
|
1586
|
+
console.warn(`\u0412 \u0444\u043E\u0440\u043C\u0435 \u043D\u0435\u0442 \u043A\u043D\u043E\u043F\u043A\u0438 \u0441 \u0442\u0438\u043F\u043E\u043C submit`);
|
|
1587
|
+
return false;
|
|
1767
1588
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
if (!
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1589
|
+
return true;
|
|
1590
|
+
}
|
|
1591
|
+
formDataToObject(formData) {
|
|
1592
|
+
const data = {};
|
|
1593
|
+
formData.forEach((value, key) => {
|
|
1594
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
1595
|
+
if (!Array.isArray(data[key])) {
|
|
1596
|
+
data[key] = [data[key]];
|
|
1597
|
+
}
|
|
1598
|
+
data[key].push(value);
|
|
1599
|
+
} else {
|
|
1600
|
+
data[key] = value;
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
return data;
|
|
1604
|
+
}
|
|
1605
|
+
appendFormDataValue(formData, key, value) {
|
|
1606
|
+
if (value === void 0 || value === null) return;
|
|
1607
|
+
if (Array.isArray(value)) {
|
|
1608
|
+
value.forEach((item, index) => {
|
|
1609
|
+
this.appendFormDataValue(formData, `${key}[${index}]`, item);
|
|
1610
|
+
});
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (value instanceof Blob) {
|
|
1614
|
+
formData.append(key, value);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
formData.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
1618
|
+
}
|
|
1619
|
+
objectToFormData(data) {
|
|
1620
|
+
const formData = new FormData();
|
|
1621
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
1622
|
+
this.appendFormDataValue(formData, key, value);
|
|
1623
|
+
});
|
|
1624
|
+
return formData;
|
|
1625
|
+
}
|
|
1626
|
+
stringifyFormDataValue(value) {
|
|
1627
|
+
return typeof value === "string" ? value : value.name;
|
|
1628
|
+
}
|
|
1629
|
+
formDataToUrlSearchParams(formData) {
|
|
1630
|
+
const params = new URLSearchParams();
|
|
1631
|
+
formData.forEach((value, key) => {
|
|
1632
|
+
params.append(key, this.stringifyFormDataValue(value));
|
|
1633
|
+
});
|
|
1634
|
+
return params;
|
|
1635
|
+
}
|
|
1636
|
+
formDataToTextBody(formData) {
|
|
1637
|
+
return Array.from(formData).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(this.stringifyFormDataValue(value))}`).join("\n");
|
|
1638
|
+
}
|
|
1639
|
+
isBodylessMethod(method) {
|
|
1640
|
+
return method === "GET" || method === "HEAD";
|
|
1641
|
+
}
|
|
1642
|
+
appendQueryString(action, params) {
|
|
1643
|
+
const query = params.toString();
|
|
1644
|
+
if (!query) return action;
|
|
1645
|
+
const separator = action.includes("?") ? "&" : "?";
|
|
1646
|
+
return `${action}${separator}${query}`;
|
|
1647
|
+
}
|
|
1648
|
+
findFieldByName(name) {
|
|
1649
|
+
const fields = Array.from(this.$el.querySelectorAll("input, textarea, select"));
|
|
1650
|
+
return fields.find(($field) => $field.name === name) || null;
|
|
1651
|
+
}
|
|
1652
|
+
async parseResponseBody(response) {
|
|
1653
|
+
try {
|
|
1654
|
+
const responseBody = await response.json();
|
|
1655
|
+
return responseBody && typeof responseBody === "object" ? responseBody : {
|
|
1656
|
+
success: false,
|
|
1657
|
+
error: true,
|
|
1658
|
+
"error-msg": "\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 \u043E\u0442\u0432\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430"
|
|
1659
|
+
};
|
|
1660
|
+
} catch (e) {
|
|
1661
|
+
return {
|
|
1662
|
+
success: false,
|
|
1663
|
+
error: true,
|
|
1664
|
+
"error-msg": "\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 \u043E\u0442\u0432\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430"
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
handleUnsuccessfulResponse(responseBody) {
|
|
1669
|
+
if (responseBody.error !== true) return;
|
|
1670
|
+
if (responseBody["error-msg"]) {
|
|
1671
|
+
this.setErrorRecord(FORM_ERROR_FIELD, "server", responseBody["error-msg"], "server");
|
|
1672
|
+
this.showErrorForm(responseBody["error-msg"]);
|
|
1673
|
+
}
|
|
1674
|
+
if (responseBody.errors) {
|
|
1675
|
+
const fieldsList = [];
|
|
1676
|
+
responseBody.errors.forEach((error) => {
|
|
1677
|
+
const $field = this.findFieldByName(error.name);
|
|
1678
|
+
if ($field) {
|
|
1679
|
+
fieldsList.push($field);
|
|
1680
|
+
this.setValidationError($field, "server", error["error-msg"], "server");
|
|
1681
|
+
this.showError($field, error["error-msg"]);
|
|
1682
|
+
$field.removeAttribute("aria-busy");
|
|
1683
|
+
this.setFieldValidationState($field, "invalid");
|
|
1684
|
+
} else {
|
|
1685
|
+
this.setErrorRecord(error.name, "server", error["error-msg"], "server");
|
|
1686
|
+
console.warn(`\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u043F\u043E\u043B\u0435 \u0441 \u0438\u043C\u0435\u043D\u0435\u043C: ${error.name}, \u0434\u043B\u044F \u0432\u044B\u0432\u043E\u0434\u0430 \u043E\u0448\u0438\u0431\u043A\u0438: ${error["error-msg"]}`);
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
if (this.config.scrollToFirstErroredInput === true) this.scrollToFirstErroredInput(fieldsList);
|
|
1690
|
+
if (this.config.focusFirstErroredInput === true) this.focusFirstErroredInput(fieldsList);
|
|
1691
|
+
}
|
|
1692
|
+
this.syncErrorSummary();
|
|
1693
|
+
}
|
|
1694
|
+
async sendData() {
|
|
1695
|
+
var _a2;
|
|
1696
|
+
const action = this.$el.getAttribute("action") || "";
|
|
1697
|
+
const method = ((_a2 = this.$el.getAttribute("method")) == null ? void 0 : _a2.toUpperCase()) || "POST";
|
|
1698
|
+
const enctype = this.$el.getAttribute("enctype") || "application/x-www-form-urlencoded";
|
|
1699
|
+
let body = null;
|
|
1700
|
+
let headers = {};
|
|
1701
|
+
const formData = serializeToFormData(this.$el);
|
|
1702
|
+
let data = this.formDataToObject(formData);
|
|
1703
|
+
if (this.config.logging) {
|
|
1704
|
+
for (const key of formData.keys()) console.log(formData.get(key));
|
|
1705
|
+
}
|
|
1706
|
+
const hasDataWrapper = typeof this.config.wrapData === "function";
|
|
1707
|
+
if (typeof this.config.wrapData === "function") {
|
|
1708
|
+
data = this.config.wrapData(data);
|
|
1709
|
+
}
|
|
1710
|
+
const requestFormData = hasDataWrapper ? this.objectToFormData(data) : formData;
|
|
1711
|
+
if (this.isBodylessMethod(method)) {
|
|
1712
|
+
return fetch(this.appendQueryString(action, this.formDataToUrlSearchParams(requestFormData)), {
|
|
1713
|
+
method,
|
|
1714
|
+
headers
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
switch (enctype) {
|
|
1718
|
+
case "application/x-www-form-urlencoded":
|
|
1719
|
+
body = this.formDataToUrlSearchParams(requestFormData).toString();
|
|
1720
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1721
|
+
break;
|
|
1722
|
+
case "multipart/form-data":
|
|
1723
|
+
body = requestFormData;
|
|
1724
|
+
break;
|
|
1725
|
+
case "text/plain":
|
|
1726
|
+
body = this.formDataToTextBody(requestFormData);
|
|
1727
|
+
headers["Content-Type"] = "text/plain";
|
|
1728
|
+
break;
|
|
1729
|
+
case "application/json":
|
|
1730
|
+
body = JSON.stringify(data);
|
|
1731
|
+
headers["Content-Type"] = "application/json";
|
|
1732
|
+
break;
|
|
1733
|
+
default:
|
|
1734
|
+
console.warn(`\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u044B\u0439 enctype: ${enctype}. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F application/x-www-form-urlencoded.`);
|
|
1735
|
+
body = this.formDataToUrlSearchParams(requestFormData).toString();
|
|
1736
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1737
|
+
}
|
|
1738
|
+
return fetch(action, {
|
|
1739
|
+
method,
|
|
1740
|
+
body,
|
|
1741
|
+
headers
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
async submit() {
|
|
1745
|
+
var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
1746
|
+
if (!this.waitResponse) {
|
|
1747
|
+
let loadingFinished = false;
|
|
1748
|
+
const finishLoading = () => {
|
|
1749
|
+
if (loadingFinished) return;
|
|
1830
1750
|
this.waitResponse = false;
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
value.forEach((item, index) => {
|
|
1869
|
-
this.appendFormDataValue(formData, `${key}[${index}]`, item);
|
|
1870
|
-
});
|
|
1871
|
-
return;
|
|
1872
|
-
}
|
|
1873
|
-
if (value instanceof Blob) {
|
|
1874
|
-
formData.append(key, value);
|
|
1875
|
-
return;
|
|
1876
|
-
}
|
|
1877
|
-
formData.append(key, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
1878
|
-
}
|
|
1879
|
-
objectToFormData(data) {
|
|
1880
|
-
const formData = new FormData();
|
|
1881
|
-
Object.entries(data).forEach(([key, value]) => {
|
|
1882
|
-
this.appendFormDataValue(formData, key, value);
|
|
1883
|
-
});
|
|
1884
|
-
return formData;
|
|
1885
|
-
}
|
|
1886
|
-
stringifyFormDataValue(value) {
|
|
1887
|
-
return typeof value === 'string' ? value : value.name;
|
|
1888
|
-
}
|
|
1889
|
-
formDataToUrlSearchParams(formData) {
|
|
1890
|
-
const params = new URLSearchParams();
|
|
1891
|
-
formData.forEach((value, key) => {
|
|
1892
|
-
params.append(key, this.stringifyFormDataValue(value));
|
|
1893
|
-
});
|
|
1894
|
-
return params;
|
|
1895
|
-
}
|
|
1896
|
-
formDataToTextBody(formData) {
|
|
1897
|
-
return Array.from(formData)
|
|
1898
|
-
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(this.stringifyFormDataValue(value))}`)
|
|
1899
|
-
.join('\n');
|
|
1900
|
-
}
|
|
1901
|
-
isBodylessMethod(method) {
|
|
1902
|
-
return method === 'GET' || method === 'HEAD';
|
|
1903
|
-
}
|
|
1904
|
-
appendQueryString(action, params) {
|
|
1905
|
-
const query = params.toString();
|
|
1906
|
-
if (!query)
|
|
1907
|
-
return action;
|
|
1908
|
-
const separator = action.includes('?') ? '&' : '?';
|
|
1909
|
-
return `${action}${separator}${query}`;
|
|
1910
|
-
}
|
|
1911
|
-
findFieldByName(name) {
|
|
1912
|
-
const fields = Array.from(this.$el.querySelectorAll('input, textarea, select'));
|
|
1913
|
-
return fields.find($field => $field.name === name) || null;
|
|
1914
|
-
}
|
|
1915
|
-
parseResponseBody(response) {
|
|
1916
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
1917
|
-
try {
|
|
1918
|
-
const responseBody = yield response.json();
|
|
1919
|
-
return responseBody && typeof responseBody === 'object'
|
|
1920
|
-
? responseBody
|
|
1921
|
-
: {
|
|
1922
|
-
success: false,
|
|
1923
|
-
error: true,
|
|
1924
|
-
'error-msg': 'Некорректный ответ сервера',
|
|
1925
|
-
};
|
|
1926
|
-
}
|
|
1927
|
-
catch (_a) {
|
|
1928
|
-
return {
|
|
1929
|
-
success: false,
|
|
1930
|
-
error: true,
|
|
1931
|
-
'error-msg': 'Некорректный ответ сервера',
|
|
1932
|
-
};
|
|
1933
|
-
}
|
|
1934
|
-
});
|
|
1935
|
-
}
|
|
1936
|
-
handleUnsuccessfulResponse(responseBody) {
|
|
1937
|
-
if (responseBody.error !== true)
|
|
1938
|
-
return;
|
|
1939
|
-
if (responseBody['error-msg']) {
|
|
1940
|
-
this.setErrorRecord(FORM_ERROR_FIELD, 'server', responseBody['error-msg'], 'server');
|
|
1941
|
-
this.showErrorForm(responseBody['error-msg']);
|
|
1942
|
-
}
|
|
1943
|
-
if (responseBody.errors) {
|
|
1944
|
-
const fieldsList = [];
|
|
1945
|
-
responseBody.errors.forEach(error => {
|
|
1946
|
-
const $field = this.findFieldByName(error.name);
|
|
1947
|
-
if ($field) {
|
|
1948
|
-
fieldsList.push($field);
|
|
1949
|
-
this.setValidationError($field, 'server', error['error-msg'], 'server');
|
|
1950
|
-
this.showError($field, error['error-msg']);
|
|
1951
|
-
$field.removeAttribute('aria-busy');
|
|
1952
|
-
this.setFieldValidationState($field, 'invalid');
|
|
1953
|
-
}
|
|
1954
|
-
else {
|
|
1955
|
-
this.setErrorRecord(error.name, 'server', error['error-msg'], 'server');
|
|
1956
|
-
console.warn(`Не найдено поле с именем: ${error.name}, для вывода ошибки: ${error['error-msg']}`);
|
|
1957
|
-
}
|
|
1958
|
-
});
|
|
1959
|
-
if (this.config.scrollToFirstErroredInput === true)
|
|
1960
|
-
this.scrollToFirstErroredInput(fieldsList);
|
|
1961
|
-
if (this.config.focusFirstErroredInput === true)
|
|
1962
|
-
this.focusFirstErroredInput(fieldsList);
|
|
1963
|
-
}
|
|
1964
|
-
this.syncErrorSummary();
|
|
1965
|
-
}
|
|
1966
|
-
sendData() {
|
|
1967
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
1968
|
-
var _a;
|
|
1969
|
-
const action = this.$el.getAttribute('action') || '';
|
|
1970
|
-
const method = ((_a = this.$el.getAttribute('method')) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || 'POST';
|
|
1971
|
-
const enctype = this.$el.getAttribute('enctype') || 'application/x-www-form-urlencoded';
|
|
1972
|
-
let body = null;
|
|
1973
|
-
let headers = {};
|
|
1974
|
-
// Сбор данных из формы
|
|
1975
|
-
const formData = serializeToFormData(this.$el);
|
|
1976
|
-
let data = this.formDataToObject(formData);
|
|
1977
|
-
if (this.config.logging) {
|
|
1978
|
-
for (const key of formData.keys())
|
|
1979
|
-
console.log(formData.get(key));
|
|
1980
|
-
}
|
|
1981
|
-
// Применение обёртки, если указано в настройках
|
|
1982
|
-
const hasDataWrapper = typeof this.config.wrapData === 'function';
|
|
1983
|
-
if (typeof this.config.wrapData === 'function') {
|
|
1984
|
-
data = this.config.wrapData(data);
|
|
1985
|
-
}
|
|
1986
|
-
const requestFormData = hasDataWrapper ? this.objectToFormData(data) : formData;
|
|
1987
|
-
if (this.isBodylessMethod(method)) {
|
|
1988
|
-
return fetch(this.appendQueryString(action, this.formDataToUrlSearchParams(requestFormData)), {
|
|
1989
|
-
method,
|
|
1990
|
-
headers,
|
|
1991
|
-
});
|
|
1992
|
-
}
|
|
1993
|
-
switch (enctype) {
|
|
1994
|
-
case 'application/x-www-form-urlencoded':
|
|
1995
|
-
body = this.formDataToUrlSearchParams(requestFormData).toString();
|
|
1996
|
-
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
1997
|
-
break;
|
|
1998
|
-
case 'multipart/form-data':
|
|
1999
|
-
body = requestFormData;
|
|
2000
|
-
// ВАЖНО: Не указываем Content-Type, т.к. браузер сам добавит с корректной границей!
|
|
2001
|
-
break;
|
|
2002
|
-
case 'text/plain':
|
|
2003
|
-
body = this.formDataToTextBody(requestFormData);
|
|
2004
|
-
headers['Content-Type'] = 'text/plain';
|
|
2005
|
-
break;
|
|
2006
|
-
case 'application/json':
|
|
2007
|
-
body = JSON.stringify(data);
|
|
2008
|
-
headers['Content-Type'] = 'application/json';
|
|
2009
|
-
break;
|
|
2010
|
-
default:
|
|
2011
|
-
console.warn(`Неизвестный enctype: ${enctype}. Используется application/x-www-form-urlencoded.`);
|
|
2012
|
-
body = this.formDataToUrlSearchParams(requestFormData).toString();
|
|
2013
|
-
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
2014
|
-
}
|
|
2015
|
-
return fetch(action, {
|
|
2016
|
-
method,
|
|
2017
|
-
body,
|
|
2018
|
-
headers,
|
|
2019
|
-
});
|
|
2020
|
-
});
|
|
2021
|
-
}
|
|
2022
|
-
submit() {
|
|
2023
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
2024
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
2025
|
-
if (!this.waitResponse) {
|
|
2026
|
-
let loadingFinished = false;
|
|
2027
|
-
const finishLoading = () => {
|
|
2028
|
-
if (loadingFinished)
|
|
2029
|
-
return;
|
|
2030
|
-
this.waitResponse = false;
|
|
2031
|
-
if (this.config.showLoaderButton === true)
|
|
2032
|
-
this.hideLoader();
|
|
2033
|
-
loadingFinished = true;
|
|
2034
|
-
};
|
|
2035
|
-
try {
|
|
2036
|
-
const beforeSubmitResult = yield ((_b = (_a = this.config).onBeforeSubmit) === null || _b === void 0 ? void 0 : _b.call(_a, this));
|
|
2037
|
-
if (beforeSubmitResult === false) {
|
|
2038
|
-
return {
|
|
2039
|
-
success: false,
|
|
2040
|
-
};
|
|
2041
|
-
}
|
|
2042
|
-
if (this.config.showLoaderButton === true)
|
|
2043
|
-
this.showLoader();
|
|
2044
|
-
(_d = (_c = this.config).onSubmit) === null || _d === void 0 ? void 0 : _d.call(_c, this);
|
|
2045
|
-
this.waitResponse = true;
|
|
2046
|
-
const response = yield this.sendData();
|
|
2047
|
-
const responseBody = yield this.parseResponseBody(response);
|
|
2048
|
-
finishLoading();
|
|
2049
|
-
(_f = (_e = this.config).onResponse) === null || _f === void 0 ? void 0 : _f.call(_e, responseBody, this);
|
|
2050
|
-
if (response.ok && responseBody.success === true) {
|
|
2051
|
-
(_h = (_g = this.config).onResponseSuccess) === null || _h === void 0 ? void 0 : _h.call(_g, responseBody, this);
|
|
2052
|
-
}
|
|
2053
|
-
else {
|
|
2054
|
-
(_k = (_j = this.config).onResponseUnsuccess) === null || _k === void 0 ? void 0 : _k.call(_j, responseBody, this);
|
|
2055
|
-
this.handleUnsuccessfulResponse(responseBody);
|
|
2056
|
-
}
|
|
2057
|
-
parseCommonResponseProperties(responseBody);
|
|
2058
|
-
return {
|
|
2059
|
-
success: response.ok && responseBody.success === true,
|
|
2060
|
-
response,
|
|
2061
|
-
responseBody,
|
|
2062
|
-
};
|
|
2063
|
-
}
|
|
2064
|
-
catch (error) {
|
|
2065
|
-
finishLoading();
|
|
2066
|
-
(_m = (_l = this.config).onSubmitError) === null || _m === void 0 ? void 0 : _m.call(_l, error, this);
|
|
2067
|
-
throw error;
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
return undefined;
|
|
2071
|
-
});
|
|
2072
|
-
}
|
|
1751
|
+
if (this.config.showLoaderButton === true) this.hideLoader();
|
|
1752
|
+
loadingFinished = true;
|
|
1753
|
+
};
|
|
1754
|
+
try {
|
|
1755
|
+
const beforeSubmitResult = await ((_b = (_a2 = this.config).onBeforeSubmit) == null ? void 0 : _b.call(_a2, this));
|
|
1756
|
+
if (beforeSubmitResult === false) {
|
|
1757
|
+
return {
|
|
1758
|
+
success: false
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
if (this.config.showLoaderButton === true) this.showLoader();
|
|
1762
|
+
(_d = (_c = this.config).onSubmit) == null ? void 0 : _d.call(_c, this);
|
|
1763
|
+
this.waitResponse = true;
|
|
1764
|
+
const response = await this.sendData();
|
|
1765
|
+
const responseBody = await this.parseResponseBody(response);
|
|
1766
|
+
finishLoading();
|
|
1767
|
+
(_f = (_e = this.config).onResponse) == null ? void 0 : _f.call(_e, responseBody, this);
|
|
1768
|
+
if (response.ok && responseBody.success === true) {
|
|
1769
|
+
(_h = (_g = this.config).onResponseSuccess) == null ? void 0 : _h.call(_g, responseBody, this);
|
|
1770
|
+
} else {
|
|
1771
|
+
(_j = (_i = this.config).onResponseUnsuccess) == null ? void 0 : _j.call(_i, responseBody, this);
|
|
1772
|
+
this.handleUnsuccessfulResponse(responseBody);
|
|
1773
|
+
}
|
|
1774
|
+
parseCommonResponseProperties(responseBody);
|
|
1775
|
+
return {
|
|
1776
|
+
success: response.ok && responseBody.success === true,
|
|
1777
|
+
response,
|
|
1778
|
+
responseBody
|
|
1779
|
+
};
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
finishLoading();
|
|
1782
|
+
(_l = (_k = this.config).onSubmitError) == null ? void 0 : _l.call(_k, error, this);
|
|
1783
|
+
throw error;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
return void 0;
|
|
1787
|
+
}
|
|
2073
1788
|
}
|
|
2074
1789
|
|
|
2075
1790
|
export { FORM_ERROR_FIELD, blockScrollBody, closest, createFieldValidator, createFormValidator, createLengthValidator, createPatternValidator, createSchemaValidator, dateOrder, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerFieldValidator, registerSchemaValidator, registerValidator, requiredIf, sameAsField, serializeFormToJSON, serializeToFormData, unblockScrollBody };
|