form-father 0.2.8 → 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
@@ -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;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -1,17 +1,27 @@
1
1
  {
2
2
  "name": "form-father",
3
- "version": "0.2.8",
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};let u=0;const p={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 h{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=p}static set defaultValidationSchema(t){d.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"},h.defaultParams,i),this.$el?(this.$submits=this.findSubmitElements(),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())})),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,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(){this.$submits.forEach((t=>{if(t.classList.add("button--loading"),t.querySelector(".button__loader"))return;const e="form-father-loader-"+u++,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`,o=document.createElement("span");o.innerHTML=i,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){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({},h.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(){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),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(){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")||"",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=h,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
File without changes
File without changes