@tuidom/elements 0.2.1 → 0.2.3
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.
|
@@ -18,11 +18,33 @@ export declare class InputElement extends TUIElement {
|
|
|
18
18
|
readonly inputState: InputState;
|
|
19
19
|
placeholder: string | undefined;
|
|
20
20
|
showBorder: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Маска поля пароля: каждый графемный кластер значения рисуется этой
|
|
23
|
+
* строкой, сам текст на экран не попадает и не отдаётся инспектору.
|
|
24
|
+
* `undefined` — обычное поле. Модель (`inputState`) хранит настоящий текст:
|
|
25
|
+
* маска — это только представление, редактирование и `onChange` работают
|
|
26
|
+
* по-прежнему с реальным значением.
|
|
27
|
+
*/
|
|
28
|
+
maskChar: string | undefined;
|
|
21
29
|
onChange: ((value: string) => void) | undefined;
|
|
22
30
|
private scrollX;
|
|
23
31
|
constructor(inputState?: InputState);
|
|
24
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Наблюдаемость для инспектора/e2e: текст, курсор, выделение, плейсхолдер.
|
|
34
|
+
* Под маской (`maskChar`) отдаётся ровно то, что на экране, — секрет наружу
|
|
35
|
+
* не уходит; `cursorOffset` при этом тоже смещение в маске.
|
|
36
|
+
*/
|
|
25
37
|
inspectState(): Record<string, unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* Что рисуем и где в нарисованном стоят курсор и выделение.
|
|
40
|
+
*
|
|
41
|
+
* Без маски — сам текст и его же смещения. С маской — по одному
|
|
42
|
+
* {@link maskChar} на графемный кластер, а смещения пересчитаны в этот новый
|
|
43
|
+
* текст: курсор за N-й графемой стоит за N-й маской. Смещения считаем через
|
|
44
|
+
* длину самой маски, а не «одна графема = один символ», — маской может быть
|
|
45
|
+
* и многокодовый глиф.
|
|
46
|
+
*/
|
|
47
|
+
private displayView;
|
|
26
48
|
getMinIntrinsicWidth(_height: number): number;
|
|
27
49
|
getMinIntrinsicHeight(_width: number): number;
|
|
28
50
|
getMaxIntrinsicHeight(_width: number): number;
|
|
@@ -19,6 +19,14 @@ export class InputElement extends TUIElement {
|
|
|
19
19
|
inputState;
|
|
20
20
|
placeholder = undefined;
|
|
21
21
|
showBorder = false;
|
|
22
|
+
/**
|
|
23
|
+
* Маска поля пароля: каждый графемный кластер значения рисуется этой
|
|
24
|
+
* строкой, сам текст на экран не попадает и не отдаётся инспектору.
|
|
25
|
+
* `undefined` — обычное поле. Модель (`inputState`) хранит настоящий текст:
|
|
26
|
+
* маска — это только представление, редактирование и `onChange` работают
|
|
27
|
+
* по-прежнему с реальным значением.
|
|
28
|
+
*/
|
|
29
|
+
maskChar = undefined;
|
|
22
30
|
onChange = undefined;
|
|
23
31
|
scrollX = 0;
|
|
24
32
|
constructor(inputState) {
|
|
@@ -29,15 +37,49 @@ export class InputElement extends TUIElement {
|
|
|
29
37
|
// ядро ставит focus-состояние и перерезолвит стиль (render дёргается).
|
|
30
38
|
this.style = { fg: "input.foreground", bg: "input.background" };
|
|
31
39
|
}
|
|
32
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Наблюдаемость для инспектора/e2e: текст, курсор, выделение, плейсхолдер.
|
|
42
|
+
* Под маской (`maskChar`) отдаётся ровно то, что на экране, — секрет наружу
|
|
43
|
+
* не уходит; `cursorOffset` при этом тоже смещение в маске.
|
|
44
|
+
*/
|
|
33
45
|
inspectState() {
|
|
46
|
+
const view = this.displayView();
|
|
34
47
|
return {
|
|
35
|
-
value:
|
|
36
|
-
cursorOffset:
|
|
48
|
+
value: view.text,
|
|
49
|
+
cursorOffset: view.cursorOffset,
|
|
37
50
|
hasSelection: this.inputState.hasSelection,
|
|
38
51
|
showsPlaceholder: this.inputState.value.length === 0 && this.placeholder !== undefined,
|
|
39
52
|
};
|
|
40
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Что рисуем и где в нарисованном стоят курсор и выделение.
|
|
56
|
+
*
|
|
57
|
+
* Без маски — сам текст и его же смещения. С маской — по одному
|
|
58
|
+
* {@link maskChar} на графемный кластер, а смещения пересчитаны в этот новый
|
|
59
|
+
* текст: курсор за N-й графемой стоит за N-й маской. Смещения считаем через
|
|
60
|
+
* длину самой маски, а не «одна графема = один символ», — маской может быть
|
|
61
|
+
* и многокодовый глиф.
|
|
62
|
+
*/
|
|
63
|
+
displayView() {
|
|
64
|
+
const state = this.inputState;
|
|
65
|
+
const mask = this.maskChar;
|
|
66
|
+
if (mask === undefined) {
|
|
67
|
+
return {
|
|
68
|
+
text: state.text,
|
|
69
|
+
cursorOffset: state.cursorOffset,
|
|
70
|
+
selectionStart: state.selectionStart,
|
|
71
|
+
selectionEnd: state.selectionEnd,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const slots = new DisplayLine(state.text).slots;
|
|
75
|
+
const toMask = (offset) => slots.filter((slot) => slot.offset < offset).length * mask.length;
|
|
76
|
+
return {
|
|
77
|
+
text: mask.repeat(slots.length),
|
|
78
|
+
cursorOffset: toMask(state.cursorOffset),
|
|
79
|
+
selectionStart: toMask(state.selectionStart),
|
|
80
|
+
selectionEnd: toMask(state.selectionEnd),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
41
83
|
// ─── Layout ─────────────────────────────────────────────────────────────
|
|
42
84
|
getMinIntrinsicWidth(_height) {
|
|
43
85
|
return this.showBorder ? 5 : 3;
|
|
@@ -57,8 +99,11 @@ export class InputElement extends TUIElement {
|
|
|
57
99
|
render(context) {
|
|
58
100
|
const w = this.layoutSize.width;
|
|
59
101
|
const h = this.layoutSize.height;
|
|
60
|
-
|
|
61
|
-
|
|
102
|
+
// Рисуем ПРЕДСТАВЛЕНИЕ значения: без маски это сам текст, под маской —
|
|
103
|
+
// её символы со смещениями, пересчитанными в них.
|
|
104
|
+
const view = this.displayView();
|
|
105
|
+
const text = view.text;
|
|
106
|
+
const cursorOffset = view.cursorOffset;
|
|
62
107
|
const focused = this.isFocused;
|
|
63
108
|
const contentXStart = this.showBorder ? 1 : 0;
|
|
64
109
|
const contentY = this.showBorder ? 1 : 0;
|
|
@@ -94,7 +139,7 @@ export class InputElement extends TUIElement {
|
|
|
94
139
|
});
|
|
95
140
|
}
|
|
96
141
|
else if (this.inputState.hasSelection) {
|
|
97
|
-
this.renderTextWithSelection(textContext, dl, text, contentXStart, contentY);
|
|
142
|
+
this.renderTextWithSelection(textContext, dl, text, view, contentXStart, contentY);
|
|
98
143
|
}
|
|
99
144
|
else {
|
|
100
145
|
textContext.drawText(contentXStart - this.scrollX, contentY, text, { fg, bg });
|
|
@@ -109,9 +154,11 @@ export class InputElement extends TUIElement {
|
|
|
109
154
|
/* v8 ignore stop */
|
|
110
155
|
}
|
|
111
156
|
}
|
|
112
|
-
renderTextWithSelection(context, dl, text, contentXStart, contentY) {
|
|
113
|
-
|
|
114
|
-
|
|
157
|
+
renderTextWithSelection(context, dl, text, view, contentXStart, contentY) {
|
|
158
|
+
// Границы выделения берём из представления: под маской они смещения в
|
|
159
|
+
// ней, а `text`/`dl` — тоже она.
|
|
160
|
+
const selStart = view.selectionStart;
|
|
161
|
+
const selEnd = view.selectionEnd;
|
|
115
162
|
const before = text.slice(0, selStart);
|
|
116
163
|
const selected = text.slice(selStart, selEnd);
|
|
117
164
|
const after = text.slice(selEnd);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuidom/elements",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Experimental terminal UI DOM — widgets: lists, trees, menus, inputs, quick pick. API is unstable.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
"./*": "./dist/*.js"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@tuidom/core": "0.2.
|
|
31
|
+
"@tuidom/core": "0.2.3"
|
|
32
32
|
}
|
|
33
33
|
}
|