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/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
- const date = new Date(dateValue);
55
- /**
56
- * Добавляет ведущие нули.
57
- *
58
- * @param {number} num
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
- if ($el == null)
78
- return null;
79
- let parent = $el;
80
- while (parent) {
81
- if (parent.matches(selector)) {
82
- return parent;
83
- }
84
- parent = parent.parentElement;
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
- window.lastBlockScrollPosition = window.pageYOffset;
91
- document.body.style.top = `${-window.lastBlockScrollPosition}px`;
92
- document.body.classList.add('body--block-scroll');
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
- document.body.classList.remove('body--block-scroll');
97
- document.body.style.top = '';
98
- window.scrollTo(0, window.lastBlockScrollPosition || 0);
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
- return /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu.test(value);
31
+ return /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu.test(
32
+ value
33
+ );
109
34
  }
110
- const HTTP_PROTOCOLS = ['http:', 'https:'];
35
+ const HTTP_PROTOCOLS = ["http:", "https:"];
111
36
  function getNormalizedUrlString(originalUrlString) {
112
- const trimmedUrl = originalUrlString.trim();
113
- if (trimmedUrl.length === 0)
114
- return '';
115
- if (trimmedUrl.startsWith('//')) {
116
- return `https:${trimmedUrl}`;
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
- const parts = hostname.split('.');
122
- if (parts.length !== 4)
123
- return false;
124
- return parts.every(part => {
125
- if (!/^\d{1,3}$/.test(part))
126
- return false;
127
- const num = Number(part);
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
- if (!hostname.startsWith('[') || !hostname.endsWith(']'))
133
- return false;
134
- const normalizedHostname = hostname.slice(1, -1);
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
- return label.length > 0 && label.length <= 63 && /^[a-z\d-]+$/i.test(label) && !label.startsWith('-') && !label.endsWith('-');
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
- const labels = hostname.split('.');
142
- return labels.length >= 2 && labels.every(isValidDomainLabel);
62
+ const labels = hostname.split(".");
63
+ return labels.length >= 2 && labels.every(isValidDomainLabel);
143
64
  }
144
65
  function isAllowedHostname(hostname) {
145
- if (!hostname)
146
- return false;
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
- const urlString = getNormalizedUrlString(originalUrlString);
158
- if (urlString.length === 0)
159
- return false;
160
- try {
161
- const url = new URL(urlString);
162
- return HTTP_PROTOCOLS.includes(url.protocol) && isAllowedHostname(url.hostname);
163
- }
164
- catch (_a) {
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
- const numbersArray = [];
177
- Array.from(value).forEach(char => {
178
- if (!!Number(char) || Number(char) === 0)
179
- numbersArray.push(char);
180
- });
181
- if (numbersArray.length < 11 || (numbersArray.length < 12 && value[1] === '+'))
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
- const elements = $element.tagName === 'FORM'
194
- ? Array.from($element.elements)
195
- : Array.from($element.querySelectorAll('input, select, textarea, button'));
196
- const data = new FormData();
197
- elements.forEach(element => {
198
- if (!(element instanceof HTMLInputElement ||
199
- element instanceof HTMLSelectElement ||
200
- element instanceof HTMLTextAreaElement ||
201
- element instanceof HTMLButtonElement)) {
202
- return;
203
- }
204
- if (!element.name || element.disabled || element.hasAttribute('data-no-serialize')) {
205
- return;
206
- }
207
- if (element instanceof HTMLButtonElement) {
208
- return;
209
- }
210
- const { name, type } = element;
211
- if (type === 'file') {
212
- if (element.multiple) {
213
- const files = element.files;
214
- if (files) {
215
- for (let i = 0; i < files.length; i++) {
216
- // Добавляем каждый файл с уникальным ключом
217
- data.append(`${name}[${i}]`, files[i]);
218
- }
219
- }
220
- }
221
- else {
222
- const files = element.files;
223
- if (files && files[0])
224
- data.append(name, files[0]);
225
- }
226
- }
227
- else {
228
- let value;
229
- if (type === 'select-one' || type === 'select-multiple') {
230
- // Обработка select элементов
231
- const select = element;
232
- if (type === 'select-multiple') {
233
- const selectedOptions = Array.from(select.selectedOptions).map(option => option.value);
234
- selectedOptions.forEach((val, index) => {
235
- data.append(`${name}[${index}]`, val);
236
- });
237
- }
238
- else {
239
- value = select.value;
240
- if (value)
241
- data.append(name, value);
242
- }
243
- }
244
- else if (type === 'tel') {
245
- value = element.value
246
- .split(/[\s()-]/)
247
- .join('')
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
- if (Object.prototype.hasOwnProperty.call(responseBody, 'redirect-url')) {
276
- if (responseBody['redirect-url-delay']) {
277
- const redirectUrlDelay = Number(responseBody['redirect-url-delay']);
278
- setTimeout(() => {
279
- window.location.href = responseBody['redirect-url'];
280
- }, redirectUrlDelay);
281
- }
282
- else {
283
- window.location.href = responseBody['redirect-url'];
284
- }
285
- }
286
- else if (Object.prototype.hasOwnProperty.call(responseBody, 'reload')) {
287
- if (responseBody.reload === true) {
288
- if (responseBody['reload-delay']) {
289
- const reloadDelay = Number(responseBody['reload-delay']);
290
- setTimeout(() => {
291
- window.location.reload();
292
- }, reloadDelay);
293
- }
294
- else {
295
- window.location.reload();
296
- }
297
- }
298
- }
299
- // Не показываем тост если делается редирект или обновление страницы (чтобы небыло вспышек) - в этом случае тосты обрабытываются в ответе onResponseSuccess | onResponseUnsuccess
300
- else {
301
- if (Object.prototype.hasOwnProperty.call(responseBody, 'toast')) {
302
- const toasts = responseBody['toast'];
303
- const normalizedToasts = Array.isArray(toasts) ? toasts : [toasts];
304
- normalizedToasts.forEach(toast => {
305
- window.showToast(toast);
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
- const obj = {};
323
- const formData = new FormData(form);
324
- formData.forEach((value, key) => {
325
- if (obj.hasOwnProperty(key)) {
326
- if (!Array.isArray(obj[key])) {
327
- obj[key] = [obj[key]];
328
- }
329
- obj[key].push(value);
330
- }
331
- else {
332
- obj[key] = value;
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
- /* ---------- singleton-storage на globalThis ---------- */
340
- const GLOBAL_KEY = Symbol.for('FormFather.validators');
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
- if (validators.has(name) && !override) {
345
- console.warn(`[FormFather] Validator "${name}" already exists; pass { override: true } to replace`);
346
- return;
347
- }
348
- validators.set(name, { fn, defaultMessage });
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
- return new Map(validators); // копию, чтобы никто не мутировал оригинал
205
+ return new Map(validators);
353
206
  }
