form-father 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
+ ## 0.5.0 - 2026-04-27
6
+
7
+ ### Added
8
+
9
+ - Added field validator DX helpers: `registerFieldValidator()`, `createPatternValidator()`, and
10
+ `createLengthValidator()`.
11
+ - Added form validator DX helpers: `createFormValidator()`, `sameAsField()`, `requiredIf()`, and `dateOrder()`.
12
+ - Added helper types for reusable form validator predicates and issue factories.
13
+
14
+ ### Changed
15
+
16
+ - Demo and documentation now use higher-level helper APIs for common password-confirm and validation rule recipes.
17
+
18
+ ### Tests
19
+
20
+ - Test suite expanded to 115 tests.
21
+ - Coverage baseline: 98.19% statements, 86.91% branches, 97.04% functions, 100% lines.
22
+
23
+ ## 0.4.0 - 2026-04-27
24
+
25
+ ### Added
26
+
27
+ - Added form-level and cross-field validation via `formValidators`.
28
+ - Added `setErrors()` for backend maps, `ErrorResponse[]`, global form errors, and custom validation issue objects.
29
+ - Added `FORM_ERROR_FIELD` constant for form-level errors in `getErrors()`.
30
+ - Added public TypeScript types for form validation contexts and issues.
31
+
32
+ ### Changed
33
+
34
+ - `validate()` now runs field-level rules first, then form-level validators over `getValues()`.
35
+ - Cross-field and backend errors share the same rendering path as field validation errors.
36
+
37
+ ### Tests
38
+
39
+ - Test suite expanded to 113 tests.
40
+ - Coverage baseline: 98.31% statements, 87.05% branches, 96.34% functions, 100% lines.
41
+
5
42
  ## 0.3.0 - 2026-04-25
6
43
 
7
44
  ### Added
