form-father 0.7.1 → 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 ADDED
@@ -0,0 +1,1791 @@
1
+ function formatDate(dateValue, format) {
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);
8
+ }
9
+ function closest($el, selector) {
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;
19
+ }
20
+ function blockScrollBody() {
21
+ window.lastBlockScrollPosition = window.pageYOffset;
22
+ document.body.style.top = `${-window.lastBlockScrollPosition}px`;
23
+ document.body.classList.add("body--block-scroll");
24
+ }
25
+ function unblockScrollBody() {
26
+ document.body.classList.remove("body--block-scroll");
27
+ document.body.style.top = "";
28
+ window.scrollTo(0, window.lastBlockScrollPosition || 0);
29
+ }
30
+ function isEmailValid(value) {
31
+ return /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu.test(
32
+ value
33
+ );
34
+ }
35
+ const HTTP_PROTOCOLS = ["http:", "https:"];
36
+ function getNormalizedUrlString(originalUrlString) {
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}`;
43
+ }
44
+ function isValidIpv4(hostname) {
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
+ });
52
+ }
53
+ function isValidIpv6(hostname) {
54
+ if (!hostname.startsWith("[") || !hostname.endsWith("]")) return false;
55
+ const normalizedHostname = hostname.slice(1, -1);
56
+ return normalizedHostname.includes(":") && /^[\da-f:.]+$/i.test(normalizedHostname);
57
+ }
58
+ function isValidDomainLabel(label) {
59
+ return label.length > 0 && label.length <= 63 && /^[a-z\d-]+$/i.test(label) && !label.startsWith("-") && !label.endsWith("-");
60
+ }
61
+ function isValidDomainHostname(hostname) {
62
+ const labels = hostname.split(".");
63
+ return labels.length >= 2 && labels.every(isValidDomainLabel);
64
+ }
65
+ function isAllowedHostname(hostname) {
66
+ if (!hostname) return false;
67
+ return hostname === "localhost" || isValidIpv4(hostname) || isValidIpv6(hostname) || isValidDomainHostname(hostname);
68
+ }
69
+ function isUrlValid(originalUrlString) {
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
+ }
78
+ }
79
+ function isPhoneValid(value) {
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;
86
+ }
87
+ function serializeToFormData($element) {
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;
143
+ }
144
+ function parseCommonResponseProperties(responseBody) {
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
+ }
176
+ }
177
+ function serializeFormToJSON(form) {
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;
191
+ }
192
+
193
+ var _a$1;
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();
196
+ function registerValidator(name, fn, defaultMessage, { override = false } = {}) {
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 });
202
+ }
203
+ const getValidator = (name) => validators.get(name);
204
+ function getAllValidators() {
205
+ return new Map(validators);
206
+ }
207
+ function registerBuiltinValidator(name, fn, defaultMessage) {
208
+ if (!validators.has(name)) {
209
+ validators.set(name, { fn, defaultMessage });
210
+ }
211
+ }
212
+ registerBuiltinValidator(
213
+ "required",
214
+ (value, $input, $form) => {
215
+ if ($input && $input.type) {
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
+ }
223
+ }
224
+ return value.trim().length > 0;
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");
232
+
233
+ function readFirstIssueMessage(error) {
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;
242
+ }
243
+ function resolveMessage(error, message) {
244
+ if (typeof message === "function") return message(error);
245
+ return message || readFirstIssueMessage(error);
246
+ }
247
+ function normalizeResult(result, message) {
248
+ if (typeof result === "object" && result !== null) return result;
249
+ if (result === void 0) return true;
250
+ return result ? true : { valid: false, message };
251
+ }
252
+ function createSchemaValidator(schema, options = {}) {
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
+ };
273
+ }
274
+ function registerSchemaValidator(name, schema, defaultMessage, options = {}) {
275
+ const { override, ...adapterOptions } = options;
276
+ registerValidator(name, createSchemaValidator(schema, adapterOptions), defaultMessage, { override });
277
+ }
278
+ function registerFieldValidator(name, predicate, defaultMessage, options = {}) {
279
+ registerValidator(name, createFieldValidator(predicate), defaultMessage, options);
280
+ }
281
+ function createFieldValidator(predicate, message) {
282
+ return async (value, $input, $form, params) => {
283
+ const valid = await predicate(value, $input, $form, params);
284
+ return valid ? true : { valid: false, message };
285
+ };
286
+ }
287
+ function createPatternValidator(pattern, message) {
288
+ return createFieldValidator((value) => {
289
+ pattern.lastIndex = 0;
290
+ return pattern.test(value);
291
+ }, message);
292
+ }
293
+ function createLengthValidator(options, 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);
301
+ }
302
+
303
+ var _a;
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";
316
+ function readFormValue(values, field) {
317
+ return values[field];
318
+ }
319
+ function isEmptyFormValue(value) {
320
+ if (Array.isArray(value)) return value.length === 0;
321
+ return value === void 0 || value === null || String(value).trim().length === 0;
322
+ }
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
+ };
337
+ }
338
+ function createFormValidator(predicate, issue) {
339
+ return async (context) => {
340
+ const valid = await predicate(context);
341
+ return valid ? true : resolveFormValidatorIssue(context, issue);
342
+ };
343
+ }
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
+ );
349
+ }
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
+ );
355
+ }
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
+ );
366
+ }
367
+ let loaderIdCounter = 0;
368
+ let errorIdCounter = 0;
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";
373
+ const INITIAL_DEFAULT_SCHEMA = {
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
+ }
394
+ };
395
+ class Form {
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()) {
451
+ this.bindSubmitHandlers();
452
+ this.initialization();
453
+ this.bindFieldValidationEvents();
454
+ this.setupMutationObserver();
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);
533
+ });
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);
722
+ };
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 = `
850
+ <svg height="38" viewBox="0 0 38 38" width="38" xmlns="http://www.w3.org/2000/svg">
851
+ <defs>
852
+ <linearGradient id="${gradientId}" x1="8.042%" x2="65.682%" y1="0%" y2="23.865%">
853
+ <stop offset="0%" stop-color="${this.config.loaderColor}" stop-opacity="0"/>
854
+ <stop offset="63.146%" stop-color="${this.config.loaderColor}" stop-opacity=".631"/>
855
+ <stop offset="100%" stop-color="${this.config.loaderColor}"/>
856
+ </linearGradient>
857
+ </defs>
858
+ <g fill-rule="evenodd" fill="none">
859
+ <g transform="translate(1 1)">
860
+ <path d="M36 18c0-9.94-8.06-18-18-18" id="Oval-2" stroke-width="5" stroke="url(#${gradientId})"></path>
861
+ <circle cx="36" cy="18" fill="${this.config.loaderColor}" r="1"></circle>
862
+ </g>
863
+ </g>
864
+ </svg>
865
+ `;
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();
941
+ }
942
+ }
943
+ focusFieldFromSummary($field) {
944
+ if (typeof $field.scrollIntoView === "function") {
945
+ $field.scrollIntoView({ behavior: "smooth", block: "center" });
946
+ }
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"
1039
+ });
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
1236
+ });
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) {
1253
+ return true;
1254
+ }
1255
+ }
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 = `
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;">
1413
+ <div class="error-block-under-input__icon-wrapper" style="position:relative;width:20px;height:20px;">
1414
+ <span class="error-block-under-input__icon" style="display:block;width:20px;height:20px;"></span>
1415
+ <span class="error-block-under-input__icon error-block-under-input__icon--animated" style="display:block;position:absolute;inset:0;"></span>
1416
+ </div>
1417
+ <p class="error-block-under-input__text" style="margin:0;">
1418
+ <span class="error-block-under-input__main-text"></span>
1419
+ <span class="error-block-under-input__secondary-text"></span>
1420
+ </p>
1421
+ </div>
1422
+ `;
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
1468
+ }
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";
1479
+ $errorWrapper.style.paddingTop = `${this._anim.paddingTopOpened}px`;
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;
1588
+ }
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;
1750
+ this.waitResponse = false;
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
+ }
1788
+ }
1789
+
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 };
1791
+ //# sourceMappingURL=index.js.map