@brandup/ui-textbox 1.0.38 → 1.0.39
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 +2 -2
- package/package.json +4 -4
- package/source/textbox.ts +49 -18
package/README.md
CHANGED
|
@@ -37,10 +37,10 @@ textbox.on(CHANGE_EVENT, (data: ChangeEventData) => {
|
|
|
37
37
|
| Тип | Поведение |
|
|
38
38
|
| --- | --- |
|
|
39
39
|
| `text` | Обычный текст |
|
|
40
|
-
| `email` |
|
|
40
|
+
| `email` | Оставляет буквы, цифры и `. - _ + @`; ограничивает длину до 256 символов (RFC 5321) — свой `maxlength` применяется, если он строже |
|
|
41
41
|
| `url` | URL-адрес |
|
|
42
42
|
| `tel` | Телефон |
|
|
43
|
-
| `number` | Только
|
|
43
|
+
| `number` | Только цифры (целые неотрицательные), вставка фильтруется |
|
|
44
44
|
|
|
45
45
|
Для многострочного ввода передайте `<textarea>`.
|
|
46
46
|
|
package/package.json
CHANGED
|
@@ -24,15 +24,15 @@
|
|
|
24
24
|
"email": "it@brandup.online"
|
|
25
25
|
},
|
|
26
26
|
"license": "Apache-2.0",
|
|
27
|
-
"version": "1.0.
|
|
27
|
+
"version": "1.0.39",
|
|
28
28
|
"main": "source/index.ts",
|
|
29
29
|
"types": "source/index.ts",
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@brandup/ui": "^2.0.7",
|
|
32
32
|
"@brandup/ui-helpers": "^2.0.7",
|
|
33
|
-
"@brandup/ui-input": "^1.0.
|
|
34
|
-
"@brandup/ui-kit": "^1.0.
|
|
35
|
-
"@brandup/ui-richeditor": "^1.0.
|
|
33
|
+
"@brandup/ui-input": "^1.0.39",
|
|
34
|
+
"@brandup/ui-kit": "^1.0.39",
|
|
35
|
+
"@brandup/ui-richeditor": "^1.0.39"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"source",
|
package/source/textbox.ts
CHANGED
|
@@ -24,6 +24,11 @@ export const MAX_EMAIL_LENGTH = 256; // https://www.rfc-editor.org/rfc/rfc5321#s
|
|
|
24
24
|
|
|
25
25
|
export type TextBoxType = "text" | "email" | "url" | "tel" | "number";
|
|
26
26
|
|
|
27
|
+
// Атрибуты поля, которые компонент подменяет под себя (фокус уходит на редактируемый элемент,
|
|
28
|
+
// ограничения типа нормализуются). При destroy возвращаем их ровно в исходное состояние —
|
|
29
|
+
// иначе после снятия компонента поле остаётся с чужими ограничениями.
|
|
30
|
+
const ATTRS_TO_RESTORE = ["tabindex", "maxlength", "step"];
|
|
31
|
+
|
|
27
32
|
type TextBoxEvents = {
|
|
28
33
|
[CHANGE_EVENT]: (data: ChangeEventData) => void;
|
|
29
34
|
};
|
|
@@ -49,6 +54,11 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
49
54
|
readonly formatMarkers: FormatMarkers;
|
|
50
55
|
|
|
51
56
|
constructor(valueElem: HTMLInputElement | HTMLTextAreaElement) {
|
|
57
|
+
// исходные атрибуты запоминаем до любых правок — компонент их подменяет, а destroy возвращает
|
|
58
|
+
const originalAttrs = ATTRS_TO_RESTORE.map(
|
|
59
|
+
(name) => [name, valueElem.getAttribute(name)] as [string, string | null]
|
|
60
|
+
);
|
|
61
|
+
|
|
52
62
|
// определяем тип ввода и нормализуем валидационные атрибуты до super()
|
|
53
63
|
let type: TextBoxType = "text";
|
|
54
64
|
if (valueElem instanceof HTMLInputElement) {
|
|
@@ -58,7 +68,9 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
58
68
|
break;
|
|
59
69
|
case "email":
|
|
60
70
|
type = "email";
|
|
61
|
-
|
|
71
|
+
// у поля без атрибута maxlength свойство равно -1, а не 0, поэтому проверять
|
|
72
|
+
// нужно именно «не задан положительный предел», иначе ограничение RFC не применялось бы
|
|
73
|
+
if (valueElem.maxLength <= 0 || valueElem.maxLength > MAX_EMAIL_LENGTH)
|
|
62
74
|
valueElem.maxLength = MAX_EMAIL_LENGTH;
|
|
63
75
|
break;
|
|
64
76
|
case "url":
|
|
@@ -76,8 +88,6 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
76
88
|
}
|
|
77
89
|
}
|
|
78
90
|
|
|
79
|
-
valueElem.classList.add(INPUT_CLASS);
|
|
80
|
-
|
|
81
91
|
const maxlength = valueElem.maxLength;
|
|
82
92
|
const symbolCounter = valueElem.hasAttribute("data-symbolcounter");
|
|
83
93
|
const autoFocus = valueElem.hasAttribute("data-autofocus");
|
|
@@ -110,13 +120,13 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
110
120
|
const actionsElem = DOM.tag("div", { class: "actions" });
|
|
111
121
|
const symbolsCountElem = DOM.tag("div", { class: "symbols" });
|
|
112
122
|
|
|
113
|
-
const container = DOM.tag("div", { class:
|
|
123
|
+
const container = DOM.tag("div", { class: ROOT_CLASS }, [
|
|
114
124
|
DOM.tag("div", { class: "decorator" }),
|
|
115
125
|
DOM.tag("div", { class: "editor" }, [inputElem, symbolsCountElem]),
|
|
116
126
|
actionsElem,
|
|
117
127
|
]);
|
|
118
128
|
|
|
119
|
-
|
|
129
|
+
TextBox.prepareValueElem(valueElem, container, INPUT_CLASS);
|
|
120
130
|
|
|
121
131
|
inputElem.tabIndex = disabled ? -1 : valueElem.tabIndex;
|
|
122
132
|
valueElem.tabIndex = -1;
|
|
@@ -144,7 +154,8 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
144
154
|
valueElem.insertAdjacentElement("afterend", container);
|
|
145
155
|
container.insertAdjacentElement("afterbegin", valueElem);
|
|
146
156
|
|
|
147
|
-
|
|
157
|
+
// класс и подменённые атрибуты вернёт базовый класс при destroy
|
|
158
|
+
super("BrandUp.TextBox", container, valueElem, { class: INPUT_CLASS, attrs: originalAttrs });
|
|
148
159
|
|
|
149
160
|
this.type = type;
|
|
150
161
|
this.maxlength = maxlength;
|
|
@@ -181,10 +192,12 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
181
192
|
onEnter: () => this.__submitForm(),
|
|
182
193
|
};
|
|
183
194
|
|
|
184
|
-
// допустим ли вводимый символ по
|
|
195
|
+
// допустим ли вводимый символ по типу.
|
|
196
|
+
// `+` в адресе — обычное дело (подадреса вида user+tag@example.com), без него такие
|
|
197
|
+
// адреса нельзя было бы набрать; остальные разрешённые в local-part символы редки
|
|
185
198
|
const typeAllowsChar = (char: string) => {
|
|
186
199
|
if (type === "number") return /\d/.test(char);
|
|
187
|
-
if (type === "email") return /[a-zA-Z\d.\-_
|
|
200
|
+
if (type === "email") return /[a-zA-Z\d.\-_+@]/.test(char);
|
|
188
201
|
return true;
|
|
189
202
|
};
|
|
190
203
|
|
|
@@ -213,7 +226,10 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
213
226
|
if (maxlength > 0) {
|
|
214
227
|
const selectionLength = window.getSelection()?.toString().length ?? 0;
|
|
215
228
|
const left = maxlength - this.__editor.getLength() + selectionLength;
|
|
216
|
-
|
|
229
|
+
// не влезает ни одного символа — это отказ, а не пустая вставка: иначе
|
|
230
|
+
// вставка молча не делала бы ничего, тогда как ввод символа на пределе мигает ошибкой
|
|
231
|
+
if (left <= 0) return null;
|
|
232
|
+
if (pasted.length > left) pasted = pasted.substring(0, left);
|
|
217
233
|
}
|
|
218
234
|
|
|
219
235
|
return pasted;
|
|
@@ -252,6 +268,10 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
252
268
|
this.__onChange();
|
|
253
269
|
});
|
|
254
270
|
|
|
271
|
+
// Счётчик — на каждый ввод, а не по change: длина считается по textContent и стоит копейки,
|
|
272
|
+
// тогда как значение поля синхронизируется реже (см. RichEditor.flushChange).
|
|
273
|
+
editable.addEventListener("input", () => this.__refreshSymbolsCount(), { signal });
|
|
274
|
+
|
|
255
275
|
// состояние фокуса контрола (рамка/заливка) — на корневом элементе
|
|
256
276
|
editable.addEventListener("focus", () => !this.disabled && this.element.classList.add("focused"), { signal });
|
|
257
277
|
editable.addEventListener("blur", () => !this.disabled && this.element.classList.remove("focused"), { signal });
|
|
@@ -280,7 +300,9 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
280
300
|
}
|
|
281
301
|
|
|
282
302
|
this.registerCommand("copy-text", async (context) => {
|
|
283
|
-
|
|
303
|
+
// повторный клик, пока показана галочка, запомнил бы её как исходную иконку —
|
|
304
|
+
// после возврата кнопка так и осталась бы с галочкой
|
|
305
|
+
if (!window.navigator.clipboard || this.disabled || context.target.classList.contains("success")) return;
|
|
284
306
|
|
|
285
307
|
await window.navigator.clipboard.writeText(this.__valueElem.value);
|
|
286
308
|
|
|
@@ -288,14 +310,26 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
288
310
|
context.target.innerHTML = doneIcon;
|
|
289
311
|
context.target.classList.add("success");
|
|
290
312
|
|
|
291
|
-
|
|
292
|
-
|
|
313
|
+
// возврат иконки отменяем вместе с компонентом: иначе таймер переживает destroy
|
|
314
|
+
// и дописывает в уже отсоединённую кнопку
|
|
315
|
+
try {
|
|
316
|
+
await FuncHelper.delay(2000, signal);
|
|
317
|
+
} catch {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
293
320
|
|
|
294
321
|
context.target.innerHTML = prevHtml;
|
|
295
322
|
context.target.classList.remove("success");
|
|
296
323
|
});
|
|
297
324
|
}
|
|
298
325
|
|
|
326
|
+
// Редактор откладывает событие изменения при печати, поэтому копия значения в поле формы
|
|
327
|
+
// отстаёт. Базовый класс зовёт этот хук перед каждым чтением значения снаружи —
|
|
328
|
+
// валидация, отправка формы, сбор FormData.
|
|
329
|
+
protected override __syncValue(): void {
|
|
330
|
+
this.__editor.flushChange();
|
|
331
|
+
}
|
|
332
|
+
|
|
299
333
|
private __toIncorrect() {
|
|
300
334
|
this.element.classList.add("incorrect");
|
|
301
335
|
window.setTimeout(() => this.element.classList.remove("incorrect"), 200);
|
|
@@ -342,6 +376,7 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
342
376
|
}
|
|
343
377
|
|
|
344
378
|
getValue(): string {
|
|
379
|
+
this.__syncValue(); // значение читают снаружи — отложенное изменение сюда обязано попасть
|
|
345
380
|
return this.__valueElem.value.trim();
|
|
346
381
|
}
|
|
347
382
|
|
|
@@ -351,7 +386,7 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
351
386
|
}
|
|
352
387
|
|
|
353
388
|
override validate(): boolean {
|
|
354
|
-
let isValid = super.validate();
|
|
389
|
+
let isValid = super.validate(); // super синхронизирует значение сам, через __syncValue
|
|
355
390
|
if (isValid) {
|
|
356
391
|
const value = this.getValue();
|
|
357
392
|
|
|
@@ -372,11 +407,7 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
372
407
|
this.__listenerAbort.abort();
|
|
373
408
|
this.__editor.destroy();
|
|
374
409
|
|
|
375
|
-
|
|
376
|
-
this.element.insertAdjacentElement("afterend", this.__valueElem);
|
|
377
|
-
this.element.remove();
|
|
378
|
-
|
|
379
|
-
super.destroy();
|
|
410
|
+
super.destroy(); // снимет слушатели формы и вернёт поле-носитель в исходный вид
|
|
380
411
|
}
|
|
381
412
|
}
|
|
382
413
|
|