package/README.md CHANGED
@@ -69,12 +69,16 @@ const forms = Form.initAll('form[data-form-father]', {
69
69
 
70
70
  ```ts
71
71
  import Form, {
72
+ FORM_ERROR_FIELD,
72
73
  type FormOptions,
73
74
  type ValidationSchema,
74
75
  type ResponseBody,
75
76
  type ValidationError,
76
77
  type SubmitResult,
77
78
  type FormResetOptions,
79
+ type FormValidator,
80
+ type ValidationIssue,
81
+ type FormValidatorPredicate,
78
82
  } from 'form-father';
79
83
  ```
80
84
 
@@ -107,6 +111,7 @@ import Form, {
107
111
  - **validationStateAttribute**: Атрибут состояния поля: `validating`, `valid`, `invalid`. По умолчанию
108
112
  `data-form-father-state`.
109
113
  - **observeMutations**: Следить за динамически добавленными полями и submit-кнопками.
114
+ - **formValidators**: Валидатор или массив валидаторов всей формы для cross-field правил.
110
115
 
111
116
  ## Отправка формы
112
117
 
@@ -126,6 +131,7 @@ import Form, {
126
131
  - **validate()**: Проверяет всю форму.
127
132
  - **validateField(field)**: Проверяет одно поле по имени или DOM-элементу.
128
133
  - **showFieldError(field, message, source)**: Показывает ошибку поля вручную.
134
+ - **setErrors(errors, source)**: Применяет backend/form-level ошибки из объекта, массива или `ErrorResponse[]`.
129
135
  - **getErrors()**: Возвращает текущие ошибки в формате `{ field, rule, message, source }`.
130
136
  - **getValues()**: Возвращает значения формы обычным объектом.
131
137
  - **setValues(values)**: Заполняет поля по `name` и диспатчит `input`/`change`.
@@ -321,6 +327,40 @@ required → schema.rules → data-validate → data-custom-validate
321
327
  - Для каждого поля отображается **только первое** найденное сообщение об ошибке.
322
328
  - Неизвестные правила логируются и пропускаются до начала проверки, поэтому не влияют на результат валидации.
323
329
 
330
+ ### Cross-field и form-level validation
331
+
332
+ ```ts
333
+ import { dateOrder, requiredIf, sameAsField } from 'form-father';
334
+
335
+ const form = new Form($form, {
336
+ formValidators: [
337
+ sameAsField('passwordConfirm', 'password', 'Пароли не совпадают'),
338
+ requiredIf('email', ({ values }) => Boolean(values.subscribe), 'Email обязателен для подписки'),
339
+ dateOrder('start', 'end', 'Дата окончания раньше начала'),
340
+ ],
341
+ });
342
+ ```
343
+
344
+ Глобальную ошибку формы можно вернуть строкой или issue без `field`. В `getErrors()` она будет иметь поле
345
+ `FORM_ERROR_FIELD` (`"_form"`).
346
+
347
+ ```ts
348
+ form.setErrors({
349
+ email: 'Email уже занят',
350
+ [FORM_ERROR_FIELD]: 'Проверьте данные формы',
351
+ });
352
+ ```
353
+
354
+ ### DX helpers для правил
355
+
356
+ ```ts
357
+ import { createLengthValidator, createPatternValidator, registerFieldValidator } from 'form-father';
358
+
359
+ registerFieldValidator('starts-with-a', value => value.startsWith('A'), 'Должно начинаться с A');
360
+ registerValidator('slug', createPatternValidator(/^[a-z0-9-]+$/), 'Только slug-символы');
361
+ registerValidator('short-name', createLengthValidator({ min: 2, max: 24 }), 'От 2 до 24 символов');
362
+ ```
363
+
324
364
  ## Адаптеры схем
325
365
 
326
366
  Form Father не тянет внешние зависимости, но умеет оборачивать Zod/Valibot/Yup-подобные схемы с `safeParse()` или
package/demos/index.html CHANGED
@@ -51,6 +51,18 @@
51
51
  />
52
52
  </label>
53
53
 
54
+ <label class="field">
55
+ <span>Repeat password</span>
56
+ <input
57
+ class="input"
58
+ name="passwordConfirm"
59
+ type="password"
60
+ autocomplete="current-password"
61
+ data-validate="required"
62
+ data-error-required="Повторите пароль"
63
+ />
64
+ </label>
65
+
54
66
  <button type="submit">Войти</button>
55
67
  </form>
56
68
  </section>
package/demos/main.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const api = window.FormFather || {};
2
2
  const Form = api.default || api.Form || api;
3
- const { registerValidator } = api;
3
+ const { createLengthValidator, registerValidator, sameAsField } = api;
4
4
  const output = document.querySelector('#demo-output');
5
5
 
6
6
  function writeOutput(title, payload) {
@@ -57,9 +57,7 @@ window.fetch = async (url, options = {}) => {
57
57
  return json({ success: false, error: true, 'error-msg': `No demo route for ${requestUrl}` }, 404);
58
58
  };
59
59
 
60
- registerValidator('min-length', (value, _input, _form, min) => value.length >= Number(min), 'Слишком короткое значение', {
61
- override: true,
62
- });
60
+ registerValidator('min-length', createLengthValidator({ min: 6 }), 'Слишком короткое значение', { override: true });
63
61
  registerValidator(
64
62
  'available-email',
65
63
  value =>
@@ -78,6 +76,7 @@ Form.initAll('form[data-form-father]', {
78
76
  validationDebounce: 120,
79
77
  focusFirstErroredInput: true,
80
78
  loaderColor: 'currentColor',
79
+ formValidators: sameAsField('passwordConfirm', 'password', 'Пароли не совпадают'),
81
80
  onValidationError(errors) {
82
81
  writeOutput('Client validation errors', errors);
83
82
  },
@@ -1,2 +1,2 @@
1
- var FormFather=function(t){"use strict";function e(t,e,i,r){return new(i||(i=Promise))((function(n,o){function s(t){try{l(r.next(t))}catch(t){o(t)}}function a(t){try{l(r.throw(t))}catch(t){o(t)}}function l(t){var e;t.done?n(t.value):(e=t.value,e instanceof i?e:new i((function(t){t(e)}))).then(s,a)}l((r=r.apply(t,e||[])).next())}))}function i(t,e){if(null==t)return null;let i=t;for(;i;){if(i.matches(e))return i;i=i.parentElement}return null}"function"==typeof SuppressedError&&SuppressedError;const r=["http:","https:"];function n(t){return t.length>0&&t.length<=63&&/^[a-z\d-]+$/i.test(t)&&!t.startsWith("-")&&!t.endsWith("-")}function o(t){return!!t&&("localhost"===t||function(t){const e=t.split(".");return 4===e.length&&e.every((t=>{if(!/^\d{1,3}$/.test(t))return!1;const e=Number(t);return e>=0&&e<=255}))}(t)||function(t){if(!t.startsWith("[")||!t.endsWith("]"))return!1;const e=t.slice(1,-1);return e.includes(":")&&/^[\da-f:.]+$/i.test(e)}(t)||function(t){const e=t.split(".");return e.length>=2&&e.every(n)}(t))}function s(t){const e=function(t){const e=t.trim();return 0===e.length?"":e.startsWith("//")?`https:${e}`:/^[a-z][a-z\d+.-]*:\/\//i.test(e)?e:`https://${e}`}(t);if(0===e.length)return!1;try{const t=new URL(e);return r.includes(t.protocol)&&o(t.hostname)}catch(t){return!1}}function a(t){const e="FORM"===t.tagName?Array.from(t.elements):Array.from(t.querySelectorAll("input, select, textarea, button")),i=new FormData;return e.forEach((t=>{if(!(t instanceof HTMLInputElement||t instanceof HTMLSelectElement||t instanceof HTMLTextAreaElement||t instanceof HTMLButtonElement))return;if(!t.name||t.disabled||t.hasAttribute("data-no-serialize"))return;if(t instanceof HTMLButtonElement)return;const{name:e,type:r}=t;if("file"===r)if(t.multiple){const r=t.files;if(r)for(let t=0;t<r.length;t++)i.append(`${e}[${t}]`,r[t])}else{const r=t.files;r&&r[0]&&i.append(e,r[0])}else{let n;if("select-one"===r||"select-multiple"===r){const o=t;if("select-multiple"===r){Array.from(o.selectedOptions).map((t=>t.value)).forEach(((t,r)=>{i.append(`${e}[${r}]`,t)}))}else n=o.value,n&&i.append(e,n)}else if("tel"===r)n=t.value.split(/[\s()-]/).join("").replace(/^8/,"+7");else if("checkbox"===r||"radio"===r){if(!t.checked)return;n=t.value}else if("date"===r){const e=t.getAttribute("data-date-format");n=e?function(t,e){const i=new Date(t),r=t=>t.toString().padStart(2,"0"),n=r(i.getDate()),o=r(i.getMonth()+1),s=i.getFullYear().toString();return e.replace("dd",n).replace("mm",o).replace("yyyy",s)}(t.value,e):t.value}else n=t.value;n&&"select-one"!==r&&"select-multiple"!==r&&i.append(e,n)}})),i}function l(t){if(Object.prototype.hasOwnProperty.call(t,"redirect-url"))if(t["redirect-url-delay"]){const e=Number(t["redirect-url-delay"]);setTimeout((()=>{window.location.href=t["redirect-url"]}),e)}else window.location.href=t["redirect-url"];else if(Object.prototype.hasOwnProperty.call(t,"reload")){if(!0===t.reload)if(t["reload-delay"]){const e=Number(t["reload-delay"]);setTimeout((()=>{window.location.reload()}),e)}else window.location.reload()}else if(Object.prototype.hasOwnProperty.call(t,"toast")){const e=t.toast;(Array.isArray(e)?e:[e]).forEach((t=>{window.showToast(t)}))}else Object.prototype.hasOwnProperty.call(t,"error-toast")&&window.showToast(t["error-toast"])}function d(t){const e={};return new FormData(t).forEach(((t,i)=>{e.hasOwnProperty(i)?(Array.isArray(e[i])||(e[i]=[e[i]]),e[i].push(t)):e[i]=t})),e}var c;const u=Symbol.for("FormFather.validators"),h=null!==(c=globalThis[u])&&void 0!==c?c:globalThis[u]=new Map;function p(t,e,i,{override:r=!1}={}){!h.has(t)||r?h.set(t,{fn:e,defaultMessage:i}):console.warn(`[FormFather] Validator "${t}" already exists; pass { override: true } to replace`)}const f=t=>h.get(t);function m(t,e,i){h.has(t)||h.set(t,{fn:e,defaultMessage:i})}function g(t,e){return"function"==typeof e?e(t):e||function(t){var e;if(!t||"object"!=typeof t)return;const i=t.issues||t.errors;if(Array.isArray(i)){const t=null===(e=i.find((t=>"string"==typeof(null==t?void 0:t.message))))||void 0===e?void 0:e.message;if(t)return t}return"string"==typeof t.message?t.message:void 0}(t)}function y(t,i={}){return(r,n,o,s)=>e(this,void 0,void 0,(function*(){const e=i.mapValue?i.mapValue(r,n,o,s):r;try{if("function"==typeof t)return a=yield t(e),l=g(void 0,i.message),"object"==typeof a&&null!==a?a:void 0===a||!!a||{valid:!1,message:l};if("function"==typeof t.safeParse){const r=yield t.safeParse(e);return!!r.success||{valid:!1,message:g(r.error||r,i.message)}}if("function"==typeof t.parse)return yield t.parse(e),!0}catch(t){return{valid:!1,message:g(t,i.message)}}var a,l;return{valid:!1,message:g(void 0,i.message)}}))}var b;m("required",((t,e,i)=>{if(e&&e.type){if("checkbox"===e.type)return e.checked;if("radio"===e.type){const t=i.querySelectorAll(`input[type="radio"][name="${e.name}"]`);return Array.from(t).some((t=>t.checked))}}return t.trim().length>0}),"Пустое значение"),m("email",(t=>/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)),"Неверный формат"),m("tel",(t=>/^\+7\d{10}$/.test(t)),"Неверный формат"),m("url",(t=>s(t)),"Неверный формат"),m("not-numbers",(t=>!/[0-9]/.test(t)),"Неверный формат");const v=Symbol.for("formfather.shared"),E=null!==(b=globalThis[v])&&void 0!==b?b:globalThis[v]={defaultParams:{},defaultValidationSchema:void 0};let w=0;const S=new WeakMap,F={tel:{selector:'input[type="tel"]',rules:["tel"]},email:{selector:'input[type="email"]',rules:["email"]},required:{selector:"[required]",rules:["required"]},url:{selector:'input[type="url"]',rules:["url"]},"not-numbers":{selector:'input[data-validate="not-numbers"]',rules:["not-numbers"]}};class A{static get defaultParams(){return E.defaultParams}static set defaultParams(t){E.defaultParams=t}static get defaultValidationSchema(){var t;return null!==(t=E.defaultValidationSchema)&&void 0!==t?t:E.defaultValidationSchema=F}static set defaultValidationSchema(t){E.defaultValidationSchema=t}constructor(t,e={}){this.$submits=[],this.waitResponse=!1,this.inputs=null,this.fieldEventHandlers=[],this.liveValidationTimers=new WeakMap,this.fieldValidationTokens=new WeakMap,this.fieldValidationTokenCounter=0,this.errors=[],this._anim={duration:220,easing:"ease",paddingTopOpened:16,iconDuration:700},this.$el=t,this.options=e;this.config=Object.assign({},{onSubmit:()=>{},onResponse:()=>{},onResponseSuccess:()=>{},onResponseUnsuccess:()=>{},customTypeError:null,loaderColor:"#fff",showLoaderButton:!0,scrollToFirstErroredInput:!0,focusFirstErroredInput:!1,logging:!1,inputSelector:".input",inputWrapperSelector:".input-primary",validateOn:"submit",revalidateOn:["input","change"],validationDebounce:0,errorContainerAttribute:"data-error-container",validationStateAttribute:"data-form-father-state",observeMutations:!1},A.defaultParams,e),this.$el?(this.$submits=this.findSubmitElements(),this.isCorrectArguments()&&(this.bindSubmitHandlers(),this.initialization(),this.bindFieldValidationEvents(),this.setupMutationObserver(),S.set(this.$el,this))):console.warn("Empty $el")}getOptions(){return this.options}static initAll(t="form[data-form-father]",e={}){return"undefined"==typeof document?[]:Array.from(document.querySelectorAll(t)).filter((t=>"FORM"===t.tagName)).map((t=>{const i=S.get(t);return i?(i.updateOptions(e),i):new A(t,e)}))}updateOptions(t){return this.options=Object.assign(Object.assign({},this.options),t),this.config=Object.assign(Object.assign({},this.config),t),this.refreshControls(),this.bindSubmitHandlers(),this.bindFieldValidationEvents(),this.setupMutationObserver(),this}getErrors(){return this.errors.map((t=>Object.assign({},t)))}getValues(){return d(this.$el)}normalizeValueList(t){return Array.isArray(t)?t.map(String):[String(t)]}setValues(t){return this.getAllFields().forEach((e=>{if(!e.name||!Object.prototype.hasOwnProperty.call(t,e.name))return;const i=t[e.name];if(!(e instanceof HTMLInputElement&&"file"===e.type)){if(e instanceof HTMLInputElement&&"checkbox"===e.type)e.checked="boolean"==typeof i?i:this.normalizeValueList(i).includes(e.value);else if(e instanceof HTMLInputElement&&"radio"===e.type)e.checked=String(i)===e.value;else if(e instanceof HTMLSelectElement&&e.multiple){const t=this.normalizeValueList(i);Array.from(e.options).forEach((e=>{e.selected=t.includes(e.value)}))}else e.value=null==i?"":String(i);e.dispatchEvent(new Event("input",{bubbles:!0})),e.dispatchEvent(new Event("change",{bubbles:!0}))}})),this}clearErrors(){return this.errors=[],this.getAllFields().forEach((t=>{this.hideError(t),this.clearFieldValidationState(t)})),this.hideErrorForm(),this}reset(t={}){return this.$el.reset(),this.refreshControls(),!1!==t.clearErrors&&this.clearErrors(),this}static setDefaultParams(t){this.defaultParams=Object.assign(Object.assign({},this.defaultParams),t)}refreshControls(){this.inputs=this.$el.querySelectorAll(this.config.inputSelector)}findSubmitElements(){return Array.from(this.$el.querySelectorAll('input[type="submit"], input[type="image"], button[type="submit"], button:not([type])'))}bindSubmitHandlers(){this._onSubmitClickHandler||(this._onSubmitClickHandler=t=>e(this,void 0,void 0,(function*(){yield this.handleSubmitEvent(t)}))),this.$submits.forEach((t=>{t.removeEventListener("click",this._onSubmitClickHandler)})),this.$submits=this.findSubmitElements(),this.$submits.forEach((t=>{t.addEventListener("click",this._onSubmitClickHandler)}))}initialization(){this.refreshControls(),this._onSubmitHandler=t=>e(this,void 0,void 0,(function*(){yield this.handleSubmitEvent(t)})),this.$el.addEventListener("submit",this._onSubmitHandler)}handleSubmitEvent(t){return e(this,void 0,void 0,(function*(){var e,i,r,n,o,s;t.preventDefault(),null===(i=(e=this.config).onBeforeValidate)||void 0===i||i.call(e,this);const a=yield this.validate();return null===(n=(r=this.config).onAfterValidate)||void 0===n||n.call(r,a,this),a?this.submit():(null===(s=(o=this.config).onValidationError)||void 0===s||s.call(o,this.getErrors(),this),{success:!1,error:this.getErrors()})}))}normalizeTriggers(t,e){const i=null==t?e:Array.isArray(t)?t:[t];return Array.from(new Set(i))}clearFieldValidationEvents(){this.fieldEventHandlers.forEach((({$input:t,event:e,handler:i})=>{t.removeEventListener(e,i)})),this.fieldEventHandlers=[]}bindFieldValidationEvents(){this.clearFieldValidationEvents();const t=this.normalizeTriggers(this.config.validateOn,["submit"]).filter((t=>"submit"!==t)),e=this.normalizeTriggers(this.config.revalidateOn,["input","change"]).filter((t=>"submit"!==t)),i=Array.from(new Set([...t,...e]));if(0===i.length)return;this.getAllFields().forEach((e=>{i.forEach((i=>{const r=()=>{(t.includes(i)||this.hasFieldError(e))&&this.scheduleFieldValidation(e)};e.addEventListener(i,r),this.fieldEventHandlers.push({$input:e,event:i,handler:r})}))}))}scheduleFieldValidation(t){const e=this.liveValidationTimers.get(t);e&&clearTimeout(e);const i=Math.max(0,Number(this.config.validationDebounce)||0),r=()=>{this.validateField(t)};if(0===i)return void r();const n=setTimeout(r,i);this.liveValidationTimers.set(t,n)}setupMutationObserver(){var t;null===(t=this.mutationObserver)||void 0===t||t.disconnect(),this.mutationObserver=void 0,this.config.observeMutations&&"undefined"!=typeof MutationObserver&&(this.mutationObserver=new MutationObserver((()=>{this.refreshControls(),this.bindSubmitHandlers(),this.bindFieldValidationEvents()})),this.mutationObserver.observe(this.$el,{childList:!0,subtree:!0}))}getErrorContainer(t){const e=this.config.errorContainerAttribute||"data-error-container",i=t.getAttribute(e);if(!i)return null;try{return document.querySelector(i)}catch(t){return console.warn(`[FormFather] Invalid error container selector "${i}"`),null}}showError(t,e){t.setAttribute("aria-invalid","true");const r=this.getErrorContainer(t);if(r)return r.textContent=e,r.hasAttribute("role")||r.setAttribute("role","alert"),void r.removeAttribute("hidden");const n=i(t,this.config.inputWrapperSelector);if(!n)return void console.warn(`Не найдена обёртка для поля с именем: ${t.name||t.type}`);const o=n.showError;if("function"==typeof o)return void o.call(n,e);n.classList.add("input__wrapper--error");let s=n.querySelector("[data-form-father-error]");s||(s=document.createElement("div"),s.setAttribute("data-form-father-error",""),s.setAttribute("role","alert"),t.insertAdjacentElement("afterend",s)),s.textContent=e}showLoader(){this.$submits.forEach((t=>{if(t.classList.add("button--loading"),t.querySelector(".button__loader"))return;const e="form-father-loader-"+w++,i=`\n\t\t\t\t<svg height="38" viewBox="0 0 38 38" width="38" xmlns="http://www.w3.org/2000/svg">\n\t\t\t\t\t<defs>\n\t\t\t\t\t<linearGradient id="${e}" x1="8.042%" x2="65.682%" y1="0%" y2="23.865%">\n\t\t\t\t\t\t<stop offset="0%" stop-color="${this.config.loaderColor}" stop-opacity="0"/>\n\t\t\t\t\t\t<stop offset="63.146%" stop-color="${this.config.loaderColor}" stop-opacity=".631"/>\n\t\t\t\t\t\t<stop offset="100%" stop-color="${this.config.loaderColor}"/>\n\t\t\t\t\t</linearGradient>\n\t\t\t\t\t</defs>\n\t\t\t\t\t<g fill-rule="evenodd" fill="none">\n\t\t\t\t\t<g transform="translate(1 1)">\n\t\t\t\t\t\t<path d="M36 18c0-9.94-8.06-18-18-18" id="Oval-2" stroke-width="5" stroke="url(#${e})"></path>\n\t\t\t\t\t\t<circle cx="36" cy="18" fill="${this.config.loaderColor}" r="1"></circle>\n\t\t\t\t\t</g>\n\t\t\t\t\t</g>\n\t\t\t\t</svg>\n\t\t\t`,r=document.createElement("span");r.innerHTML=i,r.className="button__loader",t.insertAdjacentElement("afterbegin",r),requestAnimationFrame((()=>{r.setAttribute("data-showed","")}))}))}hideLoader(){this.$submits.forEach((t=>{t.classList.remove("button--loading");const e=t.querySelector(".button__loader");e&&(e.classList.remove("active"),e.removeAttribute("data-showed"),setTimeout((()=>{e.remove()}),250))}))}hideError(t){var e;const r=i(t,this.config.inputWrapperSelector)||t.parentElement,n=this.getErrorContainer(t);t.removeAttribute("aria-invalid"),n&&(n.textContent="",n.setAttribute("hidden","")),null===(e=null==r?void 0:r.querySelector("[data-form-father-error]"))||void 0===e||e.remove(),null==r||r.classList.remove("input__wrapper--error"),r&&r.classList.contains("input__wrapper")&&r.getAttribute("data-type-error")}scrollToFirstErroredInput(t){if(t.length>0){const e=Array.from(this.$el.querySelectorAll("input, textarea, select"));let i=null,r=1/0;if(t.forEach((t=>{const n=e.findIndex((e=>e===t));n<r&&(r=n,i=t)})),i){const t=i.getBoundingClientRect(),e=window.pageYOffset||document.documentElement.scrollTop,r=t.top+e-window.innerHeight/2;document.documentElement.setAttribute("data-now-scrolling",""),window.scrollTo({top:r,behavior:"smooth"}),setTimeout((()=>{document.documentElement.removeAttribute("data-now-scrolling")}),800)}}}focusFirstErroredInput(t){var e;if(0===t.length)return;const i=Array.from(this.$el.querySelectorAll("input, textarea, select")),r=t.slice().sort(((t,e)=>i.indexOf(t)-i.indexOf(e)))[0];null===(e=null==r?void 0:r.focus)||void 0===e||e.call(r,{preventScroll:!0})}getValidationSchema(){return Object.assign(Object.assign({},A.defaultValidationSchema||{}),this.config.validationSchema||{})}getAllFields(t=this.$el){return Array.from(t.querySelectorAll("input, textarea, select"))}getFieldKey(t){return t.name||t.id||t.getAttribute("data-field")||t.type||"field"}hasFieldError(t){const e=this.getFieldKey(t);return"true"===t.getAttribute("aria-invalid")||this.errors.some((t=>t.field===e))}clearFieldErrorRecord(t){const e=this.getFieldKey(t);this.errors=this.errors.filter((t=>t.field!==e))}getValidationStateAttribute(){return this.config.validationStateAttribute||"data-form-father-state"}setFieldValidationState(t,e){const i=this.getValidationStateAttribute();e?t.setAttribute(i,e):t.removeAttribute(i)}startFieldValidation(t){const e=++this.fieldValidationTokenCounter;return this.fieldValidationTokens.set(t,e),t.setAttribute("aria-busy","true"),this.setFieldValidationState(t,"validating"),e}isLatestFieldValidation(t,e){return null==e||this.fieldValidationTokens.get(t)===e}finishFieldValidation(t,e,i){this.isLatestFieldValidation(t,e)&&(t.removeAttribute("aria-busy"),this.setFieldValidationState(t,i?"valid":"invalid"))}clearFieldValidationState(t){this.fieldValidationTokens.delete(t),t.removeAttribute("aria-busy"),this.setFieldValidationState(t,null)}setValidationError(t,e,i,r){this.clearFieldErrorRecord(t),this.errors.push({field:this.getFieldKey(t),rule:e,message:i,source:r})}resolveField(t){return"string"!=typeof t?t:this.findFieldByName(t)}parseRuleString(t){const[e,...i]=t.split(":").map((t=>t.trim()));return i.length>0?{rule:e,params:1===i.length?i[0]:i}:e}parseValidationAttribute(t){return(null!=t?t:"").split(/[,\s|]+/).map((t=>t.trim())).filter(Boolean).map((t=>this.parseRuleString(t)))}getRuleName(t){return"string"==typeof t?t:t.rule}getRuleIdentity(t){return JSON.stringify(t)}collectRules(t,e=[],i=new Set){const r=[];t.hasAttribute("required")&&r.push("required"),r.push(...e);const n=this.parseValidationAttribute(t.getAttribute("data-validate")).filter((t=>{const e=this.getRuleName(t);return f(e)||!i.has(e)}));r.push(...n),r.push(...this.parseValidationAttribute(t.getAttribute("data-custom-validate")));const o=[],s=new Set;for(const t of r){const e=this.getRuleName(t);if(!f(e)){console.warn(`[FormFather] Unknown validation rule "${e}"`);continue}const i=this.getRuleIdentity(t);s.has(i)||(s.add(i),o.push(t))}return o}getRuleMessage(t,e,i,r,n){const o=t.getAttribute(`data-error-${e}`);return n||o||i[e]||r}fieldMatchesSchemaDefinition(t,e,i){if(i.selector)try{return t.matches(i.selector)}catch(t){return console.warn(`[FormFather] Invalid validation selector "${i.selector}"`),!1}return this.parseValidationAttribute(t.getAttribute("data-validate")).some((t=>this.getRuleName(t)===e))}getFieldValidationConfig(t,e=this.getValidationSchema()){const i=[],r={};for(const[n,o]of Object.entries(e))this.fieldMatchesSchemaDefinition(t,n,o)&&(i.push(...o.rules),Object.assign(r,o.messages||{}));return{rules:this.collectRules(t,i,new Set(Object.keys(e))),messages:r}}getValidationFields(t,e){const i=new Set,r=t=>{Array.from(t).forEach((t=>{(t instanceof HTMLInputElement||t instanceof HTMLTextAreaElement||t instanceof HTMLSelectElement)&&i.add(t)}))};for(const[n,o]of Object.entries(e))if(o.selector)try{r(t.querySelectorAll(o.selector))}catch(t){console.warn(`[FormFather] Invalid validation selector "${o.selector}"`)}else this.getAllFields(t).filter((t=>this.parseValidationAttribute(t.getAttribute("data-validate")).some((t=>this.getRuleName(t)===n)))).forEach((t=>i.add(t)));return r(t.querySelectorAll("[required], [data-validate], [data-custom-validate]")),Array.from(i)}validateInput(t,i){return e(this,arguments,void 0,(function*(t,e,i={},r=this.$el,n){if(t.hasAttribute("data-no-validate"))return!0;const o=t instanceof HTMLInputElement&&("checkbox"===t.type||"radio"===t.type)?!t.checked:0===t.value.trim().length,s=e.some((t=>"required"===this.getRuleName(t)));if(o&&!s)return this.isLatestFieldValidation(t,n)?(this.clearFieldErrorRecord(t),this.hideError(t),!0):!this.hasFieldError(t);for(const o of e){const{rule:e,params:s}="string"==typeof o?{rule:o,params:void 0}:{rule:o.rule,params:o.params},a=f(e);if(!a)continue;const l=yield a.fn(t.value,t,r,s),d="object"==typeof l&&null!==l,c=d?!0===l.valid:!!l;if(!this.isLatestFieldValidation(t,n))return!this.hasFieldError(t);if(d&&l.effect&&(yield l.effect({value:t.value,$input:t,$form:r,params:s})),!this.isLatestFieldValidation(t,n))return!this.hasFieldError(t);if(!c){const r=l,n=this.getRuleMessage(t,e,i,a.defaultMessage,d&&"string"==typeof r.message?r.message:void 0);return this.setValidationError(t,e,n,"client"),this.showError(t,n),!1}if(d&&l.stopOthers)return!0}return this.isLatestFieldValidation(t,n)?(this.clearFieldErrorRecord(t),this.hideError(t),!0):!this.hasFieldError(t)}))}runFieldValidation(t,i){return e(this,arguments,void 0,(function*(t,e,i={},r=this.$el){const n=this.startFieldValidation(t);try{const o=yield this.validateInput(t,e,i,r,n);return this.finishFieldValidation(t,n,o),o}catch(e){throw this.isLatestFieldValidation(t,n)&&this.clearFieldValidationState(t),e}}))}getRadioGroup(t,e){return t instanceof HTMLInputElement&&"radio"===t.type&&t.name?Array.from(e.querySelectorAll('input[type="radio"]')).filter((e=>e.name===t.name)):[]}showFieldError(t,e,i="manual"){const r=this.resolveField(t);return r?(this.setValidationError(r,i,e,i),this.showError(r,e),r.removeAttribute("aria-busy"),this.setFieldValidationState(r,"invalid"),!0):(console.warn(`[FormFather] Field "${String(t)}" not found`),!1)}validateField(t){return e(this,void 0,void 0,(function*(){const e=this.resolveField(t);if(!e)return console.warn(`[FormFather] Field "${String(t)}" not found`),!1;const{rules:i,messages:r}=this.getFieldValidationConfig(e);return this.runFieldValidation(e,i,r)}))}validate(){return e(this,arguments,void 0,(function*(t=this.$el){const e=this.getValidationSchema(),i=[],r=new WeakSet;let n=!0;this.errors=[];for(const o of this.getValidationFields(t,e)){if(r.has(o))continue;const s=this.getRadioGroup(o,t);s.length>0?s.forEach((t=>r.add(t))):r.add(o);const{rules:a,messages:l}=this.getFieldValidationConfig(o,e);(yield this.runFieldValidation(o,a,l,t))||(n=!1,i.push(o))}const o=i.filter((t=>!t.hasAttribute("data-no-error-scroll")));return!n&&this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(o),!n&&this.config.focusFirstErroredInput&&this.focusFirstErroredInput(o),n}))}disableSubmit(){this.$submits.forEach((t=>{t.setAttribute("disabled","")}))}enableSubmit(){this.$submits.forEach((t=>{t.removeAttribute("disabled")}))}clearInputs(){var t;null===(t=this.inputs)||void 0===t||t.forEach((t=>{var e;const i=t;"radio"===i.type||"checkbox"===i.type?i.checked=!1:(i.value="",null===(e=i.parentElement)||void 0===e||e.classList.remove("filled"),i.dispatchEvent(new Event("input")))}))}_prefersReducedMotion(){var t;return"undefined"!=typeof window&&(null===(t=window.matchMedia)||void 0===t?void 0:t.call(window,"(prefers-reduced-motion: reduce)").matches)}_canAnimate(t){return!(!t||!("animate"in t)||this._prefersReducedMotion())}_cancelAnimations(t){var e;const i=t;null===(e=null==i?void 0:i.getAnimations)||void 0===e||e.call(i).forEach((t=>t.cancel()))}showErrorForm(t){var e;const i=this.$el.querySelectorAll(this.config.inputWrapperSelector),r=i[i.length-1];let n=this.$el.querySelector(".error-block-under-input__wrapper");!n||r&&r.nextElementSibling!==n?(n=document.createElement("div"),n.className="error-block-under-input__wrapper",n.innerHTML='\n <div class="error-block-under-input error-block-under-input--warning" style="display:grid;grid-template-columns:auto 1fr;gap:8px;align-items:start;">\n <div class="error-block-under-input__icon-wrapper" style="position:relative;width:20px;height:20px;">\n <span class="error-block-under-input__icon" style="display:block;width:20px;height:20px;"></span>\n <span class="error-block-under-input__icon error-block-under-input__icon--animated" style="display:block;position:absolute;inset:0;"></span>\n </div>\n <p class="error-block-under-input__text" style="margin:0;">\n <span class="error-block-under-input__main-text"></span>\n <span class="error-block-under-input__secondary-text"></span>\n </p>\n </div>\n ',Object.assign(n.style,{boxSizing:"border-box",overflow:"hidden",height:"0px",paddingTop:"0px",opacity:"0"}),r?r.insertAdjacentElement("afterend",n):this.$el.appendChild(n)):(n.style.boxSizing="border-box",n.style.overflow="hidden"),n.querySelector(".error-block-under-input__main-text").textContent=t;const o=n.querySelector(".error-block-under-input__secondary-text");o&&(o.textContent=""),null===(e=n.querySelector(".error-block-under-input"))||void 0===e||e.classList.remove("error-block-under-input--success");const s=n.style.height,a=n.style.paddingTop;n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`;const l=n.clientHeight;if(n.style.height=s||"0px",n.style.paddingTop=a||"0px",parseFloat(n.style.height)===l&&1===parseFloat(n.style.opacity||"1"))this._ringIcon(n);else{if(this._cancelAnimations(n),this._canAnimate(n)){n.animate([{height:`${parseFloat(n.style.height)||0}px`,paddingTop:`${parseFloat(n.style.paddingTop)||0}px`,opacity:parseFloat(n.style.opacity||"0")},{height:`${l}px`,paddingTop:`${this._anim.paddingTopOpened}px`,opacity:1}],{duration:this._anim.duration,easing:this._anim.easing,fill:"forwards"}).addEventListener("finish",(()=>{n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`,n.style.opacity="1"}))}else n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`,n.style.opacity="1";this._ringIcon(n)}this.$el.addEventListener("input",(()=>this.hideErrorForm()),{passive:!0,once:!0})}hideErrorForm(){const t=this.$el.querySelectorAll(this.config.inputWrapperSelector),e=t[t.length-1],i=(null==e?void 0:e.nextElementSibling)||this.$el.querySelector(".error-block-under-input__wrapper");if(!i||!i.classList.contains("error-block-under-input__wrapper"))return;if((""===i.style.height||"auto"===i.style.height)&&(i.style.height=`${i.clientHeight}px`),i.style.paddingTop=i.style.paddingTop||`${this._anim.paddingTopOpened}px`,i.style.opacity=i.style.opacity||"1",this._cancelAnimations(i),this._canAnimate(i)){i.animate([{height:`${parseFloat(i.style.height)||i.clientHeight}px`,paddingTop:`${parseFloat(i.style.paddingTop)||this._anim.paddingTopOpened}px`,opacity:parseFloat(i.style.opacity||"1")},{height:"0px",paddingTop:"0px",opacity:0}],{duration:this._anim.duration,easing:this._anim.easing,fill:"forwards"}).addEventListener("finish",(()=>{i.remove()}))}else i.remove()}_ringIcon(t){const e=t.querySelector(".error-block-under-input__icon--animated");e&&(this._cancelAnimations(e),this._canAnimate(e)&&e.animate([{transform:"rotate(0deg) scale(1)",opacity:0},{transform:"rotate(-8deg) scale(1.05)",opacity:1,offset:.15},{transform:"rotate(8deg) scale(1.05)",opacity:1,offset:.3},{transform:"rotate(-6deg) scale(1.03)",opacity:1,offset:.45},{transform:"rotate(6deg) scale(1.03)",opacity:1,offset:.6},{transform:"rotate(0deg) scale(1)",opacity:0}],{duration:this._anim.iconDuration,easing:"ease"}))}destroy(){var t;this._onSubmitHandler&&(this.$el.removeEventListener("submit",this._onSubmitHandler),this._onSubmitHandler=void 0),this._onSubmitClickHandler&&(this.$submits.forEach((t=>{t.removeEventListener("click",this._onSubmitClickHandler)})),this._onSubmitClickHandler=void 0),this.clearFieldValidationEvents(),null===(t=this.mutationObserver)||void 0===t||t.disconnect(),this.mutationObserver=void 0;const e=this.$el.querySelector(".error-block-under-input__wrapper");e&&(this._cancelAnimations(e),e.remove()),this.$submits.forEach((t=>{t.classList.remove("button--loading");const e=t.querySelector(".button__loader");e&&e.remove()}));this.$el.querySelectorAll("input, textarea, select").forEach((t=>{this.hideError(t),this.clearFieldValidationState(t)})),document.documentElement.removeAttribute("data-now-scrolling"),this.inputs=null,this.$submits=[],this.errors=[],this.waitResponse=!1,S.delete(this.$el)}isCorrectArguments(){return"FORM"!==this.$el.tagName?(console.warn("$el не является тегом form"),!1):0!==this.$submits.length||(console.warn("В форме нет кнопки с типом submit"),!1)}formDataToObject(t){const e={};return t.forEach(((t,i)=>{Object.prototype.hasOwnProperty.call(e,i)?(Array.isArray(e[i])||(e[i]=[e[i]]),e[i].push(t)):e[i]=t})),e}appendFormDataValue(t,e,i){null!=i&&(Array.isArray(i)?i.forEach(((i,r)=>{this.appendFormDataValue(t,`${e}[${r}]`,i)})):i instanceof Blob?t.append(e,i):t.append(e,"object"==typeof i?JSON.stringify(i):String(i)))}objectToFormData(t){const e=new FormData;return Object.entries(t).forEach((([t,i])=>{this.appendFormDataValue(e,t,i)})),e}stringifyFormDataValue(t){return"string"==typeof t?t:t.name}formDataToUrlSearchParams(t){const e=new URLSearchParams;return t.forEach(((t,i)=>{e.append(i,this.stringifyFormDataValue(t))})),e}formDataToTextBody(t){return Array.from(t).map((([t,e])=>`${encodeURIComponent(t)}=${encodeURIComponent(this.stringifyFormDataValue(e))}`)).join("\n")}isBodylessMethod(t){return"GET"===t||"HEAD"===t}appendQueryString(t,e){const i=e.toString();if(!i)return t;const r=t.includes("?")?"&":"?";return`${t}${r}${i}`}findFieldByName(t){return Array.from(this.$el.querySelectorAll("input, textarea, select")).find((e=>e.name===t))||null}parseResponseBody(t){return e(this,void 0,void 0,(function*(){try{const e=yield t.json();return e&&"object"==typeof e?e:{success:!1,error:!0,"error-msg":"Некорректный ответ сервера"}}catch(t){return{success:!1,error:!0,"error-msg":"Некорректный ответ сервера"}}}))}handleUnsuccessfulResponse(t){if(!0===t.error&&(t["error-msg"]&&this.showErrorForm(t["error-msg"]),t.errors)){const e=[];t.errors.forEach((t=>{const i=this.findFieldByName(t.name);i?(e.push(i),this.setValidationError(i,"server",t["error-msg"],"server"),this.showError(i,t["error-msg"]),i.removeAttribute("aria-busy"),this.setFieldValidationState(i,"invalid")):console.warn(`Не найдено поле с именем: ${t.name}, для вывода ошибки: ${t["error-msg"]}`)})),!0===this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(e),!0===this.config.focusFirstErroredInput&&this.focusFirstErroredInput(e)}}sendData(){return e(this,void 0,void 0,(function*(){var t;const e=this.$el.getAttribute("action")||"",i=(null===(t=this.$el.getAttribute("method"))||void 0===t?void 0:t.toUpperCase())||"POST",r=this.$el.getAttribute("enctype")||"application/x-www-form-urlencoded";let n=null,o={};const s=a(this.$el);let l=this.formDataToObject(s);if(this.config.logging)for(const t of s.keys())console.log(s.get(t));const d="function"==typeof this.config.wrapData;"function"==typeof this.config.wrapData&&(l=this.config.wrapData(l));const c=d?this.objectToFormData(l):s;if(this.isBodylessMethod(i))return fetch(this.appendQueryString(e,this.formDataToUrlSearchParams(c)),{method:i,headers:o});switch(r){case"application/x-www-form-urlencoded":n=this.formDataToUrlSearchParams(c).toString(),o["Content-Type"]="application/x-www-form-urlencoded";break;case"multipart/form-data":n=c;break;case"text/plain":n=this.formDataToTextBody(c),o["Content-Type"]="text/plain";break;case"application/json":n=JSON.stringify(l),o["Content-Type"]="application/json";break;default:console.warn(`Неизвестный enctype: ${r}. Используется application/x-www-form-urlencoded.`),n=this.formDataToUrlSearchParams(c).toString(),o["Content-Type"]="application/x-www-form-urlencoded"}return fetch(e,{method:i,body:n,headers:o})}))}submit(){return e(this,void 0,void 0,(function*(){var t,e,i,r,n,o,s,a,d,c,u,h;if(!this.waitResponse){let p=!1;const f=()=>{p||(this.waitResponse=!1,!0===this.config.showLoaderButton&&this.hideLoader(),p=!0)};try{if(!1===(yield null===(e=(t=this.config).onBeforeSubmit)||void 0===e?void 0:e.call(t,this)))return{success:!1};!0===this.config.showLoaderButton&&this.showLoader(),null===(r=(i=this.config).onSubmit)||void 0===r||r.call(i,this),this.waitResponse=!0;const u=yield this.sendData(),h=yield this.parseResponseBody(u);return f(),null===(o=(n=this.config).onResponse)||void 0===o||o.call(n,h,this),u.ok&&!0===h.success?null===(a=(s=this.config).onResponseSuccess)||void 0===a||a.call(s,h,this):(null===(c=(d=this.config).onResponseUnsuccess)||void 0===c||c.call(d,h,this),this.handleUnsuccessfulResponse(h)),l(h),{success:u.ok&&!0===h.success,response:u,responseBody:h}}catch(t){throw f(),null===(h=(u=this.config).onSubmitError)||void 0===h||h.call(u,t,this),t}}}))}}return t.blockScrollBody=function(){window.lastBlockScrollPosition=window.pageYOffset,document.body.style.top=-window.lastBlockScrollPosition+"px",document.body.classList.add("body--block-scroll")},t.closest=i,t.createFieldValidator=function(t,i){return(r,n,o,s)=>e(this,void 0,void 0,(function*(){return!!(yield t(r,n,o,s))||{valid:!1,message:i}}))},t.createSchemaValidator=y,t.default=A,t.getAllValidators=function(){return new Map(h)},t.getValidator=f,t.isEmailValid=function(t){return/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu.test(t)},t.isPhoneValid=function(t){const e=[];return Array.from(t).forEach((t=>{(Number(t)||0===Number(t))&&e.push(t)})),!(e.length<11||e.length<12&&"+"===t[1])},t.isUrlValid=s,t.parseCommonResponseProperties=l,t.registerSchemaValidator=function(t,e,i,r={}){const{override:n}=r;p(t,y(e,function(t,e){var i={};for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&e.indexOf(r)<0&&(i[r]=t[r]);if(null!=t&&"function"==typeof Object.getOwnPropertySymbols){var n=0;for(r=Object.getOwnPropertySymbols(t);n<r.length;n++)e.indexOf(r[n])<0&&Object.prototype.propertyIsEnumerable.call(t,r[n])&&(i[r[n]]=t[r[n]])}return i}(r,["override"])),i,{override:n})},t.registerValidator=p,t.serializeFormToJSON=d,t.serializeToFormData=a,t.unblockScrollBody=function(){document.body.classList.remove("body--block-scroll"),document.body.style.top="",window.scrollTo(0,window.lastBlockScrollPosition||0)},Object.defineProperty(t,"__esModule",{value:!0}),t}({});
1
+ var FormFather=function(t){"use strict";function e(t,e,r,i){return new(r||(r=Promise))((function(n,o){function s(t){try{l(i.next(t))}catch(t){o(t)}}function a(t){try{l(i.throw(t))}catch(t){o(t)}}function l(t){var e;t.done?n(t.value):(e=t.value,e instanceof r?e:new r((function(t){t(e)}))).then(s,a)}l((i=i.apply(t,e||[])).next())}))}function r(t,e){if(null==t)return null;let r=t;for(;r;){if(r.matches(e))return r;r=r.parentElement}return null}"function"==typeof SuppressedError&&SuppressedError;const i=["http:","https:"];function n(t){return t.length>0&&t.length<=63&&/^[a-z\d-]+$/i.test(t)&&!t.startsWith("-")&&!t.endsWith("-")}function o(t){return!!t&&("localhost"===t||function(t){const e=t.split(".");return 4===e.length&&e.every((t=>{if(!/^\d{1,3}$/.test(t))return!1;const e=Number(t);return e>=0&&e<=255}))}(t)||function(t){if(!t.startsWith("[")||!t.endsWith("]"))return!1;const e=t.slice(1,-1);return e.includes(":")&&/^[\da-f:.]+$/i.test(e)}(t)||function(t){const e=t.split(".");return e.length>=2&&e.every(n)}(t))}function s(t){const e=function(t){const e=t.trim();return 0===e.length?"":e.startsWith("//")?`https:${e}`:/^[a-z][a-z\d+.-]*:\/\//i.test(e)?e:`https://${e}`}(t);if(0===e.length)return!1;try{const t=new URL(e);return i.includes(t.protocol)&&o(t.hostname)}catch(t){return!1}}function a(t){const e="FORM"===t.tagName?Array.from(t.elements):Array.from(t.querySelectorAll("input, select, textarea, button")),r=new FormData;return e.forEach((t=>{if(!(t instanceof HTMLInputElement||t instanceof HTMLSelectElement||t instanceof HTMLTextAreaElement||t instanceof HTMLButtonElement))return;if(!t.name||t.disabled||t.hasAttribute("data-no-serialize"))return;if(t instanceof HTMLButtonElement)return;const{name:e,type:i}=t;if("file"===i)if(t.multiple){const i=t.files;if(i)for(let t=0;t<i.length;t++)r.append(`${e}[${t}]`,i[t])}else{const i=t.files;i&&i[0]&&r.append(e,i[0])}else{let n;if("select-one"===i||"select-multiple"===i){const o=t;if("select-multiple"===i){Array.from(o.selectedOptions).map((t=>t.value)).forEach(((t,i)=>{r.append(`${e}[${i}]`,t)}))}else n=o.value,n&&r.append(e,n)}else if("tel"===i)n=t.value.split(/[\s()-]/).join("").replace(/^8/,"+7");else if("checkbox"===i||"radio"===i){if(!t.checked)return;n=t.value}else if("date"===i){const e=t.getAttribute("data-date-format");n=e?function(t,e){const r=new Date(t),i=t=>t.toString().padStart(2,"0"),n=i(r.getDate()),o=i(r.getMonth()+1),s=r.getFullYear().toString();return e.replace("dd",n).replace("mm",o).replace("yyyy",s)}(t.value,e):t.value}else n=t.value;n&&"select-one"!==i&&"select-multiple"!==i&&r.append(e,n)}})),r}function l(t){if(Object.prototype.hasOwnProperty.call(t,"redirect-url"))if(t["redirect-url-delay"]){const e=Number(t["redirect-url-delay"]);setTimeout((()=>{window.location.href=t["redirect-url"]}),e)}else window.location.href=t["redirect-url"];else if(Object.prototype.hasOwnProperty.call(t,"reload")){if(!0===t.reload)if(t["reload-delay"]){const e=Number(t["reload-delay"]);setTimeout((()=>{window.location.reload()}),e)}else window.location.reload()}else if(Object.prototype.hasOwnProperty.call(t,"toast")){const e=t.toast;(Array.isArray(e)?e:[e]).forEach((t=>{window.showToast(t)}))}else Object.prototype.hasOwnProperty.call(t,"error-toast")&&window.showToast(t["error-toast"])}function d(t){const e={};return new FormData(t).forEach(((t,r)=>{e.hasOwnProperty(r)?(Array.isArray(e[r])||(e[r]=[e[r]]),e[r].push(t)):e[r]=t})),e}var c;const u=Symbol.for("FormFather.validators"),h=null!==(c=globalThis[u])&&void 0!==c?c:globalThis[u]=new Map;function p(t,e,r,{override:i=!1}={}){!h.has(t)||i?h.set(t,{fn:e,defaultMessage:r}):console.warn(`[FormFather] Validator "${t}" already exists; pass { override: true } to replace`)}const f=t=>h.get(t);function m(t,e,r){h.has(t)||h.set(t,{fn:e,defaultMessage:r})}function g(t,e){return"function"==typeof e?e(t):e||function(t){var e;if(!t||"object"!=typeof t)return;const r=t.issues||t.errors;if(Array.isArray(r)){const t=null===(e=r.find((t=>"string"==typeof(null==t?void 0:t.message))))||void 0===e?void 0:e.message;if(t)return t}return"string"==typeof t.message?t.message:void 0}(t)}function y(t,r={}){return(i,n,o,s)=>e(this,void 0,void 0,(function*(){const e=r.mapValue?r.mapValue(i,n,o,s):i;try{if("function"==typeof t)return a=yield t(e),l=g(void 0,r.message),"object"==typeof a&&null!==a?a:void 0===a||!!a||{valid:!1,message:l};if("function"==typeof t.safeParse){const i=yield t.safeParse(e);return!!i.success||{valid:!1,message:g(i.error||i,r.message)}}if("function"==typeof t.parse)return yield t.parse(e),!0}catch(t){return{valid:!1,message:g(t,r.message)}}var a,l;return{valid:!1,message:g(void 0,r.message)}}))}function v(t,r){return(i,n,o,s)=>e(this,void 0,void 0,(function*(){return!!(yield t(i,n,o,s))||{valid:!1,message:r}}))}var b;m("required",((t,e,r)=>{if(e&&e.type){if("checkbox"===e.type)return e.checked;if("radio"===e.type){const t=r.querySelectorAll(`input[type="radio"][name="${e.name}"]`);return Array.from(t).some((t=>t.checked))}}return t.trim().length>0}),"Пустое значение"),m("email",(t=>/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)),"Неверный формат"),m("tel",(t=>/^\+7\d{10}$/.test(t)),"Неверный формат"),m("url",(t=>s(t)),"Неверный формат"),m("not-numbers",(t=>!/[0-9]/.test(t)),"Неверный формат");const E=Symbol.for("formfather.shared"),w=null!==(b=globalThis[E])&&void 0!==b?b:globalThis[E]={defaultParams:{},defaultValidationSchema:void 0},S="_form";function F(t,e){return t[e]}function A(t){return Array.isArray(t)?0===t.length:null==t||0===String(t).trim().length}function V(t,r){return i=>e(this,void 0,void 0,(function*(){return!!(yield t(i))||function(t,r,i,n){return e(this,void 0,void 0,(function*(){const e="function"==typeof r?yield r(t):r;return"string"==typeof e?{field:i,rule:n,message:e}:Object.assign({field:i,rule:n},e)}))}(i,r)}))}let $=0;const T=new WeakMap,O={tel:{selector:'input[type="tel"]',rules:["tel"]},email:{selector:'input[type="email"]',rules:["email"]},required:{selector:"[required]",rules:["required"]},url:{selector:'input[type="url"]',rules:["url"]},"not-numbers":{selector:'input[data-validate="not-numbers"]',rules:["not-numbers"]}};class _{static get defaultParams(){return w.defaultParams}static set defaultParams(t){w.defaultParams=t}static get defaultValidationSchema(){var t;return null!==(t=w.defaultValidationSchema)&&void 0!==t?t:w.defaultValidationSchema=O}static set defaultValidationSchema(t){w.defaultValidationSchema=t}constructor(t,e={}){this.$submits=[],this.waitResponse=!1,this.inputs=null,this.fieldEventHandlers=[],this.liveValidationTimers=new WeakMap,this.fieldValidationTokens=new WeakMap,this.fieldValidationTokenCounter=0,this.errors=[],this._anim={duration:220,easing:"ease",paddingTopOpened:16,iconDuration:700},this.$el=t,this.options=e;this.config=Object.assign({},{onSubmit:()=>{},onResponse:()=>{},onResponseSuccess:()=>{},onResponseUnsuccess:()=>{},customTypeError:null,loaderColor:"#fff",showLoaderButton:!0,scrollToFirstErroredInput:!0,focusFirstErroredInput:!1,logging:!1,inputSelector:".input",inputWrapperSelector:".input-primary",validateOn:"submit",revalidateOn:["input","change"],validationDebounce:0,errorContainerAttribute:"data-error-container",validationStateAttribute:"data-form-father-state",observeMutations:!1},_.defaultParams,e),this.$el?(this.$submits=this.findSubmitElements(),this.isCorrectArguments()&&(this.bindSubmitHandlers(),this.initialization(),this.bindFieldValidationEvents(),this.setupMutationObserver(),T.set(this.$el,this))):console.warn("Empty $el")}getOptions(){return this.options}static initAll(t="form[data-form-father]",e={}){return"undefined"==typeof document?[]:Array.from(document.querySelectorAll(t)).filter((t=>"FORM"===t.tagName)).map((t=>{const r=T.get(t);return r?(r.updateOptions(e),r):new _(t,e)}))}updateOptions(t){return this.options=Object.assign(Object.assign({},this.options),t),this.config=Object.assign(Object.assign({},this.config),t),this.refreshControls(),this.bindSubmitHandlers(),this.bindFieldValidationEvents(),this.setupMutationObserver(),this}getErrors(){return this.errors.map((t=>Object.assign({},t)))}getValues(){return d(this.$el)}normalizeValueList(t){return Array.isArray(t)?t.map(String):[String(t)]}setValues(t){return this.getAllFields().forEach((e=>{if(!e.name||!Object.prototype.hasOwnProperty.call(t,e.name))return;const r=t[e.name];if(!(e instanceof HTMLInputElement&&"file"===e.type)){if(e instanceof HTMLInputElement&&"checkbox"===e.type)e.checked="boolean"==typeof r?r:this.normalizeValueList(r).includes(e.value);else if(e instanceof HTMLInputElement&&"radio"===e.type)e.checked=String(r)===e.value;else if(e instanceof HTMLSelectElement&&e.multiple){const t=this.normalizeValueList(r);Array.from(e.options).forEach((e=>{e.selected=t.includes(e.value)}))}else e.value=null==r?"":String(r);e.dispatchEvent(new Event("input",{bubbles:!0})),e.dispatchEvent(new Event("change",{bubbles:!0}))}})),this}clearErrors(){return this.errors=[],this.getAllFields().forEach((t=>{this.hideError(t),this.clearFieldValidationState(t)})),this.hideErrorForm(),this}isErrorResponse(t){return!!t&&"object"==typeof t&&"string"==typeof t.name&&"string"==typeof t["error-msg"]}normalizeValidationIssues(t,e){return null==t||!0===t?[]:!1===t?[{message:"Некорректные данные формы",source:e}]:"string"==typeof t?[{message:t,source:e}]:Array.isArray(t)?t.flatMap((t=>this.normalizeValidationIssues(t,e))):this.isErrorResponse(t)?[{field:t.name,rule:e,message:t["error-msg"],source:e}]:"object"==typeof t&&"message"in t&&"string"==typeof t.message?[Object.assign(Object.assign({},t),{source:t.source||e})]:"object"==typeof t?Object.entries(t).map((([t,r])=>({field:t,rule:e,message:r,source:e}))):[]}applyValidationIssue(t,e){const r=t.source||e,i=t.rule||r,n=t.field||S;if(n===S)return this.setErrorRecord(S,i,t.message,r),this.showErrorForm(t.message),null;const o=this.findFieldByName(n);return o?(this.showFieldError(o,t.message,r,i),o):(this.setErrorRecord(n,i,t.message,r),console.warn(`Не найдено поле с именем: ${n}, для вывода ошибки: ${t.message}`),null)}setErrors(t,e="server"){return this.normalizeValidationIssues(t,e).forEach((t=>{this.applyValidationIssue(t,e)})),this}validateFormValidators(){return e(this,void 0,void 0,(function*(){const t=this.config.formValidators?Array.isArray(this.config.formValidators)?this.config.formValidators:[this.config.formValidators]:[],e=[];let r=!0;for(const i of t){const t=yield i({form:this,formElement:this.$el,values:this.getValues(),errors:this.getErrors()}),n=this.normalizeValidationIssues(t,"client");0!==n.length&&(r=!1,n.forEach((t=>{const r=this.applyValidationIssue(t,"client");r&&e.push(r)})))}return{ok:r,erroredInputs:e}}))}reset(t={}){return this.$el.reset(),this.refreshControls(),!1!==t.clearErrors&&this.clearErrors(),this}static setDefaultParams(t){this.defaultParams=Object.assign(Object.assign({},this.defaultParams),t)}refreshControls(){this.inputs=this.$el.querySelectorAll(this.config.inputSelector)}findSubmitElements(){return Array.from(this.$el.querySelectorAll('input[type="submit"], input[type="image"], button[type="submit"], button:not([type])'))}bindSubmitHandlers(){this._onSubmitClickHandler||(this._onSubmitClickHandler=t=>e(this,void 0,void 0,(function*(){yield this.handleSubmitEvent(t)}))),this.$submits.forEach((t=>{t.removeEventListener("click",this._onSubmitClickHandler)})),this.$submits=this.findSubmitElements(),this.$submits.forEach((t=>{t.addEventListener("click",this._onSubmitClickHandler)}))}initialization(){this.refreshControls(),this._onSubmitHandler=t=>e(this,void 0,void 0,(function*(){yield this.handleSubmitEvent(t)})),this.$el.addEventListener("submit",this._onSubmitHandler)}handleSubmitEvent(t){return e(this,void 0,void 0,(function*(){var e,r,i,n,o,s;t.preventDefault(),null===(r=(e=this.config).onBeforeValidate)||void 0===r||r.call(e,this);const a=yield this.validate();return null===(n=(i=this.config).onAfterValidate)||void 0===n||n.call(i,a,this),a?this.submit():(null===(s=(o=this.config).onValidationError)||void 0===s||s.call(o,this.getErrors(),this),{success:!1,error:this.getErrors()})}))}normalizeTriggers(t,e){const r=null==t?e:Array.isArray(t)?t:[t];return Array.from(new Set(r))}clearFieldValidationEvents(){this.fieldEventHandlers.forEach((({$input:t,event:e,handler:r})=>{t.removeEventListener(e,r)})),this.fieldEventHandlers=[]}bindFieldValidationEvents(){this.clearFieldValidationEvents();const t=this.normalizeTriggers(this.config.validateOn,["submit"]).filter((t=>"submit"!==t)),e=this.normalizeTriggers(this.config.revalidateOn,["input","change"]).filter((t=>"submit"!==t)),r=Array.from(new Set([...t,...e]));if(0===r.length)return;this.getAllFields().forEach((e=>{r.forEach((r=>{const i=()=>{(t.includes(r)||this.hasFieldError(e))&&this.scheduleFieldValidation(e)};e.addEventListener(r,i),this.fieldEventHandlers.push({$input:e,event:r,handler:i})}))}))}scheduleFieldValidation(t){const e=this.liveValidationTimers.get(t);e&&clearTimeout(e);const r=Math.max(0,Number(this.config.validationDebounce)||0),i=()=>{this.validateField(t)};if(0===r)return void i();const n=setTimeout(i,r);this.liveValidationTimers.set(t,n)}setupMutationObserver(){var t;null===(t=this.mutationObserver)||void 0===t||t.disconnect(),this.mutationObserver=void 0,this.config.observeMutations&&"undefined"!=typeof MutationObserver&&(this.mutationObserver=new MutationObserver((()=>{this.refreshControls(),this.bindSubmitHandlers(),this.bindFieldValidationEvents()})),this.mutationObserver.observe(this.$el,{childList:!0,subtree:!0}))}getErrorContainer(t){const e=this.config.errorContainerAttribute||"data-error-container",r=t.getAttribute(e);if(!r)return null;try{return document.querySelector(r)}catch(t){return console.warn(`[FormFather] Invalid error container selector "${r}"`),null}}showError(t,e){t.setAttribute("aria-invalid","true");const i=this.getErrorContainer(t);if(i)return i.textContent=e,i.hasAttribute("role")||i.setAttribute("role","alert"),void i.removeAttribute("hidden");const n=r(t,this.config.inputWrapperSelector);if(!n)return void console.warn(`Не найдена обёртка для поля с именем: ${t.name||t.type}`);const o=n.showError;if("function"==typeof o)return void o.call(n,e);n.classList.add("input__wrapper--error");let s=n.querySelector("[data-form-father-error]");s||(s=document.createElement("div"),s.setAttribute("data-form-father-error",""),s.setAttribute("role","alert"),t.insertAdjacentElement("afterend",s)),s.textContent=e}showLoader(){this.$submits.forEach((t=>{if(t.classList.add("button--loading"),t.querySelector(".button__loader"))return;const e="form-father-loader-"+$++,r=`\n\t\t\t\t<svg height="38" viewBox="0 0 38 38" width="38" xmlns="http://www.w3.org/2000/svg">\n\t\t\t\t\t<defs>\n\t\t\t\t\t<linearGradient id="${e}" x1="8.042%" x2="65.682%" y1="0%" y2="23.865%">\n\t\t\t\t\t\t<stop offset="0%" stop-color="${this.config.loaderColor}" stop-opacity="0"/>\n\t\t\t\t\t\t<stop offset="63.146%" stop-color="${this.config.loaderColor}" stop-opacity=".631"/>\n\t\t\t\t\t\t<stop offset="100%" stop-color="${this.config.loaderColor}"/>\n\t\t\t\t\t</linearGradient>\n\t\t\t\t\t</defs>\n\t\t\t\t\t<g fill-rule="evenodd" fill="none">\n\t\t\t\t\t<g transform="translate(1 1)">\n\t\t\t\t\t\t<path d="M36 18c0-9.94-8.06-18-18-18" id="Oval-2" stroke-width="5" stroke="url(#${e})"></path>\n\t\t\t\t\t\t<circle cx="36" cy="18" fill="${this.config.loaderColor}" r="1"></circle>\n\t\t\t\t\t</g>\n\t\t\t\t\t</g>\n\t\t\t\t</svg>\n\t\t\t`,i=document.createElement("span");i.innerHTML=r,i.className="button__loader",t.insertAdjacentElement("afterbegin",i),requestAnimationFrame((()=>{i.setAttribute("data-showed","")}))}))}hideLoader(){this.$submits.forEach((t=>{t.classList.remove("button--loading");const e=t.querySelector(".button__loader");e&&(e.classList.remove("active"),e.removeAttribute("data-showed"),setTimeout((()=>{e.remove()}),250))}))}hideError(t){var e;const i=r(t,this.config.inputWrapperSelector)||t.parentElement,n=this.getErrorContainer(t);t.removeAttribute("aria-invalid"),n&&(n.textContent="",n.setAttribute("hidden","")),null===(e=null==i?void 0:i.querySelector("[data-form-father-error]"))||void 0===e||e.remove(),null==i||i.classList.remove("input__wrapper--error"),i&&i.classList.contains("input__wrapper")&&i.getAttribute("data-type-error")}scrollToFirstErroredInput(t){if(t.length>0){const e=Array.from(this.$el.querySelectorAll("input, textarea, select"));let r=null,i=1/0;if(t.forEach((t=>{const n=e.findIndex((e=>e===t));n<i&&(i=n,r=t)})),r){const t=r.getBoundingClientRect(),e=window.pageYOffset||document.documentElement.scrollTop,i=t.top+e-window.innerHeight/2;document.documentElement.setAttribute("data-now-scrolling",""),window.scrollTo({top:i,behavior:"smooth"}),setTimeout((()=>{document.documentElement.removeAttribute("data-now-scrolling")}),800)}}}focusFirstErroredInput(t){var e;if(0===t.length)return;const r=Array.from(this.$el.querySelectorAll("input, textarea, select")),i=t.slice().sort(((t,e)=>r.indexOf(t)-r.indexOf(e)))[0];null===(e=null==i?void 0:i.focus)||void 0===e||e.call(i,{preventScroll:!0})}getValidationSchema(){return Object.assign(Object.assign({},_.defaultValidationSchema||{}),this.config.validationSchema||{})}getAllFields(t=this.$el){return Array.from(t.querySelectorAll("input, textarea, select"))}getFieldKey(t){return t.name||t.id||t.getAttribute("data-field")||t.type||"field"}hasFieldError(t){const e=this.getFieldKey(t);return"true"===t.getAttribute("aria-invalid")||this.errors.some((t=>t.field===e))}clearFieldErrorRecord(t){const e=this.getFieldKey(t);this.errors=this.errors.filter((t=>t.field!==e))}setErrorRecord(t,e,r,i){this.errors.push({field:t,rule:e,message:r,source:i})}getValidationStateAttribute(){return this.config.validationStateAttribute||"data-form-father-state"}setFieldValidationState(t,e){const r=this.getValidationStateAttribute();e?t.setAttribute(r,e):t.removeAttribute(r)}startFieldValidation(t){const e=++this.fieldValidationTokenCounter;return this.fieldValidationTokens.set(t,e),t.setAttribute("aria-busy","true"),this.setFieldValidationState(t,"validating"),e}isLatestFieldValidation(t,e){return null==e||this.fieldValidationTokens.get(t)===e}finishFieldValidation(t,e,r){this.isLatestFieldValidation(t,e)&&(t.removeAttribute("aria-busy"),this.setFieldValidationState(t,r?"valid":"invalid"))}clearFieldValidationState(t){this.fieldValidationTokens.delete(t),t.removeAttribute("aria-busy"),this.setFieldValidationState(t,null)}setValidationError(t,e,r,i){this.clearFieldErrorRecord(t),this.setErrorRecord(this.getFieldKey(t),e,r,i)}resolveField(t){return"string"!=typeof t?t:this.findFieldByName(t)}parseRuleString(t){const[e,...r]=t.split(":").map((t=>t.trim()));return r.length>0?{rule:e,params:1===r.length?r[0]:r}:e}parseValidationAttribute(t){return(null!=t?t:"").split(/[,\s|]+/).map((t=>t.trim())).filter(Boolean).map((t=>this.parseRuleString(t)))}getRuleName(t){return"string"==typeof t?t:t.rule}getRuleIdentity(t){return JSON.stringify(t)}collectRules(t,e=[],r=new Set){const i=[];t.hasAttribute("required")&&i.push("required"),i.push(...e);const n=this.parseValidationAttribute(t.getAttribute("data-validate")).filter((t=>{const e=this.getRuleName(t);return f(e)||!r.has(e)}));i.push(...n),i.push(...this.parseValidationAttribute(t.getAttribute("data-custom-validate")));const o=[],s=new Set;for(const t of i){const e=this.getRuleName(t);if(!f(e)){console.warn(`[FormFather] Unknown validation rule "${e}"`);continue}const r=this.getRuleIdentity(t);s.has(r)||(s.add(r),o.push(t))}return o}getRuleMessage(t,e,r,i,n){const o=t.getAttribute(`data-error-${e}`);return n||o||r[e]||i}fieldMatchesSchemaDefinition(t,e,r){if(r.selector)try{return t.matches(r.selector)}catch(t){return console.warn(`[FormFather] Invalid validation selector "${r.selector}"`),!1}return this.parseValidationAttribute(t.getAttribute("data-validate")).some((t=>this.getRuleName(t)===e))}getFieldValidationConfig(t,e=this.getValidationSchema()){const r=[],i={};for(const[n,o]of Object.entries(e))this.fieldMatchesSchemaDefinition(t,n,o)&&(r.push(...o.rules),Object.assign(i,o.messages||{}));return{rules:this.collectRules(t,r,new Set(Object.keys(e))),messages:i}}getValidationFields(t,e){const r=new Set,i=t=>{Array.from(t).forEach((t=>{(t instanceof HTMLInputElement||t instanceof HTMLTextAreaElement||t instanceof HTMLSelectElement)&&r.add(t)}))};for(const[n,o]of Object.entries(e))if(o.selector)try{i(t.querySelectorAll(o.selector))}catch(t){console.warn(`[FormFather] Invalid validation selector "${o.selector}"`)}else this.getAllFields(t).filter((t=>this.parseValidationAttribute(t.getAttribute("data-validate")).some((t=>this.getRuleName(t)===n)))).forEach((t=>r.add(t)));return i(t.querySelectorAll("[required], [data-validate], [data-custom-validate]")),Array.from(r)}validateInput(t,r){return e(this,arguments,void 0,(function*(t,e,r={},i=this.$el,n){if(t.hasAttribute("data-no-validate"))return!0;const o=t instanceof HTMLInputElement&&("checkbox"===t.type||"radio"===t.type)?!t.checked:0===t.value.trim().length,s=e.some((t=>"required"===this.getRuleName(t)));if(o&&!s)return this.isLatestFieldValidation(t,n)?(this.clearFieldErrorRecord(t),this.hideError(t),!0):!this.hasFieldError(t);for(const o of e){const{rule:e,params:s}="string"==typeof o?{rule:o,params:void 0}:{rule:o.rule,params:o.params},a=f(e);if(!a)continue;const l=yield a.fn(t.value,t,i,s),d="object"==typeof l&&null!==l,c=d?!0===l.valid:!!l;if(!this.isLatestFieldValidation(t,n))return!this.hasFieldError(t);if(d&&l.effect&&(yield l.effect({value:t.value,$input:t,$form:i,params:s})),!this.isLatestFieldValidation(t,n))return!this.hasFieldError(t);if(!c){const i=l,n=this.getRuleMessage(t,e,r,a.defaultMessage,d&&"string"==typeof i.message?i.message:void 0);return this.setValidationError(t,e,n,"client"),this.showError(t,n),!1}if(d&&l.stopOthers)return!0}return this.isLatestFieldValidation(t,n)?(this.clearFieldErrorRecord(t),this.hideError(t),!0):!this.hasFieldError(t)}))}runFieldValidation(t,r){return e(this,arguments,void 0,(function*(t,e,r={},i=this.$el){const n=this.startFieldValidation(t);try{const o=yield this.validateInput(t,e,r,i,n);return this.finishFieldValidation(t,n,o),o}catch(e){throw this.isLatestFieldValidation(t,n)&&this.clearFieldValidationState(t),e}}))}getRadioGroup(t,e){return t instanceof HTMLInputElement&&"radio"===t.type&&t.name?Array.from(e.querySelectorAll('input[type="radio"]')).filter((e=>e.name===t.name)):[]}showFieldError(t,e,r="manual",i=r){const n=this.resolveField(t);return n?(this.setValidationError(n,i,e,r),this.showError(n,e),n.removeAttribute("aria-busy"),this.setFieldValidationState(n,"invalid"),!0):(console.warn(`[FormFather] Field "${String(t)}" not found`),!1)}validateField(t){return e(this,void 0,void 0,(function*(){const e=this.resolveField(t);if(!e)return console.warn(`[FormFather] Field "${String(t)}" not found`),!1;const{rules:r,messages:i}=this.getFieldValidationConfig(e);return this.runFieldValidation(e,r,i)}))}validate(){return e(this,arguments,void 0,(function*(t=this.$el){const e=this.getValidationSchema(),r=[],i=new WeakSet;let n=!0;this.errors=[];for(const o of this.getValidationFields(t,e)){if(i.has(o))continue;const s=this.getRadioGroup(o,t);s.length>0?s.forEach((t=>i.add(t))):i.add(o);const{rules:a,messages:l}=this.getFieldValidationConfig(o,e);(yield this.runFieldValidation(o,a,l,t))||(n=!1,r.push(o))}const o=yield this.validateFormValidators();o.ok||(n=!1,r.push(...o.erroredInputs));const s=r.filter((t=>!t.hasAttribute("data-no-error-scroll")));return!n&&this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(s),!n&&this.config.focusFirstErroredInput&&this.focusFirstErroredInput(s),n}))}disableSubmit(){this.$submits.forEach((t=>{t.setAttribute("disabled","")}))}enableSubmit(){this.$submits.forEach((t=>{t.removeAttribute("disabled")}))}clearInputs(){var t;null===(t=this.inputs)||void 0===t||t.forEach((t=>{var e;const r=t;"radio"===r.type||"checkbox"===r.type?r.checked=!1:(r.value="",null===(e=r.parentElement)||void 0===e||e.classList.remove("filled"),r.dispatchEvent(new Event("input")))}))}_prefersReducedMotion(){var t;return"undefined"!=typeof window&&(null===(t=window.matchMedia)||void 0===t?void 0:t.call(window,"(prefers-reduced-motion: reduce)").matches)}_canAnimate(t){return!(!t||!("animate"in t)||this._prefersReducedMotion())}_cancelAnimations(t){var e;const r=t;null===(e=null==r?void 0:r.getAnimations)||void 0===e||e.call(r).forEach((t=>t.cancel()))}showErrorForm(t){var e;const r=this.$el.querySelectorAll(this.config.inputWrapperSelector),i=r[r.length-1];let n=this.$el.querySelector(".error-block-under-input__wrapper");!n||i&&i.nextElementSibling!==n?(n=document.createElement("div"),n.className="error-block-under-input__wrapper",n.innerHTML='\n <div class="error-block-under-input error-block-under-input--warning" style="display:grid;grid-template-columns:auto 1fr;gap:8px;align-items:start;">\n <div class="error-block-under-input__icon-wrapper" style="position:relative;width:20px;height:20px;">\n <span class="error-block-under-input__icon" style="display:block;width:20px;height:20px;"></span>\n <span class="error-block-under-input__icon error-block-under-input__icon--animated" style="display:block;position:absolute;inset:0;"></span>\n </div>\n <p class="error-block-under-input__text" style="margin:0;">\n <span class="error-block-under-input__main-text"></span>\n <span class="error-block-under-input__secondary-text"></span>\n </p>\n </div>\n ',Object.assign(n.style,{boxSizing:"border-box",overflow:"hidden",height:"0px",paddingTop:"0px",opacity:"0"}),i?i.insertAdjacentElement("afterend",n):this.$el.appendChild(n)):(n.style.boxSizing="border-box",n.style.overflow="hidden"),n.querySelector(".error-block-under-input__main-text").textContent=t;const o=n.querySelector(".error-block-under-input__secondary-text");o&&(o.textContent=""),null===(e=n.querySelector(".error-block-under-input"))||void 0===e||e.classList.remove("error-block-under-input--success");const s=n.style.height,a=n.style.paddingTop;n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`;const l=n.clientHeight;if(n.style.height=s||"0px",n.style.paddingTop=a||"0px",parseFloat(n.style.height)===l&&1===parseFloat(n.style.opacity||"1"))this._ringIcon(n);else{if(this._cancelAnimations(n),this._canAnimate(n)){n.animate([{height:`${parseFloat(n.style.height)||0}px`,paddingTop:`${parseFloat(n.style.paddingTop)||0}px`,opacity:parseFloat(n.style.opacity||"0")},{height:`${l}px`,paddingTop:`${this._anim.paddingTopOpened}px`,opacity:1}],{duration:this._anim.duration,easing:this._anim.easing,fill:"forwards"}).addEventListener("finish",(()=>{n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`,n.style.opacity="1"}))}else n.style.height="auto",n.style.paddingTop=`${this._anim.paddingTopOpened}px`,n.style.opacity="1";this._ringIcon(n)}this.$el.addEventListener("input",(()=>this.hideErrorForm()),{passive:!0,once:!0})}hideErrorForm(){const t=this.$el.querySelectorAll(this.config.inputWrapperSelector),e=t[t.length-1],r=(null==e?void 0:e.nextElementSibling)||this.$el.querySelector(".error-block-under-input__wrapper");if(!r||!r.classList.contains("error-block-under-input__wrapper"))return;if((""===r.style.height||"auto"===r.style.height)&&(r.style.height=`${r.clientHeight}px`),r.style.paddingTop=r.style.paddingTop||`${this._anim.paddingTopOpened}px`,r.style.opacity=r.style.opacity||"1",this._cancelAnimations(r),this._canAnimate(r)){r.animate([{height:`${parseFloat(r.style.height)||r.clientHeight}px`,paddingTop:`${parseFloat(r.style.paddingTop)||this._anim.paddingTopOpened}px`,opacity:parseFloat(r.style.opacity||"1")},{height:"0px",paddingTop:"0px",opacity:0}],{duration:this._anim.duration,easing:this._anim.easing,fill:"forwards"}).addEventListener("finish",(()=>{r.remove()}))}else r.remove()}_ringIcon(t){const e=t.querySelector(".error-block-under-input__icon--animated");e&&(this._cancelAnimations(e),this._canAnimate(e)&&e.animate([{transform:"rotate(0deg) scale(1)",opacity:0},{transform:"rotate(-8deg) scale(1.05)",opacity:1,offset:.15},{transform:"rotate(8deg) scale(1.05)",opacity:1,offset:.3},{transform:"rotate(-6deg) scale(1.03)",opacity:1,offset:.45},{transform:"rotate(6deg) scale(1.03)",opacity:1,offset:.6},{transform:"rotate(0deg) scale(1)",opacity:0}],{duration:this._anim.iconDuration,easing:"ease"}))}destroy(){var t;this._onSubmitHandler&&(this.$el.removeEventListener("submit",this._onSubmitHandler),this._onSubmitHandler=void 0),this._onSubmitClickHandler&&(this.$submits.forEach((t=>{t.removeEventListener("click",this._onSubmitClickHandler)})),this._onSubmitClickHandler=void 0),this.clearFieldValidationEvents(),null===(t=this.mutationObserver)||void 0===t||t.disconnect(),this.mutationObserver=void 0;const e=this.$el.querySelector(".error-block-under-input__wrapper");e&&(this._cancelAnimations(e),e.remove()),this.$submits.forEach((t=>{t.classList.remove("button--loading");const e=t.querySelector(".button__loader");e&&e.remove()}));this.$el.querySelectorAll("input, textarea, select").forEach((t=>{this.hideError(t),this.clearFieldValidationState(t)})),document.documentElement.removeAttribute("data-now-scrolling"),this.inputs=null,this.$submits=[],this.errors=[],this.waitResponse=!1,T.delete(this.$el)}isCorrectArguments(){return"FORM"!==this.$el.tagName?(console.warn("$el не является тегом form"),!1):0!==this.$submits.length||(console.warn("В форме нет кнопки с типом submit"),!1)}formDataToObject(t){const e={};return t.forEach(((t,r)=>{Object.prototype.hasOwnProperty.call(e,r)?(Array.isArray(e[r])||(e[r]=[e[r]]),e[r].push(t)):e[r]=t})),e}appendFormDataValue(t,e,r){null!=r&&(Array.isArray(r)?r.forEach(((r,i)=>{this.appendFormDataValue(t,`${e}[${i}]`,r)})):r instanceof Blob?t.append(e,r):t.append(e,"object"==typeof r?JSON.stringify(r):String(r)))}objectToFormData(t){const e=new FormData;return Object.entries(t).forEach((([t,r])=>{this.appendFormDataValue(e,t,r)})),e}stringifyFormDataValue(t){return"string"==typeof t?t:t.name}formDataToUrlSearchParams(t){const e=new URLSearchParams;return t.forEach(((t,r)=>{e.append(r,this.stringifyFormDataValue(t))})),e}formDataToTextBody(t){return Array.from(t).map((([t,e])=>`${encodeURIComponent(t)}=${encodeURIComponent(this.stringifyFormDataValue(e))}`)).join("\n")}isBodylessMethod(t){return"GET"===t||"HEAD"===t}appendQueryString(t,e){const r=e.toString();if(!r)return t;const i=t.includes("?")?"&":"?";return`${t}${i}${r}`}findFieldByName(t){return Array.from(this.$el.querySelectorAll("input, textarea, select")).find((e=>e.name===t))||null}parseResponseBody(t){return e(this,void 0,void 0,(function*(){try{const e=yield t.json();return e&&"object"==typeof e?e:{success:!1,error:!0,"error-msg":"Некорректный ответ сервера"}}catch(t){return{success:!1,error:!0,"error-msg":"Некорректный ответ сервера"}}}))}handleUnsuccessfulResponse(t){if(!0===t.error&&(t["error-msg"]&&this.showErrorForm(t["error-msg"]),t.errors)){const e=[];t.errors.forEach((t=>{const r=this.findFieldByName(t.name);r?(e.push(r),this.setValidationError(r,"server",t["error-msg"],"server"),this.showError(r,t["error-msg"]),r.removeAttribute("aria-busy"),this.setFieldValidationState(r,"invalid")):console.warn(`Не найдено поле с именем: ${t.name}, для вывода ошибки: ${t["error-msg"]}`)})),!0===this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(e),!0===this.config.focusFirstErroredInput&&this.focusFirstErroredInput(e)}}sendData(){return e(this,void 0,void 0,(function*(){var t;const e=this.$el.getAttribute("action")||"",r=(null===(t=this.$el.getAttribute("method"))||void 0===t?void 0:t.toUpperCase())||"POST",i=this.$el.getAttribute("enctype")||"application/x-www-form-urlencoded";let n=null,o={};const s=a(this.$el);let l=this.formDataToObject(s);if(this.config.logging)for(const t of s.keys())console.log(s.get(t));const d="function"==typeof this.config.wrapData;"function"==typeof this.config.wrapData&&(l=this.config.wrapData(l));const c=d?this.objectToFormData(l):s;if(this.isBodylessMethod(r))return fetch(this.appendQueryString(e,this.formDataToUrlSearchParams(c)),{method:r,headers:o});switch(i){case"application/x-www-form-urlencoded":n=this.formDataToUrlSearchParams(c).toString(),o["Content-Type"]="application/x-www-form-urlencoded";break;case"multipart/form-data":n=c;break;case"text/plain":n=this.formDataToTextBody(c),o["Content-Type"]="text/plain";break;case"application/json":n=JSON.stringify(l),o["Content-Type"]="application/json";break;default:console.warn(`Неизвестный enctype: ${i}. Используется application/x-www-form-urlencoded.`),n=this.formDataToUrlSearchParams(c).toString(),o["Content-Type"]="application/x-www-form-urlencoded"}return fetch(e,{method:r,body:n,headers:o})}))}submit(){return e(this,void 0,void 0,(function*(){var t,e,r,i,n,o,s,a,d,c,u,h;if(!this.waitResponse){let p=!1;const f=()=>{p||(this.waitResponse=!1,!0===this.config.showLoaderButton&&this.hideLoader(),p=!0)};try{if(!1===(yield null===(e=(t=this.config).onBeforeSubmit)||void 0===e?void 0:e.call(t,this)))return{success:!1};!0===this.config.showLoaderButton&&this.showLoader(),null===(i=(r=this.config).onSubmit)||void 0===i||i.call(r,this),this.waitResponse=!0;const u=yield this.sendData(),h=yield this.parseResponseBody(u);return f(),null===(o=(n=this.config).onResponse)||void 0===o||o.call(n,h,this),u.ok&&!0===h.success?null===(a=(s=this.config).onResponseSuccess)||void 0===a||a.call(s,h,this):(null===(c=(d=this.config).onResponseUnsuccess)||void 0===c||c.call(d,h,this),this.handleUnsuccessfulResponse(h)),l(h),{success:u.ok&&!0===h.success,response:u,responseBody:h}}catch(t){throw f(),null===(h=(u=this.config).onSubmitError)||void 0===h||h.call(u,t,this),t}}}))}}return t.FORM_ERROR_FIELD=S,t.blockScrollBody=function(){window.lastBlockScrollPosition=window.pageYOffset,document.body.style.top=-window.lastBlockScrollPosition+"px",document.body.classList.add("body--block-scroll")},t.closest=r,t.createFieldValidator=v,t.createFormValidator=V,t.createLengthValidator=function(t,e){return v((e=>{const r=!1===t.trim?e:e.trim();return 0===r.length&&!0===t.emptyIsValid||!("number"==typeof t.min&&r.length<t.min)&&!("number"==typeof t.max&&r.length>t.max)}),e)},t.createPatternValidator=function(t,e){return v((e=>(t.lastIndex=0,t.test(e))),e)},t.createSchemaValidator=y,t.dateOrder=function(t,e,r="Дата окончания должна быть позже даты начала",i="date-order"){return V((({values:r})=>{const i=F(r,t),n=F(r,e);return!(!A(i)&&!A(n))||Date.parse(String(i))<=Date.parse(String(n))}),{field:e,rule:i,message:r})},t.default=_,t.getAllValidators=function(){return new Map(h)},t.getValidator=f,t.isEmailValid=function(t){return/^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu.test(t)},t.isPhoneValid=function(t){const e=[];return Array.from(t).forEach((t=>{(Number(t)||0===Number(t))&&e.push(t)})),!(e.length<11||e.length<12&&"+"===t[1])},t.isUrlValid=s,t.parseCommonResponseProperties=l,t.registerFieldValidator=function(t,e,r,i={}){p(t,v(e),r,i)},t.registerSchemaValidator=function(t,e,r,i={}){const{override:n}=i;p(t,y(e,function(t,e){var r={};for(var i in t)Object.prototype.hasOwnProperty.call(t,i)&&e.indexOf(i)<0&&(r[i]=t[i]);if(null!=t&&"function"==typeof Object.getOwnPropertySymbols){var n=0;for(i=Object.getOwnPropertySymbols(t);n<i.length;n++)e.indexOf(i[n])<0&&Object.prototype.propertyIsEnumerable.call(t,i[n])&&(r[i[n]]=t[i[n]])}return r}(i,["override"])),r,{override:n})},t.registerValidator=p,t.requiredIf=function(t,r,i="Поле обязательно",n="conditional-required"){return V((i=>e(this,void 0,void 0,(function*(){return!(yield r(i))||!A(F(i.values,t))}))),{field:t,rule:n,message:i})},t.sameAsField=function(t,e,r="Значения не совпадают",i="same-as-field"){return V((({values:r})=>F(r,t)===F(r,e)),{field:t,rule:i,message:r})},t.serializeFormToJSON=d,t.serializeToFormData=a,t.unblockScrollBody=function(){document.body.classList.remove("body--block-scroll"),document.body.style.top="",window.scrollTo(0,window.lastBlockScrollPosition||0)},Object.defineProperty(t,"__esModule",{value:!0}),t}({});
2
2
  //# sourceMappingURL=FormFather.min.js.map
package/dist/index.d.ts CHANGED
@@ -48,9 +48,21 @@ interface SchemaValidatorOptions {
48
48
  interface RegisterSchemaValidatorOptions extends SchemaValidatorOptions {
49
49
  override?: boolean;
50
50
  }
51
+ interface RegisterFieldValidatorOptions {
52
+ override?: boolean;
53
+ }
54
+ interface LengthValidatorOptions {
55
+ min?: number;
56
+ max?: number;
57
+ trim?: boolean;
58
+ emptyIsValid?: boolean;
59
+ }
51
60
  declare function createSchemaValidator(schema: SchemaLike, options?: SchemaValidatorOptions): AdapterValidator;
52
61
  declare function registerSchemaValidator(name: string, schema: SchemaLike, defaultMessage: string, options?: RegisterSchemaValidatorOptions): void;
62
+ declare function registerFieldValidator(name: string, predicate: (value: string, $input: ValidatorFieldElement, $form: HTMLElement, params?: any) => MaybePromise<boolean>, defaultMessage: string, options?: RegisterFieldValidatorOptions): void;
53
63
  declare function createFieldValidator(predicate: (value: string, $input: ValidatorFieldElement, $form: HTMLElement, params?: any) => MaybePromise<boolean>, message?: string): AdapterValidator;
64
+ declare function createPatternValidator(pattern: RegExp, message?: string): AdapterValidator;
65
+ declare function createLengthValidator(options: LengthValidatorOptions, message?: string): AdapterValidator;
54
66
 
55
67
  /**
56
68
  * Находит родительский элемент по заданому селектору.
@@ -121,6 +133,7 @@ type ValidationRule = string | {
121
133
  type ValidationTrigger = 'submit' | 'input' | 'blur' | 'change';
122
134
  type ValidationErrorSource = 'client' | 'server' | 'manual';
123
135
  type FieldReference = string | FormFieldElement;
136
+ declare const FORM_ERROR_FIELD = "_form";
124
137
  interface ValidationError {
125
138
  field: string;
126
139
  rule: string;
@@ -136,11 +149,32 @@ interface SubmitResult {
136
149
  interface FormResetOptions {
137
150
  clearErrors?: boolean;
138
151
  }
152
+ interface ValidationIssue {
153
+ field?: string;
154
+ rule?: string;
155
+ message: string;
156
+ source?: ValidationErrorSource;
157
+ }
158
+ type ValidationIssueInput = string | ValidationIssue | ErrorResponse | Record<string, string> | Array<string | ValidationIssue | ErrorResponse>;
159
+ interface FormValidatorContext {
160
+ form: Form;
161
+ formElement: HTMLFormElement;
162
+ values: Record<string, any>;
163
+ errors: ValidationError[];
164
+ }
165
+ type FormValidatorResult = boolean | string | void | ValidationIssue | Array<string | ValidationIssue>;
166
+ type FormValidator = (context: FormValidatorContext) => FormValidatorResult | Promise<FormValidatorResult>;
167
+ type FormValidatorPredicate = (context: FormValidatorContext) => boolean | Promise<boolean>;
168
+ type FormValidatorIssueFactory = string | ValidationIssue | ((context: FormValidatorContext) => string | ValidationIssue | Promise<string | ValidationIssue>);
139
169
  type ValidationSchema = Record<string, {
140
170
  rules: ValidationRule[];
141
171
  selector?: string;
142
172
  messages?: Record<string, string>;
143
173
  }>;
174
+ declare function createFormValidator(predicate: FormValidatorPredicate, issue: FormValidatorIssueFactory): FormValidator;
175
+ declare function sameAsField(field: string, otherField: string, message?: string, rule?: string): FormValidator;
176
+ declare function requiredIf(field: string, condition: FormValidatorPredicate, message?: string, rule?: string): FormValidator;
177
+ declare function dateOrder(startField: string, endField: string, message?: string, rule?: string): FormValidator;
144
178
  type FormFieldElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
145
179
  /** Параметры формы. */
146
180
  interface FormOptions {
@@ -243,6 +277,8 @@ interface FormOptions {
243
277
  wrapData?: (data: Record<string, any>) => Record<string, any>;
244
278
  /** Схема валидации: поле → массив правил + override-сообщения */
245
279
  validationSchema?: ValidationSchema;
280
+ /** Валидаторы всей формы: cross-field, conditional required, date ranges и другие проверки над `getValues()`. */
281
+ formValidators?: FormValidator | FormValidator[];
246
282
  }
247
283
  interface ErrorResponse {
248
284
  name: string;
@@ -305,6 +341,12 @@ declare class Form {
305
341
  setValues(values: Record<string, any>): this;
306
342
  /** Снимает все клиентские и серверные ошибки с формы, не меняя значения полей. */
307
343
  clearErrors(): this;
344
+ private isErrorResponse;
345
+ private normalizeValidationIssues;
346
+ private applyValidationIssue;
347
+ /** Показывает несколько ошибок формы или полей: из backend map/array, `ErrorResponse[]` или form validators. */
348
+ setErrors(issues: ValidationIssueInput, source?: ValidationErrorSource): this;
349
+ private validateFormValidators;
308
350
  /** Сбрасывает форму через native `reset()` и опционально очищает ошибки. */
309
351
  reset(options?: FormResetOptions): this;
310
352
  /**
@@ -355,6 +397,7 @@ declare class Form {
355
397
  private getFieldKey;
356
398
  private hasFieldError;
357
399
  private clearFieldErrorRecord;
400
+ private setErrorRecord;
358
401
  private getValidationStateAttribute;
359
402
  private setFieldValidationState;
360
403
  private startFieldValidation;
@@ -376,7 +419,7 @@ declare class Form {
376
419
  private runFieldValidation;
377
420
  private getRadioGroup;
378
421
  /** Показывает ошибку конкретного поля по имени или DOM-элементу. */
379
- showFieldError(field: FieldReference, message: string, source?: ValidationErrorSource): boolean;
422
+ showFieldError(field: FieldReference, message: string, source?: ValidationErrorSource, rule?: string): boolean;
380
423
  /** Проверяет одно поле по имени или DOM-элементу. */
381
424
  validateField(field: FieldReference): Promise<boolean>;
382
425
  /**
@@ -437,4 +480,4 @@ declare class Form {
437
480
  submit(): Promise<SubmitResult | undefined>;
438
481
  }
439
482
 
440
- export { type AdapterValidator, type ErrorResponse, type FieldReference, type FormFieldElement, type FormOptions, type FormResetOptions, type RegisterSchemaValidatorOptions, type ResponseBody, type SchemaLike, type SchemaParseResult, type SchemaValidatorOptions, type SubmitResult, type ValidationError, type ValidationErrorSource, type ValidationRule, type ValidationSchema, type ValidationTrigger, type ValidatorEffectCtx, type ValidatorFieldElement, type ValidatorFn, type ValidatorResult, blockScrollBody, closest, createFieldValidator, createSchemaValidator, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerSchemaValidator, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
483
+ export { type AdapterValidator, type ErrorResponse, FORM_ERROR_FIELD, type FieldReference, type FormFieldElement, type FormOptions, type FormResetOptions, type FormValidator, type FormValidatorContext, type FormValidatorIssueFactory, type FormValidatorPredicate, type FormValidatorResult, type LengthValidatorOptions, type RegisterFieldValidatorOptions, type RegisterSchemaValidatorOptions, type ResponseBody, type SchemaLike, type SchemaParseResult, type SchemaValidatorOptions, type SubmitResult, type ValidationError, type ValidationErrorSource, type ValidationIssue, type ValidationIssueInput, type ValidationRule, type ValidationSchema, type ValidationTrigger, type ValidatorEffectCtx, type ValidatorFieldElement, type ValidatorFn, type ValidatorResult, blockScrollBody, closest, createFieldValidator, createFormValidator, createLengthValidator, createPatternValidator, createSchemaValidator, dateOrder, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerFieldValidator, registerSchemaValidator, registerValidator, requiredIf, sameAsField, serializeFormToJSON, serializeToFormData, unblockScrollBody };
package/dist/index.js CHANGED
@@ -427,12 +427,33 @@ function registerSchemaValidator(name, schema, defaultMessage, options = {}) {
427
427
  const { override } = options, adapterOptions = __rest(options, ["override"]);
428
428
  registerValidator(name, createSchemaValidator(schema, adapterOptions), defaultMessage, { override });
429
429
  }
430
+ function registerFieldValidator(name, predicate, defaultMessage, options = {}) {
431
+ registerValidator(name, createFieldValidator(predicate), defaultMessage, options);
432
+ }
430
433
  function createFieldValidator(predicate, message) {
431
434
  return (value, $input, $form, params) => __awaiter(this, void 0, void 0, function* () {
432
435
  const valid = yield predicate(value, $input, $form, params);
433
436
  return valid ? true : { valid: false, message };
434
437
  });
435
438
  }
439
+ function createPatternValidator(pattern, message) {
440
+ return createFieldValidator(value => {
441
+ pattern.lastIndex = 0;
442
+ return pattern.test(value);
443
+ }, message);
444
+ }
445
+ function createLengthValidator(options, message) {
446
+ return createFieldValidator(value => {
447
+ const normalizedValue = options.trim === false ? value : value.trim();
448
+ if (normalizedValue.length === 0 && options.emptyIsValid === true)
449
+ return true;
450
+ if (typeof options.min === 'number' && normalizedValue.length < options.min)
451
+ return false;
452
+ if (typeof options.max === 'number' && normalizedValue.length > options.max)
453
+ return false;
454
+ return true;
455
+ }, message);
456
+ }
436
457
 
437
458
  var _a;
438
459
  // --- shared cross-file state (singleton via globalThis) ---
@@ -445,6 +466,49 @@ const __shared =
445
466
  defaultParams: {},
446
467
  defaultValidationSchema: undefined,
447
468
  });
469
+ const FORM_ERROR_FIELD = '_form';
470
+ function readFormValue(values, field) {
471
+ return values[field];
472
+ }
473
+ function isEmptyFormValue(value) {
474
+ if (Array.isArray(value))
475
+ return value.length === 0;
476
+ return value === undefined || value === null || String(value).trim().length === 0;
477
+ }
478
+ function resolveFormValidatorIssue(context, issue, fallbackField, fallbackRule) {
479
+ return __awaiter(this, void 0, void 0, function* () {
480
+ const resolvedIssue = typeof issue === 'function' ? yield issue(context) : issue;
481
+ if (typeof resolvedIssue === 'string') {
482
+ return {
483
+ field: fallbackField,
484
+ rule: fallbackRule,
485
+ message: resolvedIssue,
486
+ };
487
+ }
488
+ return Object.assign({ field: fallbackField, rule: fallbackRule }, resolvedIssue);
489
+ });
490
+ }
491
+ function createFormValidator(predicate, issue) {
492
+ return (context) => __awaiter(this, void 0, void 0, function* () {
493
+ const valid = yield predicate(context);
494
+ return valid ? true : resolveFormValidatorIssue(context, issue);
495
+ });
496
+ }
497
+ function sameAsField(field, otherField, message = 'Значения не совпадают', rule = 'same-as-field') {
498
+ return createFormValidator(({ values }) => readFormValue(values, field) === readFormValue(values, otherField), { field, rule, message });
499
+ }
500
+ function requiredIf(field, condition, message = 'Поле обязательно', rule = 'conditional-required') {
501
+ return createFormValidator((context) => __awaiter(this, void 0, void 0, function* () { return !(yield condition(context)) || !isEmptyFormValue(readFormValue(context.values, field)); }), { field, rule, message });
502
+ }
503
+ function dateOrder(startField, endField, message = 'Дата окончания должна быть позже даты начала', rule = 'date-order') {
504
+ return createFormValidator(({ values }) => {
505
+ const start = readFormValue(values, startField);
506
+ const end = readFormValue(values, endField);
507
+ if (isEmptyFormValue(start) || isEmptyFormValue(end))
508
+ return true;
509
+ return Date.parse(String(start)) <= Date.parse(String(end));
510
+ }, { field: endField, rule, message });
511
+ }
448
512
  let loaderIdCounter = 0;
449
513
  const FORM_INSTANCES = new WeakMap();
450
514
  /** Изначальная дефолтная схема. */
@@ -637,6 +701,99 @@ class Form {
637
701
  this.hideErrorForm();
638
702
  return this;
639
703
  }
704
+ isErrorResponse(issue) {
705
+ return (!!issue &&
706
+ typeof issue === 'object' &&
707
+ typeof issue.name === 'string' &&
708
+ typeof issue['error-msg'] === 'string');
709
+ }
710
+ normalizeValidationIssues(issues, source) {
711
+ if (issues == null || issues === true)
712
+ return [];
713
+ if (issues === false)
714
+ return [{ message: 'Некорректные данные формы', source }];
715
+ if (typeof issues === 'string')
716
+ return [{ message: issues, source }];
717
+ if (Array.isArray(issues)) {
718
+ return issues.flatMap(issue => this.normalizeValidationIssues(issue, source));
719
+ }
720
+ if (this.isErrorResponse(issues)) {
721
+ return [
722
+ {
723
+ field: issues.name,
724
+ rule: source,
725
+ message: issues['error-msg'],
726
+ source,
727
+ },
728
+ ];
729
+ }
730
+ if (typeof issues === 'object' && 'message' in issues && typeof issues.message === 'string') {
731
+ return [Object.assign(Object.assign({}, issues), { source: issues.source || source })];
732
+ }
733
+ if (typeof issues === 'object') {
734
+ return Object.entries(issues).map(([field, message]) => ({
735
+ field,
736
+ rule: source,
737
+ message,
738
+ source,
739
+ }));
740
+ }
741
+ return [];
742
+ }
743
+ applyValidationIssue(issue, source) {
744
+ const issueSource = issue.source || source;
745
+ const rule = issue.rule || issueSource;
746
+ const field = issue.field || FORM_ERROR_FIELD;
747
+ if (field === FORM_ERROR_FIELD) {
748
+ this.setErrorRecord(FORM_ERROR_FIELD, rule, issue.message, issueSource);
749
+ this.showErrorForm(issue.message);
750
+ return null;
751
+ }
752
+ const $field = this.findFieldByName(field);
753
+ if (!$field) {
754
+ this.setErrorRecord(field, rule, issue.message, issueSource);
755
+ console.warn(`Не найдено поле с именем: ${field}, для вывода ошибки: ${issue.message}`);
756
+ return null;
757
+ }
758
+ this.showFieldError($field, issue.message, issueSource, rule);
759
+ return $field;
760
+ }
761
+ /** Показывает несколько ошибок формы или полей: из backend map/array, `ErrorResponse[]` или form validators. */
762
+ setErrors(issues, source = 'server') {
763
+ this.normalizeValidationIssues(issues, source).forEach(issue => {
764
+ this.applyValidationIssue(issue, source);
765
+ });
766
+ return this;
767
+ }
768
+ validateFormValidators() {
769
+ return __awaiter(this, void 0, void 0, function* () {
770
+ const validators = this.config.formValidators
771
+ ? Array.isArray(this.config.formValidators)
772
+ ? this.config.formValidators
773
+ : [this.config.formValidators]
774
+ : [];
775
+ const erroredInputs = [];
776
+ let ok = true;
777
+ for (const validator of validators) {
778
+ const result = yield validator({
779
+ form: this,
780
+ formElement: this.$el,
781
+ values: this.getValues(),
782
+ errors: this.getErrors(),
783
+ });
784
+ const issues = this.normalizeValidationIssues(result, 'client');
785
+ if (issues.length === 0)
786
+ continue;
787
+ ok = false;
788
+ issues.forEach(issue => {
789
+ const $field = this.applyValidationIssue(issue, 'client');
790
+ if ($field)
791
+ erroredInputs.push($field);
792
+ });
793
+ }
794
+ return { ok, erroredInputs };
795
+ });
796
+ }
640
797
  /** Сбрасывает форму через native `reset()` и опционально очищает ошибки. */
641
798
  reset(options = {}) {
642
799
  this.$el.reset();
@@ -936,6 +1093,14 @@ class Form {
936
1093
  const field = this.getFieldKey($input);
937
1094
  this.errors = this.errors.filter(error => error.field !== field);
938
1095
  }
1096
+ setErrorRecord(field, rule, message, source) {
1097
+ this.errors.push({
1098
+ field,
1099
+ rule,
1100
+ message,
1101
+ source,
1102
+ });
1103
+ }
939
1104
  getValidationStateAttribute() {
940
1105
  return this.config.validationStateAttribute || 'data-form-father-state';
941
1106
  }
@@ -971,12 +1136,7 @@ class Form {
971
1136
  }
972
1137
  setValidationError($input, rule, message, source) {
973
1138
  this.clearFieldErrorRecord($input);
974
- this.errors.push({
975
- field: this.getFieldKey($input),
976
- rule,
977
- message,
978
- source,
979
- });
1139
+ this.setErrorRecord(this.getFieldKey($input), rule, message, source);
980
1140
  }
981
1141
  resolveField(field) {
982
1142
  if (typeof field !== 'string')
@@ -1161,13 +1321,13 @@ class Form {
1161
1321
  return Array.from($block.querySelectorAll('input[type="radio"]')).filter($radio => $radio.name === $input.name);
1162
1322
  }
1163
1323
  /** Показывает ошибку конкретного поля по имени или DOM-элементу. */
1164
- showFieldError(field, message, source = 'manual') {
1324
+ showFieldError(field, message, source = 'manual', rule = source) {
1165
1325
  const $field = this.resolveField(field);
1166
1326
  if (!$field) {
1167
1327
  console.warn(`[FormFather] Field "${String(field)}" not found`);
1168
1328
  return false;
1169
1329
  }
1170
- this.setValidationError($field, source, message, source);
1330
+ this.setValidationError($field, rule, message, source);
1171
1331
  this.showError($field, message);
1172
1332
  $field.removeAttribute('aria-busy');
1173
1333
  this.setFieldValidationState($field, 'invalid');
@@ -1222,6 +1382,11 @@ class Form {
1222
1382
  erroredInputs.push($input);
1223
1383
  }
1224
1384
  }
1385
+ const formValidation = yield this.validateFormValidators();
1386
+ if (!formValidation.ok) {
1387
+ ok = false;
1388
+ erroredInputs.push(...formValidation.erroredInputs);
1389
+ }
1225
1390
  const visibleErroredInputs = erroredInputs.filter(item => !item.hasAttribute('data-no-error-scroll'));
1226
1391
  if (!ok && this.config.scrollToFirstErroredInput) {
1227
1392
  this.scrollToFirstErroredInput(visibleErroredInputs);
@@ -1716,5 +1881,5 @@ class Form {
1716
1881
  }
1717
1882
  }
1718
1883
 
1719
- export { blockScrollBody, closest, createFieldValidator, createSchemaValidator, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerSchemaValidator, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
1884
+ export { FORM_ERROR_FIELD, blockScrollBody, closest, createFieldValidator, createFormValidator, createLengthValidator, createPatternValidator, createSchemaValidator, dateOrder, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerFieldValidator, registerSchemaValidator, registerValidator, requiredIf, sameAsField, serializeFormToJSON, serializeToFormData, unblockScrollBody };
1720
1885
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -22,7 +22,19 @@ export interface SchemaValidatorOptions {
22
22
  export interface RegisterSchemaValidatorOptions extends SchemaValidatorOptions {
23
23
  override?: boolean;
24
24
  }
25
+ export interface RegisterFieldValidatorOptions {
26
+ override?: boolean;
27
+ }
28
+ export interface LengthValidatorOptions {
29
+ min?: number;
30
+ max?: number;
31
+ trim?: boolean;
32
+ emptyIsValid?: boolean;
33
+ }
25
34
  export declare function createSchemaValidator(schema: SchemaLike, options?: SchemaValidatorOptions): AdapterValidator;
26
35
  export declare function registerSchemaValidator(name: string, schema: SchemaLike, defaultMessage: string, options?: RegisterSchemaValidatorOptions): void;
36
+ export declare function registerFieldValidator(name: string, predicate: (value: string, $input: ValidatorFieldElement, $form: HTMLElement, params?: any) => MaybePromise<boolean>, defaultMessage: string, options?: RegisterFieldValidatorOptions): void;
27
37
  export declare function createFieldValidator(predicate: (value: string, $input: ValidatorFieldElement, $form: HTMLElement, params?: any) => MaybePromise<boolean>, message?: string): AdapterValidator;
38
+ export declare function createPatternValidator(pattern: RegExp, message?: string): AdapterValidator;
39
+ export declare function createLengthValidator(options: LengthValidatorOptions, message?: string): AdapterValidator;
28
40
  export {};
@@ -8,6 +8,7 @@ export type ValidationRule = string | {
8
8
  export type ValidationTrigger = 'submit' | 'input' | 'blur' | 'change';
9
9
  export type ValidationErrorSource = 'client' | 'server' | 'manual';
10
10
  export type FieldReference = string | FormFieldElement;
11
+ export declare const FORM_ERROR_FIELD = "_form";
11
12
  export interface ValidationError {
12
13
  field: string;
13
14
  rule: string;
@@ -23,11 +24,32 @@ export interface SubmitResult {
23
24
  export interface FormResetOptions {
24
25
  clearErrors?: boolean;
25
26
  }
27
+ export interface ValidationIssue {
28
+ field?: string;
29
+ rule?: string;
30
+ message: string;
31
+ source?: ValidationErrorSource;
32
+ }
33
+ export type ValidationIssueInput = string | ValidationIssue | ErrorResponse | Record<string, string> | Array<string | ValidationIssue | ErrorResponse>;
34
+ export interface FormValidatorContext {
35
+ form: Form;
36
+ formElement: HTMLFormElement;
37
+ values: Record<string, any>;
38
+ errors: ValidationError[];
39
+ }
40
+ export type FormValidatorResult = boolean | string | void | ValidationIssue | Array<string | ValidationIssue>;
41
+ export type FormValidator = (context: FormValidatorContext) => FormValidatorResult | Promise<FormValidatorResult>;
42
+ export type FormValidatorPredicate = (context: FormValidatorContext) => boolean | Promise<boolean>;
43
+ export type FormValidatorIssueFactory = string | ValidationIssue | ((context: FormValidatorContext) => string | ValidationIssue | Promise<string | ValidationIssue>);
26
44
  export type ValidationSchema = Record<string, {
27
45
  rules: ValidationRule[];
28
46
  selector?: string;
29
47
  messages?: Record<string, string>;
30
48
  }>;
49
+ export declare function createFormValidator(predicate: FormValidatorPredicate, issue: FormValidatorIssueFactory): FormValidator;
50
+ export declare function sameAsField(field: string, otherField: string, message?: string, rule?: string): FormValidator;
51
+ export declare function requiredIf(field: string, condition: FormValidatorPredicate, message?: string, rule?: string): FormValidator;
52
+ export declare function dateOrder(startField: string, endField: string, message?: string, rule?: string): FormValidator;
31
53
  export type FormFieldElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
32
54
  /** Параметры формы. */
33
55
  export interface FormOptions {
@@ -130,6 +152,8 @@ export interface FormOptions {
130
152
  wrapData?: (data: Record<string, any>) => Record<string, any>;
131
153
  /** Схема валидации: поле → массив правил + override-сообщения */
132
154
  validationSchema?: ValidationSchema;
155
+ /** Валидаторы всей формы: cross-field, conditional required, date ranges и другие проверки над `getValues()`. */
156
+ formValidators?: FormValidator | FormValidator[];
133
157
  }
134
158
  export interface ErrorResponse {
135
159
  name: string;
@@ -192,6 +216,12 @@ export default class Form {
192
216
  setValues(values: Record<string, any>): this;
193
217
  /** Снимает все клиентские и серверные ошибки с формы, не меняя значения полей. */
194
218
  clearErrors(): this;
219
+ private isErrorResponse;
220
+ private normalizeValidationIssues;
221
+ private applyValidationIssue;
222
+ /** Показывает несколько ошибок формы или полей: из backend map/array, `ErrorResponse[]` или form validators. */
223
+ setErrors(issues: ValidationIssueInput, source?: ValidationErrorSource): this;
224
+ private validateFormValidators;
195
225
  /** Сбрасывает форму через native `reset()` и опционально очищает ошибки. */
196
226
  reset(options?: FormResetOptions): this;
197
227
  /**
@@ -242,6 +272,7 @@ export default class Form {
242
272
  private getFieldKey;
243
273
  private hasFieldError;
244
274
  private clearFieldErrorRecord;
275
+ private setErrorRecord;
245
276
  private getValidationStateAttribute;
246
277
  private setFieldValidationState;
247
278
  private startFieldValidation;
@@ -263,7 +294,7 @@ export default class Form {
263
294
  private runFieldValidation;
264
295
  private getRadioGroup;
265
296
  /** Показывает ошибку конкретного поля по имени или DOM-элементу. */
266
- showFieldError(field: FieldReference, message: string, source?: ValidationErrorSource): boolean;
297
+ showFieldError(field: FieldReference, message: string, source?: ValidationErrorSource, rule?: string): boolean;
267
298
  /** Проверяет одно поле по имени или DOM-элементу. */
268
299
  validateField(field: FieldReference): Promise<boolean>;
269
300
  /**
package/docs/en/README.md CHANGED
@@ -50,12 +50,16 @@ const forms = Form.initAll('form[data-form-father]', {
50
50
 
51
51
  ```ts
52
52
  import Form, {
53
+ FORM_ERROR_FIELD,
53
54
  type FormOptions,
54
55
  type ValidationSchema,
55
56
  type ResponseBody,
56
57
  type ValidationError,
57
58
  type SubmitResult,
58
59
  type FormResetOptions,
60
+ type FormValidator,
61
+ type ValidationIssue,
62
+ type FormValidatorPredicate,
59
63
  } from 'form-father';
60
64
  ```
61
65
 
@@ -82,6 +86,7 @@ import Form, {
82
86
  - **validationStateAttribute**: Field state attribute: `validating`, `valid`, or `invalid`. Defaults to
83
87
  `data-form-father-state`.
84
88
  - **observeMutations**: Watches dynamically added fields and submit buttons.
89
+ - **formValidators**: A form validator or array of validators for cross-field rules.
85
90
 
86
91
  ## Form Submission
87
92
 
@@ -101,6 +106,7 @@ import Form, {
101
106
  - **validate()**: Validates the whole form.
102
107
  - **validateField(field)**: Validates one field by name or DOM element.
103
108
  - **showFieldError(field, message, source)**: Shows a field error manually.
109
+ - **setErrors(errors, source)**: Applies backend/form-level errors from an object, array, or `ErrorResponse[]`.
104
110
  - **getErrors()**: Returns current errors as `{ field, rule, message, source }`.
105
111
  - **getValues()**: Returns form values as a plain object.
106
112
  - **setValues(values)**: Fills fields by `name` and dispatches `input`/`change`.
@@ -137,6 +143,40 @@ Final rule order:
137
143
  required → schema.rules → data-validate → data-custom-validate
138
144
  ```
139
145
 
146
+ ### Cross-field and form-level validation
147
+
148
+ ```ts
149
+ import { dateOrder, requiredIf, sameAsField } from 'form-father';
150
+
151
+ const form = new Form($form, {
152
+ formValidators: [
153
+ sameAsField('passwordConfirm', 'password', 'Passwords do not match'),
154
+ requiredIf('email', ({ values }) => Boolean(values.subscribe), 'Email is required for subscription'),
155
+ dateOrder('start', 'end', 'End date must be after start'),
156
+ ],
157
+ });
158
+ ```
159
+
160
+ Return a string or an issue without `field` to show a form-level error. In `getErrors()`, it uses
161
+ `FORM_ERROR_FIELD` (`"_form"`).
162
+
163
+ ```ts
164
+ form.setErrors({
165
+ email: 'Email is already used',
166
+ [FORM_ERROR_FIELD]: 'Please check the form',
167
+ });
168
+ ```
169
+
170
+ ### Rule DX helpers
171
+
172
+ ```ts
173
+ import { createLengthValidator, createPatternValidator, registerFieldValidator } from 'form-father';
174
+
175
+ registerFieldValidator('starts-with-a', value => value.startsWith('A'), 'Must start with A');
176
+ registerValidator('slug', createPatternValidator(/^[a-z0-9-]+$/), 'Slug characters only');
177
+ registerValidator('short-name', createLengthValidator({ min: 2, max: 24 }), 'Use 2-24 characters');
178
+ ```
179
+
140
180
  ## Schema Adapters
141
181
 
142
182
  Form Father has no schema-library runtime dependency, but can wrap Zod/Valibot/Yup-like schemas with `safeParse()` or
@@ -49,6 +49,41 @@ form.clearErrors();
49
49
  form.reset();
50
50
  ```
51
51
 
52
+ ## Cross-field validation
53
+
54
+ ```ts
55
+ import { dateOrder, requiredIf, sameAsField } from 'form-father';
56
+
57
+ const form = new Form(document.querySelector('form')!, {
58
+ formValidators: [
59
+ sameAsField('passwordConfirm', 'password', 'Passwords do not match'),
60
+ requiredIf('email', ({ values }) => Boolean(values.subscribe), 'Email is required'),
61
+ dateOrder('start', 'end', 'End date must be after start'),
62
+ ],
63
+ });
64
+ ```
65
+
66
+ ## Field rule helpers
67
+
68
+ ```ts
69
+ import { createLengthValidator, createPatternValidator, registerFieldValidator, registerValidator } from 'form-father';
70
+
71
+ registerFieldValidator('starts-with-a', value => value.startsWith('A'), 'Must start with A');
72
+ registerValidator('slug', createPatternValidator(/^[a-z0-9-]+$/), 'Slug characters only');
73
+ registerValidator('short-name', createLengthValidator({ min: 2, max: 24 }), 'Use 2-24 characters');
74
+ ```
75
+
76
+ ## Applying backend errors
77
+
78
+ ```ts
79
+ import { FORM_ERROR_FIELD } from 'form-father';
80
+
81
+ form.setErrors({
82
+ email: 'Email is already registered',
83
+ [FORM_ERROR_FIELD]: 'Please check the form',
84
+ });
85
+ ```
86
+
52
87
  ## Async validation state
53
88
 
54
89
  ```css
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form-father",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Form Father: Библиотека для обработки форм",
5
5
  "type": "module",
6
6
  "files": ["dist", "README.md", "LICENSE", "RESPONSE_API.md", "CHANGELOG.md", "docs", "demos"],