@tuidom/elements 0.1.0
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/dist/body/bodyElement.d.ts +33 -0
- package/dist/body/bodyElement.js +87 -0
- package/dist/button/buttonElement.d.ts +18 -0
- package/dist/button/buttonElement.js +68 -0
- package/dist/completionlist/completionDetailsElement.d.ts +49 -0
- package/dist/completionlist/completionDetailsElement.js +165 -0
- package/dist/completionlist/completionItemKindIcon.d.ts +2 -0
- package/dist/completionlist/completionItemKindIcon.js +43 -0
- package/dist/completionlist/completionListElement.d.ts +102 -0
- package/dist/completionlist/completionListElement.js +309 -0
- package/dist/completionlist/completionWidgetElement.d.ts +41 -0
- package/dist/completionlist/completionWidgetElement.js +102 -0
- package/dist/contextview/contextMenuController.d.ts +23 -0
- package/dist/contextview/contextMenuController.js +75 -0
- package/dist/contextview/overlayHostElement.d.ts +23 -0
- package/dist/contextview/overlayHostElement.js +47 -0
- package/dist/editorgroup/editorTabItemElement.d.ts +41 -0
- package/dist/editorgroup/editorTabItemElement.js +186 -0
- package/dist/editorgroup/editorTabStripElement.d.ts +33 -0
- package/dist/editorgroup/editorTabStripElement.js +118 -0
- package/dist/editorpart/editorPartElement.d.ts +95 -0
- package/dist/editorpart/editorPartElement.js +285 -0
- package/dist/inputbox/inputElement.d.ts +34 -0
- package/dist/inputbox/inputElement.js +207 -0
- package/dist/inputbox/inputState.d.ts +69 -0
- package/dist/inputbox/inputState.js +326 -0
- package/dist/layout/boxContainerElement.d.ts +27 -0
- package/dist/layout/boxContainerElement.js +121 -0
- package/dist/layout/boxElement.d.ts +4 -0
- package/dist/layout/boxElement.js +30 -0
- package/dist/layout/fillerElement.d.ts +12 -0
- package/dist/layout/fillerElement.js +16 -0
- package/dist/layout/fitContentElement.d.ts +18 -0
- package/dist/layout/fitContentElement.js +46 -0
- package/dist/layout/hFlexElement.d.ts +26 -0
- package/dist/layout/hFlexElement.js +134 -0
- package/dist/layout/paddingContainerElement.d.ts +30 -0
- package/dist/layout/paddingContainerElement.js +88 -0
- package/dist/layout/sizedBoxElement.d.ts +30 -0
- package/dist/layout/sizedBoxElement.js +74 -0
- package/dist/layout/vFlexElement.d.ts +32 -0
- package/dist/layout/vFlexElement.js +136 -0
- package/dist/layout/vStackElement.d.ts +18 -0
- package/dist/layout/vStackElement.js +74 -0
- package/dist/list/listViewElement.d.ts +232 -0
- package/dist/list/listViewElement.js +944 -0
- package/dist/menu/menuBarElement.d.ts +47 -0
- package/dist/menu/menuBarElement.js +285 -0
- package/dist/menu/menuBarItemElement.d.ts +20 -0
- package/dist/menu/menuBarItemElement.js +68 -0
- package/dist/menu/popupMenuElement.d.ts +81 -0
- package/dist/menu/popupMenuElement.js +318 -0
- package/dist/menu/popupMenuItemElement.d.ts +24 -0
- package/dist/menu/popupMenuItemElement.js +89 -0
- package/dist/panel/panelContainerElement.d.ts +64 -0
- package/dist/panel/panelContainerElement.js +219 -0
- package/dist/quickpick/quickPickElement.d.ts +161 -0
- package/dist/quickpick/quickPickElement.js +561 -0
- package/dist/sash/sashElement.d.ts +30 -0
- package/dist/sash/sashElement.js +78 -0
- package/dist/scrollbar/iScrollable.d.ts +8 -0
- package/dist/scrollbar/iScrollable.js +1 -0
- package/dist/scrollbar/scrollBarRenderer.d.ts +36 -0
- package/dist/scrollbar/scrollBarRenderer.js +76 -0
- package/dist/scrollbar/scrollContainerElement.d.ts +39 -0
- package/dist/scrollbar/scrollContainerElement.js +91 -0
- package/dist/scrollbar/scrollViewport.d.ts +28 -0
- package/dist/scrollbar/scrollViewport.js +65 -0
- package/dist/scrollbar/scrollableElement.d.ts +22 -0
- package/dist/scrollbar/scrollableElement.js +42 -0
- package/dist/selectbox/selectBoxElement.d.ts +48 -0
- package/dist/selectbox/selectBoxElement.js +161 -0
- package/dist/terminal/encodeKeyForPty.d.ts +3 -0
- package/dist/terminal/encodeKeyForPty.js +56 -0
- package/dist/terminal/terminalViewElement.d.ts +17 -0
- package/dist/terminal/terminalViewElement.js +172 -0
- package/dist/text/textBlockElement.d.ts +13 -0
- package/dist/text/textBlockElement.js +35 -0
- package/dist/text/textLabelElement.d.ts +28 -0
- package/dist/text/textLabelElement.js +77 -0
- package/dist/titledpanel/titledPanelElement.d.ts +14 -0
- package/dist/titledpanel/titledPanelElement.js +40 -0
- package/dist/tree/iTreeDataProvider.d.ts +19 -0
- package/dist/tree/iTreeDataProvider.js +1 -0
- package/dist/tree/treeViewElement.d.ts +116 -0
- package/dist/tree/treeViewElement.js +748 -0
- package/dist/workbenchlayout/workbenchLayoutElement.d.ts +60 -0
- package/dist/workbenchlayout/workbenchLayoutElement.js +203 -0
- package/package.json +33 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
+
import type { TUIEventBase } from "@tuidom/core/dom/events/tuiEventBase";
|
|
3
|
+
import { TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
4
|
+
import type { MenuEntry } from "./popupMenuElement.js";
|
|
5
|
+
export interface MenuBarItem {
|
|
6
|
+
label: string;
|
|
7
|
+
mnemonic?: string;
|
|
8
|
+
entries: MenuEntry[];
|
|
9
|
+
}
|
|
10
|
+
export declare class MenuBarElement extends TUIElement {
|
|
11
|
+
readonly items: readonly MenuBarItem[];
|
|
12
|
+
activeIndex: number;
|
|
13
|
+
private activeMenu;
|
|
14
|
+
private activeMenuSession;
|
|
15
|
+
private itemElements;
|
|
16
|
+
private hflex;
|
|
17
|
+
private previousFocusedElement;
|
|
18
|
+
private readonly mnemonicHandler;
|
|
19
|
+
private mnemonicTarget;
|
|
20
|
+
private updateItemActiveStates;
|
|
21
|
+
constructor(items: MenuBarItem[]);
|
|
22
|
+
/**
|
|
23
|
+
* Мнемоники (Alt+F и т.п.) слушаются на КОРНЕ дерева: keydown без фокуса
|
|
24
|
+
* диспатчится прямо в корень и до промежуточных контейнеров не всплывает,
|
|
25
|
+
* а открывать меню он должен всё равно. Подписка живёт строго на
|
|
26
|
+
* подключённом дереве (Н4): в неукоренённом поддереве мнемоники не
|
|
27
|
+
* работают — раньше fallback вешал слушатель на промежуточного родителя,
|
|
28
|
+
* и тот никогда не переезжал на появившийся позже корень.
|
|
29
|
+
*/
|
|
30
|
+
protected onDidConnect(root: TUIElement): void;
|
|
31
|
+
protected onDidDisconnect(): void;
|
|
32
|
+
get isMenuOpen(): boolean;
|
|
33
|
+
getMinIntrinsicWidth(height: number): number;
|
|
34
|
+
getMaxIntrinsicWidth(height: number): number;
|
|
35
|
+
getMinIntrinsicHeight(_width: number): number;
|
|
36
|
+
getMaxIntrinsicHeight(_width: number): number;
|
|
37
|
+
protected performLayout(constraints: BoxConstraints): Size;
|
|
38
|
+
protected performDefaultAction(event: TUIEventBase): void;
|
|
39
|
+
private handleKeydownDefault;
|
|
40
|
+
private openMenu;
|
|
41
|
+
private closePopup;
|
|
42
|
+
private deactivate;
|
|
43
|
+
private forwardToPopup;
|
|
44
|
+
private wrapIndex;
|
|
45
|
+
private findMnemonicMatch;
|
|
46
|
+
private getMenuGlobalPosition;
|
|
47
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { BoxConstraints, Offset, Point, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
+
import { TUIKeyboardEvent } from "@tuidom/core/dom/events/tuiKeyboardEvent";
|
|
3
|
+
import { TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
4
|
+
import { HFlexElement, hflexFill, hflexFit, hflexFixed } from "../layout/hFlexElement.js";
|
|
5
|
+
import { MenuBarFillerElement, MenuBarItemElement } from "./menuBarItemElement.js";
|
|
6
|
+
import { PopupMenuElement } from "./popupMenuElement.js";
|
|
7
|
+
export class MenuBarElement extends TUIElement {
|
|
8
|
+
items;
|
|
9
|
+
activeIndex = -1;
|
|
10
|
+
activeMenu = null;
|
|
11
|
+
activeMenuSession = null;
|
|
12
|
+
itemElements = [];
|
|
13
|
+
hflex;
|
|
14
|
+
previousFocusedElement = null;
|
|
15
|
+
mnemonicHandler = (event) => {
|
|
16
|
+
const match = this.findMnemonicMatch(event);
|
|
17
|
+
if (match >= 0) {
|
|
18
|
+
this.focus();
|
|
19
|
+
this.openMenu(match);
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
mnemonicTarget = null;
|
|
24
|
+
updateItemActiveStates() {
|
|
25
|
+
for (let i = 0; i < this.itemElements.length; i++) {
|
|
26
|
+
this.itemElements[i].active = i === this.activeIndex && this.isFocused;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
constructor(items) {
|
|
30
|
+
super();
|
|
31
|
+
// Полоса красится токенами; пункты наследуют (активный — menubar.selection*).
|
|
32
|
+
this.style = { fg: "menuBar.foreground", bg: "menuBar.background" };
|
|
33
|
+
this.focusable = true;
|
|
34
|
+
this.items = items;
|
|
35
|
+
this.hflex = new HFlexElement();
|
|
36
|
+
this.hflex.addChild(new MenuBarFillerElement(), { width: hflexFixed(2), height: 1 });
|
|
37
|
+
this.itemElements = items.map((item, index) => {
|
|
38
|
+
const el = new MenuBarItemElement(item.label, item.mnemonic);
|
|
39
|
+
el.onActivate = () => {
|
|
40
|
+
if (this.activeMenu && this.activeIndex === index) {
|
|
41
|
+
this.closePopup();
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
this.focus();
|
|
45
|
+
this.openMenu(index);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
// While a menu is open, hovering another top-level item switches to it (VS Code behavior).
|
|
49
|
+
el.onHover = () => {
|
|
50
|
+
if (this.activeMenu && this.activeIndex !== index) {
|
|
51
|
+
this.openMenu(index);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
this.hflex.addChild(el, { width: hflexFit(), height: 1 });
|
|
55
|
+
return el;
|
|
56
|
+
});
|
|
57
|
+
this.hflex.addChild(new MenuBarFillerElement(), { width: hflexFill(), height: 1 });
|
|
58
|
+
this.appendChild(this.hflex);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Мнемоники (Alt+F и т.п.) слушаются на КОРНЕ дерева: keydown без фокуса
|
|
62
|
+
* диспатчится прямо в корень и до промежуточных контейнеров не всплывает,
|
|
63
|
+
* а открывать меню он должен всё равно. Подписка живёт строго на
|
|
64
|
+
* подключённом дереве (Н4): в неукоренённом поддереве мнемоники не
|
|
65
|
+
* работают — раньше fallback вешал слушатель на промежуточного родителя,
|
|
66
|
+
* и тот никогда не переезжал на появившийся позже корень.
|
|
67
|
+
*/
|
|
68
|
+
onDidConnect(root) {
|
|
69
|
+
this.mnemonicTarget = root;
|
|
70
|
+
root.addEventListener("keydown", this.mnemonicHandler);
|
|
71
|
+
}
|
|
72
|
+
onDidDisconnect() {
|
|
73
|
+
this.mnemonicTarget?.removeEventListener("keydown", this.mnemonicHandler);
|
|
74
|
+
this.mnemonicTarget = null;
|
|
75
|
+
}
|
|
76
|
+
get isMenuOpen() {
|
|
77
|
+
return this.activeMenu !== null;
|
|
78
|
+
}
|
|
79
|
+
getMinIntrinsicWidth(height) {
|
|
80
|
+
return this.hflex.getMinIntrinsicWidth(height);
|
|
81
|
+
}
|
|
82
|
+
getMaxIntrinsicWidth(height) {
|
|
83
|
+
return this.hflex.getMaxIntrinsicWidth(height);
|
|
84
|
+
}
|
|
85
|
+
getMinIntrinsicHeight(_width) {
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
getMaxIntrinsicHeight(_width) {
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
performLayout(constraints) {
|
|
92
|
+
const containerSize = super.performLayout(constraints);
|
|
93
|
+
this.layoutChild(this.hflex, 0, 0, BoxConstraints.tight(new Size(containerSize.width, 1)));
|
|
94
|
+
return containerSize;
|
|
95
|
+
}
|
|
96
|
+
performDefaultAction(event) {
|
|
97
|
+
if (event.type === "focus") {
|
|
98
|
+
this.previousFocusedElement = event.relatedTarget;
|
|
99
|
+
if (this.activeIndex < 0) {
|
|
100
|
+
this.activeIndex = 0;
|
|
101
|
+
}
|
|
102
|
+
this.updateItemActiveStates();
|
|
103
|
+
this.markDirty();
|
|
104
|
+
}
|
|
105
|
+
else if (event.type === "blur") {
|
|
106
|
+
this.deactivate();
|
|
107
|
+
this.previousFocusedElement = null;
|
|
108
|
+
}
|
|
109
|
+
else if (event.type === "keydown") {
|
|
110
|
+
this.handleKeydownDefault(event);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
handleKeydownDefault(event) {
|
|
114
|
+
if (this.activeIndex < 0) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (event.key === "Escape") {
|
|
118
|
+
if (this.activeMenu) {
|
|
119
|
+
this.closePopup();
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const prev = this.previousFocusedElement;
|
|
123
|
+
this.deactivate();
|
|
124
|
+
if (prev) {
|
|
125
|
+
prev.focus();
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
this.blur();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (event.key === "ArrowLeft") {
|
|
134
|
+
const next = this.wrapIndex(this.activeIndex - 1);
|
|
135
|
+
if (this.activeMenu) {
|
|
136
|
+
this.openMenu(next);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
this.activeIndex = next;
|
|
140
|
+
this.updateItemActiveStates();
|
|
141
|
+
this.markDirty();
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (event.key === "ArrowRight") {
|
|
146
|
+
const next = this.wrapIndex(this.activeIndex + 1);
|
|
147
|
+
if (this.activeMenu) {
|
|
148
|
+
this.openMenu(next);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
this.activeIndex = next;
|
|
152
|
+
this.updateItemActiveStates();
|
|
153
|
+
this.markDirty();
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (event.key === "ArrowDown" || event.key === "Enter") {
|
|
158
|
+
if (this.activeMenu) {
|
|
159
|
+
this.forwardToPopup(event);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
this.openMenu(this.activeIndex);
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (event.key === "ArrowUp") {
|
|
167
|
+
if (this.activeMenu) {
|
|
168
|
+
this.forwardToPopup(event);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (this.activeMenu) {
|
|
173
|
+
this.forwardToPopup(event);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
openMenu(index) {
|
|
178
|
+
if (index < 0 || index >= this.items.length) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.closePopup();
|
|
182
|
+
const wrappedEntries = this.items[index].entries.map((entry) => {
|
|
183
|
+
if (entry.type === "separator" || entry.type === "submenu") {
|
|
184
|
+
return entry;
|
|
185
|
+
}
|
|
186
|
+
const originalOnSelect = entry.onSelect;
|
|
187
|
+
return {
|
|
188
|
+
...entry,
|
|
189
|
+
onSelect: () => {
|
|
190
|
+
originalOnSelect?.();
|
|
191
|
+
this.deactivate();
|
|
192
|
+
this.blur();
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
this.activeIndex = index;
|
|
197
|
+
this.updateItemActiveStates();
|
|
198
|
+
const menu = new PopupMenuElement(wrappedEntries);
|
|
199
|
+
this.activeMenu = menu;
|
|
200
|
+
const layer = this.getOverlayLayer();
|
|
201
|
+
/* v8 ignore start -- defensive: меню-бар всегда прикреплён к BodyElement со слоем */
|
|
202
|
+
if (!layer)
|
|
203
|
+
return;
|
|
204
|
+
/* v8 ignore stop */
|
|
205
|
+
const position = this.getMenuGlobalPosition(index);
|
|
206
|
+
let session = null;
|
|
207
|
+
session = layer.createSession(menu, position, {
|
|
208
|
+
visible: true,
|
|
209
|
+
// Меню-бар сам управляет закрытием дропдауна: клик снаружи уводит фокус →
|
|
210
|
+
// blur → deactivate, а повторный клик по тому же пункту тоглит его. Перехват
|
|
211
|
+
// в OverlayLayer (close-on-outside) гонялся бы с этим тоглом, поэтому passthrough.
|
|
212
|
+
pointerPolicy: "passthrough",
|
|
213
|
+
// ...но клавиатурой дропдаун владеет (стрелки/Enter/Escape/мнемоники), поэтому
|
|
214
|
+
// глобальные кейбинды при открытом меню гасим явным опт-ином.
|
|
215
|
+
capturesKeyboard: true,
|
|
216
|
+
disposeOnClose: true,
|
|
217
|
+
onClose: () => {
|
|
218
|
+
/* v8 ignore start -- guards against a stale onClose after the menu was switched; switching disposes the old session without firing onClose, so the mismatch (false) side is unreachable via the public API */
|
|
219
|
+
if (this.activeMenuSession === session) {
|
|
220
|
+
this.activeMenuSession = null;
|
|
221
|
+
}
|
|
222
|
+
if (this.activeMenu === menu) {
|
|
223
|
+
this.activeMenu = null;
|
|
224
|
+
}
|
|
225
|
+
/* v8 ignore stop */
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
menu.onClose = () => {
|
|
229
|
+
session.close();
|
|
230
|
+
};
|
|
231
|
+
this.activeMenuSession = session;
|
|
232
|
+
this.markDirty();
|
|
233
|
+
}
|
|
234
|
+
closePopup() {
|
|
235
|
+
this.activeMenuSession?.dispose();
|
|
236
|
+
this.activeMenuSession = null;
|
|
237
|
+
this.activeMenu = null;
|
|
238
|
+
this.markDirty();
|
|
239
|
+
}
|
|
240
|
+
deactivate() {
|
|
241
|
+
this.closePopup();
|
|
242
|
+
this.activeIndex = -1;
|
|
243
|
+
this.updateItemActiveStates();
|
|
244
|
+
this.markDirty();
|
|
245
|
+
}
|
|
246
|
+
forwardToPopup(event) {
|
|
247
|
+
this.activeMenu?.dispatchEvent(new TUIKeyboardEvent("keydown", {
|
|
248
|
+
key: event.key,
|
|
249
|
+
code: event.code,
|
|
250
|
+
ctrlKey: event.ctrlKey,
|
|
251
|
+
shiftKey: event.shiftKey,
|
|
252
|
+
altKey: event.altKey,
|
|
253
|
+
metaKey: event.metaKey,
|
|
254
|
+
raw: event.raw,
|
|
255
|
+
bubbles: false,
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
wrapIndex(index) {
|
|
259
|
+
if (this.items.length === 0) {
|
|
260
|
+
return -1;
|
|
261
|
+
}
|
|
262
|
+
if (index < 0) {
|
|
263
|
+
return this.items.length - 1;
|
|
264
|
+
}
|
|
265
|
+
if (index >= this.items.length) {
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
return index;
|
|
269
|
+
}
|
|
270
|
+
findMnemonicMatch(event) {
|
|
271
|
+
if (!event.altKey || event.ctrlKey || event.metaKey || event.key.length !== 1) {
|
|
272
|
+
return -1;
|
|
273
|
+
}
|
|
274
|
+
const normalizedKey = event.key.toLowerCase();
|
|
275
|
+
return this.items.findIndex((item) => {
|
|
276
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
277
|
+
const mnemonic = (item.mnemonic ?? item.label[0] ?? "").toLowerCase();
|
|
278
|
+
return mnemonic === normalizedKey;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
getMenuGlobalPosition(index) {
|
|
282
|
+
const el = this.itemElements[index];
|
|
283
|
+
return new Point(this.globalPosition.x + el.localPosition.dx, this.globalPosition.y + 1);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CompositeElement } from "@tuidom/core/dom/compositeElement";
|
|
2
|
+
import { TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
3
|
+
export declare class MenuBarItemElement extends CompositeElement {
|
|
4
|
+
readonly label: string;
|
|
5
|
+
readonly mnemonic: string | undefined;
|
|
6
|
+
onActivate: (() => void) | null;
|
|
7
|
+
/** Fired when the mouse moves over this item — used to switch the open menu on hover. */
|
|
8
|
+
onHover: (() => void) | null;
|
|
9
|
+
constructor(label: string, mnemonic?: string);
|
|
10
|
+
get active(): boolean;
|
|
11
|
+
set active(value: boolean);
|
|
12
|
+
private getMnemonicIndex;
|
|
13
|
+
}
|
|
14
|
+
export declare class MenuBarFillerElement extends TUIElement {
|
|
15
|
+
constructor();
|
|
16
|
+
getMinIntrinsicWidth(_height: number): number;
|
|
17
|
+
getMaxIntrinsicWidth(_height: number): number;
|
|
18
|
+
getMinIntrinsicHeight(_width: number): number;
|
|
19
|
+
getMaxIntrinsicHeight(_width: number): number;
|
|
20
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { StyleFlags } from "@tuidom/core/common/styleFlags";
|
|
2
|
+
import { CompositeElement } from "@tuidom/core/dom/compositeElement";
|
|
3
|
+
import { INHERITED_BG, INHERITED_FG } from "@tuidom/core/dom/styles/tuiStyle";
|
|
4
|
+
import { TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
5
|
+
import { TextLabelElement } from "../text/textLabelElement.js";
|
|
6
|
+
export class MenuBarItemElement extends CompositeElement {
|
|
7
|
+
label;
|
|
8
|
+
mnemonic;
|
|
9
|
+
onActivate = null;
|
|
10
|
+
/** Fired when the mouse moves over this item — used to switch the open menu on hover. */
|
|
11
|
+
onHover = null;
|
|
12
|
+
constructor(label, mnemonic) {
|
|
13
|
+
super();
|
|
14
|
+
this.label = label;
|
|
15
|
+
this.mnemonic = mnemonic;
|
|
16
|
+
this.addEventListener("click", (event) => {
|
|
17
|
+
if (event.defaultPrevented)
|
|
18
|
+
return;
|
|
19
|
+
this.onActivate?.();
|
|
20
|
+
});
|
|
21
|
+
this.addEventListener("mousemove", (event) => {
|
|
22
|
+
if (event.defaultPrevented)
|
|
23
|
+
return;
|
|
24
|
+
this.onHover?.();
|
|
25
|
+
});
|
|
26
|
+
// Обычное состояние наследует цвета полосы (их задаёт MenuBarElement
|
|
27
|
+
// токенами menuBar.*), активный пункт — токены menubar.selection*.
|
|
28
|
+
this.style = {
|
|
29
|
+
when: [{ states: ["active"], fg: "menubar.selectionForeground", bg: "menubar.selectionBackground" }],
|
|
30
|
+
};
|
|
31
|
+
const text = new TextLabelElement(` ${this.label} `);
|
|
32
|
+
const mnemonicIndex = this.getMnemonicIndex();
|
|
33
|
+
if (mnemonicIndex >= 0) {
|
|
34
|
+
text.setCharStyle(mnemonicIndex + 1, { style: StyleFlags.Underline });
|
|
35
|
+
}
|
|
36
|
+
this.setRootChild(text);
|
|
37
|
+
}
|
|
38
|
+
get active() {
|
|
39
|
+
return this.hasStyleState("active");
|
|
40
|
+
}
|
|
41
|
+
set active(value) {
|
|
42
|
+
this.setStyleState("active", value);
|
|
43
|
+
}
|
|
44
|
+
getMnemonicIndex() {
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
46
|
+
const mnemonic = (this.mnemonic ?? this.label[0] ?? "").toLowerCase();
|
|
47
|
+
return this.label.toLowerCase().indexOf(mnemonic);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export class MenuBarFillerElement extends TUIElement {
|
|
51
|
+
constructor() {
|
|
52
|
+
super();
|
|
53
|
+
// «Владею фоном, крашу унаследованным» — заливает база.
|
|
54
|
+
this.style = { fg: INHERITED_FG, bg: INHERITED_BG };
|
|
55
|
+
}
|
|
56
|
+
getMinIntrinsicWidth(_height) {
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
getMaxIntrinsicWidth(_height) {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
getMinIntrinsicHeight(_width) {
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
getMaxIntrinsicHeight(_width) {
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { BoxConstraints, Size } from "@tuidom/core/common/geometryPromitives";
|
|
2
|
+
import type { TUIEventBase } from "@tuidom/core/dom/events/tuiEventBase";
|
|
3
|
+
import { RenderContext, TUIElement } from "@tuidom/core/dom/tuiElement";
|
|
4
|
+
export interface MenuItemEntry {
|
|
5
|
+
type?: "item";
|
|
6
|
+
label: string;
|
|
7
|
+
shortcut?: string;
|
|
8
|
+
icon?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Стабильный ключ пункта (у нас — id команды). Попапу не нужен; его читают
|
|
11
|
+
* потребители, которые рисуют тот же пункт не строкой меню, а кнопкой
|
|
12
|
+
* (inline-действия в заголовке view).
|
|
13
|
+
*/
|
|
14
|
+
id?: string;
|
|
15
|
+
onSelect?: () => void;
|
|
16
|
+
}
|
|
17
|
+
export interface MenuSeparatorEntry {
|
|
18
|
+
type: "separator";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Вложенное подменю: строка с индикатором `›`, открывающая дочерний попап.
|
|
22
|
+
* `entries` может быть ленивым — резолвится в момент открытия подменю.
|
|
23
|
+
*/
|
|
24
|
+
export interface MenuSubmenuEntry {
|
|
25
|
+
type: "submenu";
|
|
26
|
+
label: string;
|
|
27
|
+
icon?: string;
|
|
28
|
+
entries: MenuEntry[] | (() => MenuEntry[]);
|
|
29
|
+
}
|
|
30
|
+
export type MenuEntry = MenuItemEntry | MenuSeparatorEntry | MenuSubmenuEntry;
|
|
31
|
+
export declare class PopupMenuElement extends TUIElement {
|
|
32
|
+
readonly entries: MenuEntry[];
|
|
33
|
+
selectedIndex: number;
|
|
34
|
+
onClose?: () => void;
|
|
35
|
+
private selectableIndices;
|
|
36
|
+
private vstack;
|
|
37
|
+
private itemElements;
|
|
38
|
+
private separatorElements;
|
|
39
|
+
/** Открытое дочернее подменю (сессия — в том же overlay-слое). */
|
|
40
|
+
private child;
|
|
41
|
+
private childEntryIndex;
|
|
42
|
+
/** Родитель в цепочке подменю; у корневого меню — null. */
|
|
43
|
+
private parentMenu;
|
|
44
|
+
constructor(entries: MenuEntry[]);
|
|
45
|
+
/** Раздаёт стили меню (свои + всем пунктам и сепараторам). */
|
|
46
|
+
/** Observable state: item labels (separators as null) and the selected index. */
|
|
47
|
+
inspectState(): Record<string, unknown>;
|
|
48
|
+
getMinIntrinsicWidth(_height: number): number;
|
|
49
|
+
getMaxIntrinsicWidth(_height: number): number;
|
|
50
|
+
getMinIntrinsicHeight(_width: number): number;
|
|
51
|
+
getMaxIntrinsicHeight(_width: number): number;
|
|
52
|
+
getIntrinsicSize(): Size;
|
|
53
|
+
protected performLayout(constraints: BoxConstraints): Size;
|
|
54
|
+
render(context: RenderContext): void;
|
|
55
|
+
private computeConfig;
|
|
56
|
+
protected performDefaultAction(event: TUIEventBase): void;
|
|
57
|
+
/**
|
|
58
|
+
* Клавиатура цепочки меню: фокус остаётся на корне, клавиши роутятся в
|
|
59
|
+
* глубочайшее открытое подменю. ArrowLeft/Escape закрывают один уровень
|
|
60
|
+
* (Escape без подменю — всё меню), ArrowRight/Enter открывают подменю.
|
|
61
|
+
*/
|
|
62
|
+
private handleMenuKey;
|
|
63
|
+
/** Есть ли открытое подменю (на любом уровне достаточно первого). */
|
|
64
|
+
hasOpenSubmenu(): boolean;
|
|
65
|
+
/** target внутри цепочки подменю этого меню (для close-on-outside владельца). */
|
|
66
|
+
ownsElement(target: TUIElement): boolean;
|
|
67
|
+
/** Каскадно закрывает всю цепочку открытых подменю. */
|
|
68
|
+
closeSubmenus(): void;
|
|
69
|
+
private closeChild;
|
|
70
|
+
private openSubmenu;
|
|
71
|
+
/** Закрыть всю цепочку: вверх до корня; корень закрывает свою сессию. */
|
|
72
|
+
private closeAll;
|
|
73
|
+
private updateItemSelectedStates;
|
|
74
|
+
/**
|
|
75
|
+
* Moves the selection onto the item at the given entry index. Only called
|
|
76
|
+
* from item hover callbacks, so `index` is always a selectable entry.
|
|
77
|
+
*/
|
|
78
|
+
private selectByEntryIndex;
|
|
79
|
+
private moveSelection;
|
|
80
|
+
private activateSelected;
|
|
81
|
+
}
|