@brandup/ui-textbox 1.0.25 → 1.0.28
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 +92 -15
- package/package.json +8 -9
- package/source/index.ts +1 -1
- package/source/textbox.ts +275 -278
- package/source/typings/less.d.ts +1 -0
- package/source/typings/svg.d.ts +3 -3
package/README.md
CHANGED
|
@@ -1,27 +1,104 @@
|
|
|
1
|
-
# brandup
|
|
1
|
+
# @brandup/ui-textbox
|
|
2
2
|
|
|
3
|
-
[](https://dev.azure.com/brandup/BrandUp%20Core/_build/latest?definitionId=81&branchName=main)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Компонент текстового поля на базе `contenteditable`. Заменяет стандартные `<input>` и `<textarea>`, добавляя расширенное поведение: счётчик символов, кнопку копирования, фильтрацию ввода по типу и валидацию.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Установка
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
npm i @brandup/ui-textbox
|
|
10
|
+
npm i @brandup/ui-textbox
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
`Textbox` - это класс компонента для ввода текста, который расширяет возможности стандартных элементов `input` и `textarea`.
|
|
16
|
-
Наследуется от `InputControl`.
|
|
13
|
+
## Использование
|
|
17
14
|
|
|
18
15
|
```html
|
|
19
|
-
<input id="name" type="text" />
|
|
16
|
+
<input id="name" type="text" placeholder="Введите имя" />
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import TextBox from "@brandup/ui-textbox";
|
|
21
|
+
import { CHANGE_EVENT, ChangeEventData } from "@brandup/ui-textbox";
|
|
22
|
+
|
|
23
|
+
const inputElem = document.getElementById("name") as HTMLInputElement;
|
|
24
|
+
const textbox = new TextBox(inputElem);
|
|
25
|
+
|
|
26
|
+
textbox.on(CHANGE_EVENT, (data: ChangeEventData) => {
|
|
27
|
+
console.log(data.value);
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Поддерживаемые типы
|
|
32
|
+
|
|
33
|
+
`TextBox` принимает `<input>` со следующими значениями атрибута `type`:
|
|
34
|
+
|
|
35
|
+
| Тип | Поведение |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `text` | Обычный текст |
|
|
38
|
+
| `email` | Фильтрует недопустимые символы, ограничивает длину до 256 символов (RFC 5321) |
|
|
39
|
+
| `url` | URL-адрес |
|
|
40
|
+
| `tel` | Телефон |
|
|
41
|
+
| `number` | Только цифры, вставка фильтруется |
|
|
42
|
+
|
|
43
|
+
Для многострочного ввода передайте `<textarea>`.
|
|
44
|
+
|
|
45
|
+
## Data-атрибуты
|
|
46
|
+
|
|
47
|
+
| Атрибут | Описание |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `data-symbolcounter` | Показывает счётчик введённых символов (и максимума, если задан `maxlength`) |
|
|
50
|
+
| `data-autofocus` | Автофокус при инициализации (игнорируется на touch-устройствах) |
|
|
51
|
+
| `data-copy-button` | Добавляет кнопку копирования значения в буфер обмена |
|
|
52
|
+
| `data-allow-empty-strings` | Разрешает значение из одних пробелов |
|
|
53
|
+
| `data-readonly` | Альтернативный способ задать режим только для чтения |
|
|
54
|
+
|
|
55
|
+
## API
|
|
56
|
+
|
|
57
|
+
### Методы
|
|
58
|
+
|
|
59
|
+
| Метод | Описание |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `getValue(): string` | Возвращает текущее значение (обрезает пробелы по краям) |
|
|
62
|
+
| `setValue(value: string): void` | Устанавливает значение программно |
|
|
63
|
+
| `hasValue(): boolean` | `true`, если значение не пустое |
|
|
64
|
+
| `validate(): boolean` | Валидирует значение, добавляет/снимает класс `invalid` |
|
|
65
|
+
| `focus(): void` | Устанавливает фокус, прокручивает в видимую область |
|
|
66
|
+
| `destroy(): void` | Восстанавливает исходный DOM-элемент и освобождает ресурсы |
|
|
67
|
+
|
|
68
|
+
### Свойства
|
|
69
|
+
|
|
70
|
+
| Свойство | Тип | Описание |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `type` | `TextBoxType` | Тип ввода: `"text"` \| `"email"` \| `"url"` \| `"tel"` \| `"number"` |
|
|
73
|
+
| `multyline` | `boolean` | `true` для `<textarea>` |
|
|
74
|
+
| `maxlength` | `number` | Максимальная длина (из атрибута `maxlength`) |
|
|
75
|
+
| `copyButton` | `boolean` | Наличие кнопки копирования |
|
|
76
|
+
| `symbolCounter` | `boolean` | Наличие счётчика символов |
|
|
77
|
+
| `placeholder` | `string \| null` | Текст-заглушка |
|
|
78
|
+
|
|
79
|
+
### Событие textbox-change
|
|
80
|
+
|
|
81
|
+
Генерируется при каждом изменении значения.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { CHANGE_EVENT, ChangeEventData } from "@brandup/ui-textbox";
|
|
85
|
+
|
|
86
|
+
textbox.on(CHANGE_EVENT, (data: ChangeEventData) => {
|
|
87
|
+
console.log(data.value); // текущее значение
|
|
88
|
+
console.log(data.textbox); // ссылка на экземпляр TextBox
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Или через helper-метод
|
|
92
|
+
textbox.onChange((data) => { ... });
|
|
20
93
|
```
|
|
21
94
|
|
|
22
|
-
|
|
23
|
-
const inputElem = document.getElementById("name");
|
|
24
|
-
const textbox = new Textbox(inputElem);
|
|
95
|
+
## CSS-классы состояний
|
|
25
96
|
|
|
26
|
-
|
|
27
|
-
|
|
97
|
+
| Класс | Условие |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `focused` | Поле в фокусе |
|
|
100
|
+
| `invalid` | Значение не прошло валидацию |
|
|
101
|
+
| `incorrect` | Введён недопустимый символ (мигает, затем снимается) |
|
|
102
|
+
| `required` | Атрибут `required` задан |
|
|
103
|
+
| `readonly` | Режим только для чтения |
|
|
104
|
+
| `disabled` | Элемент отключён |
|
package/package.json
CHANGED
|
@@ -11,25 +11,24 @@
|
|
|
11
11
|
"name": "Dmitry Kovyazin",
|
|
12
12
|
"email": "it@brandup.online"
|
|
13
13
|
},
|
|
14
|
-
"homepage": "https://github.com/brandup-online/brandup-ui/npm/brandup-ui-textbox",
|
|
14
|
+
"homepage": "https://github.com/brandup-online/brandup-ui-kit/npm/brandup-ui-textbox",
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "https://github.com/brandup-online/brandup-ui.git"
|
|
17
|
+
"url": "https://github.com/brandup-online/brandup-ui-kit.git"
|
|
18
18
|
},
|
|
19
19
|
"bugs": {
|
|
20
|
-
"url": "https://github.com/brandup-online/brandup-ui/issues",
|
|
20
|
+
"url": "https://github.com/brandup-online/brandup-ui-kit/issues",
|
|
21
21
|
"email": "it@brandup.online"
|
|
22
22
|
},
|
|
23
23
|
"license": "Apache-2.0",
|
|
24
|
-
"version": "1.0.
|
|
24
|
+
"version": "1.0.28",
|
|
25
25
|
"main": "source/index.ts",
|
|
26
26
|
"types": "source/index.ts",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@brandup/ui": "^
|
|
29
|
-
"@brandup/ui-
|
|
30
|
-
"@brandup/ui-
|
|
31
|
-
"@brandup/ui-
|
|
32
|
-
"@brandup/ui-kit": "^1.0.25"
|
|
28
|
+
"@brandup/ui": "^2.0.2",
|
|
29
|
+
"@brandup/ui-helpers": "^2.0.2",
|
|
30
|
+
"@brandup/ui-input": "^1.0.28",
|
|
31
|
+
"@brandup/ui-kit": "^1.0.28"
|
|
33
32
|
},
|
|
34
33
|
"files": [
|
|
35
34
|
"source",
|
package/source/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from
|
|
1
|
+
export { default } from "./textbox";
|
package/source/textbox.ts
CHANGED
|
@@ -2,7 +2,7 @@ import "./textbox.less"; // стили компонента
|
|
|
2
2
|
|
|
3
3
|
import { InputControl } from "@brandup/ui-input";
|
|
4
4
|
import { IS_TOUCH_DEVICE } from "@brandup/ui-kit";
|
|
5
|
-
import { DOM } from "@brandup/ui
|
|
5
|
+
import { DOM } from "@brandup/ui";
|
|
6
6
|
import { FuncHelper } from "@brandup/ui-helpers";
|
|
7
7
|
import copyIcon from "../svg/copy.svg";
|
|
8
8
|
import doneIcon from "../svg/tick.svg";
|
|
@@ -15,10 +15,14 @@ export const MAX_EMAIL_LENGTH = 256; // https://www.rfc-editor.org/rfc/rfc5321#s
|
|
|
15
15
|
|
|
16
16
|
export type TextBoxType = "text" | "email" | "url" | "tel" | "number";
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
type TextBoxEvents = {
|
|
19
|
+
[CHANGE_EVENT]: (data: ChangeEventData) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAreaElement, TextBoxEvents> {
|
|
19
23
|
private __inputElem: HTMLElement;
|
|
20
|
-
private
|
|
21
|
-
private
|
|
24
|
+
private __symbolsCountElem: HTMLElement;
|
|
25
|
+
private __listenerAbort = new AbortController();
|
|
22
26
|
|
|
23
27
|
readonly type: TextBoxType;
|
|
24
28
|
readonly allowEmptyStrings: boolean;
|
|
@@ -30,309 +34,316 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
30
34
|
readonly symbolCounter: boolean;
|
|
31
35
|
readonly autoFocus: boolean;
|
|
32
36
|
|
|
33
|
-
get typeName(): string { return "BrandUp.TextBox"; }
|
|
34
|
-
|
|
35
37
|
constructor(valueElem: HTMLInputElement | HTMLTextAreaElement) {
|
|
36
|
-
super(
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
switch (
|
|
38
|
+
// определяем тип ввода и нормализуем валидационные атрибуты до super()
|
|
39
|
+
let type: TextBoxType = "text";
|
|
40
|
+
if (valueElem instanceof HTMLInputElement) {
|
|
41
|
+
switch (valueElem.type) {
|
|
40
42
|
case "text":
|
|
41
|
-
|
|
43
|
+
type = "text";
|
|
42
44
|
break;
|
|
43
45
|
case "email":
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
this.__valueElem.maxLength = MAX_EMAIL_LENGTH;
|
|
48
|
-
|
|
46
|
+
type = "email";
|
|
47
|
+
if (!valueElem.maxLength || valueElem.maxLength > MAX_EMAIL_LENGTH)
|
|
48
|
+
valueElem.maxLength = MAX_EMAIL_LENGTH;
|
|
49
49
|
break;
|
|
50
50
|
case "url":
|
|
51
|
-
|
|
51
|
+
type = "url";
|
|
52
52
|
break;
|
|
53
53
|
case "tel":
|
|
54
|
-
|
|
54
|
+
type = "tel";
|
|
55
55
|
break;
|
|
56
56
|
case "number":
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
type = "number";
|
|
58
|
+
valueElem.step = "1"; // Поддерживаем пока что только целые числа
|
|
59
59
|
break;
|
|
60
60
|
default:
|
|
61
|
-
throw new Error(`Тип ввода ${
|
|
61
|
+
throw new Error(`Тип ввода ${valueElem.type} не поддерживается.`);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
else
|
|
65
|
-
this.type = "text";
|
|
66
64
|
|
|
67
|
-
|
|
65
|
+
valueElem.classList.add(INPUT_CLASS);
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
const maxlength = valueElem.maxLength;
|
|
68
|
+
const symbolCounter = valueElem.hasAttribute("data-symbolcounter");
|
|
69
|
+
const autoFocus = valueElem.hasAttribute("data-autofocus");
|
|
70
|
+
const allowEmptyStrings = valueElem.hasAttribute("data-allow-empty-strings");
|
|
71
|
+
const placeholder = valueElem.getAttribute("placeholder");
|
|
72
|
+
const inputmode = valueElem.inputMode;
|
|
73
|
+
const multyline = valueElem instanceof HTMLTextAreaElement;
|
|
74
|
+
const copyButton = valueElem.hasAttribute("data-copy-button") || valueElem.hasAttribute("data-copybutton");
|
|
75
|
+
const disabled = valueElem.disabled;
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
const inputElem = DOM.tag("div", { class: "input" });
|
|
78
|
+
const actionsElem = DOM.tag("div", { class: "actions" });
|
|
79
|
+
const symbolsCountElem = DOM.tag("div", { class: "symbols" });
|
|
81
80
|
|
|
82
|
-
|
|
83
|
-
this.__initLogic();
|
|
84
|
-
this.__initText();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private __renderUI() {
|
|
88
|
-
const container = DOM.tag("div", { class: [ROOT_CLASS].concat(Array.from(this.__valueElem.classList)) }, [
|
|
81
|
+
const container = DOM.tag("div", { class: [ROOT_CLASS].concat(Array.from(valueElem.classList)) }, [
|
|
89
82
|
DOM.tag("div", { class: "decorator" }),
|
|
90
|
-
DOM.tag("div", "editor", [
|
|
91
|
-
|
|
92
|
-
this.__symbolsCountElem = DOM.tag("div", { class: "symbols" })
|
|
93
|
-
]),
|
|
94
|
-
this.__actionsElem
|
|
83
|
+
DOM.tag("div", { class: "editor" }, [inputElem, symbolsCountElem]),
|
|
84
|
+
actionsElem,
|
|
95
85
|
]);
|
|
96
86
|
|
|
97
87
|
container.classList.remove(INPUT_CLASS);
|
|
98
88
|
|
|
99
|
-
|
|
100
|
-
|
|
89
|
+
inputElem.tabIndex = valueElem.tabIndex;
|
|
90
|
+
valueElem.tabIndex = -1;
|
|
91
|
+
|
|
92
|
+
if (multyline) container.classList.add("multyline");
|
|
93
|
+
if (symbolCounter) container.classList.add("counter");
|
|
101
94
|
|
|
102
|
-
if (
|
|
95
|
+
if (disabled) inputElem.tabIndex = -1;
|
|
96
|
+
else inputElem.contentEditable = "true";
|
|
103
97
|
|
|
104
|
-
if (
|
|
98
|
+
if (inputmode) inputElem.inputMode = inputmode;
|
|
105
99
|
|
|
106
|
-
if (
|
|
107
|
-
|
|
100
|
+
if (copyButton) {
|
|
101
|
+
const buttonElem = DOM.tag(
|
|
102
|
+
"button",
|
|
103
|
+
{ command: "copy-text", title: "Скопировать в буфер обмена" },
|
|
104
|
+
copyIcon
|
|
105
|
+
);
|
|
106
|
+
if (disabled) buttonElem.disabled = true;
|
|
107
|
+
actionsElem.insertAdjacentElement("beforeend", buttonElem);
|
|
108
108
|
}
|
|
109
|
-
else
|
|
110
|
-
this.__inputElem.contentEditable = "true";
|
|
111
109
|
|
|
112
|
-
|
|
110
|
+
inputElem.setAttribute("data-placeholder", placeholder ?? "");
|
|
113
111
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
this.__actionsElem.insertAdjacentElement("beforeend", buttonElem);
|
|
112
|
+
// убираем висящую миниатюру, если есть, и вставляем container на место valueElem
|
|
113
|
+
if (valueElem.nextElementSibling) {
|
|
114
|
+
const nextElem = valueElem.nextElementSibling as HTMLElement;
|
|
115
|
+
if (nextElem.classList.contains(MINIATURE_CLASS)) nextElem.remove();
|
|
119
116
|
}
|
|
117
|
+
valueElem.insertAdjacentElement("afterend", container);
|
|
118
|
+
container.insertAdjacentElement("afterbegin", valueElem);
|
|
120
119
|
|
|
121
|
-
|
|
120
|
+
super("BrandUp.TextBox", container, valueElem);
|
|
122
121
|
|
|
123
|
-
this.
|
|
122
|
+
this.type = type;
|
|
123
|
+
this.maxlength = maxlength;
|
|
124
|
+
this.symbolCounter = symbolCounter;
|
|
125
|
+
this.autoFocus = autoFocus;
|
|
126
|
+
this.allowEmptyStrings = allowEmptyStrings;
|
|
127
|
+
this.placeholder = placeholder;
|
|
128
|
+
this.inputmode = inputmode;
|
|
129
|
+
this.multyline = multyline;
|
|
130
|
+
this.copyButton = copyButton;
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const nextElem = <HTMLElement>this.__valueElem.nextElementSibling;
|
|
128
|
-
if (nextElem.classList.contains(MINIATURE_CLASS)) nextElem.remove();
|
|
129
|
-
}
|
|
132
|
+
this.__inputElem = inputElem;
|
|
133
|
+
this.__symbolsCountElem = symbolsCountElem;
|
|
130
134
|
|
|
131
|
-
this.
|
|
132
|
-
|
|
135
|
+
this.__initLogic();
|
|
136
|
+
this.__initText();
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
private __initLogic() {
|
|
136
|
-
|
|
137
|
-
return;
|
|
140
|
+
const { signal } = this.__listenerAbort; // один AbortController отписывает все listener'ы в destroy
|
|
138
141
|
|
|
139
|
-
this.element.addEventListener("drop", (e) => e.preventDefault());
|
|
140
|
-
this.element.addEventListener("dragenter", (e) => e.preventDefault());
|
|
142
|
+
this.element.addEventListener("drop", (e) => e.preventDefault(), { signal });
|
|
143
|
+
this.element.addEventListener("dragenter", (e) => e.preventDefault(), { signal });
|
|
141
144
|
|
|
142
|
-
this.__valueElem.addEventListener(
|
|
143
|
-
|
|
144
|
-
e
|
|
145
|
-
|
|
145
|
+
this.__valueElem.addEventListener(
|
|
146
|
+
"change",
|
|
147
|
+
(e: Event) => {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
e.stopImmediatePropagation();
|
|
150
|
+
},
|
|
151
|
+
{ signal }
|
|
152
|
+
);
|
|
146
153
|
|
|
147
154
|
let hasInputClick = false;
|
|
148
|
-
this.__inputElem.addEventListener(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
155
|
+
this.__inputElem.addEventListener(
|
|
156
|
+
"mousedown",
|
|
157
|
+
() => {
|
|
158
|
+
if (this.disabled) return;
|
|
159
|
+
|
|
160
|
+
hasInputClick = true;
|
|
161
|
+
},
|
|
162
|
+
{ signal }
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
this.__inputElem.addEventListener(
|
|
166
|
+
"focus",
|
|
167
|
+
() => {
|
|
168
|
+
if (this.disabled) return;
|
|
169
|
+
|
|
170
|
+
this.element.classList.add("focused");
|
|
171
|
+
|
|
172
|
+
if (this.readonly) this.__selectAll();
|
|
173
|
+
else if (!hasInputClick) this.__carretToEnd(); // пыремещаем курсов в конец, если клик не по строке
|
|
174
|
+
},
|
|
175
|
+
{ signal }
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
this.__inputElem.addEventListener(
|
|
179
|
+
"blur",
|
|
180
|
+
() => {
|
|
181
|
+
hasInputClick = false;
|
|
182
|
+
|
|
183
|
+
if (this.disabled) return;
|
|
184
|
+
|
|
185
|
+
this.element.classList.remove("focused");
|
|
186
|
+
|
|
187
|
+
// когда удаляем весь текст, то браузер оставляет один BR, что означает что текста нет
|
|
188
|
+
// удалить BR нужно, чтобы появился placeholder
|
|
189
|
+
if (this.__inputElem.firstChild?.nodeName === "BR") DOM.empty(this.__inputElem);
|
|
190
|
+
},
|
|
191
|
+
{ signal }
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
this.__inputElem.addEventListener(
|
|
195
|
+
"dblclick",
|
|
196
|
+
() => {
|
|
197
|
+
if (this.disabled) return;
|
|
198
|
+
|
|
199
|
+
if (this.copyButton && this.readonly) this.__selectAll();
|
|
200
|
+
},
|
|
201
|
+
{ signal }
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
this.element.addEventListener(
|
|
205
|
+
"paste",
|
|
206
|
+
(e: ClipboardEvent) => {
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
e.stopPropagation();
|
|
192
209
|
|
|
193
|
-
|
|
194
|
-
return false;
|
|
210
|
+
if (this.readonly || this.disabled) return false;
|
|
195
211
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return false;
|
|
212
|
+
let pastedData = e.clipboardData?.getData("text/plain");
|
|
213
|
+
if (!pastedData) return false;
|
|
199
214
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
215
|
+
if (this.type == "number") {
|
|
216
|
+
const numberData = /[\d\s]+/.exec(pastedData);
|
|
217
|
+
if (numberData && numberData.length) pastedData = numberData[0].replace(/\s/g, "");
|
|
218
|
+
else {
|
|
219
|
+
this.element.classList.add("incorrect");
|
|
220
|
+
window.setTimeout(() => this.element.classList.remove("incorrect"), 300);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
208
223
|
}
|
|
209
|
-
}
|
|
210
224
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return false; // TODO
|
|
225
|
+
const selection = window.getSelection();
|
|
226
|
+
if (!selection) return false; // TODO
|
|
214
227
|
|
|
215
|
-
|
|
216
|
-
|
|
228
|
+
if (this.maxlength > 0) {
|
|
229
|
+
// обрезаем вставляемый текст по кол-ву оставшихся символов для ввода
|
|
217
230
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
231
|
+
const selectionLength = selection.toString().length;
|
|
232
|
+
const currentTextLength = this.__getTextLength();
|
|
233
|
+
const leftSymbols = this.maxlength - currentTextLength + selectionLength; // осталось символов для ввода
|
|
221
234
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const lines = pastedData.split(/\n/);
|
|
227
|
-
// тримим все строки, кроме начала первой строки, вдруг так нужно
|
|
228
|
-
const output = lines.map((line, index) => index === 0 ? line.trimEnd() : line.trim());
|
|
229
|
-
|
|
230
|
-
var fragment = document.createDocumentFragment();
|
|
231
|
-
if (!this.multyline) {
|
|
232
|
-
fragment.appendChild(document.createTextNode(output.join(" ")));
|
|
233
|
-
} else {
|
|
234
|
-
output.forEach((line, index) => {
|
|
235
|
-
if (index > 0) fragment.appendChild(document.createElement("br"));
|
|
235
|
+
if (pastedData.length > leftSymbols) pastedData = pastedData.substring(0, leftSymbols);
|
|
236
|
+
}
|
|
236
237
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
const lines = pastedData.split(/\n/);
|
|
239
|
+
// тримим все строки, кроме начала первой строки, вдруг так нужно
|
|
240
|
+
const output = lines.map((line, index) => (index === 0 ? line.trimEnd() : line.trim()));
|
|
240
241
|
|
|
241
|
-
|
|
242
|
-
|
|
242
|
+
const fragment = document.createDocumentFragment();
|
|
243
|
+
if (!this.multyline) {
|
|
244
|
+
fragment.appendChild(document.createTextNode(output.join(" ")));
|
|
245
|
+
} else {
|
|
246
|
+
output.forEach((line, index) => {
|
|
247
|
+
if (index > 0) fragment.appendChild(document.createElement("br"));
|
|
243
248
|
|
|
244
|
-
|
|
245
|
-
|
|
249
|
+
fragment.appendChild(document.createTextNode(line));
|
|
250
|
+
});
|
|
251
|
+
}
|
|
246
252
|
|
|
247
|
-
|
|
248
|
-
|
|
253
|
+
// Удаляем выделенную область
|
|
254
|
+
selection.getRangeAt(0).deleteContents();
|
|
249
255
|
|
|
250
|
-
|
|
256
|
+
// Вставляем текст
|
|
257
|
+
selection.getRangeAt(0).insertNode(fragment);
|
|
251
258
|
|
|
252
|
-
|
|
253
|
-
|
|
259
|
+
// Перемещаем курсор в конец вставленной области
|
|
260
|
+
selection.setPosition(selection.focusNode, selection.focusOffset);
|
|
254
261
|
|
|
255
|
-
|
|
256
|
-
if (!this.element)
|
|
257
|
-
return false;
|
|
262
|
+
this.__applyValue();
|
|
258
263
|
|
|
259
|
-
|
|
264
|
+
return true;
|
|
265
|
+
},
|
|
266
|
+
{ signal }
|
|
267
|
+
);
|
|
260
268
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
269
|
+
this.__inputElem.addEventListener(
|
|
270
|
+
"keydown",
|
|
271
|
+
(e: KeyboardEvent) => {
|
|
272
|
+
const isChar = e.key.length === 1;
|
|
266
273
|
|
|
267
|
-
|
|
268
|
-
const currentTextLength = this.__getTextLenght();
|
|
269
|
-
if (currentTextLength >= this.maxlength) {
|
|
274
|
+
if ((this.readonly || this.disabled) && isChar && !e.ctrlKey) {
|
|
270
275
|
e.preventDefault();
|
|
271
276
|
e.stopPropagation();
|
|
272
|
-
|
|
273
|
-
this.__toIncorrect();
|
|
274
277
|
return false;
|
|
275
278
|
}
|
|
276
|
-
}
|
|
277
279
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (!/[a-zA-Z\d\.\-\_\@]/.test(e.key))
|
|
288
|
-
isIncorrect = true;
|
|
289
|
-
break;
|
|
280
|
+
if (this.maxlength > 0 && isChar && !e.ctrlKey) {
|
|
281
|
+
const currentTextLength = this.__getTextLength();
|
|
282
|
+
if (currentTextLength >= this.maxlength) {
|
|
283
|
+
e.preventDefault();
|
|
284
|
+
e.stopPropagation();
|
|
285
|
+
|
|
286
|
+
this.__toIncorrect();
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
290
289
|
}
|
|
291
290
|
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
291
|
+
if (isChar && !e.ctrlKey) {
|
|
292
|
+
let isIncorrect = false;
|
|
293
|
+
|
|
294
|
+
switch (this.type) {
|
|
295
|
+
case "number":
|
|
296
|
+
if (!/\d/.test(e.key)) isIncorrect = true;
|
|
297
|
+
break;
|
|
298
|
+
case "email":
|
|
299
|
+
if (!/[a-zA-Z\d.\-_@]/.test(e.key)) isIncorrect = true;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (isIncorrect) {
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
e.stopPropagation();
|
|
306
|
+
|
|
307
|
+
this.__toIncorrect();
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
295
311
|
|
|
296
|
-
|
|
312
|
+
if (!this.multyline && (e.key == "U+000A" || e.key == "Enter")) {
|
|
313
|
+
// Если однострочный режим и нажат enter, то отправляем submit формы
|
|
314
|
+
e.preventDefault();
|
|
315
|
+
this.__submitForm();
|
|
297
316
|
return false;
|
|
298
317
|
}
|
|
299
|
-
}
|
|
300
318
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
return;
|
|
314
|
-
|
|
315
|
-
if (this.multyline && this.__inputElem.children.length === 1) {
|
|
316
|
-
const child = this.__inputElem.children.item(0);
|
|
317
|
-
if (child && child.tagName === "BR")
|
|
318
|
-
this.__inputElem.innerHTML = '';
|
|
319
|
-
}
|
|
319
|
+
return true;
|
|
320
|
+
},
|
|
321
|
+
{ signal }
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
this.__inputElem.addEventListener(
|
|
325
|
+
"input",
|
|
326
|
+
() => {
|
|
327
|
+
if (this.multyline && this.__inputElem.children.length === 1) {
|
|
328
|
+
const child = this.__inputElem.children.item(0);
|
|
329
|
+
if (child && child.tagName === "BR") this.__inputElem.innerHTML = "";
|
|
330
|
+
}
|
|
320
331
|
|
|
321
|
-
|
|
332
|
+
this.__applyValue();
|
|
322
333
|
|
|
323
|
-
|
|
334
|
+
let clearInvalidState = true;
|
|
324
335
|
|
|
325
|
-
|
|
326
|
-
clearInvalidState = this.validate(); // Если уже не валидно, то перепроверяем
|
|
336
|
+
if (this.element.classList.contains("invalid")) clearInvalidState = this.validate(); // Если уже не валидно, то перепроверяем
|
|
327
337
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
338
|
+
if (clearInvalidState) this.element.classList.remove("invalid");
|
|
339
|
+
},
|
|
340
|
+
{ signal }
|
|
341
|
+
);
|
|
331
342
|
|
|
332
|
-
this.registerCommand("copy-text", async context => {
|
|
343
|
+
this.registerCommand("copy-text", async (context) => {
|
|
333
344
|
if (!window.navigator.clipboard || this.disabled) return;
|
|
334
345
|
|
|
335
|
-
await window.navigator.clipboard.writeText(this.__valueElem.value)
|
|
346
|
+
await window.navigator.clipboard.writeText(this.__valueElem.value);
|
|
336
347
|
|
|
337
348
|
const prevHtml = context.target.innerHTML;
|
|
338
349
|
context.target.innerHTML = doneIcon;
|
|
@@ -347,32 +358,30 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
347
358
|
}
|
|
348
359
|
|
|
349
360
|
private __initText() {
|
|
350
|
-
|
|
361
|
+
DOM.empty(this.__inputElem);
|
|
351
362
|
|
|
352
363
|
const text = this.__valueElem.value;
|
|
353
364
|
if (text) {
|
|
354
365
|
const lines = text.split(/\n/);
|
|
355
|
-
|
|
366
|
+
lines.forEach((line, index) => {
|
|
356
367
|
line = line.trim();
|
|
357
368
|
|
|
358
|
-
if (index
|
|
359
|
-
|
|
360
|
-
|
|
369
|
+
if (index === 0) this.__inputElem.append(document.createTextNode(line));
|
|
370
|
+
else {
|
|
371
|
+
const lineElem = document.createElement("div");
|
|
372
|
+
lineElem.textContent = line;
|
|
373
|
+
this.__inputElem.append(lineElem);
|
|
374
|
+
}
|
|
361
375
|
});
|
|
362
|
-
|
|
363
|
-
html = output.join("");
|
|
364
376
|
}
|
|
365
377
|
|
|
366
|
-
this.__inputElem.innerHTML = html;
|
|
367
|
-
|
|
368
378
|
this.__refreshSymbolsCount();
|
|
369
379
|
|
|
370
|
-
if (this.autoFocus && !IS_TOUCH_DEVICE && !this.disabled && !this.readonly)
|
|
371
|
-
this.__inputElem.focus();
|
|
380
|
+
if (this.autoFocus && !IS_TOUCH_DEVICE && !this.disabled && !this.readonly) this.__inputElem.focus();
|
|
372
381
|
}
|
|
373
382
|
|
|
374
383
|
private __applyValue() {
|
|
375
|
-
|
|
384
|
+
const newValue = this.__inputElem.innerText.trim();
|
|
376
385
|
this.__valueElem.value = newValue;
|
|
377
386
|
|
|
378
387
|
this.__refreshSymbolsCount();
|
|
@@ -380,29 +389,21 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
380
389
|
}
|
|
381
390
|
|
|
382
391
|
private __toIncorrect() {
|
|
383
|
-
if (!this.element)
|
|
384
|
-
return;
|
|
385
|
-
|
|
386
392
|
this.element.classList.add("incorrect");
|
|
387
|
-
window.setTimeout(() => this.element
|
|
393
|
+
window.setTimeout(() => this.element.classList.remove("incorrect"), 200);
|
|
388
394
|
}
|
|
389
395
|
|
|
390
396
|
private __refreshSymbolsCount() {
|
|
391
|
-
if (!this.__symbolsCountElem)
|
|
392
|
-
return;
|
|
397
|
+
if (!this.__symbolsCountElem) return;
|
|
393
398
|
|
|
394
|
-
const textLength = this.
|
|
399
|
+
const textLength = this.__getTextLength();
|
|
395
400
|
let counterValue: string;
|
|
396
401
|
|
|
397
402
|
if (this.maxlength > 0) {
|
|
398
403
|
counterValue = `${textLength}/${this.maxlength}`;
|
|
399
|
-
if (this.maxlength < textLength)
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
this.__symbolsCountElem.classList.remove("invalid");
|
|
403
|
-
}
|
|
404
|
-
else
|
|
405
|
-
counterValue = textLength.toString();
|
|
404
|
+
if (this.maxlength < textLength) this.__symbolsCountElem.classList.add("invalid");
|
|
405
|
+
else this.__symbolsCountElem.classList.remove("invalid");
|
|
406
|
+
} else counterValue = textLength.toString();
|
|
406
407
|
|
|
407
408
|
this.__symbolsCountElem.textContent = counterValue;
|
|
408
409
|
}
|
|
@@ -410,28 +411,30 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
410
411
|
private __selectAll() {
|
|
411
412
|
this.__inputElem.focus();
|
|
412
413
|
|
|
413
|
-
window.getSelection()?.selectAllChildren(this.__inputElem)
|
|
414
|
+
window.getSelection()?.selectAllChildren(this.__inputElem);
|
|
414
415
|
}
|
|
415
416
|
|
|
416
417
|
private __carretToEnd() {
|
|
417
|
-
|
|
418
|
+
const range = document.createRange();
|
|
418
419
|
range.selectNodeContents(this.__inputElem);
|
|
419
420
|
range.collapse(false);
|
|
420
|
-
|
|
421
|
+
const sel = window.getSelection();
|
|
421
422
|
if (sel) {
|
|
422
423
|
sel.removeAllRanges();
|
|
423
424
|
sel.addRange(range);
|
|
424
425
|
}
|
|
425
426
|
}
|
|
426
427
|
|
|
427
|
-
private
|
|
428
|
-
|
|
428
|
+
private __getTextLength() {
|
|
429
|
+
// textContent не вставляет \n между блочными детьми (в отличие от innerText), так что multiline-контент считается корректно;
|
|
430
|
+
// заодно работает в jsdom, где innerText не реализован.
|
|
431
|
+
return this.__inputElem.textContent?.length ?? 0;
|
|
429
432
|
}
|
|
430
433
|
|
|
431
434
|
private __onChange() {
|
|
432
435
|
this.trigger(CHANGE_EVENT, <ChangeEventData>{
|
|
433
436
|
textbox: this,
|
|
434
|
-
value: this.getValue()
|
|
437
|
+
value: this.getValue(),
|
|
435
438
|
});
|
|
436
439
|
}
|
|
437
440
|
|
|
@@ -444,7 +447,7 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
444
447
|
}
|
|
445
448
|
|
|
446
449
|
getValue(): string {
|
|
447
|
-
return this.__valueElem.value
|
|
450
|
+
return this.__valueElem.value.trim();
|
|
448
451
|
}
|
|
449
452
|
|
|
450
453
|
setValue(value: string): void {
|
|
@@ -455,33 +458,27 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
455
458
|
}
|
|
456
459
|
|
|
457
460
|
override validate(): boolean {
|
|
458
|
-
if (!this.element)
|
|
459
|
-
return false;
|
|
460
|
-
|
|
461
461
|
let isValid = super.validate();
|
|
462
462
|
if (isValid) {
|
|
463
|
-
|
|
463
|
+
const value = this.getValue();
|
|
464
464
|
|
|
465
|
-
if (this.required && !value)
|
|
466
|
-
isValid = false;
|
|
465
|
+
if (this.required && !value) isValid = false;
|
|
467
466
|
|
|
468
|
-
if (this.maxlength > 0 && this.maxlength < value.length)
|
|
469
|
-
isValid = false;
|
|
467
|
+
if (this.maxlength > 0 && this.maxlength < value.length) isValid = false;
|
|
470
468
|
}
|
|
471
469
|
|
|
472
|
-
if (!isValid)
|
|
473
|
-
|
|
474
|
-
else
|
|
475
|
-
this.element.classList.remove("invalid");
|
|
470
|
+
if (!isValid) this.element.classList.add("invalid");
|
|
471
|
+
else this.element.classList.remove("invalid");
|
|
476
472
|
|
|
477
473
|
return isValid;
|
|
478
474
|
}
|
|
479
475
|
|
|
480
476
|
override destroy(): void {
|
|
477
|
+
this.__listenerAbort.abort();
|
|
481
478
|
this.__valueElem.tabIndex = this.__inputElem.tabIndex;
|
|
482
479
|
|
|
483
|
-
this.element
|
|
484
|
-
this.element
|
|
480
|
+
this.element.insertAdjacentElement("afterend", this.__valueElem);
|
|
481
|
+
this.element.remove();
|
|
485
482
|
|
|
486
483
|
super.destroy();
|
|
487
484
|
}
|
|
@@ -490,4 +487,4 @@ export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAre
|
|
|
490
487
|
export interface ChangeEventData {
|
|
491
488
|
textbox: TextBox;
|
|
492
489
|
value: string;
|
|
493
|
-
}
|
|
490
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*.less";
|
package/source/typings/svg.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
declare module "*.svg" {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
2
|
+
const content: any;
|
|
3
|
+
export default content;
|
|
4
|
+
}
|