form-father 0.2.9 → 0.2.10

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/README.md CHANGED
@@ -36,7 +36,7 @@ npm install form-father
36
36
  ```
37
37
 
38
38
  ```javascript
39
- import Form from 'form-father';
39
+ import Form, { isUrlValid, serializeToFormData } from 'form-father';
40
40
 
41
41
  Form.setDefaultParams({
42
42
  showLoaderButton: false,
@@ -58,6 +58,11 @@ const options = {
58
58
  };
59
59
 
60
60
  const form = new Form(formElement, options);
61
+ const simpleForm = new Form(document.querySelector('#simpleForm')); // options можно не передавать
62
+ ```
63
+
64
+ ```ts
65
+ import Form, { type FormOptions, type ValidationSchema, type ResponseBody } from 'form-father';
61
66
  ```
62
67
 
63
68
  ## Формат ответов сервера
@@ -71,14 +76,24 @@ const form = new Form(formElement, options);
71
76
 
72
77
  - **onSubmit**: Функция обратного вызова, вызываемая при отправке формы.
73
78
  - **onResponse**: Функция обратного вызова при получении ответа от сервера.
74
- - **onResponseSuccess**: Функция вызывается при успешном ответе сервера (статус 200).
75
- - **onResponseUnsuccess**: Функция вызывается при неуспешном ответе сервера (статус не 200).
79
+ - **onResponseSuccess**: Функция вызывается при успешном HTTP-ответе и `success: true`.
80
+ - **onResponseUnsuccess**: Функция вызывается при неуспешном HTTP-ответе, `success !== true` или некорректном JSON.
76
81
  - **showLoaderButton**: Показывать ли лоадер в кнопке отправки. По умолчанию `true`.
77
82
  - **scrollToFirstErroredInput**: Прокручивать ли к первому полю с ошибкой. По умолчанию `true`.
78
83
  - **customTypeError**: Кастомный тип ошибки.
79
84
  - **loaderColor**: Цвет лоадера в кнопке отправки.
80
85
  - **logging**: Нужно ли выводить данные в консоль. По умолчанию `false`.
81
86
 
87
+ ## Отправка формы
88
+
89
+ - Submit-элементами считаются `button[type="submit"]`, `button` без `type`, `input[type="submit"]` и
90
+ `input[type="image"]`.
91
+ - Для `method="GET"` и `method="HEAD"` данные добавляются в query string `action`, тело запроса не отправляется.
92
+ - `wrapData` применяется для всех поддержанных `enctype`: `application/x-www-form-urlencoded`, `multipart/form-data`,
93
+ `text/plain`, `application/json`.
94
+ - `onResponseSuccess` вызывается только при успешном HTTP-ответе и `success: true`.
95
+ - `onResponseUnsuccess` вызывается при неуспешном HTTP-ответе, `success !== true` или некорректном JSON-ответе.
96
+
82
97
  ## Методы
83
98
 
84
99
  - **clearInputs()**: Очищает все поля ввода формы.
@@ -98,6 +113,7 @@ const form = new Form(formElement, options);
98
113
  - **blockScrollBody()**: Блокирует прокрутку страницы.
99
114
  - **unblockScrollBody()**: Разблокирует прокрутку страницы.
100
115
  - **parseCommonResponseProperties(responseBody)**: Обрабатывает общие свойства ответа сервера.
116
+ - **serializeFormToJSON(form)**: Сериализует данные формы в обычный объект.
101
117
 
102
118
  ## Валидаторы
103
119
 
@@ -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"==typeof SuppressedError&&SuppressedError;const i=["http:","https:"];function r(t){return t.length>0&&t.length<=63&&/^[a-z\d-]+$/i.test(t)&&!t.startsWith("-")&&!t.endsWith("-")}function n(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(r)}(t))}function o(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.hasAttribute("data-no-serialize"))return;const{name:e,type:r}=t;if("file"===r)if("true"===t.getAttribute("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}var s;const a=Symbol.for("FormFather.validators"),l=null!==(s=globalThis[a])&&void 0!==s?s:globalThis[a]=new Map;function c(t,e,i,{override:r=!1}={}){!l.has(t)||r?l.set(t,{fn:e,defaultMessage:i}):console.warn(`[FormFather] Validator "${t}" already exists; pass { override: true } to replace`)}const d=t=>l.get(t);var u;c("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}),"Пустое значение"),c("email",(t=>/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)),"Неверный формат"),c("tel",(t=>/^\+7\d{10}$/.test(t)),"Неверный формат"),c("url",(t=>function(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)&&n(t.hostname)}catch(t){return!1}}(t)),"Неверный формат"),c("not-numbers",(t=>!/[0-9]/.test(t)),"Неверный формат");const p=Symbol.for("formfather.shared"),h=null!==(u=globalThis[p])&&void 0!==u?u:globalThis[p]={defaultParams:{},defaultValidationSchema:void 0};let f=0;const m={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 g{static get defaultParams(){return h.defaultParams}static set defaultParams(t){h.defaultParams=t}static get defaultValidationSchema(){var t;return null!==(t=h.defaultValidationSchema)&&void 0!==t?t:h.defaultValidationSchema=m}static set defaultValidationSchema(t){h.defaultValidationSchema=t}constructor(t,i){this.$submits=[],this.waitResponse=!1,this.inputs=null,this._anim={duration:220,easing:"ease",paddingTopOpened:16,iconDuration:700},this.$el=t,this.options=i;this.config=Object.assign({},{onSubmit:()=>{},onResponse:()=>{},onResponseSuccess:()=>{},onResponseUnsuccess:()=>{},customTypeError:null,loaderColor:"#fff",showLoaderButton:!0,scrollToFirstErroredInput:!0,logging:!1,inputSelector:".input",inputWrapperSelector:".input-primary"},g.defaultParams,i),this.$el?(this.$submits=this.findSubmitElements(),this._onSubmitClickHandler=t=>e(this,void 0,void 0,(function*(){var e,i,r,n;t.preventDefault(),null===(i=(e=this.config).onBeforeValidate)||void 0===i||i.call(e,this);const o=yield this.validate();null===(n=(r=this.config).onAfterValidate)||void 0===n||n.call(r,o,this),o&&(yield this.submit())})),this.isCorrectArguments()&&(this.$submits.forEach((t=>{t.addEventListener("click",this._onSubmitClickHandler)})),this.initialization())):console.warn("Empty $el")}getOptions(){return this.options}static setDefaultParams(t){this.defaultParams=Object.assign(Object.assign({},this.defaultParams),t)}findSubmitElements(){return Array.from(this.$el.querySelectorAll('input[type="submit"], button[type="submit"]'))}initialization(){this.inputs=this.$el.querySelectorAll(this.config.inputSelector),this._onSubmitHandler=t=>e(this,void 0,void 0,(function*(){var e,i,r,n;t.preventDefault(),null===(i=(e=this.config).onBeforeValidate)||void 0===i||i.call(e,this);const o=yield this.validate();null===(n=(r=this.config).onAfterValidate)||void 0===n||n.call(r,o,this),o&&this.submit()})),this.$el.addEventListener("submit",this._onSubmitHandler)}showError(t,e){(function(t,e){if(null==t)return null;let i=t;for(;i;){if(i.matches(e))return i;i=i.parentElement}return null})(t,this.config.inputWrapperSelector).showError(e)}showLoader(){this.$submits.forEach((t=>{if(t.classList.add("button--loading"),t.querySelector(".button__loader"))return;const e="form-father-loader-"+f++,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){const e=t.parentElement;e&&e.classList.contains("input__wrapper")&&(e.getAttribute("data-type-error"),e.classList.remove("input__wrapper--error"))}scrollToFirstErroredInput(t){if(t.length>0){const e=Array.from(this.$el.querySelectorAll("input"));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)}}}validate(){return e(this,arguments,void 0,(function*(t=this.$el){const i=Object.assign(Object.assign({},g.defaultValidationSchema||{}),this.config.validationSchema||{}),r=t=>console.warn(`[FormFather] Unknown validation rule "${t}"`),n=[],o=new WeakSet;let s=!0;const a=(t,e=[])=>{var i;const n=[];t.hasAttribute("required")&&n.push("required"),n.push(...e);const o=(null!==(i=t.getAttribute("data-custom-validate"))&&void 0!==i?i:"").split(",").map((t=>t.trim())).filter(Boolean);n.push(...o);const s=[];for(const t of n){const e="string"==typeof t?t:t.rule;d(e)?s.push(t):r(e)}return s},l=(i,r,...o)=>e(this,[i,r,...o],void 0,(function*(e,i,r={}){if(e.hasAttribute("data-no-validate"))return;if(!("checkbox"===e.type||"radio"===e.type?!e.checked:0===e.value.trim().length)||i.includes("required")){for(const o of i){const{rule:i,params:a}="string"==typeof o?{rule:o,params:void 0}:o,l=d(i),c=yield l.fn(e.value,e,t,a),u="object"==typeof c&&null!==c,p=u?!0===c.valid:!!c;if(u&&c.effect&&(yield c.effect({value:e.value,$input:e,$form:t,params:a})),!p){s=!1;const t=c,o=u&&"string"==typeof t.message&&t.message||r[i]||l.defaultMessage;return this.showError(e,o),void n.push(e)}if(u&&c.stopOthers)return}this.hideError(e)}else this.hideError(e)}));for(const[e,r]of Object.entries(i)){const{selector:i,rules:n=[],messages:s={}}=r,c=i?t.querySelectorAll(i):t.querySelectorAll(`[data-validate="${e}"]`);for(const t of Array.from(c))o.has(t)||(o.add(t),yield l(t,a(t,n),s))}const c=Array.from(t.querySelectorAll("[data-custom-validate]")).filter((t=>!o.has(t)));for(const t of c)o.add(t),yield l(t,a(t,[]));return!s&&this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(n.filter((t=>!t.hasAttribute("data-no-error-scroll")))),s}))}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;null===(e=null==t?void 0:t.getAnimations)||void 0===e||e.call(t).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.nextElementSibling===n?(n.style.boxSizing="border-box",n.style.overflow="hidden"):(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.insertAdjacentElement("afterend",n)),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"),n.querySelector(".error-block-under-input");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].nextElementSibling;if(!e||!e.classList.contains("error-block-under-input__wrapper"))return;if((""===e.style.height||"auto"===e.style.height)&&(e.style.height=`${e.clientHeight}px`),e.style.paddingTop=e.style.paddingTop||`${this._anim.paddingTopOpened}px`,e.style.opacity=e.style.opacity||"1",this._cancelAnimations(e),this._canAnimate(e)){e.animate([{height:`${parseFloat(e.style.height)||e.clientHeight}px`,paddingTop:`${parseFloat(e.style.paddingTop)||this._anim.paddingTopOpened}px`,opacity:parseFloat(e.style.opacity||"1")},{height:"0px",paddingTop:"0px",opacity:0}],{duration:this._anim.duration,easing:this._anim.easing,fill:"forwards"}).addEventListener("finish",(()=>{e.remove()}))}else e.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(){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);const t=this.$el.querySelector(".error-block-under-input__wrapper");t&&(this._cancelAnimations(t),t.remove()),this.$submits.forEach((t=>{t.classList.remove("button--loading");const e=t.querySelector(".button__loader");e&&e.remove()}));this.$el.querySelectorAll("input, textarea").forEach((t=>{t instanceof HTMLInputElement&&this.hideError(t)})),document.documentElement.removeAttribute("data-now-scrolling"),this.inputs=null,this.$submits=[],this.waitResponse=!1}isCorrectArguments(){return"FORM"!==this.$el.tagName?(console.warn("$el не является тегом form"),!1):0!==this.$submits.length||(console.warn("В форме нет кнопки с типом submit"),!1)}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,s={};const a=o(this.$el);let l={};if(a.forEach(((t,e)=>{l[e]=t})),this.config.logging)for(const t of a.keys())console.log(a.get(t));switch("function"==typeof this.config.wrapData&&(l=this.config.wrapData(l)),r){case"application/x-www-form-urlencoded":n=new URLSearchParams(o(this.$el)).toString(),s["Content-Type"]="application/x-www-form-urlencoded";break;case"multipart/form-data":n=o(this.$el);break;case"text/plain":n=Array.from(o(this.$el)).map((([t,e])=>`${encodeURIComponent(t)}=${encodeURIComponent(e)}`)).join("\n"),s["Content-Type"]="text/plain";break;case"application/json":n=JSON.stringify(l),s["Content-Type"]="application/json";break;default:console.warn(`Неизвестный enctype: ${r}. Используется application/x-www-form-urlencoded.`),n=new URLSearchParams(o(this.$el)).toString(),s["Content-Type"]="application/x-www-form-urlencoded"}return fetch(e,{method:i,body:n,headers:s})}))}submit(){return e(this,void 0,void 0,(function*(){var t,e,i,r,n,o,s,a;if(!this.waitResponse){!0===this.config.showLoaderButton&&this.showLoader(),null===(e=(t=this.config).onSubmit)||void 0===e||e.call(t,this),this.waitResponse=!0;const l=yield this.sendData(),c=yield l.json();if(this.waitResponse=!1,!0===this.config.showLoaderButton&&this.hideLoader(),null===(r=(i=this.config).onResponse)||void 0===r||r.call(i,c,this),200===l.status){if(!0===c.success)null===(o=(n=this.config).onResponseSuccess)||void 0===o||o.call(n,c,this);else if(null===(a=(s=this.config).onResponseUnsuccess)||void 0===a||a.call(s,c,this),!0===c.error&&(c["error-msg"]&&this.showErrorForm(c["error-msg"]),c.errors)){const t=[];c.errors.forEach((e=>{const i=this.$el.querySelector(`input[name="${e.name}"]`);i?(t.push(i),this.showError(i,e["error-msg"])):console.warn(`Не найдено поле с именем: ${e.name}, для вывода ошибки: ${e["error-msg"]}`)})),!0===this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(t)}!function(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"])}(c)}}}))}}return t.default=g,t.getAllValidators=function(){return new Map(l)},t.getValidator=d,t.registerValidator=c,Object.defineProperty(t,"__esModule",{value:!0}),t}({});
1
+ var FormFather=function(t){"use strict";function e(t,e,r,o){return new(r||(r=Promise))((function(n,i){function s(t){try{l(o.next(t))}catch(t){i(t)}}function a(t){try{l(o.throw(t))}catch(t){i(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((o=o.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 o=["http:","https:"];function n(t){return t.length>0&&t.length<=63&&/^[a-z\d-]+$/i.test(t)&&!t.startsWith("-")&&!t.endsWith("-")}function i(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 o.includes(t.protocol)&&i(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:o}=t;if("file"===o)if(t.multiple){const o=t.files;if(o)for(let t=0;t<o.length;t++)r.append(`${e}[${t}]`,o[t])}else{const o=t.files;o&&o[0]&&r.append(e,o[0])}else{let n;if("select-one"===o||"select-multiple"===o){const i=t;if("select-multiple"===o){Array.from(i.selectedOptions).map((t=>t.value)).forEach(((t,o)=>{r.append(`${e}[${o}]`,t)}))}else n=i.value,n&&r.append(e,n)}else if("tel"===o)n=t.value.split(/[\s()-]/).join("").replace(/^8/,"+7");else if("checkbox"===o||"radio"===o){if(!t.checked)return;n=t.value}else if("date"===o){const e=t.getAttribute("data-date-format");n=e?function(t,e){const r=new Date(t),o=t=>t.toString().padStart(2,"0"),n=o(r.getDate()),i=o(r.getMonth()+1),s=r.getFullYear().toString();return e.replace("dd",n).replace("mm",i).replace("yyyy",s)}(t.value,e):t.value}else n=t.value;n&&"select-one"!==o&&"select-multiple"!==o&&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"])}var c;const d=Symbol.for("FormFather.validators"),u=null!==(c=globalThis[d])&&void 0!==c?c:globalThis[d]=new Map;const p=t=>u.get(t);function h(t,e,r){u.has(t)||u.set(t,{fn:e,defaultMessage:r})}var f;h("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}),"Пустое значение"),h("email",(t=>/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)),"Неверный формат"),h("tel",(t=>/^\+7\d{10}$/.test(t)),"Неверный формат"),h("url",(t=>s(t)),"Неверный формат"),h("not-numbers",(t=>!/[0-9]/.test(t)),"Неверный формат");const m=Symbol.for("formfather.shared"),y=null!==(f=globalThis[m])&&void 0!==f?f:globalThis[m]={defaultParams:{},defaultValidationSchema:void 0};let g=0;const b={tel:{selector:'input[type="tel"]',rules:["tel"]},email:{selector:'input[type="email"]',rules:["email"]},required:{selector:"[required]",rules:["required"]},url:{selector:'input[type="url"]',rules:["url"]},"not-numbers":{selector:'input[data-validate="not-numbers"]',rules:["not-numbers"]}};class v{static get defaultParams(){return y.defaultParams}static set defaultParams(t){y.defaultParams=t}static get defaultValidationSchema(){var t;return null!==(t=y.defaultValidationSchema)&&void 0!==t?t:y.defaultValidationSchema=b}static set defaultValidationSchema(t){y.defaultValidationSchema=t}constructor(t,r={}){this.$submits=[],this.waitResponse=!1,this.inputs=null,this._anim={duration:220,easing:"ease",paddingTopOpened:16,iconDuration:700},this.$el=t,this.options=r;this.config=Object.assign({},{onSubmit:()=>{},onResponse:()=>{},onResponseSuccess:()=>{},onResponseUnsuccess:()=>{},customTypeError:null,loaderColor:"#fff",showLoaderButton:!0,scrollToFirstErroredInput:!0,logging:!1,inputSelector:".input",inputWrapperSelector:".input-primary"},v.defaultParams,r),this.$el?(this.$submits=this.findSubmitElements(),this._onSubmitClickHandler=t=>e(this,void 0,void 0,(function*(){var e,r,o,n;t.preventDefault(),null===(r=(e=this.config).onBeforeValidate)||void 0===r||r.call(e,this);const i=yield this.validate();null===(n=(o=this.config).onAfterValidate)||void 0===n||n.call(o,i,this),i&&(yield this.submit())})),this.isCorrectArguments()&&(this.$submits.forEach((t=>{t.addEventListener("click",this._onSubmitClickHandler)})),this.initialization())):console.warn("Empty $el")}getOptions(){return this.options}static setDefaultParams(t){this.defaultParams=Object.assign(Object.assign({},this.defaultParams),t)}findSubmitElements(){return Array.from(this.$el.querySelectorAll('input[type="submit"], input[type="image"], button[type="submit"], button:not([type])'))}initialization(){this.inputs=this.$el.querySelectorAll(this.config.inputSelector),this._onSubmitHandler=t=>e(this,void 0,void 0,(function*(){var e,r,o,n;t.preventDefault(),null===(r=(e=this.config).onBeforeValidate)||void 0===r||r.call(e,this);const i=yield this.validate();null===(n=(o=this.config).onAfterValidate)||void 0===n||n.call(o,i,this),i&&this.submit()})),this.$el.addEventListener("submit",this._onSubmitHandler)}showError(t,e){const o=r(t,this.config.inputWrapperSelector);if(!o)return void console.warn(`Не найдена обёртка для поля с именем: ${t.name||t.type}`);const n=o.showError;if("function"==typeof n)return void n.call(o,e);t.setAttribute("aria-invalid","true"),o.classList.add("input__wrapper--error");let i=o.querySelector("[data-form-father-error]");i||(i=document.createElement("div"),i.setAttribute("data-form-father-error",""),i.setAttribute("role","alert"),t.insertAdjacentElement("afterend",i)),i.textContent=e}showLoader(){this.$submits.forEach((t=>{if(t.classList.add("button--loading"),t.querySelector(".button__loader"))return;const e="form-father-loader-"+g++,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`,o=document.createElement("span");o.innerHTML=r,o.className="button__loader",t.insertAdjacentElement("afterbegin",o),requestAnimationFrame((()=>{o.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 o=r(t,this.config.inputWrapperSelector)||t.parentElement;t.removeAttribute("aria-invalid"),null===(e=null==o?void 0:o.querySelector("[data-form-father-error]"))||void 0===e||e.remove(),null==o||o.classList.remove("input__wrapper--error"),o&&o.classList.contains("input__wrapper")&&o.getAttribute("data-type-error")}scrollToFirstErroredInput(t){if(t.length>0){const e=Array.from(this.$el.querySelectorAll("input"));let r=null,o=1/0;if(t.forEach((t=>{const n=e.findIndex((e=>e===t));n<o&&(o=n,r=t)})),r){const t=r.getBoundingClientRect(),e=window.pageYOffset||document.documentElement.scrollTop,o=t.top+e-window.innerHeight/2;document.documentElement.setAttribute("data-now-scrolling",""),window.scrollTo({top:o,behavior:"smooth"}),setTimeout((()=>{document.documentElement.removeAttribute("data-now-scrolling")}),800)}}}validate(){return e(this,arguments,void 0,(function*(t=this.$el){const r=Object.assign(Object.assign({},v.defaultValidationSchema||{}),this.config.validationSchema||{}),o=t=>console.warn(`[FormFather] Unknown validation rule "${t}"`),n=[],i=new WeakSet;let s=!0;const a=(t,e=[])=>{var r;const n=[];t.hasAttribute("required")&&n.push("required"),n.push(...e);const i=(null!==(r=t.getAttribute("data-custom-validate"))&&void 0!==r?r:"").split(",").map((t=>t.trim())).filter(Boolean);n.push(...i.map((t=>{const[e,...r]=t.split(":").map((t=>t.trim()));return r.length>0?{rule:e,params:1===r.length?r[0]:r}:e})));const s=[];for(const t of n){const e="string"==typeof t?t:t.rule;p(e)?s.push(t):o(e)}return s},l=(r,o,...i)=>e(this,[r,o,...i],void 0,(function*(e,r,o={}){if(e.hasAttribute("data-no-validate"))return;const i="checkbox"===e.type||"radio"===e.type?!e.checked:0===e.value.trim().length,a=r.some((t=>"string"==typeof t?"required"===t:"required"===t.rule));if(!i||a){for(const i of r){const{rule:r,params:a}="string"==typeof i?{rule:i,params:void 0}:i,l=p(r),c=yield l.fn(e.value,e,t,a),d="object"==typeof c&&null!==c,u=d?!0===c.valid:!!c;if(d&&c.effect&&(yield c.effect({value:e.value,$input:e,$form:t,params:a})),!u){s=!1;const t=c,i=d&&"string"==typeof t.message&&t.message||o[r]||l.defaultMessage;return this.showError(e,i),void n.push(e)}if(d&&c.stopOthers)return}this.hideError(e)}else this.hideError(e)})),c=e=>"radio"===e.type&&e.name?Array.from(t.querySelectorAll('input[type="radio"]')).filter((t=>t.name===e.name)):[];for(const[e,o]of Object.entries(r)){const{selector:r,rules:n=[],messages:s={}}=o,d=r?t.querySelectorAll(r):t.querySelectorAll(`[data-validate="${e}"]`);for(const t of Array.from(d)){if(i.has(t))continue;const e=c(t);e.length>0?(e.forEach((t=>i.add(t))),yield l(t,a(t,n),s)):(i.add(t),yield l(t,a(t,n),s))}}const d=Array.from(t.querySelectorAll("[data-custom-validate]")).filter((t=>!i.has(t)));for(const t of d)i.add(t),yield l(t,a(t,[]));return!s&&this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(n.filter((t=>!t.hasAttribute("data-no-error-scroll")))),s}))}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),o=r[r.length-1];let n=this.$el.querySelector(".error-block-under-input__wrapper");!n||o&&o.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"}),o?o.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 i=n.querySelector(".error-block-under-input__secondary-text");i&&(i.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(){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);const t=this.$el.querySelector(".error-block-under-input__wrapper");t&&(this._cancelAnimations(t),t.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))),document.documentElement.removeAttribute("data-now-scrolling"),this.inputs=null,this.$submits=[],this.waitResponse=!1}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,o)=>{this.appendFormDataValue(t,`${e}[${o}]`,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 o=t.includes("?")?"&":"?";return`${t}${o}${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.showError(r,t["error-msg"])):console.warn(`Не найдено поле с именем: ${t.name}, для вывода ошибки: ${t["error-msg"]}`)})),!0===this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(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",o=this.$el.getAttribute("enctype")||"application/x-www-form-urlencoded";let n=null,i={};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 c="function"==typeof this.config.wrapData;"function"==typeof this.config.wrapData&&(l=this.config.wrapData(l));const d=c?this.objectToFormData(l):s;if(this.isBodylessMethod(r))return fetch(this.appendQueryString(e,this.formDataToUrlSearchParams(d)),{method:r,headers:i});switch(o){case"application/x-www-form-urlencoded":n=this.formDataToUrlSearchParams(d).toString(),i["Content-Type"]="application/x-www-form-urlencoded";break;case"multipart/form-data":n=d;break;case"text/plain":n=this.formDataToTextBody(d),i["Content-Type"]="text/plain";break;case"application/json":n=JSON.stringify(l),i["Content-Type"]="application/json";break;default:console.warn(`Неизвестный enctype: ${o}. Используется application/x-www-form-urlencoded.`),n=this.formDataToUrlSearchParams(d).toString(),i["Content-Type"]="application/x-www-form-urlencoded"}return fetch(e,{method:r,body:n,headers:i})}))}submit(){return e(this,void 0,void 0,(function*(){var t,e,r,o,n,i,s,a;if(!this.waitResponse){!0===this.config.showLoaderButton&&this.showLoader(),null===(e=(t=this.config).onSubmit)||void 0===e||e.call(t,this),this.waitResponse=!0;let c=!1;const d=()=>{c||(this.waitResponse=!1,!0===this.config.showLoaderButton&&this.hideLoader(),c=!0)};try{const t=yield this.sendData(),e=yield this.parseResponseBody(t);d(),null===(o=(r=this.config).onResponse)||void 0===o||o.call(r,e,this),t.ok&&!0===e.success?null===(i=(n=this.config).onResponseSuccess)||void 0===i||i.call(n,e,this):(null===(a=(s=this.config).onResponseUnsuccess)||void 0===a||a.call(s,e,this),this.handleUnsuccessfulResponse(e)),l(e)}catch(t){throw d(),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=r,t.default=v,t.getAllValidators=function(){return new Map(u)},t.getValidator=p,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.registerValidator=function(t,e,r,{override:o=!1}={}){!u.has(t)||o?u.set(t,{fn:e,defaultMessage:r}):console.warn(`[FormFather] Validator "${t}" already exists; pass { override: true } to replace`)},t.serializeFormToJSON=function(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},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
@@ -24,12 +24,75 @@ declare function registerValidator(name: string, fn: ValidatorFn, defaultMessage
24
24
  declare const getValidator: (name: string) => Validator | undefined;
25
25
  declare function getAllValidators(): Map<string, Validator>;
26
26
 
27
+ /**
28
+ * Находит родительский элемент по заданому селектору.
29
+ *
30
+ * @param {HTMLElement | null} $el - Элемент, чего родителя нужно найти.
31
+ * @param {string} selector - Селектор родителя.
32
+ *
33
+ * @returns {HTMLElement | null} Элемент родителя. Но если родителя с таким селектори не было найдено возвращается null.
34
+ */
35
+ declare function closest($el: HTMLElement | null, selector: string): HTMLElement | null;
36
+ /** Включает блокировку скролла страницы */
37
+ declare function blockScrollBody(): void;
38
+ /** Отменяет блокировку скролла страницы */
39
+ declare function unblockScrollBody(): void;
40
+ /**
41
+ * Проверяет является ли строка валидным адресом электронной почты.
42
+ *
43
+ * @param {string} value - Строка, которую нужно проверить.
44
+ *
45
+ * @returns {boolean} Является ли строка валидным адресом электронной почты true/false.
46
+ */
47
+ declare function isEmailValid(value: string): boolean;
48
+ /**
49
+ * Проверяет является ли строка валидным url.
50
+ *
51
+ * @param {string} originalUrlString - Строка, которую нужно проверить.
52
+ *
53
+ * @returns {boolean} Является ли строка валидным url адресом true/false.
54
+ */
55
+ declare function isUrlValid(originalUrlString: string): boolean;
56
+ /**
57
+ * Проверяет является ли строка валидным номером телефона.
58
+ *
59
+ * @param {string} value - Строка, которую нужно проверить.
60
+ *
61
+ * @returns {boolean} Является ли строка валидным номером телефона true/false.
62
+ */
63
+ declare function isPhoneValid(value: string): boolean;
64
+ /**
65
+ * Кастомная сериализация формы в объект FormData.
66
+ *
67
+ * @param {HTMLElement} $element - DOM элемент для сериализации.
68
+ *
69
+ * @returns {FormData} Сериализованные данные формы в объекте FormData.
70
+ */
71
+ declare function serializeToFormData($element: HTMLElement): FormData;
72
+ /**
73
+ * Функция для разбора общих свойств ответа.
74
+ *
75
+ * @param {any} responseBody - Тело ответа.
76
+ */
77
+ declare function parseCommonResponseProperties(responseBody: any): void;
78
+ /**
79
+ * Сериализует данные формы в объект JSON.
80
+ *
81
+ * @param {HTMLFormElement} form - Элемент формы.
82
+ *
83
+ * @returns {Object} Сериализованные данные формы.
84
+ */
85
+ declare function serializeFormToJSON(form: HTMLFormElement): {
86
+ [key: string]: any;
87
+ };
88
+
89
+ type ValidationRule = string | {
90
+ rule: string;
91
+ params?: any;
92
+ };
27
93
  type ValidationSchema = Record<string, {
28
- rules: (string | {
29
- rule: string;
30
- params?: any;
31
- })[];
32
- selector: string;
94
+ rules: ValidationRule[];
95
+ selector?: string;
33
96
  messages?: Record<string, string>;
34
97
  }>;
35
98
  /** Параметры формы. */
@@ -59,21 +122,21 @@ interface FormOptions {
59
122
  * @param responseBody - Тело ответа.
60
123
  * @param formInstance - Инстанс формы.
61
124
  */
62
- onResponse?: (responseBody: any, formInstance: Form) => void;
125
+ onResponse?: (responseBody: ResponseBody, formInstance: Form) => void;
63
126
  /**
64
- * Функция обратного вызова. Запускается после ответа сервера 200.
127
+ * Функция обратного вызова. Запускается после успешного HTTP-ответа и `success: true`.
65
128
  *
66
129
  * @param responseBody - Тело ответа.
67
130
  * @param formInstance - Инстанс формы.
68
131
  */
69
- onResponseSuccess?: (responseBody: any, formInstance: Form) => void;
132
+ onResponseSuccess?: (responseBody: ResponseBody, formInstance: Form) => void;
70
133
  /**
71
- * Функция обратного вызова. Запускается после ответа сервера не 200.
134
+ * Функция обратного вызова. Запускается после неуспешного HTTP-ответа или `success !== true`.
72
135
  *
73
136
  * @param responseBody - Тело ответа.
74
137
  * @param formInstance - Инстанс формы.
75
138
  */
76
- onResponseUnsuccess?: (responseBody: any, formInstance: Form) => void;
139
+ onResponseUnsuccess?: (responseBody: ResponseBody, formInstance: Form) => void;
77
140
  /** Нужно ли показывать loader в кнопке. По умолчанию `true`. */
78
141
  showLoaderButton?: boolean;
79
142
  /** Нужно ли проскроливать до первого по порядку элемента с ошибкой. По умолчанию `true`. */
@@ -96,6 +159,17 @@ interface FormOptions {
96
159
  /** Схема валидации: поле → массив правил + override-сообщения */
97
160
  validationSchema?: ValidationSchema;
98
161
  }
162
+ interface ErrorResponse {
163
+ name: string;
164
+ 'error-msg': string;
165
+ }
166
+ interface ResponseBody {
167
+ [key: string]: any;
168
+ success?: boolean;
169
+ error?: boolean;
170
+ 'error-msg'?: string;
171
+ errors?: ErrorResponse[];
172
+ }
99
173
  /**
100
174
  * Реализует форму отправки данных.
101
175
  *
@@ -120,7 +194,7 @@ declare class Form {
120
194
  * @param {HTMLElement} $el - Элемент формы (тег form!).
121
195
  * @param {Object} options - Параметры формы.
122
196
  */
123
- constructor($el: HTMLElement, options: FormOptions);
197
+ constructor($el: HTMLElement, options?: FormOptions);
124
198
  getOptions(): FormOptions;
125
199
  /**
126
200
  * Обновляет параметры по умолчанию для настроек формы. Метод объединяет переданные параметры с уже существующими
@@ -197,8 +271,19 @@ declare class Form {
197
271
  * @returns {boolean} Корректны данные или нет (true/false).
198
272
  */
199
273
  private isCorrectArguments;
274
+ private formDataToObject;
275
+ private appendFormDataValue;
276
+ private objectToFormData;
277
+ private stringifyFormDataValue;
278
+ private formDataToUrlSearchParams;
279
+ private formDataToTextBody;
280
+ private isBodylessMethod;
281
+ private appendQueryString;
282
+ private findFieldByName;
283
+ private parseResponseBody;
284
+ private handleUnsuccessfulResponse;
200
285
  private sendData;
201
286
  private submit;
202
287
  }
203
288
 
204
- export { type ValidatorEffectCtx, type ValidatorResult, Form as default, getAllValidators, getValidator, registerValidator };
289
+ export { type ErrorResponse, type FormOptions, type ResponseBody, type ValidationRule, type ValidationSchema, type ValidatorEffectCtx, type ValidatorResult, blockScrollBody, closest, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
package/dist/index.js CHANGED
@@ -73,6 +73,28 @@ function closest($el, selector) {
73
73
  }
74
74
  return null;
75
75
  }
76
+ /** Включает блокировку скролла страницы */
77
+ function blockScrollBody() {
78
+ window.lastBlockScrollPosition = window.pageYOffset;
79
+ document.body.style.top = `${-window.lastBlockScrollPosition}px`;
80
+ document.body.classList.add('body--block-scroll');
81
+ }
82
+ /** Отменяет блокировку скролла страницы */
83
+ function unblockScrollBody() {
84
+ document.body.classList.remove('body--block-scroll');
85
+ document.body.style.top = '';
86
+ window.scrollTo(0, window.lastBlockScrollPosition || 0);
87
+ }
88
+ /**
89
+ * Проверяет является ли строка валидным адресом электронной почты.
90
+ *
91
+ * @param {string} value - Строка, которую нужно проверить.
92
+ *
93
+ * @returns {boolean} Является ли строка валидным адресом электронной почты true/false.
94
+ */
95
+ function isEmailValid(value) {
96
+ return /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/iu.test(value);
97
+ }
76
98
  const HTTP_PROTOCOLS = ['http:', 'https:'];
77
99
  function getNormalizedUrlString(originalUrlString) {
78
100
  const trimmedUrl = originalUrlString.trim();
@@ -131,6 +153,23 @@ function isUrlValid(originalUrlString) {
131
153
  return false;
132
154
  }
133
155
  }
156
+ /**
157
+ * Проверяет является ли строка валидным номером телефона.
158
+ *
159
+ * @param {string} value - Строка, которую нужно проверить.
160
+ *
161
+ * @returns {boolean} Является ли строка валидным номером телефона true/false.
162
+ */
163
+ function isPhoneValid(value) {
164
+ const numbersArray = [];
165
+ Array.from(value).forEach(char => {
166
+ if (!!Number(char) || Number(char) === 0)
167
+ numbersArray.push(char);
168
+ });
169
+ if (numbersArray.length < 11 || (numbersArray.length < 12 && value[1] === '+'))
170
+ return false;
171
+ return true;
172
+ }
134
173
  /**
135
174
  * Кастомная сериализация формы в объект FormData.
136
175
  *
@@ -150,12 +189,15 @@ function serializeToFormData($element) {
150
189
  element instanceof HTMLButtonElement)) {
151
190
  return;
152
191
  }
153
- if (!element.name || element.hasAttribute('data-no-serialize')) {
192
+ if (!element.name || element.disabled || element.hasAttribute('data-no-serialize')) {
193
+ return;
194
+ }
195
+ if (element instanceof HTMLButtonElement) {
154
196
  return;
155
197
  }
156
198
  const { name, type } = element;
157
199
  if (type === 'file') {
158
- if (element.getAttribute('multiple') === 'true') {
200
+ if (element.multiple) {
159
201
  const files = element.files;
160
202
  if (files) {
161
203
  for (let i = 0; i < files.length; i++) {
@@ -257,6 +299,29 @@ function parseCommonResponseProperties(responseBody) {
257
299
  }
258
300
  }
259
301
  }
302
+ /**
303
+ * Сериализует данные формы в объект JSON.
304
+ *
305
+ * @param {HTMLFormElement} form - Элемент формы.
306
+ *
307
+ * @returns {Object} Сериализованные данные формы.
308
+ */
309
+ function serializeFormToJSON(form) {
310
+ const obj = {};
311
+ const formData = new FormData(form);
312
+ formData.forEach((value, key) => {
313
+ if (obj.hasOwnProperty(key)) {
314
+ if (!Array.isArray(obj[key])) {
315
+ obj[key] = [obj[key]];
316
+ }
317
+ obj[key].push(value);
318
+ }
319
+ else {
320
+ obj[key] = value;
321
+ }
322
+ });
323
+ return obj;
324
+ }
260
325
 
261
326
  var _a$1;
262
327
  /* ---------- singleton-storage на globalThis ---------- */
@@ -275,7 +340,12 @@ function getAllValidators() {
275
340
  return new Map(validators); // копию, чтобы никто не мутировал оригинал
276
341
  }
277
342
  /* ---------- built-in rules ---------- */
278
- registerValidator('required', (value, $input, $form) => {
343
+ function registerBuiltinValidator(name, fn, defaultMessage) {
344
+ if (!validators.has(name)) {
345
+ validators.set(name, { fn, defaultMessage });
346
+ }
347
+ }
348
+ registerBuiltinValidator('required', (value, $input, $form) => {
279
349
  if ($input && $input.type) {
280
350
  if ($input.type === 'checkbox') {
281
351
  return $input.checked;
@@ -288,10 +358,10 @@ registerValidator('required', (value, $input, $form) => {
288
358
  // fallback — если нет $input (например, в unit-тестах)
289
359
  return value.trim().length > 0;
290
360
  }, 'Пустое значение');
291
- registerValidator('email', v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), 'Неверный формат');
292
- registerValidator('tel', v => /^\+7\d{10}$/.test(v), 'Неверный формат');
293
- registerValidator('url', v => isUrlValid(v), 'Неверный формат');
294
- registerValidator('not-numbers', v => !/[0-9]/.test(v), 'Неверный формат');
361
+ registerBuiltinValidator('email', v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), 'Неверный формат');
362
+ registerBuiltinValidator('tel', v => /^\+7\d{10}$/.test(v), 'Неверный формат');
363
+ registerBuiltinValidator('url', v => isUrlValid(v), 'Неверный формат');
364
+ registerBuiltinValidator('not-numbers', v => !/[0-9]/.test(v), 'Неверный формат');
295
365
 
296
366
  var _a;
297
367
  // --- shared cross-file state (singleton via globalThis) ---
@@ -353,7 +423,7 @@ class Form {
353
423
  * @param {HTMLElement} $el - Элемент формы (тег form!).
354
424
  * @param {Object} options - Параметры формы.
355
425
  */
356
- constructor($el, options) {
426
+ constructor($el, options = {}) {
357
427
  this.$submits = [];
358
428
  this.waitResponse = false;
359
429
  this.inputs = null;
@@ -418,20 +488,10 @@ class Form {
418
488
  this.defaultParams = Object.assign(Object.assign({}, this.defaultParams), params);
419
489
  }
420
490
  findSubmitElements() {
421
- return Array.from(this.$el.querySelectorAll('input[type="submit"], button[type="submit"]'));
491
+ return Array.from(this.$el.querySelectorAll('input[type="submit"], input[type="image"], button[type="submit"], button:not([type])'));
422
492
  }
423
493
  initialization() {
424
494
  this.inputs = this.$el.querySelectorAll(this.config.inputSelector);
425
- /* TODO: Добавить параметр с возможностью отключать/включать кнопку, в зависимости от валидности поля. */
426
- // this.$licensesCheckbox = this.$el.querySelector('input[data-input-name="user-consent"]') as HTMLInputElement;
427
- // if (this.$licensesCheckbox) {
428
- // if (this.$licensesCheckbox.checked) this.enableSubmit();
429
- // else this.disableSubmit();
430
- // this.$licensesCheckbox.addEventListener('change', () => {
431
- // if (this.$licensesCheckbox!.checked) this.enableSubmit();
432
- // else this.disableSubmit();
433
- // });
434
- // }
435
495
  this._onSubmitHandler = (e) => __awaiter(this, void 0, void 0, function* () {
436
496
  var _a, _b, _c, _d;
437
497
  e.preventDefault();
@@ -450,7 +510,26 @@ class Form {
450
510
  * @param {String} text - Текст ошибки.
451
511
  */
452
512
  showError($input, text) {
453
- closest($input, this.config.inputWrapperSelector).showError(text);
513
+ const $inputWrapper = closest($input, this.config.inputWrapperSelector);
514
+ if (!$inputWrapper) {
515
+ console.warn(`Не найдена обёртка для поля с именем: ${$input.name || $input.type}`);
516
+ return;
517
+ }
518
+ const customShowError = $inputWrapper.showError;
519
+ if (typeof customShowError === 'function') {
520
+ customShowError.call($inputWrapper, text);
521
+ return;
522
+ }
523
+ $input.setAttribute('aria-invalid', 'true');
524
+ $inputWrapper.classList.add('input__wrapper--error');
525
+ let $error = $inputWrapper.querySelector('[data-form-father-error]');
526
+ if (!$error) {
527
+ $error = document.createElement('div');
528
+ $error.setAttribute('data-form-father-error', '');
529
+ $error.setAttribute('role', 'alert');
530
+ $input.insertAdjacentElement('afterend', $error);
531
+ }
532
+ $error.textContent = text;
454
533
  }
455
534
  /** Показывает лоадер */
456
535
  showLoader() {
@@ -505,10 +584,13 @@ class Form {
505
584
  * @param {HTMLInputElement} $input - Поле ввода.
506
585
  */
507
586
  hideError($input) {
508
- const $inputWrapper = $input.parentElement;
587
+ var _a;
588
+ const $inputWrapper = closest($input, this.config.inputWrapperSelector) || $input.parentElement;
589
+ $input.removeAttribute('aria-invalid');
590
+ (_a = $inputWrapper === null || $inputWrapper === void 0 ? void 0 : $inputWrapper.querySelector('[data-form-father-error]')) === null || _a === void 0 ? void 0 : _a.remove();
591
+ $inputWrapper === null || $inputWrapper === void 0 ? void 0 : $inputWrapper.classList.remove('input__wrapper--error');
509
592
  if ($inputWrapper && $inputWrapper.classList.contains('input__wrapper')) {
510
593
  $inputWrapper.getAttribute('data-type-error') || 'default';
511
- $inputWrapper.classList.remove('input__wrapper--error');
512
594
  }
513
595
  }
514
596
  /**
@@ -575,7 +657,10 @@ class Form {
575
657
  .split(',')
576
658
  .map(s => s.trim())
577
659
  .filter(Boolean);
578
- list.push(...custom); // 3
660
+ list.push(...custom.map(ruleString => {
661
+ const [rule, ...params] = ruleString.split(':').map(s => s.trim());
662
+ return params.length > 0 ? { rule, params: params.length === 1 ? params[0] : params } : rule;
663
+ })); // 3
579
664
  /* —–– предварительная фильтрация unknown ––– */
580
665
  const filtered = [];
581
666
  for (const r of list) {
@@ -591,7 +676,8 @@ class Form {
591
676
  if ($input.hasAttribute('data-no-validate'))
592
677
  return;
593
678
  const empty = $input.type === 'checkbox' || $input.type === 'radio' ? !$input.checked : $input.value.trim().length === 0;
594
- if (empty && !rules.includes('required')) {
679
+ const hasRequiredRule = rules.some(r => (typeof r === 'string' ? r === 'required' : r.rule === 'required'));
680
+ if (empty && !hasRequiredRule) {
595
681
  this.hideError($input);
596
682
  return;
597
683
  }
@@ -625,6 +711,11 @@ class Form {
625
711
  }
626
712
  this.hideError($input);
627
713
  });
714
+ const getRadioGroup = ($input) => {
715
+ if ($input.type !== 'radio' || !$input.name)
716
+ return [];
717
+ return Array.from($block.querySelectorAll('input[type="radio"]')).filter($radio => $radio.name === $input.name);
718
+ };
628
719
  /* ---------- 1. inputs из schema (config + defaults) ---------- */
629
720
  for (const [key, def] of Object.entries(schema)) {
630
721
  const { selector, rules = [], messages = {} } = def;
@@ -634,6 +725,12 @@ class Form {
634
725
  for (const $input of Array.from(nodeList)) {
635
726
  if (processed.has($input))
636
727
  continue;
728
+ const radioGroup = getRadioGroup($input);
729
+ if (radioGroup.length > 0) {
730
+ radioGroup.forEach($radio => processed.add($radio));
731
+ yield validateInput($input, collectRules($input, rules), messages);
732
+ continue;
733
+ }
637
734
  processed.add($input);
638
735
  yield validateInput($input, collectRules($input, rules), messages);
639
736
  }
@@ -689,8 +786,8 @@ class Form {
689
786
  _cancelAnimations(el) {
690
787
  var _a;
691
788
  // Отменяем все активные WAAPI-анимации у элемента
692
- // @ts-ignore
693
- (_a = el === null || el === void 0 ? void 0 : el.getAnimations) === null || _a === void 0 ? void 0 : _a.call(el).forEach((a) => a.cancel());
789
+ const animatedEl = el;
790
+ (_a = animatedEl === null || animatedEl === void 0 ? void 0 : animatedEl.getAnimations) === null || _a === void 0 ? void 0 : _a.call(animatedEl).forEach(animation => animation.cancel());
694
791
  }
695
792
  /**
696
793
  * Показывает ошибку под полями ввода - ошибку, относящуюся ко всей форме.
@@ -702,7 +799,7 @@ class Form {
702
799
  const inputWrappersList = this.$el.querySelectorAll(this.config.inputWrapperSelector);
703
800
  const $lastInputWrapper = inputWrappersList[inputWrappersList.length - 1];
704
801
  let $errorWrapper = this.$el.querySelector('.error-block-under-input__wrapper');
705
- if (!$errorWrapper || !($lastInputWrapper.nextElementSibling === $errorWrapper)) {
802
+ if (!$errorWrapper || ($lastInputWrapper && !($lastInputWrapper.nextElementSibling === $errorWrapper))) {
706
803
  $errorWrapper = document.createElement('div');
707
804
  $errorWrapper.className = 'error-block-under-input__wrapper';
708
805
  $errorWrapper.innerHTML = `
@@ -725,7 +822,12 @@ class Form {
725
822
  paddingTop: '0px',
726
823
  opacity: '0',
727
824
  });
728
- $lastInputWrapper.insertAdjacentElement('afterend', $errorWrapper);
825
+ if ($lastInputWrapper) {
826
+ $lastInputWrapper.insertAdjacentElement('afterend', $errorWrapper);
827
+ }
828
+ else {
829
+ this.$el.appendChild($errorWrapper);
830
+ }
729
831
  }
730
832
  else {
731
833
  // убедимся, что нужные стартовые inline-стили есть
@@ -740,7 +842,6 @@ class Form {
740
842
  // Сброс success-класса, если он устанавливался где-то в другом коде
741
843
  (_a = $errorWrapper.querySelector('.error-block-under-input')) === null || _a === void 0 ? void 0 : _a.classList.remove('error-block-under-input--success');
742
844
  // Посчитать целевую высоту
743
- $errorWrapper.querySelector('.error-block-under-input');
744
845
  // Чтобы корректно мерить, временно выставим auto
745
846
  const prevHeight = $errorWrapper.style.height;
746
847
  const prevPadding = $errorWrapper.style.paddingTop;
@@ -793,7 +894,7 @@ class Form {
793
894
  hideErrorForm() {
794
895
  const inputWrappersList = this.$el.querySelectorAll(this.config.inputWrapperSelector);
795
896
  const $lastInputWrapper = inputWrappersList[inputWrappersList.length - 1];
796
- const $errorWrapper = $lastInputWrapper.nextElementSibling;
897
+ const $errorWrapper = (($lastInputWrapper === null || $lastInputWrapper === void 0 ? void 0 : $lastInputWrapper.nextElementSibling) || this.$el.querySelector('.error-block-under-input__wrapper'));
797
898
  if (!$errorWrapper || !$errorWrapper.classList.contains('error-block-under-input__wrapper'))
798
899
  return;
799
900
  // Текущая высота (если была auto) — измерим и зафиксируем
@@ -874,11 +975,8 @@ class Form {
874
975
  btnLoader.remove();
875
976
  });
876
977
  // 4) Сбросить ошибки у инпутов (если разметка позволяет)
877
- const inputs = this.$el.querySelectorAll('input, textarea');
878
- inputs.forEach($i => {
879
- if ($i instanceof HTMLInputElement)
880
- this.hideError($i);
881
- });
978
+ const inputs = this.$el.querySelectorAll('input, textarea, select');
979
+ inputs.forEach($i => this.hideError($i));
882
980
  // 5) Убрать возможный флаг скролла
883
981
  document.documentElement.removeAttribute('data-now-scrolling');
884
982
  // 6) Очистить ссылки на элементы, чтобы помочь GC
@@ -902,6 +1000,115 @@ class Form {
902
1000
  }
903
1001
  return true;
904
1002
  }
1003
+ formDataToObject(formData) {
1004
+ const data = {};
1005
+ formData.forEach((value, key) => {
1006
+ if (Object.prototype.hasOwnProperty.call(data, key)) {
1007
+ if (!Array.isArray(data[key])) {
1008
+ data[key] = [data[key]];
1009
+ }
1010
+ data[key].push(value);
1011
+ }
1012
+ else {
1013
+ data[key] = value;
1014
+ }
1015
+ });
1016
+ return data;
1017
+ }
1018
+ appendFormDataValue(formData, key, value) {
1019
+ if (value === undefined || value === null)
1020
+ return;
1021
+ if (Array.isArray(value)) {
1022
+ value.forEach((item, index) => {
1023
+ this.appendFormDataValue(formData, `${key}[${index}]`, item);
1024
+ });
1025
+ return;
1026
+ }
1027
+ if (value instanceof Blob) {
1028
+ formData.append(key, value);
1029
+ return;
1030
+ }
1031
+ formData.append(key, typeof value === 'object' ? JSON.stringify(value) : String(value));
1032
+ }
1033
+ objectToFormData(data) {
1034
+ const formData = new FormData();
1035
+ Object.entries(data).forEach(([key, value]) => {
1036
+ this.appendFormDataValue(formData, key, value);
1037
+ });
1038
+ return formData;
1039
+ }
1040
+ stringifyFormDataValue(value) {
1041
+ return typeof value === 'string' ? value : value.name;
1042
+ }
1043
+ formDataToUrlSearchParams(formData) {
1044
+ const params = new URLSearchParams();
1045
+ formData.forEach((value, key) => {
1046
+ params.append(key, this.stringifyFormDataValue(value));
1047
+ });
1048
+ return params;
1049
+ }
1050
+ formDataToTextBody(formData) {
1051
+ return Array.from(formData)
1052
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(this.stringifyFormDataValue(value))}`)
1053
+ .join('\n');
1054
+ }
1055
+ isBodylessMethod(method) {
1056
+ return method === 'GET' || method === 'HEAD';
1057
+ }
1058
+ appendQueryString(action, params) {
1059
+ const query = params.toString();
1060
+ if (!query)
1061
+ return action;
1062
+ const separator = action.includes('?') ? '&' : '?';
1063
+ return `${action}${separator}${query}`;
1064
+ }
1065
+ findFieldByName(name) {
1066
+ const fields = Array.from(this.$el.querySelectorAll('input, textarea, select'));
1067
+ return fields.find($field => $field.name === name) || null;
1068
+ }
1069
+ parseResponseBody(response) {
1070
+ return __awaiter(this, void 0, void 0, function* () {
1071
+ try {
1072
+ const responseBody = yield response.json();
1073
+ return responseBody && typeof responseBody === 'object'
1074
+ ? responseBody
1075
+ : {
1076
+ success: false,
1077
+ error: true,
1078
+ 'error-msg': 'Некорректный ответ сервера',
1079
+ };
1080
+ }
1081
+ catch (_a) {
1082
+ return {
1083
+ success: false,
1084
+ error: true,
1085
+ 'error-msg': 'Некорректный ответ сервера',
1086
+ };
1087
+ }
1088
+ });
1089
+ }
1090
+ handleUnsuccessfulResponse(responseBody) {
1091
+ if (responseBody.error !== true)
1092
+ return;
1093
+ if (responseBody['error-msg']) {
1094
+ this.showErrorForm(responseBody['error-msg']);
1095
+ }
1096
+ if (responseBody.errors) {
1097
+ const fieldsList = [];
1098
+ responseBody.errors.forEach(error => {
1099
+ const $field = this.findFieldByName(error.name);
1100
+ if ($field) {
1101
+ fieldsList.push($field);
1102
+ this.showError($field, error['error-msg']);
1103
+ }
1104
+ else {
1105
+ console.warn(`Не найдено поле с именем: ${error.name}, для вывода ошибки: ${error['error-msg']}`);
1106
+ }
1107
+ });
1108
+ if (this.config.scrollToFirstErroredInput === true)
1109
+ this.scrollToFirstErroredInput(fieldsList);
1110
+ }
1111
+ }
905
1112
  sendData() {
906
1113
  return __awaiter(this, void 0, void 0, function* () {
907
1114
  var _a;
@@ -912,31 +1119,34 @@ class Form {
912
1119
  let headers = {};
913
1120
  // Сбор данных из формы
914
1121
  const formData = serializeToFormData(this.$el);
915
- let data = {};
916
- formData.forEach((value, key) => {
917
- data[key] = value;
918
- });
1122
+ let data = this.formDataToObject(formData);
919
1123
  if (this.config.logging) {
920
1124
  for (const key of formData.keys())
921
1125
  console.log(formData.get(key));
922
1126
  }
923
1127
  // Применение обёртки, если указано в настройках
1128
+ const hasDataWrapper = typeof this.config.wrapData === 'function';
924
1129
  if (typeof this.config.wrapData === 'function') {
925
1130
  data = this.config.wrapData(data);
926
1131
  }
1132
+ const requestFormData = hasDataWrapper ? this.objectToFormData(data) : formData;
1133
+ if (this.isBodylessMethod(method)) {
1134
+ return fetch(this.appendQueryString(action, this.formDataToUrlSearchParams(requestFormData)), {
1135
+ method,
1136
+ headers,
1137
+ });
1138
+ }
927
1139
  switch (enctype) {
928
1140
  case 'application/x-www-form-urlencoded':
929
- body = new URLSearchParams(serializeToFormData(this.$el)).toString();
1141
+ body = this.formDataToUrlSearchParams(requestFormData).toString();
930
1142
  headers['Content-Type'] = 'application/x-www-form-urlencoded';
931
1143
  break;
932
1144
  case 'multipart/form-data':
933
- body = serializeToFormData(this.$el);
1145
+ body = requestFormData;
934
1146
  // ВАЖНО: Не указываем Content-Type, т.к. браузер сам добавит с корректной границей!
935
1147
  break;
936
1148
  case 'text/plain':
937
- body = Array.from(serializeToFormData(this.$el))
938
- .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
939
- .join('\n');
1149
+ body = this.formDataToTextBody(requestFormData);
940
1150
  headers['Content-Type'] = 'text/plain';
941
1151
  break;
942
1152
  case 'application/json':
@@ -945,7 +1155,7 @@ class Form {
945
1155
  break;
946
1156
  default:
947
1157
  console.warn(`Неизвестный enctype: ${enctype}. Используется application/x-www-form-urlencoded.`);
948
- body = new URLSearchParams(serializeToFormData(this.$el)).toString();
1158
+ body = this.formDataToUrlSearchParams(requestFormData).toString();
949
1159
  headers['Content-Type'] = 'application/x-www-form-urlencoded';
950
1160
  }
951
1161
  return fetch(action, {
@@ -963,45 +1173,37 @@ class Form {
963
1173
  this.showLoader();
964
1174
  (_b = (_a = this.config).onSubmit) === null || _b === void 0 ? void 0 : _b.call(_a, this);
965
1175
  this.waitResponse = true;
966
- const response = yield this.sendData();
967
- const responseBody = yield response.json();
968
- this.waitResponse = false;
969
- if (this.config.showLoaderButton === true)
970
- this.hideLoader();
971
- (_d = (_c = this.config).onResponse) === null || _d === void 0 ? void 0 : _d.call(_c, responseBody, this);
972
- if (response.status === 200) {
973
- if (responseBody.success === true) {
1176
+ let loadingFinished = false;
1177
+ const finishLoading = () => {
1178
+ if (loadingFinished)
1179
+ return;
1180
+ this.waitResponse = false;
1181
+ if (this.config.showLoaderButton === true)
1182
+ this.hideLoader();
1183
+ loadingFinished = true;
1184
+ };
1185
+ try {
1186
+ const response = yield this.sendData();
1187
+ const responseBody = yield this.parseResponseBody(response);
1188
+ finishLoading();
1189
+ (_d = (_c = this.config).onResponse) === null || _d === void 0 ? void 0 : _d.call(_c, responseBody, this);
1190
+ if (response.ok && responseBody.success === true) {
974
1191
  (_f = (_e = this.config).onResponseSuccess) === null || _f === void 0 ? void 0 : _f.call(_e, responseBody, this);
975
1192
  }
976
1193
  else {
977
1194
  (_h = (_g = this.config).onResponseUnsuccess) === null || _h === void 0 ? void 0 : _h.call(_g, responseBody, this);
978
- if (responseBody.error === true) {
979
- if (responseBody['error-msg']) {
980
- this.showErrorForm(responseBody['error-msg']);
981
- }
982
- if (responseBody.errors) {
983
- const inputsList = [];
984
- responseBody.errors.forEach(error => {
985
- const $input = this.$el.querySelector(`input[name="${error.name}"]`);
986
- if ($input) {
987
- inputsList.push($input);
988
- this.showError($input, error['error-msg']);
989
- }
990
- else {
991
- console.warn(`Не найдено поле с именем: ${error.name}, для вывода ошибки: ${error['error-msg']}`);
992
- }
993
- });
994
- if (this.config.scrollToFirstErroredInput === true)
995
- this.scrollToFirstErroredInput(inputsList);
996
- }
997
- }
1195
+ this.handleUnsuccessfulResponse(responseBody);
998
1196
  }
999
1197
  parseCommonResponseProperties(responseBody);
1000
1198
  }
1199
+ catch (error) {
1200
+ finishLoading();
1201
+ throw error;
1202
+ }
1001
1203
  }
1002
1204
  });
1003
1205
  }
1004
1206
  }
1005
1207
 
1006
- export { Form as default, getAllValidators, getValidator, registerValidator };
1208
+ export { blockScrollBody, closest, Form as default, getAllValidators, getValidator, isEmailValid, isPhoneValid, isUrlValid, parseCommonResponseProperties, registerValidator, serializeFormToJSON, serializeToFormData, unblockScrollBody };
1007
1209
  //# 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,14 +1,16 @@
1
1
  export * from './validators';
2
- type ValidationSchema = Record<string, {
3
- rules: (string | {
4
- rule: string;
5
- params?: any;
6
- })[];
7
- selector: string;
2
+ export { serializeToFormData, isEmailValid, isUrlValid, parseCommonResponseProperties, closest, isPhoneValid, blockScrollBody, unblockScrollBody, serializeFormToJSON, } from './helpers';
3
+ export type ValidationRule = string | {
4
+ rule: string;
5
+ params?: any;
6
+ };
7
+ export type ValidationSchema = Record<string, {
8
+ rules: ValidationRule[];
9
+ selector?: string;
8
10
  messages?: Record<string, string>;
9
11
  }>;
10
12
  /** Параметры формы. */
11
- interface FormOptions {
13
+ export interface FormOptions {
12
14
  /**
13
15
  * Функция обратного вызова. Запускается, перед началом валидации формы.
14
16
  *
@@ -34,21 +36,21 @@ interface FormOptions {
34
36
  * @param responseBody - Тело ответа.
35
37
  * @param formInstance - Инстанс формы.
36
38
  */
37
- onResponse?: (responseBody: any, formInstance: Form) => void;
39
+ onResponse?: (responseBody: ResponseBody, formInstance: Form) => void;
38
40
  /**
39
- * Функция обратного вызова. Запускается после ответа сервера 200.
41
+ * Функция обратного вызова. Запускается после успешного HTTP-ответа и `success: true`.
40
42
  *
41
43
  * @param responseBody - Тело ответа.
42
44
  * @param formInstance - Инстанс формы.
43
45
  */
44
- onResponseSuccess?: (responseBody: any, formInstance: Form) => void;
46
+ onResponseSuccess?: (responseBody: ResponseBody, formInstance: Form) => void;
45
47
  /**
46
- * Функция обратного вызова. Запускается после ответа сервера не 200.
48
+ * Функция обратного вызова. Запускается после неуспешного HTTP-ответа или `success !== true`.
47
49
  *
48
50
  * @param responseBody - Тело ответа.
49
51
  * @param formInstance - Инстанс формы.
50
52
  */
51
- onResponseUnsuccess?: (responseBody: any, formInstance: Form) => void;
53
+ onResponseUnsuccess?: (responseBody: ResponseBody, formInstance: Form) => void;
52
54
  /** Нужно ли показывать loader в кнопке. По умолчанию `true`. */
53
55
  showLoaderButton?: boolean;
54
56
  /** Нужно ли проскроливать до первого по порядку элемента с ошибкой. По умолчанию `true`. */
@@ -71,6 +73,17 @@ interface FormOptions {
71
73
  /** Схема валидации: поле → массив правил + override-сообщения */
72
74
  validationSchema?: ValidationSchema;
73
75
  }
76
+ export interface ErrorResponse {
77
+ name: string;
78
+ 'error-msg': string;
79
+ }
80
+ export interface ResponseBody {
81
+ [key: string]: any;
82
+ success?: boolean;
83
+ error?: boolean;
84
+ 'error-msg'?: string;
85
+ errors?: ErrorResponse[];
86
+ }
74
87
  /**
75
88
  * Реализует форму отправки данных.
76
89
  *
@@ -95,7 +108,7 @@ export default class Form {
95
108
  * @param {HTMLElement} $el - Элемент формы (тег form!).
96
109
  * @param {Object} options - Параметры формы.
97
110
  */
98
- constructor($el: HTMLElement, options: FormOptions);
111
+ constructor($el: HTMLElement, options?: FormOptions);
99
112
  getOptions(): FormOptions;
100
113
  /**
101
114
  * Обновляет параметры по умолчанию для настроек формы. Метод объединяет переданные параметры с уже существующими
@@ -172,6 +185,17 @@ export default class Form {
172
185
  * @returns {boolean} Корректны данные или нет (true/false).
173
186
  */
174
187
  private isCorrectArguments;
188
+ private formDataToObject;
189
+ private appendFormDataValue;
190
+ private objectToFormData;
191
+ private stringifyFormDataValue;
192
+ private formDataToUrlSearchParams;
193
+ private formDataToTextBody;
194
+ private isBodylessMethod;
195
+ private appendQueryString;
196
+ private findFieldByName;
197
+ private parseResponseBody;
198
+ private handleUnsuccessfulResponse;
175
199
  private sendData;
176
200
  private submit;
177
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form-father",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "Form Father: Библиотека для обработки форм",
5
5
  "type": "module",
6
6
  "files": ["dist", "README.md", "LICENSE", "RESPONSE_API.md"],