form-father 0.3.0 → 0.4.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 +19 -0
- package/README.md +33 -0
- package/demos/index.html +12 -0
- package/demos/main.js +11 -0
- package/dist/FormFather.min.js +1 -1
- package/dist/index.d.ts +27 -2
- package/dist/index.js +111 -9
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +26 -1
- package/docs/en/README.md +33 -0
- package/docs/recipes/README.md +26 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.4.0 - 2026-04-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added form-level and cross-field validation via `formValidators`.
|
|
10
|
+
- Added `setErrors()` for backend maps, `ErrorResponse[]`, global form errors, and custom validation issue objects.
|
|
11
|
+
- Added `FORM_ERROR_FIELD` constant for form-level errors in `getErrors()`.
|
|
12
|
+
- Added public TypeScript types for form validation contexts and issues.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- `validate()` now runs field-level rules first, then form-level validators over `getValues()`.
|
|
17
|
+
- Cross-field and backend errors share the same rendering path as field validation errors.
|
|
18
|
+
|
|
19
|
+
### Tests
|
|
20
|
+
|
|
21
|
+
- Test suite expanded to 113 tests.
|
|
22
|
+
- Coverage baseline: 98.31% statements, 87.05% branches, 96.34% functions, 100% lines.
|
|
23
|
+
|
|
5
24
|
## 0.3.0 - 2026-04-25
|
|
6
25
|
|
|
7
26
|
### Added
|
package/README.md
CHANGED
|
@@ -69,12 +69,15 @@ 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,
|
|
78
81
|
} from 'form-father';
|
|
79
82
|
```
|
|
80
83
|
|
|
@@ -107,6 +110,7 @@ import Form, {
|
|
|
107
110
|
- **validationStateAttribute**: Атрибут состояния поля: `validating`, `valid`, `invalid`. По умолчанию
|
|
108
111
|
`data-form-father-state`.
|
|
109
112
|
- **observeMutations**: Следить за динамически добавленными полями и submit-кнопками.
|
|
113
|
+
- **formValidators**: Валидатор или массив валидаторов всей формы для cross-field правил.
|
|
110
114
|
|
|
111
115
|
## Отправка формы
|
|
112
116
|
|
|
@@ -126,6 +130,7 @@ import Form, {
|
|
|
126
130
|
- **validate()**: Проверяет всю форму.
|
|
127
131
|
- **validateField(field)**: Проверяет одно поле по имени или DOM-элементу.
|
|
128
132
|
- **showFieldError(field, message, source)**: Показывает ошибку поля вручную.
|
|
133
|
+
- **setErrors(errors, source)**: Применяет backend/form-level ошибки из объекта, массива или `ErrorResponse[]`.
|
|
129
134
|
- **getErrors()**: Возвращает текущие ошибки в формате `{ field, rule, message, source }`.
|
|
130
135
|
- **getValues()**: Возвращает значения формы обычным объектом.
|
|
131
136
|
- **setValues(values)**: Заполняет поля по `name` и диспатчит `input`/`change`.
|
|
@@ -321,6 +326,34 @@ required → schema.rules → data-validate → data-custom-validate
|
|
|
321
326
|
- Для каждого поля отображается **только первое** найденное сообщение об ошибке.
|
|
322
327
|
- Неизвестные правила логируются и пропускаются до начала проверки, поэтому не влияют на результат валидации.
|
|
323
328
|
|
|
329
|
+
### Cross-field и form-level validation
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
const form = new Form($form, {
|
|
333
|
+
formValidators: [
|
|
334
|
+
({ values }) =>
|
|
335
|
+
values.password === values.passwordConfirm
|
|
336
|
+
? true
|
|
337
|
+
: {
|
|
338
|
+
field: 'passwordConfirm',
|
|
339
|
+
rule: 'same-as-password',
|
|
340
|
+
message: 'Пароли не совпадают',
|
|
341
|
+
},
|
|
342
|
+
({ values }) => (values.start <= values.end ? true : { field: 'end', message: 'Дата окончания раньше начала' }),
|
|
343
|
+
],
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Глобальную ошибку формы можно вернуть строкой или issue без `field`. В `getErrors()` она будет иметь поле
|
|
348
|
+
`FORM_ERROR_FIELD` (`"_form"`).
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
form.setErrors({
|
|
352
|
+
email: 'Email уже занят',
|
|
353
|
+
[FORM_ERROR_FIELD]: 'Проверьте данные формы',
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
324
357
|
## Адаптеры схем
|
|
325
358
|
|
|
326
359
|
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
|
@@ -78,6 +78,17 @@ Form.initAll('form[data-form-father]', {
|
|
|
78
78
|
validationDebounce: 120,
|
|
79
79
|
focusFirstErroredInput: true,
|
|
80
80
|
loaderColor: 'currentColor',
|
|
81
|
+
formValidators({ values }) {
|
|
82
|
+
if (values.password && values.passwordConfirm && values.password !== values.passwordConfirm) {
|
|
83
|
+
return {
|
|
84
|
+
field: 'passwordConfirm',
|
|
85
|
+
rule: 'same-as-password',
|
|
86
|
+
message: 'Пароли не совпадают',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
},
|
|
81
92
|
onValidationError(errors) {
|
|
82
93
|
writeOutput('Client validation errors', errors);
|
|
83
94
|
},
|
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,s){function o(t){try{l(i.next(t))}catch(t){s(t)}}function a(t){try{l(i.throw(t))}catch(t){s(t)}}function l(t){var e;t.done?n(t.value):(e=t.value,e instanceof r?e:new r((function(t){t(e)}))).then(o,a)}l((i=i.apply(t,e||[])).next())}))}function r(t,e){if(null==t)return null;let r=t;for(;r;){if(r.matches(e))return r;r=r.parentElement}return null}"function"==typeof SuppressedError&&SuppressedError;const i=["http:","https:"];function n(t){return t.length>0&&t.length<=63&&/^[a-z\d-]+$/i.test(t)&&!t.startsWith("-")&&!t.endsWith("-")}function s(t){return!!t&&("localhost"===t||function(t){const e=t.split(".");return 4===e.length&&e.every((t=>{if(!/^\d{1,3}$/.test(t))return!1;const e=Number(t);return e>=0&&e<=255}))}(t)||function(t){if(!t.startsWith("[")||!t.endsWith("]"))return!1;const e=t.slice(1,-1);return e.includes(":")&&/^[\da-f:.]+$/i.test(e)}(t)||function(t){const e=t.split(".");return e.length>=2&&e.every(n)}(t))}function o(t){const e=function(t){const e=t.trim();return 0===e.length?"":e.startsWith("//")?`https:${e}`:/^[a-z][a-z\d+.-]*:\/\//i.test(e)?e:`https://${e}`}(t);if(0===e.length)return!1;try{const t=new URL(e);return i.includes(t.protocol)&&s(t.hostname)}catch(t){return!1}}function a(t){const e="FORM"===t.tagName?Array.from(t.elements):Array.from(t.querySelectorAll("input, select, textarea, button")),r=new FormData;return e.forEach((t=>{if(!(t instanceof HTMLInputElement||t instanceof HTMLSelectElement||t instanceof HTMLTextAreaElement||t instanceof HTMLButtonElement))return;if(!t.name||t.disabled||t.hasAttribute("data-no-serialize"))return;if(t instanceof HTMLButtonElement)return;const{name:e,type:i}=t;if("file"===i)if(t.multiple){const i=t.files;if(i)for(let t=0;t<i.length;t++)r.append(`${e}[${t}]`,i[t])}else{const i=t.files;i&&i[0]&&r.append(e,i[0])}else{let n;if("select-one"===i||"select-multiple"===i){const s=t;if("select-multiple"===i){Array.from(s.selectedOptions).map((t=>t.value)).forEach(((t,i)=>{r.append(`${e}[${i}]`,t)}))}else n=s.value,n&&r.append(e,n)}else if("tel"===i)n=t.value.split(/[\s()-]/).join("").replace(/^8/,"+7");else if("checkbox"===i||"radio"===i){if(!t.checked)return;n=t.value}else if("date"===i){const e=t.getAttribute("data-date-format");n=e?function(t,e){const r=new Date(t),i=t=>t.toString().padStart(2,"0"),n=i(r.getDate()),s=i(r.getMonth()+1),o=r.getFullYear().toString();return e.replace("dd",n).replace("mm",s).replace("yyyy",o)}(t.value,e):t.value}else n=t.value;n&&"select-one"!==i&&"select-multiple"!==i&&r.append(e,n)}})),r}function l(t){if(Object.prototype.hasOwnProperty.call(t,"redirect-url"))if(t["redirect-url-delay"]){const e=Number(t["redirect-url-delay"]);setTimeout((()=>{window.location.href=t["redirect-url"]}),e)}else window.location.href=t["redirect-url"];else if(Object.prototype.hasOwnProperty.call(t,"reload")){if(!0===t.reload)if(t["reload-delay"]){const e=Number(t["reload-delay"]);setTimeout((()=>{window.location.reload()}),e)}else window.location.reload()}else if(Object.prototype.hasOwnProperty.call(t,"toast")){const e=t.toast;(Array.isArray(e)?e:[e]).forEach((t=>{window.showToast(t)}))}else Object.prototype.hasOwnProperty.call(t,"error-toast")&&window.showToast(t["error-toast"])}function d(t){const e={};return new FormData(t).forEach(((t,r)=>{e.hasOwnProperty(r)?(Array.isArray(e[r])||(e[r]=[e[r]]),e[r].push(t)):e[r]=t})),e}var c;const u=Symbol.for("FormFather.validators"),h=null!==(c=globalThis[u])&&void 0!==c?c:globalThis[u]=new Map;function p(t,e,r,{override:i=!1}={}){!h.has(t)||i?h.set(t,{fn:e,defaultMessage:r}):console.warn(`[FormFather] Validator "${t}" already exists; pass { override: true } to replace`)}const f=t=>h.get(t);function m(t,e,r){h.has(t)||h.set(t,{fn:e,defaultMessage:r})}function g(t,e){return"function"==typeof e?e(t):e||function(t){var e;if(!t||"object"!=typeof t)return;const r=t.issues||t.errors;if(Array.isArray(r)){const t=null===(e=r.find((t=>"string"==typeof(null==t?void 0:t.message))))||void 0===e?void 0:e.message;if(t)return t}return"string"==typeof t.message?t.message:void 0}(t)}function y(t,r={}){return(i,n,s,o)=>e(this,void 0,void 0,(function*(){const e=r.mapValue?r.mapValue(i,n,s,o):i;try{if("function"==typeof t)return a=yield t(e),l=g(void 0,r.message),"object"==typeof a&&null!==a?a:void 0===a||!!a||{valid:!1,message:l};if("function"==typeof t.safeParse){const i=yield t.safeParse(e);return!!i.success||{valid:!1,message:g(i.error||i,r.message)}}if("function"==typeof t.parse)return yield t.parse(e),!0}catch(t){return{valid:!1,message:g(t,r.message)}}var a,l;return{valid:!1,message:g(void 0,r.message)}}))}var b;m("required",((t,e,r)=>{if(e&&e.type){if("checkbox"===e.type)return e.checked;if("radio"===e.type){const t=r.querySelectorAll(`input[type="radio"][name="${e.name}"]`);return Array.from(t).some((t=>t.checked))}}return t.trim().length>0}),"Пустое значение"),m("email",(t=>/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)),"Неверный формат"),m("tel",(t=>/^\+7\d{10}$/.test(t)),"Неверный формат"),m("url",(t=>o(t)),"Неверный формат"),m("not-numbers",(t=>!/[0-9]/.test(t)),"Неверный формат");const v=Symbol.for("formfather.shared"),E=null!==(b=globalThis[v])&&void 0!==b?b:globalThis[v]={defaultParams:{},defaultValidationSchema:void 0},w="_form";let S=0;const F=new WeakMap,A={tel:{selector:'input[type="tel"]',rules:["tel"]},email:{selector:'input[type="email"]',rules:["email"]},required:{selector:"[required]",rules:["required"]},url:{selector:'input[type="url"]',rules:["url"]},"not-numbers":{selector:'input[data-validate="not-numbers"]',rules:["not-numbers"]}};class V{static get defaultParams(){return E.defaultParams}static set defaultParams(t){E.defaultParams=t}static get defaultValidationSchema(){var t;return null!==(t=E.defaultValidationSchema)&&void 0!==t?t:E.defaultValidationSchema=A}static set defaultValidationSchema(t){E.defaultValidationSchema=t}constructor(t,e={}){this.$submits=[],this.waitResponse=!1,this.inputs=null,this.fieldEventHandlers=[],this.liveValidationTimers=new WeakMap,this.fieldValidationTokens=new WeakMap,this.fieldValidationTokenCounter=0,this.errors=[],this._anim={duration:220,easing:"ease",paddingTopOpened:16,iconDuration:700},this.$el=t,this.options=e;this.config=Object.assign({},{onSubmit:()=>{},onResponse:()=>{},onResponseSuccess:()=>{},onResponseUnsuccess:()=>{},customTypeError:null,loaderColor:"#fff",showLoaderButton:!0,scrollToFirstErroredInput:!0,focusFirstErroredInput:!1,logging:!1,inputSelector:".input",inputWrapperSelector:".input-primary",validateOn:"submit",revalidateOn:["input","change"],validationDebounce:0,errorContainerAttribute:"data-error-container",validationStateAttribute:"data-form-father-state",observeMutations:!1},V.defaultParams,e),this.$el?(this.$submits=this.findSubmitElements(),this.isCorrectArguments()&&(this.bindSubmitHandlers(),this.initialization(),this.bindFieldValidationEvents(),this.setupMutationObserver(),F.set(this.$el,this))):console.warn("Empty $el")}getOptions(){return this.options}static initAll(t="form[data-form-father]",e={}){return"undefined"==typeof document?[]:Array.from(document.querySelectorAll(t)).filter((t=>"FORM"===t.tagName)).map((t=>{const r=F.get(t);return r?(r.updateOptions(e),r):new V(t,e)}))}updateOptions(t){return this.options=Object.assign(Object.assign({},this.options),t),this.config=Object.assign(Object.assign({},this.config),t),this.refreshControls(),this.bindSubmitHandlers(),this.bindFieldValidationEvents(),this.setupMutationObserver(),this}getErrors(){return this.errors.map((t=>Object.assign({},t)))}getValues(){return d(this.$el)}normalizeValueList(t){return Array.isArray(t)?t.map(String):[String(t)]}setValues(t){return this.getAllFields().forEach((e=>{if(!e.name||!Object.prototype.hasOwnProperty.call(t,e.name))return;const r=t[e.name];if(!(e instanceof HTMLInputElement&&"file"===e.type)){if(e instanceof HTMLInputElement&&"checkbox"===e.type)e.checked="boolean"==typeof r?r:this.normalizeValueList(r).includes(e.value);else if(e instanceof HTMLInputElement&&"radio"===e.type)e.checked=String(r)===e.value;else if(e instanceof HTMLSelectElement&&e.multiple){const t=this.normalizeValueList(r);Array.from(e.options).forEach((e=>{e.selected=t.includes(e.value)}))}else e.value=null==r?"":String(r);e.dispatchEvent(new Event("input",{bubbles:!0})),e.dispatchEvent(new Event("change",{bubbles:!0}))}})),this}clearErrors(){return this.errors=[],this.getAllFields().forEach((t=>{this.hideError(t),this.clearFieldValidationState(t)})),this.hideErrorForm(),this}isErrorResponse(t){return!!t&&"object"==typeof t&&"string"==typeof t.name&&"string"==typeof t["error-msg"]}normalizeValidationIssues(t,e){return null==t||!0===t?[]:!1===t?[{message:"Некорректные данные формы",source:e}]:"string"==typeof t?[{message:t,source:e}]:Array.isArray(t)?t.flatMap((t=>this.normalizeValidationIssues(t,e))):this.isErrorResponse(t)?[{field:t.name,rule:e,message:t["error-msg"],source:e}]:"object"==typeof t&&"message"in t&&"string"==typeof t.message?[Object.assign(Object.assign({},t),{source:t.source||e})]:"object"==typeof t?Object.entries(t).map((([t,r])=>({field:t,rule:e,message:r,source:e}))):[]}applyValidationIssue(t,e){const r=t.source||e,i=t.rule||r,n=t.field||w;if(n===w)return this.setErrorRecord(w,i,t.message,r),this.showErrorForm(t.message),null;const s=this.findFieldByName(n);return s?(this.showFieldError(s,t.message,r,i),s):(this.setErrorRecord(n,i,t.message,r),console.warn(`Не найдено поле с именем: ${n}, для вывода ошибки: ${t.message}`),null)}setErrors(t,e="server"){return this.normalizeValidationIssues(t,e).forEach((t=>{this.applyValidationIssue(t,e)})),this}validateFormValidators(){return e(this,void 0,void 0,(function*(){const t=this.config.formValidators?Array.isArray(this.config.formValidators)?this.config.formValidators:[this.config.formValidators]:[],e=[];let r=!0;for(const i of t){const t=yield i({form:this,formElement:this.$el,values:this.getValues(),errors:this.getErrors()}),n=this.normalizeValidationIssues(t,"client");0!==n.length&&(r=!1,n.forEach((t=>{const r=this.applyValidationIssue(t,"client");r&&e.push(r)})))}return{ok:r,erroredInputs:e}}))}reset(t={}){return this.$el.reset(),this.refreshControls(),!1!==t.clearErrors&&this.clearErrors(),this}static setDefaultParams(t){this.defaultParams=Object.assign(Object.assign({},this.defaultParams),t)}refreshControls(){this.inputs=this.$el.querySelectorAll(this.config.inputSelector)}findSubmitElements(){return Array.from(this.$el.querySelectorAll('input[type="submit"], input[type="image"], button[type="submit"], button:not([type])'))}bindSubmitHandlers(){this._onSubmitClickHandler||(this._onSubmitClickHandler=t=>e(this,void 0,void 0,(function*(){yield this.handleSubmitEvent(t)}))),this.$submits.forEach((t=>{t.removeEventListener("click",this._onSubmitClickHandler)})),this.$submits=this.findSubmitElements(),this.$submits.forEach((t=>{t.addEventListener("click",this._onSubmitClickHandler)}))}initialization(){this.refreshControls(),this._onSubmitHandler=t=>e(this,void 0,void 0,(function*(){yield this.handleSubmitEvent(t)})),this.$el.addEventListener("submit",this._onSubmitHandler)}handleSubmitEvent(t){return e(this,void 0,void 0,(function*(){var e,r,i,n,s,o;t.preventDefault(),null===(r=(e=this.config).onBeforeValidate)||void 0===r||r.call(e,this);const a=yield this.validate();return null===(n=(i=this.config).onAfterValidate)||void 0===n||n.call(i,a,this),a?this.submit():(null===(o=(s=this.config).onValidationError)||void 0===o||o.call(s,this.getErrors(),this),{success:!1,error:this.getErrors()})}))}normalizeTriggers(t,e){const r=null==t?e:Array.isArray(t)?t:[t];return Array.from(new Set(r))}clearFieldValidationEvents(){this.fieldEventHandlers.forEach((({$input:t,event:e,handler:r})=>{t.removeEventListener(e,r)})),this.fieldEventHandlers=[]}bindFieldValidationEvents(){this.clearFieldValidationEvents();const t=this.normalizeTriggers(this.config.validateOn,["submit"]).filter((t=>"submit"!==t)),e=this.normalizeTriggers(this.config.revalidateOn,["input","change"]).filter((t=>"submit"!==t)),r=Array.from(new Set([...t,...e]));if(0===r.length)return;this.getAllFields().forEach((e=>{r.forEach((r=>{const i=()=>{(t.includes(r)||this.hasFieldError(e))&&this.scheduleFieldValidation(e)};e.addEventListener(r,i),this.fieldEventHandlers.push({$input:e,event:r,handler:i})}))}))}scheduleFieldValidation(t){const e=this.liveValidationTimers.get(t);e&&clearTimeout(e);const r=Math.max(0,Number(this.config.validationDebounce)||0),i=()=>{this.validateField(t)};if(0===r)return void i();const n=setTimeout(i,r);this.liveValidationTimers.set(t,n)}setupMutationObserver(){var t;null===(t=this.mutationObserver)||void 0===t||t.disconnect(),this.mutationObserver=void 0,this.config.observeMutations&&"undefined"!=typeof MutationObserver&&(this.mutationObserver=new MutationObserver((()=>{this.refreshControls(),this.bindSubmitHandlers(),this.bindFieldValidationEvents()})),this.mutationObserver.observe(this.$el,{childList:!0,subtree:!0}))}getErrorContainer(t){const e=this.config.errorContainerAttribute||"data-error-container",r=t.getAttribute(e);if(!r)return null;try{return document.querySelector(r)}catch(t){return console.warn(`[FormFather] Invalid error container selector "${r}"`),null}}showError(t,e){t.setAttribute("aria-invalid","true");const i=this.getErrorContainer(t);if(i)return i.textContent=e,i.hasAttribute("role")||i.setAttribute("role","alert"),void i.removeAttribute("hidden");const n=r(t,this.config.inputWrapperSelector);if(!n)return void console.warn(`Не найдена обёртка для поля с именем: ${t.name||t.type}`);const s=n.showError;if("function"==typeof s)return void s.call(n,e);n.classList.add("input__wrapper--error");let o=n.querySelector("[data-form-father-error]");o||(o=document.createElement("div"),o.setAttribute("data-form-father-error",""),o.setAttribute("role","alert"),t.insertAdjacentElement("afterend",o)),o.textContent=e}showLoader(){this.$submits.forEach((t=>{if(t.classList.add("button--loading"),t.querySelector(".button__loader"))return;const e="form-father-loader-"+S++,r=`\n\t\t\t\t<svg height="38" viewBox="0 0 38 38" width="38" xmlns="http://www.w3.org/2000/svg">\n\t\t\t\t\t<defs>\n\t\t\t\t\t<linearGradient id="${e}" x1="8.042%" x2="65.682%" y1="0%" y2="23.865%">\n\t\t\t\t\t\t<stop offset="0%" stop-color="${this.config.loaderColor}" stop-opacity="0"/>\n\t\t\t\t\t\t<stop offset="63.146%" stop-color="${this.config.loaderColor}" stop-opacity=".631"/>\n\t\t\t\t\t\t<stop offset="100%" stop-color="${this.config.loaderColor}"/>\n\t\t\t\t\t</linearGradient>\n\t\t\t\t\t</defs>\n\t\t\t\t\t<g fill-rule="evenodd" fill="none">\n\t\t\t\t\t<g transform="translate(1 1)">\n\t\t\t\t\t\t<path d="M36 18c0-9.94-8.06-18-18-18" id="Oval-2" stroke-width="5" stroke="url(#${e})"></path>\n\t\t\t\t\t\t<circle cx="36" cy="18" fill="${this.config.loaderColor}" r="1"></circle>\n\t\t\t\t\t</g>\n\t\t\t\t\t</g>\n\t\t\t\t</svg>\n\t\t\t`,i=document.createElement("span");i.innerHTML=r,i.className="button__loader",t.insertAdjacentElement("afterbegin",i),requestAnimationFrame((()=>{i.setAttribute("data-showed","")}))}))}hideLoader(){this.$submits.forEach((t=>{t.classList.remove("button--loading");const e=t.querySelector(".button__loader");e&&(e.classList.remove("active"),e.removeAttribute("data-showed"),setTimeout((()=>{e.remove()}),250))}))}hideError(t){var e;const i=r(t,this.config.inputWrapperSelector)||t.parentElement,n=this.getErrorContainer(t);t.removeAttribute("aria-invalid"),n&&(n.textContent="",n.setAttribute("hidden","")),null===(e=null==i?void 0:i.querySelector("[data-form-father-error]"))||void 0===e||e.remove(),null==i||i.classList.remove("input__wrapper--error"),i&&i.classList.contains("input__wrapper")&&i.getAttribute("data-type-error")}scrollToFirstErroredInput(t){if(t.length>0){const e=Array.from(this.$el.querySelectorAll("input, textarea, select"));let r=null,i=1/0;if(t.forEach((t=>{const n=e.findIndex((e=>e===t));n<i&&(i=n,r=t)})),r){const t=r.getBoundingClientRect(),e=window.pageYOffset||document.documentElement.scrollTop,i=t.top+e-window.innerHeight/2;document.documentElement.setAttribute("data-now-scrolling",""),window.scrollTo({top:i,behavior:"smooth"}),setTimeout((()=>{document.documentElement.removeAttribute("data-now-scrolling")}),800)}}}focusFirstErroredInput(t){var e;if(0===t.length)return;const r=Array.from(this.$el.querySelectorAll("input, textarea, select")),i=t.slice().sort(((t,e)=>r.indexOf(t)-r.indexOf(e)))[0];null===(e=null==i?void 0:i.focus)||void 0===e||e.call(i,{preventScroll:!0})}getValidationSchema(){return Object.assign(Object.assign({},V.defaultValidationSchema||{}),this.config.validationSchema||{})}getAllFields(t=this.$el){return Array.from(t.querySelectorAll("input, textarea, select"))}getFieldKey(t){return t.name||t.id||t.getAttribute("data-field")||t.type||"field"}hasFieldError(t){const e=this.getFieldKey(t);return"true"===t.getAttribute("aria-invalid")||this.errors.some((t=>t.field===e))}clearFieldErrorRecord(t){const e=this.getFieldKey(t);this.errors=this.errors.filter((t=>t.field!==e))}setErrorRecord(t,e,r,i){this.errors.push({field:t,rule:e,message:r,source:i})}getValidationStateAttribute(){return this.config.validationStateAttribute||"data-form-father-state"}setFieldValidationState(t,e){const r=this.getValidationStateAttribute();e?t.setAttribute(r,e):t.removeAttribute(r)}startFieldValidation(t){const e=++this.fieldValidationTokenCounter;return this.fieldValidationTokens.set(t,e),t.setAttribute("aria-busy","true"),this.setFieldValidationState(t,"validating"),e}isLatestFieldValidation(t,e){return null==e||this.fieldValidationTokens.get(t)===e}finishFieldValidation(t,e,r){this.isLatestFieldValidation(t,e)&&(t.removeAttribute("aria-busy"),this.setFieldValidationState(t,r?"valid":"invalid"))}clearFieldValidationState(t){this.fieldValidationTokens.delete(t),t.removeAttribute("aria-busy"),this.setFieldValidationState(t,null)}setValidationError(t,e,r,i){this.clearFieldErrorRecord(t),this.setErrorRecord(this.getFieldKey(t),e,r,i)}resolveField(t){return"string"!=typeof t?t:this.findFieldByName(t)}parseRuleString(t){const[e,...r]=t.split(":").map((t=>t.trim()));return r.length>0?{rule:e,params:1===r.length?r[0]:r}:e}parseValidationAttribute(t){return(null!=t?t:"").split(/[,\s|]+/).map((t=>t.trim())).filter(Boolean).map((t=>this.parseRuleString(t)))}getRuleName(t){return"string"==typeof t?t:t.rule}getRuleIdentity(t){return JSON.stringify(t)}collectRules(t,e=[],r=new Set){const i=[];t.hasAttribute("required")&&i.push("required"),i.push(...e);const n=this.parseValidationAttribute(t.getAttribute("data-validate")).filter((t=>{const e=this.getRuleName(t);return f(e)||!r.has(e)}));i.push(...n),i.push(...this.parseValidationAttribute(t.getAttribute("data-custom-validate")));const s=[],o=new Set;for(const t of i){const e=this.getRuleName(t);if(!f(e)){console.warn(`[FormFather] Unknown validation rule "${e}"`);continue}const r=this.getRuleIdentity(t);o.has(r)||(o.add(r),s.push(t))}return s}getRuleMessage(t,e,r,i,n){const s=t.getAttribute(`data-error-${e}`);return n||s||r[e]||i}fieldMatchesSchemaDefinition(t,e,r){if(r.selector)try{return t.matches(r.selector)}catch(t){return console.warn(`[FormFather] Invalid validation selector "${r.selector}"`),!1}return this.parseValidationAttribute(t.getAttribute("data-validate")).some((t=>this.getRuleName(t)===e))}getFieldValidationConfig(t,e=this.getValidationSchema()){const r=[],i={};for(const[n,s]of Object.entries(e))this.fieldMatchesSchemaDefinition(t,n,s)&&(r.push(...s.rules),Object.assign(i,s.messages||{}));return{rules:this.collectRules(t,r,new Set(Object.keys(e))),messages:i}}getValidationFields(t,e){const r=new Set,i=t=>{Array.from(t).forEach((t=>{(t instanceof HTMLInputElement||t instanceof HTMLTextAreaElement||t instanceof HTMLSelectElement)&&r.add(t)}))};for(const[n,s]of Object.entries(e))if(s.selector)try{i(t.querySelectorAll(s.selector))}catch(t){console.warn(`[FormFather] Invalid validation selector "${s.selector}"`)}else this.getAllFields(t).filter((t=>this.parseValidationAttribute(t.getAttribute("data-validate")).some((t=>this.getRuleName(t)===n)))).forEach((t=>r.add(t)));return i(t.querySelectorAll("[required], [data-validate], [data-custom-validate]")),Array.from(r)}validateInput(t,r){return e(this,arguments,void 0,(function*(t,e,r={},i=this.$el,n){if(t.hasAttribute("data-no-validate"))return!0;const s=t instanceof HTMLInputElement&&("checkbox"===t.type||"radio"===t.type)?!t.checked:0===t.value.trim().length,o=e.some((t=>"required"===this.getRuleName(t)));if(s&&!o)return this.isLatestFieldValidation(t,n)?(this.clearFieldErrorRecord(t),this.hideError(t),!0):!this.hasFieldError(t);for(const s of e){const{rule:e,params:o}="string"==typeof s?{rule:s,params:void 0}:{rule:s.rule,params:s.params},a=f(e);if(!a)continue;const l=yield a.fn(t.value,t,i,o),d="object"==typeof l&&null!==l,c=d?!0===l.valid:!!l;if(!this.isLatestFieldValidation(t,n))return!this.hasFieldError(t);if(d&&l.effect&&(yield l.effect({value:t.value,$input:t,$form:i,params:o})),!this.isLatestFieldValidation(t,n))return!this.hasFieldError(t);if(!c){const i=l,n=this.getRuleMessage(t,e,r,a.defaultMessage,d&&"string"==typeof i.message?i.message:void 0);return this.setValidationError(t,e,n,"client"),this.showError(t,n),!1}if(d&&l.stopOthers)return!0}return this.isLatestFieldValidation(t,n)?(this.clearFieldErrorRecord(t),this.hideError(t),!0):!this.hasFieldError(t)}))}runFieldValidation(t,r){return e(this,arguments,void 0,(function*(t,e,r={},i=this.$el){const n=this.startFieldValidation(t);try{const s=yield this.validateInput(t,e,r,i,n);return this.finishFieldValidation(t,n,s),s}catch(e){throw this.isLatestFieldValidation(t,n)&&this.clearFieldValidationState(t),e}}))}getRadioGroup(t,e){return t instanceof HTMLInputElement&&"radio"===t.type&&t.name?Array.from(e.querySelectorAll('input[type="radio"]')).filter((e=>e.name===t.name)):[]}showFieldError(t,e,r="manual",i=r){const n=this.resolveField(t);return n?(this.setValidationError(n,i,e,r),this.showError(n,e),n.removeAttribute("aria-busy"),this.setFieldValidationState(n,"invalid"),!0):(console.warn(`[FormFather] Field "${String(t)}" not found`),!1)}validateField(t){return e(this,void 0,void 0,(function*(){const e=this.resolveField(t);if(!e)return console.warn(`[FormFather] Field "${String(t)}" not found`),!1;const{rules:r,messages:i}=this.getFieldValidationConfig(e);return this.runFieldValidation(e,r,i)}))}validate(){return e(this,arguments,void 0,(function*(t=this.$el){const e=this.getValidationSchema(),r=[],i=new WeakSet;let n=!0;this.errors=[];for(const s of this.getValidationFields(t,e)){if(i.has(s))continue;const o=this.getRadioGroup(s,t);o.length>0?o.forEach((t=>i.add(t))):i.add(s);const{rules:a,messages:l}=this.getFieldValidationConfig(s,e);(yield this.runFieldValidation(s,a,l,t))||(n=!1,r.push(s))}const s=yield this.validateFormValidators();s.ok||(n=!1,r.push(...s.erroredInputs));const o=r.filter((t=>!t.hasAttribute("data-no-error-scroll")));return!n&&this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(o),!n&&this.config.focusFirstErroredInput&&this.focusFirstErroredInput(o),n}))}disableSubmit(){this.$submits.forEach((t=>{t.setAttribute("disabled","")}))}enableSubmit(){this.$submits.forEach((t=>{t.removeAttribute("disabled")}))}clearInputs(){var t;null===(t=this.inputs)||void 0===t||t.forEach((t=>{var e;const r=t;"radio"===r.type||"checkbox"===r.type?r.checked=!1:(r.value="",null===(e=r.parentElement)||void 0===e||e.classList.remove("filled"),r.dispatchEvent(new Event("input")))}))}_prefersReducedMotion(){var t;return"undefined"!=typeof window&&(null===(t=window.matchMedia)||void 0===t?void 0:t.call(window,"(prefers-reduced-motion: reduce)").matches)}_canAnimate(t){return!(!t||!("animate"in t)||this._prefersReducedMotion())}_cancelAnimations(t){var e;const r=t;null===(e=null==r?void 0:r.getAnimations)||void 0===e||e.call(r).forEach((t=>t.cancel()))}showErrorForm(t){var e;const r=this.$el.querySelectorAll(this.config.inputWrapperSelector),i=r[r.length-1];let n=this.$el.querySelector(".error-block-under-input__wrapper");!n||i&&i.nextElementSibling!==n?(n=document.createElement("div"),n.className="error-block-under-input__wrapper",n.innerHTML='\n <div class="error-block-under-input error-block-under-input--warning" style="display:grid;grid-template-columns:auto 1fr;gap:8px;align-items:start;">\n <div class="error-block-under-input__icon-wrapper" style="position:relative;width:20px;height:20px;">\n <span class="error-block-under-input__icon" style="display:block;width:20px;height:20px;"></span>\n <span class="error-block-under-input__icon error-block-under-input__icon--animated" style="display:block;position:absolute;inset:0;"></span>\n </div>\n <p class="error-block-under-input__text" style="margin:0;">\n <span class="error-block-under-input__main-text"></span>\n <span class="error-block-under-input__secondary-text"></span>\n </p>\n </div>\n ',Object.assign(n.style,{boxSizing:"border-box",overflow:"hidden",height:"0px",paddingTop:"0px",opacity:"0"}),i?i.insertAdjacentElement("afterend",n):this.$el.appendChild(n)):(n.style.boxSizing="border-box",n.style.overflow="hidden"),n.querySelector(".error-block-under-input__main-text").textContent=t;const s=n.querySelector(".error-block-under-input__secondary-text");s&&(s.textContent=""),null===(e=n.querySelector(".error-block-under-input"))||void 0===e||e.classList.remove("error-block-under-input--success");const o=n.style.height,a=n.style.paddingTop;n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`;const l=n.clientHeight;if(n.style.height=o||"0px",n.style.paddingTop=a||"0px",parseFloat(n.style.height)===l&&1===parseFloat(n.style.opacity||"1"))this._ringIcon(n);else{if(this._cancelAnimations(n),this._canAnimate(n)){n.animate([{height:`${parseFloat(n.style.height)||0}px`,paddingTop:`${parseFloat(n.style.paddingTop)||0}px`,opacity:parseFloat(n.style.opacity||"0")},{height:`${l}px`,paddingTop:`${this._anim.paddingTopOpened}px`,opacity:1}],{duration:this._anim.duration,easing:this._anim.easing,fill:"forwards"}).addEventListener("finish",(()=>{n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`,n.style.opacity="1"}))}else n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`,n.style.opacity="1";this._ringIcon(n)}this.$el.addEventListener("input",(()=>this.hideErrorForm()),{passive:!0,once:!0})}hideErrorForm(){const t=this.$el.querySelectorAll(this.config.inputWrapperSelector),e=t[t.length-1],r=(null==e?void 0:e.nextElementSibling)||this.$el.querySelector(".error-block-under-input__wrapper");if(!r||!r.classList.contains("error-block-under-input__wrapper"))return;if((""===r.style.height||"auto"===r.style.height)&&(r.style.height=`${r.clientHeight}px`),r.style.paddingTop=r.style.paddingTop||`${this._anim.paddingTopOpened}px`,r.style.opacity=r.style.opacity||"1",this._cancelAnimations(r),this._canAnimate(r)){r.animate([{height:`${parseFloat(r.style.height)||r.clientHeight}px`,paddingTop:`${parseFloat(r.style.paddingTop)||this._anim.paddingTopOpened}px`,opacity:parseFloat(r.style.opacity||"1")},{height:"0px",paddingTop:"0px",opacity:0}],{duration:this._anim.duration,easing:this._anim.easing,fill:"forwards"}).addEventListener("finish",(()=>{r.remove()}))}else r.remove()}_ringIcon(t){const e=t.querySelector(".error-block-under-input__icon--animated");e&&(this._cancelAnimations(e),this._canAnimate(e)&&e.animate([{transform:"rotate(0deg) scale(1)",opacity:0},{transform:"rotate(-8deg) scale(1.05)",opacity:1,offset:.15},{transform:"rotate(8deg) scale(1.05)",opacity:1,offset:.3},{transform:"rotate(-6deg) scale(1.03)",opacity:1,offset:.45},{transform:"rotate(6deg) scale(1.03)",opacity:1,offset:.6},{transform:"rotate(0deg) scale(1)",opacity:0}],{duration:this._anim.iconDuration,easing:"ease"}))}destroy(){var t;this._onSubmitHandler&&(this.$el.removeEventListener("submit",this._onSubmitHandler),this._onSubmitHandler=void 0),this._onSubmitClickHandler&&(this.$submits.forEach((t=>{t.removeEventListener("click",this._onSubmitClickHandler)})),this._onSubmitClickHandler=void 0),this.clearFieldValidationEvents(),null===(t=this.mutationObserver)||void 0===t||t.disconnect(),this.mutationObserver=void 0;const e=this.$el.querySelector(".error-block-under-input__wrapper");e&&(this._cancelAnimations(e),e.remove()),this.$submits.forEach((t=>{t.classList.remove("button--loading");const e=t.querySelector(".button__loader");e&&e.remove()}));this.$el.querySelectorAll("input, textarea, select").forEach((t=>{this.hideError(t),this.clearFieldValidationState(t)})),document.documentElement.removeAttribute("data-now-scrolling"),this.inputs=null,this.$submits=[],this.errors=[],this.waitResponse=!1,F.delete(this.$el)}isCorrectArguments(){return"FORM"!==this.$el.tagName?(console.warn("$el не является тегом form"),!1):0!==this.$submits.length||(console.warn("В форме нет кнопки с типом submit"),!1)}formDataToObject(t){const e={};return t.forEach(((t,r)=>{Object.prototype.hasOwnProperty.call(e,r)?(Array.isArray(e[r])||(e[r]=[e[r]]),e[r].push(t)):e[r]=t})),e}appendFormDataValue(t,e,r){null!=r&&(Array.isArray(r)?r.forEach(((r,i)=>{this.appendFormDataValue(t,`${e}[${i}]`,r)})):r instanceof Blob?t.append(e,r):t.append(e,"object"==typeof r?JSON.stringify(r):String(r)))}objectToFormData(t){const e=new FormData;return Object.entries(t).forEach((([t,r])=>{this.appendFormDataValue(e,t,r)})),e}stringifyFormDataValue(t){return"string"==typeof t?t:t.name}formDataToUrlSearchParams(t){const e=new URLSearchParams;return t.forEach(((t,r)=>{e.append(r,this.stringifyFormDataValue(t))})),e}formDataToTextBody(t){return Array.from(t).map((([t,e])=>`${encodeURIComponent(t)}=${encodeURIComponent(this.stringifyFormDataValue(e))}`)).join("\n")}isBodylessMethod(t){return"GET"===t||"HEAD"===t}appendQueryString(t,e){const r=e.toString();if(!r)return t;const i=t.includes("?")?"&":"?";return`${t}${i}${r}`}findFieldByName(t){return Array.from(this.$el.querySelectorAll("input, textarea, select")).find((e=>e.name===t))||null}parseResponseBody(t){return e(this,void 0,void 0,(function*(){try{const e=yield t.json();return e&&"object"==typeof e?e:{success:!1,error:!0,"error-msg":"Некорректный ответ сервера"}}catch(t){return{success:!1,error:!0,"error-msg":"Некорректный ответ сервера"}}}))}handleUnsuccessfulResponse(t){if(!0===t.error&&(t["error-msg"]&&this.showErrorForm(t["error-msg"]),t.errors)){const e=[];t.errors.forEach((t=>{const r=this.findFieldByName(t.name);r?(e.push(r),this.setValidationError(r,"server",t["error-msg"],"server"),this.showError(r,t["error-msg"]),r.removeAttribute("aria-busy"),this.setFieldValidationState(r,"invalid")):console.warn(`Не найдено поле с именем: ${t.name}, для вывода ошибки: ${t["error-msg"]}`)})),!0===this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(e),!0===this.config.focusFirstErroredInput&&this.focusFirstErroredInput(e)}}sendData(){return e(this,void 0,void 0,(function*(){var t;const e=this.$el.getAttribute("action")||"",r=(null===(t=this.$el.getAttribute("method"))||void 0===t?void 0:t.toUpperCase())||"POST",i=this.$el.getAttribute("enctype")||"application/x-www-form-urlencoded";let n=null,s={};const o=a(this.$el);let l=this.formDataToObject(o);if(this.config.logging)for(const t of o.keys())console.log(o.get(t));const d="function"==typeof this.config.wrapData;"function"==typeof this.config.wrapData&&(l=this.config.wrapData(l));const c=d?this.objectToFormData(l):o;if(this.isBodylessMethod(r))return fetch(this.appendQueryString(e,this.formDataToUrlSearchParams(c)),{method:r,headers:s});switch(i){case"application/x-www-form-urlencoded":n=this.formDataToUrlSearchParams(c).toString(),s["Content-Type"]="application/x-www-form-urlencoded";break;case"multipart/form-data":n=c;break;case"text/plain":n=this.formDataToTextBody(c),s["Content-Type"]="text/plain";break;case"application/json":n=JSON.stringify(l),s["Content-Type"]="application/json";break;default:console.warn(`Неизвестный enctype: ${i}. Используется application/x-www-form-urlencoded.`),n=this.formDataToUrlSearchParams(c).toString(),s["Content-Type"]="application/x-www-form-urlencoded"}return fetch(e,{method:r,body:n,headers:s})}))}submit(){return e(this,void 0,void 0,(function*(){var t,e,r,i,n,s,o,a,d,c,u,h;if(!this.waitResponse){let p=!1;const f=()=>{p||(this.waitResponse=!1,!0===this.config.showLoaderButton&&this.hideLoader(),p=!0)};try{if(!1===(yield null===(e=(t=this.config).onBeforeSubmit)||void 0===e?void 0:e.call(t,this)))return{success:!1};!0===this.config.showLoaderButton&&this.showLoader(),null===(i=(r=this.config).onSubmit)||void 0===i||i.call(r,this),this.waitResponse=!0;const u=yield this.sendData(),h=yield this.parseResponseBody(u);return f(),null===(s=(n=this.config).onResponse)||void 0===s||s.call(n,h,this),u.ok&&!0===h.success?null===(a=(o=this.config).onResponseSuccess)||void 0===a||a.call(o,h,this):(null===(c=(d=this.config).onResponseUnsuccess)||void 0===c||c.call(d,h,this),this.handleUnsuccessfulResponse(h)),l(h),{success:u.ok&&!0===h.success,response:u,responseBody:h}}catch(t){throw f(),null===(h=(u=this.config).onSubmitError)||void 0===h||h.call(u,t,this),t}}}))}}return t.FORM_ERROR_FIELD=w,t.blockScrollBody=function(){window.lastBlockScrollPosition=window.pageYOffset,document.body.style.top=-window.lastBlockScrollPosition+"px",document.body.classList.add("body--block-scroll")},t.closest=r,t.createFieldValidator=function(t,r){return(i,n,s,o)=>e(this,void 0,void 0,(function*(){return!!(yield t(i,n,s,o))||{valid:!1,message:r}}))},t.createSchemaValidator=y,t.default=V,t.getAllValidators=function(){return new Map(h)},t.getValidator=f,t.isEmailValid=function(t){return/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu.test(t)},t.isPhoneValid=function(t){const e=[];return Array.from(t).forEach((t=>{(Number(t)||0===Number(t))&&e.push(t)})),!(e.length<11||e.length<12&&"+"===t[1])},t.isUrlValid=o,t.parseCommonResponseProperties=l,t.registerSchemaValidator=function(t,e,r,i={}){const{override:n}=i;p(t,y(e,function(t,e){var r={};for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&e.indexOf(i)<0&&(r[i]=t[i]);if(null!=t&&"function"==typeof Object.getOwnPropertySymbols){var n=0;for(i=Object.getOwnPropertySymbols(t);n<i.length;n++)e.indexOf(i[n])<0&&Object.prototype.propertyIsEnumerable.call(t,i[n])&&(r[i[n]]=t[i[n]])}return r}(i,["override"])),r,{override:n})},t.registerValidator=p,t.serializeFormToJSON=d,t.serializeToFormData=a,t.unblockScrollBody=function(){document.body.classList.remove("body--block-scroll"),document.body.style.top="",window.scrollTo(0,window.lastBlockScrollPosition||0)},Object.defineProperty(t,"__esModule",{value:!0}),t}({});
|
|
2
2
|
//# sourceMappingURL=FormFather.min.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -121,6 +121,7 @@ type ValidationRule = string | {
|
|
|
121
121
|
type ValidationTrigger = 'submit' | 'input' | 'blur' | 'change';
|
|
122
122
|
type ValidationErrorSource = 'client' | 'server' | 'manual';
|
|
123
123
|
type FieldReference = string | FormFieldElement;
|
|
124
|
+
declare const FORM_ERROR_FIELD = "_form";
|
|
124
125
|
interface ValidationError {
|
|
125
126
|
field: string;
|
|
126
127
|
rule: string;
|
|
@@ -136,6 +137,21 @@ interface SubmitResult {
|
|
|
136
137
|
interface FormResetOptions {
|
|
137
138
|
clearErrors?: boolean;
|
|
138
139
|
}
|
|
140
|
+
interface ValidationIssue {
|
|
141
|
+
field?: string;
|
|
142
|
+
rule?: string;
|
|
143
|
+
message: string;
|
|
144
|
+
source?: ValidationErrorSource;
|
|
145
|
+
}
|
|
146
|
+
type ValidationIssueInput = string | ValidationIssue | ErrorResponse | Record<string, string> | Array<string | ValidationIssue | ErrorResponse>;
|
|
147
|
+
interface FormValidatorContext {
|
|
148
|
+
form: Form;
|
|
149
|
+
formElement: HTMLFormElement;
|
|
150
|
+
values: Record<string, any>;
|
|
151
|
+
errors: ValidationError[];
|
|
152
|
+
}
|
|
153
|
+
type FormValidatorResult = boolean | string | void | ValidationIssue | Array<string | ValidationIssue>;
|
|
154
|
+
type FormValidator = (context: FormValidatorContext) => FormValidatorResult | Promise<FormValidatorResult>;
|
|
139
155
|
type ValidationSchema = Record<string, {
|
|
140
156
|
rules: ValidationRule[];
|
|
141
157
|
selector?: string;
|
|
@@ -243,6 +259,8 @@ interface FormOptions {
|
|
|
243
259
|
wrapData?: (data: Record<string, any>) => Record<string, any>;
|
|
244
260
|
/** Схема валидации: поле → массив правил + override-сообщения */
|
|
245
261
|
validationSchema?: ValidationSchema;
|
|
262
|
+
/** Валидаторы всей формы: cross-field, conditional required, date ranges и другие проверки над `getValues()`. */
|
|
263
|
+
formValidators?: FormValidator | FormValidator[];
|
|
246
264
|
}
|
|
247
265
|
interface ErrorResponse {
|
|
248
266
|
name: string;
|
|
@@ -305,6 +323,12 @@ declare class Form {
|
|
|
305
323
|
setValues(values: Record<string, any>): this;
|
|
306
324
|
/** Снимает все клиентские и серверные ошибки с формы, не меняя значения полей. */
|
|
307
325
|
clearErrors(): this;
|
|
326
|
+
private isErrorResponse;
|
|
327
|
+
private normalizeValidationIssues;
|
|
328
|
+
private applyValidationIssue;
|
|
329
|
+
/** Показывает несколько ошибок формы или полей: из backend map/array, `ErrorResponse[]` или form validators. */
|
|
330
|
+
setErrors(issues: ValidationIssueInput, source?: ValidationErrorSource): this;
|
|
331
|
+
private validateFormValidators;
|
|
308
332
|
/** Сбрасывает форму через native `reset()` и опционально очищает ошибки. */
|
|
309
333
|
reset(options?: FormResetOptions): this;
|
|
310
334
|
/**
|
|
@@ -355,6 +379,7 @@ declare class Form {
|
|
|
355
379
|
private getFieldKey;
|
|
356
380
|
private hasFieldError;
|
|
357
381
|
private clearFieldErrorRecord;
|
|
382
|
+
private setErrorRecord;
|
|
358
383
|
private getValidationStateAttribute;
|
|
359
384
|
private setFieldValidationState;
|
|
360
385
|
private startFieldValidation;
|
|
@@ -376,7 +401,7 @@ declare class Form {
|
|
|
376
401
|
private runFieldValidation;
|
|
377
402
|
private getRadioGroup;
|
|
378
403
|
/** Показывает ошибку конкретного поля по имени или DOM-элементу. */
|
|
379
|
-
showFieldError(field: FieldReference, message: string, source?: ValidationErrorSource): boolean;
|
|
404
|
+
showFieldError(field: FieldReference, message: string, source?: ValidationErrorSource, rule?: string): boolean;
|
|
380
405
|
/** Проверяет одно поле по имени или DOM-элементу. */
|
|
381
406
|
validateField(field: FieldReference): Promise<boolean>;
|
|
382
407
|
/**
|
|
@@ -437,4 +462,4 @@ declare class Form {
|
|
|
437
462
|
submit(): Promise<SubmitResult | undefined>;
|
|
438
463
|
}
|
|
439
464
|
|
|
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 };
|
|
465
|
+
export { type AdapterValidator, type ErrorResponse, FORM_ERROR_FIELD, type FieldReference, type FormFieldElement, type FormOptions, type FormResetOptions, type FormValidator, type FormValidatorContext, type FormValidatorResult, type RegisterSchemaValidatorOptions, type ResponseBody, type SchemaLike, type SchemaParseResult, type SchemaValidatorOptions, type SubmitResult, type ValidationError, type ValidationErrorSource, type ValidationIssue, type ValidationIssueInput, type ValidationRule, type ValidationSchema, type ValidationTrigger, type ValidatorEffectCtx, type ValidatorFieldElement, type ValidatorFn, type ValidatorResult, blockScrollBody, closest, createFieldValidator, createSchemaValidator, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerSchemaValidator, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
|
package/dist/index.js
CHANGED
|
@@ -445,6 +445,7 @@ const __shared =
|
|
|
445
445
|
defaultParams: {},
|
|
446
446
|
defaultValidationSchema: undefined,
|
|
447
447
|
});
|
|
448
|
+
const FORM_ERROR_FIELD = '_form';
|
|
448
449
|
let loaderIdCounter = 0;
|
|
449
450
|
const FORM_INSTANCES = new WeakMap();
|
|
450
451
|
/** Изначальная дефолтная схема. */
|
|
@@ -637,6 +638,99 @@ class Form {
|
|
|
637
638
|
this.hideErrorForm();
|
|
638
639
|
return this;
|
|
639
640
|
}
|
|
641
|
+
isErrorResponse(issue) {
|
|
642
|
+
return (!!issue &&
|
|
643
|
+
typeof issue === 'object' &&
|
|
644
|
+
typeof issue.name === 'string' &&
|
|
645
|
+
typeof issue['error-msg'] === 'string');
|
|
646
|
+
}
|
|
647
|
+
normalizeValidationIssues(issues, source) {
|
|
648
|
+
if (issues == null || issues === true)
|
|
649
|
+
return [];
|
|
650
|
+
if (issues === false)
|
|
651
|
+
return [{ message: 'Некорректные данные формы', source }];
|
|
652
|
+
if (typeof issues === 'string')
|
|
653
|
+
return [{ message: issues, source }];
|
|
654
|
+
if (Array.isArray(issues)) {
|
|
655
|
+
return issues.flatMap(issue => this.normalizeValidationIssues(issue, source));
|
|
656
|
+
}
|
|
657
|
+
if (this.isErrorResponse(issues)) {
|
|
658
|
+
return [
|
|
659
|
+
{
|
|
660
|
+
field: issues.name,
|
|
661
|
+
rule: source,
|
|
662
|
+
message: issues['error-msg'],
|
|
663
|
+
source,
|
|
664
|
+
},
|
|
665
|
+
];
|
|
666
|
+
}
|
|
667
|
+
if (typeof issues === 'object' && 'message' in issues && typeof issues.message === 'string') {
|
|
668
|
+
return [Object.assign(Object.assign({}, issues), { source: issues.source || source })];
|
|
669
|
+
}
|
|
670
|
+
if (typeof issues === 'object') {
|
|
671
|
+
return Object.entries(issues).map(([field, message]) => ({
|
|
672
|
+
field,
|
|
673
|
+
rule: source,
|
|
674
|
+
message,
|
|
675
|
+
source,
|
|
676
|
+
}));
|
|
677
|
+
}
|
|
678
|
+
return [];
|
|
679
|
+
}
|
|
680
|
+
applyValidationIssue(issue, source) {
|
|
681
|
+
const issueSource = issue.source || source;
|
|
682
|
+
const rule = issue.rule || issueSource;
|
|
683
|
+
const field = issue.field || FORM_ERROR_FIELD;
|
|
684
|
+
if (field === FORM_ERROR_FIELD) {
|
|
685
|
+
this.setErrorRecord(FORM_ERROR_FIELD, rule, issue.message, issueSource);
|
|
686
|
+
this.showErrorForm(issue.message);
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
const $field = this.findFieldByName(field);
|
|
690
|
+
if (!$field) {
|
|
691
|
+
this.setErrorRecord(field, rule, issue.message, issueSource);
|
|
692
|
+
console.warn(`Не найдено поле с именем: ${field}, для вывода ошибки: ${issue.message}`);
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
this.showFieldError($field, issue.message, issueSource, rule);
|
|
696
|
+
return $field;
|
|
697
|
+
}
|
|
698
|
+
/** Показывает несколько ошибок формы или полей: из backend map/array, `ErrorResponse[]` или form validators. */
|
|
699
|
+
setErrors(issues, source = 'server') {
|
|
700
|
+
this.normalizeValidationIssues(issues, source).forEach(issue => {
|
|
701
|
+
this.applyValidationIssue(issue, source);
|
|
702
|
+
});
|
|
703
|
+
return this;
|
|
704
|
+
}
|
|
705
|
+
validateFormValidators() {
|
|
706
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
707
|
+
const validators = this.config.formValidators
|
|
708
|
+
? Array.isArray(this.config.formValidators)
|
|
709
|
+
? this.config.formValidators
|
|
710
|
+
: [this.config.formValidators]
|
|
711
|
+
: [];
|
|
712
|
+
const erroredInputs = [];
|
|
713
|
+
let ok = true;
|
|
714
|
+
for (const validator of validators) {
|
|
715
|
+
const result = yield validator({
|
|
716
|
+
form: this,
|
|
717
|
+
formElement: this.$el,
|
|
718
|
+
values: this.getValues(),
|
|
719
|
+
errors: this.getErrors(),
|
|
720
|
+
});
|
|
721
|
+
const issues = this.normalizeValidationIssues(result, 'client');
|
|
722
|
+
if (issues.length === 0)
|
|
723
|
+
continue;
|
|
724
|
+
ok = false;
|
|
725
|
+
issues.forEach(issue => {
|
|
726
|
+
const $field = this.applyValidationIssue(issue, 'client');
|
|
727
|
+
if ($field)
|
|
728
|
+
erroredInputs.push($field);
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
return { ok, erroredInputs };
|
|
732
|
+
});
|
|
733
|
+
}
|
|
640
734
|
/** Сбрасывает форму через native `reset()` и опционально очищает ошибки. */
|
|
641
735
|
reset(options = {}) {
|
|
642
736
|
this.$el.reset();
|
|
@@ -936,6 +1030,14 @@ class Form {
|
|
|
936
1030
|
const field = this.getFieldKey($input);
|
|
937
1031
|
this.errors = this.errors.filter(error => error.field !== field);
|
|
938
1032
|
}
|
|
1033
|
+
setErrorRecord(field, rule, message, source) {
|
|
1034
|
+
this.errors.push({
|
|
1035
|
+
field,
|
|
1036
|
+
rule,
|
|
1037
|
+
message,
|
|
1038
|
+
source,
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
939
1041
|
getValidationStateAttribute() {
|
|
940
1042
|
return this.config.validationStateAttribute || 'data-form-father-state';
|
|
941
1043
|
}
|
|
@@ -971,12 +1073,7 @@ class Form {
|
|
|
971
1073
|
}
|
|
972
1074
|
setValidationError($input, rule, message, source) {
|
|
973
1075
|
this.clearFieldErrorRecord($input);
|
|
974
|
-
this.
|
|
975
|
-
field: this.getFieldKey($input),
|
|
976
|
-
rule,
|
|
977
|
-
message,
|
|
978
|
-
source,
|
|
979
|
-
});
|
|
1076
|
+
this.setErrorRecord(this.getFieldKey($input), rule, message, source);
|
|
980
1077
|
}
|
|
981
1078
|
resolveField(field) {
|
|
982
1079
|
if (typeof field !== 'string')
|
|
@@ -1161,13 +1258,13 @@ class Form {
|
|
|
1161
1258
|
return Array.from($block.querySelectorAll('input[type="radio"]')).filter($radio => $radio.name === $input.name);
|
|
1162
1259
|
}
|
|
1163
1260
|
/** Показывает ошибку конкретного поля по имени или DOM-элементу. */
|
|
1164
|
-
showFieldError(field, message, source = 'manual') {
|
|
1261
|
+
showFieldError(field, message, source = 'manual', rule = source) {
|
|
1165
1262
|
const $field = this.resolveField(field);
|
|
1166
1263
|
if (!$field) {
|
|
1167
1264
|
console.warn(`[FormFather] Field "${String(field)}" not found`);
|
|
1168
1265
|
return false;
|
|
1169
1266
|
}
|
|
1170
|
-
this.setValidationError($field,
|
|
1267
|
+
this.setValidationError($field, rule, message, source);
|
|
1171
1268
|
this.showError($field, message);
|
|
1172
1269
|
$field.removeAttribute('aria-busy');
|
|
1173
1270
|
this.setFieldValidationState($field, 'invalid');
|
|
@@ -1222,6 +1319,11 @@ class Form {
|
|
|
1222
1319
|
erroredInputs.push($input);
|
|
1223
1320
|
}
|
|
1224
1321
|
}
|
|
1322
|
+
const formValidation = yield this.validateFormValidators();
|
|
1323
|
+
if (!formValidation.ok) {
|
|
1324
|
+
ok = false;
|
|
1325
|
+
erroredInputs.push(...formValidation.erroredInputs);
|
|
1326
|
+
}
|
|
1225
1327
|
const visibleErroredInputs = erroredInputs.filter(item => !item.hasAttribute('data-no-error-scroll'));
|
|
1226
1328
|
if (!ok && this.config.scrollToFirstErroredInput) {
|
|
1227
1329
|
this.scrollToFirstErroredInput(visibleErroredInputs);
|
|
@@ -1716,5 +1818,5 @@ class Form {
|
|
|
1716
1818
|
}
|
|
1717
1819
|
}
|
|
1718
1820
|
|
|
1719
|
-
export { blockScrollBody, closest, createFieldValidator, createSchemaValidator, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerSchemaValidator, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
|
|
1821
|
+
export { FORM_ERROR_FIELD, blockScrollBody, closest, createFieldValidator, createSchemaValidator, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerSchemaValidator, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
|
|
1720
1822
|
//# 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/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,6 +24,21 @@ 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>;
|
|
26
42
|
export type ValidationSchema = Record<string, {
|
|
27
43
|
rules: ValidationRule[];
|
|
28
44
|
selector?: string;
|
|
@@ -130,6 +146,8 @@ export interface FormOptions {
|
|
|
130
146
|
wrapData?: (data: Record<string, any>) => Record<string, any>;
|
|
131
147
|
/** Схема валидации: поле → массив правил + override-сообщения */
|
|
132
148
|
validationSchema?: ValidationSchema;
|
|
149
|
+
/** Валидаторы всей формы: cross-field, conditional required, date ranges и другие проверки над `getValues()`. */
|
|
150
|
+
formValidators?: FormValidator | FormValidator[];
|
|
133
151
|
}
|
|
134
152
|
export interface ErrorResponse {
|
|
135
153
|
name: string;
|
|
@@ -192,6 +210,12 @@ export default class Form {
|
|
|
192
210
|
setValues(values: Record<string, any>): this;
|
|
193
211
|
/** Снимает все клиентские и серверные ошибки с формы, не меняя значения полей. */
|
|
194
212
|
clearErrors(): this;
|
|
213
|
+
private isErrorResponse;
|
|
214
|
+
private normalizeValidationIssues;
|
|
215
|
+
private applyValidationIssue;
|
|
216
|
+
/** Показывает несколько ошибок формы или полей: из backend map/array, `ErrorResponse[]` или form validators. */
|
|
217
|
+
setErrors(issues: ValidationIssueInput, source?: ValidationErrorSource): this;
|
|
218
|
+
private validateFormValidators;
|
|
195
219
|
/** Сбрасывает форму через native `reset()` и опционально очищает ошибки. */
|
|
196
220
|
reset(options?: FormResetOptions): this;
|
|
197
221
|
/**
|
|
@@ -242,6 +266,7 @@ export default class Form {
|
|
|
242
266
|
private getFieldKey;
|
|
243
267
|
private hasFieldError;
|
|
244
268
|
private clearFieldErrorRecord;
|
|
269
|
+
private setErrorRecord;
|
|
245
270
|
private getValidationStateAttribute;
|
|
246
271
|
private setFieldValidationState;
|
|
247
272
|
private startFieldValidation;
|
|
@@ -263,7 +288,7 @@ export default class Form {
|
|
|
263
288
|
private runFieldValidation;
|
|
264
289
|
private getRadioGroup;
|
|
265
290
|
/** Показывает ошибку конкретного поля по имени или DOM-элементу. */
|
|
266
|
-
showFieldError(field: FieldReference, message: string, source?: ValidationErrorSource): boolean;
|
|
291
|
+
showFieldError(field: FieldReference, message: string, source?: ValidationErrorSource, rule?: string): boolean;
|
|
267
292
|
/** Проверяет одно поле по имени или DOM-элементу. */
|
|
268
293
|
validateField(field: FieldReference): Promise<boolean>;
|
|
269
294
|
/**
|
package/docs/en/README.md
CHANGED
|
@@ -50,12 +50,15 @@ 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,
|
|
59
62
|
} from 'form-father';
|
|
60
63
|
```
|
|
61
64
|
|
|
@@ -82,6 +85,7 @@ import Form, {
|
|
|
82
85
|
- **validationStateAttribute**: Field state attribute: `validating`, `valid`, or `invalid`. Defaults to
|
|
83
86
|
`data-form-father-state`.
|
|
84
87
|
- **observeMutations**: Watches dynamically added fields and submit buttons.
|
|
88
|
+
- **formValidators**: A form validator or array of validators for cross-field rules.
|
|
85
89
|
|
|
86
90
|
## Form Submission
|
|
87
91
|
|
|
@@ -101,6 +105,7 @@ import Form, {
|
|
|
101
105
|
- **validate()**: Validates the whole form.
|
|
102
106
|
- **validateField(field)**: Validates one field by name or DOM element.
|
|
103
107
|
- **showFieldError(field, message, source)**: Shows a field error manually.
|
|
108
|
+
- **setErrors(errors, source)**: Applies backend/form-level errors from an object, array, or `ErrorResponse[]`.
|
|
104
109
|
- **getErrors()**: Returns current errors as `{ field, rule, message, source }`.
|
|
105
110
|
- **getValues()**: Returns form values as a plain object.
|
|
106
111
|
- **setValues(values)**: Fills fields by `name` and dispatches `input`/`change`.
|
|
@@ -137,6 +142,34 @@ Final rule order:
|
|
|
137
142
|
required → schema.rules → data-validate → data-custom-validate
|
|
138
143
|
```
|
|
139
144
|
|
|
145
|
+
### Cross-field and form-level validation
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const form = new Form($form, {
|
|
149
|
+
formValidators: [
|
|
150
|
+
({ values }) =>
|
|
151
|
+
values.password === values.passwordConfirm
|
|
152
|
+
? true
|
|
153
|
+
: {
|
|
154
|
+
field: 'passwordConfirm',
|
|
155
|
+
rule: 'same-as-password',
|
|
156
|
+
message: 'Passwords do not match',
|
|
157
|
+
},
|
|
158
|
+
({ values }) => (values.start <= values.end ? true : { field: 'end', message: 'End date must be after start' }),
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Return a string or an issue without `field` to show a form-level error. In `getErrors()`, it uses
|
|
164
|
+
`FORM_ERROR_FIELD` (`"_form"`).
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
form.setErrors({
|
|
168
|
+
email: 'Email is already used',
|
|
169
|
+
[FORM_ERROR_FIELD]: 'Please check the form',
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
140
173
|
## Schema Adapters
|
|
141
174
|
|
|
142
175
|
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,32 @@ form.clearErrors();
|
|
|
49
49
|
form.reset();
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
## Cross-field validation
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const form = new Form(document.querySelector('form')!, {
|
|
56
|
+
formValidators: ({ values }) =>
|
|
57
|
+
values.password === values.passwordConfirm
|
|
58
|
+
? true
|
|
59
|
+
: {
|
|
60
|
+
field: 'passwordConfirm',
|
|
61
|
+
rule: 'same-as-password',
|
|
62
|
+
message: 'Passwords do not match',
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Applying backend errors
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { FORM_ERROR_FIELD } from 'form-father';
|
|
71
|
+
|
|
72
|
+
form.setErrors({
|
|
73
|
+
email: 'Email is already registered',
|
|
74
|
+
[FORM_ERROR_FIELD]: 'Please check the form',
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
52
78
|
## Async validation state
|
|
53
79
|
|
|
54
80
|
```css
|
package/package.json
CHANGED