form-father 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +50 -9
- package/demos/index.html +8 -0
- package/demos/main.js +6 -14
- package/demos/styles.css +40 -0
- package/dist/FormFather.min.js +1 -1
- package/dist/index.d.ts +49 -1
- package/dist/index.js +258 -4
- package/dist/index.js.map +1 -1
- package/dist/types/adapters.d.ts +12 -0
- package/dist/types/index.d.ts +36 -0
- package/docs/en/README.md +50 -9
- package/docs/recipes/README.md +43 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.6.0 - 2026-04-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added accessible field error wiring via `aria-describedby` with safe preservation of existing descriptions.
|
|
10
|
+
- Added `errorSummary` option with default rendering, custom selector support, focus control, and custom renderers.
|
|
11
|
+
- Added `ariaDescribeErrors` and `errorIdPrefix` options for UI/a11y integration control.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Field, form-level, backend, and manual errors can now share one visible summary while keeping the existing inline error
|
|
16
|
+
rendering path.
|
|
17
|
+
- Demo forms now include accessible error summaries.
|
|
18
|
+
|
|
19
|
+
### Tests
|
|
20
|
+
|
|
21
|
+
- Test suite expanded to 122 tests.
|
|
22
|
+
- Coverage baseline: 98.30% statements, 86.88% branches, 97.25% functions, 100% lines.
|
|
23
|
+
|
|
24
|
+
## 0.5.0 - 2026-04-27
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Added field validator DX helpers: `registerFieldValidator()`, `createPatternValidator()`, and
|
|
29
|
+
`createLengthValidator()`.
|
|
30
|
+
- Added form validator DX helpers: `createFormValidator()`, `sameAsField()`, `requiredIf()`, and `dateOrder()`.
|
|
31
|
+
- Added helper types for reusable form validator predicates and issue factories.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Demo and documentation now use higher-level helper APIs for common password-confirm and validation rule recipes.
|
|
36
|
+
|
|
37
|
+
### Tests
|
|
38
|
+
|
|
39
|
+
- Test suite expanded to 115 tests.
|
|
40
|
+
- Coverage baseline: 98.19% statements, 86.91% branches, 97.04% functions, 100% lines.
|
|
41
|
+
|
|
5
42
|
## 0.4.0 - 2026-04-27
|
|
6
43
|
|
|
7
44
|
### Added
|
package/README.md
CHANGED
|
@@ -78,6 +78,8 @@ import Form, {
|
|
|
78
78
|
type FormResetOptions,
|
|
79
79
|
type FormValidator,
|
|
80
80
|
type ValidationIssue,
|
|
81
|
+
type FormValidatorPredicate,
|
|
82
|
+
type ErrorSummaryOptions,
|
|
81
83
|
} from 'form-father';
|
|
82
84
|
```
|
|
83
85
|
|
|
@@ -109,6 +111,9 @@ import Form, {
|
|
|
109
111
|
- **errorContainerAttribute**: Атрибут с CSS-селектором контейнера ошибки. По умолчанию `data-error-container`.
|
|
110
112
|
- **validationStateAttribute**: Атрибут состояния поля: `validating`, `valid`, `invalid`. По умолчанию
|
|
111
113
|
`data-form-father-state`.
|
|
114
|
+
- **ariaDescribeErrors**: Связывать inline-ошибку с полем через `aria-describedby`. По умолчанию `true`.
|
|
115
|
+
- **errorIdPrefix**: Префикс id для автоматически созданных элементов ошибок. По умолчанию `form-father-error`.
|
|
116
|
+
- **errorSummary**: Summary ошибок формы: `true` для дефолтного блока или объект `{ selector, title, focus, render }`.
|
|
112
117
|
- **observeMutations**: Следить за динамически добавленными полями и submit-кнопками.
|
|
113
118
|
- **formValidators**: Валидатор или массив валидаторов всей формы для cross-field правил.
|
|
114
119
|
|
|
@@ -247,8 +252,38 @@ _Порядок применения для одного `<input>`:_
|
|
|
247
252
|
- `data-error-container` указывает контейнер для ошибки без обязательной wrapper-разметки.
|
|
248
253
|
- Во время async-валидации поле получает `aria-busy="true"` и state-атрибут `validating`; устаревшие ответы
|
|
249
254
|
валидаторов игнорируются.
|
|
255
|
+
- Inline-ошибки автоматически получают `role="alert"` и связываются с полем через `aria-describedby`.
|
|
250
256
|
- `data-custom-validate` сохранён для обратной совместимости.
|
|
251
257
|
|
|
258
|
+
### Error summary и a11y
|
|
259
|
+
|
|
260
|
+
```html
|
|
261
|
+
<form data-form-father novalidate>
|
|
262
|
+
<div data-form-father-summary hidden></div>
|
|
263
|
+
<label>
|
|
264
|
+
Email
|
|
265
|
+
<input class="input" name="email" data-validate="required|email" />
|
|
266
|
+
</label>
|
|
267
|
+
<button type="submit">Отправить</button>
|
|
268
|
+
</form>
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
new Form($form, {
|
|
273
|
+
inputWrapperSelector: 'label',
|
|
274
|
+
errorSummary: {
|
|
275
|
+
title: 'Проверьте форму',
|
|
276
|
+
focus: true,
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
- Если в форме уже есть `[data-form-father-summary]`, библиотека использует его; иначе при `errorSummary: true` создаёт
|
|
282
|
+
блок в начале формы.
|
|
283
|
+
- `selector` позволяет отдать summary в свой контейнер.
|
|
284
|
+
- `render(errors, form)` позволяет полностью заменить разметку summary.
|
|
285
|
+
- `ariaDescribeErrors: false` отключает автоматическое добавление id ошибки в `aria-describedby`.
|
|
286
|
+
|
|
252
287
|
### Правило через `data-custom-validate`
|
|
253
288
|
|
|
254
289
|
```html
|
|
@@ -329,17 +364,13 @@ required → schema.rules → data-validate → data-custom-validate
|
|
|
329
364
|
### Cross-field и form-level validation
|
|
330
365
|
|
|
331
366
|
```ts
|
|
367
|
+
import { dateOrder, requiredIf, sameAsField } from 'form-father';
|
|
368
|
+
|
|
332
369
|
const form = new Form($form, {
|
|
333
370
|
formValidators: [
|
|
334
|
-
(
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
: {
|
|
338
|
-
field: 'passwordConfirm',
|
|
339
|
-
rule: 'same-as-password',
|
|
340
|
-
message: 'Пароли не совпадают',
|
|
341
|
-
},
|
|
342
|
-
({ values }) => (values.start <= values.end ? true : { field: 'end', message: 'Дата окончания раньше начала' }),
|
|
371
|
+
sameAsField('passwordConfirm', 'password', 'Пароли не совпадают'),
|
|
372
|
+
requiredIf('email', ({ values }) => Boolean(values.subscribe), 'Email обязателен для подписки'),
|
|
373
|
+
dateOrder('start', 'end', 'Дата окончания раньше начала'),
|
|
343
374
|
],
|
|
344
375
|
});
|
|
345
376
|
```
|
|
@@ -354,6 +385,16 @@ form.setErrors({
|
|
|
354
385
|
});
|
|
355
386
|
```
|
|
356
387
|
|
|
388
|
+
### DX helpers для правил
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
import { createLengthValidator, createPatternValidator, registerFieldValidator } from 'form-father';
|
|
392
|
+
|
|
393
|
+
registerFieldValidator('starts-with-a', value => value.startsWith('A'), 'Должно начинаться с A');
|
|
394
|
+
registerValidator('slug', createPatternValidator(/^[a-z0-9-]+$/), 'Только slug-символы');
|
|
395
|
+
registerValidator('short-name', createLengthValidator({ min: 2, max: 24 }), 'От 2 до 24 символов');
|
|
396
|
+
```
|
|
397
|
+
|
|
357
398
|
## Адаптеры схем
|
|
358
399
|
|
|
359
400
|
Form Father не тянет внешние зависимости, но умеет оборачивать Zod/Valibot/Yup-подобные схемы с `safeParse()` или
|
package/demos/index.html
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
<h2>Live validation</h2>
|
|
23
23
|
</div>
|
|
24
24
|
<form data-form-father action="/demo/login" method="post" enctype="application/json" data-demo="login" novalidate>
|
|
25
|
+
<div data-form-father-summary hidden></div>
|
|
26
|
+
|
|
25
27
|
<label class="field">
|
|
26
28
|
<span>Email</span>
|
|
27
29
|
<input
|
|
@@ -73,6 +75,8 @@
|
|
|
73
75
|
<h2>Server field errors</h2>
|
|
74
76
|
</div>
|
|
75
77
|
<form data-form-father action="/demo/callback" method="post" data-demo="callback" novalidate>
|
|
78
|
+
<div data-form-father-summary hidden></div>
|
|
79
|
+
|
|
76
80
|
<label class="field">
|
|
77
81
|
<span>Имя</span>
|
|
78
82
|
<input class="input" name="name" data-validate="required" data-error-required="Укажите имя" />
|
|
@@ -100,6 +104,8 @@
|
|
|
100
104
|
<h2>GET query</h2>
|
|
101
105
|
</div>
|
|
102
106
|
<form data-form-father action="/demo/search?source=demos" method="get" data-demo="search" novalidate>
|
|
107
|
+
<div data-form-father-summary hidden></div>
|
|
108
|
+
|
|
103
109
|
<label class="field">
|
|
104
110
|
<span>Запрос</span>
|
|
105
111
|
<input class="input" name="q" value="form father" data-validate="required" />
|
|
@@ -124,6 +130,8 @@
|
|
|
124
130
|
<h2>FormData upload</h2>
|
|
125
131
|
</div>
|
|
126
132
|
<form data-form-father action="/demo/upload" method="post" enctype="multipart/form-data" data-demo="upload" novalidate>
|
|
133
|
+
<div data-form-father-summary hidden></div>
|
|
134
|
+
|
|
127
135
|
<label class="field">
|
|
128
136
|
<span>Название</span>
|
|
129
137
|
<input class="input" name="title" data-validate="required" />
|
package/demos/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const api = window.FormFather || {};
|
|
2
2
|
const Form = api.default || api.Form || api;
|
|
3
|
-
const { registerValidator } = api;
|
|
3
|
+
const { createLengthValidator, registerValidator, sameAsField } = api;
|
|
4
4
|
const output = document.querySelector('#demo-output');
|
|
5
5
|
|
|
6
6
|
function writeOutput(title, payload) {
|
|
@@ -57,9 +57,7 @@ window.fetch = async (url, options = {}) => {
|
|
|
57
57
|
return json({ success: false, error: true, 'error-msg': `No demo route for ${requestUrl}` }, 404);
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
registerValidator('min-length', (
|
|
61
|
-
override: true,
|
|
62
|
-
});
|
|
60
|
+
registerValidator('min-length', createLengthValidator({ min: 6 }), 'Слишком короткое значение', { override: true });
|
|
63
61
|
registerValidator(
|
|
64
62
|
'available-email',
|
|
65
63
|
value =>
|
|
@@ -78,17 +76,11 @@ Form.initAll('form[data-form-father]', {
|
|
|
78
76
|
validationDebounce: 120,
|
|
79
77
|
focusFirstErroredInput: true,
|
|
80
78
|
loaderColor: 'currentColor',
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
field: 'passwordConfirm',
|
|
85
|
-
rule: 'same-as-password',
|
|
86
|
-
message: 'Пароли не совпадают',
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return true;
|
|
79
|
+
errorSummary: {
|
|
80
|
+
title: 'Проверьте поля формы',
|
|
81
|
+
focus: true,
|
|
91
82
|
},
|
|
83
|
+
formValidators: sameAsField('passwordConfirm', 'password', 'Пароли не совпадают'),
|
|
92
84
|
onValidationError(errors) {
|
|
93
85
|
writeOutput('Client validation errors', errors);
|
|
94
86
|
},
|
package/demos/styles.css
CHANGED
|
@@ -116,6 +116,46 @@ form {
|
|
|
116
116
|
gap: 14px;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
[data-form-father-summary]:not([hidden]) {
|
|
120
|
+
display: grid;
|
|
121
|
+
gap: 8px;
|
|
122
|
+
padding: 12px;
|
|
123
|
+
border: 1px solid rgba(179, 38, 30, 0.28);
|
|
124
|
+
border-radius: 6px;
|
|
125
|
+
background: rgba(179, 38, 30, 0.06);
|
|
126
|
+
color: var(--danger);
|
|
127
|
+
font-size: 14px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
[data-form-father-summary-title] {
|
|
131
|
+
margin: 0;
|
|
132
|
+
font-weight: 700;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
[data-form-father-summary-list] {
|
|
136
|
+
display: grid;
|
|
137
|
+
gap: 4px;
|
|
138
|
+
margin: 0;
|
|
139
|
+
padding-left: 18px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
[data-form-father-summary] button {
|
|
143
|
+
min-height: 0;
|
|
144
|
+
padding: 0;
|
|
145
|
+
border: 0;
|
|
146
|
+
background: transparent;
|
|
147
|
+
color: inherit;
|
|
148
|
+
font-weight: 700;
|
|
149
|
+
text-align: left;
|
|
150
|
+
text-decoration: underline;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
[data-form-father-summary] button:hover,
|
|
154
|
+
[data-form-father-summary] button:focus-visible {
|
|
155
|
+
background: transparent;
|
|
156
|
+
color: var(--accent-strong);
|
|
157
|
+
}
|
|
158
|
+
|
|
119
159
|
.field {
|
|
120
160
|
display: grid;
|
|
121
161
|
gap: 6px;
|
package/dist/FormFather.min.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var FormFather=function(t){"use strict";function e(t,e,r,i){return new(r||(r=Promise))((function(n,s){function o(t){try{l(i.next(t))}catch(t){s(t)}}function a(t){try{l(i.throw(t))}catch(t){s(t)}}function l(t){var e;t.done?n(t.value):(e=t.value,e instanceof r?e:new r((function(t){t(e)}))).then(o,a)}l((i=i.apply(t,e||[])).next())}))}function r(t,e){if(null==t)return null;let r=t;for(;r;){if(r.matches(e))return r;r=r.parentElement}return null}"function"==typeof SuppressedError&&SuppressedError;const i=["http:","https:"];function n(t){return t.length>0&&t.length<=63&&/^[a-z\d-]+$/i.test(t)&&!t.startsWith("-")&&!t.endsWith("-")}function s(t){return!!t&&("localhost"===t||function(t){const e=t.split(".");return 4===e.length&&e.every((t=>{if(!/^\d{1,3}$/.test(t))return!1;const e=Number(t);return e>=0&&e<=255}))}(t)||function(t){if(!t.startsWith("[")||!t.endsWith("]"))return!1;const e=t.slice(1,-1);return e.includes(":")&&/^[\da-f:.]+$/i.test(e)}(t)||function(t){const e=t.split(".");return e.length>=2&&e.every(n)}(t))}function o(t){const e=function(t){const e=t.trim();return 0===e.length?"":e.startsWith("//")?`https:${e}`:/^[a-z][a-z\d+.-]*:\/\//i.test(e)?e:`https://${e}`}(t);if(0===e.length)return!1;try{const t=new URL(e);return i.includes(t.protocol)&&s(t.hostname)}catch(t){return!1}}function a(t){const e="FORM"===t.tagName?Array.from(t.elements):Array.from(t.querySelectorAll("input, select, textarea, button")),r=new FormData;return e.forEach((t=>{if(!(t instanceof HTMLInputElement||t instanceof HTMLSelectElement||t instanceof HTMLTextAreaElement||t instanceof HTMLButtonElement))return;if(!t.name||t.disabled||t.hasAttribute("data-no-serialize"))return;if(t instanceof HTMLButtonElement)return;const{name:e,type:i}=t;if("file"===i)if(t.multiple){const i=t.files;if(i)for(let t=0;t<i.length;t++)r.append(`${e}[${t}]`,i[t])}else{const i=t.files;i&&i[0]&&r.append(e,i[0])}else{let n;if("select-one"===i||"select-multiple"===i){const s=t;if("select-multiple"===i){Array.from(s.selectedOptions).map((t=>t.value)).forEach(((t,i)=>{r.append(`${e}[${i}]`,t)}))}else n=s.value,n&&r.append(e,n)}else if("tel"===i)n=t.value.split(/[\s()-]/).join("").replace(/^8/,"+7");else if("checkbox"===i||"radio"===i){if(!t.checked)return;n=t.value}else if("date"===i){const e=t.getAttribute("data-date-format");n=e?function(t,e){const r=new Date(t),i=t=>t.toString().padStart(2,"0"),n=i(r.getDate()),s=i(r.getMonth()+1),o=r.getFullYear().toString();return e.replace("dd",n).replace("mm",s).replace("yyyy",o)}(t.value,e):t.value}else n=t.value;n&&"select-one"!==i&&"select-multiple"!==i&&r.append(e,n)}})),r}function l(t){if(Object.prototype.hasOwnProperty.call(t,"redirect-url"))if(t["redirect-url-delay"]){const e=Number(t["redirect-url-delay"]);setTimeout((()=>{window.location.href=t["redirect-url"]}),e)}else window.location.href=t["redirect-url"];else if(Object.prototype.hasOwnProperty.call(t,"reload")){if(!0===t.reload)if(t["reload-delay"]){const e=Number(t["reload-delay"]);setTimeout((()=>{window.location.reload()}),e)}else window.location.reload()}else if(Object.prototype.hasOwnProperty.call(t,"toast")){const e=t.toast;(Array.isArray(e)?e:[e]).forEach((t=>{window.showToast(t)}))}else Object.prototype.hasOwnProperty.call(t,"error-toast")&&window.showToast(t["error-toast"])}function d(t){const e={};return new FormData(t).forEach(((t,r)=>{e.hasOwnProperty(r)?(Array.isArray(e[r])||(e[r]=[e[r]]),e[r].push(t)):e[r]=t})),e}var c;const u=Symbol.for("FormFather.validators"),h=null!==(c=globalThis[u])&&void 0!==c?c:globalThis[u]=new Map;function p(t,e,r,{override:i=!1}={}){!h.has(t)||i?h.set(t,{fn:e,defaultMessage:r}):console.warn(`[FormFather] Validator "${t}" already exists; pass { override: true } to replace`)}const f=t=>h.get(t);function m(t,e,r){h.has(t)||h.set(t,{fn:e,defaultMessage:r})}function g(t,e){return"function"==typeof e?e(t):e||function(t){var e;if(!t||"object"!=typeof t)return;const r=t.issues||t.errors;if(Array.isArray(r)){const t=null===(e=r.find((t=>"string"==typeof(null==t?void 0:t.message))))||void 0===e?void 0:e.message;if(t)return t}return"string"==typeof t.message?t.message:void 0}(t)}function y(t,r={}){return(i,n,s,o)=>e(this,void 0,void 0,(function*(){const e=r.mapValue?r.mapValue(i,n,s,o):i;try{if("function"==typeof t)return a=yield t(e),l=g(void 0,r.message),"object"==typeof a&&null!==a?a:void 0===a||!!a||{valid:!1,message:l};if("function"==typeof t.safeParse){const i=yield t.safeParse(e);return!!i.success||{valid:!1,message:g(i.error||i,r.message)}}if("function"==typeof t.parse)return yield t.parse(e),!0}catch(t){return{valid:!1,message:g(t,r.message)}}var a,l;return{valid:!1,message:g(void 0,r.message)}}))}var b;m("required",((t,e,r)=>{if(e&&e.type){if("checkbox"===e.type)return e.checked;if("radio"===e.type){const t=r.querySelectorAll(`input[type="radio"][name="${e.name}"]`);return Array.from(t).some((t=>t.checked))}}return t.trim().length>0}),"Пустое значение"),m("email",(t=>/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)),"Неверный формат"),m("tel",(t=>/^\+7\d{10}$/.test(t)),"Неверный формат"),m("url",(t=>o(t)),"Неверный формат"),m("not-numbers",(t=>!/[0-9]/.test(t)),"Неверный формат");const v=Symbol.for("formfather.shared"),E=null!==(b=globalThis[v])&&void 0!==b?b:globalThis[v]={defaultParams:{},defaultValidationSchema:void 0},w="_form";let S=0;const F=new WeakMap,A={tel:{selector:'input[type="tel"]',rules:["tel"]},email:{selector:'input[type="email"]',rules:["email"]},required:{selector:"[required]",rules:["required"]},url:{selector:'input[type="url"]',rules:["url"]},"not-numbers":{selector:'input[data-validate="not-numbers"]',rules:["not-numbers"]}};class V{static get defaultParams(){return E.defaultParams}static set defaultParams(t){E.defaultParams=t}static get defaultValidationSchema(){var t;return null!==(t=E.defaultValidationSchema)&&void 0!==t?t:E.defaultValidationSchema=A}static set defaultValidationSchema(t){E.defaultValidationSchema=t}constructor(t,e={}){this.$submits=[],this.waitResponse=!1,this.inputs=null,this.fieldEventHandlers=[],this.liveValidationTimers=new WeakMap,this.fieldValidationTokens=new WeakMap,this.fieldValidationTokenCounter=0,this.errors=[],this._anim={duration:220,easing:"ease",paddingTopOpened:16,iconDuration:700},this.$el=t,this.options=e;this.config=Object.assign({},{onSubmit:()=>{},onResponse:()=>{},onResponseSuccess:()=>{},onResponseUnsuccess:()=>{},customTypeError:null,loaderColor:"#fff",showLoaderButton:!0,scrollToFirstErroredInput:!0,focusFirstErroredInput:!1,logging:!1,inputSelector:".input",inputWrapperSelector:".input-primary",validateOn:"submit",revalidateOn:["input","change"],validationDebounce:0,errorContainerAttribute:"data-error-container",validationStateAttribute:"data-form-father-state",observeMutations:!1},V.defaultParams,e),this.$el?(this.$submits=this.findSubmitElements(),this.isCorrectArguments()&&(this.bindSubmitHandlers(),this.initialization(),this.bindFieldValidationEvents(),this.setupMutationObserver(),F.set(this.$el,this))):console.warn("Empty $el")}getOptions(){return this.options}static initAll(t="form[data-form-father]",e={}){return"undefined"==typeof document?[]:Array.from(document.querySelectorAll(t)).filter((t=>"FORM"===t.tagName)).map((t=>{const r=F.get(t);return r?(r.updateOptions(e),r):new V(t,e)}))}updateOptions(t){return this.options=Object.assign(Object.assign({},this.options),t),this.config=Object.assign(Object.assign({},this.config),t),this.refreshControls(),this.bindSubmitHandlers(),this.bindFieldValidationEvents(),this.setupMutationObserver(),this}getErrors(){return this.errors.map((t=>Object.assign({},t)))}getValues(){return d(this.$el)}normalizeValueList(t){return Array.isArray(t)?t.map(String):[String(t)]}setValues(t){return this.getAllFields().forEach((e=>{if(!e.name||!Object.prototype.hasOwnProperty.call(t,e.name))return;const r=t[e.name];if(!(e instanceof HTMLInputElement&&"file"===e.type)){if(e instanceof HTMLInputElement&&"checkbox"===e.type)e.checked="boolean"==typeof r?r:this.normalizeValueList(r).includes(e.value);else if(e instanceof HTMLInputElement&&"radio"===e.type)e.checked=String(r)===e.value;else if(e instanceof HTMLSelectElement&&e.multiple){const t=this.normalizeValueList(r);Array.from(e.options).forEach((e=>{e.selected=t.includes(e.value)}))}else e.value=null==r?"":String(r);e.dispatchEvent(new Event("input",{bubbles:!0})),e.dispatchEvent(new Event("change",{bubbles:!0}))}})),this}clearErrors(){return this.errors=[],this.getAllFields().forEach((t=>{this.hideError(t),this.clearFieldValidationState(t)})),this.hideErrorForm(),this}isErrorResponse(t){return!!t&&"object"==typeof t&&"string"==typeof t.name&&"string"==typeof t["error-msg"]}normalizeValidationIssues(t,e){return null==t||!0===t?[]:!1===t?[{message:"Некорректные данные формы",source:e}]:"string"==typeof t?[{message:t,source:e}]:Array.isArray(t)?t.flatMap((t=>this.normalizeValidationIssues(t,e))):this.isErrorResponse(t)?[{field:t.name,rule:e,message:t["error-msg"],source:e}]:"object"==typeof t&&"message"in t&&"string"==typeof t.message?[Object.assign(Object.assign({},t),{source:t.source||e})]:"object"==typeof t?Object.entries(t).map((([t,r])=>({field:t,rule:e,message:r,source:e}))):[]}applyValidationIssue(t,e){const r=t.source||e,i=t.rule||r,n=t.field||w;if(n===w)return this.setErrorRecord(w,i,t.message,r),this.showErrorForm(t.message),null;const s=this.findFieldByName(n);return s?(this.showFieldError(s,t.message,r,i),s):(this.setErrorRecord(n,i,t.message,r),console.warn(`Не найдено поле с именем: ${n}, для вывода ошибки: ${t.message}`),null)}setErrors(t,e="server"){return this.normalizeValidationIssues(t,e).forEach((t=>{this.applyValidationIssue(t,e)})),this}validateFormValidators(){return e(this,void 0,void 0,(function*(){const t=this.config.formValidators?Array.isArray(this.config.formValidators)?this.config.formValidators:[this.config.formValidators]:[],e=[];let r=!0;for(const i of t){const t=yield i({form:this,formElement:this.$el,values:this.getValues(),errors:this.getErrors()}),n=this.normalizeValidationIssues(t,"client");0!==n.length&&(r=!1,n.forEach((t=>{const r=this.applyValidationIssue(t,"client");r&&e.push(r)})))}return{ok:r,erroredInputs:e}}))}reset(t={}){return this.$el.reset(),this.refreshControls(),!1!==t.clearErrors&&this.clearErrors(),this}static setDefaultParams(t){this.defaultParams=Object.assign(Object.assign({},this.defaultParams),t)}refreshControls(){this.inputs=this.$el.querySelectorAll(this.config.inputSelector)}findSubmitElements(){return Array.from(this.$el.querySelectorAll('input[type="submit"], input[type="image"], button[type="submit"], button:not([type])'))}bindSubmitHandlers(){this._onSubmitClickHandler||(this._onSubmitClickHandler=t=>e(this,void 0,void 0,(function*(){yield this.handleSubmitEvent(t)}))),this.$submits.forEach((t=>{t.removeEventListener("click",this._onSubmitClickHandler)})),this.$submits=this.findSubmitElements(),this.$submits.forEach((t=>{t.addEventListener("click",this._onSubmitClickHandler)}))}initialization(){this.refreshControls(),this._onSubmitHandler=t=>e(this,void 0,void 0,(function*(){yield this.handleSubmitEvent(t)})),this.$el.addEventListener("submit",this._onSubmitHandler)}handleSubmitEvent(t){return e(this,void 0,void 0,(function*(){var e,r,i,n,s,o;t.preventDefault(),null===(r=(e=this.config).onBeforeValidate)||void 0===r||r.call(e,this);const a=yield this.validate();return null===(n=(i=this.config).onAfterValidate)||void 0===n||n.call(i,a,this),a?this.submit():(null===(o=(s=this.config).onValidationError)||void 0===o||o.call(s,this.getErrors(),this),{success:!1,error:this.getErrors()})}))}normalizeTriggers(t,e){const r=null==t?e:Array.isArray(t)?t:[t];return Array.from(new Set(r))}clearFieldValidationEvents(){this.fieldEventHandlers.forEach((({$input:t,event:e,handler:r})=>{t.removeEventListener(e,r)})),this.fieldEventHandlers=[]}bindFieldValidationEvents(){this.clearFieldValidationEvents();const t=this.normalizeTriggers(this.config.validateOn,["submit"]).filter((t=>"submit"!==t)),e=this.normalizeTriggers(this.config.revalidateOn,["input","change"]).filter((t=>"submit"!==t)),r=Array.from(new Set([...t,...e]));if(0===r.length)return;this.getAllFields().forEach((e=>{r.forEach((r=>{const i=()=>{(t.includes(r)||this.hasFieldError(e))&&this.scheduleFieldValidation(e)};e.addEventListener(r,i),this.fieldEventHandlers.push({$input:e,event:r,handler:i})}))}))}scheduleFieldValidation(t){const e=this.liveValidationTimers.get(t);e&&clearTimeout(e);const r=Math.max(0,Number(this.config.validationDebounce)||0),i=()=>{this.validateField(t)};if(0===r)return void i();const n=setTimeout(i,r);this.liveValidationTimers.set(t,n)}setupMutationObserver(){var t;null===(t=this.mutationObserver)||void 0===t||t.disconnect(),this.mutationObserver=void 0,this.config.observeMutations&&"undefined"!=typeof MutationObserver&&(this.mutationObserver=new MutationObserver((()=>{this.refreshControls(),this.bindSubmitHandlers(),this.bindFieldValidationEvents()})),this.mutationObserver.observe(this.$el,{childList:!0,subtree:!0}))}getErrorContainer(t){const e=this.config.errorContainerAttribute||"data-error-container",r=t.getAttribute(e);if(!r)return null;try{return document.querySelector(r)}catch(t){return console.warn(`[FormFather] Invalid error container selector "${r}"`),null}}showError(t,e){t.setAttribute("aria-invalid","true");const i=this.getErrorContainer(t);if(i)return i.textContent=e,i.hasAttribute("role")||i.setAttribute("role","alert"),void i.removeAttribute("hidden");const n=r(t,this.config.inputWrapperSelector);if(!n)return void console.warn(`Не найдена обёртка для поля с именем: ${t.name||t.type}`);const s=n.showError;if("function"==typeof s)return void s.call(n,e);n.classList.add("input__wrapper--error");let o=n.querySelector("[data-form-father-error]");o||(o=document.createElement("div"),o.setAttribute("data-form-father-error",""),o.setAttribute("role","alert"),t.insertAdjacentElement("afterend",o)),o.textContent=e}showLoader(){this.$submits.forEach((t=>{if(t.classList.add("button--loading"),t.querySelector(".button__loader"))return;const e="form-father-loader-"+S++,r=`\n\t\t\t\t<svg height="38" viewBox="0 0 38 38" width="38" xmlns="http://www.w3.org/2000/svg">\n\t\t\t\t\t<defs>\n\t\t\t\t\t<linearGradient id="${e}" x1="8.042%" x2="65.682%" y1="0%" y2="23.865%">\n\t\t\t\t\t\t<stop offset="0%" stop-color="${this.config.loaderColor}" stop-opacity="0"/>\n\t\t\t\t\t\t<stop offset="63.146%" stop-color="${this.config.loaderColor}" stop-opacity=".631"/>\n\t\t\t\t\t\t<stop offset="100%" stop-color="${this.config.loaderColor}"/>\n\t\t\t\t\t</linearGradient>\n\t\t\t\t\t</defs>\n\t\t\t\t\t<g fill-rule="evenodd" fill="none">\n\t\t\t\t\t<g transform="translate(1 1)">\n\t\t\t\t\t\t<path d="M36 18c0-9.94-8.06-18-18-18" id="Oval-2" stroke-width="5" stroke="url(#${e})"></path>\n\t\t\t\t\t\t<circle cx="36" cy="18" fill="${this.config.loaderColor}" r="1"></circle>\n\t\t\t\t\t</g>\n\t\t\t\t\t</g>\n\t\t\t\t</svg>\n\t\t\t`,i=document.createElement("span");i.innerHTML=r,i.className="button__loader",t.insertAdjacentElement("afterbegin",i),requestAnimationFrame((()=>{i.setAttribute("data-showed","")}))}))}hideLoader(){this.$submits.forEach((t=>{t.classList.remove("button--loading");const e=t.querySelector(".button__loader");e&&(e.classList.remove("active"),e.removeAttribute("data-showed"),setTimeout((()=>{e.remove()}),250))}))}hideError(t){var e;const i=r(t,this.config.inputWrapperSelector)||t.parentElement,n=this.getErrorContainer(t);t.removeAttribute("aria-invalid"),n&&(n.textContent="",n.setAttribute("hidden","")),null===(e=null==i?void 0:i.querySelector("[data-form-father-error]"))||void 0===e||e.remove(),null==i||i.classList.remove("input__wrapper--error"),i&&i.classList.contains("input__wrapper")&&i.getAttribute("data-type-error")}scrollToFirstErroredInput(t){if(t.length>0){const e=Array.from(this.$el.querySelectorAll("input, textarea, select"));let r=null,i=1/0;if(t.forEach((t=>{const n=e.findIndex((e=>e===t));n<i&&(i=n,r=t)})),r){const t=r.getBoundingClientRect(),e=window.pageYOffset||document.documentElement.scrollTop,i=t.top+e-window.innerHeight/2;document.documentElement.setAttribute("data-now-scrolling",""),window.scrollTo({top:i,behavior:"smooth"}),setTimeout((()=>{document.documentElement.removeAttribute("data-now-scrolling")}),800)}}}focusFirstErroredInput(t){var e;if(0===t.length)return;const r=Array.from(this.$el.querySelectorAll("input, textarea, select")),i=t.slice().sort(((t,e)=>r.indexOf(t)-r.indexOf(e)))[0];null===(e=null==i?void 0:i.focus)||void 0===e||e.call(i,{preventScroll:!0})}getValidationSchema(){return Object.assign(Object.assign({},V.defaultValidationSchema||{}),this.config.validationSchema||{})}getAllFields(t=this.$el){return Array.from(t.querySelectorAll("input, textarea, select"))}getFieldKey(t){return t.name||t.id||t.getAttribute("data-field")||t.type||"field"}hasFieldError(t){const e=this.getFieldKey(t);return"true"===t.getAttribute("aria-invalid")||this.errors.some((t=>t.field===e))}clearFieldErrorRecord(t){const e=this.getFieldKey(t);this.errors=this.errors.filter((t=>t.field!==e))}setErrorRecord(t,e,r,i){this.errors.push({field:t,rule:e,message:r,source:i})}getValidationStateAttribute(){return this.config.validationStateAttribute||"data-form-father-state"}setFieldValidationState(t,e){const r=this.getValidationStateAttribute();e?t.setAttribute(r,e):t.removeAttribute(r)}startFieldValidation(t){const e=++this.fieldValidationTokenCounter;return this.fieldValidationTokens.set(t,e),t.setAttribute("aria-busy","true"),this.setFieldValidationState(t,"validating"),e}isLatestFieldValidation(t,e){return null==e||this.fieldValidationTokens.get(t)===e}finishFieldValidation(t,e,r){this.isLatestFieldValidation(t,e)&&(t.removeAttribute("aria-busy"),this.setFieldValidationState(t,r?"valid":"invalid"))}clearFieldValidationState(t){this.fieldValidationTokens.delete(t),t.removeAttribute("aria-busy"),this.setFieldValidationState(t,null)}setValidationError(t,e,r,i){this.clearFieldErrorRecord(t),this.setErrorRecord(this.getFieldKey(t),e,r,i)}resolveField(t){return"string"!=typeof t?t:this.findFieldByName(t)}parseRuleString(t){const[e,...r]=t.split(":").map((t=>t.trim()));return r.length>0?{rule:e,params:1===r.length?r[0]:r}:e}parseValidationAttribute(t){return(null!=t?t:"").split(/[,\s|]+/).map((t=>t.trim())).filter(Boolean).map((t=>this.parseRuleString(t)))}getRuleName(t){return"string"==typeof t?t:t.rule}getRuleIdentity(t){return JSON.stringify(t)}collectRules(t,e=[],r=new Set){const i=[];t.hasAttribute("required")&&i.push("required"),i.push(...e);const n=this.parseValidationAttribute(t.getAttribute("data-validate")).filter((t=>{const e=this.getRuleName(t);return f(e)||!r.has(e)}));i.push(...n),i.push(...this.parseValidationAttribute(t.getAttribute("data-custom-validate")));const s=[],o=new Set;for(const t of i){const e=this.getRuleName(t);if(!f(e)){console.warn(`[FormFather] Unknown validation rule "${e}"`);continue}const r=this.getRuleIdentity(t);o.has(r)||(o.add(r),s.push(t))}return s}getRuleMessage(t,e,r,i,n){const s=t.getAttribute(`data-error-${e}`);return n||s||r[e]||i}fieldMatchesSchemaDefinition(t,e,r){if(r.selector)try{return t.matches(r.selector)}catch(t){return console.warn(`[FormFather] Invalid validation selector "${r.selector}"`),!1}return this.parseValidationAttribute(t.getAttribute("data-validate")).some((t=>this.getRuleName(t)===e))}getFieldValidationConfig(t,e=this.getValidationSchema()){const r=[],i={};for(const[n,s]of Object.entries(e))this.fieldMatchesSchemaDefinition(t,n,s)&&(r.push(...s.rules),Object.assign(i,s.messages||{}));return{rules:this.collectRules(t,r,new Set(Object.keys(e))),messages:i}}getValidationFields(t,e){const r=new Set,i=t=>{Array.from(t).forEach((t=>{(t instanceof HTMLInputElement||t instanceof HTMLTextAreaElement||t instanceof HTMLSelectElement)&&r.add(t)}))};for(const[n,s]of Object.entries(e))if(s.selector)try{i(t.querySelectorAll(s.selector))}catch(t){console.warn(`[FormFather] Invalid validation selector "${s.selector}"`)}else this.getAllFields(t).filter((t=>this.parseValidationAttribute(t.getAttribute("data-validate")).some((t=>this.getRuleName(t)===n)))).forEach((t=>r.add(t)));return i(t.querySelectorAll("[required], [data-validate], [data-custom-validate]")),Array.from(r)}validateInput(t,r){return e(this,arguments,void 0,(function*(t,e,r={},i=this.$el,n){if(t.hasAttribute("data-no-validate"))return!0;const s=t instanceof HTMLInputElement&&("checkbox"===t.type||"radio"===t.type)?!t.checked:0===t.value.trim().length,o=e.some((t=>"required"===this.getRuleName(t)));if(s&&!o)return this.isLatestFieldValidation(t,n)?(this.clearFieldErrorRecord(t),this.hideError(t),!0):!this.hasFieldError(t);for(const s of e){const{rule:e,params:o}="string"==typeof s?{rule:s,params:void 0}:{rule:s.rule,params:s.params},a=f(e);if(!a)continue;const l=yield a.fn(t.value,t,i,o),d="object"==typeof l&&null!==l,c=d?!0===l.valid:!!l;if(!this.isLatestFieldValidation(t,n))return!this.hasFieldError(t);if(d&&l.effect&&(yield l.effect({value:t.value,$input:t,$form:i,params:o})),!this.isLatestFieldValidation(t,n))return!this.hasFieldError(t);if(!c){const i=l,n=this.getRuleMessage(t,e,r,a.defaultMessage,d&&"string"==typeof i.message?i.message:void 0);return this.setValidationError(t,e,n,"client"),this.showError(t,n),!1}if(d&&l.stopOthers)return!0}return this.isLatestFieldValidation(t,n)?(this.clearFieldErrorRecord(t),this.hideError(t),!0):!this.hasFieldError(t)}))}runFieldValidation(t,r){return e(this,arguments,void 0,(function*(t,e,r={},i=this.$el){const n=this.startFieldValidation(t);try{const s=yield this.validateInput(t,e,r,i,n);return this.finishFieldValidation(t,n,s),s}catch(e){throw this.isLatestFieldValidation(t,n)&&this.clearFieldValidationState(t),e}}))}getRadioGroup(t,e){return t instanceof HTMLInputElement&&"radio"===t.type&&t.name?Array.from(e.querySelectorAll('input[type="radio"]')).filter((e=>e.name===t.name)):[]}showFieldError(t,e,r="manual",i=r){const n=this.resolveField(t);return n?(this.setValidationError(n,i,e,r),this.showError(n,e),n.removeAttribute("aria-busy"),this.setFieldValidationState(n,"invalid"),!0):(console.warn(`[FormFather] Field "${String(t)}" not found`),!1)}validateField(t){return e(this,void 0,void 0,(function*(){const e=this.resolveField(t);if(!e)return console.warn(`[FormFather] Field "${String(t)}" not found`),!1;const{rules:r,messages:i}=this.getFieldValidationConfig(e);return this.runFieldValidation(e,r,i)}))}validate(){return e(this,arguments,void 0,(function*(t=this.$el){const e=this.getValidationSchema(),r=[],i=new WeakSet;let n=!0;this.errors=[];for(const s of this.getValidationFields(t,e)){if(i.has(s))continue;const o=this.getRadioGroup(s,t);o.length>0?o.forEach((t=>i.add(t))):i.add(s);const{rules:a,messages:l}=this.getFieldValidationConfig(s,e);(yield this.runFieldValidation(s,a,l,t))||(n=!1,r.push(s))}const s=yield this.validateFormValidators();s.ok||(n=!1,r.push(...s.erroredInputs));const o=r.filter((t=>!t.hasAttribute("data-no-error-scroll")));return!n&&this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(o),!n&&this.config.focusFirstErroredInput&&this.focusFirstErroredInput(o),n}))}disableSubmit(){this.$submits.forEach((t=>{t.setAttribute("disabled","")}))}enableSubmit(){this.$submits.forEach((t=>{t.removeAttribute("disabled")}))}clearInputs(){var t;null===(t=this.inputs)||void 0===t||t.forEach((t=>{var e;const r=t;"radio"===r.type||"checkbox"===r.type?r.checked=!1:(r.value="",null===(e=r.parentElement)||void 0===e||e.classList.remove("filled"),r.dispatchEvent(new Event("input")))}))}_prefersReducedMotion(){var t;return"undefined"!=typeof window&&(null===(t=window.matchMedia)||void 0===t?void 0:t.call(window,"(prefers-reduced-motion: reduce)").matches)}_canAnimate(t){return!(!t||!("animate"in t)||this._prefersReducedMotion())}_cancelAnimations(t){var e;const r=t;null===(e=null==r?void 0:r.getAnimations)||void 0===e||e.call(r).forEach((t=>t.cancel()))}showErrorForm(t){var e;const r=this.$el.querySelectorAll(this.config.inputWrapperSelector),i=r[r.length-1];let n=this.$el.querySelector(".error-block-under-input__wrapper");!n||i&&i.nextElementSibling!==n?(n=document.createElement("div"),n.className="error-block-under-input__wrapper",n.innerHTML='\n <div class="error-block-under-input error-block-under-input--warning" style="display:grid;grid-template-columns:auto 1fr;gap:8px;align-items:start;">\n <div class="error-block-under-input__icon-wrapper" style="position:relative;width:20px;height:20px;">\n <span class="error-block-under-input__icon" style="display:block;width:20px;height:20px;"></span>\n <span class="error-block-under-input__icon error-block-under-input__icon--animated" style="display:block;position:absolute;inset:0;"></span>\n </div>\n <p class="error-block-under-input__text" style="margin:0;">\n <span class="error-block-under-input__main-text"></span>\n <span class="error-block-under-input__secondary-text"></span>\n </p>\n </div>\n ',Object.assign(n.style,{boxSizing:"border-box",overflow:"hidden",height:"0px",paddingTop:"0px",opacity:"0"}),i?i.insertAdjacentElement("afterend",n):this.$el.appendChild(n)):(n.style.boxSizing="border-box",n.style.overflow="hidden"),n.querySelector(".error-block-under-input__main-text").textContent=t;const s=n.querySelector(".error-block-under-input__secondary-text");s&&(s.textContent=""),null===(e=n.querySelector(".error-block-under-input"))||void 0===e||e.classList.remove("error-block-under-input--success");const o=n.style.height,a=n.style.paddingTop;n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`;const l=n.clientHeight;if(n.style.height=o||"0px",n.style.paddingTop=a||"0px",parseFloat(n.style.height)===l&&1===parseFloat(n.style.opacity||"1"))this._ringIcon(n);else{if(this._cancelAnimations(n),this._canAnimate(n)){n.animate([{height:`${parseFloat(n.style.height)||0}px`,paddingTop:`${parseFloat(n.style.paddingTop)||0}px`,opacity:parseFloat(n.style.opacity||"0")},{height:`${l}px`,paddingTop:`${this._anim.paddingTopOpened}px`,opacity:1}],{duration:this._anim.duration,easing:this._anim.easing,fill:"forwards"}).addEventListener("finish",(()=>{n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`,n.style.opacity="1"}))}else n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`,n.style.opacity="1";this._ringIcon(n)}this.$el.addEventListener("input",(()=>this.hideErrorForm()),{passive:!0,once:!0})}hideErrorForm(){const t=this.$el.querySelectorAll(this.config.inputWrapperSelector),e=t[t.length-1],r=(null==e?void 0:e.nextElementSibling)||this.$el.querySelector(".error-block-under-input__wrapper");if(!r||!r.classList.contains("error-block-under-input__wrapper"))return;if((""===r.style.height||"auto"===r.style.height)&&(r.style.height=`${r.clientHeight}px`),r.style.paddingTop=r.style.paddingTop||`${this._anim.paddingTopOpened}px`,r.style.opacity=r.style.opacity||"1",this._cancelAnimations(r),this._canAnimate(r)){r.animate([{height:`${parseFloat(r.style.height)||r.clientHeight}px`,paddingTop:`${parseFloat(r.style.paddingTop)||this._anim.paddingTopOpened}px`,opacity:parseFloat(r.style.opacity||"1")},{height:"0px",paddingTop:"0px",opacity:0}],{duration:this._anim.duration,easing:this._anim.easing,fill:"forwards"}).addEventListener("finish",(()=>{r.remove()}))}else r.remove()}_ringIcon(t){const e=t.querySelector(".error-block-under-input__icon--animated");e&&(this._cancelAnimations(e),this._canAnimate(e)&&e.animate([{transform:"rotate(0deg) scale(1)",opacity:0},{transform:"rotate(-8deg) scale(1.05)",opacity:1,offset:.15},{transform:"rotate(8deg) scale(1.05)",opacity:1,offset:.3},{transform:"rotate(-6deg) scale(1.03)",opacity:1,offset:.45},{transform:"rotate(6deg) scale(1.03)",opacity:1,offset:.6},{transform:"rotate(0deg) scale(1)",opacity:0}],{duration:this._anim.iconDuration,easing:"ease"}))}destroy(){var t;this._onSubmitHandler&&(this.$el.removeEventListener("submit",this._onSubmitHandler),this._onSubmitHandler=void 0),this._onSubmitClickHandler&&(this.$submits.forEach((t=>{t.removeEventListener("click",this._onSubmitClickHandler)})),this._onSubmitClickHandler=void 0),this.clearFieldValidationEvents(),null===(t=this.mutationObserver)||void 0===t||t.disconnect(),this.mutationObserver=void 0;const e=this.$el.querySelector(".error-block-under-input__wrapper");e&&(this._cancelAnimations(e),e.remove()),this.$submits.forEach((t=>{t.classList.remove("button--loading");const e=t.querySelector(".button__loader");e&&e.remove()}));this.$el.querySelectorAll("input, textarea, select").forEach((t=>{this.hideError(t),this.clearFieldValidationState(t)})),document.documentElement.removeAttribute("data-now-scrolling"),this.inputs=null,this.$submits=[],this.errors=[],this.waitResponse=!1,F.delete(this.$el)}isCorrectArguments(){return"FORM"!==this.$el.tagName?(console.warn("$el не является тегом form"),!1):0!==this.$submits.length||(console.warn("В форме нет кнопки с типом submit"),!1)}formDataToObject(t){const e={};return t.forEach(((t,r)=>{Object.prototype.hasOwnProperty.call(e,r)?(Array.isArray(e[r])||(e[r]=[e[r]]),e[r].push(t)):e[r]=t})),e}appendFormDataValue(t,e,r){null!=r&&(Array.isArray(r)?r.forEach(((r,i)=>{this.appendFormDataValue(t,`${e}[${i}]`,r)})):r instanceof Blob?t.append(e,r):t.append(e,"object"==typeof r?JSON.stringify(r):String(r)))}objectToFormData(t){const e=new FormData;return Object.entries(t).forEach((([t,r])=>{this.appendFormDataValue(e,t,r)})),e}stringifyFormDataValue(t){return"string"==typeof t?t:t.name}formDataToUrlSearchParams(t){const e=new URLSearchParams;return t.forEach(((t,r)=>{e.append(r,this.stringifyFormDataValue(t))})),e}formDataToTextBody(t){return Array.from(t).map((([t,e])=>`${encodeURIComponent(t)}=${encodeURIComponent(this.stringifyFormDataValue(e))}`)).join("\n")}isBodylessMethod(t){return"GET"===t||"HEAD"===t}appendQueryString(t,e){const r=e.toString();if(!r)return t;const i=t.includes("?")?"&":"?";return`${t}${i}${r}`}findFieldByName(t){return Array.from(this.$el.querySelectorAll("input, textarea, select")).find((e=>e.name===t))||null}parseResponseBody(t){return e(this,void 0,void 0,(function*(){try{const e=yield t.json();return e&&"object"==typeof e?e:{success:!1,error:!0,"error-msg":"Некорректный ответ сервера"}}catch(t){return{success:!1,error:!0,"error-msg":"Некорректный ответ сервера"}}}))}handleUnsuccessfulResponse(t){if(!0===t.error&&(t["error-msg"]&&this.showErrorForm(t["error-msg"]),t.errors)){const e=[];t.errors.forEach((t=>{const r=this.findFieldByName(t.name);r?(e.push(r),this.setValidationError(r,"server",t["error-msg"],"server"),this.showError(r,t["error-msg"]),r.removeAttribute("aria-busy"),this.setFieldValidationState(r,"invalid")):console.warn(`Не найдено поле с именем: ${t.name}, для вывода ошибки: ${t["error-msg"]}`)})),!0===this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(e),!0===this.config.focusFirstErroredInput&&this.focusFirstErroredInput(e)}}sendData(){return e(this,void 0,void 0,(function*(){var t;const e=this.$el.getAttribute("action")||"",r=(null===(t=this.$el.getAttribute("method"))||void 0===t?void 0:t.toUpperCase())||"POST",i=this.$el.getAttribute("enctype")||"application/x-www-form-urlencoded";let n=null,s={};const o=a(this.$el);let l=this.formDataToObject(o);if(this.config.logging)for(const t of o.keys())console.log(o.get(t));const d="function"==typeof this.config.wrapData;"function"==typeof this.config.wrapData&&(l=this.config.wrapData(l));const c=d?this.objectToFormData(l):o;if(this.isBodylessMethod(r))return fetch(this.appendQueryString(e,this.formDataToUrlSearchParams(c)),{method:r,headers:s});switch(i){case"application/x-www-form-urlencoded":n=this.formDataToUrlSearchParams(c).toString(),s["Content-Type"]="application/x-www-form-urlencoded";break;case"multipart/form-data":n=c;break;case"text/plain":n=this.formDataToTextBody(c),s["Content-Type"]="text/plain";break;case"application/json":n=JSON.stringify(l),s["Content-Type"]="application/json";break;default:console.warn(`Неизвестный enctype: ${i}. Используется application/x-www-form-urlencoded.`),n=this.formDataToUrlSearchParams(c).toString(),s["Content-Type"]="application/x-www-form-urlencoded"}return fetch(e,{method:r,body:n,headers:s})}))}submit(){return e(this,void 0,void 0,(function*(){var t,e,r,i,n,s,o,a,d,c,u,h;if(!this.waitResponse){let p=!1;const f=()=>{p||(this.waitResponse=!1,!0===this.config.showLoaderButton&&this.hideLoader(),p=!0)};try{if(!1===(yield null===(e=(t=this.config).onBeforeSubmit)||void 0===e?void 0:e.call(t,this)))return{success:!1};!0===this.config.showLoaderButton&&this.showLoader(),null===(i=(r=this.config).onSubmit)||void 0===i||i.call(r,this),this.waitResponse=!0;const u=yield this.sendData(),h=yield this.parseResponseBody(u);return f(),null===(s=(n=this.config).onResponse)||void 0===s||s.call(n,h,this),u.ok&&!0===h.success?null===(a=(o=this.config).onResponseSuccess)||void 0===a||a.call(o,h,this):(null===(c=(d=this.config).onResponseUnsuccess)||void 0===c||c.call(d,h,this),this.handleUnsuccessfulResponse(h)),l(h),{success:u.ok&&!0===h.success,response:u,responseBody:h}}catch(t){throw f(),null===(h=(u=this.config).onSubmitError)||void 0===h||h.call(u,t,this),t}}}))}}return t.FORM_ERROR_FIELD=w,t.blockScrollBody=function(){window.lastBlockScrollPosition=window.pageYOffset,document.body.style.top=-window.lastBlockScrollPosition+"px",document.body.classList.add("body--block-scroll")},t.closest=r,t.createFieldValidator=function(t,r){return(i,n,s,o)=>e(this,void 0,void 0,(function*(){return!!(yield t(i,n,s,o))||{valid:!1,message:r}}))},t.createSchemaValidator=y,t.default=V,t.getAllValidators=function(){return new Map(h)},t.getValidator=f,t.isEmailValid=function(t){return/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu.test(t)},t.isPhoneValid=function(t){const e=[];return Array.from(t).forEach((t=>{(Number(t)||0===Number(t))&&e.push(t)})),!(e.length<11||e.length<12&&"+"===t[1])},t.isUrlValid=o,t.parseCommonResponseProperties=l,t.registerSchemaValidator=function(t,e,r,i={}){const{override:n}=i;p(t,y(e,function(t,e){var r={};for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&e.indexOf(i)<0&&(r[i]=t[i]);if(null!=t&&"function"==typeof Object.getOwnPropertySymbols){var n=0;for(i=Object.getOwnPropertySymbols(t);n<i.length;n++)e.indexOf(i[n])<0&&Object.prototype.propertyIsEnumerable.call(t,i[n])&&(r[i[n]]=t[i[n]])}return r}(i,["override"])),r,{override:n})},t.registerValidator=p,t.serializeFormToJSON=d,t.serializeToFormData=a,t.unblockScrollBody=function(){document.body.classList.remove("body--block-scroll"),document.body.style.top="",window.scrollTo(0,window.lastBlockScrollPosition||0)},Object.defineProperty(t,"__esModule",{value:!0}),t}({});
|
|
1
|
+
var FormFather=function(t){"use strict";function e(t,e,r,i){return new(r||(r=Promise))((function(n,o){function s(t){try{l(i.next(t))}catch(t){o(t)}}function a(t){try{l(i.throw(t))}catch(t){o(t)}}function l(t){var e;t.done?n(t.value):(e=t.value,e instanceof r?e:new r((function(t){t(e)}))).then(s,a)}l((i=i.apply(t,e||[])).next())}))}function r(t,e){if(null==t)return null;let r=t;for(;r;){if(r.matches(e))return r;r=r.parentElement}return null}"function"==typeof SuppressedError&&SuppressedError;const i=["http:","https:"];function n(t){return t.length>0&&t.length<=63&&/^[a-z\d-]+$/i.test(t)&&!t.startsWith("-")&&!t.endsWith("-")}function o(t){return!!t&&("localhost"===t||function(t){const e=t.split(".");return 4===e.length&&e.every((t=>{if(!/^\d{1,3}$/.test(t))return!1;const e=Number(t);return e>=0&&e<=255}))}(t)||function(t){if(!t.startsWith("[")||!t.endsWith("]"))return!1;const e=t.slice(1,-1);return e.includes(":")&&/^[\da-f:.]+$/i.test(e)}(t)||function(t){const e=t.split(".");return e.length>=2&&e.every(n)}(t))}function s(t){const e=function(t){const e=t.trim();return 0===e.length?"":e.startsWith("//")?`https:${e}`:/^[a-z][a-z\d+.-]*:\/\//i.test(e)?e:`https://${e}`}(t);if(0===e.length)return!1;try{const t=new URL(e);return i.includes(t.protocol)&&o(t.hostname)}catch(t){return!1}}function a(t){const e="FORM"===t.tagName?Array.from(t.elements):Array.from(t.querySelectorAll("input, select, textarea, button")),r=new FormData;return e.forEach((t=>{if(!(t instanceof HTMLInputElement||t instanceof HTMLSelectElement||t instanceof HTMLTextAreaElement||t instanceof HTMLButtonElement))return;if(!t.name||t.disabled||t.hasAttribute("data-no-serialize"))return;if(t instanceof HTMLButtonElement)return;const{name:e,type:i}=t;if("file"===i)if(t.multiple){const i=t.files;if(i)for(let t=0;t<i.length;t++)r.append(`${e}[${t}]`,i[t])}else{const i=t.files;i&&i[0]&&r.append(e,i[0])}else{let n;if("select-one"===i||"select-multiple"===i){const o=t;if("select-multiple"===i){Array.from(o.selectedOptions).map((t=>t.value)).forEach(((t,i)=>{r.append(`${e}[${i}]`,t)}))}else n=o.value,n&&r.append(e,n)}else if("tel"===i)n=t.value.split(/[\s()-]/).join("").replace(/^8/,"+7");else if("checkbox"===i||"radio"===i){if(!t.checked)return;n=t.value}else if("date"===i){const e=t.getAttribute("data-date-format");n=e?function(t,e){const r=new Date(t),i=t=>t.toString().padStart(2,"0"),n=i(r.getDate()),o=i(r.getMonth()+1),s=r.getFullYear().toString();return e.replace("dd",n).replace("mm",o).replace("yyyy",s)}(t.value,e):t.value}else n=t.value;n&&"select-one"!==i&&"select-multiple"!==i&&r.append(e,n)}})),r}function l(t){if(Object.prototype.hasOwnProperty.call(t,"redirect-url"))if(t["redirect-url-delay"]){const e=Number(t["redirect-url-delay"]);setTimeout((()=>{window.location.href=t["redirect-url"]}),e)}else window.location.href=t["redirect-url"];else if(Object.prototype.hasOwnProperty.call(t,"reload")){if(!0===t.reload)if(t["reload-delay"]){const e=Number(t["reload-delay"]);setTimeout((()=>{window.location.reload()}),e)}else window.location.reload()}else if(Object.prototype.hasOwnProperty.call(t,"toast")){const e=t.toast;(Array.isArray(e)?e:[e]).forEach((t=>{window.showToast(t)}))}else Object.prototype.hasOwnProperty.call(t,"error-toast")&&window.showToast(t["error-toast"])}function d(t){const e={};return new FormData(t).forEach(((t,r)=>{e.hasOwnProperty(r)?(Array.isArray(e[r])||(e[r]=[e[r]]),e[r].push(t)):e[r]=t})),e}var c;const u=Symbol.for("FormFather.validators"),h=null!==(c=globalThis[u])&&void 0!==c?c:globalThis[u]=new Map;function m(t,e,r,{override:i=!1}={}){!h.has(t)||i?h.set(t,{fn:e,defaultMessage:r}):console.warn(`[FormFather] Validator "${t}" already exists; pass { override: true } to replace`)}const f=t=>h.get(t);function p(t,e,r){h.has(t)||h.set(t,{fn:e,defaultMessage:r})}function g(t,e){return"function"==typeof e?e(t):e||function(t){var e;if(!t||"object"!=typeof t)return;const r=t.issues||t.errors;if(Array.isArray(r)){const t=null===(e=r.find((t=>"string"==typeof(null==t?void 0:t.message))))||void 0===e?void 0:e.message;if(t)return t}return"string"==typeof t.message?t.message:void 0}(t)}function y(t,r={}){return(i,n,o,s)=>e(this,void 0,void 0,(function*(){const e=r.mapValue?r.mapValue(i,n,o,s):i;try{if("function"==typeof t)return a=yield t(e),l=g(void 0,r.message),"object"==typeof a&&null!==a?a:void 0===a||!!a||{valid:!1,message:l};if("function"==typeof t.safeParse){const i=yield t.safeParse(e);return!!i.success||{valid:!1,message:g(i.error||i,r.message)}}if("function"==typeof t.parse)return yield t.parse(e),!0}catch(t){return{valid:!1,message:g(t,r.message)}}var a,l;return{valid:!1,message:g(void 0,r.message)}}))}function b(t,r){return(i,n,o,s)=>e(this,void 0,void 0,(function*(){return!!(yield t(i,n,o,s))||{valid:!1,message:r}}))}var v;p("required",((t,e,r)=>{if(e&&e.type){if("checkbox"===e.type)return e.checked;if("radio"===e.type){const t=r.querySelectorAll(`input[type="radio"][name="${e.name}"]`);return Array.from(t).some((t=>t.checked))}}return t.trim().length>0}),"Пустое значение"),p("email",(t=>/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)),"Неверный формат"),p("tel",(t=>/^\+7\d{10}$/.test(t)),"Неверный формат"),p("url",(t=>s(t)),"Неверный формат"),p("not-numbers",(t=>!/[0-9]/.test(t)),"Неверный формат");const E=Symbol.for("formfather.shared"),S=null!==(v=globalThis[E])&&void 0!==v?v:globalThis[E]={defaultParams:{},defaultValidationSchema:void 0},w="_form";function A(t,e){return t[e]}function F(t){return Array.isArray(t)?0===t.length:null==t||0===String(t).trim().length}function V(t,r){return i=>e(this,void 0,void 0,(function*(){return!!(yield t(i))||function(t,r,i,n){return e(this,void 0,void 0,(function*(){const e="function"==typeof r?yield r(t):r;return"string"==typeof e?{field:i,rule:n,message:e}:Object.assign({field:i,rule:n},e)}))}(i,r)}))}let $=0,T=0;const O=new WeakMap,_="data-form-father-describedby-id",x="data-form-father-added-describedby-id",k="data-form-father-summary",L={tel:{selector:'input[type="tel"]',rules:["tel"]},email:{selector:'input[type="email"]',rules:["email"]},required:{selector:"[required]",rules:["required"]},url:{selector:'input[type="url"]',rules:["url"]},"not-numbers":{selector:'input[data-validate="not-numbers"]',rules:["not-numbers"]}};class C{static get defaultParams(){return S.defaultParams}static set defaultParams(t){S.defaultParams=t}static get defaultValidationSchema(){var t;return null!==(t=S.defaultValidationSchema)&&void 0!==t?t:S.defaultValidationSchema=L}static set defaultValidationSchema(t){S.defaultValidationSchema=t}constructor(t,e={}){this.$submits=[],this.waitResponse=!1,this.inputs=null,this.fieldEventHandlers=[],this.liveValidationTimers=new WeakMap,this.fieldValidationTokens=new WeakMap,this.fieldValidationTokenCounter=0,this.errors=[],this._anim={duration:220,easing:"ease",paddingTopOpened:16,iconDuration:700},this.$el=t,this.options=e;this.config=Object.assign({},{onSubmit:()=>{},onResponse:()=>{},onResponseSuccess:()=>{},onResponseUnsuccess:()=>{},customTypeError:null,loaderColor:"#fff",showLoaderButton:!0,scrollToFirstErroredInput:!0,focusFirstErroredInput:!1,logging:!1,inputSelector:".input",inputWrapperSelector:".input-primary",validateOn:"submit",revalidateOn:["input","change"],validationDebounce:0,errorContainerAttribute:"data-error-container",validationStateAttribute:"data-form-father-state",ariaDescribeErrors:!0,errorIdPrefix:"form-father-error",errorSummary:!1,observeMutations:!1},C.defaultParams,e),this.$el?(this.$submits=this.findSubmitElements(),this.isCorrectArguments()&&(this.bindSubmitHandlers(),this.initialization(),this.bindFieldValidationEvents(),this.setupMutationObserver(),O.set(this.$el,this))):console.warn("Empty $el")}getOptions(){return this.options}static initAll(t="form[data-form-father]",e={}){return"undefined"==typeof document?[]:Array.from(document.querySelectorAll(t)).filter((t=>"FORM"===t.tagName)).map((t=>{const r=O.get(t);return r?(r.updateOptions(e),r):new C(t,e)}))}updateOptions(t){return this.options=Object.assign(Object.assign({},this.options),t),this.config=Object.assign(Object.assign({},this.config),t),this.refreshControls(),this.bindSubmitHandlers(),this.bindFieldValidationEvents(),this.setupMutationObserver(),this}getErrors(){return this.errors.map((t=>Object.assign({},t)))}getValues(){return d(this.$el)}normalizeValueList(t){return Array.isArray(t)?t.map(String):[String(t)]}setValues(t){return this.getAllFields().forEach((e=>{if(!e.name||!Object.prototype.hasOwnProperty.call(t,e.name))return;const r=t[e.name];if(!(e instanceof HTMLInputElement&&"file"===e.type)){if(e instanceof HTMLInputElement&&"checkbox"===e.type)e.checked="boolean"==typeof r?r:this.normalizeValueList(r).includes(e.value);else if(e instanceof HTMLInputElement&&"radio"===e.type)e.checked=String(r)===e.value;else if(e instanceof HTMLSelectElement&&e.multiple){const t=this.normalizeValueList(r);Array.from(e.options).forEach((e=>{e.selected=t.includes(e.value)}))}else e.value=null==r?"":String(r);e.dispatchEvent(new Event("input",{bubbles:!0})),e.dispatchEvent(new Event("change",{bubbles:!0}))}})),this}clearErrors(){return this.errors=[],this.getAllFields().forEach((t=>{this.hideError(t),this.clearFieldValidationState(t)})),this.hideErrorForm(),this.hideErrorSummary(),this}isErrorResponse(t){return!!t&&"object"==typeof t&&"string"==typeof t.name&&"string"==typeof t["error-msg"]}normalizeValidationIssues(t,e){return null==t||!0===t?[]:!1===t?[{message:"Некорректные данные формы",source:e}]:"string"==typeof t?[{message:t,source:e}]:Array.isArray(t)?t.flatMap((t=>this.normalizeValidationIssues(t,e))):this.isErrorResponse(t)?[{field:t.name,rule:e,message:t["error-msg"],source:e}]:"object"==typeof t&&"message"in t&&"string"==typeof t.message?[Object.assign(Object.assign({},t),{source:t.source||e})]:"object"==typeof t?Object.entries(t).map((([t,r])=>({field:t,rule:e,message:r,source:e}))):[]}applyValidationIssue(t,e){const r=t.source||e,i=t.rule||r,n=t.field||w;if(n===w)return this.setErrorRecord(w,i,t.message,r),this.showErrorForm(t.message),null;const o=this.findFieldByName(n);return o?(this.showFieldError(o,t.message,r,i),o):(this.setErrorRecord(n,i,t.message,r),console.warn(`Не найдено поле с именем: ${n}, для вывода ошибки: ${t.message}`),null)}setErrors(t,e="server"){return this.normalizeValidationIssues(t,e).forEach((t=>{this.applyValidationIssue(t,e)})),this.syncErrorSummary(),this}validateFormValidators(){return e(this,void 0,void 0,(function*(){const t=this.config.formValidators?Array.isArray(this.config.formValidators)?this.config.formValidators:[this.config.formValidators]:[],e=[];let r=!0;for(const i of t){const t=yield i({form:this,formElement:this.$el,values:this.getValues(),errors:this.getErrors()}),n=this.normalizeValidationIssues(t,"client");0!==n.length&&(r=!1,n.forEach((t=>{const r=this.applyValidationIssue(t,"client");r&&e.push(r)})))}return{ok:r,erroredInputs:e}}))}reset(t={}){return this.$el.reset(),this.refreshControls(),!1!==t.clearErrors&&this.clearErrors(),this}static setDefaultParams(t){this.defaultParams=Object.assign(Object.assign({},this.defaultParams),t)}refreshControls(){this.inputs=this.$el.querySelectorAll(this.config.inputSelector)}findSubmitElements(){return Array.from(this.$el.querySelectorAll('input[type="submit"], input[type="image"], button[type="submit"], button:not([type])'))}bindSubmitHandlers(){this._onSubmitClickHandler||(this._onSubmitClickHandler=t=>e(this,void 0,void 0,(function*(){yield this.handleSubmitEvent(t)}))),this.$submits.forEach((t=>{t.removeEventListener("click",this._onSubmitClickHandler)})),this.$submits=this.findSubmitElements(),this.$submits.forEach((t=>{t.addEventListener("click",this._onSubmitClickHandler)}))}initialization(){this.refreshControls(),this._onSubmitHandler=t=>e(this,void 0,void 0,(function*(){yield this.handleSubmitEvent(t)})),this.$el.addEventListener("submit",this._onSubmitHandler)}handleSubmitEvent(t){return e(this,void 0,void 0,(function*(){var e,r,i,n,o,s;t.preventDefault(),null===(r=(e=this.config).onBeforeValidate)||void 0===r||r.call(e,this);const a=yield this.validate();return null===(n=(i=this.config).onAfterValidate)||void 0===n||n.call(i,a,this),a?this.submit():(null===(s=(o=this.config).onValidationError)||void 0===s||s.call(o,this.getErrors(),this),{success:!1,error:this.getErrors()})}))}normalizeTriggers(t,e){const r=null==t?e:Array.isArray(t)?t:[t];return Array.from(new Set(r))}clearFieldValidationEvents(){this.fieldEventHandlers.forEach((({$input:t,event:e,handler:r})=>{t.removeEventListener(e,r)})),this.fieldEventHandlers=[]}bindFieldValidationEvents(){this.clearFieldValidationEvents();const t=this.normalizeTriggers(this.config.validateOn,["submit"]).filter((t=>"submit"!==t)),e=this.normalizeTriggers(this.config.revalidateOn,["input","change"]).filter((t=>"submit"!==t)),r=Array.from(new Set([...t,...e]));if(0===r.length)return;this.getAllFields().forEach((e=>{r.forEach((r=>{const i=()=>{(t.includes(r)||this.hasFieldError(e))&&this.scheduleFieldValidation(e)};e.addEventListener(r,i),this.fieldEventHandlers.push({$input:e,event:r,handler:i})}))}))}scheduleFieldValidation(t){const e=this.liveValidationTimers.get(t);e&&clearTimeout(e);const r=Math.max(0,Number(this.config.validationDebounce)||0),i=()=>{this.validateField(t)};if(0===r)return void i();const n=setTimeout(i,r);this.liveValidationTimers.set(t,n)}setupMutationObserver(){var t;null===(t=this.mutationObserver)||void 0===t||t.disconnect(),this.mutationObserver=void 0,this.config.observeMutations&&"undefined"!=typeof MutationObserver&&(this.mutationObserver=new MutationObserver((()=>{this.refreshControls(),this.bindSubmitHandlers(),this.bindFieldValidationEvents()})),this.mutationObserver.observe(this.$el,{childList:!0,subtree:!0}))}getErrorContainer(t){const e=this.config.errorContainerAttribute||"data-error-container",r=t.getAttribute(e);if(!r)return null;try{return document.querySelector(r)}catch(t){return console.warn(`[FormFather] Invalid error container selector "${r}"`),null}}toSafeIdPart(t){return t.trim().toLowerCase().replace(/[^a-z0-9_-]+/gi,"-").replace(/^-+|-+$/g,"")||"field"}createErrorId(t){const e=this.config.errorIdPrefix||"form-father-error";return`${this.toSafeIdPart(e)}-${this.toSafeIdPart(this.getFieldKey(t))}-${++T}`}ensureElementId(t,e){return e.id||(e.id=t.getAttribute(_)||this.createErrorId(t)),e.id}addErrorDescription(t,e){e.hasAttribute("role")||e.setAttribute("role","alert");const r=this.ensureElementId(t,e);if(t.setAttribute(_,r),!1===this.config.ariaDescribeErrors)return;const i=(t.getAttribute("aria-describedby")||"").split(/\s+/).filter(Boolean);i.includes(r)||(t.setAttribute("aria-describedby",[...i,r].join(" ")),t.setAttribute(x,r))}removeErrorDescription(t){const e=t.getAttribute(x);if(e){const r=(t.getAttribute("aria-describedby")||"").split(/\s+/).filter((t=>t&&t!==e));r.length>0?t.setAttribute("aria-describedby",r.join(" ")):t.removeAttribute("aria-describedby")}t.removeAttribute(_),t.removeAttribute(x)}showError(t,e){t.setAttribute("aria-invalid","true");const i=this.getErrorContainer(t);if(i)return i.textContent=e,this.addErrorDescription(t,i),void i.removeAttribute("hidden");const n=r(t,this.config.inputWrapperSelector);if(!n)return void console.warn(`Не найдена обёртка для поля с именем: ${t.name||t.type}`);const o=n.showError;if("function"==typeof o){o.call(n,e);const r=n.querySelector("[data-form-father-error]");return void(r&&this.addErrorDescription(t,r))}n.classList.add("input__wrapper--error");let s=n.querySelector("[data-form-father-error]");s||(s=document.createElement("div"),s.setAttribute("data-form-father-error",""),s.setAttribute("role","alert"),t.insertAdjacentElement("afterend",s)),s.textContent=e,this.addErrorDescription(t,s)}showLoader(){this.$submits.forEach((t=>{if(t.classList.add("button--loading"),t.querySelector(".button__loader"))return;const e="form-father-loader-"+$++,r=`\n\t\t\t\t<svg height="38" viewBox="0 0 38 38" width="38" xmlns="http://www.w3.org/2000/svg">\n\t\t\t\t\t<defs>\n\t\t\t\t\t<linearGradient id="${e}" x1="8.042%" x2="65.682%" y1="0%" y2="23.865%">\n\t\t\t\t\t\t<stop offset="0%" stop-color="${this.config.loaderColor}" stop-opacity="0"/>\n\t\t\t\t\t\t<stop offset="63.146%" stop-color="${this.config.loaderColor}" stop-opacity=".631"/>\n\t\t\t\t\t\t<stop offset="100%" stop-color="${this.config.loaderColor}"/>\n\t\t\t\t\t</linearGradient>\n\t\t\t\t\t</defs>\n\t\t\t\t\t<g fill-rule="evenodd" fill="none">\n\t\t\t\t\t<g transform="translate(1 1)">\n\t\t\t\t\t\t<path d="M36 18c0-9.94-8.06-18-18-18" id="Oval-2" stroke-width="5" stroke="url(#${e})"></path>\n\t\t\t\t\t\t<circle cx="36" cy="18" fill="${this.config.loaderColor}" r="1"></circle>\n\t\t\t\t\t</g>\n\t\t\t\t\t</g>\n\t\t\t\t</svg>\n\t\t\t`,i=document.createElement("span");i.innerHTML=r,i.className="button__loader",t.insertAdjacentElement("afterbegin",i),requestAnimationFrame((()=>{i.setAttribute("data-showed","")}))}))}hideLoader(){this.$submits.forEach((t=>{t.classList.remove("button--loading");const e=t.querySelector(".button__loader");e&&(e.classList.remove("active"),e.removeAttribute("data-showed"),setTimeout((()=>{e.remove()}),250))}))}hideError(t){var e;const i=r(t,this.config.inputWrapperSelector)||t.parentElement,n=this.getErrorContainer(t);t.removeAttribute("aria-invalid"),this.removeErrorDescription(t),n&&(n.textContent="",n.setAttribute("hidden","")),null===(e=null==i?void 0:i.querySelector("[data-form-father-error]"))||void 0===e||e.remove(),null==i||i.classList.remove("input__wrapper--error"),i&&i.classList.contains("input__wrapper")&&i.getAttribute("data-type-error")}getErrorSummaryOptions(){return this.config.errorSummary?!0===this.config.errorSummary?{}:this.config.errorSummary:null}getErrorSummaryContainer(t,e){if(t.selector)try{return this.$el.querySelector(t.selector)||document.querySelector(t.selector)}catch(e){return console.warn(`[FormFather] Invalid error summary selector "${t.selector}"`),null}const r=this.$el.querySelector(`[${k}]`);if(r||!e)return r;const i=document.createElement("div");return i.setAttribute(k,""),this.$el.insertAdjacentElement("afterbegin",i),i}prepareErrorSummaryContainer(t){t.setAttribute(k,""),t.hasAttribute("role")||t.setAttribute("role","alert"),t.hasAttribute("tabindex")||t.setAttribute("tabindex","-1"),t.removeAttribute("hidden")}focusErrorSummary(t){try{t.focus({preventScroll:!0})}catch(e){t.focus()}}focusFieldFromSummary(t){"function"==typeof t.scrollIntoView&&t.scrollIntoView({behavior:"smooth",block:"center"});try{t.focus({preventScroll:!0})}catch(e){t.focus()}}renderDefaultErrorSummary(t,e,r){t.textContent="";const i=document.createElement("p");i.setAttribute("data-form-father-summary-title",""),i.textContent=r.title||"Проверьте поля формы";const n=document.createElement("ul");n.setAttribute("data-form-father-summary-list",""),e.forEach((t=>{const e=document.createElement("li"),r=t.field===w?null:this.findFieldByName(t.field);if(r){const i=document.createElement("button");i.type="button",i.setAttribute("data-form-father-summary-field",t.field),i.textContent=t.message,i.addEventListener("click",(()=>this.focusFieldFromSummary(r))),e.appendChild(i)}else{const r=document.createElement("span");r.textContent=t.message,e.appendChild(r)}n.appendChild(e)})),t.append(i,n)}renderErrorSummary(t=this.getErrors()){var e;const r=this.getErrorSummaryOptions();if(!r)return;if(0===t.length)return void this.hideErrorSummary();const i=this.getErrorSummaryContainer(r,!0);if(!i)return;this.prepareErrorSummaryContainer(i);const n=null===(e=r.render)||void 0===e?void 0:e.call(r,t.map((t=>Object.assign({},t))),this);n instanceof HTMLElement?i.replaceChildren(n):"string"==typeof n?i.innerHTML=n:r.render||this.renderDefaultErrorSummary(i,t,r),r.focus&&this.focusErrorSummary(i)}hideErrorSummary(){const t=this.getErrorSummaryOptions()||{},e=this.getErrorSummaryContainer(t,!1);e&&(e.replaceChildren(),e.setAttribute("hidden",""))}syncErrorSummary(){0!==this.errors.length?this.renderErrorSummary():this.hideErrorSummary()}scrollToFirstErroredInput(t){if(t.length>0){const e=Array.from(this.$el.querySelectorAll("input, textarea, select"));let r=null,i=1/0;if(t.forEach((t=>{const n=e.findIndex((e=>e===t));n<i&&(i=n,r=t)})),r){const t=r.getBoundingClientRect(),e=window.pageYOffset||document.documentElement.scrollTop,i=t.top+e-window.innerHeight/2;document.documentElement.setAttribute("data-now-scrolling",""),window.scrollTo({top:i,behavior:"smooth"}),setTimeout((()=>{document.documentElement.removeAttribute("data-now-scrolling")}),800)}}}focusFirstErroredInput(t){var e;if(0===t.length)return;const r=Array.from(this.$el.querySelectorAll("input, textarea, select")),i=t.slice().sort(((t,e)=>r.indexOf(t)-r.indexOf(e)))[0];null===(e=null==i?void 0:i.focus)||void 0===e||e.call(i,{preventScroll:!0})}getValidationSchema(){return Object.assign(Object.assign({},C.defaultValidationSchema||{}),this.config.validationSchema||{})}getAllFields(t=this.$el){return Array.from(t.querySelectorAll("input, textarea, select"))}getFieldKey(t){return t.name||t.id||t.getAttribute("data-field")||t.type||"field"}hasFieldError(t){const e=this.getFieldKey(t);return"true"===t.getAttribute("aria-invalid")||this.errors.some((t=>t.field===e))}clearFieldErrorRecord(t){const e=this.getFieldKey(t);this.errors=this.errors.filter((t=>t.field!==e))}setErrorRecord(t,e,r,i){this.errors.push({field:t,rule:e,message:r,source:i})}getValidationStateAttribute(){return this.config.validationStateAttribute||"data-form-father-state"}setFieldValidationState(t,e){const r=this.getValidationStateAttribute();e?t.setAttribute(r,e):t.removeAttribute(r)}startFieldValidation(t){const e=++this.fieldValidationTokenCounter;return this.fieldValidationTokens.set(t,e),t.setAttribute("aria-busy","true"),this.setFieldValidationState(t,"validating"),e}isLatestFieldValidation(t,e){return null==e||this.fieldValidationTokens.get(t)===e}finishFieldValidation(t,e,r){this.isLatestFieldValidation(t,e)&&(t.removeAttribute("aria-busy"),this.setFieldValidationState(t,r?"valid":"invalid"))}clearFieldValidationState(t){this.fieldValidationTokens.delete(t),t.removeAttribute("aria-busy"),this.setFieldValidationState(t,null)}setValidationError(t,e,r,i){this.clearFieldErrorRecord(t),this.setErrorRecord(this.getFieldKey(t),e,r,i)}resolveField(t){return"string"!=typeof t?t:this.findFieldByName(t)}parseRuleString(t){const[e,...r]=t.split(":").map((t=>t.trim()));return r.length>0?{rule:e,params:1===r.length?r[0]:r}:e}parseValidationAttribute(t){return(null!=t?t:"").split(/[,\s|]+/).map((t=>t.trim())).filter(Boolean).map((t=>this.parseRuleString(t)))}getRuleName(t){return"string"==typeof t?t:t.rule}getRuleIdentity(t){return JSON.stringify(t)}collectRules(t,e=[],r=new Set){const i=[];t.hasAttribute("required")&&i.push("required"),i.push(...e);const n=this.parseValidationAttribute(t.getAttribute("data-validate")).filter((t=>{const e=this.getRuleName(t);return f(e)||!r.has(e)}));i.push(...n),i.push(...this.parseValidationAttribute(t.getAttribute("data-custom-validate")));const o=[],s=new Set;for(const t of i){const e=this.getRuleName(t);if(!f(e)){console.warn(`[FormFather] Unknown validation rule "${e}"`);continue}const r=this.getRuleIdentity(t);s.has(r)||(s.add(r),o.push(t))}return o}getRuleMessage(t,e,r,i,n){const o=t.getAttribute(`data-error-${e}`);return n||o||r[e]||i}fieldMatchesSchemaDefinition(t,e,r){if(r.selector)try{return t.matches(r.selector)}catch(t){return console.warn(`[FormFather] Invalid validation selector "${r.selector}"`),!1}return this.parseValidationAttribute(t.getAttribute("data-validate")).some((t=>this.getRuleName(t)===e))}getFieldValidationConfig(t,e=this.getValidationSchema()){const r=[],i={};for(const[n,o]of Object.entries(e))this.fieldMatchesSchemaDefinition(t,n,o)&&(r.push(...o.rules),Object.assign(i,o.messages||{}));return{rules:this.collectRules(t,r,new Set(Object.keys(e))),messages:i}}getValidationFields(t,e){const r=new Set,i=t=>{Array.from(t).forEach((t=>{(t instanceof HTMLInputElement||t instanceof HTMLTextAreaElement||t instanceof HTMLSelectElement)&&r.add(t)}))};for(const[n,o]of Object.entries(e))if(o.selector)try{i(t.querySelectorAll(o.selector))}catch(t){console.warn(`[FormFather] Invalid validation selector "${o.selector}"`)}else this.getAllFields(t).filter((t=>this.parseValidationAttribute(t.getAttribute("data-validate")).some((t=>this.getRuleName(t)===n)))).forEach((t=>r.add(t)));return i(t.querySelectorAll("[required], [data-validate], [data-custom-validate]")),Array.from(r)}validateInput(t,r){return e(this,arguments,void 0,(function*(t,e,r={},i=this.$el,n){if(t.hasAttribute("data-no-validate"))return!0;const o=t instanceof HTMLInputElement&&("checkbox"===t.type||"radio"===t.type)?!t.checked:0===t.value.trim().length,s=e.some((t=>"required"===this.getRuleName(t)));if(o&&!s)return this.isLatestFieldValidation(t,n)?(this.clearFieldErrorRecord(t),this.hideError(t),!0):!this.hasFieldError(t);for(const o of e){const{rule:e,params:s}="string"==typeof o?{rule:o,params:void 0}:{rule:o.rule,params:o.params},a=f(e);if(!a)continue;const l=yield a.fn(t.value,t,i,s),d="object"==typeof l&&null!==l,c=d?!0===l.valid:!!l;if(!this.isLatestFieldValidation(t,n))return!this.hasFieldError(t);if(d&&l.effect&&(yield l.effect({value:t.value,$input:t,$form:i,params:s})),!this.isLatestFieldValidation(t,n))return!this.hasFieldError(t);if(!c){const i=l,n=this.getRuleMessage(t,e,r,a.defaultMessage,d&&"string"==typeof i.message?i.message:void 0);return this.setValidationError(t,e,n,"client"),this.showError(t,n),!1}if(d&&l.stopOthers)return!0}return this.isLatestFieldValidation(t,n)?(this.clearFieldErrorRecord(t),this.hideError(t),!0):!this.hasFieldError(t)}))}runFieldValidation(t,r){return e(this,arguments,void 0,(function*(t,e,r={},i=this.$el){const n=this.startFieldValidation(t);try{const o=yield this.validateInput(t,e,r,i,n);return this.finishFieldValidation(t,n,o),o}catch(e){throw this.isLatestFieldValidation(t,n)&&this.clearFieldValidationState(t),e}}))}getRadioGroup(t,e){return t instanceof HTMLInputElement&&"radio"===t.type&&t.name?Array.from(e.querySelectorAll('input[type="radio"]')).filter((e=>e.name===t.name)):[]}showFieldError(t,e,r="manual",i=r){const n=this.resolveField(t);return n?(this.setValidationError(n,i,e,r),this.showError(n,e),n.removeAttribute("aria-busy"),this.setFieldValidationState(n,"invalid"),this.syncErrorSummary(),!0):(console.warn(`[FormFather] Field "${String(t)}" not found`),!1)}validateField(t){return e(this,void 0,void 0,(function*(){const e=this.resolveField(t);if(!e)return console.warn(`[FormFather] Field "${String(t)}" not found`),!1;const{rules:r,messages:i}=this.getFieldValidationConfig(e),n=yield this.runFieldValidation(e,r,i);return this.syncErrorSummary(),n}))}validate(){return e(this,arguments,void 0,(function*(t=this.$el){const e=this.getValidationSchema(),r=[],i=new WeakSet;let n=!0;this.errors=[];for(const o of this.getValidationFields(t,e)){if(i.has(o))continue;const s=this.getRadioGroup(o,t);s.length>0?s.forEach((t=>i.add(t))):i.add(o);const{rules:a,messages:l}=this.getFieldValidationConfig(o,e);(yield this.runFieldValidation(o,a,l,t))||(n=!1,r.push(o))}const o=yield this.validateFormValidators();o.ok||(n=!1,r.push(...o.erroredInputs));const s=r.filter((t=>!t.hasAttribute("data-no-error-scroll")));return!n&&this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(s),!n&&this.config.focusFirstErroredInput&&this.focusFirstErroredInput(s),n?this.hideErrorSummary():this.renderErrorSummary(),n}))}disableSubmit(){this.$submits.forEach((t=>{t.setAttribute("disabled","")}))}enableSubmit(){this.$submits.forEach((t=>{t.removeAttribute("disabled")}))}clearInputs(){var t;null===(t=this.inputs)||void 0===t||t.forEach((t=>{var e;const r=t;"radio"===r.type||"checkbox"===r.type?r.checked=!1:(r.value="",null===(e=r.parentElement)||void 0===e||e.classList.remove("filled"),r.dispatchEvent(new Event("input")))}))}_prefersReducedMotion(){var t;return"undefined"!=typeof window&&(null===(t=window.matchMedia)||void 0===t?void 0:t.call(window,"(prefers-reduced-motion: reduce)").matches)}_canAnimate(t){return!(!t||!("animate"in t)||this._prefersReducedMotion())}_cancelAnimations(t){var e;const r=t;null===(e=null==r?void 0:r.getAnimations)||void 0===e||e.call(r).forEach((t=>t.cancel()))}showErrorForm(t){var e;const r=this.$el.querySelectorAll(this.config.inputWrapperSelector),i=r[r.length-1];let n=this.$el.querySelector(".error-block-under-input__wrapper");!n||i&&i.nextElementSibling!==n?(n=document.createElement("div"),n.className="error-block-under-input__wrapper",n.innerHTML='\n <div class="error-block-under-input error-block-under-input--warning" style="display:grid;grid-template-columns:auto 1fr;gap:8px;align-items:start;">\n <div class="error-block-under-input__icon-wrapper" style="position:relative;width:20px;height:20px;">\n <span class="error-block-under-input__icon" style="display:block;width:20px;height:20px;"></span>\n <span class="error-block-under-input__icon error-block-under-input__icon--animated" style="display:block;position:absolute;inset:0;"></span>\n </div>\n <p class="error-block-under-input__text" style="margin:0;">\n <span class="error-block-under-input__main-text"></span>\n <span class="error-block-under-input__secondary-text"></span>\n </p>\n </div>\n ',Object.assign(n.style,{boxSizing:"border-box",overflow:"hidden",height:"0px",paddingTop:"0px",opacity:"0"}),i?i.insertAdjacentElement("afterend",n):this.$el.appendChild(n)):(n.style.boxSizing="border-box",n.style.overflow="hidden"),n.querySelector(".error-block-under-input__main-text").textContent=t;const o=n.querySelector(".error-block-under-input__secondary-text");o&&(o.textContent=""),null===(e=n.querySelector(".error-block-under-input"))||void 0===e||e.classList.remove("error-block-under-input--success");const s=n.style.height,a=n.style.paddingTop;n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`;const l=n.clientHeight;if(n.style.height=s||"0px",n.style.paddingTop=a||"0px",parseFloat(n.style.height)===l&&1===parseFloat(n.style.opacity||"1"))this._ringIcon(n);else{if(this._cancelAnimations(n),this._canAnimate(n)){n.animate([{height:`${parseFloat(n.style.height)||0}px`,paddingTop:`${parseFloat(n.style.paddingTop)||0}px`,opacity:parseFloat(n.style.opacity||"0")},{height:`${l}px`,paddingTop:`${this._anim.paddingTopOpened}px`,opacity:1}],{duration:this._anim.duration,easing:this._anim.easing,fill:"forwards"}).addEventListener("finish",(()=>{n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`,n.style.opacity="1"}))}else n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`,n.style.opacity="1";this._ringIcon(n)}this.$el.addEventListener("input",(()=>this.hideErrorForm()),{passive:!0,once:!0})}hideErrorForm(){const t=this.$el.querySelectorAll(this.config.inputWrapperSelector),e=t[t.length-1],r=(null==e?void 0:e.nextElementSibling)||this.$el.querySelector(".error-block-under-input__wrapper");if(!r||!r.classList.contains("error-block-under-input__wrapper"))return;if((""===r.style.height||"auto"===r.style.height)&&(r.style.height=`${r.clientHeight}px`),r.style.paddingTop=r.style.paddingTop||`${this._anim.paddingTopOpened}px`,r.style.opacity=r.style.opacity||"1",this._cancelAnimations(r),this._canAnimate(r)){r.animate([{height:`${parseFloat(r.style.height)||r.clientHeight}px`,paddingTop:`${parseFloat(r.style.paddingTop)||this._anim.paddingTopOpened}px`,opacity:parseFloat(r.style.opacity||"1")},{height:"0px",paddingTop:"0px",opacity:0}],{duration:this._anim.duration,easing:this._anim.easing,fill:"forwards"}).addEventListener("finish",(()=>{r.remove()}))}else r.remove()}_ringIcon(t){const e=t.querySelector(".error-block-under-input__icon--animated");e&&(this._cancelAnimations(e),this._canAnimate(e)&&e.animate([{transform:"rotate(0deg) scale(1)",opacity:0},{transform:"rotate(-8deg) scale(1.05)",opacity:1,offset:.15},{transform:"rotate(8deg) scale(1.05)",opacity:1,offset:.3},{transform:"rotate(-6deg) scale(1.03)",opacity:1,offset:.45},{transform:"rotate(6deg) scale(1.03)",opacity:1,offset:.6},{transform:"rotate(0deg) scale(1)",opacity:0}],{duration:this._anim.iconDuration,easing:"ease"}))}destroy(){var t;this._onSubmitHandler&&(this.$el.removeEventListener("submit",this._onSubmitHandler),this._onSubmitHandler=void 0),this._onSubmitClickHandler&&(this.$submits.forEach((t=>{t.removeEventListener("click",this._onSubmitClickHandler)})),this._onSubmitClickHandler=void 0),this.clearFieldValidationEvents(),null===(t=this.mutationObserver)||void 0===t||t.disconnect(),this.mutationObserver=void 0;const e=this.$el.querySelector(".error-block-under-input__wrapper");e&&(this._cancelAnimations(e),e.remove()),this.hideErrorSummary(),this.$submits.forEach((t=>{t.classList.remove("button--loading");const e=t.querySelector(".button__loader");e&&e.remove()}));this.$el.querySelectorAll("input, textarea, select").forEach((t=>{this.hideError(t),this.clearFieldValidationState(t)})),document.documentElement.removeAttribute("data-now-scrolling"),this.inputs=null,this.$submits=[],this.errors=[],this.waitResponse=!1,O.delete(this.$el)}isCorrectArguments(){return"FORM"!==this.$el.tagName?(console.warn("$el не является тегом form"),!1):0!==this.$submits.length||(console.warn("В форме нет кнопки с типом submit"),!1)}formDataToObject(t){const e={};return t.forEach(((t,r)=>{Object.prototype.hasOwnProperty.call(e,r)?(Array.isArray(e[r])||(e[r]=[e[r]]),e[r].push(t)):e[r]=t})),e}appendFormDataValue(t,e,r){null!=r&&(Array.isArray(r)?r.forEach(((r,i)=>{this.appendFormDataValue(t,`${e}[${i}]`,r)})):r instanceof Blob?t.append(e,r):t.append(e,"object"==typeof r?JSON.stringify(r):String(r)))}objectToFormData(t){const e=new FormData;return Object.entries(t).forEach((([t,r])=>{this.appendFormDataValue(e,t,r)})),e}stringifyFormDataValue(t){return"string"==typeof t?t:t.name}formDataToUrlSearchParams(t){const e=new URLSearchParams;return t.forEach(((t,r)=>{e.append(r,this.stringifyFormDataValue(t))})),e}formDataToTextBody(t){return Array.from(t).map((([t,e])=>`${encodeURIComponent(t)}=${encodeURIComponent(this.stringifyFormDataValue(e))}`)).join("\n")}isBodylessMethod(t){return"GET"===t||"HEAD"===t}appendQueryString(t,e){const r=e.toString();if(!r)return t;const i=t.includes("?")?"&":"?";return`${t}${i}${r}`}findFieldByName(t){return Array.from(this.$el.querySelectorAll("input, textarea, select")).find((e=>e.name===t))||null}parseResponseBody(t){return e(this,void 0,void 0,(function*(){try{const e=yield t.json();return e&&"object"==typeof e?e:{success:!1,error:!0,"error-msg":"Некорректный ответ сервера"}}catch(t){return{success:!1,error:!0,"error-msg":"Некорректный ответ сервера"}}}))}handleUnsuccessfulResponse(t){if(!0===t.error){if(t["error-msg"]&&(this.setErrorRecord(w,"server",t["error-msg"],"server"),this.showErrorForm(t["error-msg"])),t.errors){const e=[];t.errors.forEach((t=>{const r=this.findFieldByName(t.name);r?(e.push(r),this.setValidationError(r,"server",t["error-msg"],"server"),this.showError(r,t["error-msg"]),r.removeAttribute("aria-busy"),this.setFieldValidationState(r,"invalid")):(this.setErrorRecord(t.name,"server",t["error-msg"],"server"),console.warn(`Не найдено поле с именем: ${t.name}, для вывода ошибки: ${t["error-msg"]}`))})),!0===this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(e),!0===this.config.focusFirstErroredInput&&this.focusFirstErroredInput(e)}this.syncErrorSummary()}}sendData(){return e(this,void 0,void 0,(function*(){var t;const e=this.$el.getAttribute("action")||"",r=(null===(t=this.$el.getAttribute("method"))||void 0===t?void 0:t.toUpperCase())||"POST",i=this.$el.getAttribute("enctype")||"application/x-www-form-urlencoded";let n=null,o={};const s=a(this.$el);let l=this.formDataToObject(s);if(this.config.logging)for(const t of s.keys())console.log(s.get(t));const d="function"==typeof this.config.wrapData;"function"==typeof this.config.wrapData&&(l=this.config.wrapData(l));const c=d?this.objectToFormData(l):s;if(this.isBodylessMethod(r))return fetch(this.appendQueryString(e,this.formDataToUrlSearchParams(c)),{method:r,headers:o});switch(i){case"application/x-www-form-urlencoded":n=this.formDataToUrlSearchParams(c).toString(),o["Content-Type"]="application/x-www-form-urlencoded";break;case"multipart/form-data":n=c;break;case"text/plain":n=this.formDataToTextBody(c),o["Content-Type"]="text/plain";break;case"application/json":n=JSON.stringify(l),o["Content-Type"]="application/json";break;default:console.warn(`Неизвестный enctype: ${i}. Используется application/x-www-form-urlencoded.`),n=this.formDataToUrlSearchParams(c).toString(),o["Content-Type"]="application/x-www-form-urlencoded"}return fetch(e,{method:r,body:n,headers:o})}))}submit(){return e(this,void 0,void 0,(function*(){var t,e,r,i,n,o,s,a,d,c,u,h;if(!this.waitResponse){let m=!1;const f=()=>{m||(this.waitResponse=!1,!0===this.config.showLoaderButton&&this.hideLoader(),m=!0)};try{if(!1===(yield null===(e=(t=this.config).onBeforeSubmit)||void 0===e?void 0:e.call(t,this)))return{success:!1};!0===this.config.showLoaderButton&&this.showLoader(),null===(i=(r=this.config).onSubmit)||void 0===i||i.call(r,this),this.waitResponse=!0;const u=yield this.sendData(),h=yield this.parseResponseBody(u);return f(),null===(o=(n=this.config).onResponse)||void 0===o||o.call(n,h,this),u.ok&&!0===h.success?null===(a=(s=this.config).onResponseSuccess)||void 0===a||a.call(s,h,this):(null===(c=(d=this.config).onResponseUnsuccess)||void 0===c||c.call(d,h,this),this.handleUnsuccessfulResponse(h)),l(h),{success:u.ok&&!0===h.success,response:u,responseBody:h}}catch(t){throw f(),null===(h=(u=this.config).onSubmitError)||void 0===h||h.call(u,t,this),t}}}))}}return t.FORM_ERROR_FIELD=w,t.blockScrollBody=function(){window.lastBlockScrollPosition=window.pageYOffset,document.body.style.top=-window.lastBlockScrollPosition+"px",document.body.classList.add("body--block-scroll")},t.closest=r,t.createFieldValidator=b,t.createFormValidator=V,t.createLengthValidator=function(t,e){return b((e=>{const r=!1===t.trim?e:e.trim();return 0===r.length&&!0===t.emptyIsValid||!("number"==typeof t.min&&r.length<t.min)&&!("number"==typeof t.max&&r.length>t.max)}),e)},t.createPatternValidator=function(t,e){return b((e=>(t.lastIndex=0,t.test(e))),e)},t.createSchemaValidator=y,t.dateOrder=function(t,e,r="Дата окончания должна быть позже даты начала",i="date-order"){return V((({values:r})=>{const i=A(r,t),n=A(r,e);return!(!F(i)&&!F(n))||Date.parse(String(i))<=Date.parse(String(n))}),{field:e,rule:i,message:r})},t.default=C,t.getAllValidators=function(){return new Map(h)},t.getValidator=f,t.isEmailValid=function(t){return/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu.test(t)},t.isPhoneValid=function(t){const e=[];return Array.from(t).forEach((t=>{(Number(t)||0===Number(t))&&e.push(t)})),!(e.length<11||e.length<12&&"+"===t[1])},t.isUrlValid=s,t.parseCommonResponseProperties=l,t.registerFieldValidator=function(t,e,r,i={}){m(t,b(e),r,i)},t.registerSchemaValidator=function(t,e,r,i={}){const{override:n}=i;m(t,y(e,function(t,e){var r={};for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&e.indexOf(i)<0&&(r[i]=t[i]);if(null!=t&&"function"==typeof Object.getOwnPropertySymbols){var n=0;for(i=Object.getOwnPropertySymbols(t);n<i.length;n++)e.indexOf(i[n])<0&&Object.prototype.propertyIsEnumerable.call(t,i[n])&&(r[i[n]]=t[i[n]])}return r}(i,["override"])),r,{override:n})},t.registerValidator=m,t.requiredIf=function(t,r,i="Поле обязательно",n="conditional-required"){return V((i=>e(this,void 0,void 0,(function*(){return!(yield r(i))||!F(A(i.values,t))}))),{field:t,rule:n,message:i})},t.sameAsField=function(t,e,r="Значения не совпадают",i="same-as-field"){return V((({values:r})=>A(r,t)===A(r,e)),{field:t,rule:i,message:r})},t.serializeFormToJSON=d,t.serializeToFormData=a,t.unblockScrollBody=function(){document.body.classList.remove("body--block-scroll"),document.body.style.top="",window.scrollTo(0,window.lastBlockScrollPosition||0)},Object.defineProperty(t,"__esModule",{value:!0}),t}({});
|
|
2
2
|
//# sourceMappingURL=FormFather.min.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -48,9 +48,21 @@ interface SchemaValidatorOptions {
|
|
|
48
48
|
interface RegisterSchemaValidatorOptions extends SchemaValidatorOptions {
|
|
49
49
|
override?: boolean;
|
|
50
50
|
}
|
|
51
|
+
interface RegisterFieldValidatorOptions {
|
|
52
|
+
override?: boolean;
|
|
53
|
+
}
|
|
54
|
+
interface LengthValidatorOptions {
|
|
55
|
+
min?: number;
|
|
56
|
+
max?: number;
|
|
57
|
+
trim?: boolean;
|
|
58
|
+
emptyIsValid?: boolean;
|
|
59
|
+
}
|
|
51
60
|
declare function createSchemaValidator(schema: SchemaLike, options?: SchemaValidatorOptions): AdapterValidator;
|
|
52
61
|
declare function registerSchemaValidator(name: string, schema: SchemaLike, defaultMessage: string, options?: RegisterSchemaValidatorOptions): void;
|
|
62
|
+
declare function registerFieldValidator(name: string, predicate: (value: string, $input: ValidatorFieldElement, $form: HTMLElement, params?: any) => MaybePromise<boolean>, defaultMessage: string, options?: RegisterFieldValidatorOptions): void;
|
|
53
63
|
declare function createFieldValidator(predicate: (value: string, $input: ValidatorFieldElement, $form: HTMLElement, params?: any) => MaybePromise<boolean>, message?: string): AdapterValidator;
|
|
64
|
+
declare function createPatternValidator(pattern: RegExp, message?: string): AdapterValidator;
|
|
65
|
+
declare function createLengthValidator(options: LengthValidatorOptions, message?: string): AdapterValidator;
|
|
54
66
|
|
|
55
67
|
/**
|
|
56
68
|
* Находит родительский элемент по заданому селектору.
|
|
@@ -137,6 +149,16 @@ interface SubmitResult {
|
|
|
137
149
|
interface FormResetOptions {
|
|
138
150
|
clearErrors?: boolean;
|
|
139
151
|
}
|
|
152
|
+
interface ErrorSummaryOptions {
|
|
153
|
+
/** Existing summary container selector. If omitted, Form Father creates `[data-form-father-summary]` in the form. */
|
|
154
|
+
selector?: string;
|
|
155
|
+
/** Heading text for the default summary renderer. */
|
|
156
|
+
title?: string;
|
|
157
|
+
/** Move focus to the summary after rendering. Useful for submit-time screen reader announcements. */
|
|
158
|
+
focus?: boolean;
|
|
159
|
+
/** Custom renderer. Return an element/string or render into your own container and return nothing. */
|
|
160
|
+
render?: (errors: ValidationError[], formInstance: Form) => HTMLElement | string | void;
|
|
161
|
+
}
|
|
140
162
|
interface ValidationIssue {
|
|
141
163
|
field?: string;
|
|
142
164
|
rule?: string;
|
|
@@ -152,11 +174,17 @@ interface FormValidatorContext {
|
|
|
152
174
|
}
|
|
153
175
|
type FormValidatorResult = boolean | string | void | ValidationIssue | Array<string | ValidationIssue>;
|
|
154
176
|
type FormValidator = (context: FormValidatorContext) => FormValidatorResult | Promise<FormValidatorResult>;
|
|
177
|
+
type FormValidatorPredicate = (context: FormValidatorContext) => boolean | Promise<boolean>;
|
|
178
|
+
type FormValidatorIssueFactory = string | ValidationIssue | ((context: FormValidatorContext) => string | ValidationIssue | Promise<string | ValidationIssue>);
|
|
155
179
|
type ValidationSchema = Record<string, {
|
|
156
180
|
rules: ValidationRule[];
|
|
157
181
|
selector?: string;
|
|
158
182
|
messages?: Record<string, string>;
|
|
159
183
|
}>;
|
|
184
|
+
declare function createFormValidator(predicate: FormValidatorPredicate, issue: FormValidatorIssueFactory): FormValidator;
|
|
185
|
+
declare function sameAsField(field: string, otherField: string, message?: string, rule?: string): FormValidator;
|
|
186
|
+
declare function requiredIf(field: string, condition: FormValidatorPredicate, message?: string, rule?: string): FormValidator;
|
|
187
|
+
declare function dateOrder(startField: string, endField: string, message?: string, rule?: string): FormValidator;
|
|
160
188
|
type FormFieldElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
161
189
|
/** Параметры формы. */
|
|
162
190
|
interface FormOptions {
|
|
@@ -253,6 +281,12 @@ interface FormOptions {
|
|
|
253
281
|
errorContainerAttribute?: string;
|
|
254
282
|
/** Атрибут, куда пишется состояние поля: `validating`, `valid` или `invalid`. */
|
|
255
283
|
validationStateAttribute?: string;
|
|
284
|
+
/** Связывать текст ошибки с полем через `aria-describedby`. По умолчанию `true`. */
|
|
285
|
+
ariaDescribeErrors?: boolean;
|
|
286
|
+
/** Префикс id для автоматически созданных элементов ошибок. */
|
|
287
|
+
errorIdPrefix?: string;
|
|
288
|
+
/** Error summary для всей формы. По умолчанию выключен; `true` включает дефолтный renderer. */
|
|
289
|
+
errorSummary?: boolean | ErrorSummaryOptions;
|
|
256
290
|
/** Следить за динамически добавленными/удалёнными полями и submit-кнопками. По умолчанию `false`. */
|
|
257
291
|
observeMutations?: boolean;
|
|
258
292
|
/** Функция для обёртки отправляемых данных. */
|
|
@@ -350,6 +384,11 @@ declare class Form {
|
|
|
350
384
|
private scheduleFieldValidation;
|
|
351
385
|
private setupMutationObserver;
|
|
352
386
|
private getErrorContainer;
|
|
387
|
+
private toSafeIdPart;
|
|
388
|
+
private createErrorId;
|
|
389
|
+
private ensureElementId;
|
|
390
|
+
private addErrorDescription;
|
|
391
|
+
private removeErrorDescription;
|
|
353
392
|
/**
|
|
354
393
|
* Показывает ошибку для поля ввода.
|
|
355
394
|
*
|
|
@@ -367,6 +406,15 @@ declare class Form {
|
|
|
367
406
|
* @param {HTMLInputElement} $input - Поле ввода.
|
|
368
407
|
*/
|
|
369
408
|
private hideError;
|
|
409
|
+
private getErrorSummaryOptions;
|
|
410
|
+
private getErrorSummaryContainer;
|
|
411
|
+
private prepareErrorSummaryContainer;
|
|
412
|
+
private focusErrorSummary;
|
|
413
|
+
private focusFieldFromSummary;
|
|
414
|
+
private renderDefaultErrorSummary;
|
|
415
|
+
private renderErrorSummary;
|
|
416
|
+
private hideErrorSummary;
|
|
417
|
+
private syncErrorSummary;
|
|
370
418
|
/**
|
|
371
419
|
* Проскроливает до первого ошибочного поля.
|
|
372
420
|
*
|
|
@@ -462,4 +510,4 @@ declare class Form {
|
|
|
462
510
|
submit(): Promise<SubmitResult | undefined>;
|
|
463
511
|
}
|
|
464
512
|
|
|
465
|
-
export { type AdapterValidator, type ErrorResponse, FORM_ERROR_FIELD, type FieldReference, type FormFieldElement, type FormOptions, type FormResetOptions, type FormValidator, type FormValidatorContext, type FormValidatorResult, type RegisterSchemaValidatorOptions, type ResponseBody, type SchemaLike, type SchemaParseResult, type SchemaValidatorOptions, type SubmitResult, type ValidationError, type ValidationErrorSource, type ValidationIssue, type ValidationIssueInput, type ValidationRule, type ValidationSchema, type ValidationTrigger, type ValidatorEffectCtx, type ValidatorFieldElement, type ValidatorFn, type ValidatorResult, blockScrollBody, closest, createFieldValidator, createSchemaValidator, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerSchemaValidator, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
|
|
513
|
+
export { type AdapterValidator, type ErrorResponse, type ErrorSummaryOptions, FORM_ERROR_FIELD, type FieldReference, type FormFieldElement, type FormOptions, type FormResetOptions, type FormValidator, type FormValidatorContext, type FormValidatorIssueFactory, type FormValidatorPredicate, type FormValidatorResult, type LengthValidatorOptions, type RegisterFieldValidatorOptions, type RegisterSchemaValidatorOptions, type ResponseBody, type SchemaLike, type SchemaParseResult, type SchemaValidatorOptions, type SubmitResult, type ValidationError, type ValidationErrorSource, type ValidationIssue, type ValidationIssueInput, type ValidationRule, type ValidationSchema, type ValidationTrigger, type ValidatorEffectCtx, type ValidatorFieldElement, type ValidatorFn, type ValidatorResult, 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 };
|
package/dist/index.js
CHANGED
|
@@ -427,12 +427,33 @@ function registerSchemaValidator(name, schema, defaultMessage, options = {}) {
|
|
|
427
427
|
const { override } = options, adapterOptions = __rest(options, ["override"]);
|
|
428
428
|
registerValidator(name, createSchemaValidator(schema, adapterOptions), defaultMessage, { override });
|
|
429
429
|
}
|
|
430
|
+
function registerFieldValidator(name, predicate, defaultMessage, options = {}) {
|
|
431
|
+
registerValidator(name, createFieldValidator(predicate), defaultMessage, options);
|
|
432
|
+
}
|
|
430
433
|
function createFieldValidator(predicate, message) {
|
|
431
434
|
return (value, $input, $form, params) => __awaiter(this, void 0, void 0, function* () {
|
|
432
435
|
const valid = yield predicate(value, $input, $form, params);
|
|
433
436
|
return valid ? true : { valid: false, message };
|
|
434
437
|
});
|
|
435
438
|
}
|
|
439
|
+
function createPatternValidator(pattern, message) {
|
|
440
|
+
return createFieldValidator(value => {
|
|
441
|
+
pattern.lastIndex = 0;
|
|
442
|
+
return pattern.test(value);
|
|
443
|
+
}, message);
|
|
444
|
+
}
|
|
445
|
+
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);
|
|
456
|
+
}
|
|
436
457
|
|
|
437
458
|
var _a;
|
|
438
459
|
// --- shared cross-file state (singleton via globalThis) ---
|
|
@@ -446,8 +467,54 @@ const __shared =
|
|
|
446
467
|
defaultValidationSchema: undefined,
|
|
447
468
|
});
|
|
448
469
|
const FORM_ERROR_FIELD = '_form';
|
|
470
|
+
function readFormValue(values, field) {
|
|
471
|
+
return values[field];
|
|
472
|
+
}
|
|
473
|
+
function isEmptyFormValue(value) {
|
|
474
|
+
if (Array.isArray(value))
|
|
475
|
+
return value.length === 0;
|
|
476
|
+
return value === undefined || value === null || String(value).trim().length === 0;
|
|
477
|
+
}
|
|
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
|
+
});
|
|
490
|
+
}
|
|
491
|
+
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
|
+
});
|
|
496
|
+
}
|
|
497
|
+
function sameAsField(field, otherField, message = 'Значения не совпадают', rule = 'same-as-field') {
|
|
498
|
+
return createFormValidator(({ values }) => readFormValue(values, field) === readFormValue(values, otherField), { field, rule, message });
|
|
499
|
+
}
|
|
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 });
|
|
502
|
+
}
|
|
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 });
|
|
511
|
+
}
|
|
449
512
|
let loaderIdCounter = 0;
|
|
513
|
+
let errorIdCounter = 0;
|
|
450
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';
|
|
451
518
|
/** Изначальная дефолтная схема. */
|
|
452
519
|
const INITIAL_DEFAULT_SCHEMA = {
|
|
453
520
|
tel: {
|
|
@@ -532,6 +599,9 @@ class Form {
|
|
|
532
599
|
validationDebounce: 0,
|
|
533
600
|
errorContainerAttribute: 'data-error-container',
|
|
534
601
|
validationStateAttribute: 'data-form-father-state',
|
|
602
|
+
ariaDescribeErrors: true,
|
|
603
|
+
errorIdPrefix: 'form-father-error',
|
|
604
|
+
errorSummary: false,
|
|
535
605
|
observeMutations: false,
|
|
536
606
|
};
|
|
537
607
|
/* Слияние параметров: глобальные параметры → пользовательские параметры */
|
|
@@ -636,6 +706,7 @@ class Form {
|
|
|
636
706
|
this.clearFieldValidationState($field);
|
|
637
707
|
});
|
|
638
708
|
this.hideErrorForm();
|
|
709
|
+
this.hideErrorSummary();
|
|
639
710
|
return this;
|
|
640
711
|
}
|
|
641
712
|
isErrorResponse(issue) {
|
|
@@ -700,6 +771,7 @@ class Form {
|
|
|
700
771
|
this.normalizeValidationIssues(issues, source).forEach(issue => {
|
|
701
772
|
this.applyValidationIssue(issue, source);
|
|
702
773
|
});
|
|
774
|
+
this.syncErrorSummary();
|
|
703
775
|
return this;
|
|
704
776
|
}
|
|
705
777
|
validateFormValidators() {
|
|
@@ -868,6 +940,52 @@ class Form {
|
|
|
868
940
|
return null;
|
|
869
941
|
}
|
|
870
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
|
+
}
|
|
871
989
|
/**
|
|
872
990
|
* Показывает ошибку для поля ввода.
|
|
873
991
|
*
|
|
@@ -879,8 +997,7 @@ class Form {
|
|
|
879
997
|
const $errorContainer = this.getErrorContainer($input);
|
|
880
998
|
if ($errorContainer) {
|
|
881
999
|
$errorContainer.textContent = text;
|
|
882
|
-
|
|
883
|
-
$errorContainer.setAttribute('role', 'alert');
|
|
1000
|
+
this.addErrorDescription($input, $errorContainer);
|
|
884
1001
|
$errorContainer.removeAttribute('hidden');
|
|
885
1002
|
return;
|
|
886
1003
|
}
|
|
@@ -892,6 +1009,9 @@ class Form {
|
|
|
892
1009
|
const customShowError = $inputWrapper.showError;
|
|
893
1010
|
if (typeof customShowError === 'function') {
|
|
894
1011
|
customShowError.call($inputWrapper, text);
|
|
1012
|
+
const $customError = $inputWrapper.querySelector('[data-form-father-error]');
|
|
1013
|
+
if ($customError)
|
|
1014
|
+
this.addErrorDescription($input, $customError);
|
|
895
1015
|
return;
|
|
896
1016
|
}
|
|
897
1017
|
$inputWrapper.classList.add('input__wrapper--error');
|
|
@@ -903,6 +1023,7 @@ class Form {
|
|
|
903
1023
|
$input.insertAdjacentElement('afterend', $error);
|
|
904
1024
|
}
|
|
905
1025
|
$error.textContent = text;
|
|
1026
|
+
this.addErrorDescription($input, $error);
|
|
906
1027
|
}
|
|
907
1028
|
/** Показывает лоадер */
|
|
908
1029
|
showLoader() {
|
|
@@ -961,6 +1082,7 @@ class Form {
|
|
|
961
1082
|
const $inputWrapper = closest($input, this.config.inputWrapperSelector) || $input.parentElement;
|
|
962
1083
|
const $errorContainer = this.getErrorContainer($input);
|
|
963
1084
|
$input.removeAttribute('aria-invalid');
|
|
1085
|
+
this.removeErrorDescription($input);
|
|
964
1086
|
if ($errorContainer) {
|
|
965
1087
|
$errorContainer.textContent = '';
|
|
966
1088
|
$errorContainer.setAttribute('hidden', '');
|
|
@@ -971,6 +1093,125 @@ class Form {
|
|
|
971
1093
|
$inputWrapper.getAttribute('data-type-error') || 'default';
|
|
972
1094
|
}
|
|
973
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
|
+
}
|
|
974
1215
|
/**
|
|
975
1216
|
* Проскроливает до первого ошибочного поля.
|
|
976
1217
|
*
|
|
@@ -1268,6 +1509,7 @@ class Form {
|
|
|
1268
1509
|
this.showError($field, message);
|
|
1269
1510
|
$field.removeAttribute('aria-busy');
|
|
1270
1511
|
this.setFieldValidationState($field, 'invalid');
|
|
1512
|
+
this.syncErrorSummary();
|
|
1271
1513
|
return true;
|
|
1272
1514
|
}
|
|
1273
1515
|
/** Проверяет одно поле по имени или DOM-элементу. */
|
|
@@ -1279,7 +1521,9 @@ class Form {
|
|
|
1279
1521
|
return false;
|
|
1280
1522
|
}
|
|
1281
1523
|
const { rules, messages } = this.getFieldValidationConfig($field);
|
|
1282
|
-
|
|
1524
|
+
const isValid = yield this.runFieldValidation($field, rules, messages);
|
|
1525
|
+
this.syncErrorSummary();
|
|
1526
|
+
return isValid;
|
|
1283
1527
|
});
|
|
1284
1528
|
}
|
|
1285
1529
|
/**
|
|
@@ -1331,6 +1575,12 @@ class Form {
|
|
|
1331
1575
|
if (!ok && this.config.focusFirstErroredInput) {
|
|
1332
1576
|
this.focusFirstErroredInput(visibleErroredInputs);
|
|
1333
1577
|
}
|
|
1578
|
+
if (ok) {
|
|
1579
|
+
this.hideErrorSummary();
|
|
1580
|
+
}
|
|
1581
|
+
else {
|
|
1582
|
+
this.renderErrorSummary();
|
|
1583
|
+
}
|
|
1334
1584
|
return ok;
|
|
1335
1585
|
});
|
|
1336
1586
|
}
|
|
@@ -1557,6 +1807,7 @@ class Form {
|
|
|
1557
1807
|
this._cancelAnimations(formErrorWrapper);
|
|
1558
1808
|
formErrorWrapper.remove();
|
|
1559
1809
|
}
|
|
1810
|
+
this.hideErrorSummary();
|
|
1560
1811
|
// 3) Скрыть/удалить лоадер на кнопке submit
|
|
1561
1812
|
this.$submits.forEach($submit => {
|
|
1562
1813
|
$submit.classList.remove('button--loading');
|
|
@@ -1686,6 +1937,7 @@ class Form {
|
|
|
1686
1937
|
if (responseBody.error !== true)
|
|
1687
1938
|
return;
|
|
1688
1939
|
if (responseBody['error-msg']) {
|
|
1940
|
+
this.setErrorRecord(FORM_ERROR_FIELD, 'server', responseBody['error-msg'], 'server');
|
|
1689
1941
|
this.showErrorForm(responseBody['error-msg']);
|
|
1690
1942
|
}
|
|
1691
1943
|
if (responseBody.errors) {
|
|
@@ -1700,6 +1952,7 @@ class Form {
|
|
|
1700
1952
|
this.setFieldValidationState($field, 'invalid');
|
|
1701
1953
|
}
|
|
1702
1954
|
else {
|
|
1955
|
+
this.setErrorRecord(error.name, 'server', error['error-msg'], 'server');
|
|
1703
1956
|
console.warn(`Не найдено поле с именем: ${error.name}, для вывода ошибки: ${error['error-msg']}`);
|
|
1704
1957
|
}
|
|
1705
1958
|
});
|
|
@@ -1708,6 +1961,7 @@ class Form {
|
|
|
1708
1961
|
if (this.config.focusFirstErroredInput === true)
|
|
1709
1962
|
this.focusFirstErroredInput(fieldsList);
|
|
1710
1963
|
}
|
|
1964
|
+
this.syncErrorSummary();
|
|
1711
1965
|
}
|
|
1712
1966
|
sendData() {
|
|
1713
1967
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -1818,5 +2072,5 @@ class Form {
|
|
|
1818
2072
|
}
|
|
1819
2073
|
}
|
|
1820
2074
|
|
|
1821
|
-
export { FORM_ERROR_FIELD, blockScrollBody, closest, createFieldValidator, createSchemaValidator, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerSchemaValidator, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
|
|
2075
|
+
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 };
|
|
1822
2076
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/types/adapters.d.ts
CHANGED
|
@@ -22,7 +22,19 @@ export interface SchemaValidatorOptions {
|
|
|
22
22
|
export interface RegisterSchemaValidatorOptions extends SchemaValidatorOptions {
|
|
23
23
|
override?: boolean;
|
|
24
24
|
}
|
|
25
|
+
export interface RegisterFieldValidatorOptions {
|
|
26
|
+
override?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface LengthValidatorOptions {
|
|
29
|
+
min?: number;
|
|
30
|
+
max?: number;
|
|
31
|
+
trim?: boolean;
|
|
32
|
+
emptyIsValid?: boolean;
|
|
33
|
+
}
|
|
25
34
|
export declare function createSchemaValidator(schema: SchemaLike, options?: SchemaValidatorOptions): AdapterValidator;
|
|
26
35
|
export declare function registerSchemaValidator(name: string, schema: SchemaLike, defaultMessage: string, options?: RegisterSchemaValidatorOptions): void;
|
|
36
|
+
export declare function registerFieldValidator(name: string, predicate: (value: string, $input: ValidatorFieldElement, $form: HTMLElement, params?: any) => MaybePromise<boolean>, defaultMessage: string, options?: RegisterFieldValidatorOptions): void;
|
|
27
37
|
export declare function createFieldValidator(predicate: (value: string, $input: ValidatorFieldElement, $form: HTMLElement, params?: any) => MaybePromise<boolean>, message?: string): AdapterValidator;
|
|
38
|
+
export declare function createPatternValidator(pattern: RegExp, message?: string): AdapterValidator;
|
|
39
|
+
export declare function createLengthValidator(options: LengthValidatorOptions, message?: string): AdapterValidator;
|
|
28
40
|
export {};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -24,6 +24,16 @@ export interface SubmitResult {
|
|
|
24
24
|
export interface FormResetOptions {
|
|
25
25
|
clearErrors?: boolean;
|
|
26
26
|
}
|
|
27
|
+
export interface ErrorSummaryOptions {
|
|
28
|
+
/** Existing summary container selector. If omitted, Form Father creates `[data-form-father-summary]` in the form. */
|
|
29
|
+
selector?: string;
|
|
30
|
+
/** Heading text for the default summary renderer. */
|
|
31
|
+
title?: string;
|
|
32
|
+
/** Move focus to the summary after rendering. Useful for submit-time screen reader announcements. */
|
|
33
|
+
focus?: boolean;
|
|
34
|
+
/** Custom renderer. Return an element/string or render into your own container and return nothing. */
|
|
35
|
+
render?: (errors: ValidationError[], formInstance: Form) => HTMLElement | string | void;
|
|
36
|
+
}
|
|
27
37
|
export interface ValidationIssue {
|
|
28
38
|
field?: string;
|
|
29
39
|
rule?: string;
|
|
@@ -39,11 +49,17 @@ export interface FormValidatorContext {
|
|
|
39
49
|
}
|
|
40
50
|
export type FormValidatorResult = boolean | string | void | ValidationIssue | Array<string | ValidationIssue>;
|
|
41
51
|
export type FormValidator = (context: FormValidatorContext) => FormValidatorResult | Promise<FormValidatorResult>;
|
|
52
|
+
export type FormValidatorPredicate = (context: FormValidatorContext) => boolean | Promise<boolean>;
|
|
53
|
+
export type FormValidatorIssueFactory = string | ValidationIssue | ((context: FormValidatorContext) => string | ValidationIssue | Promise<string | ValidationIssue>);
|
|
42
54
|
export type ValidationSchema = Record<string, {
|
|
43
55
|
rules: ValidationRule[];
|
|
44
56
|
selector?: string;
|
|
45
57
|
messages?: Record<string, string>;
|
|
46
58
|
}>;
|
|
59
|
+
export declare function createFormValidator(predicate: FormValidatorPredicate, issue: FormValidatorIssueFactory): FormValidator;
|
|
60
|
+
export declare function sameAsField(field: string, otherField: string, message?: string, rule?: string): FormValidator;
|
|
61
|
+
export declare function requiredIf(field: string, condition: FormValidatorPredicate, message?: string, rule?: string): FormValidator;
|
|
62
|
+
export declare function dateOrder(startField: string, endField: string, message?: string, rule?: string): FormValidator;
|
|
47
63
|
export type FormFieldElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
48
64
|
/** Параметры формы. */
|
|
49
65
|
export interface FormOptions {
|
|
@@ -140,6 +156,12 @@ export interface FormOptions {
|
|
|
140
156
|
errorContainerAttribute?: string;
|
|
141
157
|
/** Атрибут, куда пишется состояние поля: `validating`, `valid` или `invalid`. */
|
|
142
158
|
validationStateAttribute?: string;
|
|
159
|
+
/** Связывать текст ошибки с полем через `aria-describedby`. По умолчанию `true`. */
|
|
160
|
+
ariaDescribeErrors?: boolean;
|
|
161
|
+
/** Префикс id для автоматически созданных элементов ошибок. */
|
|
162
|
+
errorIdPrefix?: string;
|
|
163
|
+
/** Error summary для всей формы. По умолчанию выключен; `true` включает дефолтный renderer. */
|
|
164
|
+
errorSummary?: boolean | ErrorSummaryOptions;
|
|
143
165
|
/** Следить за динамически добавленными/удалёнными полями и submit-кнопками. По умолчанию `false`. */
|
|
144
166
|
observeMutations?: boolean;
|
|
145
167
|
/** Функция для обёртки отправляемых данных. */
|
|
@@ -237,6 +259,11 @@ export default class Form {
|
|
|
237
259
|
private scheduleFieldValidation;
|
|
238
260
|
private setupMutationObserver;
|
|
239
261
|
private getErrorContainer;
|
|
262
|
+
private toSafeIdPart;
|
|
263
|
+
private createErrorId;
|
|
264
|
+
private ensureElementId;
|
|
265
|
+
private addErrorDescription;
|
|
266
|
+
private removeErrorDescription;
|
|
240
267
|
/**
|
|
241
268
|
* Показывает ошибку для поля ввода.
|
|
242
269
|
*
|
|
@@ -254,6 +281,15 @@ export default class Form {
|
|
|
254
281
|
* @param {HTMLInputElement} $input - Поле ввода.
|
|
255
282
|
*/
|
|
256
283
|
private hideError;
|
|
284
|
+
private getErrorSummaryOptions;
|
|
285
|
+
private getErrorSummaryContainer;
|
|
286
|
+
private prepareErrorSummaryContainer;
|
|
287
|
+
private focusErrorSummary;
|
|
288
|
+
private focusFieldFromSummary;
|
|
289
|
+
private renderDefaultErrorSummary;
|
|
290
|
+
private renderErrorSummary;
|
|
291
|
+
private hideErrorSummary;
|
|
292
|
+
private syncErrorSummary;
|
|
257
293
|
/**
|
|
258
294
|
* Проскроливает до первого ошибочного поля.
|
|
259
295
|
*
|
package/docs/en/README.md
CHANGED
|
@@ -59,6 +59,8 @@ import Form, {
|
|
|
59
59
|
type FormResetOptions,
|
|
60
60
|
type FormValidator,
|
|
61
61
|
type ValidationIssue,
|
|
62
|
+
type FormValidatorPredicate,
|
|
63
|
+
type ErrorSummaryOptions,
|
|
62
64
|
} from 'form-father';
|
|
63
65
|
```
|
|
64
66
|
|
|
@@ -84,6 +86,9 @@ import Form, {
|
|
|
84
86
|
`data-error-container`.
|
|
85
87
|
- **validationStateAttribute**: Field state attribute: `validating`, `valid`, or `invalid`. Defaults to
|
|
86
88
|
`data-form-father-state`.
|
|
89
|
+
- **ariaDescribeErrors**: Links inline error text to the field with `aria-describedby`. Defaults to `true`.
|
|
90
|
+
- **errorIdPrefix**: Prefix for automatically generated error element ids. Defaults to `form-father-error`.
|
|
91
|
+
- **errorSummary**: Form error summary: `true` for the default renderer or `{ selector, title, focus, render }`.
|
|
87
92
|
- **observeMutations**: Watches dynamically added fields and submit buttons.
|
|
88
93
|
- **formValidators**: A form validator or array of validators for cross-field rules.
|
|
89
94
|
|
|
@@ -134,8 +139,38 @@ import Form, {
|
|
|
134
139
|
- `data-error-container` points to a custom error container without requiring wrapper markup.
|
|
135
140
|
- During async validation, fields receive `aria-busy="true"` and a `validating` state attribute; stale validator
|
|
136
141
|
responses are ignored.
|
|
142
|
+
- Inline errors receive `role="alert"` and are linked to fields with `aria-describedby`.
|
|
137
143
|
- `data-custom-validate` is still supported for backward compatibility.
|
|
138
144
|
|
|
145
|
+
### Error summary and a11y
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
<form data-form-father novalidate>
|
|
149
|
+
<div data-form-father-summary hidden></div>
|
|
150
|
+
<label>
|
|
151
|
+
Email
|
|
152
|
+
<input class="input" name="email" data-validate="required|email" />
|
|
153
|
+
</label>
|
|
154
|
+
<button type="submit">Submit</button>
|
|
155
|
+
</form>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
new Form($form, {
|
|
160
|
+
inputWrapperSelector: 'label',
|
|
161
|
+
errorSummary: {
|
|
162
|
+
title: 'Please check the form',
|
|
163
|
+
focus: true,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- If the form already contains `[data-form-father-summary]`, Form Father uses it; otherwise `errorSummary: true` creates
|
|
169
|
+
a summary at the start of the form.
|
|
170
|
+
- `selector` sends the summary into your own container.
|
|
171
|
+
- `render(errors, form)` lets you fully replace the summary markup.
|
|
172
|
+
- `ariaDescribeErrors: false` disables automatic error id insertion into `aria-describedby`.
|
|
173
|
+
|
|
139
174
|
Final rule order:
|
|
140
175
|
|
|
141
176
|
```text
|
|
@@ -145,17 +180,13 @@ required → schema.rules → data-validate → data-custom-validate
|
|
|
145
180
|
### Cross-field and form-level validation
|
|
146
181
|
|
|
147
182
|
```ts
|
|
183
|
+
import { dateOrder, requiredIf, sameAsField } from 'form-father';
|
|
184
|
+
|
|
148
185
|
const form = new Form($form, {
|
|
149
186
|
formValidators: [
|
|
150
|
-
(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
: {
|
|
154
|
-
field: 'passwordConfirm',
|
|
155
|
-
rule: 'same-as-password',
|
|
156
|
-
message: 'Passwords do not match',
|
|
157
|
-
},
|
|
158
|
-
({ values }) => (values.start <= values.end ? true : { field: 'end', message: 'End date must be after start' }),
|
|
187
|
+
sameAsField('passwordConfirm', 'password', 'Passwords do not match'),
|
|
188
|
+
requiredIf('email', ({ values }) => Boolean(values.subscribe), 'Email is required for subscription'),
|
|
189
|
+
dateOrder('start', 'end', 'End date must be after start'),
|
|
159
190
|
],
|
|
160
191
|
});
|
|
161
192
|
```
|
|
@@ -170,6 +201,16 @@ form.setErrors({
|
|
|
170
201
|
});
|
|
171
202
|
```
|
|
172
203
|
|
|
204
|
+
### Rule DX helpers
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { createLengthValidator, createPatternValidator, registerFieldValidator } from 'form-father';
|
|
208
|
+
|
|
209
|
+
registerFieldValidator('starts-with-a', value => value.startsWith('A'), 'Must start with A');
|
|
210
|
+
registerValidator('slug', createPatternValidator(/^[a-z0-9-]+$/), 'Slug characters only');
|
|
211
|
+
registerValidator('short-name', createLengthValidator({ min: 2, max: 24 }), 'Use 2-24 characters');
|
|
212
|
+
```
|
|
213
|
+
|
|
173
214
|
## Schema Adapters
|
|
174
215
|
|
|
175
216
|
Form Father has no schema-library runtime dependency, but can wrap Zod/Valibot/Yup-like schemas with `safeParse()` or
|
package/docs/recipes/README.md
CHANGED
|
@@ -52,18 +52,27 @@ form.reset();
|
|
|
52
52
|
## Cross-field validation
|
|
53
53
|
|
|
54
54
|
```ts
|
|
55
|
+
import { dateOrder, requiredIf, sameAsField } from 'form-father';
|
|
56
|
+
|
|
55
57
|
const form = new Form(document.querySelector('form')!, {
|
|
56
|
-
formValidators:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
rule: 'same-as-password',
|
|
62
|
-
message: 'Passwords do not match',
|
|
63
|
-
},
|
|
58
|
+
formValidators: [
|
|
59
|
+
sameAsField('passwordConfirm', 'password', 'Passwords do not match'),
|
|
60
|
+
requiredIf('email', ({ values }) => Boolean(values.subscribe), 'Email is required'),
|
|
61
|
+
dateOrder('start', 'end', 'End date must be after start'),
|
|
62
|
+
],
|
|
64
63
|
});
|
|
65
64
|
```
|
|
66
65
|
|
|
66
|
+
## Field rule helpers
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { createLengthValidator, createPatternValidator, registerFieldValidator, registerValidator } from 'form-father';
|
|
70
|
+
|
|
71
|
+
registerFieldValidator('starts-with-a', value => value.startsWith('A'), 'Must start with A');
|
|
72
|
+
registerValidator('slug', createPatternValidator(/^[a-z0-9-]+$/), 'Slug characters only');
|
|
73
|
+
registerValidator('short-name', createLengthValidator({ min: 2, max: 24 }), 'Use 2-24 characters');
|
|
74
|
+
```
|
|
75
|
+
|
|
67
76
|
## Applying backend errors
|
|
68
77
|
|
|
69
78
|
```ts
|
|
@@ -113,3 +122,29 @@ adding runtime dependencies to Form Father.
|
|
|
113
122
|
```
|
|
114
123
|
|
|
115
124
|
Field errors are shown through the same rendering path as client-side validation errors.
|
|
125
|
+
|
|
126
|
+
## Accessible error summary
|
|
127
|
+
|
|
128
|
+
```html
|
|
129
|
+
<form data-form-father novalidate>
|
|
130
|
+
<div data-form-father-summary hidden></div>
|
|
131
|
+
<label>
|
|
132
|
+
Email
|
|
133
|
+
<input class="input" name="email" data-validate="required|email" />
|
|
134
|
+
</label>
|
|
135
|
+
<button type="submit">Submit</button>
|
|
136
|
+
</form>
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const form = new Form(document.querySelector('form')!, {
|
|
141
|
+
inputWrapperSelector: 'label',
|
|
142
|
+
errorSummary: {
|
|
143
|
+
title: 'Please check the form',
|
|
144
|
+
focus: true,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Inline errors are linked to fields with `aria-describedby` by default. Use `ariaDescribeErrors: false` if your design
|
|
150
|
+
system owns that relationship itself.
|
package/package.json
CHANGED