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 +19 -3
- package/dist/FormFather.min.js +1 -1
- package/dist/index.d.ts +97 -12
- package/dist/index.js +276 -74
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +37 -13
- package/package.json +1 -1
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**: Функция вызывается при успешном
|
|
75
|
-
- **onResponseUnsuccess**: Функция вызывается при неуспешном
|
|
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
|
|
package/dist/FormFather.min.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var FormFather=function(t){"use strict";function e(t,e,i,r){return new(i||(i=Promise))((function(n,o){function s(t){try{l(r.next(t))}catch(t){o(t)}}function a(t){try{l(r.throw(t))}catch(t){o(t)}}function l(t){var e;t.done?n(t.value):(e=t.value,e instanceof i?e:new i((function(t){t(e)}))).then(s,a)}l((r=r.apply(t,e||[])).next())}))}"function"==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:
|
|
29
|
-
|
|
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:
|
|
125
|
+
onResponse?: (responseBody: ResponseBody, formInstance: Form) => void;
|
|
63
126
|
/**
|
|
64
|
-
* Функция обратного вызова. Запускается после
|
|
127
|
+
* Функция обратного вызова. Запускается после успешного HTTP-ответа и `success: true`.
|
|
65
128
|
*
|
|
66
129
|
* @param responseBody - Тело ответа.
|
|
67
130
|
* @param formInstance - Инстанс формы.
|
|
68
131
|
*/
|
|
69
|
-
onResponseSuccess?: (responseBody:
|
|
132
|
+
onResponseSuccess?: (responseBody: ResponseBody, formInstance: Form) => void;
|
|
70
133
|
/**
|
|
71
|
-
* Функция обратного вызова. Запускается после
|
|
134
|
+
* Функция обратного вызова. Запускается после неуспешного HTTP-ответа или `success !== true`.
|
|
72
135
|
*
|
|
73
136
|
* @param responseBody - Тело ответа.
|
|
74
137
|
* @param formInstance - Инстанс формы.
|
|
75
138
|
*/
|
|
76
|
-
onResponseUnsuccess?: (responseBody:
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
693
|
-
(_a =
|
|
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
|
|
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 =
|
|
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 =
|
|
1145
|
+
body = requestFormData;
|
|
934
1146
|
// ВАЖНО: Не указываем Content-Type, т.к. браузер сам добавит с корректной границей!
|
|
935
1147
|
break;
|
|
936
1148
|
case 'text/plain':
|
|
937
|
-
body =
|
|
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 =
|
|
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
|
-
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
this.
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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
|
-
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
export * from './validators';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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:
|
|
39
|
+
onResponse?: (responseBody: ResponseBody, formInstance: Form) => void;
|
|
38
40
|
/**
|
|
39
|
-
* Функция обратного вызова. Запускается после
|
|
41
|
+
* Функция обратного вызова. Запускается после успешного HTTP-ответа и `success: true`.
|
|
40
42
|
*
|
|
41
43
|
* @param responseBody - Тело ответа.
|
|
42
44
|
* @param formInstance - Инстанс формы.
|
|
43
45
|
*/
|
|
44
|
-
onResponseSuccess?: (responseBody:
|
|
46
|
+
onResponseSuccess?: (responseBody: ResponseBody, formInstance: Form) => void;
|
|
45
47
|
/**
|
|
46
|
-
* Функция обратного вызова. Запускается после
|
|
48
|
+
* Функция обратного вызова. Запускается после неуспешного HTTP-ответа или `success !== true`.
|
|
47
49
|
*
|
|
48
50
|
* @param responseBody - Тело ответа.
|
|
49
51
|
* @param formInstance - Инстанс формы.
|
|
50
52
|
*/
|
|
51
|
-
onResponseUnsuccess?: (responseBody:
|
|
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
|
|
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
|
}
|