354
- /* ---------- built-in rules ---------- */
355
207
  function registerBuiltinValidator(name, fn, defaultMessage) {
356
- if (!validators.has(name)) {
357
- validators.set(name, { fn, defaultMessage });
358
- }
208
+ if (!validators.has(name)) {
209
+ validators.set(name, { fn, defaultMessage });
210
+ }
359
211
  }
360
- registerBuiltinValidator('required', (value, $input, $form) => {
212
+ registerBuiltinValidator(
213
+ "required",
214
+ (value, $input, $form) => {
361
215
  if ($input && $input.type) {
362
- if ($input.type === 'checkbox') {
363
- return $input.checked;
364
- }
365
- if ($input.type === 'radio') {
366
- const group = $form.querySelectorAll(`input[type="radio"][name="${$input.name}"]`);
367
- return Array.from(group).some(el => el.checked);
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
- registerBuiltinValidator('email', v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), 'Неверный формат');
374
- registerBuiltinValidator('tel', v => /^\+7\d{10}$/.test(v), 'Неверный формат');
375
- registerBuiltinValidator('url', v => isUrlValid(v), 'Неверный формат');
376
- registerBuiltinValidator('not-numbers', v => !/[0-9]/.test(v), 'Неверный формат');
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
- var _a;
380
- if (!error || typeof error !== 'object')
381
- return undefined;
382
- const issues = error.issues || error.errors;
383
- if (Array.isArray(issues)) {
384
- const firstMessage = (_a = issues.find(issue => typeof (issue === null || issue === void 0 ? void 0 : issue.message) === 'string')) === null || _a === void 0 ? void 0 : _a.message;
385
- if (firstMessage)
386
- return firstMessage;
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
- if (typeof message === 'function')
392
- return message(error);
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
- if (typeof result === 'object' && result !== null)
397
- return result;
398
- if (result === undefined)
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
- return (value, $input, $form, params) => __awaiter(this, void 0, void 0, function* () {
404
- const mappedValue = options.mapValue ? options.mapValue(value, $input, $form, params) : value;
405
- try {
406
- if (typeof schema === 'function') {
407
- return normalizeResult(yield schema(mappedValue), resolveMessage(undefined, options.message));
408
- }
409
- if (typeof schema.safeParse === 'function') {
410
- const result = yield schema.safeParse(mappedValue);
411
- if (result.success)
412
- return true;
413
- return { valid: false, message: resolveMessage(result.error || result, options.message) };
414
- }
415
- if (typeof schema.parse === 'function') {
416
- yield schema.parse(mappedValue);
417
- return true;
418
- }
419
- }
420
- catch (error) {
421
- return { valid: false, message: resolveMessage(error, options.message) };
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
- const { override } = options, adapterOptions = __rest(options, ["override"]);
428
- registerValidator(name, createSchemaValidator(schema, adapterOptions), defaultMessage, { override });
275
+ const { override, ...adapterOptions } = options;
276
+ registerValidator(name, createSchemaValidator(schema, adapterOptions), defaultMessage, { override });
429
277
  }
430
278
  function registerFieldValidator(name, predicate, defaultMessage, options = {}) {
431
- registerValidator(name, createFieldValidator(predicate), defaultMessage, options);
279
+ registerValidator(name, createFieldValidator(predicate), defaultMessage, options);
432
280
  }
433
281
  function createFieldValidator(predicate, message) {
434
- return (value, $input, $form, params) => __awaiter(this, void 0, void 0, function* () {
435
- const valid = yield predicate(value, $input, $form, params);
436
- return valid ? true : { valid: false, message };
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
- return createFieldValidator(value => {
441
- pattern.lastIndex = 0;
442
- return pattern.test(value);
443
- }, message);
288
+ return createFieldValidator((value) => {
289
+ pattern.lastIndex = 0;
290
+ return pattern.test(value);
291
+ }, message);
444
292
  }
445
293
  function createLengthValidator(options, message) {
446
- return createFieldValidator(value => {
447
- const normalizedValue = options.trim === false ? value : value.trim();
448
- if (normalizedValue.length === 0 && options.emptyIsValid === true)
449
- return true;
450
- if (typeof options.min === 'number' && normalizedValue.length < options.min)
451
- return false;
452
- if (typeof options.max === 'number' && normalizedValue.length > options.max)
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
- // --- shared cross-file state (singleton via globalThis) ---
460
- const FORM_GLOBAL_KEY = Symbol.for('formfather.shared');
461
- const __shared =
462
- // уже существует? используем его
463
- (_a = globalThis[FORM_GLOBAL_KEY]) !== null && _a !== void 0 ? _a :
464
- // иначе создаём
465
- (globalThis[FORM_GLOBAL_KEY] = {
466
- defaultParams: {},
467
- defaultValidationSchema: undefined,
468
- });
469
- const FORM_ERROR_FIELD = '_form';
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
- return values[field];
317
+ return values[field];
472
318
  }
473
319
  function isEmptyFormValue(value) {
474
- if (Array.isArray(value))
475
- return value.length === 0;
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
- return __awaiter(this, void 0, void 0, function* () {
480
- const resolvedIssue = typeof issue === 'function' ? yield issue(context) : issue;
481
- if (typeof resolvedIssue === 'string') {
482
- return {
483
- field: fallbackField,
484
- rule: fallbackRule,
485
- message: resolvedIssue,
486
- };
487
- }
488
- return Object.assign({ field: fallbackField, rule: fallbackRule }, resolvedIssue);
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
- return (context) => __awaiter(this, void 0, void 0, function* () {
493
- const valid = yield predicate(context);
494
- return valid ? true : resolveFormValidatorIssue(context, issue);
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 = 'Значения не совпадают', rule = 'same-as-field') {
498
- return createFormValidator(({ values }) => readFormValue(values, field) === readFormValue(values, otherField), { field, rule, message });
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 = 'Поле обязательно', rule = 'conditional-required') {
501
- return createFormValidator((context) => __awaiter(this, void 0, void 0, function* () { return !(yield condition(context)) || !isEmptyFormValue(readFormValue(context.values, field)); }), { field, rule, message });
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 = 'Дата окончания должна быть позже даты начала', rule = 'date-order') {
504
- return createFormValidator(({ values }) => {
505
- const start = readFormValue(values, startField);
506
- const end = readFormValue(values, endField);
507
- if (isEmptyFormValue(start) || isEmptyFormValue(end))
508
- return true;
509
- return Date.parse(String(start)) <= Date.parse(String(end));
510
- }, { field: endField, rule, message });
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 = 'data-form-father-describedby-id';
516
- const ADDED_DESCRIBEDBY_ID_ATTRIBUTE = 'data-form-father-added-describedby-id';
517
- const ERROR_SUMMARY_ATTRIBUTE = 'data-form-father-summary';
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
- tel: {
521
- selector: 'input[type="tel"]',
522
- rules: ['tel'],
523
- },
524
- email: {
525
- selector: 'input[type="email"]',
526
- rules: ['email'],
527
- },
528
- required: {
529
- selector: '[required]',
530
- rules: ['required'],
531
- },
532
- url: {
533
- selector: 'input[type="url"]',
534
- rules: ['url'],
535
- },
536
- 'not-numbers': {
537
- selector: 'input[data-validate="not-numbers"]',
538
- rules: ['not-numbers'],
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
- static get defaultParams() {
548
- return __shared.defaultParams;
549
- }
550
- static set defaultParams(v) {
551
- __shared.defaultParams = v;
552
- }
553
- static get defaultValidationSchema() {
554
- var _a;
555
- return ((_a = __shared.defaultValidationSchema) !== null && _a !== void 0 ? _a : (__shared.defaultValidationSchema = INITIAL_DEFAULT_SCHEMA));
556
- }
557
- static set defaultValidationSchema(v) {
558
- __shared.defaultValidationSchema = v;
559
- }
560
- /**
561
- * Создать форму.
562
- *
563
- * @param {HTMLElement} $el - Элемент формы (тег form!).
564
- * @param {Object} options - Параметры формы.
565
- */
566
- constructor($el, options = {}) {
567
- this.$submits = [];
568
- this.waitResponse = false;
569
- this.inputs = null;
570
- this.fieldEventHandlers = [];
571
- this.liveValidationTimers = new WeakMap();
572
- this.fieldValidationTokens = new WeakMap();
573
- this.fieldValidationTokenCounter = 0;
574
- this.errors = [];
575
- /** Общие настройки анимаций (можно подправить под вкус) */
576
- this._anim = {
577
- duration: 220,
578
- easing: 'ease',
579
- paddingTopOpened: 16,
580
- iconDuration: 700,
581
- };
582
- this.$el = $el;
583
- this.options = options;
584
- const defaultConfig = {
585
- onSubmit: () => { },
586
- onResponse: () => { },
587
- onResponseSuccess: () => { },
588
- onResponseUnsuccess: () => { },
589
- customTypeError: null,
590
- loaderColor: '#fff',
591
- showLoaderButton: true,
592
- scrollToFirstErroredInput: true,
593
- focusFirstErroredInput: false,
594
- logging: false,
595
- inputSelector: '.input',
596
- inputWrapperSelector: '.input-primary',
597
- validateOn: 'submit',
598
- revalidateOn: ['input', 'change'],
599
- validationDebounce: 0,
600
- errorContainerAttribute: 'data-error-container',
601
- validationStateAttribute: 'data-form-father-state',
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
- return this;
656
- }
657
- /** Возвращает текущий список ошибок формы. */
658
- getErrors() {
659
- return this.errors.map(error => (Object.assign({}, error)));
660
- }
661
- /** Возвращает значения формы обычным объектом. */
662
- getValues() {
663
- return serializeFormToJSON(this.$el);
664
- }
665
- normalizeValueList(value) {
666
- return Array.isArray(value) ? value.map(String) : [String(value)];
667
- }
668
- /** Проставляет значения полей по `name` и уведомляет UI через `input`/`change` события. */
669
- setValues(values) {
670
- this.getAllFields().forEach($field => {
671
- if (!$field.name || !Object.prototype.hasOwnProperty.call(values, $field.name))
672
- return;
673
- const value = values[$field.name];
674
- if ($field instanceof HTMLInputElement && $field.type === 'file')
675
- return;
676
- if ($field instanceof HTMLInputElement && $field.type === 'checkbox') {
677
- if (typeof value === 'boolean') {
678
- $field.checked = value;
679
- }
680
- else {
681
- $field.checked = this.normalizeValueList(value).includes($field.value);
682
- }
683
- }
684
- else if ($field instanceof HTMLInputElement && $field.type === 'radio') {
685
- $field.checked = String(value) === $field.value;
686
- }
687
- else if ($field instanceof HTMLSelectElement && $field.multiple) {
688
- const valuesList = this.normalizeValueList(value);
689
- Array.from($field.options).forEach(option => {
690
- option.selected = valuesList.includes(option.value);
691
- });
692
- }
693
- else {
694
- $field.value = value == null ? '' : String(value);
695
- }
696
- $field.dispatchEvent(new Event('input', { bubbles: true }));
697
- $field.dispatchEvent(new Event('change', { bubbles: true }));
698
- });
699
- return this;
700
- }
701
- /** Снимает все клиентские и серверные ошибки с формы, не меняя значения полей. */
702
- clearErrors() {
703
- this.errors = [];
704
- this.getAllFields().forEach($field => {
705
- this.hideError($field);
706
- this.clearFieldValidationState($field);
707
- });
708
- this.hideErrorForm();
709
- this.hideErrorSummary();
710
- return this;
711
- }
712
- isErrorResponse(issue) {
713
- return (!!issue &&
714
- typeof issue === 'object' &&
715
- typeof issue.name === 'string' &&
716
- typeof issue['error-msg'] === 'string');
717
- }
718
- normalizeValidationIssues(issues, source) {
719
- if (issues == null || issues === true)
720
- return [];
721
- if (issues === false)
722
- return [{ message: 'Некорректные данные формы', source }];
723
- if (typeof issues === 'string')
724
- return [{ message: issues, source }];
725
- if (Array.isArray(issues)) {
726
- return issues.flatMap(issue => this.normalizeValidationIssues(issue, source));
727
- }
728
- if (this.isErrorResponse(issues)) {
729
- return [
730
- {
731
- field: issues.name,
732
- rule: source,
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
- scheduleFieldValidation($input) {
900
- const currentTimer = this.liveValidationTimers.get($input);
901
- if (currentTimer)
902
- clearTimeout(currentTimer);
903
- const delay = Math.max(0, Number(this.config.validationDebounce) || 0);
904
- const run = () => {
905
- void this.validateField($input);
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
- if (delay === 0) {
908
- run();
909
- return;
910
- }
911
- const timer = setTimeout(run, delay);
912
- this.liveValidationTimers.set($input, timer);
913
- }
914
- setupMutationObserver() {
915
- var _a;
916
- (_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
917
- this.mutationObserver = undefined;
918
- if (!this.config.observeMutations || typeof MutationObserver === 'undefined')
919
- return;
920
- this.mutationObserver = new MutationObserver(() => {
921
- this.refreshControls();
922
- this.bindSubmitHandlers();
923
- this.bindFieldValidationEvents();
924
- });
925
- this.mutationObserver.observe(this.$el, {
926
- childList: true,
927
- subtree: true,
928
- });
929
- }
930
- getErrorContainer($input) {
931
- const attrName = this.config.errorContainerAttribute || 'data-error-container';
932
- const selector = $input.getAttribute(attrName);
933
- if (!selector)
934
- return null;
935
- try {
936
- return document.querySelector(selector);
937
- }
938
- catch (_a) {
939
- console.warn(`[FormFather] Invalid error container selector "${selector}"`);
940
- return null;
941
- }
942
- }
943
- toSafeIdPart(value) {
944
- return (value
945
- .trim()
946
- .toLowerCase()
947
- .replace(/[^a-z0-9_-]+/gi, '-')
948
- .replace(/^-+|-+$/g, '') || 'field');
949
- }
950
- createErrorId($input) {
951
- const prefix = this.config.errorIdPrefix || 'form-father-error';
952
- return `${this.toSafeIdPart(prefix)}-${this.toSafeIdPart(this.getFieldKey($input))}-${++errorIdCounter}`;
953
- }
954
- ensureElementId($input, $error) {
955
- if (!$error.id) {
956
- $error.id = $input.getAttribute(ERROR_DESCRIBEDBY_ID_ATTRIBUTE) || this.createErrorId($input);
957
- }
958
- return $error.id;
959
- }
960
- addErrorDescription($input, $error) {
961
- if (!$error.hasAttribute('role'))
962
- $error.setAttribute('role', 'alert');
963
- const errorId = this.ensureElementId($input, $error);
964
- $input.setAttribute(ERROR_DESCRIBEDBY_ID_ATTRIBUTE, errorId);
965
- if (this.config.ariaDescribeErrors === false)
966
- return;
967
- const currentIds = ($input.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
968
- if (!currentIds.includes(errorId)) {
969
- $input.setAttribute('aria-describedby', [...currentIds, errorId].join(' '));
970
- $input.setAttribute(ADDED_DESCRIBEDBY_ID_ATTRIBUTE, errorId);
971
- }
972
- }
973
- removeErrorDescription($input) {
974
- const addedId = $input.getAttribute(ADDED_DESCRIBEDBY_ID_ATTRIBUTE);
975
- if (addedId) {
976
- const nextIds = ($input.getAttribute('aria-describedby') || '')
977
- .split(/\s+/)
978
- .filter(id => id && id !== addedId);
979
- if (nextIds.length > 0) {
980
- $input.setAttribute('aria-describedby', nextIds.join(' '));
981
- }
982
- else {
983
- $input.removeAttribute('aria-describedby');
984
- }
985
- }
986
- $input.removeAttribute(ERROR_DESCRIBEDBY_ID_ATTRIBUTE);
987
- $input.removeAttribute(ADDED_DESCRIBEDBY_ID_ATTRIBUTE);
988
- }
989
- /**
990
- * Показывает ошибку для поля ввода.
991
- *
992
- * @param {HTMLInputElement} $input - Поле ввода.
993
- * @param {String} text - Текст ошибки.
994
- */
995
- showError($input, text) {
996
- $input.setAttribute('aria-invalid', 'true');
997
- const $errorContainer = this.getErrorContainer($input);
998
- if ($errorContainer) {
999
- $errorContainer.textContent = text;
1000
- this.addErrorDescription($input, $errorContainer);
1001
- $errorContainer.removeAttribute('hidden');
1002
- return;
1003
- }
1004
- const $inputWrapper = closest($input, this.config.inputWrapperSelector);
1005
- if (!$inputWrapper) {
1006
- console.warn(`Не найдена обёртка для поля с именем: ${$input.name || $input.type}`);
1007
- return;
1008
- }
1009
- const customShowError = $inputWrapper.showError;
1010
- if (typeof customShowError === 'function') {
1011
- customShowError.call($inputWrapper, text);
1012
- const $customError = $inputWrapper.querySelector('[data-form-father-error]');
1013
- if ($customError)
1014
- this.addErrorDescription($input, $customError);
1015
- return;
1016
- }
1017
- $inputWrapper.classList.add('input__wrapper--error');
1018
- let $error = $inputWrapper.querySelector('[data-form-father-error]');
1019
- if (!$error) {
1020
- $error = document.createElement('div');
1021
- $error.setAttribute('data-form-father-error', '');
1022
- $error.setAttribute('role', 'alert');
1023
- $input.insertAdjacentElement('afterend', $error);
1024
- }
1025
- $error.textContent = text;
1026
- this.addErrorDescription($input, $error);
1027
- }
1028
- /** Показывает лоадер */
1029
- showLoader() {
1030
- this.$submits.forEach($submit => {
1031
- $submit.classList.add('button--loading');
1032
- if ($submit.querySelector('.button__loader'))
1033
- return;
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
- const $loaderEl = document.createElement('span');
1053
- $loaderEl.innerHTML = loaderSvg;
1054
- $loaderEl.className = 'button__loader';
1055
- $submit.insertAdjacentElement('afterbegin', $loaderEl);
1056
- requestAnimationFrame(() => {
1057
- $loaderEl.setAttribute('data-showed', '');
1058
- });
1059
- });
1060
- }
1061
- /** Скрывает лоадер */
1062
- hideLoader() {
1063
- this.$submits.forEach($submit => {
1064
- $submit.classList.remove('button--loading');
1065
- const $loader = $submit.querySelector('.button__loader');
1066
- if ($loader) {
1067
- $loader.classList.remove('active');
1068
- $loader.removeAttribute('data-showed');
1069
- setTimeout(() => {
1070
- $loader.remove();
1071
- }, 250);
1072
- }
1073
- });
1074
- }
1075
- /**
1076
- * Скрывает ошибку.
1077
- *
1078
- * @param {HTMLInputElement} $input - Поле ввода.
1079
- */
1080
- hideError($input) {
1081
- var _a;
1082
- const $inputWrapper = closest($input, this.config.inputWrapperSelector) || $input.parentElement;
1083
- const $errorContainer = this.getErrorContainer($input);
1084
- $input.removeAttribute('aria-invalid');
1085
- this.removeErrorDescription($input);
1086
- if ($errorContainer) {
1087
- $errorContainer.textContent = '';
1088
- $errorContainer.setAttribute('hidden', '');
1089
- }
1090
- (_a = $inputWrapper === null || $inputWrapper === void 0 ? void 0 : $inputWrapper.querySelector('[data-form-father-error]')) === null || _a === void 0 ? void 0 : _a.remove();
1091
- $inputWrapper === null || $inputWrapper === void 0 ? void 0 : $inputWrapper.classList.remove('input__wrapper--error');
1092
- if ($inputWrapper && $inputWrapper.classList.contains('input__wrapper')) {
1093
- $inputWrapper.getAttribute('data-type-error') || 'default';
1094
- }
1095
- }
1096
- getErrorSummaryOptions() {
1097
- if (!this.config.errorSummary)
1098
- return null;
1099
- return this.config.errorSummary === true ? {} : this.config.errorSummary;
1100
- }
1101
- getErrorSummaryContainer(options, create) {
1102
- if (options.selector) {
1103
- try {
1104
- return (this.$el.querySelector(options.selector) ||
1105
- document.querySelector(options.selector));
1106
- }
1107
- catch (_a) {
1108
- console.warn(`[FormFather] Invalid error summary selector "${options.selector}"`);
1109
- return null;
1110
- }
1111
- }
1112
- const existing = this.$el.querySelector(`[${ERROR_SUMMARY_ATTRIBUTE}]`);
1113
- if (existing || !create)
1114
- return existing;
1115
- const $summary = document.createElement('div');
1116
- $summary.setAttribute(ERROR_SUMMARY_ATTRIBUTE, '');
1117
- this.$el.insertAdjacentElement('afterbegin', $summary);
1118
- return $summary;
1119
- }
1120
- prepareErrorSummaryContainer($summary) {
1121
- $summary.setAttribute(ERROR_SUMMARY_ATTRIBUTE, '');
1122
- if (!$summary.hasAttribute('role'))
1123
- $summary.setAttribute('role', 'alert');
1124
- if (!$summary.hasAttribute('tabindex'))
1125
- $summary.setAttribute('tabindex', '-1');
1126
- $summary.removeAttribute('hidden');
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
- getRuleMessage($input, rule, messages, defaultMessage, overrideMessage) {
1369
- const attrMessage = $input.getAttribute(`data-error-${rule}`);
1370
- return overrideMessage || attrMessage || messages[rule] || defaultMessage;
942
+ }
943
+ focusFieldFromSummary($field) {
944
+ if (typeof $field.scrollIntoView === "function") {
945
+ $field.scrollIntoView({ behavior: "smooth", block: "center" });
1371
946
  }
1372
- fieldMatchesSchemaDefinition($input, key, def) {
1373
- if (def.selector) {
1374
- try {
1375
- return $input.matches(def.selector);
1376
- }
1377
- catch (_a) {
1378
- console.warn(`[FormFather] Invalid validation selector "${def.selector}"`);
1379
- return false;
1380
- }
1381
- }
1382
- return this.parseValidationAttribute($input.getAttribute('data-validate')).some(rule => this.getRuleName(rule) === key);
1383
- }
1384
- getFieldValidationConfig($input, schema = this.getValidationSchema()) {
1385
- const rules = [];
1386
- const messages = {};
1387
- for (const [key, def] of Object.entries(schema)) {
1388
- if (!this.fieldMatchesSchemaDefinition($input, key, def))
1389
- continue;
1390
- rules.push(...def.rules);
1391
- Object.assign(messages, def.messages || {});
1392
- }
1393
- return {
1394
- rules: this.collectRules($input, rules, new Set(Object.keys(schema))),
1395
- messages,
1396
- };
1397
- }
1398
- getValidationFields($block, schema) {
1399
- const fields = new Set();
1400
- const addFields = (nodeList) => {
1401
- Array.from(nodeList).forEach($input => {
1402
- if ($input instanceof HTMLInputElement ||
1403
- $input instanceof HTMLTextAreaElement ||
1404
- $input instanceof HTMLSelectElement) {
1405
- fields.add($input);
1406
- }
1407
- });
1408
- };
1409
- for (const [key, def] of Object.entries(schema)) {
1410
- if (def.selector) {
1411
- try {
1412
- addFields($block.querySelectorAll(def.selector));
1413
- }
1414
- catch (_a) {
1415
- console.warn(`[FormFather] Invalid validation selector "${def.selector}"`);
1416
- }
1417
- continue;
1418
- }
1419
- this.getAllFields($block)
1420
- .filter($input => this.parseValidationAttribute($input.getAttribute('data-validate')).some(rule => this.getRuleName(rule) === key))
1421
- .forEach($input => fields.add($input));
1422
- }
1423
- addFields($block.querySelectorAll('[required], [data-validate], [data-custom-validate]'));
1424
- return Array.from(fields);
1425
- }
1426
- validateInput($input_1, rules_1) {
1427
- return __awaiter(this, arguments, void 0, function* ($input, rules, messages = {}, $block = this.$el, validationToken) {
1428
- if ($input.hasAttribute('data-no-validate'))
1429
- return true;
1430
- const isCheckable = $input instanceof HTMLInputElement && ($input.type === 'checkbox' || $input.type === 'radio');
1431
- const empty = isCheckable ? !$input.checked : $input.value.trim().length === 0;
1432
- const hasRequiredRule = rules.some(rule => this.getRuleName(rule) === 'required');
1433
- if (empty && !hasRequiredRule) {
1434
- if (!this.isLatestFieldValidation($input, validationToken))
1435
- return !this.hasFieldError($input);
1436
- this.clearFieldErrorRecord($input);
1437
- this.hideError($input);
1438
- return true;
1439
- }
1440
- for (const validationRule of rules) {
1441
- const { rule, params } = typeof validationRule === 'string'
1442
- ? { rule: validationRule, params: undefined }
1443
- : { rule: validationRule.rule, params: validationRule.params };
1444
- const validator = getValidator(rule);
1445
- if (!validator)
1446
- continue;
1447
- const raw = yield validator.fn($input.value, $input, $block, params);
1448
- const isObjectResult = typeof raw === 'object' && raw !== null;
1449
- const passed = isObjectResult ? raw.valid === true : !!raw;
1450
- if (!this.isLatestFieldValidation($input, validationToken))
1451
- return !this.hasFieldError($input);
1452
- if (isObjectResult && raw.effect) {
1453
- yield raw.effect({
1454
- value: $input.value,
1455
- $input,
1456
- $form: $block,
1457
- params,
1458
- });
1459
- }
1460
- if (!this.isLatestFieldValidation($input, validationToken))
1461
- return !this.hasFieldError($input);
1462
- if (!passed) {
1463
- const result = raw;
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
- runFieldValidation($input_1, rules_1) {
1481
- return __awaiter(this, arguments, void 0, function* ($input, rules, messages = {}, $block = this.$el) {
1482
- const validationToken = this.startFieldValidation($input);
1483
- try {
1484
- const isValid = yield this.validateInput($input, rules, messages, $block, validationToken);
1485
- this.finishFieldValidation($input, validationToken, isValid);
1486
- return isValid;
1487
- }
1488
- catch (error) {
1489
- if (this.isLatestFieldValidation($input, validationToken)) {
1490
- this.clearFieldValidationState($input);
1491
- }
1492
- throw error;
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
- getRadioGroup($input, $block) {
1497
- if (!($input instanceof HTMLInputElement) || $input.type !== 'radio' || !$input.name)
1498
- return [];
1499
- return Array.from($block.querySelectorAll('input[type="radio"]')).filter($radio => $radio.name === $input.name);
1500
- }
1501
- /** Показывает ошибку конкретного поля по имени или DOM-элементу. */
1502
- showFieldError(field, message, source = 'manual', rule = source) {
1503
- const $field = this.resolveField(field);
1504
- if (!$field) {
1505
- console.warn(`[FormFather] Field "${String(field)}" not found`);
1506
- return false;
1507
- }
1508
- this.setValidationError($field, rule, message, source);
1509
- this.showError($field, message);
1510
- $field.removeAttribute('aria-busy');
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
- /** Проверяет одно поле по имени или DOM-элементу. */
1516
- validateField(field) {
1517
- return __awaiter(this, void 0, void 0, function* () {
1518
- const $field = this.resolveField(field);
1519
- if (!$field) {
1520
- console.warn(`[FormFather] Field "${String(field)}" not found`);
1521
- return false;
1522
- }
1523
- const { rules, messages } = this.getFieldValidationConfig($field);
1524
- const isValid = yield this.runFieldValidation($field, rules, messages);
1525
- this.syncErrorSummary();
1526
- return isValid;
1527
- });
1528
- }
1529
- /**
1530
- * Проверяет все поля ввода.
1531
- *
1532
- * Порядок для каждого поля:
1533
- *
1534
- * 1. `required` — если атрибут `required`
1535
- * 2. правила из `validationSchema`
1536
- * 3. правила из `data-validate`
1537
- * 4. правила из `data-custom-validate`
1538
- *
1539
- * Для одного поля показывается только первая ошибка. Неизвестные правила фиксируются `console.warn` и исключаются ДО
1540
- * валидации.
1541
- */
1542
- validate() {
1543
- return __awaiter(this, arguments, void 0, function* ($block = this.$el) {
1544
- const schema = this.getValidationSchema();
1545
- const erroredInputs = [];
1546
- const processed = new WeakSet();
1547
- let ok = true;
1548
- this.errors = [];
1549
- for (const $input of this.getValidationFields($block, schema)) {
1550
- if (processed.has($input))
1551
- continue;
1552
- const radioGroup = this.getRadioGroup($input, $block);
1553
- if (radioGroup.length > 0) {
1554
- radioGroup.forEach($radio => processed.add($radio));
1555
- }
1556
- else {
1557
- processed.add($input);
1558
- }
1559
- const { rules, messages } = this.getFieldValidationConfig($input, schema);
1560
- const fieldValid = yield this.runFieldValidation($input, rules, messages, $block);
1561
- if (!fieldValid) {
1562
- ok = false;
1563
- erroredInputs.push($input);
1564
- }
1565
- }
1566
- const formValidation = yield this.validateFormValidators();
1567
- if (!formValidation.ok) {
1568
- ok = false;
1569
- erroredInputs.push(...formValidation.erroredInputs);
1570
- }
1571
- const visibleErroredInputs = erroredInputs.filter(item => !item.hasAttribute('data-no-error-scroll'));
1572
- if (!ok && this.config.scrollToFirstErroredInput) {
1573
- this.scrollToFirstErroredInput(visibleErroredInputs);
1574
- }
1575
- if (!ok && this.config.focusFirstErroredInput) {
1576
- this.focusFirstErroredInput(visibleErroredInputs);
1577
- }
1578
- if (ok) {
1579
- this.hideErrorSummary();
1580
- }
1581
- else {
1582
- this.renderErrorSummary();
1583
- }
1584
- return ok;
1585
- });
1586
- }
1587
- /** Блокирует кнопку отправки данных. */
1588
- disableSubmit() {
1589
- this.$submits.forEach($submit => {
1590
- $submit.setAttribute('disabled', '');
1591
- });
1592
- }
1593
- /** Снимает блокировку с кнопки отправки данных. */
1594
- enableSubmit() {
1595
- this.$submits.forEach($submit => {
1596
- $submit.removeAttribute('disabled');
1597
- });
1598
- }
1599
- /** Очищает поля ввода формы. */
1600
- clearInputs() {
1601
- var _a;
1602
- (_a = this.inputs) === null || _a === void 0 ? void 0 : _a.forEach($inputEl => {
1603
- var _a;
1604
- const $input = $inputEl;
1605
- if ($input.type === 'radio' || $input.type === 'checkbox') {
1606
- $input.checked = false;
1607
- }
1608
- else {
1609
- $input.value = '';
1610
- (_a = $input.parentElement) === null || _a === void 0 ? void 0 : _a.classList.remove('filled');
1611
- $input.dispatchEvent(new Event('input'));
1612
- }
1613
- });
1614
- }
1615
- _prefersReducedMotion() {
1616
- var _a;
1617
- return typeof window !== 'undefined' && ((_a = window.matchMedia) === null || _a === void 0 ? void 0 : _a.call(window, '(prefers-reduced-motion: reduce)').matches);
1618
- }
1619
- _canAnimate(el) {
1620
- return !!(el && 'animate' in el && !this._prefersReducedMotion());
1621
- }
1622
- _cancelAnimations(el) {
1623
- var _a;
1624
- // Отменяем все активные WAAPI-анимации у элемента
1625
- const animatedEl = el;
1626
- (_a = animatedEl === null || animatedEl === void 0 ? void 0 : animatedEl.getAnimations) === null || _a === void 0 ? void 0 : _a.call(animatedEl).forEach(animation => animation.cancel());
1627
- }
1628
- /**
1629
- * Показывает ошибку под полями ввода - ошибку, относящуюся ко всей форме.
1630
- *
1631
- * @param {String} text - Текст ошибки.
1632
- */
1633
- showErrorForm(text) {
1634
- var _a;
1635
- const inputWrappersList = this.$el.querySelectorAll(this.config.inputWrapperSelector);
1636
- const $lastInputWrapper = inputWrappersList[inputWrappersList.length - 1];
1637
- let $errorWrapper = this.$el.querySelector('.error-block-under-input__wrapper');
1638
- if (!$errorWrapper || ($lastInputWrapper && !($lastInputWrapper.nextElementSibling === $errorWrapper))) {
1639
- $errorWrapper = document.createElement('div');
1640
- $errorWrapper.className = 'error-block-under-input__wrapper';
1641
- $errorWrapper.innerHTML = `
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
- // Стартовые inline-стили для анимаций
1654
- Object.assign($errorWrapper.style, {
1655
- boxSizing: 'border-box',
1656
- overflow: 'hidden',
1657
- height: '0px',
1658
- paddingTop: '0px',
1659
- opacity: '0',
1660
- });
1661
- if ($lastInputWrapper) {
1662
- $lastInputWrapper.insertAdjacentElement('afterend', $errorWrapper);
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
- else {
1665
- this.$el.appendChild($errorWrapper);
1666
- }
1667
- }
1668
- else {
1669
- // убедимся, что нужные стартовые inline-стили есть
1670
- $errorWrapper.style.boxSizing = 'border-box';
1671
- $errorWrapper.style.overflow = 'hidden';
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
- const targetHeight = $errorWrapper.clientHeight; // фактическая высота открытого состояния
1687
- // Вернуть стартовое состояние (схлопнуто)
1688
- $errorWrapper.style.height = prevHeight || '0px';
1689
- $errorWrapper.style.paddingTop = prevPadding || '0px';
1690
- // Если уже раскрыт на нужную высоту — просто «дзынь» иконке и выходим
1691
- if (parseFloat($errorWrapper.style.height) === targetHeight &&
1692
- parseFloat($errorWrapper.style.opacity || '1') === 1) {
1693
- this._ringIcon($errorWrapper);
1694
- }
1695
- else {
1696
- // Анимация раскрытия
1697
- this._cancelAnimations($errorWrapper);
1698
- if (this._canAnimate($errorWrapper)) {
1699
- const anim = $errorWrapper.animate([
1700
- {
1701
- height: `${parseFloat($errorWrapper.style.height) || 0}px`,
1702
- paddingTop: `${parseFloat($errorWrapper.style.paddingTop) || 0}px`,
1703
- opacity: parseFloat($errorWrapper.style.opacity || '0'),
1704
- },
1705
- {
1706
- height: `${targetHeight}px`,
1707
- paddingTop: `${this._anim.paddingTopOpened}px`,
1708
- opacity: 1,
1709
- },
1710
- ], { duration: this._anim.duration, easing: this._anim.easing, fill: 'forwards' });
1711
- anim.addEventListener('finish', () => {
1712
- // После анимации: фиксируем авто-высоту, чтобы текст мог меняться
1713
- $errorWrapper.style.height = 'auto';
1714
- $errorWrapper.style.paddingTop = `${this._anim.paddingTopOpened}px`;
1715
- $errorWrapper.style.opacity = '1';
1716
- });
1717
- }
1718
- else {
1719
- // Фолбэк без анимации
1720
- $errorWrapper.style.height = 'auto';
1721
- $errorWrapper.style.paddingTop = `${this._anim.paddingTopOpened}px`;
1722
- $errorWrapper.style.opacity = '1';
1723
- }
1724
- this._ringIcon($errorWrapper);
1725
- }
1726
- // Скрыть при первом вводе после показа
1727
- this.$el.addEventListener('input', () => this.hideErrorForm(), { passive: true, once: true });
1728
- }
1729
- /** Скрывает ошибку под полями ввода - ошибку, относящуюся ко всей форме. */
1730
- hideErrorForm() {
1731
- const inputWrappersList = this.$el.querySelectorAll(this.config.inputWrapperSelector);
1732
- const $lastInputWrapper = inputWrappersList[inputWrappersList.length - 1];
1733
- const $errorWrapper = (($lastInputWrapper === null || $lastInputWrapper === void 0 ? void 0 : $lastInputWrapper.nextElementSibling) || this.$el.querySelector('.error-block-under-input__wrapper'));
1734
- if (!$errorWrapper || !$errorWrapper.classList.contains('error-block-under-input__wrapper'))
1735
- return;
1736
- // Текущая высота (если была auto) — измерим и зафиксируем
1737
- const wasAuto = $errorWrapper.style.height === '' || $errorWrapper.style.height === 'auto';
1738
- if (wasAuto) {
1739
- $errorWrapper.style.height = `${$errorWrapper.clientHeight}px`;
1740
- }
1741
- $errorWrapper.style.paddingTop = $errorWrapper.style.paddingTop || `${this._anim.paddingTopOpened}px`;
1742
- $errorWrapper.style.opacity = $errorWrapper.style.opacity || '1';
1743
- // Анимация схлопывания
1744
- this._cancelAnimations($errorWrapper);
1745
- if (this._canAnimate($errorWrapper)) {
1746
- const anim = $errorWrapper.animate([
1747
- {
1748
- height: `${parseFloat($errorWrapper.style.height) || $errorWrapper.clientHeight}px`,
1749
- paddingTop: `${parseFloat($errorWrapper.style.paddingTop) || this._anim.paddingTopOpened}px`,
1750
- opacity: parseFloat($errorWrapper.style.opacity || '1'),
1751
- },
1752
- { height: '0px', paddingTop: '0px', opacity: 0 },
1753
- ], { duration: this._anim.duration, easing: this._anim.easing, fill: 'forwards' });
1754
- anim.addEventListener('finish', () => {
1755
- // После закрытия можно удалить узел или оставить схлопнутым
1756
- $errorWrapper.remove();
1757
- // Если хотите оставлять в DOM:
1758
- // $errorWrapper.style.height = '0px';
1759
- // $errorWrapper.style.paddingTop = '0px';
1760
- // $errorWrapper.style.opacity = '0';
1761
- });
1762
- }
1763
- else {
1764
- // Фолбэк без анимации
1765
- $errorWrapper.remove();
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
- /** Короткая анимация иконки без CSS */
1769
- _ringIcon($wrapper) {
1770
- const $icon = $wrapper.querySelector('.error-block-under-input__icon--animated');
1771
- if (!$icon)
1772
- return;
1773
- this._cancelAnimations($icon);
1774
- if (!this._canAnimate($icon))
1775
- return;
1776
- // Небольшое «качание» и вспышка прозрачности
1777
- $icon.animate([
1778
- { transform: 'rotate(0deg) scale(1)', opacity: 0 },
1779
- { transform: 'rotate(-8deg) scale(1.05)', opacity: 1, offset: 0.15 },
1780
- { transform: 'rotate(8deg) scale(1.05)', opacity: 1, offset: 0.3 },
1781
- { transform: 'rotate(-6deg) scale(1.03)', opacity: 1, offset: 0.45 },
1782
- { transform: 'rotate(6deg) scale(1.03)', opacity: 1, offset: 0.6 },
1783
- { transform: 'rotate(0deg) scale(1)', opacity: 0 },
1784
- ], { duration: this._anim.iconDuration, easing: 'ease' });
1785
- }
1786
- /** Полностью снимает слушатели и чистит артефакты формы. */
1787
- destroy() {
1788
- var _a;
1789
- // 1) Снять submit-слушатель
1790
- if (this._onSubmitHandler) {
1791
- this.$el.removeEventListener('submit', this._onSubmitHandler);
1792
- this._onSubmitHandler = undefined;
1793
- }
1794
- // 1.1) Снять click-слушатель с кнопки submit
1795
- if (this._onSubmitClickHandler) {
1796
- this.$submits.forEach($submit => {
1797
- $submit.removeEventListener('click', this._onSubmitClickHandler);
1798
- });
1799
- this._onSubmitClickHandler = undefined;
1800
- }
1801
- this.clearFieldValidationEvents();
1802
- (_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
1803
- this.mutationObserver = undefined;
1804
- // 2) Удалить формовый error-блок под инпутами (если есть)
1805
- const formErrorWrapper = this.$el.querySelector('.error-block-under-input__wrapper');
1806
- if (formErrorWrapper) {
1807
- this._cancelAnimations(formErrorWrapper);
1808
- formErrorWrapper.remove();
1809
- }
1810
- this.hideErrorSummary();
1811
- // 3) Скрыть/удалить лоадер на кнопке submit
1812
- this.$submits.forEach($submit => {
1813
- $submit.classList.remove('button--loading');
1814
- const btnLoader = $submit.querySelector('.button__loader');
1815
- if (btnLoader)
1816
- btnLoader.remove();
1817
- });
1818
- // 4) Сбросить ошибки у инпутов (если разметка позволяет)
1819
- const inputs = this.$el.querySelectorAll('input, textarea, select');
1820
- inputs.forEach($i => {
1821
- this.hideError($i);
1822
- this.clearFieldValidationState($i);
1823
- });
1824
- // 5) Убрать возможный флаг скролла
1825
- document.documentElement.removeAttribute('data-now-scrolling');
1826
- // 6) Очистить ссылки на элементы, чтобы помочь GC
1827
- this.inputs = null;
1828
- this.$submits = [];
1829
- this.errors = [];
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
- FORM_INSTANCES.delete(this.$el);
1832
- }
1833
- /**
1834
- * Проверяет входные данные на их корректность.
1835
- *
1836
- * @returns {boolean} Корректны данные или нет (true/false).
1837
- */
1838
- isCorrectArguments() {
1839
- if (this.$el.tagName !== 'FORM') {
1840
- console.warn(`$el не является тегом form`);
1841
- return false;
1842
- }
1843
- if (this.$submits.length === 0) {
1844
- console.warn(`В форме нет кнопки с типом submit`);
1845
- return false;
1846
- }
1847
- return true;
1848
- }
1849
- formDataToObject(formData) {
1850
- const data = {};
1851
- formData.forEach((value, key) => {
1852
- if (Object.prototype.hasOwnProperty.call(data, key)) {
1853
- if (!Array.isArray(data[key])) {
1854
- data[key] = [data[key]];
1855
- }
1856
- data[key].push(value);
1857
- }
1858
- else {
1859
- data[key] = value;
1860
- }
1861
- });
1862
- return data;
1863
- }
1864
- appendFormDataValue(formData, key, value) {
1865
- if (value === undefined || value === null)
1866
- return;
1867
- if (Array.isArray(value)) {
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 };