@brandup/ui-input 1.0.53 → 1.0.55
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 +45 -3
- package/package.json +2 -2
- package/source/editor-control.ts +53 -2
- package/source/input.ts +169 -3
package/README.md
CHANGED
|
@@ -24,13 +24,14 @@ npm i @brandup/ui-input
|
|
|
24
24
|
| `disabled` | `boolean` | Поле отключено |
|
|
25
25
|
| `required` | `boolean` | Поле обязательно для заполнения |
|
|
26
26
|
| `readonly` | `boolean` | Поле только для чтения (`readonly` или `data-readonly`) |
|
|
27
|
+
| `autoFocus` | `boolean` | Объявлен ли автофокус (`autofocus` или `data-autofocus`) |
|
|
27
28
|
|
|
28
29
|
### Методы
|
|
29
30
|
|
|
30
31
|
| Метод | Описание |
|
|
31
32
|
| --- | --- |
|
|
32
33
|
| `validate(): boolean` | Синхронизирует значение и проверяет его нативным `checkValidity()` |
|
|
33
|
-
| `focus(): void` | Ведёт фокус в контрол и прокручивает его в видимую
|
|
34
|
+
| `focus(scroll?): void` | Ведёт фокус в контрол и прокручивает его в видимую область. `scroll` — `ScrollLogicalPosition` для `scrollIntoView`: по умолчанию `"center"` (фокус из кода обычно ведут к тому, что нужно показать), автофокус просит `"nearest"` |
|
|
34
35
|
| `destroy(): void` | Снимает обработчики, возвращает поле-носитель в исходное состояние и удаляет контейнер контрола |
|
|
35
36
|
|
|
36
37
|
### Поведение
|
|
@@ -40,6 +41,21 @@ npm i @brandup/ui-input
|
|
|
40
41
|
- Переносит значение в поле-носитель до отправки формы — и в фазе перехвата на документе, чтобы обработчик `submit`, повешенный приложением раньше контрола, тоже увидел актуальное значение.
|
|
41
42
|
- `focus()` ничего не делает у выключенного контрола — как нативный `disabled` input, который игнорирует `focus()` сам. Поле только для чтения фокусируется: это его нативное поведение, текст читают, выделяют и копируют.
|
|
42
43
|
|
|
44
|
+
### Автофокус
|
|
45
|
+
|
|
46
|
+
Поле объявляет его нативным `autofocus` либо `data-autofocus`. Браузер разбирает нативный атрибут при разборе разметки, когда контрола ещё нет: поле, скрытое классом уже в разметке, он пропустит, а видимое — сфокусирует, но перенос поля в контейнер контрола этот фокус тут же собьёт (браузер снимает фокус с перемещаемого узла). Так что фокус в любом случае ставит контрол, а атрибут остаётся объявлением намерения. Наследник зовёт `__applyAutoFocus()` в конце своего конструктора — раньше нельзя, ввод контрола ещё не собран.
|
|
47
|
+
|
|
48
|
+
Фокус ставится через тот же `focus("nearest")`: вид двигается на минимум — страницу ещё не читали, и сдвигать её ради поля, которое и так на виду, не за чем. Автофокус — пожелание разметки, а не команда, поэтому молча отменяется, если:
|
|
49
|
+
|
|
50
|
+
- поле выключено или только для чтения;
|
|
51
|
+
- вводят пальцем (`(pointer: coarse)`) — экранная клавиатура закрыла бы страницу, которую ещё не читали;
|
|
52
|
+
- пользователь уже прокрутил страницу сам — вид принадлежит ему;
|
|
53
|
+
- фокус держит поле ввода или другой контрол ввода — там уже работают, и двух автофокусов на странице не бывает. Кнопка или ссылка, которой пришли на страницу, автофокусу не помеха.
|
|
54
|
+
|
|
55
|
+
Контрол, собранный вне документа (страница рендерится во фрагмент и попадает на экран уже собранной), ждёт появления в документе и перепроверяет условия заново — за время ожидания пользователь мог и прокрутить страницу, и уйти в другое поле.
|
|
56
|
+
|
|
57
|
+
Прокрутку пользователя различает `hasUserScrolled()` из `@brandup/ui-kit`: считаются жесты (колесо, свайп, полоса прокрутки, автопрокрутка средней кнопкой, клавиши прокрутки, действие которых никто не отменил), а не событие `scroll` — его поднимает и программная прокрутка. `UiKitMiddleware` забывает прокрутку при переходе на другую страницу — перед тем, как её нарисуют.
|
|
58
|
+
|
|
43
59
|
### Интерфейс IInputControl
|
|
44
60
|
|
|
45
61
|
```typescript
|
|
@@ -50,9 +66,10 @@ interface IInputControl {
|
|
|
50
66
|
get disabled(): boolean;
|
|
51
67
|
get required(): boolean;
|
|
52
68
|
get readonly(): boolean;
|
|
69
|
+
get autoFocus(): boolean;
|
|
53
70
|
|
|
54
71
|
validate(): boolean;
|
|
55
|
-
focus(): void;
|
|
72
|
+
focus(scroll?: ScrollLogicalPosition): void;
|
|
56
73
|
destroy(): void;
|
|
57
74
|
}
|
|
58
75
|
```
|
|
@@ -66,6 +83,8 @@ interface IInputControl {
|
|
|
66
83
|
| `__focusValue(): void` | Хук: куда именно ведёт фокус контрола. По умолчанию — поле-носитель; общие проверки и прокрутку делает `focus()` |
|
|
67
84
|
| `__requestSubmit(): void` | Неявная отправка формы (Enter), как у обычного `input`: через `form.requestSubmit()` с первой кнопкой отправки |
|
|
68
85
|
| `__submitForm(): void` | Досылает форме синтетический `submit` — движкам без `requestSubmit()` |
|
|
86
|
+
| `__applyAutoFocus(): boolean` | Ставит автофокус, если поле его объявило. Зовётся наследником в конце конструктора; возвращает, поставлен ли фокус сейчас (отложенный до появления в документе даёт `false`) |
|
|
87
|
+
| `static isAutoFocus(valueElem: HTMLElement): boolean` | Объявлен ли автофокус: `autofocus` либо `data-autofocus`. Статический — нужен и до `super(...)` |
|
|
69
88
|
| `static isReadonly(valueElem: HTMLElement): boolean` | Признано ли поле только для чтения: атрибут `readonly` либо `data-readonly` (последний нужен полям без нативного атрибута, например `select`). Статический, потому что режим нужен и до `super(...)` — он влияет на сборку разметки контрола |
|
|
70
89
|
| `static prepareValueElem(valueElem, container, inputClass)` | Переносит собственные классы поля на контейнер, а класс-скрыватель — на поле |
|
|
71
90
|
|
|
@@ -114,6 +133,7 @@ class MyInput extends InputControl<HTMLInputElement> {
|
|
|
114
133
|
| `setValue(value: string): void` | Передаёт значение редактору — тот нормализует его и поднимет своё изменение |
|
|
115
134
|
| `hasValue(): boolean` | Есть ли непустое значение |
|
|
116
135
|
| `onChange(handler): void` | Подписка на событие изменения контрола (имя события задаёт `EditorControlInit.changeEvent`) |
|
|
136
|
+
| `caret` | Куда контрол ставит каретку при фокусе из кода (`EditorControlInit.caret`, по умолчанию `end`) |
|
|
117
137
|
|
|
118
138
|
### Защищённые члены
|
|
119
139
|
|
|
@@ -151,11 +171,33 @@ export interface ValueEditor {
|
|
|
151
171
|
export interface EditorControlInit {
|
|
152
172
|
/** Имя события изменения контрола — на него подписывает onChange. */
|
|
153
173
|
changeEvent: string;
|
|
154
|
-
/**
|
|
174
|
+
/** Куда ставить каретку при фокусе из кода. По умолчанию — в конец текста. */
|
|
175
|
+
caret?: FocusCaret;
|
|
176
|
+
/** @deprecated Замещён caret и учитывается, только если тот не задан: true — "end", false — "start". */
|
|
155
177
|
focusAtEnd?: boolean;
|
|
156
178
|
}
|
|
157
179
|
```
|
|
158
180
|
|
|
181
|
+
### FocusCaret
|
|
182
|
+
|
|
183
|
+
Куда встаёт каретка, когда фокус в контрол ставят из кода — автофокусом или вызовом `focus()`. Фокус мышью и с клавиатуры это не трогает: там место каретки выбирает пользователь.
|
|
184
|
+
|
|
185
|
+
| Режим | Поведение |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| `end` | В конец текста — по умолчанию: фокус получают, чтобы продолжать писать, а не чтобы вставлять перед написанным |
|
|
188
|
+
| `start` | В начало текста |
|
|
189
|
+
| `all` | Выделить весь текст, чтобы следующий ввод его заменил |
|
|
190
|
+
|
|
191
|
+
Режим по умолчанию доверен редактору: каретку, уже стоявшую в содержимом, он бережёт сам — правку продолжают там, где её прервали. `start` и `all` этому не подчиняются: их просили явно, поэтому контрол ставит их сам и после редактора.
|
|
192
|
+
|
|
193
|
+
Режим из разметки разбирает `parseFocusCaret(value)` — всё, кроме объявленных режимов, даёт `end`:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { parseFocusCaret } from "@brandup/ui-input";
|
|
197
|
+
|
|
198
|
+
const caret = parseFocusCaret(valueElem.dataset.caret);
|
|
199
|
+
```
|
|
200
|
+
|
|
159
201
|
### Порядок конструирования
|
|
160
202
|
|
|
161
203
|
Редактор создаёт наследник: его опции замыкаются на `this` и собираются только после `super(...)`. Сразу после создания редактор обязан уйти в `__attachEditor` — до этого момента базовый класс уже привязал элемент, повесил слушатели формы и включил авторазрушение по удалению из DOM, но редактора у него ещё нет. Методы базы этот промежуток терпят (падение конструктора наследника не оставляет на странице обработчиков, которые ломали бы отправку любой формы), но контрол без редактора не работает.
|
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.55",
|
|
28
28
|
"main": "source/index.ts",
|
|
29
29
|
"types": "source/index.ts",
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@brandup/ui": "^2.0.9",
|
|
32
|
-
"@brandup/ui-kit": "^1.0.
|
|
32
|
+
"@brandup/ui-kit": "^1.0.55"
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"source",
|
package/source/editor-control.ts
CHANGED
|
@@ -21,11 +21,32 @@ export interface ValueEditor {
|
|
|
21
21
|
destroy(): void;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Куда встаёт каретка, когда фокус в контрол ставят из кода — автофокусом или вызовом
|
|
26
|
+
* {@link InputControl.focus}: в начало текста, в конец или на весь текст выделением, чтобы
|
|
27
|
+
* следующий ввод его заменил.
|
|
28
|
+
*
|
|
29
|
+
* Фокус мышью и с клавиатуры это не трогает: там место каретки выбирает пользователь.
|
|
30
|
+
*/
|
|
31
|
+
export type FocusCaret = "start" | "end" | "all";
|
|
32
|
+
|
|
33
|
+
/** Разбирает режим каретки из разметки: всё, кроме объявленных режимов, — режим по умолчанию. */
|
|
34
|
+
export const parseFocusCaret = (value: string | null | undefined): FocusCaret =>
|
|
35
|
+
value === "start" || value === "all" ? value : "end";
|
|
36
|
+
|
|
24
37
|
/** Что базовому классу нужно знать о конкретном контроле. */
|
|
25
38
|
export interface EditorControlInit {
|
|
26
39
|
/** Имя события изменения контрола — на него подписывает {@link EditorInputControl.onChange}. */
|
|
27
40
|
changeEvent: string;
|
|
28
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Куда ставить каретку при фокусе из кода. По умолчанию — в конец текста: фокус получают,
|
|
43
|
+
* чтобы продолжать писать, а не чтобы вставлять перед написанным.
|
|
44
|
+
*/
|
|
45
|
+
caret?: FocusCaret;
|
|
46
|
+
/**
|
|
47
|
+
* @deprecated Замещён {@link caret} и учитывается, только если тот не задан: `true` — то же,
|
|
48
|
+
* что `"end"`, `false` — `"start"`.
|
|
49
|
+
*/
|
|
29
50
|
focusAtEnd?: boolean;
|
|
30
51
|
}
|
|
31
52
|
|
|
@@ -148,6 +169,14 @@ export abstract class EditorInputControl<TEditor extends ValueEditor, TChangeDat
|
|
|
148
169
|
*/
|
|
149
170
|
protected __refreshValidity(): void {}
|
|
150
171
|
|
|
172
|
+
/** Куда контрол ставит каретку при фокусе из кода (см. {@link EditorControlInit.caret}). */
|
|
173
|
+
get caret(): FocusCaret {
|
|
174
|
+
const { caret, focusAtEnd } = this.__init;
|
|
175
|
+
if (caret) return caret;
|
|
176
|
+
|
|
177
|
+
return focusAtEnd === false ? "start" : "end"; // устаревший флаг, пока его ещё передают
|
|
178
|
+
}
|
|
179
|
+
|
|
151
180
|
onChange(handler: (e: TChangeData) => void) {
|
|
152
181
|
// имя события у каждого контрола своё — типизацию даёт TEvents наследника
|
|
153
182
|
this.on(this.__init.changeEvent as keyof TEvents & string, handler as never);
|
|
@@ -172,9 +201,31 @@ export abstract class EditorInputControl<TEditor extends ValueEditor, TChangeDat
|
|
|
172
201
|
* Поле-носитель уведено с экрана (visibility: collapse) и в браузере фокус не принимает —
|
|
173
202
|
* ведём фокус в редактор. Проверки состояния и прокрутку к контролу делает базовый
|
|
174
203
|
* {@link InputControl.focus}.
|
|
204
|
+
*
|
|
205
|
+
* Каретку ставим по режиму контрола (см. {@link EditorControlInit.caret}). Режим по умолчанию
|
|
206
|
+
* доверяем редактору: своё место каретки, если оно было, он бережёт сам — правку продолжают
|
|
207
|
+
* там, где прервали. Начало и выделение всего текста этому не подчиняются: их просили явно,
|
|
208
|
+
* поэтому ставим их сами и после редактора.
|
|
175
209
|
*/
|
|
176
210
|
protected override __focusValue(): void {
|
|
177
|
-
this.__editor
|
|
211
|
+
if (!this.__editor) return;
|
|
212
|
+
|
|
213
|
+
const caret = this.caret;
|
|
214
|
+
const editable = this.__editor.editable;
|
|
215
|
+
|
|
216
|
+
this.__editor.focus(caret === "end");
|
|
217
|
+
if (caret === "end") return;
|
|
218
|
+
|
|
219
|
+
const selection = editable.ownerDocument.defaultView?.getSelection();
|
|
220
|
+
if (!selection) return;
|
|
221
|
+
|
|
222
|
+
if (caret === "all") return selection.selectAllChildren(editable);
|
|
223
|
+
|
|
224
|
+
const range = editable.ownerDocument.createRange();
|
|
225
|
+
range.selectNodeContents(editable);
|
|
226
|
+
range.collapse(true); // схлопываем к началу содержимого
|
|
227
|
+
selection.removeAllRanges();
|
|
228
|
+
selection.addRange(range);
|
|
178
229
|
}
|
|
179
230
|
|
|
180
231
|
override destroy(): void {
|
package/source/input.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { UIElementBound } from "@brandup/ui";
|
|
2
|
+
// Из кита берём ровно два признака и берём их напрямую: его общий вход тянет за собой попап,
|
|
3
|
+
// модальное окно, стили и @brandup/ui-app — базе ввода это не нужно, а сборке пакета мешает.
|
|
4
|
+
import { isCoarsePointer } from "@brandup/ui-kit/source/utils/compatibility";
|
|
5
|
+
import { hasUserScrolled } from "@brandup/ui-kit/source/utils/user-scroll";
|
|
2
6
|
import "./input.less";
|
|
3
7
|
|
|
4
8
|
type InputType = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
@@ -6,6 +10,66 @@ type FormInput<T> = T extends InputType ? T : never;
|
|
|
6
10
|
|
|
7
11
|
export const INPUT_CSS_CLASS = "ui-input";
|
|
8
12
|
|
|
13
|
+
/** Контрол, ждущий появления в документе: `__onConnected` возвращает `true`, когда дождался. */
|
|
14
|
+
interface ConnectedWaiter {
|
|
15
|
+
__onConnected(): boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ждущие контролы — слабыми ссылками, и наблюдатель один на документ.
|
|
20
|
+
*
|
|
21
|
+
* Один на документ, потому что вставку страницы видит любой, а каждый лишний наблюдатель платит
|
|
22
|
+
* обходом всех мутаций документа. Слабыми, потому что контрол, собранный во фрагмент и там же
|
|
23
|
+
* выброшенный, ждать иначе не перестанет: авторазрушение снимает контрол, удалённый из документа,
|
|
24
|
+
* а никогда не вставленный не удаляют — сильная ссылка держала бы и его, и всё его дерево
|
|
25
|
+
* до ухода со страницы.
|
|
26
|
+
*
|
|
27
|
+
* Наблюдатель живёт, только пока есть кого ждать: последнее снятое ожидание его отключает.
|
|
28
|
+
*/
|
|
29
|
+
const connectedWaiters = new Map<Document, { observer: MutationObserver; waiters: Set<WeakRef<ConnectedWaiter>> }>();
|
|
30
|
+
|
|
31
|
+
function waitConnected(doc: Document, waiter: WeakRef<ConnectedWaiter>): void {
|
|
32
|
+
let entry = connectedWaiters.get(doc);
|
|
33
|
+
|
|
34
|
+
if (!entry) {
|
|
35
|
+
const observer = new MutationObserver(() => {
|
|
36
|
+
const current = connectedWaiters.get(doc);
|
|
37
|
+
if (!current) return;
|
|
38
|
+
|
|
39
|
+
for (const ref of [...current.waiters]) {
|
|
40
|
+
const pending = ref.deref();
|
|
41
|
+
if (!pending || pending.__onConnected()) current.waiters.delete(ref); // убранный сборщиком тоже отжил
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!current.waiters.size) stopObserving(doc);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
connectedWaiters.set(doc, (entry = { observer, waiters: new Set() }));
|
|
48
|
+
observer.observe(doc, { childList: true, subtree: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
entry.waiters.add(waiter);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stopWaitConnected(doc: Document, waiter: WeakRef<ConnectedWaiter>): void {
|
|
55
|
+
const entry = connectedWaiters.get(doc);
|
|
56
|
+
if (!entry?.waiters.delete(waiter) || entry.waiters.size) return;
|
|
57
|
+
|
|
58
|
+
stopObserving(doc);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stopObserving(doc: Document): void {
|
|
62
|
+
connectedWaiters.get(doc)?.observer.disconnect();
|
|
63
|
+
connectedWaiters.delete(doc);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Принимает ли элемент ввод текста — то есть значит ли фокус на нём, что в нём работают. */
|
|
67
|
+
const isTextEntry = (elem: Element): boolean =>
|
|
68
|
+
elem instanceof HTMLInputElement ||
|
|
69
|
+
elem instanceof HTMLTextAreaElement ||
|
|
70
|
+
elem instanceof HTMLSelectElement ||
|
|
71
|
+
(elem instanceof HTMLElement && elem.isContentEditable);
|
|
72
|
+
|
|
9
73
|
export abstract class InputControl<T extends InputType, TEvents = {}>
|
|
10
74
|
extends UIElementBound<TEvents>
|
|
11
75
|
implements IInputControl
|
|
@@ -17,11 +81,18 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
|
|
|
17
81
|
private __overrides?: ValueElemOverrides;
|
|
18
82
|
private __isValidating?: boolean; // true, когда выполняется checkValidity в validate.
|
|
19
83
|
|
|
84
|
+
/** Объявлен ли автофокус; ставит его наследник вызовом {@link __applyAutoFocus}. */
|
|
85
|
+
readonly autoFocus: boolean;
|
|
86
|
+
|
|
87
|
+
/** Ожидание появления контрола в документе — только пока автофокус отложен. */
|
|
88
|
+
private __autoFocusWait?: WeakRef<ConnectedWaiter>;
|
|
89
|
+
|
|
20
90
|
constructor(typeName: string, elem: HTMLElement, valueElem: FormInput<T>, overrides?: ValueElemOverrides) {
|
|
21
91
|
super(typeName, elem);
|
|
22
92
|
|
|
23
93
|
this.__valueElem = valueElem;
|
|
24
94
|
this.__overrides = overrides;
|
|
95
|
+
this.autoFocus = InputControl.isAutoFocus(valueElem);
|
|
25
96
|
|
|
26
97
|
// то, что раньше делал _onRenderElement-override; теперь применяем после super, чтобы видеть valueElem
|
|
27
98
|
elem.classList.add(INPUT_CSS_CLASS);
|
|
@@ -43,6 +114,23 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
|
|
|
43
114
|
return valueElem.hasAttribute("readonly") || valueElem.hasAttribute("data-readonly");
|
|
44
115
|
}
|
|
45
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Объявлен ли автофокус: нативный `autofocus` либо `data-autofocus` — второй нужен разметке,
|
|
119
|
+
* которой нативный атрибут не подходит по другим причинам (валидаторы, серверный рендер).
|
|
120
|
+
*
|
|
121
|
+
* Браузер разбирает нативный атрибут при разборе разметки, когда контрола ещё нет: поле,
|
|
122
|
+
* скрытое классом уже в разметке, он пропустит, а видимое — сфокусирует, но перенос поля
|
|
123
|
+
* в контейнер контрола этот фокус тут же собьёт (браузер снимает фокус с перемещаемого узла).
|
|
124
|
+
* Так что фокус в любом случае ставит контрол — см. {@link __applyAutoFocus}, — а атрибут
|
|
125
|
+
* остаётся объявлением намерения.
|
|
126
|
+
*
|
|
127
|
+
* Статический, потому что контролам это нужно и до `super(...)`; после конструирования
|
|
128
|
+
* то же самое отдаёт {@link autoFocus}.
|
|
129
|
+
*/
|
|
130
|
+
protected static isAutoFocus(valueElem: HTMLElement): boolean {
|
|
131
|
+
return valueElem.hasAttribute("autofocus") || valueElem.hasAttribute("data-autofocus");
|
|
132
|
+
}
|
|
133
|
+
|
|
46
134
|
/**
|
|
47
135
|
* Готовит поле-носитель к обёртке контейнером контрола: класс-скрыватель переезжает на поле,
|
|
48
136
|
* а собственные классы поля — на контейнер, чтобы оформление из разметки применялось к тому,
|
|
@@ -181,12 +269,17 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
|
|
|
181
269
|
*
|
|
182
270
|
* Поле только для чтения фокусируется: это его нативное поведение — текст читают,
|
|
183
271
|
* выделяют и копируют, и уводить от него клавиатуру нельзя.
|
|
272
|
+
*
|
|
273
|
+
* `scroll` — насколько двигать вид: по умолчанию контрол выводится в середину экрана, потому
|
|
274
|
+
* что фокус из кода обычно ведут к тому, что нужно показать (ошибка формы, шаг мастера).
|
|
275
|
+
* Автофокус просит `"nearest"` — там страницу ещё не читали, и сдвигать её ради поля,
|
|
276
|
+
* которое и так на виду, не за чем.
|
|
184
277
|
*/
|
|
185
|
-
focus(): void {
|
|
278
|
+
focus(scroll: ScrollLogicalPosition = "center"): void {
|
|
186
279
|
if (this.disabled) return;
|
|
187
280
|
|
|
188
281
|
this.__focusValue();
|
|
189
|
-
this.element.scrollIntoView({ block:
|
|
282
|
+
this.element.scrollIntoView({ block: scroll, inline: scroll });
|
|
190
283
|
}
|
|
191
284
|
|
|
192
285
|
/**
|
|
@@ -198,6 +291,76 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
|
|
|
198
291
|
this.__valueElem.focus();
|
|
199
292
|
}
|
|
200
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Ставит фокус в контрол, если поле объявило автофокус (см. {@link isAutoFocus}). Зовёт
|
|
296
|
+
* наследник в конце своего конструктора: базовый отработал раньше, чем собран ввод контрола
|
|
297
|
+
* (редактор привязывается уже после `super(...)`), и фокусировать там было бы нечего.
|
|
298
|
+
*
|
|
299
|
+
* Отказывает молча, потому что автофокус — пожелание разметки, а не команда:
|
|
300
|
+
* - выключенное поле фокус не принимает, а поле только для чтения нечего править;
|
|
301
|
+
* - вводят пальцем — экранная клавиатура закрыла бы страницу, которую ещё не читали;
|
|
302
|
+
* - пользователь уже прокрутил страницу сам — вид принадлежит ему;
|
|
303
|
+
* - фокус держит поле ввода или другой контрол — там уже работают.
|
|
304
|
+
*
|
|
305
|
+
* @returns Поставлен ли фокус сейчас; отложенный до появления в документе даёт `false`.
|
|
306
|
+
*/
|
|
307
|
+
protected __applyAutoFocus(): boolean {
|
|
308
|
+
if (!this.autoFocus) return false;
|
|
309
|
+
|
|
310
|
+
// Контрол собирают и вне документа: страница рендерится во фрагмент и попадает на экран
|
|
311
|
+
// уже собранной. В неподключённый элемент браузер фокус не ставит, а прокручивать нечего,
|
|
312
|
+
// поэтому ждём появления в документе — и там же перепроверяем условия заново: за время
|
|
313
|
+
// ожидания пользователь мог и прокрутить страницу, и встать в другое поле.
|
|
314
|
+
if (!this.element.isConnected) {
|
|
315
|
+
this.__waitConnected();
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
this.__stopWaitConnected();
|
|
320
|
+
|
|
321
|
+
if (this.disabled || this.readonly || isCoarsePointer() || hasUserScrolled()) return false;
|
|
322
|
+
|
|
323
|
+
// Чужой фокус не отбираем: его держит либо поле ввода — там уже печатают, — либо контрол
|
|
324
|
+
// ввода, инициализированный раньше (двух автофокусов на странице не бывает). Кнопка или
|
|
325
|
+
// ссылка, которой пришли на страницу, автофокусу не помеха: фокус на ней остался от клика,
|
|
326
|
+
// а не ради ввода. Фокус внутри самого контрола — тем более.
|
|
327
|
+
const active = this.__valueElem.ownerDocument.activeElement;
|
|
328
|
+
if (active && !this.element.contains(active) && (isTextEntry(active) || active.closest(`.${INPUT_CSS_CLASS}`)))
|
|
329
|
+
return false;
|
|
330
|
+
|
|
331
|
+
this.focus("nearest");
|
|
332
|
+
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Ждёт появления контрола в документе, чтобы поставить отложенный автофокус. */
|
|
337
|
+
private __waitConnected() {
|
|
338
|
+
if (this.__autoFocusWait) return;
|
|
339
|
+
|
|
340
|
+
// приведение — ради приватного метода: реестру ожиданий нужен только он
|
|
341
|
+
this.__autoFocusWait = new WeakRef(this as unknown as ConnectedWaiter);
|
|
342
|
+
waitConnected(this.__valueElem.ownerDocument, this.__autoFocusWait);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Контрол появился в документе — ставим отложенный автофокус. Не приватный только потому,
|
|
347
|
+
* что зовут его снаружи класса — из реестра ожиданий этого модуля.
|
|
348
|
+
*/
|
|
349
|
+
protected __onConnected(): boolean {
|
|
350
|
+
if (!this.element.isConnected) return false;
|
|
351
|
+
|
|
352
|
+
this.__applyAutoFocus(); // подключённый контрол сам снимет ожидание
|
|
353
|
+
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private __stopWaitConnected() {
|
|
358
|
+
if (!this.__autoFocusWait) return;
|
|
359
|
+
|
|
360
|
+
stopWaitConnected(this.__valueElem.ownerDocument, this.__autoFocusWait);
|
|
361
|
+
this.__autoFocusWait = undefined;
|
|
362
|
+
}
|
|
363
|
+
|
|
201
364
|
/**
|
|
202
365
|
* Возвращает поле-носитель в исходное состояние: снимает класс, которым контрол увёл его
|
|
203
366
|
* с экрана, возвращает подменённые атрибуты и вынимает поле из контейнера, а сам контейнер
|
|
@@ -219,6 +382,8 @@ export abstract class InputControl<T extends InputType, TEvents = {}>
|
|
|
219
382
|
}
|
|
220
383
|
|
|
221
384
|
override destroy() {
|
|
385
|
+
this.__stopWaitConnected();
|
|
386
|
+
|
|
222
387
|
if (this.form && this.__submitEvent) this.form.removeEventListener("submit", this.__submitEvent);
|
|
223
388
|
|
|
224
389
|
if (this.__submitCaptureEvent)
|
|
@@ -250,8 +415,9 @@ export interface IInputControl {
|
|
|
250
415
|
get disabled(): boolean;
|
|
251
416
|
get required(): boolean;
|
|
252
417
|
get readonly(): boolean;
|
|
418
|
+
get autoFocus(): boolean;
|
|
253
419
|
|
|
254
420
|
validate(): boolean;
|
|
255
|
-
focus(): void;
|
|
421
|
+
focus(scroll?: ScrollLogicalPosition): void;
|
|
256
422
|
destroy(): void;
|
|
257
423
|
}
|