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 +204 -6
- package/RESPONSE_API.md +233 -0
- package/dist/FormFather.min.js +2 -0
- package/{index.d.ts → dist/index.d.ts} +2 -1
- package/{index.js → dist/index.js} +127 -54
- package/{index.js.map → dist/index.js.map} +1 -1
- package/{types → dist/types}/index.d.ts +2 -1
- package/package.json +20 -3
- package/FormFather.min.js +0 -2
- /package/{FormFather.min.js.map → dist/FormFather.min.js.map} +0 -0
- /package/{types → dist/types}/helpers.d.ts +0 -0
- /package/{types → dist/types}/validators.d.ts +0 -0
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/form-father)
|
|
4
4
|
[](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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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)**:
|
|
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
|
-
|
|
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
|
package/RESPONSE_API.md
ADDED
|
@@ -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 $
|
|
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 =>
|
|
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
|
-
|
|
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.$
|
|
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
|
-
(
|
|
339
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
<
|
|
401
|
-
<
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
<
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
$
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
589
|
-
|
|
656
|
+
this.$submits.forEach($submit => {
|
|
657
|
+
$submit.setAttribute('disabled', '');
|
|
658
|
+
});
|
|
590
659
|
}
|
|
591
660
|
/** Снимает блокировку с кнопки отправки данных. */
|
|
592
661
|
enableSubmit() {
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
btnLoader
|
|
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.$
|
|
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 (
|
|
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 $
|
|
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.
|
|
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
|
|
File without changes
|