form-father 0.2.7 → 0.2.9

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
@@ -3,6 +3,8 @@
3
3
  [![npm version](https://img.shields.io/npm/v/form-father)](https://www.npmjs.com/package/form-father)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/form-father)](https://www.npmjs.com/package/form-father)
5
5
 
6
+ - [🇬🇧 English documentation](https://github.com/Poliklot/form-father/blob/master/docs/en/README.md)
7
+
6
8
  **Form Father** — это библиотека для обработки форм на чистом JavaScript, обеспечивающая удобную валидацию и отправку
7
9
  форм с использованием TypeScript.
8
10
 
@@ -16,13 +18,31 @@ npm install form-father
16
18
 
17
19
  ## Использование
18
20
 
21
+ ```html
22
+ <form
23
+ novalidate
24
+ onsubmit="return false"
25
+ method="post"
26
+ action="/callback.php"
27
+ enctype="multipart/form-data"
28
+ >
29
+
30
+ <!-- inputs -->
31
+
32
+ <button type="submit">
33
+ Отправить
34
+ </button>
35
+ </form>
36
+ ```
37
+
19
38
  ```javascript
20
39
  import Form from 'form-father';
21
40
 
22
41
  Form.setDefaultParams({
23
- showLoaderButton: false,
24
- scrollToFirstErroredInput: false,
25
- logging: true,
42
+ showLoaderButton: false,
43
+ scrollToFirstErroredInput: false,
44
+ logging: false,
45
+ loaderColor: 'currentColor',
26
46
  });
27
47
 
28
48
  const formElement = document.querySelector('#myForm');
@@ -32,6 +52,7 @@ const options = {
32
52
  },
33
53
  onResponse: (responseBody, formInstance) => {
34
54
  // Действия при получении ответа от сервера
55
+ formInstance.clearInputs();
35
56
  },
36
57
  // Другие опции...
37
58
  };
@@ -39,6 +60,13 @@ const options = {
39
60
  const form = new Form(formElement, options);
40
61
  ```
41
62
 
63
+ ## Формат ответов сервера
64
+
65
+ Данная библиотека абстрагирует процесс парсинга HTTP-ответов и выполняет действия на их основе. Для её корректной работы
66
+ сервер должен возвращать ответы в строго определённом формате. Полное описание формата (схемы) ответа от сервера
67
+ представлено в данном техническом документе:
68
+ [Спецификация формата ответов API.](https://github.com/Poliklot/form-father/blob/master/RESPONSE_API.md)
69
+
42
70
  ## Опции
43
71
 
44
72
  - **onSubmit**: Функция обратного вызова, вызываемая при отправке формы.
@@ -50,11 +78,13 @@ const form = new Form(formElement, options);
50
78
  - **customTypeError**: Кастомный тип ошибки.
51
79
  - **loaderColor**: Цвет лоадера в кнопке отправки.
52
80
  - **logging**: Нужно ли выводить данные в консоль. По умолчанию `false`.
53
- - **setDefaultParams(params)**: Метод setDefaultParams используется для установки значений по умолчанию для всех экземпляров формы. Эти параметры можно переопределить при инициализации конкретной формы.
54
81
 
55
82
  ## Методы
56
83
 
57
84
  - **clearInputs()**: Очищает все поля ввода формы.
85
+ - **setDefaultParams(params)**: Метод setDefaultParams используется для установки значений по умолчанию для всех
86
+ экземпляров формы. Эти параметры можно переопределить при инициализации конкретной формы.
87
+ - **destroy()**: Очищает инициализацию формы.
58
88
 
59
89
  ## Хелперы
60
90
 
@@ -62,11 +92,179 @@ const form = new Form(formElement, options);
62
92
 
63
93
  - **serializeToFormData($element)**: Сериализует данные формы в объект `FormData`.
64
94
  - **isEmailValid(value)**: Проверяет, является ли строка валидным адресом электронной почты.
65
- - **isUrlValid(value)**: Проверяет, является ли строка валидным URL.
95
+ - **isUrlValid(value)**: Проверяет `http(s)`-адрес, домен, IP, `localhost`, схема может быть опущена.
66
96
  - **isPhoneValid(value)**: Проверяет, является ли строка валидным номером телефона.
67
97
  - **closest($el, selector)**: Находит ближайший родительский элемент по заданному селектору.
68
98
  - **blockScrollBody()**: Блокирует прокрутку страницы.
69
99
  - **unblockScrollBody()**: Разблокирует прокрутку страницы.
70
100
  - **parseCommonResponseProperties(responseBody)**: Обрабатывает общие свойства ответа сервера.
71
101
 
72
- © 2024 Poliklot
102
+ ## Валидаторы
103
+
104
+ Form Father поставляется с базовым набором правил валидации, а также позволяет легко добавлять и переопределять
105
+ собственные.
106
+
107
+ ### Базовые правила
108
+
109
+ | Ключ | Проверка | Сообщение по умолчанию |
110
+ | ------------- | ------------------------------- | ---------------------- |
111
+ | `required` | значение непустое | **Пустое значение** |
112
+ | `email` | валидный e-mail | **Неверный формат** |
113
+ | `tel` | российский номер `+7XXXXXXXXXX` | **Неверный формат** |
114
+ | `url` | домен/IP/localhost, схема optional | **Неверный формат** |
115
+ | `not-numbers` | строка без цифр | **Неверный формат** |
116
+
117
+ > [!NOTE] Полный список валидаторов можно получить так:
118
+ >
119
+ > ```js
120
+ > import { getAllValidators } from 'form-father';
121
+ >
122
+ > const all = getAllValidators();
123
+ > console.log([...all.keys()]);
124
+ > ```
125
+
126
+ ---
127
+
128
+ ### Глобальная схема (`Form.defaultValidationSchema`)
129
+
130
+ ```ts
131
+ Form.defaultValidationSchema = {
132
+ email: {
133
+ selector: 'input[type="email"]', // CSS-селектор, по которому выбираются inputs
134
+ rules: ['required', 'email'],
135
+ },
136
+ tel: {
137
+ selector: 'input[type="tel"]',
138
+ rules: ['required', 'tel'],
139
+ },
140
+ };
141
+ ```
142
+
143
+ - **selector** — произвольный CSS. Если он указан, правила применяются ко всем элементам, соответствующим этому
144
+ селектору. Если селектора нет, библиотека использует атрибут `data-validate`.
145
+ - Ту же схему можно переопределить/дополнить локально через `validationSchema` при инициализации формы.
146
+
147
+ ---
148
+
149
+ ### Локальная схема (`validationSchema`)
150
+
151
+ ```ts
152
+ const form = new Form($form, {
153
+ validationSchema: {
154
+ tel: {
155
+ // ключ произвольный
156
+ selector: '[data-validate="tel"]',
157
+ rules: ['tel', 'begin+79277'],
158
+ messages: {
159
+ tel: 'Телефон в формате +7…',
160
+ 'begin+79277': 'Номер должен начинаться на +79277',
161
+ },
162
+ },
163
+ },
164
+ });
165
+ ```
166
+
167
+ _Порядок применения для одного `<input>`:_
168
+
169
+ 1. `required` (если атрибут `required`)
170
+ 2. правила из `validationSchema`
171
+ 3. правила из `data-custom-validate`
172
+
173
+ ---
174
+
175
+ ### Правило через `data-custom-validate`
176
+
177
+ ```html
178
+ <input name="code" data-custom-validate="only-digits:6" required />
179
+ ```
180
+
181
+ - Если правило неизвестно, в консоль выводится `[FormFather] Unknown validation rule "foo"`.
182
+
183
+ ---
184
+
185
+ ### Собственный валидатор
186
+
187
+ ```ts
188
+ import { registerValidator } from 'form-father';
189
+
190
+ registerValidator(
191
+ 'only-digits',
192
+ (v, _input, _form, len) => new RegExp(`^\d{${len}}$`).test(v),
193
+ 'Допустимы только цифры',
194
+ );
195
+ ```
196
+
197
+ Теперь можно использовать:
198
+
199
+ ```html
200
+ <input name="code" data-custom-validate="only-digits:6" required />
201
+ ```
202
+
203
+ ---
204
+
205
+ ### Переопределение валидатора
206
+
207
+ ```ts
208
+ registerValidator(
209
+ 'tel',
210
+ v => /^8\d{10}$/.test(v), // новая проверка
211
+ 'Номер должен начинаться на 8…', // новое сообщение
212
+ { override: true }, // ← обязательно!
213
+ );
214
+ ```
215
+
216
+ _Без `override: true` библиотека оставит старую версию и выведет warn._
217
+
218
+ ---
219
+
220
+ ### Изменение только текста ошибки
221
+
222
+ #### Глобально
223
+
224
+ ```ts
225
+ const vd = getValidator('email');
226
+ if (vd) vd.defaultMessage = 'Некорректный e-mail';
227
+ ```
228
+
229
+ #### Локально для формы
230
+
231
+ ```ts
232
+ validationSchema: {
233
+ email: {
234
+ selector: 'input[type="email"]',
235
+ rules: ['email'],
236
+ messages: { email: 'Некорректный e-mail' },
237
+ },
238
+ }
239
+ ```
240
+
241
+ ---
242
+
243
+ ### Итоговый порядок проверки
244
+
245
+ ```
246
+ required → schema.rules → data-custom-validate
247
+ ```
248
+
249
+ - Для каждого поля отображается **только первое** найденное сообщение об ошибке.
250
+ - Неизвестные правила логируются и пропускаются до начала проверки, поэтому не влияют на результат валидации.
251
+
252
+ ## Сборка и разработка
253
+
254
+ Для сборки проекта используется **Rollup**. Основные команды:
255
+
256
+ - **npm run build**: Сборка проекта.
257
+ - **npm run watch**: Сборка в режиме наблюдения.
258
+ - **npm run demos**: Запуск демо-версии с использованием Vite.
259
+
260
+ ## Вклад
261
+
262
+ Будем рады вашим предложениям и улучшениям! Пожалуйста, создавайте
263
+ [issues](https://github.com/poliklot/form-father/issues) и отправляйте
264
+ [pull requests](https://github.com/poliklot/form-father/pulls).
265
+
266
+ ## Лицензия
267
+
268
+ [MIT](LICENSE)
269
+
270
+ © 2025 Poliklot
@@ -0,0 +1,233 @@
1
+ ## Краткое введение
2
+
3
+ Эй, ребята! Собрались здесь для того, чтобы понять, как у нас отвечает сервер после того, как отправляем формы через
4
+ AJAX. Это будет не какая-то скучная докуха, а всё по-кубански, с размахом и только по сути!
5
+
6
+ ## Что и как 🛠️
7
+
8
+ ### `success`
9
+
10
+ - **Тип**: boolean.
11
+ - **Описание**: Флаг, показывающий, всё ли круто.
12
+ - **Как Работает**: Если сервер справился, то `true`, а если нет – `false`.
13
+
14
+ ### `redirect-url`
15
+
16
+ - **Тип**: string (опционально).
17
+ - **Описание**: Куда пойти, если всё зашибись.
18
+ - **Когда Появляется**: Только если `success` равно `true`.
19
+
20
+ ### `redirect-url-delay`
21
+
22
+ - **Тип**: number (опционально).
23
+ - **Описание**: Задержка в ms перед редиректом.
24
+
25
+ ### `reload`
26
+
27
+ - **Тип**: boolean (опционально).
28
+ - **Описание**: Как перезагрузить страничку.
29
+ - **Когда Используем**: Когда сервер скажет "обновись".
30
+
31
+ ### `reload-delay`
32
+
33
+ - **Тип**: number (опционально).
34
+ - **Описание**: Задержка в ms перед перезагрузкой страницы.
35
+
36
+ ### `error`
37
+
38
+ - **Тип**: boolean (опционально).
39
+ - **Описание**: Флаг о проблемах.
40
+ - **Как Работает**: Если есть косяк, то `true`.
41
+
42
+ ### `error-msg`
43
+
44
+ - **Тип**: string (опционально).
45
+ - **Описание**: Пояснения от сервера о косяке.
46
+ - **Пример**: "Тут беда, братишка".
47
+
48
+ ### `errors`
49
+
50
+ - **Тип**: array (опционально).
51
+ - **Описание**: Список мелких недоразумений.
52
+ - **Содержание**: Конкретные поля и тексты ошибок.
53
+
54
+ Каждый объект в массиве содержит:
55
+
56
+ - `name` (string): Имя поля, в котором произошла ошибка.
57
+ - `error-msg` (string): Текст ошибки для данного поля.
58
+
59
+ ### `error-toast`
60
+
61
+ - Тип: string (опционально).
62
+ - Описание: Выводится всплывашка(toast) с ошибкой.
63
+ - Пример: "Тут беда, братишка".
64
+
65
+ > [!Caution]
66
+ > `error-toast` DEPRECATED!!!
67
+
68
+ ### `toast`
69
+ - Тип: объект | массив объектов.
70
+ - Описание: Выводит определённый тип тоста и передаёт опции.
71
+ - Пример, один тост:
72
+ ```
73
+ {
74
+ type: 'success',
75
+ title: 'Успешно',
76
+ }
77
+ ```
78
+
79
+ - Пример, несколько тостов за раз:
80
+ ```
81
+ [
82
+ {
83
+ type: 'success',
84
+ title: 'Успешно',
85
+ },
86
+ {
87
+ type: 'error',
88
+ title: 'Ошибка',
89
+ }
90
+ ]
91
+ ```
92
+
93
+
94
+ ## Примеры ответов 📝
95
+
96
+ ### `Когда всё чик-пук, и можно идти дальше.`
97
+
98
+ ```json
99
+ {
100
+ "success": true,
101
+ }
102
+ ```
103
+
104
+ ### `Если нужен редирект.`
105
+
106
+ ```json
107
+ {
108
+ "success": true,
109
+ "redirect-url": "https://example.com/success",
110
+ "redirect-url-delay": 5000, // если нужна задержка перед редиректом
111
+ }
112
+ ```
113
+
114
+ ### `Если нужна перезагрузка страницы.`
115
+
116
+ ```json
117
+ {
118
+ "success": true,
119
+ "reload": true,
120
+ "reload-delay": 5000, // если нужна задержка перед обновлением страницы
121
+ }
122
+ ```
123
+
124
+ ### `Когда надо что-то менять.`
125
+
126
+ ```json
127
+ {
128
+ "success": false,
129
+ "error": true,
130
+ "error-msg": "Тут беда, братишка",
131
+ "errors": [
132
+ {
133
+ "name": "email",
134
+ "error-msg": "Эй, мэйл какой-то кривой"
135
+ }
136
+ ]
137
+ }
138
+ ```
139
+
140
+ Вот и всё, ребятки! Теперь вы знаете, как наш сервер отзывается на ваши AJAX-формы. Дерзайте!
141
+
142
+ # Как работать с e-commerce
143
+
144
+ - В сниппете #src/snippets/\_ym-metrika.html лежит пример кода инициализации Яндекс.Метрики.
145
+ - В скрипте с id='ecommerceScript' есть 2 важных момента: первый - это пример запуска необходимого кода сразу после
146
+ инициализации Яндекс.метрики, а второй - это функция, которая принимает данные и с учётом этого происходят действия
147
+ необходимые для метрик.
148
+ - На данный момент существует только 4 события: `click`, `submit`, `ajax-success` и `ajax-fail`.
149
+ - Атрибут `data-ecom-data` нужно указывать, только для тех событий, которые не ожидают ответа от сервера.
150
+
151
+ ## Примеры использования атрибутов:
152
+
153
+ ### Обычный клик на ссылке
154
+
155
+ ```html
156
+ <a
157
+ href="#"
158
+ data-ecom-action="click"
159
+ data-ecom-data='{
160
+ "click": {
161
+ "ymCounter123123123": "pull-right",
162
+ "dataLayer": []
163
+ }
164
+ }'
165
+ >
166
+ Оформить заказ
167
+ </a>
168
+ ```
169
+
170
+ ### Множественное количество событий на Форме-кнопке
171
+
172
+ ```html
173
+ <button
174
+ data-form-button
175
+ data-form-button-action="/endpoint"
176
+ data-form-button-method="POST"
177
+ data-form-button-data='{"someProperties": "1234"}'
178
+ data-ecom-action="click,ajax-success,submit"
179
+ data-ecom-data='{
180
+ "click": {
181
+ "ymCounter123123123": "pull-right",
182
+ "dataLayer": []
183
+ },
184
+ "submit": {
185
+ "ymCounter123123123": "pull-right",
186
+ "dataLayer": []
187
+ }
188
+ }'
189
+ >
190
+ Оформить заказ
191
+ </button>
192
+ ```
193
+
194
+ ### Cобытие у элемента формы
195
+
196
+ ```html
197
+ <form
198
+ class="ordering-page__body"
199
+ action="/api/order/"
200
+ method="POST"
201
+ name="ORDER_FORM"
202
+ enctype="multipart/form-data"
203
+ novalidate
204
+ data-ecom-action="ajax-success"
205
+ >
206
+ ...
207
+ </form>
208
+ ```
209
+
210
+ # Сериализация форм и полей
211
+
212
+ ## Работа с атрибутом `data-no-serialize-before-changes`
213
+
214
+ ### Общее описание
215
+
216
+ Атрибут `data-no-serialize-before-changes` предназначен для контроля сериализации полей формы в AJAX-запросах. С его
217
+ помощью можно указать, какие поля формы не должны быть сериализованы и отправлены на сервер до того момента, как в них
218
+ произойдут изменения со стороны пользователя.
219
+
220
+ ### Применение
221
+
222
+ Для использования атрибута достаточно добавить его к любому элементу формы, например, `<input>`, `<select>` или
223
+ `<textarea>`. Пока пользователь не изменит значение такого поля, оно не будет включено в сериализованные данные формы
224
+ при AJAX-запросе.
225
+
226
+ ### Пример
227
+
228
+ ```html
229
+ <input type="text" name="user-email" data-no-serialize-before-changes />
230
+ ```
231
+
232
+ В данном случае, поле для ввода email пользователя не будет сериализовано и отправлено на сервер до тех пор, пока
233
+ пользователь явно не изменит его содержимое.
@@ -0,0 +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}({});
2
+ //# sourceMappingURL=FormFather.min.js.map
@@ -105,7 +105,7 @@ declare class Form {
105
105
  $el: HTMLFormElement;
106
106
  private options;
107
107
  private config;
108
- private $submit;
108
+ private $submits;
109
109
  private waitResponse;
110
110
  private inputs;
111
111
  private _onSubmitHandler?;
@@ -130,6 +130,7 @@ declare class Form {
130
130
  * свойства, которые необходимо обновить; остальные сохранятся без изменений.
131
131
  */
132
132
  static setDefaultParams(params: Partial<FormOptions>): void;
133
+ private findSubmitElements;
133
134
  private initialization;
134
135
  /**
135
136
  * Показывает ошибку для поля ввода.
@@ -73,6 +73,64 @@ function closest($el, selector) {
73
73
  }
74
74
  return null;
75
75
  }
76
+ const HTTP_PROTOCOLS = ['http:', 'https:'];
77
+ function getNormalizedUrlString(originalUrlString) {
78
+ const trimmedUrl = originalUrlString.trim();
79
+ if (trimmedUrl.length === 0)
80
+ return '';
81
+ if (trimmedUrl.startsWith('//')) {
82
+ return `https:${trimmedUrl}`;
83
+ }
84
+ return /^[a-z][a-z\d+.-]*:\/\//i.test(trimmedUrl) ? trimmedUrl : `https://${trimmedUrl}`;
85
+ }
86
+ function isValidIpv4(hostname) {
87
+ const parts = hostname.split('.');
88
+ if (parts.length !== 4)
89
+ return false;
90
+ return parts.every(part => {
91
+ if (!/^\d{1,3}$/.test(part))
92
+ return false;
93
+ const num = Number(part);
94
+ return num >= 0 && num <= 255;
95
+ });
96
+ }
97
+ function isValidIpv6(hostname) {
98
+ if (!hostname.startsWith('[') || !hostname.endsWith(']'))
99
+ return false;
100
+ const normalizedHostname = hostname.slice(1, -1);
101
+ return normalizedHostname.includes(':') && /^[\da-f:.]+$/i.test(normalizedHostname);
102
+ }
103
+ function isValidDomainLabel(label) {
104
+ return label.length > 0 && label.length <= 63 && /^[a-z\d-]+$/i.test(label) && !label.startsWith('-') && !label.endsWith('-');
105
+ }
106
+ function isValidDomainHostname(hostname) {
107
+ const labels = hostname.split('.');
108
+ return labels.length >= 2 && labels.every(isValidDomainLabel);
109
+ }
110
+ function isAllowedHostname(hostname) {
111
+ if (!hostname)
112
+ return false;
113
+ return (hostname === 'localhost' || isValidIpv4(hostname) || isValidIpv6(hostname) || isValidDomainHostname(hostname));
114
+ }
115
+ /**
116
+ * Проверяет является ли строка валидным url.
117
+ *
118
+ * @param {string} originalUrlString - Строка, которую нужно проверить.
119
+ *
120
+ * @returns {boolean} Является ли строка валидным url адресом true/false.
121
+ */
122
+ function isUrlValid(originalUrlString) {
123
+ const urlString = getNormalizedUrlString(originalUrlString);
124
+ if (urlString.length === 0)
125
+ return false;
126
+ try {
127
+ const url = new URL(urlString);
128
+ return HTTP_PROTOCOLS.includes(url.protocol) && isAllowedHostname(url.hostname);
129
+ }
130
+ catch (_a) {
131
+ return false;
132
+ }
133
+ }
76
134
  /**
77
135
  * Кастомная сериализация формы в объект FormData.
78
136
  *
@@ -232,7 +290,7 @@ registerValidator('required', (value, $input, $form) => {
232
290
  }, 'Пустое значение');
233
291
  registerValidator('email', v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), 'Неверный формат');
234
292
  registerValidator('tel', v => /^\+7\d{10}$/.test(v), 'Неверный формат');
235
- registerValidator('url', v => /^(https?:\/\/).+\..+/.test(v), 'Неверный формат');
293
+ registerValidator('url', v => isUrlValid(v), 'Неверный формат');
236
294
  registerValidator('not-numbers', v => !/[0-9]/.test(v), 'Неверный формат');
237
295
 
238
296
  var _a;
@@ -246,6 +304,7 @@ const __shared =
246
304
  defaultParams: {},
247
305
  defaultValidationSchema: undefined,
248
306
  });
307
+ let loaderIdCounter = 0;
249
308
  /** Изначальная дефолтная схема. */
250
309
  const INITIAL_DEFAULT_SCHEMA = {
251
310
  tel: {
@@ -295,8 +354,7 @@ class Form {
295
354
  * @param {Object} options - Параметры формы.
296
355
  */
297
356
  constructor($el, options) {
298
- var _a;
299
- this.$submit = null;
357
+ this.$submits = [];
300
358
  this.waitResponse = false;
301
359
  this.inputs = null;
302
360
  /** Общие настройки анимаций (можно подправить под вкус) */
@@ -324,7 +382,7 @@ class Form {
324
382
  /* Слияние параметров: глобальные параметры → пользовательские параметры */
325
383
  this.config = Object.assign({}, defaultConfig, Form.defaultParams, options);
326
384
  if (this.$el) {
327
- this.$submit = this.$el.querySelector('input[type="submit"], button[type="submit"]');
385
+ this.$submits = this.findSubmitElements();
328
386
  this._onSubmitClickHandler = (e) => __awaiter(this, void 0, void 0, function* () {
329
387
  var _a, _b, _c, _d;
330
388
  e.preventDefault();
@@ -335,9 +393,12 @@ class Form {
335
393
  yield this.submit();
336
394
  }
337
395
  });
338
- (_a = this.$submit) === null || _a === void 0 ? void 0 : _a.addEventListener('click', this._onSubmitClickHandler);
339
- if (this.isCorrectArguments())
396
+ if (this.isCorrectArguments()) {
397
+ this.$submits.forEach($submit => {
398
+ $submit.addEventListener('click', this._onSubmitClickHandler);
399
+ });
340
400
  this.initialization();
401
+ }
341
402
  }
342
403
  else {
343
404
  console.warn('Empty $el');
@@ -356,6 +417,9 @@ class Form {
356
417
  static setDefaultParams(params) {
357
418
  this.defaultParams = Object.assign(Object.assign({}, this.defaultParams), params);
358
419
  }
420
+ findSubmitElements() {
421
+ return Array.from(this.$el.querySelectorAll('input[type="submit"], button[type="submit"]'));
422
+ }
359
423
  initialization() {
360
424
  this.inputs = this.$el.querySelectorAll(this.config.inputSelector);
361
425
  /* TODO: Добавить параметр с возможностью отключать/включать кнопку, в зависимости от валидности поля. */
@@ -390,46 +454,50 @@ class Form {
390
454
  }
391
455
  /** Показывает лоадер */
392
456
  showLoader() {
393
- var _a;
394
- // Создаем SVG элемент
395
- const loaderSvg = `
396
- <svg height="38" viewBox="0 0 38 38" width="38" xmlns="http://www.w3.org/2000/svg">
397
- <defs>
398
- <linearGradient id="form-father-loader" x1="8.042%" x2="65.682%" y1="0%" y2="23.865%">
399
- <stop offset="0%" stop-color="${this.config.loaderColor}" stop-opacity="0"/>
400
- <stop offset="63.146%" stop-color="${this.config.loaderColor}" stop-opacity=".631"/>
401
- <stop offset="100%" stop-color="${this.config.loaderColor}"/>
402
- </linearGradient>
403
- </defs>
404
- <g fill-rule="evenodd" fill="none">
405
- <g transform="translate(1 1)">
406
- <path d="M36 18c0-9.94-8.06-18-18-18" id="Oval-2" stroke-width="5" stroke="url(#form-father-loader)"></path>
407
- <circle cx="36" cy="18" fill="${this.config.loaderColor}" r="1"></circle>
408
- </g>
409
- </g>
410
- </svg>
411
- `;
412
- // Создаем элемент загрузчика и добавляем SVG внутрь
413
- const $loaderEl = document.createElement('span');
414
- $loaderEl.innerHTML = loaderSvg;
415
- $loaderEl.className = 'button__loader';
416
- (_a = this.$submit) === null || _a === void 0 ? void 0 : _a.insertAdjacentElement('afterbegin', $loaderEl);
417
- requestAnimationFrame(() => {
418
- $loaderEl.setAttribute('data-showed', '');
457
+ this.$submits.forEach($submit => {
458
+ $submit.classList.add('button--loading');
459
+ if ($submit.querySelector('.button__loader'))
460
+ return;
461
+ const gradientId = `form-father-loader-${loaderIdCounter++}`;
462
+ const loaderSvg = `
463
+ <svg height="38" viewBox="0 0 38 38" width="38" xmlns="http://www.w3.org/2000/svg">
464
+ <defs>
465
+ <linearGradient id="${gradientId}" x1="8.042%" x2="65.682%" y1="0%" y2="23.865%">
466
+ <stop offset="0%" stop-color="${this.config.loaderColor}" stop-opacity="0"/>
467
+ <stop offset="63.146%" stop-color="${this.config.loaderColor}" stop-opacity=".631"/>
468
+ <stop offset="100%" stop-color="${this.config.loaderColor}"/>
469
+ </linearGradient>
470
+ </defs>
471
+ <g fill-rule="evenodd" fill="none">
472
+ <g transform="translate(1 1)">
473
+ <path d="M36 18c0-9.94-8.06-18-18-18" id="Oval-2" stroke-width="5" stroke="url(#${gradientId})"></path>
474
+ <circle cx="36" cy="18" fill="${this.config.loaderColor}" r="1"></circle>
475
+ </g>
476
+ </g>
477
+ </svg>
478
+ `;
479
+ const $loaderEl = document.createElement('span');
480
+ $loaderEl.innerHTML = loaderSvg;
481
+ $loaderEl.className = 'button__loader';
482
+ $submit.insertAdjacentElement('afterbegin', $loaderEl);
483
+ requestAnimationFrame(() => {
484
+ $loaderEl.setAttribute('data-showed', '');
485
+ });
419
486
  });
420
487
  }
421
488
  /** Скрывает лоадер */
422
489
  hideLoader() {
423
- var _a, _b;
424
- (_a = this.$submit) === null || _a === void 0 ? void 0 : _a.classList.remove('button--loading');
425
- const $loader = (_b = this.$submit) === null || _b === void 0 ? void 0 : _b.querySelector('.button__loader');
426
- if ($loader) {
427
- $loader.classList.remove('active');
428
- $loader.removeAttribute('data-showed');
429
- setTimeout(() => {
430
- $loader.remove();
431
- }, 250);
432
- }
490
+ this.$submits.forEach($submit => {
491
+ $submit.classList.remove('button--loading');
492
+ const $loader = $submit.querySelector('.button__loader');
493
+ if ($loader) {
494
+ $loader.classList.remove('active');
495
+ $loader.removeAttribute('data-showed');
496
+ setTimeout(() => {
497
+ $loader.remove();
498
+ }, 250);
499
+ }
500
+ });
433
501
  }
434
502
  /**
435
503
  * Скрывает ошибку.
@@ -585,13 +653,15 @@ class Form {
585
653
  }
586
654
  /** Блокирует кнопку отправки данных. */
587
655
  disableSubmit() {
588
- var _a;
589
- (_a = this.$submit) === null || _a === void 0 ? void 0 : _a.setAttribute('disabled', '');
656
+ this.$submits.forEach($submit => {
657
+ $submit.setAttribute('disabled', '');
658
+ });
590
659
  }
591
660
  /** Снимает блокировку с кнопки отправки данных. */
592
661
  enableSubmit() {
593
- var _a;
594
- (_a = this.$submit) === null || _a === void 0 ? void 0 : _a.removeAttribute('disabled');
662
+ this.$submits.forEach($submit => {
663
+ $submit.removeAttribute('disabled');
664
+ });
595
665
  }
596
666
  /** Очищает поля ввода формы. */
597
667
  clearInputs() {
@@ -778,7 +848,6 @@ class Form {
778
848
  }
779
849
  /** Полностью снимает слушатели и чистит артефакты формы. */
780
850
  destroy() {
781
- var _a, _b, _c;
782
851
  // 1) Снять submit-слушатель
783
852
  if (this._onSubmitHandler) {
784
853
  this.$el.removeEventListener('submit', this._onSubmitHandler);
@@ -786,7 +855,9 @@ class Form {
786
855
  }
787
856
  // 1.1) Снять click-слушатель с кнопки submit
788
857
  if (this._onSubmitClickHandler) {
789
- (_a = this.$submit) === null || _a === void 0 ? void 0 : _a.removeEventListener('click', this._onSubmitClickHandler);
858
+ this.$submits.forEach($submit => {
859
+ $submit.removeEventListener('click', this._onSubmitClickHandler);
860
+ });
790
861
  this._onSubmitClickHandler = undefined;
791
862
  }
792
863
  // 2) Удалить формовый error-блок под инпутами (если есть)
@@ -796,10 +867,12 @@ class Form {
796
867
  formErrorWrapper.remove();
797
868
  }
798
869
  // 3) Скрыть/удалить лоадер на кнопке submit
799
- (_b = this.$submit) === null || _b === void 0 ? void 0 : _b.classList.remove('button--loading');
800
- const btnLoader = (_c = this.$submit) === null || _c === void 0 ? void 0 : _c.querySelector('.button__loader');
801
- if (btnLoader)
802
- btnLoader.remove();
870
+ this.$submits.forEach($submit => {
871
+ $submit.classList.remove('button--loading');
872
+ const btnLoader = $submit.querySelector('.button__loader');
873
+ if (btnLoader)
874
+ btnLoader.remove();
875
+ });
803
876
  // 4) Сбросить ошибки у инпутов (если разметка позволяет)
804
877
  const inputs = this.$el.querySelectorAll('input, textarea');
805
878
  inputs.forEach($i => {
@@ -810,7 +883,7 @@ class Form {
810
883
  document.documentElement.removeAttribute('data-now-scrolling');
811
884
  // 6) Очистить ссылки на элементы, чтобы помочь GC
812
885
  this.inputs = null;
813
- this.$submit = null;
886
+ this.$submits = [];
814
887
  this.waitResponse = false;
815
888
  }
816
889
  /**
@@ -823,7 +896,7 @@ class Form {
823
896
  console.warn(`$el не является тегом form`);
824
897
  return false;
825
898
  }
826
- if (!this.$submit) {
899
+ if (this.$submits.length === 0) {
827
900
  console.warn(`В форме нет кнопки с типом submit`);
828
901
  return false;
829
902
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -80,7 +80,7 @@ export default class Form {
80
80
  $el: HTMLFormElement;
81
81
  private options;
82
82
  private config;
83
- private $submit;
83
+ private $submits;
84
84
  private waitResponse;
85
85
  private inputs;
86
86
  private _onSubmitHandler?;
@@ -105,6 +105,7 @@ export default class Form {
105
105
  * свойства, которые необходимо обновить; остальные сохранятся без изменений.
106
106
  */
107
107
  static setDefaultParams(params: Partial<FormOptions>): void;
108
+ private findSubmitElements;
108
109
  private initialization;
109
110
  /**
110
111
  * Показывает ошибку для поля ввода.
package/package.json CHANGED
@@ -1,17 +1,27 @@
1
1
  {
2
2
  "name": "form-father",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Form Father: Библиотека для обработки форм",
5
5
  "type": "module",
6
+ "files": ["dist", "README.md", "LICENSE", "RESPONSE_API.md"],
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ }
12
+ },
6
13
  "scripts": {
7
14
  "build": "rollup -c",
8
15
  "watch": "rollup -c -w",
9
16
  "release": "node scripts/release.js",
17
+ "test": "jest --coverage",
18
+ "prepublishOnly": "npm test -- --runInBand --watchman=false && npm run build",
19
+ "versions:sync": "node scripts/sync-version.js",
10
20
  "demos": "BUILD_TARGET=demos npm run build && concurrently --kill-others \"vite\" \"npm run watch\""
11
21
  },
12
22
  "repository": {
13
23
  "type": "git",
14
- "url": "https://github.com/poliklot/form-father.git"
24
+ "url": "git+https://github.com/poliklot/form-father.git"
15
25
  },
16
26
  "keywords": [
17
27
  "form",
@@ -34,10 +44,15 @@
34
44
  "@rollup/plugin-json": "^6.1.0",
35
45
  "@rollup/plugin-node-resolve": "^15.3.0",
36
46
  "@rollup/plugin-terser": "^0.4.4",
47
+ "@types/jest": "^30.0.0",
48
+ "@types/jsdom": "^21.1.7",
37
49
  "chalk": "^5.3.0",
38
50
  "concurrently": "^9.0.1",
39
51
  "fs-extra": "^11.2.0",
40
52
  "inquirer": "^12.1.0",
53
+ "jest": "^30.0.5",
54
+ "jest-environment-jsdom": "^30.0.5",
55
+ "jsdom": "^26.1.0",
41
56
  "ora": "^8.1.1",
42
57
  "postcss-preset-env": "^10.0.8",
43
58
  "prettier-plugin-jsdoc": "^1.3.0",
@@ -47,7 +62,9 @@
47
62
  "rollup-plugin-dts": "^6.1.1",
48
63
  "rollup-plugin-typescript2": "^0.36.0",
49
64
  "sass-embedded": "^1.80.5",
65
+ "ts-jest": "^29.4.0",
66
+ "ts-node": "^10.9.2",
50
67
  "typescript": "^5.6.3",
51
68
  "vite": "^5.4.10"
52
69
  }
53
- }
70
+ }
package/FormFather.min.js DELETED
@@ -1,2 +0,0 @@
1
- var FormFather=function(t){"use strict";function e(t,e,i,o){return new(i||(i=Promise))((function(r,n){function s(t){try{l(o.next(t))}catch(t){n(t)}}function a(t){try{l(o.throw(t))}catch(t){n(t)}}function l(t){var e;t.done?r(t.value):(e=t.value,e instanceof i?e:new i((function(t){t(e)}))).then(s,a)}l((o=o.apply(t,e||[])).next())}))}function i(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:o}=t;if("file"===o)if("true"===t.getAttribute("multiple")){const o=t.files;if(o)for(let t=0;t<o.length;t++)i.append(`${e}[${t}]`,o[t])}else{const o=t.files;o&&o[0]&&i.append(e,o[0])}else{let r;if("select-one"===o||"select-multiple"===o){const n=t;if("select-multiple"===o){Array.from(n.selectedOptions).map((t=>t.value)).forEach(((t,o)=>{i.append(`${e}[${o}]`,t)}))}else r=n.value,r&&i.append(e,r)}else if("tel"===o)r=t.value.split(/[\s()-]/).join("").replace(/^8/,"+7");else if("checkbox"===o||"radio"===o){if(!t.checked)return;r=t.value}else if("date"===o){const e=t.getAttribute("data-date-format");r=e?function(t,e){const i=new Date(t),o=t=>t.toString().padStart(2,"0"),r=o(i.getDate()),n=o(i.getMonth()+1),s=i.getFullYear().toString();return e.replace("dd",r).replace("mm",n).replace("yyyy",s)}(t.value,e):t.value}else r=t.value;r&&"select-one"!==o&&"select-multiple"!==o&&i.append(e,r)}})),i}var o;"function"==typeof SuppressedError&&SuppressedError;const r=Symbol.for("FormFather.validators"),n=null!==(o=globalThis[r])&&void 0!==o?o:globalThis[r]=new Map;function s(t,e,i,{override:o=!1}={}){!n.has(t)||o?n.set(t,{fn:e,defaultMessage:i}):console.warn(`[FormFather] Validator "${t}" already exists; pass { override: true } to replace`)}const a=t=>n.get(t);var l;s("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}),"Пустое значение"),s("email",(t=>/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)),"Неверный формат"),s("tel",(t=>/^\+7\d{10}$/.test(t)),"Неверный формат"),s("url",(t=>/^(https?:\/\/).+\..+/.test(t)),"Неверный формат"),s("not-numbers",(t=>!/[0-9]/.test(t)),"Неверный формат");const c=Symbol.for("formfather.shared"),d=null!==(l=globalThis[c])&&void 0!==l?l:globalThis[c]={defaultParams:{},defaultValidationSchema:void 0},u={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 p{static get defaultParams(){return d.defaultParams}static set defaultParams(t){d.defaultParams=t}static get defaultValidationSchema(){var t;return null!==(t=d.defaultValidationSchema)&&void 0!==t?t:d.defaultValidationSchema=u}static set defaultValidationSchema(t){d.defaultValidationSchema=t}constructor(t,i){var o;this.$submit=null,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"},p.defaultParams,i),this.$el?(this.$submit=this.$el.querySelector('input[type="submit"], button[type="submit"]'),this._onSubmitClickHandler=t=>e(this,void 0,void 0,(function*(){var e,i,o,r;t.preventDefault(),null===(i=(e=this.config).onBeforeValidate)||void 0===i||i.call(e,this);const n=yield this.validate();null===(r=(o=this.config).onAfterValidate)||void 0===r||r.call(o,n,this),n&&(yield this.submit())})),null===(o=this.$submit)||void 0===o||o.addEventListener("click",this._onSubmitClickHandler),this.isCorrectArguments()&&this.initialization()):console.warn("Empty $el")}getOptions(){return this.options}static setDefaultParams(t){this.defaultParams=Object.assign(Object.assign({},this.defaultParams),t)}initialization(){this.inputs=this.$el.querySelectorAll(this.config.inputSelector),this._onSubmitHandler=t=>e(this,void 0,void 0,(function*(){var e,i,o,r;t.preventDefault(),null===(i=(e=this.config).onBeforeValidate)||void 0===i||i.call(e,this);const n=yield this.validate();null===(r=(o=this.config).onAfterValidate)||void 0===r||r.call(o,n,this),n&&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(){var t;const e=`\n\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<defs>\n\t\t\t\t<linearGradient id="form-father-loader" x1="8.042%" x2="65.682%" y1="0%" y2="23.865%">\n\t\t\t\t\t<stop offset="0%" stop-color="${this.config.loaderColor}" stop-opacity="0"/>\n\t\t\t\t\t<stop offset="63.146%" stop-color="${this.config.loaderColor}" stop-opacity=".631"/>\n\t\t\t\t\t<stop offset="100%" stop-color="${this.config.loaderColor}"/>\n\t\t\t\t</linearGradient>\n\t\t\t\t</defs>\n\t\t\t\t<g fill-rule="evenodd" fill="none">\n\t\t\t\t<g transform="translate(1 1)">\n\t\t\t\t\t<path d="M36 18c0-9.94-8.06-18-18-18" id="Oval-2" stroke-width="5" stroke="url(#form-father-loader)"></path>\n\t\t\t\t\t<circle cx="36" cy="18" fill="${this.config.loaderColor}" r="1"></circle>\n\t\t\t\t</g>\n\t\t\t\t</g>\n\t\t\t</svg>\n\t\t`,i=document.createElement("span");i.innerHTML=e,i.className="button__loader",null===(t=this.$submit)||void 0===t||t.insertAdjacentElement("afterbegin",i),requestAnimationFrame((()=>{i.setAttribute("data-showed","")}))}hideLoader(){var t,e;null===(t=this.$submit)||void 0===t||t.classList.remove("button--loading");const i=null===(e=this.$submit)||void 0===e?void 0:e.querySelector(".button__loader");i&&(i.classList.remove("active"),i.removeAttribute("data-showed"),setTimeout((()=>{i.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,o=1/0;if(t.forEach((t=>{const r=e.findIndex((e=>e===t));r<o&&(o=r,i=t)})),i){const t=i.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 i=Object.assign(Object.assign({},p.defaultValidationSchema||{}),this.config.validationSchema||{}),o=t=>console.warn(`[FormFather] Unknown validation rule "${t}"`),r=[],n=new WeakSet;let s=!0;const l=(t,e=[])=>{var i;const r=[];t.hasAttribute("required")&&r.push("required"),r.push(...e);const n=(null!==(i=t.getAttribute("data-custom-validate"))&&void 0!==i?i:"").split(",").map((t=>t.trim())).filter(Boolean);r.push(...n);const s=[];for(const t of r){const e="string"==typeof t?t:t.rule;a(e)?s.push(t):o(e)}return s},c=(i,o,...n)=>e(this,[i,o,...n],void 0,(function*(e,i,o={}){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 n of i){const{rule:i,params:l}="string"==typeof n?{rule:n,params:void 0}:n,c=a(i),d=yield c.fn(e.value,e,t,l),u="object"==typeof d&&null!==d,p=u?!0===d.valid:!!d;if(u&&d.effect&&(yield d.effect({value:e.value,$input:e,$form:t,params:l})),!p){s=!1;const t=d,n=u&&"string"==typeof t.message&&t.message||o[i]||c.defaultMessage;return this.showError(e,n),void r.push(e)}if(u&&d.stopOthers)return}this.hideError(e)}else this.hideError(e)}));for(const[e,o]of Object.entries(i)){const{selector:i,rules:r=[],messages:s={}}=o,a=i?t.querySelectorAll(i):t.querySelectorAll(`[data-validate="${e}"]`);for(const t of Array.from(a))n.has(t)||(n.add(t),yield c(t,l(t,r),s))}const d=Array.from(t.querySelectorAll("[data-custom-validate]")).filter((t=>!n.has(t)));for(const t of d)n.add(t),yield c(t,l(t,[]));return!s&&this.config.scrollToFirstErroredInput&&this.scrollToFirstErroredInput(r.filter((t=>!t.hasAttribute("data-no-error-scroll")))),s}))}disableSubmit(){var t;null===(t=this.$submit)||void 0===t||t.setAttribute("disabled","")}enableSubmit(){var t;null===(t=this.$submit)||void 0===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),o=i[i.length-1];let r=this.$el.querySelector(".error-block-under-input__wrapper");r&&o.nextElementSibling===r?(r.style.boxSizing="border-box",r.style.overflow="hidden"):(r=document.createElement("div"),r.className="error-block-under-input__wrapper",r.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(r.style,{boxSizing:"border-box",overflow:"hidden",height:"0px",paddingTop:"0px",opacity:"0"}),o.insertAdjacentElement("afterend",r)),r.querySelector(".error-block-under-input__main-text").textContent=t;const n=r.querySelector(".error-block-under-input__secondary-text");n&&(n.textContent=""),null===(e=r.querySelector(".error-block-under-input"))||void 0===e||e.classList.remove("error-block-under-input--success"),r.querySelector(".error-block-under-input");const s=r.style.height,a=r.style.paddingTop;r.style.height="auto",r.style.paddingTop=`${this._anim.paddingTopOpened}px`;const l=r.clientHeight;if(r.style.height=s||"0px",r.style.paddingTop=a||"0px",parseFloat(r.style.height)===l&&1===parseFloat(r.style.opacity||"1"))this._ringIcon(r);else{if(this._cancelAnimations(r),this._canAnimate(r)){r.animate([{height:`${parseFloat(r.style.height)||0}px`,paddingTop:`${parseFloat(r.style.paddingTop)||0}px`,opacity:parseFloat(r.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",(()=>{r.style.height="auto",r.style.paddingTop=`${this._anim.paddingTopOpened}px`,r.style.opacity="1"}))}else r.style.height="auto",r.style.paddingTop=`${this._anim.paddingTopOpened}px`,r.style.opacity="1";this._ringIcon(r)}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(){var t,e,i;this._onSubmitHandler&&(this.$el.removeEventListener("submit",this._onSubmitHandler),this._onSubmitHandler=void 0),this._onSubmitClickHandler&&(null===(t=this.$submit)||void 0===t||t.removeEventListener("click",this._onSubmitClickHandler),this._onSubmitClickHandler=void 0);const o=this.$el.querySelector(".error-block-under-input__wrapper");o&&(this._cancelAnimations(o),o.remove()),null===(e=this.$submit)||void 0===e||e.classList.remove("button--loading");const r=null===(i=this.$submit)||void 0===i?void 0:i.querySelector(".button__loader");r&&r.remove();this.$el.querySelectorAll("input, textarea").forEach((t=>{t instanceof HTMLInputElement&&this.hideError(t)})),document.documentElement.removeAttribute("data-now-scrolling"),this.inputs=null,this.$submit=null,this.waitResponse=!1}isCorrectArguments(){return"FORM"!==this.$el.tagName?(console.warn("$el не является тегом form"),!1):!!this.$submit||(console.warn("В форме нет кнопки с типом submit"),!1)}sendData(){return e(this,void 0,void 0,(function*(){var t;const e=this.$el.getAttribute("action")||"",o=(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=i(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(i(this.$el)).toString(),s["Content-Type"]="application/x-www-form-urlencoded";break;case"multipart/form-data":n=i(this.$el);break;case"text/plain":n=Array.from(i(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(i(this.$el)).toString(),s["Content-Type"]="application/x-www-form-urlencoded"}return fetch(e,{method:o,body:n,headers:s})}))}submit(){return e(this,void 0,void 0,(function*(){var t,e,i,o,r,n,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===(o=(i=this.config).onResponse)||void 0===o||o.call(i,c,this),200===l.status){if(!0===c.success)null===(n=(r=this.config).onResponseSuccess)||void 0===n||n.call(r,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=p,t.getAllValidators=function(){return new Map(n)},t.getValidator=a,t.registerValidator=s,Object.defineProperty(t,"__esModule",{value:!0}),t}({});
2
- //# sourceMappingURL=FormFather.min.js.map
File without changes
File without changes