@brandup/ui-textbox 1.0.14
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 +27 -0
- package/package.json +38 -0
- package/source/index.ts +1 -0
- package/source/svg/copy.svg +3 -0
- package/source/svg/tick.svg +3 -0
- package/source/textbox.less +264 -0
- package/source/textbox.ts +487 -0
- package/source/typings/svg.d.ts +4 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# brandup-ui-textbox
|
|
2
|
+
|
|
3
|
+
[]()
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install NPM package [@brandup/ui-textbox](https://www.npmjs.com/package/@brandup/ui-textbox).
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm i @brandup/ui-textbox@latest
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## TextBox
|
|
14
|
+
|
|
15
|
+
`Textbox` - это класс компонента для ввода текста, который расширяет возможности стандартных элементов `input` и `textarea`.
|
|
16
|
+
Наследуется от `InputControl`.
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<input id="name" type="text" />
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```TypeScript
|
|
23
|
+
const inputElem = document.getElementById("name");
|
|
24
|
+
const textbox = new Textbox(inputElem);
|
|
25
|
+
|
|
26
|
+
textbox.on(CHANGE_EVENT, (e: ChangeEventData) => { });
|
|
27
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brandup/ui-textbox",
|
|
3
|
+
"description": "Textbox control type and styles.",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"brandup",
|
|
6
|
+
"javascript",
|
|
7
|
+
"typescript",
|
|
8
|
+
"ui"
|
|
9
|
+
],
|
|
10
|
+
"author": {
|
|
11
|
+
"name": "Dmitry Kovyazin",
|
|
12
|
+
"email": "it@brandup.online"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/brandup-online/brandup-ui/npm/brandup-ui-textbox",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/brandup-online/brandup-ui.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/brandup-online/brandup-ui/issues",
|
|
21
|
+
"email": "it@brandup.online"
|
|
22
|
+
},
|
|
23
|
+
"license": "Apache-2.0",
|
|
24
|
+
"version": "1.0.14",
|
|
25
|
+
"main": "source/index.ts",
|
|
26
|
+
"types": "source/index.ts",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@brandup/ui": "^1.0.32",
|
|
29
|
+
"@brandup/ui-dom": "^1.0.32",
|
|
30
|
+
"@brandup/ui-helpers": "^1.0.32",
|
|
31
|
+
"@brandup/ui-input": "^1.0.14",
|
|
32
|
+
"@brandup/ui-kit": "^1.0.14"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"source",
|
|
36
|
+
"README.md"
|
|
37
|
+
]
|
|
38
|
+
}
|
package/source/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './textbox'
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg viewBox="0 0 26 26" >
|
|
2
|
+
<path d="M5.8 4C4.8055 4 4 4.8055 4 5.8V18.4H5.8V5.8H18.4V4H5.8ZM9.4 7.6C8.4055 7.6 7.6 8.4055 7.6 9.4V20.2C7.6 21.1945 8.4055 22 9.4 22H20.2C21.1945 22 22 21.1945 22 20.2V9.4C22 8.4055 21.1945 7.6 20.2 7.6H9.4ZM9.4 9.4H20.2V20.2H9.4V9.4Z" />
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg viewBox="0 0 42 42" >
|
|
2
|
+
<path d="M37.2371 8.00075C36.7922 8.01416 36.37 8.20224 36.0599 8.52514L15.0108 29.8187L5.94773 20.6504C5.78995 20.4841 5.60097 20.3514 5.39185 20.26C5.18273 20.1686 4.95768 20.1203 4.72988 20.1179C4.50207 20.1156 4.2761 20.1592 4.06518 20.2463C3.85427 20.3334 3.66266 20.4623 3.50157 20.6252C3.34048 20.7882 3.21315 20.982 3.12704 21.1954C3.04093 21.4087 2.99777 21.6373 3.00009 21.8678C3.00241 22.0982 3.05015 22.3259 3.14054 22.5375C3.23092 22.749 3.36212 22.9402 3.52645 23.0998L13.8002 33.4929C14.1213 33.8176 14.5568 34 15.0108 34C15.4648 34 15.9003 33.8176 16.2214 33.4929L38.4811 10.9745C38.7286 10.7312 38.8976 10.418 38.966 10.0759C39.0344 9.73372 38.9991 9.37865 38.8646 9.05718C38.7301 8.73571 38.5028 8.46284 38.2123 8.27433C37.9219 8.08582 37.582 7.99046 37.2371 8.00075Z" />
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--textbox-maxheight: 200px;
|
|
3
|
+
--textbox-multyline-height: 100px;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.ui-textbox {
|
|
7
|
+
min-height: var(--input-height);
|
|
8
|
+
max-height: var(--textbox-maxheight);
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-flow: row nowrap;
|
|
11
|
+
align-items: stretch;
|
|
12
|
+
position: relative;
|
|
13
|
+
background-color: var(--input-fill);
|
|
14
|
+
border-radius: var(--input-border-radius); // чтобы заливка при состояниях не выходила за пределы импута
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
outline: none;
|
|
17
|
+
transition: background-color ease 60ms;
|
|
18
|
+
line-height: var(--input-line-height);
|
|
19
|
+
font-size: var(--input-font-size);
|
|
20
|
+
font-weight: var(--input-font-weight);
|
|
21
|
+
color: var(--input-color);
|
|
22
|
+
|
|
23
|
+
--svg-fill: var(--input-color);
|
|
24
|
+
--svg-size: 18px;
|
|
25
|
+
|
|
26
|
+
& .decorator {
|
|
27
|
+
display: block;
|
|
28
|
+
position: absolute;
|
|
29
|
+
left: 0;
|
|
30
|
+
top: 0;
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: 100%;
|
|
33
|
+
border: var(--input-border-type) var(--input-border-width) var(--input-border-color);
|
|
34
|
+
border-radius: var(--input-border-radius);
|
|
35
|
+
z-index: 1;
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
transition: border-color ease 120ms;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
& .editor {
|
|
41
|
+
flex: 1 1 auto;
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-flow: row nowrap;
|
|
44
|
+
gap: 15px;
|
|
45
|
+
padding: calc((var(--input-height) - var(--input-line-height)) / 2) var(--input-padding-lr);
|
|
46
|
+
box-sizing: border-box;
|
|
47
|
+
cursor: text;
|
|
48
|
+
position: relative;
|
|
49
|
+
overflow-y: auto;
|
|
50
|
+
z-index: 2;
|
|
51
|
+
margin-right: 5px;
|
|
52
|
+
padding-right: calc(var(--input-padding-lr) - 5px);
|
|
53
|
+
scrollbar-width: 6px;
|
|
54
|
+
|
|
55
|
+
&::-webkit-scrollbar {
|
|
56
|
+
width: 6px;
|
|
57
|
+
background: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&::-webkit-scrollbar-track {
|
|
61
|
+
border-radius: 3px;
|
|
62
|
+
background-color: transparent;
|
|
63
|
+
margin: 6px 0;
|
|
64
|
+
cursor: default;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
&::-webkit-scrollbar-thumb {
|
|
68
|
+
background-color: #8696a0;
|
|
69
|
+
border-radius: 3px;
|
|
70
|
+
cursor: default;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
& .input {
|
|
75
|
+
appearance: none;
|
|
76
|
+
position: relative;
|
|
77
|
+
flex: 1 1 auto;
|
|
78
|
+
box-sizing: border-box;
|
|
79
|
+
word-wrap: anywhere;
|
|
80
|
+
outline: none;
|
|
81
|
+
|
|
82
|
+
&:empty {
|
|
83
|
+
&:after {
|
|
84
|
+
content: attr(data-placeholder);
|
|
85
|
+
display: block;
|
|
86
|
+
position: absolute;
|
|
87
|
+
left: 0;
|
|
88
|
+
top: 0;
|
|
89
|
+
right: 0;
|
|
90
|
+
color: var(--placeholder-color);
|
|
91
|
+
font-weight: var(--placeholder-font-weight);
|
|
92
|
+
font-style: var(--placeholder-font-style);
|
|
93
|
+
box-sizing: border-box;
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
text-overflow: ellipsis;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
& ~ .symbols {
|
|
100
|
+
display: none;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
& .symbols {
|
|
106
|
+
flex: 0 0 auto;
|
|
107
|
+
display: none;
|
|
108
|
+
font-size: 13px;
|
|
109
|
+
font-weight: 400;
|
|
110
|
+
z-index: 5;
|
|
111
|
+
user-select: none;
|
|
112
|
+
color: #656565;
|
|
113
|
+
opacity: 0.8;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
& .actions {
|
|
117
|
+
flex: 0 0 auto;
|
|
118
|
+
position: relative;
|
|
119
|
+
z-index: 4;
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-flow: row nowrap;
|
|
122
|
+
align-items: center;
|
|
123
|
+
height: var(--input-height);
|
|
124
|
+
|
|
125
|
+
& button {
|
|
126
|
+
display: flex;
|
|
127
|
+
flex-flow: row nowrap;
|
|
128
|
+
justify-content: center;
|
|
129
|
+
align-items: center;
|
|
130
|
+
width: var(--input-height);
|
|
131
|
+
height: var(--input-height);
|
|
132
|
+
background: transparent;
|
|
133
|
+
border: none;
|
|
134
|
+
padding: 0;
|
|
135
|
+
margin: 0;
|
|
136
|
+
outline: none;
|
|
137
|
+
transition: all ease 100ms;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
&:empty {
|
|
141
|
+
display: none;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// мультистроковый режим
|
|
146
|
+
&.multyline {
|
|
147
|
+
// data-multyline == true
|
|
148
|
+
min-height: var(--textbox-multyline-height);
|
|
149
|
+
|
|
150
|
+
& .editor {
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
gap: 2px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
& .input {
|
|
156
|
+
flex: 1 10 auto;
|
|
157
|
+
|
|
158
|
+
&:empty {
|
|
159
|
+
&:after {
|
|
160
|
+
white-space: normal;
|
|
161
|
+
text-overflow: unset;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// включен счётчик символов
|
|
168
|
+
&.counter {
|
|
169
|
+
// data-symbolcounter == true
|
|
170
|
+
|
|
171
|
+
& .symbols {
|
|
172
|
+
display: block;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
& .actions {
|
|
176
|
+
margin-left: -10px;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
&:hover {
|
|
181
|
+
--input-border-color: var(--hover--input-border-color);
|
|
182
|
+
--input-fill: var(--hover--input-fill);
|
|
183
|
+
--input-color: var(--hover--input-color);
|
|
184
|
+
--svg-fill: var(--hover--input-color);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
&.focused {
|
|
188
|
+
--input-border-color: var(--focus--input-border-color);
|
|
189
|
+
--input-fill: var(--focus--input-fill);
|
|
190
|
+
--input-color: var(--focus--input-color);
|
|
191
|
+
--svg-fill: var(--focus--input-color);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
&.readonly {
|
|
195
|
+
--input-fill: var(--readonly--input-fill);
|
|
196
|
+
--input-color: var(--readonly--input-color);
|
|
197
|
+
--svg-fill: var(--readonly--input-color);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
&.disabled {
|
|
201
|
+
--input-border-color: var(--disabled--input-border-color);
|
|
202
|
+
--input-fill: var(--disabled--input-fill);
|
|
203
|
+
--input-color: var(--disabled--input-color);
|
|
204
|
+
--svg-fill: var(--disabled--input-color);
|
|
205
|
+
|
|
206
|
+
& .editor {
|
|
207
|
+
user-select: none;
|
|
208
|
+
cursor: default;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
& .actions {
|
|
212
|
+
& button {
|
|
213
|
+
cursor: default;
|
|
214
|
+
|
|
215
|
+
& svg {
|
|
216
|
+
fill: var(--disabled--input-color);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
&.invalid {
|
|
223
|
+
--input-border-color: var(--invalid--input-border-color);
|
|
224
|
+
--input-fill: var(--invalid--input-fill);
|
|
225
|
+
--input-color: var(--invalid--input-color);
|
|
226
|
+
--placeholder-color: var(--invalid--input-color);
|
|
227
|
+
--svg-fill: var(--invalid--input-color);
|
|
228
|
+
|
|
229
|
+
& .decorator {
|
|
230
|
+
transition-delay: 0;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
&.incorrect {
|
|
235
|
+
--input-fill: var(--incorrect--input-fill);
|
|
236
|
+
--input-color: var(--incorrect--input-color);
|
|
237
|
+
--svg-fill: var(--incorrect--input-color);
|
|
238
|
+
|
|
239
|
+
& .decorator {
|
|
240
|
+
transition-delay: 0;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.textbox-input {
|
|
246
|
+
opacity: 0;
|
|
247
|
+
position: absolute;
|
|
248
|
+
width: 1px;
|
|
249
|
+
height: 1px;
|
|
250
|
+
outline: none;
|
|
251
|
+
visibility: collapse;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
textarea.textbox-input ~ .textbox-miniature {
|
|
255
|
+
min-height: 100px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.textbox-miniature {
|
|
259
|
+
border: var(--input-border-type) var(--input-border-width) var(--input-border-color);
|
|
260
|
+
border-radius: var(--input-border-radius);
|
|
261
|
+
height: var(--input-height);
|
|
262
|
+
background: white;
|
|
263
|
+
box-sizing: border-box;
|
|
264
|
+
}
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import "./textbox.less"; // стили компонента
|
|
2
|
+
|
|
3
|
+
import { InputControl } from "@brandup/ui-input";
|
|
4
|
+
import { IS_TOUCH_DEVICE } from "@brandup/ui-kit";
|
|
5
|
+
import { DOM } from "@brandup/ui-dom";
|
|
6
|
+
import { FuncHelper } from "@brandup/ui-helpers";
|
|
7
|
+
import copyIcon from "./svg/copy.svg";
|
|
8
|
+
import doneIcon from "./svg/tick.svg";
|
|
9
|
+
|
|
10
|
+
export const ROOT_CLASS = "ui-textbox";
|
|
11
|
+
export const INPUT_CLASS = "textbox-input";
|
|
12
|
+
export const MINIATURE_CLASS = "textbox-miniature";
|
|
13
|
+
export const CHANGE_EVENT = "textbox-change";
|
|
14
|
+
export const MAX_EMAIL_LENGTH = 256; // https://www.rfc-editor.org/rfc/rfc5321#section-4.5.3
|
|
15
|
+
|
|
16
|
+
export type TextBoxType = "text" | "email" | "url" | "tel" | "number";
|
|
17
|
+
|
|
18
|
+
export default class TextBox extends InputControl<HTMLInputElement | HTMLTextAreaElement> {
|
|
19
|
+
private __inputElem: HTMLElement;
|
|
20
|
+
private __actionsElem: HTMLElement;
|
|
21
|
+
private __symbolsCountElem?: HTMLElement;
|
|
22
|
+
|
|
23
|
+
readonly type: TextBoxType;
|
|
24
|
+
readonly allowEmptyStrings: boolean;
|
|
25
|
+
readonly multyline: boolean;
|
|
26
|
+
readonly placeholder: string | null;
|
|
27
|
+
readonly copyButton: boolean;
|
|
28
|
+
readonly maxlength: number;
|
|
29
|
+
readonly inputmode: string;
|
|
30
|
+
readonly symbolCounter: boolean;
|
|
31
|
+
readonly autoFocus: boolean;
|
|
32
|
+
|
|
33
|
+
get typeName(): string { return "BrandUp.TextBox"; }
|
|
34
|
+
|
|
35
|
+
constructor(valueElem: HTMLInputElement | HTMLTextAreaElement) {
|
|
36
|
+
super(valueElem);
|
|
37
|
+
|
|
38
|
+
if (this.__valueElem instanceof HTMLInputElement) {
|
|
39
|
+
switch (this.__valueElem.type) {
|
|
40
|
+
case "text":
|
|
41
|
+
this.type = "text";
|
|
42
|
+
break;
|
|
43
|
+
case "email":
|
|
44
|
+
this.type = "email";
|
|
45
|
+
|
|
46
|
+
if (!this.__valueElem.maxLength || this.__valueElem.maxLength > MAX_EMAIL_LENGTH)
|
|
47
|
+
this.__valueElem.maxLength = MAX_EMAIL_LENGTH;
|
|
48
|
+
|
|
49
|
+
break;
|
|
50
|
+
case "url":
|
|
51
|
+
this.type = "url";
|
|
52
|
+
break;
|
|
53
|
+
case "tel":
|
|
54
|
+
this.type = "tel";
|
|
55
|
+
break;
|
|
56
|
+
case "number":
|
|
57
|
+
this.type = "number";
|
|
58
|
+
this.__valueElem.step = "1"; // Поддерживаем пока что только целые числа
|
|
59
|
+
break;
|
|
60
|
+
default:
|
|
61
|
+
throw new Error(`Тип ввода ${this.__valueElem.type} не поддерживается.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else
|
|
65
|
+
this.type = "text";
|
|
66
|
+
|
|
67
|
+
this.__valueElem.classList.add(INPUT_CLASS);
|
|
68
|
+
|
|
69
|
+
// Инициализация свойств
|
|
70
|
+
this.maxlength = this.__valueElem.maxLength;
|
|
71
|
+
this.symbolCounter = this.__valueElem.hasAttribute("data-symbolcounter");
|
|
72
|
+
this.autoFocus = this.__valueElem.hasAttribute("data-autofocus");
|
|
73
|
+
this.allowEmptyStrings = this.__valueElem.hasAttribute("data-allow-empty-strings");
|
|
74
|
+
this.placeholder = this.__valueElem.getAttribute("placeholder");
|
|
75
|
+
this.inputmode = this.__valueElem.inputMode;
|
|
76
|
+
this.multyline = valueElem instanceof HTMLTextAreaElement;
|
|
77
|
+
this.copyButton = this.__valueElem.hasAttribute("data-copy-button") || this.__valueElem.hasAttribute("data-copybutton");
|
|
78
|
+
|
|
79
|
+
this.__inputElem = DOM.tag("div", { class: "input" });
|
|
80
|
+
this.__actionsElem = DOM.tag("div", { class: "actions" });
|
|
81
|
+
|
|
82
|
+
this.__renderUI();
|
|
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)) }, [
|
|
89
|
+
DOM.tag("div", { class: "decorator" }),
|
|
90
|
+
DOM.tag("div", "editor", [
|
|
91
|
+
this.__inputElem,
|
|
92
|
+
this.__symbolsCountElem = DOM.tag("div", { class: "symbols" })
|
|
93
|
+
]),
|
|
94
|
+
this.__actionsElem
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
container.classList.remove(INPUT_CLASS);
|
|
98
|
+
|
|
99
|
+
this.__inputElem.tabIndex = this.__valueElem.tabIndex;
|
|
100
|
+
this.__valueElem.tabIndex = -1;
|
|
101
|
+
|
|
102
|
+
if (this.multyline) container.classList.add("multyline");
|
|
103
|
+
|
|
104
|
+
if (this.symbolCounter) container.classList.add("counter");
|
|
105
|
+
|
|
106
|
+
if (this.disabled) {
|
|
107
|
+
this.__inputElem.tabIndex = -1;
|
|
108
|
+
}
|
|
109
|
+
else
|
|
110
|
+
this.__inputElem.contentEditable = "true";
|
|
111
|
+
|
|
112
|
+
if (this.inputmode) this.__inputElem.inputMode = this.inputmode;
|
|
113
|
+
|
|
114
|
+
if (this.copyButton) {
|
|
115
|
+
const buttonElem = <HTMLButtonElement>DOM.tag("button", { command: "copy-text", title: "Скопировать в буфер обмена" }, copyIcon);
|
|
116
|
+
if (this.disabled)
|
|
117
|
+
buttonElem.disabled = true;
|
|
118
|
+
this.__actionsElem.insertAdjacentElement("beforeend", buttonElem);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.__inputElem.setAttribute("data-placeholder", this.placeholder ?? "");
|
|
122
|
+
|
|
123
|
+
this.setElement(container);
|
|
124
|
+
|
|
125
|
+
if (this.__valueElem.nextElementSibling) {
|
|
126
|
+
// Если следующий элемент это миниатюра пока textbox не отрисован
|
|
127
|
+
const nextElem = <HTMLElement>this.__valueElem.nextElementSibling;
|
|
128
|
+
if (nextElem.classList.contains(MINIATURE_CLASS)) nextElem.remove();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.__valueElem.insertAdjacentElement("afterend", container);
|
|
132
|
+
container.insertAdjacentElement("afterbegin", this.__valueElem);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private __initLogic() {
|
|
136
|
+
if (!this.element)
|
|
137
|
+
return;
|
|
138
|
+
|
|
139
|
+
this.element.addEventListener("drop", (e) => e.preventDefault());
|
|
140
|
+
this.element.addEventListener("dragenter", (e) => e.preventDefault());
|
|
141
|
+
|
|
142
|
+
this.__valueElem.addEventListener("change", (e: Event) => {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
e.stopImmediatePropagation();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
let hasInputClick = false;
|
|
148
|
+
this.__inputElem.addEventListener("mousedown", () => {
|
|
149
|
+
if (this.disabled)
|
|
150
|
+
return;
|
|
151
|
+
|
|
152
|
+
hasInputClick = true;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
this.__inputElem.addEventListener("focus", () => {
|
|
156
|
+
if (this.disabled)
|
|
157
|
+
return;
|
|
158
|
+
|
|
159
|
+
this.element?.classList.add("focused");
|
|
160
|
+
|
|
161
|
+
if (this.readonly)
|
|
162
|
+
this.__selectAll();
|
|
163
|
+
else if (!hasInputClick)
|
|
164
|
+
this.__carretToEnd(); // пыремещаем курсов в конец, если клик не по строке
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this.__inputElem.addEventListener("blur", (e) => {
|
|
168
|
+
hasInputClick = false;
|
|
169
|
+
|
|
170
|
+
if (this.disabled)
|
|
171
|
+
return;
|
|
172
|
+
|
|
173
|
+
this.element?.classList.remove("focused");
|
|
174
|
+
|
|
175
|
+
// когда удаляем весь текст, то браузер оставляет один BR, что означает что текста нет
|
|
176
|
+
// удалить BR нужно, чтобы появился placeholder
|
|
177
|
+
if (this.__inputElem.firstChild?.nodeName === "BR")
|
|
178
|
+
DOM.empty(this.__inputElem);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
this.__inputElem.addEventListener("dblclick", () => {
|
|
182
|
+
if (this.disabled)
|
|
183
|
+
return;
|
|
184
|
+
|
|
185
|
+
if (this.copyButton && this.readonly)
|
|
186
|
+
this.__selectAll();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
this.element.addEventListener("paste", (e) => {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
e.stopPropagation();
|
|
192
|
+
|
|
193
|
+
if (this.readonly || this.disabled || !this.element) return false;
|
|
194
|
+
|
|
195
|
+
let pastedData = e.clipboardData?.getData("text/plain");
|
|
196
|
+
if (pastedData) {
|
|
197
|
+
if (this.type == "number") {
|
|
198
|
+
const numberData = /[\d\s]+/.exec(pastedData);
|
|
199
|
+
if (numberData && numberData.length)
|
|
200
|
+
pastedData = numberData[0].replace(' ', '');
|
|
201
|
+
else {
|
|
202
|
+
this.element.classList.add("incorrect");
|
|
203
|
+
window.setTimeout(() => this.element?.classList.remove("incorrect"), 300);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const selection = window.getSelection();
|
|
209
|
+
if (!selection)
|
|
210
|
+
return; // TODO
|
|
211
|
+
|
|
212
|
+
if (this.maxlength > 0) {
|
|
213
|
+
// обрезаем вставляемый текст по кол-ву оставшихся символов для ввода
|
|
214
|
+
|
|
215
|
+
const selectionLength = selection.toString().length;
|
|
216
|
+
const currentTextLength = this.__getTextLenght();
|
|
217
|
+
const leftSymbols = this.maxlength - currentTextLength + selectionLength; // осталось символов для ввода
|
|
218
|
+
|
|
219
|
+
if (pastedData.length > leftSymbols)
|
|
220
|
+
pastedData = pastedData.substring(0, leftSymbols);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const lines = pastedData.split(/\n/);
|
|
224
|
+
// тримим все строки, кроме начала первой строки, вдруг так нужно
|
|
225
|
+
const output = lines.map((line, index) => index === 0 ? line.trimEnd() : line.trim());
|
|
226
|
+
|
|
227
|
+
var fragment = document.createDocumentFragment();
|
|
228
|
+
if (!this.multyline) {
|
|
229
|
+
fragment.appendChild(document.createTextNode(output.join(" ")));
|
|
230
|
+
} else {
|
|
231
|
+
output.forEach((line, index) => {
|
|
232
|
+
if (index > 0) fragment.appendChild(document.createElement("br"));
|
|
233
|
+
|
|
234
|
+
fragment.appendChild(document.createTextNode(line));
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Удаляем выделенную область
|
|
239
|
+
selection.getRangeAt(0).deleteContents();
|
|
240
|
+
|
|
241
|
+
// Вставляем текст
|
|
242
|
+
selection.getRangeAt(0).insertNode(fragment);
|
|
243
|
+
|
|
244
|
+
// Перемещаем курсор в конец вставленной области
|
|
245
|
+
selection.setPosition(selection.focusNode, selection.focusOffset);
|
|
246
|
+
|
|
247
|
+
this.__applyValue();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
this.__inputElem.addEventListener("keydown", (e: KeyboardEvent) => {
|
|
252
|
+
if (!this.element)
|
|
253
|
+
return;
|
|
254
|
+
|
|
255
|
+
const isChar = e.key.length === 1;
|
|
256
|
+
|
|
257
|
+
if ((this.readonly || this.disabled) && isChar && !e.ctrlKey) {
|
|
258
|
+
e.preventDefault();
|
|
259
|
+
e.stopPropagation();
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (this.maxlength > 0 && isChar && !e.ctrlKey) {
|
|
264
|
+
const currentTextLength = this.__getTextLenght();
|
|
265
|
+
if (currentTextLength >= this.maxlength) {
|
|
266
|
+
e.preventDefault();
|
|
267
|
+
e.stopPropagation();
|
|
268
|
+
|
|
269
|
+
this.__toIncorrect();
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (isChar && !e.ctrlKey) {
|
|
275
|
+
let isIncorrect = false;
|
|
276
|
+
|
|
277
|
+
switch (this.type) {
|
|
278
|
+
case "number":
|
|
279
|
+
if (!/\d/.test(e.key))
|
|
280
|
+
isIncorrect = true;
|
|
281
|
+
break;
|
|
282
|
+
case "email":
|
|
283
|
+
if (!/[a-zA-Z\d\.\-\_\@]/.test(e.key))
|
|
284
|
+
isIncorrect = true;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (isIncorrect) {
|
|
289
|
+
e.preventDefault();
|
|
290
|
+
e.stopPropagation();
|
|
291
|
+
|
|
292
|
+
this.__toIncorrect();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!this.multyline && (e.key == "U+000A" || e.key == "Enter")) {
|
|
298
|
+
// Если однострочный режим и нажат enter, то отправляем submit формы
|
|
299
|
+
e.preventDefault();
|
|
300
|
+
this.__submitForm();
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
this.__inputElem.addEventListener("input", () => {
|
|
306
|
+
if (!this.element)
|
|
307
|
+
return;
|
|
308
|
+
|
|
309
|
+
if (this.multyline && this.__inputElem.children.length === 1) {
|
|
310
|
+
const child = this.__inputElem.children.item(0);
|
|
311
|
+
if (child && child.tagName === "BR")
|
|
312
|
+
this.__inputElem.innerHTML = '';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.__applyValue();
|
|
316
|
+
|
|
317
|
+
let clearInvalidState = true;
|
|
318
|
+
|
|
319
|
+
if (this.element.classList.contains("invalid"))
|
|
320
|
+
clearInvalidState = this.validate(); // Если уже не валидно, то перепроверяем
|
|
321
|
+
|
|
322
|
+
if (clearInvalidState)
|
|
323
|
+
this.element.classList.remove("invalid");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
this.registerCommand("copy-text", async context => {
|
|
327
|
+
if (!window.navigator.clipboard || this.disabled) return;
|
|
328
|
+
|
|
329
|
+
await window.navigator.clipboard.writeText(this.__valueElem.value)
|
|
330
|
+
|
|
331
|
+
const prevHtml = context.target.innerHTML;
|
|
332
|
+
context.target.innerHTML = doneIcon;
|
|
333
|
+
context.target.classList.add("success");
|
|
334
|
+
|
|
335
|
+
const abort = new AbortController();
|
|
336
|
+
await FuncHelper.delay(2000, abort.signal);
|
|
337
|
+
|
|
338
|
+
context.target.innerHTML = prevHtml;
|
|
339
|
+
context.target.classList.remove("success");
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private __initText() {
|
|
344
|
+
var html = "";
|
|
345
|
+
|
|
346
|
+
const text = this.__valueElem.value;
|
|
347
|
+
if (text) {
|
|
348
|
+
const lines = text.split(/\n/);
|
|
349
|
+
const output = lines.map((line, index) => {
|
|
350
|
+
line = line.trim();
|
|
351
|
+
|
|
352
|
+
if (index > 0) line = `<div>${line}</div>`;
|
|
353
|
+
|
|
354
|
+
return line;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
html = output.join("");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
this.__inputElem.innerHTML = html;
|
|
361
|
+
|
|
362
|
+
this.__refreshSymbolsCount();
|
|
363
|
+
|
|
364
|
+
if (this.autoFocus && !IS_TOUCH_DEVICE && !this.disabled && !this.readonly)
|
|
365
|
+
this.__inputElem.focus();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private __applyValue() {
|
|
369
|
+
let newValue = this.__inputElem.innerText.trim();
|
|
370
|
+
this.__valueElem.value = newValue;
|
|
371
|
+
|
|
372
|
+
this.__refreshSymbolsCount();
|
|
373
|
+
this.__onChange();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private __toIncorrect() {
|
|
377
|
+
if (!this.element)
|
|
378
|
+
return;
|
|
379
|
+
|
|
380
|
+
this.element.classList.add("incorrect");
|
|
381
|
+
window.setTimeout(() => this.element?.classList.remove("incorrect"), 200);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private __refreshSymbolsCount() {
|
|
385
|
+
if (!this.__symbolsCountElem)
|
|
386
|
+
return;
|
|
387
|
+
|
|
388
|
+
const textLength = this.__getTextLenght();
|
|
389
|
+
let counterValue: string;
|
|
390
|
+
|
|
391
|
+
if (this.maxlength > 0) {
|
|
392
|
+
counterValue = `${textLength}/${this.maxlength}`;
|
|
393
|
+
if (this.maxlength < textLength)
|
|
394
|
+
this.__symbolsCountElem.classList.add("invalid");
|
|
395
|
+
else
|
|
396
|
+
this.__symbolsCountElem.classList.remove("invalid");
|
|
397
|
+
}
|
|
398
|
+
else
|
|
399
|
+
counterValue = textLength.toString();
|
|
400
|
+
|
|
401
|
+
this.__symbolsCountElem.textContent = counterValue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private __selectAll() {
|
|
405
|
+
this.__inputElem.focus();
|
|
406
|
+
|
|
407
|
+
window.getSelection()?.selectAllChildren(this.__inputElem)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private __carretToEnd() {
|
|
411
|
+
var range = document.createRange();
|
|
412
|
+
range.selectNodeContents(this.__inputElem);
|
|
413
|
+
range.collapse(false);
|
|
414
|
+
var sel = window.getSelection();
|
|
415
|
+
if (sel) {
|
|
416
|
+
sel.removeAllRanges();
|
|
417
|
+
sel.addRange(range);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private __getTextLenght() {
|
|
422
|
+
return this.__inputElem.innerText.length;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private __onChange() {
|
|
426
|
+
this.trigger(CHANGE_EVENT, <ChangeEventData>{
|
|
427
|
+
textbox: this,
|
|
428
|
+
value: this.getValue()
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
onChange(handler: (e: ChangeEventData) => void) {
|
|
433
|
+
this.on(CHANGE_EVENT, handler);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
hasValue(): boolean {
|
|
437
|
+
return !!this.getValue();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
getValue(): string {
|
|
441
|
+
return this.__valueElem.value?.trim() ?? null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
setValue(value: string): void {
|
|
445
|
+
this.__valueElem.value = value?.trim() ?? "";
|
|
446
|
+
|
|
447
|
+
this.__initText();
|
|
448
|
+
this.__onChange();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
validate(): boolean {
|
|
452
|
+
if (!this.element)
|
|
453
|
+
return false;
|
|
454
|
+
|
|
455
|
+
let isValid = super.validate();
|
|
456
|
+
if (isValid) {
|
|
457
|
+
let value = this.getValue();
|
|
458
|
+
|
|
459
|
+
if (this.required && !value)
|
|
460
|
+
isValid = false;
|
|
461
|
+
|
|
462
|
+
if (this.maxlength > 0 && this.maxlength < value.length)
|
|
463
|
+
isValid = false;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (!isValid)
|
|
467
|
+
this.element.classList.add("invalid");
|
|
468
|
+
else
|
|
469
|
+
this.element.classList.remove("invalid");
|
|
470
|
+
|
|
471
|
+
return isValid;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
destroy(): void {
|
|
475
|
+
this.__valueElem.tabIndex = this.__inputElem.tabIndex;
|
|
476
|
+
|
|
477
|
+
this.element?.insertAdjacentElement("afterend", this.__valueElem);
|
|
478
|
+
this.element?.remove();
|
|
479
|
+
|
|
480
|
+
super.destroy();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export interface ChangeEventData {
|
|
485
|
+
textbox: TextBox;
|
|
486
|
+
value: string;
|
|
487
|
+
}
|