form-father 0.3.0 → 0.5.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 +40 -0
- package/demos/index.html +12 -0
- package/demos/main.js +3 -4
- package/dist/FormFather.min.js +1 -1
- package/dist/index.d.ts +45 -2
- package/dist/index.js +174 -9
- package/dist/index.js.map +1 -1
- package/dist/types/adapters.d.ts +12 -0
- package/dist/types/index.d.ts +32 -1
- package/docs/en/README.md +40 -0
- package/docs/recipes/README.md +35 -0
- 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.5.0 - 2026-04-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added field validator DX helpers: `registerFieldValidator()`, `createPatternValidator()`, and
|
|
10
|
+
`createLengthValidator()`.
|
|
11
|
+
- Added form validator DX helpers: `createFormValidator()`, `sameAsField()`, `requiredIf()`, and `dateOrder()`.
|
|
12
|
+
- Added helper types for reusable form validator predicates and issue factories.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Demo and documentation now use higher-level helper APIs for common password-confirm and validation rule recipes.
|
|
17
|
+
|
|
18
|
+
### Tests
|
|
19
|
+
|
|
20
|
+
- Test suite expanded to 115 tests.
|
|
21
|
+
- Coverage baseline: 98.19% statements, 86.91% branches, 97.04% functions, 100% lines.
|
|
22
|
+
|
|
23
|
+
## 0.4.0 - 2026-04-27
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Added form-level and cross-field validation via `formValidators`.
|
|
28
|
+
- Added `setErrors()` for backend maps, `ErrorResponse[]`, global form errors, and custom validation issue objects.
|
|
29
|
+
- Added `FORM_ERROR_FIELD` constant for form-level errors in `getErrors()`.
|
|
30
|
+
- Added public TypeScript types for form validation contexts and issues.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- `validate()` now runs field-level rules first, then form-level validators over `getValues()`.
|
|
35
|
+
- Cross-field and backend errors share the same rendering path as field validation errors.
|
|
36
|
+
|
|
37
|
+
### Tests
|
|
38
|
+
|
|
39
|
+
- Test suite expanded to 113 tests.
|
|
40
|
+
- Coverage baseline: 98.31% statements, 87.05% branches, 96.34% functions, 100% lines.
|
|
41
|
+
|
|
5
42
|
## 0.3.0 - 2026-04-25
|
|
6
43
|
|
|
7
44
|
### Added
|
package/README.md
CHANGED
|
@@ -69,12 +69,16 @@ const forms = Form.initAll('form[data-form-father]', {
|
|
|
69
69
|
|
|
70
70
|
```ts
|
|
71
71
|
import Form, {
|
|
72
|
+
FORM_ERROR_FIELD,
|
|
72
73
|
type FormOptions,
|
|
73
74
|
type ValidationSchema,
|
|
74
75
|
type ResponseBody,
|
|
75
76
|
type ValidationError,
|
|
76
77
|
type SubmitResult,
|
|
77
78
|
type FormResetOptions,
|
|
79
|
+
type FormValidator,
|
|
80
|
+
type ValidationIssue,
|
|
81
|
+
type FormValidatorPredicate,
|
|
78
82
|
} from 'form-father';
|
|
79
83
|
```
|
|
80
84
|
|
|
@@ -107,6 +111,7 @@ import Form, {
|
|
|
107
111
|
- **validationStateAttribute**: Атрибут состояния поля: `validating`, `valid`, `invalid`. По умолчанию
|
|
108
112
|
`data-form-father-state`.
|
|
109
113
|
- **observeMutations**: Следить за динамически добавленными полями и submit-кнопками.
|
|
114
|
+
- **formValidators**: Валидатор или массив валидаторов всей формы для cross-field правил.
|
|
110
115
|
|
|
111
116
|
## Отправка формы
|
|
112
117
|
|
|
@@ -126,6 +131,7 @@ import Form, {
|
|
|
126
131
|
- **validate()**: Проверяет всю форму.
|
|
127
132
|
- **validateField(field)**: Проверяет одно поле по имени или DOM-элементу.
|
|
128
133
|
- **showFieldError(field, message, source)**: Показывает ошибку поля вручную.
|
|
134
|
+
- **setErrors(errors, source)**: Применяет backend/form-level ошибки из объекта, массива или `ErrorResponse[]`.
|
|
129
135
|
- **getErrors()**: Возвращает текущие ошибки в формате `{ field, rule, message, source }`.
|
|
130
136
|
- **getValues()**: Возвращает значения формы обычным объектом.
|
|
131
137
|
- **setValues(values)**: Заполняет поля по `name` и диспатчит `input`/`change`.
|
|
@@ -321,6 +327,40 @@ required → schema.rules → data-validate → data-custom-validate
|
|
|
321
327
|
- Для каждого поля отображается **только первое** найденное сообщение об ошибке.
|
|
322
328
|
- Неизвестные правила логируются и пропускаются до начала проверки, поэтому не влияют на результат валидации.
|
|
323
329
|
|
|
330
|
+
### Cross-field и form-level validation
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import { dateOrder, requiredIf, sameAsField } from 'form-father';
|
|
334
|
+
|
|
335
|
+
const form = new Form($form, {
|
|
336
|
+
formValidators: [
|
|
337
|
+
sameAsField('passwordConfirm', 'password', 'Пароли не совпадают'),
|
|
338
|
+
requiredIf('email', ({ values }) => Boolean(values.subscribe), 'Email обязателен для подписки'),
|
|
339
|
+
dateOrder('start', 'end', 'Дата окончания раньше начала'),
|
|
340
|
+
],
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Глобальную ошибку формы можно вернуть строкой или issue без `field`. В `getErrors()` она будет иметь поле
|
|
345
|
+
`FORM_ERROR_FIELD` (`"_form"`).
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
form.setErrors({
|
|
349
|
+
email: 'Email уже занят',
|
|
350
|
+
[FORM_ERROR_FIELD]: 'Проверьте данные формы',
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### DX helpers для правил
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
import { createLengthValidator, createPatternValidator, registerFieldValidator } from 'form-father';
|
|
358
|
+
|
|
359
|
+
registerFieldValidator('starts-with-a', value => value.startsWith('A'), 'Должно начинаться с A');
|
|
360
|
+
registerValidator('slug', createPatternValidator(/^[a-z0-9-]+$/), 'Только slug-символы');
|
|
361
|
+
registerValidator('short-name', createLengthValidator({ min: 2, max: 24 }), 'От 2 до 24 символов');
|
|
362
|
+
```
|
|
363
|
+
|
|
324
364
|
## Адаптеры схем
|
|
325
365
|
|
|
326
366
|
Form Father не тянет внешние зависимости, но умеет оборачивать Zod/Valibot/Yup-подобные схемы с `safeParse()` или
|
package/demos/index.html
CHANGED
|
@@ -51,6 +51,18 @@
|
|
|
51
51
|
/>
|
|
52
52
|
</label>
|
|
53
53
|
|
|
54
|
+
<label class="field">
|
|
55
|
+
<span>Repeat password</span>
|
|
56
|
+
<input
|
|
57
|
+
class="input"
|
|
58
|
+
name="passwordConfirm"
|
|
59
|
+
type="password"
|
|
60
|
+
autocomplete="current-password"
|
|
61
|
+
data-validate="required"
|
|
62
|
+
data-error-required="Повторите пароль"
|
|
63
|
+
/>
|
|
64
|
+
</label>
|
|
65
|
+
|
|
54
66
|
<button type="submit">Войти</button>
|
|
55
67
|
</form>
|
|
56
68
|
</section>
|
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,6 +76,7 @@ Form.initAll('form[data-form-father]', {
|
|
|
78
76
|
validationDebounce: 120,
|
|
79
77
|
focusFirstErroredInput: true,
|
|
80
78
|
loaderColor: 'currentColor',
|
|
79
|
+
formValidators: sameAsField('passwordConfirm', 'password', 'Пароли не совпадают'),
|
|
81
80
|
onValidationError(errors) {
|
|
82
81
|
writeOutput('Client validation errors', errors);
|
|
83
82
|
},
|
package/dist/FormFather.min.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var FormFather=function(t){"use strict";function e(t,e,i,r){return new(i||(i=Promise))((function(n,o){function s(t){try{l(r.next(t))}catch(t){o(t)}}function a(t){try{l(r.throw(t))}catch(t){o(t)}}function l(t){var e;t.done?n(t.value):(e=t.value,e instanceof i?e:new i((function(t){t(e)}))).then(s,a)}l((r=r.apply(t,e||[])).next())}))}function i(t,e){if(null==t)return null;let i=t;for(;i;){if(i.matches(e))return i;i=i.parentElement}return null}"function"==typeof SuppressedError&&SuppressedError;const r=["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 r.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")),i=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:r}=t;if("file"===r)if(t.multiple){const r=t.files;if(r)for(let t=0;t<r.length;t++)i.append(`${e}[${t}]`,r[t])}else{const r=t.files;r&&r[0]&&i.append(e,r[0])}else{let n;if("select-one"===r||"select-multiple"===r){const o=t;if("select-multiple"===r){Array.from(o.selectedOptions).map((t=>t.value)).forEach(((t,r)=>{i.append(`${e}[${r}]`,t)}))}else n=o.value,n&&i.append(e,n)}else if("tel"===r)n=t.value.split(/[\s()-]/).join("").replace(/^8/,"+7");else if("checkbox"===r||"radio"===r){if(!t.checked)return;n=t.value}else if("date"===r){const e=t.getAttribute("data-date-format");n=e?function(t,e){const i=new Date(t),r=t=>t.toString().padStart(2,"0"),n=r(i.getDate()),o=r(i.getMonth()+1),s=i.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"!==r&&"select-multiple"!==r&&i.append(e,n)}})),i}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,i)=>{e.hasOwnProperty(i)?(Array.isArray(e[i])||(e[i]=[e[i]]),e[i].push(t)):e[i]=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,i,{override:r=!1}={}){!h.has(t)||r?h.set(t,{fn:e,defaultMessage:i}):console.warn(`[FormFather] Validator "${t}" already exists; pass { override: true } to replace`)}const f=t=>h.get(t);function m(t,e,i){h.has(t)||h.set(t,{fn:e,defaultMessage:i})}function g(t,e){return"function"==typeof e?e(t):e||function(t){var e;if(!t||"object"!=typeof t)return;const i=t.issues||t.errors;if(Array.isArray(i)){const t=null===(e=i.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,i={}){return(r,n,o,s)=>e(this,void 0,void 0,(function*(){const e=i.mapValue?i.mapValue(r,n,o,s):r;try{if("function"==typeof t)return a=yield t(e),l=g(void 0,i.message),"object"==typeof a&&null!==a?a:void 0===a||!!a||{valid:!1,message:l};if("function"==typeof t.safeParse){const r=yield t.safeParse(e);return!!r.success||{valid:!1,message:g(r.error||r,i.message)}}if("function"==typeof t.parse)return yield t.parse(e),!0}catch(t){return{valid:!1,message:g(t,i.message)}}var a,l;return{valid:!1,message:g(void 0,i.message)}}))}var b;m("required",((t,e,i)=>{if(e&&e.type){if("checkbox"===e.type)return e.checked;if("radio"===e.type){const t=i.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=>s(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};let w=0;const S=new WeakMap,F={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 A{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=F}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},A.defaultParams,e),this.$el?(this.$submits=this.findSubmitElements(),this.isCorrectArguments()&&(this.bindSubmitHandlers(),this.initialization(),this.bindFieldValidationEvents(),this.setupMutationObserver(),S.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 i=S.get(t);return i?(i.updateOptions(e),i):new A(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 i=t[e.name];if(!(e instanceof HTMLInputElement&&"file"===e.type)){if(e instanceof HTMLInputElement&&"checkbox"===e.type)e.checked="boolean"==typeof i?i:this.normalizeValueList(i).includes(e.value);else if(e instanceof HTMLInputElement&&"radio"===e.type)e.checked=String(i)===e.value;else if(e instanceof HTMLSelectElement&&e.multiple){const t=this.normalizeValueList(i);Array.from(e.options).forEach((e=>{e.selected=t.includes(e.value)}))}else e.value=null==i?"":String(i);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}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,i,r,n,o,s;t.preventDefault(),null===(i=(e=this.config).onBeforeValidate)||void 0===i||i.call(e,this);const a=yield this.validate();return null===(n=(r=this.config).onAfterValidate)||void 0===n||n.call(r,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 i=null==t?e:Array.isArray(t)?t:[t];return Array.from(new Set(i))}clearFieldValidationEvents(){this.fieldEventHandlers.forEach((({$input:t,event:e,handler:i})=>{t.removeEventListener(e,i)})),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)),i=Array.from(new Set([...t,...e]));if(0===i.length)return;this.getAllFields().forEach((e=>{i.forEach((i=>{const r=()=>{(t.includes(i)||this.hasFieldError(e))&&this.scheduleFieldValidation(e)};e.addEventListener(i,r),this.fieldEventHandlers.push({$input:e,event:i,handler:r})}))}))}scheduleFieldValidation(t){const e=this.liveValidationTimers.get(t);e&&clearTimeout(e);const i=Math.max(0,Number(this.config.validationDebounce)||0),r=()=>{this.validateField(t)};if(0===i)return void r();const n=setTimeout(r,i);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",i=t.getAttribute(e);if(!i)return null;try{return document.querySelector(i)}catch(t){return console.warn(`[FormFather] Invalid error container selector "${i}"`),null}}showError(t,e){t.setAttribute("aria-invalid","true");const r=this.getErrorContainer(t);if(r)return r.textContent=e,r.hasAttribute("role")||r.setAttribute("role","alert"),void r.removeAttribute("hidden");const n=i(t,this.config.inputWrapperSelector);if(!n)return void console.warn(`Не найдена обёртка для поля с именем: ${t.name||t.type}`);const o=n.showError;if("function"==typeof o)return void o.call(n,e);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}showLoader(){this.$submits.forEach((t=>{if(t.classList.add("button--loading"),t.querySelector(".button__loader"))return;const e="form-father-loader-"+w++,i=`\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`,r=document.createElement("span");r.innerHTML=i,r.className="button__loader",t.insertAdjacentElement("afterbegin",r),requestAnimationFrame((()=>{r.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 r=i(t,this.config.inputWrapperSelector)||t.parentElement,n=this.getErrorContainer(t);t.removeAttribute("aria-invalid"),n&&(n.textContent="",n.setAttribute("hidden","")),null===(e=null==r?void 0:r.querySelector("[data-form-father-error]"))||void 0===e||e.remove(),null==r||r.classList.remove("input__wrapper--error"),r&&r.classList.contains("input__wrapper")&&r.getAttribute("data-type-error")}scrollToFirstErroredInput(t){if(t.length>0){const e=Array.from(this.$el.querySelectorAll("input, textarea, select"));let i=null,r=1/0;if(t.forEach((t=>{const n=e.findIndex((e=>e===t));n<r&&(r=n,i=t)})),i){const t=i.getBoundingClientRect(),e=window.pageYOffset||document.documentElement.scrollTop,r=t.top+e-window.innerHeight/2;document.documentElement.setAttribute("data-now-scrolling",""),window.scrollTo({top:r,behavior:"smooth"}),setTimeout((()=>{document.documentElement.removeAttribute("data-now-scrolling")}),800)}}}focusFirstErroredInput(t){var e;if(0===t.length)return;const i=Array.from(this.$el.querySelectorAll("input, textarea, select")),r=t.slice().sort(((t,e)=>i.indexOf(t)-i.indexOf(e)))[0];null===(e=null==r?void 0:r.focus)||void 0===e||e.call(r,{preventScroll:!0})}getValidationSchema(){return Object.assign(Object.assign({},A.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))}getValidationStateAttribute(){return this.config.validationStateAttribute||"data-form-father-state"}setFieldValidationState(t,e){const i=this.getValidationStateAttribute();e?t.setAttribute(i,e):t.removeAttribute(i)}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,i){this.isLatestFieldValidation(t,e)&&(t.removeAttribute("aria-busy"),this.setFieldValidationState(t,i?"valid":"invalid"))}clearFieldValidationState(t){this.fieldValidationTokens.delete(t),t.removeAttribute("aria-busy"),this.setFieldValidationState(t,null)}setValidationError(t,e,i,r){this.clearFieldErrorRecord(t),this.errors.push({field:this.getFieldKey(t),rule:e,message:i,source:r})}resolveField(t){return"string"!=typeof t?t:this.findFieldByName(t)}parseRuleString(t){const[e,...i]=t.split(":").map((t=>t.trim()));return i.length>0?{rule:e,params:1===i.length?i[0]:i}: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=[],i=new Set){const r=[];t.hasAttribute("required")&&r.push("required"),r.push(...e);const n=this.parseValidationAttribute(t.getAttribute("data-validate")).filter((t=>{const e=this.getRuleName(t);return f(e)||!i.has(e)}));r.push(...n),r.push(...this.parseValidationAttribute(t.getAttribute("data-custom-validate")));const o=[],s=new Set;for(const t of r){const e=this.getRuleName(t);if(!f(e)){console.warn(`[FormFather] Unknown validation rule "${e}"`);continue}const i=this.getRuleIdentity(t);s.has(i)||(s.add(i),o.push(t))}return o}getRuleMessage(t,e,i,r,n){const o=t.getAttribute(`data-error-${e}`);return n||o||i[e]||r}fieldMatchesSchemaDefinition(t,e,i){if(i.selector)try{return t.matches(i.selector)}catch(t){return console.warn(`[FormFather] Invalid validation selector "${i.selector}"`),!1}return this.parseValidationAttribute(t.getAttribute("data-validate")).some((t=>this.getRuleName(t)===e))}getFieldValidationConfig(t,e=this.getValidationSchema()){const i=[],r={};for(const[n,o]of Object.entries(e))this.fieldMatchesSchemaDefinition(t,n,o)&&(i.push(...o.rules),Object.assign(r,o.messages||{}));return{rules:this.collectRules(t,i,new Set(Object.keys(e))),messages:r}}getValidationFields(t,e){const i=new Set,r=t=>{Array.from(t).forEach((t=>{(t instanceof HTMLInputElement||t instanceof HTMLTextAreaElement||t instanceof HTMLSelectElement)&&i.add(t)}))};for(const[n,o]of Object.entries(e))if(o.selector)try{r(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=>i.add(t)));return r(t.querySelectorAll("[required], [data-validate], [data-custom-validate]")),Array.from(i)}validateInput(t,i){return e(this,arguments,void 0,(function*(t,e,i={},r=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,r,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:r,params:s})),!this.isLatestFieldValidation(t,n))return!this.hasFieldError(t);if(!c){const r=l,n=this.getRuleMessage(t,e,i,a.defaultMessage,d&&"string"==typeof r.message?r.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,i){return e(this,arguments,void 0,(function*(t,e,i={},r=this.$el){const n=this.startFieldValidation(t);try{const o=yield this.validateInput(t,e,i,r,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,i="manual"){const r=this.resolveField(t);return r?(this.setValidationError(r,i,e,i),this.showError(r,e),r.removeAttribute("aria-busy"),this.setFieldValidationState(r,"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:i,messages:r}=this.getFieldValidationConfig(e);return this.runFieldValidation(e,i,r)}))}validate(){return e(this,arguments,void 0,(function*(t=this.$el){const e=this.getValidationSchema(),i=[],r=new WeakSet;let n=!0;this.errors=[];for(const o of this.getValidationFields(t,e)){if(r.has(o))continue;const s=this.getRadioGroup(o,t);s.length>0?s.forEach((t=>r.add(t))):r.add(o);const{rules:a,messages:l}=this.getFieldValidationConfig(o,e);(yield this.runFieldValidation(o,a,l,t))||(n=!1,i.push(o))}const o=i.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 i=t;"radio"===i.type||"checkbox"===i.type?i.checked=!1:(i.value="",null===(e=i.parentElement)||void 0===e||e.classList.remove("filled"),i.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 i=t;null===(e=null==i?void 0:i.getAnimations)||void 0===e||e.call(i).forEach((t=>t.cancel()))}showErrorForm(t){var e;const i=this.$el.querySelectorAll(this.config.inputWrapperSelector),r=i[i.length-1];let n=this.$el.querySelector(".error-block-under-input__wrapper");!n||r&&r.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"}),r?r.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],i=(null==e?void 0:e.nextElementSibling)||this.$el.querySelector(".error-block-under-input__wrapper");if(!i||!i.classList.contains("error-block-under-input__wrapper"))return;if((""===i.style.height||"auto"===i.style.height)&&(i.style.height=`${i.clientHeight}px`),i.style.paddingTop=i.style.paddingTop||`${this._anim.paddingTopOpened}px`,i.style.opacity=i.style.opacity||"1",this._cancelAnimations(i),this._canAnimate(i)){i.animate([{height:`${parseFloat(i.style.height)||i.clientHeight}px`,paddingTop:`${parseFloat(i.style.paddingTop)||this._anim.paddingTopOpened}px`,opacity:parseFloat(i.style.opacity||"1")},{height:"0px",paddingTop:"0px",opacity:0}],{duration:this._anim.duration,easing:this._anim.easing,fill:"forwards"}).addEventListener("finish",(()=>{i.remove()}))}else i.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,S.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,i)=>{Object.prototype.hasOwnProperty.call(e,i)?(Array.isArray(e[i])||(e[i]=[e[i]]),e[i].push(t)):e[i]=t})),e}appendFormDataValue(t,e,i){null!=i&&(Array.isArray(i)?i.forEach(((i,r)=>{this.appendFormDataValue(t,`${e}[${r}]`,i)})):i instanceof Blob?t.append(e,i):t.append(e,"object"==typeof i?JSON.stringify(i):String(i)))}objectToFormData(t){const e=new FormData;return Object.entries(t).forEach((([t,i])=>{this.appendFormDataValue(e,t,i)})),e}stringifyFormDataValue(t){return"string"==typeof t?t:t.name}formDataToUrlSearchParams(t){const e=new URLSearchParams;return t.forEach(((t,i)=>{e.append(i,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 i=e.toString();if(!i)return t;const r=t.includes("?")?"&":"?";return`${t}${r}${i}`}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 i=this.findFieldByName(t.name);i?(e.push(i),this.setValidationError(i,"server",t["error-msg"],"server"),this.showError(i,t["error-msg"]),i.removeAttribute("aria-busy"),this.setFieldValidationState(i,"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")||"",i=(null===(t=this.$el.getAttribute("method"))||void 0===t?void 0:t.toUpperCase())||"POST",r=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(i))return fetch(this.appendQueryString(e,this.formDataToUrlSearchParams(c)),{method:i,headers:o});switch(r){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: ${r}. Используется application/x-www-form-urlencoded.`),n=this.formDataToUrlSearchParams(c).toString(),o["Content-Type"]="application/x-www-form-urlencoded"}return fetch(e,{method:i,body:n,headers:o})}))}submit(){return e(this,void 0,void 0,(function*(){var t,e,i,r,n,o,s,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===(r=(i=this.config).onSubmit)||void 0===r||r.call(i,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.blockScrollBody=function(){window.lastBlockScrollPosition=window.pageYOffset,document.body.style.top=-window.lastBlockScrollPosition+"px",document.body.classList.add("body--block-scroll")},t.closest=i,t.createFieldValidator=function(t,i){return(r,n,o,s)=>e(this,void 0,void 0,(function*(){return!!(yield t(r,n,o,s))||{valid:!1,message:i}}))},t.createSchemaValidator=y,t.default=A,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.registerSchemaValidator=function(t,e,i,r={}){const{override:n}=r;p(t,y(e,function(t,e){var i={};for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&e.indexOf(r)<0&&(i[r]=t[r]);if(null!=t&&"function"==typeof Object.getOwnPropertySymbols){var n=0;for(r=Object.getOwnPropertySymbols(t);n<r.length;n++)e.indexOf(r[n])<0&&Object.prototype.propertyIsEnumerable.call(t,r[n])&&(i[r[n]]=t[r[n]])}return i}(r,["override"])),i,{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 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,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 v(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 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=>s(t)),"Неверный формат"),m("not-numbers",(t=>!/[0-9]/.test(t)),"Неверный формат");const E=Symbol.for("formfather.shared"),w=null!==(b=globalThis[E])&&void 0!==b?b:globalThis[E]={defaultParams:{},defaultValidationSchema:void 0},S="_form";function F(t,e){return t[e]}function A(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;const T=new WeakMap,O={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 _{static get defaultParams(){return w.defaultParams}static set defaultParams(t){w.defaultParams=t}static get defaultValidationSchema(){var t;return null!==(t=w.defaultValidationSchema)&&void 0!==t?t:w.defaultValidationSchema=O}static set defaultValidationSchema(t){w.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},_.defaultParams,e),this.$el?(this.$submits=this.findSubmitElements(),this.isCorrectArguments()&&(this.bindSubmitHandlers(),this.initialization(),this.bindFieldValidationEvents(),this.setupMutationObserver(),T.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=T.get(t);return r?(r.updateOptions(e),r):new _(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||S;if(n===S)return this.setErrorRecord(S,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}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}}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 o=n.showError;if("function"==typeof o)return void o.call(n,e);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}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"),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({},_.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"),!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 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}))}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.$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,T.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,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 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===(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=S,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=v,t.createFormValidator=V,t.createLengthValidator=function(t,e){return v((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 v((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=F(r,t),n=F(r,e);return!(!A(i)&&!A(n))||Date.parse(String(i))<=Date.parse(String(n))}),{field:e,rule:i,message:r})},t.default=_,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={}){p(t,v(e),r,i)},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.requiredIf=function(t,r,i="Поле обязательно",n="conditional-required"){return V((i=>e(this,void 0,void 0,(function*(){return!(yield r(i))||!A(F(i.values,t))}))),{field:t,rule:n,message:i})},t.sameAsField=function(t,e,r="Значения не совпадают",i="same-as-field"){return V((({values:r})=>F(r,t)===F(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
|
* Находит родительский элемент по заданому селектору.
|
|
@@ -121,6 +133,7 @@ type ValidationRule = string | {
|
|
|
121
133
|
type ValidationTrigger = 'submit' | 'input' | 'blur' | 'change';
|
|
122
134
|
type ValidationErrorSource = 'client' | 'server' | 'manual';
|
|
123
135
|
type FieldReference = string | FormFieldElement;
|
|
136
|
+
declare const FORM_ERROR_FIELD = "_form";
|
|
124
137
|
interface ValidationError {
|
|
125
138
|
field: string;
|
|
126
139
|
rule: string;
|
|
@@ -136,11 +149,32 @@ interface SubmitResult {
|
|
|
136
149
|
interface FormResetOptions {
|
|
137
150
|
clearErrors?: boolean;
|
|
138
151
|
}
|
|
152
|
+
interface ValidationIssue {
|
|
153
|
+
field?: string;
|
|
154
|
+
rule?: string;
|
|
155
|
+
message: string;
|
|
156
|
+
source?: ValidationErrorSource;
|
|
157
|
+
}
|
|
158
|
+
type ValidationIssueInput = string | ValidationIssue | ErrorResponse | Record<string, string> | Array<string | ValidationIssue | ErrorResponse>;
|
|
159
|
+
interface FormValidatorContext {
|
|
160
|
+
form: Form;
|
|
161
|
+
formElement: HTMLFormElement;
|
|
162
|
+
values: Record<string, any>;
|
|
163
|
+
errors: ValidationError[];
|
|
164
|
+
}
|
|
165
|
+
type FormValidatorResult = boolean | string | void | ValidationIssue | Array<string | ValidationIssue>;
|
|
166
|
+
type FormValidator = (context: FormValidatorContext) => FormValidatorResult | Promise<FormValidatorResult>;
|
|
167
|
+
type FormValidatorPredicate = (context: FormValidatorContext) => boolean | Promise<boolean>;
|
|
168
|
+
type FormValidatorIssueFactory = string | ValidationIssue | ((context: FormValidatorContext) => string | ValidationIssue | Promise<string | ValidationIssue>);
|
|
139
169
|
type ValidationSchema = Record<string, {
|
|
140
170
|
rules: ValidationRule[];
|
|
141
171
|
selector?: string;
|
|
142
172
|
messages?: Record<string, string>;
|
|
143
173
|
}>;
|
|
174
|
+
declare function createFormValidator(predicate: FormValidatorPredicate, issue: FormValidatorIssueFactory): FormValidator;
|
|
175
|
+
declare function sameAsField(field: string, otherField: string, message?: string, rule?: string): FormValidator;
|
|
176
|
+
declare function requiredIf(field: string, condition: FormValidatorPredicate, message?: string, rule?: string): FormValidator;
|
|
177
|
+
declare function dateOrder(startField: string, endField: string, message?: string, rule?: string): FormValidator;
|
|
144
178
|
type FormFieldElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
145
179
|
/** Параметры формы. */
|
|
146
180
|
interface FormOptions {
|
|
@@ -243,6 +277,8 @@ interface FormOptions {
|
|
|
243
277
|
wrapData?: (data: Record<string, any>) => Record<string, any>;
|
|
244
278
|
/** Схема валидации: поле → массив правил + override-сообщения */
|
|
245
279
|
validationSchema?: ValidationSchema;
|
|
280
|
+
/** Валидаторы всей формы: cross-field, conditional required, date ranges и другие проверки над `getValues()`. */
|
|
281
|
+
formValidators?: FormValidator | FormValidator[];
|
|
246
282
|
}
|
|
247
283
|
interface ErrorResponse {
|
|
248
284
|
name: string;
|
|
@@ -305,6 +341,12 @@ declare class Form {
|
|
|
305
341
|
setValues(values: Record<string, any>): this;
|
|
306
342
|
/** Снимает все клиентские и серверные ошибки с формы, не меняя значения полей. */
|
|
307
343
|
clearErrors(): this;
|
|
344
|
+
private isErrorResponse;
|
|
345
|
+
private normalizeValidationIssues;
|
|
346
|
+
private applyValidationIssue;
|
|
347
|
+
/** Показывает несколько ошибок формы или полей: из backend map/array, `ErrorResponse[]` или form validators. */
|
|
348
|
+
setErrors(issues: ValidationIssueInput, source?: ValidationErrorSource): this;
|
|
349
|
+
private validateFormValidators;
|
|
308
350
|
/** Сбрасывает форму через native `reset()` и опционально очищает ошибки. */
|
|
309
351
|
reset(options?: FormResetOptions): this;
|
|
310
352
|
/**
|
|
@@ -355,6 +397,7 @@ declare class Form {
|
|
|
355
397
|
private getFieldKey;
|
|
356
398
|
private hasFieldError;
|
|
357
399
|
private clearFieldErrorRecord;
|
|
400
|
+
private setErrorRecord;
|
|
358
401
|
private getValidationStateAttribute;
|
|
359
402
|
private setFieldValidationState;
|
|
360
403
|
private startFieldValidation;
|
|
@@ -376,7 +419,7 @@ declare class Form {
|
|
|
376
419
|
private runFieldValidation;
|
|
377
420
|
private getRadioGroup;
|
|
378
421
|
/** Показывает ошибку конкретного поля по имени или DOM-элементу. */
|
|
379
|
-
showFieldError(field: FieldReference, message: string, source?: ValidationErrorSource): boolean;
|
|
422
|
+
showFieldError(field: FieldReference, message: string, source?: ValidationErrorSource, rule?: string): boolean;
|
|
380
423
|
/** Проверяет одно поле по имени или DOM-элементу. */
|
|
381
424
|
validateField(field: FieldReference): Promise<boolean>;
|
|
382
425
|
/**
|
|
@@ -437,4 +480,4 @@ declare class Form {
|
|
|
437
480
|
submit(): Promise<SubmitResult | undefined>;
|
|
438
481
|
}
|
|
439
482
|
|
|
440
|
-
export { type AdapterValidator, type ErrorResponse, type FieldReference, type FormFieldElement, type FormOptions, type FormResetOptions, type RegisterSchemaValidatorOptions, type ResponseBody, type SchemaLike, type SchemaParseResult, type SchemaValidatorOptions, type SubmitResult, type ValidationError, type ValidationErrorSource, 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 };
|
|
483
|
+
export { type AdapterValidator, type ErrorResponse, 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) ---
|
|
@@ -445,6 +466,49 @@ const __shared =
|
|
|
445
466
|
defaultParams: {},
|
|
446
467
|
defaultValidationSchema: undefined,
|
|
447
468
|
});
|
|
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
|
+
}
|
|
448
512
|
let loaderIdCounter = 0;
|
|
449
513
|
const FORM_INSTANCES = new WeakMap();
|
|
450
514
|
/** Изначальная дефолтная схема. */
|
|
@@ -637,6 +701,99 @@ class Form {
|
|
|
637
701
|
this.hideErrorForm();
|
|
638
702
|
return this;
|
|
639
703
|
}
|
|
704
|
+
isErrorResponse(issue) {
|
|
705
|
+
return (!!issue &&
|
|
706
|
+
typeof issue === 'object' &&
|
|
707
|
+
typeof issue.name === 'string' &&
|
|
708
|
+
typeof issue['error-msg'] === 'string');
|
|
709
|
+
}
|
|
710
|
+
normalizeValidationIssues(issues, source) {
|
|
711
|
+
if (issues == null || issues === true)
|
|
712
|
+
return [];
|
|
713
|
+
if (issues === false)
|
|
714
|
+
return [{ message: 'Некорректные данные формы', source }];
|
|
715
|
+
if (typeof issues === 'string')
|
|
716
|
+
return [{ message: issues, source }];
|
|
717
|
+
if (Array.isArray(issues)) {
|
|
718
|
+
return issues.flatMap(issue => this.normalizeValidationIssues(issue, source));
|
|
719
|
+
}
|
|
720
|
+
if (this.isErrorResponse(issues)) {
|
|
721
|
+
return [
|
|
722
|
+
{
|
|
723
|
+
field: issues.name,
|
|
724
|
+
rule: source,
|
|
725
|
+
message: issues['error-msg'],
|
|
726
|
+
source,
|
|
727
|
+
},
|
|
728
|
+
];
|
|
729
|
+
}
|
|
730
|
+
if (typeof issues === 'object' && 'message' in issues && typeof issues.message === 'string') {
|
|
731
|
+
return [Object.assign(Object.assign({}, issues), { source: issues.source || source })];
|
|
732
|
+
}
|
|
733
|
+
if (typeof issues === 'object') {
|
|
734
|
+
return Object.entries(issues).map(([field, message]) => ({
|
|
735
|
+
field,
|
|
736
|
+
rule: source,
|
|
737
|
+
message,
|
|
738
|
+
source,
|
|
739
|
+
}));
|
|
740
|
+
}
|
|
741
|
+
return [];
|
|
742
|
+
}
|
|
743
|
+
applyValidationIssue(issue, source) {
|
|
744
|
+
const issueSource = issue.source || source;
|
|
745
|
+
const rule = issue.rule || issueSource;
|
|
746
|
+
const field = issue.field || FORM_ERROR_FIELD;
|
|
747
|
+
if (field === FORM_ERROR_FIELD) {
|
|
748
|
+
this.setErrorRecord(FORM_ERROR_FIELD, rule, issue.message, issueSource);
|
|
749
|
+
this.showErrorForm(issue.message);
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
const $field = this.findFieldByName(field);
|
|
753
|
+
if (!$field) {
|
|
754
|
+
this.setErrorRecord(field, rule, issue.message, issueSource);
|
|
755
|
+
console.warn(`Не найдено поле с именем: ${field}, для вывода ошибки: ${issue.message}`);
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
this.showFieldError($field, issue.message, issueSource, rule);
|
|
759
|
+
return $field;
|
|
760
|
+
}
|
|
761
|
+
/** Показывает несколько ошибок формы или полей: из backend map/array, `ErrorResponse[]` или form validators. */
|
|
762
|
+
setErrors(issues, source = 'server') {
|
|
763
|
+
this.normalizeValidationIssues(issues, source).forEach(issue => {
|
|
764
|
+
this.applyValidationIssue(issue, source);
|
|
765
|
+
});
|
|
766
|
+
return this;
|
|
767
|
+
}
|
|
768
|
+
validateFormValidators() {
|
|
769
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
770
|
+
const validators = this.config.formValidators
|
|
771
|
+
? Array.isArray(this.config.formValidators)
|
|
772
|
+
? this.config.formValidators
|
|
773
|
+
: [this.config.formValidators]
|
|
774
|
+
: [];
|
|
775
|
+
const erroredInputs = [];
|
|
776
|
+
let ok = true;
|
|
777
|
+
for (const validator of validators) {
|
|
778
|
+
const result = yield validator({
|
|
779
|
+
form: this,
|
|
780
|
+
formElement: this.$el,
|
|
781
|
+
values: this.getValues(),
|
|
782
|
+
errors: this.getErrors(),
|
|
783
|
+
});
|
|
784
|
+
const issues = this.normalizeValidationIssues(result, 'client');
|
|
785
|
+
if (issues.length === 0)
|
|
786
|
+
continue;
|
|
787
|
+
ok = false;
|
|
788
|
+
issues.forEach(issue => {
|
|
789
|
+
const $field = this.applyValidationIssue(issue, 'client');
|
|
790
|
+
if ($field)
|
|
791
|
+
erroredInputs.push($field);
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return { ok, erroredInputs };
|
|
795
|
+
});
|
|
796
|
+
}
|
|
640
797
|
/** Сбрасывает форму через native `reset()` и опционально очищает ошибки. */
|
|
641
798
|
reset(options = {}) {
|
|
642
799
|
this.$el.reset();
|
|
@@ -936,6 +1093,14 @@ class Form {
|
|
|
936
1093
|
const field = this.getFieldKey($input);
|
|
937
1094
|
this.errors = this.errors.filter(error => error.field !== field);
|
|
938
1095
|
}
|
|
1096
|
+
setErrorRecord(field, rule, message, source) {
|
|
1097
|
+
this.errors.push({
|
|
1098
|
+
field,
|
|
1099
|
+
rule,
|
|
1100
|
+
message,
|
|
1101
|
+
source,
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
939
1104
|
getValidationStateAttribute() {
|
|
940
1105
|
return this.config.validationStateAttribute || 'data-form-father-state';
|
|
941
1106
|
}
|
|
@@ -971,12 +1136,7 @@ class Form {
|
|
|
971
1136
|
}
|
|
972
1137
|
setValidationError($input, rule, message, source) {
|
|
973
1138
|
this.clearFieldErrorRecord($input);
|
|
974
|
-
this.
|
|
975
|
-
field: this.getFieldKey($input),
|
|
976
|
-
rule,
|
|
977
|
-
message,
|
|
978
|
-
source,
|
|
979
|
-
});
|
|
1139
|
+
this.setErrorRecord(this.getFieldKey($input), rule, message, source);
|
|
980
1140
|
}
|
|
981
1141
|
resolveField(field) {
|
|
982
1142
|
if (typeof field !== 'string')
|
|
@@ -1161,13 +1321,13 @@ class Form {
|
|
|
1161
1321
|
return Array.from($block.querySelectorAll('input[type="radio"]')).filter($radio => $radio.name === $input.name);
|
|
1162
1322
|
}
|
|
1163
1323
|
/** Показывает ошибку конкретного поля по имени или DOM-элементу. */
|
|
1164
|
-
showFieldError(field, message, source = 'manual') {
|
|
1324
|
+
showFieldError(field, message, source = 'manual', rule = source) {
|
|
1165
1325
|
const $field = this.resolveField(field);
|
|
1166
1326
|
if (!$field) {
|
|
1167
1327
|
console.warn(`[FormFather] Field "${String(field)}" not found`);
|
|
1168
1328
|
return false;
|
|
1169
1329
|
}
|
|
1170
|
-
this.setValidationError($field,
|
|
1330
|
+
this.setValidationError($field, rule, message, source);
|
|
1171
1331
|
this.showError($field, message);
|
|
1172
1332
|
$field.removeAttribute('aria-busy');
|
|
1173
1333
|
this.setFieldValidationState($field, 'invalid');
|
|
@@ -1222,6 +1382,11 @@ class Form {
|
|
|
1222
1382
|
erroredInputs.push($input);
|
|
1223
1383
|
}
|
|
1224
1384
|
}
|
|
1385
|
+
const formValidation = yield this.validateFormValidators();
|
|
1386
|
+
if (!formValidation.ok) {
|
|
1387
|
+
ok = false;
|
|
1388
|
+
erroredInputs.push(...formValidation.erroredInputs);
|
|
1389
|
+
}
|
|
1225
1390
|
const visibleErroredInputs = erroredInputs.filter(item => !item.hasAttribute('data-no-error-scroll'));
|
|
1226
1391
|
if (!ok && this.config.scrollToFirstErroredInput) {
|
|
1227
1392
|
this.scrollToFirstErroredInput(visibleErroredInputs);
|
|
@@ -1716,5 +1881,5 @@ class Form {
|
|
|
1716
1881
|
}
|
|
1717
1882
|
}
|
|
1718
1883
|
|
|
1719
|
-
export { blockScrollBody, closest, createFieldValidator, createSchemaValidator, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerSchemaValidator, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
|
|
1884
|
+
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 };
|
|
1720
1885
|
//# 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
|
@@ -8,6 +8,7 @@ export type ValidationRule = string | {
|
|
|
8
8
|
export type ValidationTrigger = 'submit' | 'input' | 'blur' | 'change';
|
|
9
9
|
export type ValidationErrorSource = 'client' | 'server' | 'manual';
|
|
10
10
|
export type FieldReference = string | FormFieldElement;
|
|
11
|
+
export declare const FORM_ERROR_FIELD = "_form";
|
|
11
12
|
export interface ValidationError {
|
|
12
13
|
field: string;
|
|
13
14
|
rule: string;
|
|
@@ -23,11 +24,32 @@ export interface SubmitResult {
|
|
|
23
24
|
export interface FormResetOptions {
|
|
24
25
|
clearErrors?: boolean;
|
|
25
26
|
}
|
|
27
|
+
export interface ValidationIssue {
|
|
28
|
+
field?: string;
|
|
29
|
+
rule?: string;
|
|
30
|
+
message: string;
|
|
31
|
+
source?: ValidationErrorSource;
|
|
32
|
+
}
|
|
33
|
+
export type ValidationIssueInput = string | ValidationIssue | ErrorResponse | Record<string, string> | Array<string | ValidationIssue | ErrorResponse>;
|
|
34
|
+
export interface FormValidatorContext {
|
|
35
|
+
form: Form;
|
|
36
|
+
formElement: HTMLFormElement;
|
|
37
|
+
values: Record<string, any>;
|
|
38
|
+
errors: ValidationError[];
|
|
39
|
+
}
|
|
40
|
+
export type FormValidatorResult = boolean | string | void | ValidationIssue | Array<string | ValidationIssue>;
|
|
41
|
+
export type FormValidator = (context: FormValidatorContext) => FormValidatorResult | Promise<FormValidatorResult>;
|
|
42
|
+
export type FormValidatorPredicate = (context: FormValidatorContext) => boolean | Promise<boolean>;
|
|
43
|
+
export type FormValidatorIssueFactory = string | ValidationIssue | ((context: FormValidatorContext) => string | ValidationIssue | Promise<string | ValidationIssue>);
|
|
26
44
|
export type ValidationSchema = Record<string, {
|
|
27
45
|
rules: ValidationRule[];
|
|
28
46
|
selector?: string;
|
|
29
47
|
messages?: Record<string, string>;
|
|
30
48
|
}>;
|
|
49
|
+
export declare function createFormValidator(predicate: FormValidatorPredicate, issue: FormValidatorIssueFactory): FormValidator;
|
|
50
|
+
export declare function sameAsField(field: string, otherField: string, message?: string, rule?: string): FormValidator;
|
|
51
|
+
export declare function requiredIf(field: string, condition: FormValidatorPredicate, message?: string, rule?: string): FormValidator;
|
|
52
|
+
export declare function dateOrder(startField: string, endField: string, message?: string, rule?: string): FormValidator;
|
|
31
53
|
export type FormFieldElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
32
54
|
/** Параметры формы. */
|
|
33
55
|
export interface FormOptions {
|
|
@@ -130,6 +152,8 @@ export interface FormOptions {
|
|
|
130
152
|
wrapData?: (data: Record<string, any>) => Record<string, any>;
|
|
131
153
|
/** Схема валидации: поле → массив правил + override-сообщения */
|
|
132
154
|
validationSchema?: ValidationSchema;
|
|
155
|
+
/** Валидаторы всей формы: cross-field, conditional required, date ranges и другие проверки над `getValues()`. */
|
|
156
|
+
formValidators?: FormValidator | FormValidator[];
|
|
133
157
|
}
|
|
134
158
|
export interface ErrorResponse {
|
|
135
159
|
name: string;
|
|
@@ -192,6 +216,12 @@ export default class Form {
|
|
|
192
216
|
setValues(values: Record<string, any>): this;
|
|
193
217
|
/** Снимает все клиентские и серверные ошибки с формы, не меняя значения полей. */
|
|
194
218
|
clearErrors(): this;
|
|
219
|
+
private isErrorResponse;
|
|
220
|
+
private normalizeValidationIssues;
|
|
221
|
+
private applyValidationIssue;
|
|
222
|
+
/** Показывает несколько ошибок формы или полей: из backend map/array, `ErrorResponse[]` или form validators. */
|
|
223
|
+
setErrors(issues: ValidationIssueInput, source?: ValidationErrorSource): this;
|
|
224
|
+
private validateFormValidators;
|
|
195
225
|
/** Сбрасывает форму через native `reset()` и опционально очищает ошибки. */
|
|
196
226
|
reset(options?: FormResetOptions): this;
|
|
197
227
|
/**
|
|
@@ -242,6 +272,7 @@ export default class Form {
|
|
|
242
272
|
private getFieldKey;
|
|
243
273
|
private hasFieldError;
|
|
244
274
|
private clearFieldErrorRecord;
|
|
275
|
+
private setErrorRecord;
|
|
245
276
|
private getValidationStateAttribute;
|
|
246
277
|
private setFieldValidationState;
|
|
247
278
|
private startFieldValidation;
|
|
@@ -263,7 +294,7 @@ export default class Form {
|
|
|
263
294
|
private runFieldValidation;
|
|
264
295
|
private getRadioGroup;
|
|
265
296
|
/** Показывает ошибку конкретного поля по имени или DOM-элементу. */
|
|
266
|
-
showFieldError(field: FieldReference, message: string, source?: ValidationErrorSource): boolean;
|
|
297
|
+
showFieldError(field: FieldReference, message: string, source?: ValidationErrorSource, rule?: string): boolean;
|
|
267
298
|
/** Проверяет одно поле по имени или DOM-элементу. */
|
|
268
299
|
validateField(field: FieldReference): Promise<boolean>;
|
|
269
300
|
/**
|
package/docs/en/README.md
CHANGED
|
@@ -50,12 +50,16 @@ const forms = Form.initAll('form[data-form-father]', {
|
|
|
50
50
|
|
|
51
51
|
```ts
|
|
52
52
|
import Form, {
|
|
53
|
+
FORM_ERROR_FIELD,
|
|
53
54
|
type FormOptions,
|
|
54
55
|
type ValidationSchema,
|
|
55
56
|
type ResponseBody,
|
|
56
57
|
type ValidationError,
|
|
57
58
|
type SubmitResult,
|
|
58
59
|
type FormResetOptions,
|
|
60
|
+
type FormValidator,
|
|
61
|
+
type ValidationIssue,
|
|
62
|
+
type FormValidatorPredicate,
|
|
59
63
|
} from 'form-father';
|
|
60
64
|
```
|
|
61
65
|
|
|
@@ -82,6 +86,7 @@ import Form, {
|
|
|
82
86
|
- **validationStateAttribute**: Field state attribute: `validating`, `valid`, or `invalid`. Defaults to
|
|
83
87
|
`data-form-father-state`.
|
|
84
88
|
- **observeMutations**: Watches dynamically added fields and submit buttons.
|
|
89
|
+
- **formValidators**: A form validator or array of validators for cross-field rules.
|
|
85
90
|
|
|
86
91
|
## Form Submission
|
|
87
92
|
|
|
@@ -101,6 +106,7 @@ import Form, {
|
|
|
101
106
|
- **validate()**: Validates the whole form.
|
|
102
107
|
- **validateField(field)**: Validates one field by name or DOM element.
|
|
103
108
|
- **showFieldError(field, message, source)**: Shows a field error manually.
|
|
109
|
+
- **setErrors(errors, source)**: Applies backend/form-level errors from an object, array, or `ErrorResponse[]`.
|
|
104
110
|
- **getErrors()**: Returns current errors as `{ field, rule, message, source }`.
|
|
105
111
|
- **getValues()**: Returns form values as a plain object.
|
|
106
112
|
- **setValues(values)**: Fills fields by `name` and dispatches `input`/`change`.
|
|
@@ -137,6 +143,40 @@ Final rule order:
|
|
|
137
143
|
required → schema.rules → data-validate → data-custom-validate
|
|
138
144
|
```
|
|
139
145
|
|
|
146
|
+
### Cross-field and form-level validation
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { dateOrder, requiredIf, sameAsField } from 'form-father';
|
|
150
|
+
|
|
151
|
+
const form = new Form($form, {
|
|
152
|
+
formValidators: [
|
|
153
|
+
sameAsField('passwordConfirm', 'password', 'Passwords do not match'),
|
|
154
|
+
requiredIf('email', ({ values }) => Boolean(values.subscribe), 'Email is required for subscription'),
|
|
155
|
+
dateOrder('start', 'end', 'End date must be after start'),
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Return a string or an issue without `field` to show a form-level error. In `getErrors()`, it uses
|
|
161
|
+
`FORM_ERROR_FIELD` (`"_form"`).
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
form.setErrors({
|
|
165
|
+
email: 'Email is already used',
|
|
166
|
+
[FORM_ERROR_FIELD]: 'Please check the form',
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Rule DX helpers
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { createLengthValidator, createPatternValidator, registerFieldValidator } from 'form-father';
|
|
174
|
+
|
|
175
|
+
registerFieldValidator('starts-with-a', value => value.startsWith('A'), 'Must start with A');
|
|
176
|
+
registerValidator('slug', createPatternValidator(/^[a-z0-9-]+$/), 'Slug characters only');
|
|
177
|
+
registerValidator('short-name', createLengthValidator({ min: 2, max: 24 }), 'Use 2-24 characters');
|
|
178
|
+
```
|
|
179
|
+
|
|
140
180
|
## Schema Adapters
|
|
141
181
|
|
|
142
182
|
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
|
@@ -49,6 +49,41 @@ form.clearErrors();
|
|
|
49
49
|
form.reset();
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
## Cross-field validation
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { dateOrder, requiredIf, sameAsField } from 'form-father';
|
|
56
|
+
|
|
57
|
+
const form = new Form(document.querySelector('form')!, {
|
|
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
|
+
],
|
|
63
|
+
});
|
|
64
|
+
```
|
|
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
|
+
|
|
76
|
+
## Applying backend errors
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { FORM_ERROR_FIELD } from 'form-father';
|
|
80
|
+
|
|
81
|
+
form.setErrors({
|
|
82
|
+
email: 'Email is already registered',
|
|
83
|
+
[FORM_ERROR_FIELD]: 'Please check the form',
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
52
87
|
## Async validation state
|
|
53
88
|
|
|
54
89
|
```css
|
package/package.json
CHANGED