@brandup/ui-input 1.0.38 → 1.0.40
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/package.json +2 -2
- package/source/input.ts +118 -16
package/package.json
CHANGED
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
"email": "it@brandup.online"
|
|
25
25
|
},
|
|
26
26
|
"license": "Apache-2.0",
|
|
27
|
-
"version": "1.0.
|
|
27
|
+
"version": "1.0.40",
|
|
28
28
|
"main": "source/index.ts",
|
|
29
29
|
"types": "source/index.ts",
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@brandup/ui": "^2.0.7",
|
|
32
|
-
"@brandup/ui-kit": "^1.0.
|
|
32
|
+
"@brandup/ui-kit": "^1.0.40"
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"source",
|
package/source/input.ts
CHANGED
|
@@ -12,13 +12,16 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
|
|
|
12
12
|
{
|
|
13
13
|
protected __valueElem: FormInput<T>;
|
|
14
14
|
protected __submitEvent?: (e: SubmitEvent) => void;
|
|
15
|
+
private __submitCaptureEvent?: (e: Event) => void;
|
|
15
16
|
private __invalidEvent?: (e: Event) => void;
|
|
17
|
+
private __overrides?: ValueElemOverrides;
|
|
16
18
|
private __isValidating?: boolean; // true, когда выполняется checkValidity в validate.
|
|
17
19
|
|
|
18
|
-
constructor(typeName: string, elem: HTMLElement, valueElem: FormInput<T
|
|
20
|
+
constructor(typeName: string, elem: HTMLElement, valueElem: FormInput<T>, overrides?: ValueElemOverrides) {
|
|
19
21
|
super(typeName, elem);
|
|
20
22
|
|
|
21
23
|
this.__valueElem = valueElem;
|
|
24
|
+
this.__overrides = overrides;
|
|
22
25
|
|
|
23
26
|
// то, что раньше делал _onRenderElement-override; теперь применяем после super, чтобы видеть valueElem
|
|
24
27
|
elem.classList.add(INPUT_CSS_CLASS);
|
|
@@ -29,6 +32,20 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
|
|
|
29
32
|
this.__initForm();
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Готовит поле-носитель к обёртке контейнером контрола: класс-скрыватель переезжает на поле,
|
|
37
|
+
* а собственные классы поля — на контейнер, чтобы оформление из разметки применялось к тому,
|
|
38
|
+
* что видно. Статический, потому что вызывается до `super(...)`.
|
|
39
|
+
*
|
|
40
|
+
* Саму вставку в DOM делает контрол: поле встаёт в контейнер первым или последним
|
|
41
|
+
* в зависимости от вёрстки, и на этот порядок завязаны соседские селекторы в стилях.
|
|
42
|
+
*/
|
|
43
|
+
protected static prepareValueElem(valueElem: HTMLElement, container: HTMLElement, inputClass: string) {
|
|
44
|
+
container.classList.add(...Array.from(valueElem.classList));
|
|
45
|
+
container.classList.remove(inputClass);
|
|
46
|
+
valueElem.classList.add(inputClass);
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
get form(): HTMLFormElement | null {
|
|
33
50
|
return this.__valueElem.form;
|
|
34
51
|
}
|
|
@@ -42,32 +59,77 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
|
|
|
42
59
|
return this.__valueElem.hasAttribute("readonly") || this.__valueElem.hasAttribute("data-readonly");
|
|
43
60
|
}
|
|
44
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Довести значение до `__valueElem`, если контрол держит его отдельно и обновляет не мгновенно
|
|
64
|
+
* (например, редактор с отложенной сериализацией). Вызывается перед каждым чтением значения
|
|
65
|
+
* снаружи: валидация, отправка формы, сбор `FormData`. По умолчанию ничего не делает —
|
|
66
|
+
* у контролов, пишущих в поле сразу, синхронизировать нечего.
|
|
67
|
+
*/
|
|
68
|
+
protected __syncValue(): void {}
|
|
69
|
+
|
|
45
70
|
private __initForm() {
|
|
71
|
+
// Логика валидации — штатная: ограничения объявлены на поле-носителе, решение принимает
|
|
72
|
+
// браузер, он же блокирует отправку и поднимает invalid. Контрол только отражает это
|
|
73
|
+
// классом и своё событие submit не досылает — обработчики формы получают ровно то же,
|
|
74
|
+
// что и у обычных input/textarea.
|
|
75
|
+
//
|
|
76
|
+
// Гасим лишь показ подсказки: поле уведено с экрана, привязать её не к чему, и браузер
|
|
77
|
+
// вместо неё пишет в консоль «not focusable». Состояние видно по классу invalid.
|
|
46
78
|
this.__invalidEvent = (e: Event) => {
|
|
47
79
|
e.preventDefault();
|
|
48
80
|
|
|
49
|
-
this.
|
|
81
|
+
this.element.classList.add("invalid");
|
|
50
82
|
};
|
|
51
83
|
this.__valueElem.addEventListener("invalid", this.__invalidEvent);
|
|
52
84
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
85
|
+
// Значение переносим в поле формы до отправки. Проверять здесь нечего: контрол объявляет
|
|
86
|
+
// свои ограничения через setCustomValidity, а решение принимает браузер — до submit он
|
|
87
|
+
// уже отказал бы. Синхронизация нужна и при отключённой валидации, поэтому она первым делом.
|
|
88
|
+
this.__submitEvent = () => this.__syncValue();
|
|
89
|
+
|
|
90
|
+
if (!this.form) return;
|
|
91
|
+
|
|
92
|
+
this.form.addEventListener("submit", this.__submitEvent);
|
|
93
|
+
|
|
94
|
+
// Тот же сброс, но гарантированно раньше любого обработчика самой формы: в фазе перехвата
|
|
95
|
+
// на документе событие приходит до цели, в каком бы порядке ни вешали слушатели. Иначе
|
|
96
|
+
// обработчик submit, повешенный приложением раньше контрола, успел бы собрать FormData
|
|
97
|
+
// со старым значением. Для формы вне документа перехвата не будет — там работает
|
|
98
|
+
// __submitEvent выше.
|
|
99
|
+
//
|
|
100
|
+
// Документ берём у самого поля, а не глобальный: destroy может случиться в любой момент
|
|
101
|
+
// (UIElement разрушает контрол сам, заметив удаление элемента через MutationObserver),
|
|
102
|
+
// и тогда обращаться к глобальному окружению уже небезопасно.
|
|
103
|
+
this.__submitCaptureEvent = (e: Event) => {
|
|
104
|
+
if (e.target === this.form) this.__syncValue();
|
|
105
|
+
};
|
|
106
|
+
this.__valueElem.ownerDocument.addEventListener("submit", this.__submitCaptureEvent, true);
|
|
107
|
+
}
|
|
56
108
|
|
|
57
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Отправка формы по Enter — как у обычного `input`: браузер сам проверит валидность,
|
|
111
|
+
* поднимет отменяемый `submit` и отправит форму.
|
|
112
|
+
*
|
|
113
|
+
* Не то же, что {@link __submitForm}: тот лишь диспатчит событие, а синтетическое событие
|
|
114
|
+
* вызывает обработчиков, но саму отправку не запускает — форма с `action` никуда не уходила.
|
|
115
|
+
*
|
|
116
|
+
* Неявная отправка в браузере равносильна нажатию первой кнопки отправки, поэтому её же
|
|
117
|
+
* передаём инициатором: от неё зависят `formaction`, `formnovalidate` и `e.submitter`.
|
|
118
|
+
*/
|
|
119
|
+
protected __requestSubmit() {
|
|
120
|
+
const form = this.form;
|
|
121
|
+
if (this.readonly || this.disabled || !form) return;
|
|
58
122
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
123
|
+
if (typeof form.requestSubmit !== "function") {
|
|
124
|
+
this.__submitForm(); // движок без requestSubmit — хотя бы уведомим обработчиков
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
64
127
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
};
|
|
128
|
+
const submitter = form.querySelector<HTMLButtonElement>(
|
|
129
|
+
"button[type=submit], input[type=submit], button:not([type])"
|
|
130
|
+
);
|
|
69
131
|
|
|
70
|
-
|
|
132
|
+
form.requestSubmit(submitter ?? undefined);
|
|
71
133
|
}
|
|
72
134
|
|
|
73
135
|
protected __submitForm() {
|
|
@@ -79,6 +141,8 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
|
|
|
79
141
|
validate(): boolean {
|
|
80
142
|
if (this.__isValidating) return true;
|
|
81
143
|
|
|
144
|
+
this.__syncValue(); // checkValidity читает поле напрямую
|
|
145
|
+
|
|
82
146
|
this.__isValidating = true;
|
|
83
147
|
const result = this.__valueElem.checkValidity();
|
|
84
148
|
this.__isValidating = false;
|
|
@@ -91,15 +155,53 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
|
|
|
91
155
|
this.element.scrollIntoView({ block: "center", inline: "center" });
|
|
92
156
|
}
|
|
93
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Возвращает поле-носитель в исходное состояние: снимает класс, которым контрол увёл его
|
|
160
|
+
* с экрана, возвращает подменённые атрибуты и вынимает поле из контейнера, а сам контейнер
|
|
161
|
+
* удаляет. Одинаково для всех контролов: поле остаётся в форме, а UI над ним — временный.
|
|
162
|
+
*/
|
|
163
|
+
private __restoreValueElem() {
|
|
164
|
+
if (this.__overrides?.class) this.__valueElem.classList.remove(this.__overrides.class);
|
|
165
|
+
|
|
166
|
+
for (const [name, value] of this.__overrides?.attrs ?? []) {
|
|
167
|
+
if (value === null) this.__valueElem.removeAttribute(name);
|
|
168
|
+
else this.__valueElem.setAttribute(name, value);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// контрол мог и не оборачивать поле (элемент контрола — само поле)
|
|
172
|
+
if (this.element === this.__valueElem || !this.element.parentElement) return;
|
|
173
|
+
|
|
174
|
+
this.element.insertAdjacentElement("afterend", this.__valueElem);
|
|
175
|
+
this.element.remove();
|
|
176
|
+
}
|
|
177
|
+
|
|
94
178
|
override destroy() {
|
|
95
179
|
if (this.form && this.__submitEvent) this.form.removeEventListener("submit", this.__submitEvent);
|
|
96
180
|
|
|
181
|
+
if (this.__submitCaptureEvent)
|
|
182
|
+
this.__valueElem.ownerDocument.removeEventListener("submit", this.__submitCaptureEvent, true);
|
|
183
|
+
|
|
97
184
|
if (this.__invalidEvent) this.__valueElem.removeEventListener("invalid", this.__invalidEvent);
|
|
98
185
|
|
|
186
|
+
this.__restoreValueElem();
|
|
187
|
+
|
|
99
188
|
super.destroy();
|
|
100
189
|
}
|
|
101
190
|
}
|
|
102
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Что контрол навязал полю-носителю и что нужно вернуть при `destroy`.
|
|
194
|
+
*
|
|
195
|
+
* Снимок делается до правок, то есть до `super(...)`, поэтому передаётся снаружи, а не
|
|
196
|
+
* собирается базовым классом.
|
|
197
|
+
*/
|
|
198
|
+
export interface ValueElemOverrides {
|
|
199
|
+
/** Класс, добавленный полю контролом (обычно уводит его с экрана). */
|
|
200
|
+
class?: string;
|
|
201
|
+
/** Подменённые атрибуты: имя и исходное значение (`null` — атрибута не было). */
|
|
202
|
+
attrs?: [name: string, value: string | null][];
|
|
203
|
+
}
|
|
204
|
+
|
|
103
205
|
export interface IInputControl {
|
|
104
206
|
get form(): HTMLFormElement | null;
|
|
105
207
|
get disabled(): boolean;
|