@flighthq/menu 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/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/menu-templates.d.ts +8 -0
- package/dist/menu-templates.d.ts.map +1 -0
- package/dist/menu-templates.js +293 -0
- package/dist/menu-templates.js.map +1 -0
- package/dist/menu.d.ts +13 -0
- package/dist/menu.d.ts.map +1 -0
- package/dist/menu.js +329 -0
- package/dist/menu.js.map +1 -0
- package/package.json +38 -0
- package/src/menu-templates.test.ts +162 -0
- package/src/menu.test.ts +162 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MenuItemTemplate } from '@flighthq/types';
|
|
2
|
+
export declare function createDefaultAppMenuTemplate(appName: string): MenuItemTemplate;
|
|
3
|
+
export declare function createDefaultEditMenuTemplate(): MenuItemTemplate;
|
|
4
|
+
export declare function createDefaultFileMenuTemplate(): MenuItemTemplate;
|
|
5
|
+
export declare function createDefaultHelpMenuTemplate(): MenuItemTemplate;
|
|
6
|
+
export declare function createDefaultViewMenuTemplate(): MenuItemTemplate;
|
|
7
|
+
export declare function createDefaultWindowMenuTemplate(): MenuItemTemplate;
|
|
8
|
+
//# sourceMappingURL=menu-templates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-templates.d.ts","sourceRoot":"","sources":["../src/menu-templates.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAMxD,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,CA0D9E;AAMD,wBAAgB,6BAA6B,IAAI,gBAAgB,CAyEhE;AAID,wBAAgB,6BAA6B,IAAI,gBAAgB,CA+ChE;AAID,wBAAgB,6BAA6B,IAAI,gBAAgB,CAgBhE;AAID,wBAAgB,6BAA6B,IAAI,gBAAgB,CAmDhE;AAID,wBAAgB,+BAA+B,IAAI,gBAAgB,CAyBlE"}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { WellKnownMenuItemRole } from '@flighthq/types';
|
|
2
|
+
// Returns a standard macOS-style application menu (the first menu, labeled with the app name).
|
|
3
|
+
// On Windows/Linux this menu is typically omitted; native backends handle that automatically when
|
|
4
|
+
// the whole-submenu role 'appMenu' is set. Includes About, Services, Hide/Show, and Quit.
|
|
5
|
+
export function createDefaultAppMenuTemplate(appName) {
|
|
6
|
+
return {
|
|
7
|
+
label: appName,
|
|
8
|
+
role: WellKnownMenuItemRole.appMenu,
|
|
9
|
+
type: 'submenu',
|
|
10
|
+
submenu: [
|
|
11
|
+
{
|
|
12
|
+
id: 'about',
|
|
13
|
+
label: `About ${appName}`,
|
|
14
|
+
role: WellKnownMenuItemRole.about,
|
|
15
|
+
type: 'normal',
|
|
16
|
+
enabled: true,
|
|
17
|
+
},
|
|
18
|
+
{ type: 'separator', enabled: true },
|
|
19
|
+
{
|
|
20
|
+
id: 'services',
|
|
21
|
+
label: 'Services',
|
|
22
|
+
role: WellKnownMenuItemRole.services,
|
|
23
|
+
type: 'submenu',
|
|
24
|
+
submenu: [],
|
|
25
|
+
enabled: true,
|
|
26
|
+
},
|
|
27
|
+
{ type: 'separator', enabled: true },
|
|
28
|
+
{
|
|
29
|
+
id: 'hide',
|
|
30
|
+
label: `Hide ${appName}`,
|
|
31
|
+
role: WellKnownMenuItemRole.hide,
|
|
32
|
+
accelerator: 'CmdOrCtrl+H',
|
|
33
|
+
type: 'normal',
|
|
34
|
+
enabled: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'hideOthers',
|
|
38
|
+
label: 'Hide Others',
|
|
39
|
+
role: WellKnownMenuItemRole.hideOthers,
|
|
40
|
+
accelerator: 'CmdOrCtrl+Alt+H',
|
|
41
|
+
type: 'normal',
|
|
42
|
+
enabled: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'unhide',
|
|
46
|
+
label: 'Show All',
|
|
47
|
+
role: WellKnownMenuItemRole.unhide,
|
|
48
|
+
type: 'normal',
|
|
49
|
+
enabled: true,
|
|
50
|
+
},
|
|
51
|
+
{ type: 'separator', enabled: true },
|
|
52
|
+
{
|
|
53
|
+
id: 'quit',
|
|
54
|
+
label: `Quit ${appName}`,
|
|
55
|
+
role: WellKnownMenuItemRole.quit,
|
|
56
|
+
accelerator: 'CmdOrCtrl+Q',
|
|
57
|
+
type: 'normal',
|
|
58
|
+
enabled: true,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
enabled: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Returns a standard Edit menu template with common text-editing operations. The whole-submenu
|
|
65
|
+
// role 'editMenu' is set so native hosts (Electron/Tauri) can substitute their own platform-native
|
|
66
|
+
// Edit menu where that is more appropriate. Items are also individually role-tagged for finer
|
|
67
|
+
// native control.
|
|
68
|
+
export function createDefaultEditMenuTemplate() {
|
|
69
|
+
return {
|
|
70
|
+
label: 'Edit',
|
|
71
|
+
role: WellKnownMenuItemRole.editMenu,
|
|
72
|
+
type: 'submenu',
|
|
73
|
+
submenu: [
|
|
74
|
+
{
|
|
75
|
+
id: 'undo',
|
|
76
|
+
label: 'Undo',
|
|
77
|
+
role: WellKnownMenuItemRole.undo,
|
|
78
|
+
accelerator: 'CmdOrCtrl+Z',
|
|
79
|
+
type: 'normal',
|
|
80
|
+
enabled: true,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'redo',
|
|
84
|
+
label: 'Redo',
|
|
85
|
+
role: WellKnownMenuItemRole.redo,
|
|
86
|
+
accelerator: 'Shift+CmdOrCtrl+Z',
|
|
87
|
+
type: 'normal',
|
|
88
|
+
enabled: true,
|
|
89
|
+
},
|
|
90
|
+
{ type: 'separator', enabled: true },
|
|
91
|
+
{
|
|
92
|
+
id: 'cut',
|
|
93
|
+
label: 'Cut',
|
|
94
|
+
role: WellKnownMenuItemRole.cut,
|
|
95
|
+
accelerator: 'CmdOrCtrl+X',
|
|
96
|
+
type: 'normal',
|
|
97
|
+
enabled: true,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'copy',
|
|
101
|
+
label: 'Copy',
|
|
102
|
+
role: WellKnownMenuItemRole.copy,
|
|
103
|
+
accelerator: 'CmdOrCtrl+C',
|
|
104
|
+
type: 'normal',
|
|
105
|
+
enabled: true,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'paste',
|
|
109
|
+
label: 'Paste',
|
|
110
|
+
role: WellKnownMenuItemRole.paste,
|
|
111
|
+
accelerator: 'CmdOrCtrl+V',
|
|
112
|
+
type: 'normal',
|
|
113
|
+
enabled: true,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'pasteAndMatchStyle',
|
|
117
|
+
label: 'Paste and Match Style',
|
|
118
|
+
role: WellKnownMenuItemRole.pasteAndMatchStyle,
|
|
119
|
+
accelerator: 'Shift+CmdOrCtrl+V',
|
|
120
|
+
type: 'normal',
|
|
121
|
+
enabled: true,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'delete',
|
|
125
|
+
label: 'Delete',
|
|
126
|
+
role: WellKnownMenuItemRole.delete,
|
|
127
|
+
type: 'normal',
|
|
128
|
+
enabled: true,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'selectAll',
|
|
132
|
+
label: 'Select All',
|
|
133
|
+
role: WellKnownMenuItemRole.selectAll,
|
|
134
|
+
accelerator: 'CmdOrCtrl+A',
|
|
135
|
+
type: 'normal',
|
|
136
|
+
enabled: true,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
enabled: true,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Returns a standard File menu template. The whole-submenu role 'fileMenu' signals native backends
|
|
143
|
+
// to place the menu in the standard File slot. Includes New, Open, Save, Save As, and Close.
|
|
144
|
+
export function createDefaultFileMenuTemplate() {
|
|
145
|
+
return {
|
|
146
|
+
label: 'File',
|
|
147
|
+
role: WellKnownMenuItemRole.fileMenu,
|
|
148
|
+
type: 'submenu',
|
|
149
|
+
submenu: [
|
|
150
|
+
{
|
|
151
|
+
id: 'newFile',
|
|
152
|
+
label: 'New',
|
|
153
|
+
type: 'normal',
|
|
154
|
+
accelerator: 'CmdOrCtrl+N',
|
|
155
|
+
enabled: true,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: 'openFile',
|
|
159
|
+
label: 'Open…',
|
|
160
|
+
type: 'normal',
|
|
161
|
+
accelerator: 'CmdOrCtrl+O',
|
|
162
|
+
enabled: true,
|
|
163
|
+
},
|
|
164
|
+
{ type: 'separator', enabled: true },
|
|
165
|
+
{
|
|
166
|
+
id: 'saveFile',
|
|
167
|
+
label: 'Save',
|
|
168
|
+
type: 'normal',
|
|
169
|
+
accelerator: 'CmdOrCtrl+S',
|
|
170
|
+
enabled: true,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 'saveFileAs',
|
|
174
|
+
label: 'Save As…',
|
|
175
|
+
type: 'normal',
|
|
176
|
+
accelerator: 'Shift+CmdOrCtrl+S',
|
|
177
|
+
enabled: true,
|
|
178
|
+
},
|
|
179
|
+
{ type: 'separator', enabled: true },
|
|
180
|
+
{
|
|
181
|
+
id: 'closeFile',
|
|
182
|
+
label: 'Close',
|
|
183
|
+
role: WellKnownMenuItemRole.close,
|
|
184
|
+
accelerator: 'CmdOrCtrl+W',
|
|
185
|
+
type: 'normal',
|
|
186
|
+
enabled: true,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
enabled: true,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// Returns a standard Help menu template. The whole-submenu role 'helpMenu' signals native backends
|
|
193
|
+
// to place the menu in the standard Help slot (macOS help-menu convention).
|
|
194
|
+
export function createDefaultHelpMenuTemplate() {
|
|
195
|
+
return {
|
|
196
|
+
label: 'Help',
|
|
197
|
+
role: WellKnownMenuItemRole.helpMenu,
|
|
198
|
+
type: 'submenu',
|
|
199
|
+
submenu: [
|
|
200
|
+
{
|
|
201
|
+
id: 'help',
|
|
202
|
+
label: 'Search',
|
|
203
|
+
role: WellKnownMenuItemRole.help,
|
|
204
|
+
type: 'normal',
|
|
205
|
+
enabled: true,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
enabled: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// Returns a standard View menu template covering reload and zoom. The whole-submenu role 'viewMenu'
|
|
212
|
+
// signals native backends to substitute a platform-native View menu where appropriate.
|
|
213
|
+
export function createDefaultViewMenuTemplate() {
|
|
214
|
+
return {
|
|
215
|
+
label: 'View',
|
|
216
|
+
role: WellKnownMenuItemRole.viewMenu,
|
|
217
|
+
type: 'submenu',
|
|
218
|
+
submenu: [
|
|
219
|
+
{
|
|
220
|
+
id: 'reload',
|
|
221
|
+
label: 'Reload',
|
|
222
|
+
role: WellKnownMenuItemRole.reload,
|
|
223
|
+
accelerator: 'CmdOrCtrl+R',
|
|
224
|
+
type: 'normal',
|
|
225
|
+
enabled: true,
|
|
226
|
+
},
|
|
227
|
+
{ type: 'separator', enabled: true },
|
|
228
|
+
{
|
|
229
|
+
id: 'resetZoom',
|
|
230
|
+
label: 'Actual Size',
|
|
231
|
+
role: WellKnownMenuItemRole.resetZoom,
|
|
232
|
+
accelerator: 'CmdOrCtrl+0',
|
|
233
|
+
type: 'normal',
|
|
234
|
+
enabled: true,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: 'zoomIn',
|
|
238
|
+
label: 'Zoom In',
|
|
239
|
+
role: WellKnownMenuItemRole.zoomIn,
|
|
240
|
+
accelerator: 'CmdOrCtrl+Plus',
|
|
241
|
+
type: 'normal',
|
|
242
|
+
enabled: true,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: 'zoomOut',
|
|
246
|
+
label: 'Zoom Out',
|
|
247
|
+
role: WellKnownMenuItemRole.zoomOut,
|
|
248
|
+
accelerator: 'CmdOrCtrl+-',
|
|
249
|
+
type: 'normal',
|
|
250
|
+
enabled: true,
|
|
251
|
+
},
|
|
252
|
+
{ type: 'separator', enabled: true },
|
|
253
|
+
{
|
|
254
|
+
id: 'toggleFullscreen',
|
|
255
|
+
label: 'Toggle Full Screen',
|
|
256
|
+
role: WellKnownMenuItemRole.toggleFullscreen,
|
|
257
|
+
accelerator: 'F11',
|
|
258
|
+
type: 'normal',
|
|
259
|
+
enabled: true,
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
enabled: true,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
// Returns a standard Window menu template. The whole-submenu role 'windowMenu' signals native
|
|
266
|
+
// backends (notably macOS) to manage the Window menu list automatically.
|
|
267
|
+
export function createDefaultWindowMenuTemplate() {
|
|
268
|
+
return {
|
|
269
|
+
label: 'Window',
|
|
270
|
+
role: WellKnownMenuItemRole.windowMenu,
|
|
271
|
+
type: 'submenu',
|
|
272
|
+
submenu: [
|
|
273
|
+
{
|
|
274
|
+
id: 'minimize',
|
|
275
|
+
label: 'Minimize',
|
|
276
|
+
role: WellKnownMenuItemRole.minimize,
|
|
277
|
+
accelerator: 'CmdOrCtrl+M',
|
|
278
|
+
type: 'normal',
|
|
279
|
+
enabled: true,
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: 'close',
|
|
283
|
+
label: 'Close',
|
|
284
|
+
role: WellKnownMenuItemRole.close,
|
|
285
|
+
accelerator: 'CmdOrCtrl+W',
|
|
286
|
+
type: 'normal',
|
|
287
|
+
enabled: true,
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
enabled: true,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=menu-templates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu-templates.js","sourceRoot":"","sources":["../src/menu-templates.ts"],"names":[],"mappings":"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"}
|
package/dist/menu.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MenuBackend, MenuItemTemplate, MenuSignals } from '@flighthq/types';
|
|
2
|
+
export declare function cloneMenuTemplate(template: Readonly<MenuItemTemplate>): MenuItemTemplate;
|
|
3
|
+
export declare function createMenuItemTemplate(template?: Readonly<Partial<MenuItemTemplate>>): MenuItemTemplate;
|
|
4
|
+
export declare function createWebMenuBackend(): MenuBackend;
|
|
5
|
+
export declare function enableMenuSignals(): MenuSignals;
|
|
6
|
+
export declare function getMenuBackend(): MenuBackend;
|
|
7
|
+
export declare function getMenuSignals(): Readonly<MenuSignals> | null;
|
|
8
|
+
export declare function onMenuSelect(listener: (id: string) => void): () => void;
|
|
9
|
+
export declare function setApplicationMenu(items: readonly MenuItemTemplate[]): boolean;
|
|
10
|
+
export declare function setMenuBackend(backend: MenuBackend | null): void;
|
|
11
|
+
export declare function showContextMenu(items: readonly MenuItemTemplate[], x: number, y: number): Promise<string | null>;
|
|
12
|
+
export declare function validateMenuItemTemplate(template: Readonly<MenuItemTemplate>): string | null;
|
|
13
|
+
//# sourceMappingURL=menu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../src/menu.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAIlF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GAAG,gBAAgB,CAMxF;AAKD,wBAAgB,sBAAsB,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,GAAG,gBAAgB,CAUvG;AAKD,wBAAgB,oBAAoB,IAAI,WAAW,CAclD;AAKD,wBAAgB,iBAAiB,IAAI,WAAW,CAU/C;AAGD,wBAAgB,cAAc,IAAI,WAAW,CAG5C;AAGD,wBAAgB,cAAc,IAAI,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,CAE7D;AAKD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAKvE;AAID,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,GAAG,OAAO,CAE9E;AAGD,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAEhE;AAKD,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAQhH;AAKD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,CAAC,gBAAgB,CAAC,GAAG,MAAM,GAAG,IAAI,CAE5F"}
|
package/dist/menu.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { createSignal, emitSignal } from '@flighthq/signals';
|
|
2
|
+
// Deep-clones a MenuItemTemplate tree. The returned tree has the same shape and values. Safe to call
|
|
3
|
+
// with a template carrying a submenu — children are cloned recursively.
|
|
4
|
+
export function cloneMenuTemplate(template) {
|
|
5
|
+
const clone = { ...template };
|
|
6
|
+
if (template.submenu !== undefined) {
|
|
7
|
+
clone.submenu = template.submenu.map(cloneMenuTemplate);
|
|
8
|
+
}
|
|
9
|
+
return clone;
|
|
10
|
+
}
|
|
11
|
+
// Builds a menu item template, filling defaults (type 'normal', enabled true). Recursively normalizes
|
|
12
|
+
// any submenu children through the same default-fill, so every item in the tree has canonical defaults
|
|
13
|
+
// regardless of nesting depth.
|
|
14
|
+
export function createMenuItemTemplate(template) {
|
|
15
|
+
const item = {
|
|
16
|
+
type: 'normal',
|
|
17
|
+
enabled: true,
|
|
18
|
+
...template,
|
|
19
|
+
};
|
|
20
|
+
if (item.submenu !== undefined) {
|
|
21
|
+
item.submenu = item.submenu.map((child) => createMenuItemTemplate(child));
|
|
22
|
+
}
|
|
23
|
+
return item;
|
|
24
|
+
}
|
|
25
|
+
// Builds the default web backend. The app-menu bar returns false (native host required for a true menu
|
|
26
|
+
// bar). popupContextMenu delegates to the DOM-rendered context-menu popup. subscribeSelect never fires
|
|
27
|
+
// on web (no native app menu).
|
|
28
|
+
export function createWebMenuBackend() {
|
|
29
|
+
return {
|
|
30
|
+
setApplicationMenu() {
|
|
31
|
+
// Web has no native menu bar; return false (not an error — caller should handle gracefully).
|
|
32
|
+
return false;
|
|
33
|
+
},
|
|
34
|
+
popupContextMenu(items, x, y) {
|
|
35
|
+
return showWebContextMenu(items, x, y);
|
|
36
|
+
},
|
|
37
|
+
subscribeSelect() {
|
|
38
|
+
// Web app-menu has no select source — native host required.
|
|
39
|
+
return () => { };
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Activates the optional MenuSignals group and returns it. Calling this is when the cost is assumed.
|
|
44
|
+
// The returned object is shared for the lifetime of the package; calling enableMenuSignals multiple
|
|
45
|
+
// times returns the same instance. Connect slots via connectSignal from @flighthq/signals.
|
|
46
|
+
export function enableMenuSignals() {
|
|
47
|
+
if (_menuSignals === null) {
|
|
48
|
+
_menuSignals = {
|
|
49
|
+
onContextMenuClose: createSignal(),
|
|
50
|
+
onContextMenuOpen: createSignal(),
|
|
51
|
+
onMenuItemHighlight: createSignal(),
|
|
52
|
+
onMenuItemSelect: createSignal(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return _menuSignals;
|
|
56
|
+
}
|
|
57
|
+
// Returns the active backend; lazily creates a web default on first call.
|
|
58
|
+
export function getMenuBackend() {
|
|
59
|
+
if (_backend === null)
|
|
60
|
+
_backend = createWebMenuBackend();
|
|
61
|
+
return _backend;
|
|
62
|
+
}
|
|
63
|
+
// Returns the active MenuSignals group, or null if enableMenuSignals has not been called.
|
|
64
|
+
export function getMenuSignals() {
|
|
65
|
+
return _menuSignals;
|
|
66
|
+
}
|
|
67
|
+
// Subscribes to application menu item selections by item id. Returns an unsubscribe function. On web
|
|
68
|
+
// this never fires (no native app menu bar). Selections are also fanned out to onMenuItemSelect when
|
|
69
|
+
// the MenuSignals group is enabled.
|
|
70
|
+
export function onMenuSelect(listener) {
|
|
71
|
+
return getMenuBackend().subscribeSelect((id) => {
|
|
72
|
+
listener(id);
|
|
73
|
+
if (_menuSignals !== null)
|
|
74
|
+
emitSignal(_menuSignals.onMenuItemSelect, id);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Installs the application menu bar. Returns true on success, or false when the host lacks a native
|
|
78
|
+
// menu bar (e.g. web).
|
|
79
|
+
export function setApplicationMenu(items) {
|
|
80
|
+
return getMenuBackend().setApplicationMenu(items);
|
|
81
|
+
}
|
|
82
|
+
// Sets the active menu backend; pass null to fall back to the web default.
|
|
83
|
+
export function setMenuBackend(backend) {
|
|
84
|
+
_backend = backend;
|
|
85
|
+
}
|
|
86
|
+
// Pops up a context menu at (x, y) and resolves the clicked item id, or null when dismissed. On web,
|
|
87
|
+
// renders a positioned DOM popup (separators, enabled/checked rendering, dismiss on outside-click /
|
|
88
|
+
// Escape). On a native host, delegates to the OS context menu.
|
|
89
|
+
export function showContextMenu(items, x, y) {
|
|
90
|
+
if (_menuSignals !== null)
|
|
91
|
+
emitSignal(_menuSignals.onContextMenuOpen);
|
|
92
|
+
const promise = getMenuBackend().popupContextMenu(items, x, y);
|
|
93
|
+
if (_menuSignals !== null) {
|
|
94
|
+
const signals = _menuSignals;
|
|
95
|
+
void promise.then(() => emitSignal(signals.onContextMenuClose));
|
|
96
|
+
}
|
|
97
|
+
return promise;
|
|
98
|
+
}
|
|
99
|
+
// Validates a MenuItemTemplate tree for consistency. Returns null on success, or a string describing
|
|
100
|
+
// the first violation found. Does not throw — returns a sentinel for expected failures. Throws only
|
|
101
|
+
// for cyclic submenu references (programmer error).
|
|
102
|
+
export function validateMenuItemTemplate(template) {
|
|
103
|
+
return _validateItem(template, new Set());
|
|
104
|
+
}
|
|
105
|
+
let _backend = null;
|
|
106
|
+
let _menuSignals = null;
|
|
107
|
+
function _validateItem(item, seen) {
|
|
108
|
+
if (seen.has(item)) {
|
|
109
|
+
throw new Error('validateMenuItemTemplate: cyclic submenu reference detected');
|
|
110
|
+
}
|
|
111
|
+
if (item.type === 'separator') {
|
|
112
|
+
if (item.label !== undefined && item.label !== '') {
|
|
113
|
+
return `separator item has a label: "${item.label}" (separators should not have labels)`;
|
|
114
|
+
}
|
|
115
|
+
if (item.accelerator !== undefined) {
|
|
116
|
+
return `separator item has an accelerator: "${item.accelerator}"`;
|
|
117
|
+
}
|
|
118
|
+
if (item.submenu !== undefined) {
|
|
119
|
+
return 'separator item has a submenu';
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
if (item.type !== 'submenu' && item.submenu !== undefined && item.submenu.length > 0) {
|
|
124
|
+
return `item type "${item.type ?? 'normal'}" has a submenu (only type "submenu" should carry children)`;
|
|
125
|
+
}
|
|
126
|
+
if (item.submenu !== undefined) {
|
|
127
|
+
seen.add(item);
|
|
128
|
+
for (const child of item.submenu) {
|
|
129
|
+
const err = _validateItem(child, seen);
|
|
130
|
+
if (err !== null)
|
|
131
|
+
return err;
|
|
132
|
+
}
|
|
133
|
+
seen.delete(item);
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
// --- Web context-menu renderer ---
|
|
138
|
+
// Renders a minimal DOM popup for showContextMenu in a browser environment. Returns the clicked item
|
|
139
|
+
// id or null when the menu is dismissed without a selection. Supports keyboard navigation (ArrowUp/
|
|
140
|
+
// Down, Enter, Escape) and submenu expansion on hover/arrow-right.
|
|
141
|
+
function showWebContextMenu(items, x, y) {
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
if (typeof document === 'undefined') {
|
|
144
|
+
resolve(null);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const overlay = document.createElement('div');
|
|
148
|
+
overlay.style.cssText = 'position:fixed;inset:0;z-index:2147483646;';
|
|
149
|
+
const menu = buildWebMenuElement(items, (id) => close(id));
|
|
150
|
+
function clampMenu(el, anchorX, anchorY) {
|
|
151
|
+
el.style.left = `${anchorX}px`;
|
|
152
|
+
el.style.top = `${anchorY}px`;
|
|
153
|
+
const rect = el.getBoundingClientRect();
|
|
154
|
+
const vw = typeof window !== 'undefined' ? window.innerWidth : 0;
|
|
155
|
+
const vh = typeof window !== 'undefined' ? window.innerHeight : 0;
|
|
156
|
+
if (rect.right > vw)
|
|
157
|
+
el.style.left = `${Math.max(0, anchorX - rect.width)}px`;
|
|
158
|
+
if (rect.bottom > vh)
|
|
159
|
+
el.style.top = `${Math.max(0, anchorY - rect.height)}px`;
|
|
160
|
+
}
|
|
161
|
+
function close(selectedId) {
|
|
162
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
163
|
+
overlay.remove();
|
|
164
|
+
menu.remove();
|
|
165
|
+
resolve(selectedId);
|
|
166
|
+
}
|
|
167
|
+
// Keyboard navigation
|
|
168
|
+
const focusableItems = menu.querySelectorAll('li[data-enabled="true"]');
|
|
169
|
+
let focusIndex = -1;
|
|
170
|
+
function moveFocus(delta) {
|
|
171
|
+
const items = Array.from(focusableItems);
|
|
172
|
+
if (items.length === 0)
|
|
173
|
+
return;
|
|
174
|
+
focusIndex = (focusIndex + delta + items.length) % items.length;
|
|
175
|
+
items.forEach((el, i) => {
|
|
176
|
+
if (i === focusIndex) {
|
|
177
|
+
el.setAttribute('data-focused', 'true');
|
|
178
|
+
el.style.background = '#0066cc';
|
|
179
|
+
el.style.color = '#fff';
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
el.removeAttribute('data-focused');
|
|
183
|
+
el.style.background = '';
|
|
184
|
+
el.style.color = '#111';
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
if (_menuSignals !== null) {
|
|
188
|
+
const focused = items[focusIndex];
|
|
189
|
+
const itemId = focused?.dataset['itemId'];
|
|
190
|
+
if (itemId !== undefined)
|
|
191
|
+
emitSignal(_menuSignals.onMenuItemHighlight, itemId);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function onKeyDown(e) {
|
|
195
|
+
if (e.key === 'Escape') {
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
close(null);
|
|
198
|
+
}
|
|
199
|
+
else if (e.key === 'ArrowDown') {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
moveFocus(1);
|
|
202
|
+
}
|
|
203
|
+
else if (e.key === 'ArrowUp') {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
moveFocus(-1);
|
|
206
|
+
}
|
|
207
|
+
else if (e.key === 'Enter' || e.key === ' ') {
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
const focused = Array.from(focusableItems)[focusIndex];
|
|
210
|
+
if (focused !== undefined) {
|
|
211
|
+
const itemId = focused.dataset['itemId'];
|
|
212
|
+
if (itemId !== undefined)
|
|
213
|
+
close(itemId);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
overlay.addEventListener('click', () => close(null));
|
|
218
|
+
document.addEventListener('keydown', onKeyDown);
|
|
219
|
+
document.body.appendChild(overlay);
|
|
220
|
+
document.body.appendChild(menu);
|
|
221
|
+
clampMenu(menu, x, y);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
function buildWebMenuElement(items, onSelect) {
|
|
225
|
+
const menu = document.createElement('ul');
|
|
226
|
+
menu.style.cssText = [
|
|
227
|
+
'position:fixed',
|
|
228
|
+
'z-index:2147483647',
|
|
229
|
+
'margin:0',
|
|
230
|
+
'padding:4px 0',
|
|
231
|
+
'list-style:none',
|
|
232
|
+
'background:#fff',
|
|
233
|
+
'border:1px solid #ccc',
|
|
234
|
+
'border-radius:4px',
|
|
235
|
+
'box-shadow:0 4px 12px rgba(0,0,0,.15)',
|
|
236
|
+
'min-width:160px',
|
|
237
|
+
'font:13px/1.4 system-ui,sans-serif',
|
|
238
|
+
'color:#111',
|
|
239
|
+
'user-select:none',
|
|
240
|
+
].join(';');
|
|
241
|
+
for (const item of items) {
|
|
242
|
+
const li = document.createElement('li');
|
|
243
|
+
if (item.type === 'separator') {
|
|
244
|
+
li.style.cssText = 'height:1px;margin:4px 8px;background:#e0e0e0;';
|
|
245
|
+
menu.appendChild(li);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const enabled = item.enabled !== false;
|
|
249
|
+
const hasSubmenu = item.submenu !== undefined && item.submenu.length > 0;
|
|
250
|
+
li.setAttribute('data-enabled', enabled ? 'true' : 'false');
|
|
251
|
+
if (item.id !== undefined)
|
|
252
|
+
li.dataset['itemId'] = item.id;
|
|
253
|
+
li.style.cssText = [
|
|
254
|
+
'display:flex',
|
|
255
|
+
'align-items:center',
|
|
256
|
+
'padding:5px 12px 5px 28px',
|
|
257
|
+
'cursor:' + (enabled ? 'default' : 'not-allowed'),
|
|
258
|
+
'color:' + (enabled ? '#111' : '#999'),
|
|
259
|
+
'position:relative',
|
|
260
|
+
].join(';');
|
|
261
|
+
// Checkmark / radio dot
|
|
262
|
+
if (item.checked === true) {
|
|
263
|
+
const mark = document.createElement('span');
|
|
264
|
+
mark.textContent = item.type === 'radio' ? '●' : '✓';
|
|
265
|
+
mark.style.cssText = 'position:absolute;left:8px;font-size:11px;';
|
|
266
|
+
li.appendChild(mark);
|
|
267
|
+
}
|
|
268
|
+
const labelEl = document.createElement('span');
|
|
269
|
+
labelEl.textContent = item.label ?? '';
|
|
270
|
+
labelEl.style.cssText = 'flex:1;';
|
|
271
|
+
li.appendChild(labelEl);
|
|
272
|
+
if (hasSubmenu) {
|
|
273
|
+
const arrow = document.createElement('span');
|
|
274
|
+
arrow.textContent = '▶';
|
|
275
|
+
arrow.style.cssText = 'margin-left:8px;font-size:9px;color:#888;';
|
|
276
|
+
li.appendChild(arrow);
|
|
277
|
+
}
|
|
278
|
+
else if (item.accelerator !== undefined) {
|
|
279
|
+
const accel = document.createElement('span');
|
|
280
|
+
accel.textContent = item.accelerator;
|
|
281
|
+
accel.style.cssText = 'margin-left:24px;color:#888;font-size:11px;';
|
|
282
|
+
li.appendChild(accel);
|
|
283
|
+
}
|
|
284
|
+
if (enabled) {
|
|
285
|
+
li.addEventListener('mouseenter', () => {
|
|
286
|
+
li.style.background = '#0066cc';
|
|
287
|
+
li.style.color = '#fff';
|
|
288
|
+
const accelEl = li.querySelector('span:last-child');
|
|
289
|
+
if (accelEl !== null && accelEl !== labelEl)
|
|
290
|
+
accelEl.style.color = 'rgba(255,255,255,.7)';
|
|
291
|
+
if (_menuSignals !== null && item.id !== undefined) {
|
|
292
|
+
emitSignal(_menuSignals.onMenuItemHighlight, item.id);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
li.addEventListener('mouseleave', () => {
|
|
296
|
+
li.style.background = '';
|
|
297
|
+
li.style.color = '#111';
|
|
298
|
+
const accelEl = li.querySelector('span:last-child');
|
|
299
|
+
if (accelEl !== null && accelEl !== labelEl)
|
|
300
|
+
accelEl.style.color = '#888';
|
|
301
|
+
});
|
|
302
|
+
if (hasSubmenu) {
|
|
303
|
+
// Submenu: open on hover, select from child list.
|
|
304
|
+
const submenuEl = buildWebMenuElement(item.submenu, onSelect);
|
|
305
|
+
submenuEl.style.position = 'absolute';
|
|
306
|
+
submenuEl.style.top = '0';
|
|
307
|
+
submenuEl.style.left = '100%';
|
|
308
|
+
submenuEl.style.display = 'none';
|
|
309
|
+
li.appendChild(submenuEl);
|
|
310
|
+
li.addEventListener('mouseenter', () => {
|
|
311
|
+
submenuEl.style.display = 'block';
|
|
312
|
+
});
|
|
313
|
+
li.addEventListener('mouseleave', () => {
|
|
314
|
+
submenuEl.style.display = 'none';
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
else if (item.id !== undefined) {
|
|
318
|
+
const itemId = item.id;
|
|
319
|
+
li.addEventListener('click', (e) => {
|
|
320
|
+
e.stopPropagation();
|
|
321
|
+
onSelect(itemId);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
menu.appendChild(li);
|
|
326
|
+
}
|
|
327
|
+
return menu;
|
|
328
|
+
}
|
|
329
|
+
//# sourceMappingURL=menu.js.map
|
package/dist/menu.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu.js","sourceRoot":"","sources":["../src/menu.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/menu",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/signals": "0.1.0",
|
|
31
|
+
"@flighthq/types": "0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^5.3.0"
|
|
35
|
+
},
|
|
36
|
+
"description": "Native application and context menus over a swappable web/native backend",
|
|
37
|
+
"sideEffects": false
|
|
38
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { MenuItemTemplate } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createDefaultAppMenuTemplate,
|
|
5
|
+
createDefaultEditMenuTemplate,
|
|
6
|
+
createDefaultFileMenuTemplate,
|
|
7
|
+
createDefaultHelpMenuTemplate,
|
|
8
|
+
createDefaultViewMenuTemplate,
|
|
9
|
+
createDefaultWindowMenuTemplate,
|
|
10
|
+
} from './menu-templates';
|
|
11
|
+
|
|
12
|
+
function collectIds(items: readonly MenuItemTemplate[]): string[] {
|
|
13
|
+
const ids: string[] = [];
|
|
14
|
+
for (const item of items) {
|
|
15
|
+
if (item.id !== undefined) ids.push(item.id);
|
|
16
|
+
if (item.submenu !== undefined) ids.push(...collectIds(item.submenu));
|
|
17
|
+
}
|
|
18
|
+
return ids;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('createDefaultAppMenuTemplate', () => {
|
|
22
|
+
it('returns a submenu template with the app name as label', () => {
|
|
23
|
+
const menu = createDefaultAppMenuTemplate('MyApp');
|
|
24
|
+
expect(menu.type).toBe('submenu');
|
|
25
|
+
expect(menu.label).toBe('MyApp');
|
|
26
|
+
expect(menu.role).toBe('appMenu');
|
|
27
|
+
});
|
|
28
|
+
it('includes about, hide, quit items with app name interpolation', () => {
|
|
29
|
+
const menu = createDefaultAppMenuTemplate('MyApp');
|
|
30
|
+
const sub = menu.submenu!;
|
|
31
|
+
const about = sub.find((i) => i.id === 'about');
|
|
32
|
+
const quit = sub.find((i) => i.id === 'quit');
|
|
33
|
+
const hide = sub.find((i) => i.id === 'hide');
|
|
34
|
+
expect(about?.label).toBe('About MyApp');
|
|
35
|
+
expect(quit?.label).toBe('Quit MyApp');
|
|
36
|
+
expect(hide?.label).toBe('Hide MyApp');
|
|
37
|
+
});
|
|
38
|
+
it('includes accelerators for hide and quit', () => {
|
|
39
|
+
const menu = createDefaultAppMenuTemplate('MyApp');
|
|
40
|
+
const sub = menu.submenu!;
|
|
41
|
+
const quit = sub.find((i) => i.id === 'quit');
|
|
42
|
+
const hide = sub.find((i) => i.id === 'hide');
|
|
43
|
+
expect(quit?.accelerator).toBeDefined();
|
|
44
|
+
expect(hide?.accelerator).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
it('all items have enabled true by default', () => {
|
|
47
|
+
const menu = createDefaultAppMenuTemplate('MyApp');
|
|
48
|
+
const items = [menu, ...(menu.submenu ?? [])];
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
if (item.type !== 'separator') {
|
|
51
|
+
expect(item.enabled).toBe(true);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('createDefaultEditMenuTemplate', () => {
|
|
58
|
+
it('returns a submenu template with label Edit and role editMenu', () => {
|
|
59
|
+
const menu = createDefaultEditMenuTemplate();
|
|
60
|
+
expect(menu.type).toBe('submenu');
|
|
61
|
+
expect(menu.label).toBe('Edit');
|
|
62
|
+
expect(menu.role).toBe('editMenu');
|
|
63
|
+
});
|
|
64
|
+
it('includes undo, redo, cut, copy, paste, delete, selectAll', () => {
|
|
65
|
+
const menu = createDefaultEditMenuTemplate();
|
|
66
|
+
const ids = collectIds([menu]);
|
|
67
|
+
expect(ids).toContain('undo');
|
|
68
|
+
expect(ids).toContain('redo');
|
|
69
|
+
expect(ids).toContain('cut');
|
|
70
|
+
expect(ids).toContain('copy');
|
|
71
|
+
expect(ids).toContain('paste');
|
|
72
|
+
expect(ids).toContain('delete');
|
|
73
|
+
expect(ids).toContain('selectAll');
|
|
74
|
+
});
|
|
75
|
+
it('items carry well-known role strings', () => {
|
|
76
|
+
const menu = createDefaultEditMenuTemplate();
|
|
77
|
+
const undo = menu.submenu!.find((i) => i.id === 'undo');
|
|
78
|
+
expect(undo?.role).toBe('undo');
|
|
79
|
+
});
|
|
80
|
+
it('includes separators between groups', () => {
|
|
81
|
+
const menu = createDefaultEditMenuTemplate();
|
|
82
|
+
const hasSeparator = menu.submenu!.some((i) => i.type === 'separator');
|
|
83
|
+
expect(hasSeparator).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('createDefaultFileMenuTemplate', () => {
|
|
88
|
+
it('returns a submenu template with label File and role fileMenu', () => {
|
|
89
|
+
const menu = createDefaultFileMenuTemplate();
|
|
90
|
+
expect(menu.type).toBe('submenu');
|
|
91
|
+
expect(menu.label).toBe('File');
|
|
92
|
+
expect(menu.role).toBe('fileMenu');
|
|
93
|
+
});
|
|
94
|
+
it('includes newFile, openFile, saveFile, saveFileAs, closeFile', () => {
|
|
95
|
+
const menu = createDefaultFileMenuTemplate();
|
|
96
|
+
const ids = collectIds([menu]);
|
|
97
|
+
expect(ids).toContain('newFile');
|
|
98
|
+
expect(ids).toContain('openFile');
|
|
99
|
+
expect(ids).toContain('saveFile');
|
|
100
|
+
expect(ids).toContain('saveFileAs');
|
|
101
|
+
expect(ids).toContain('closeFile');
|
|
102
|
+
});
|
|
103
|
+
it('includes accelerators for New and Save', () => {
|
|
104
|
+
const menu = createDefaultFileMenuTemplate();
|
|
105
|
+
const newItem = menu.submenu!.find((i) => i.id === 'newFile');
|
|
106
|
+
const saveItem = menu.submenu!.find((i) => i.id === 'saveFile');
|
|
107
|
+
expect(newItem?.accelerator).toBeDefined();
|
|
108
|
+
expect(saveItem?.accelerator).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
it('includes separators between groups', () => {
|
|
111
|
+
const menu = createDefaultFileMenuTemplate();
|
|
112
|
+
const hasSeparator = menu.submenu!.some((i) => i.type === 'separator');
|
|
113
|
+
expect(hasSeparator).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('createDefaultHelpMenuTemplate', () => {
|
|
118
|
+
it('returns a submenu template with label Help and role helpMenu', () => {
|
|
119
|
+
const menu = createDefaultHelpMenuTemplate();
|
|
120
|
+
expect(menu.type).toBe('submenu');
|
|
121
|
+
expect(menu.label).toBe('Help');
|
|
122
|
+
expect(menu.role).toBe('helpMenu');
|
|
123
|
+
});
|
|
124
|
+
it('includes a help search item', () => {
|
|
125
|
+
const menu = createDefaultHelpMenuTemplate();
|
|
126
|
+
const ids = collectIds([menu]);
|
|
127
|
+
expect(ids).toContain('help');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('createDefaultViewMenuTemplate', () => {
|
|
132
|
+
it('returns a submenu template with label View and role viewMenu', () => {
|
|
133
|
+
const menu = createDefaultViewMenuTemplate();
|
|
134
|
+
expect(menu.type).toBe('submenu');
|
|
135
|
+
expect(menu.label).toBe('View');
|
|
136
|
+
expect(menu.role).toBe('viewMenu');
|
|
137
|
+
});
|
|
138
|
+
it('includes reload, zoomIn, zoomOut, resetZoom, toggleFullscreen', () => {
|
|
139
|
+
const menu = createDefaultViewMenuTemplate();
|
|
140
|
+
const ids = collectIds([menu]);
|
|
141
|
+
expect(ids).toContain('reload');
|
|
142
|
+
expect(ids).toContain('zoomIn');
|
|
143
|
+
expect(ids).toContain('zoomOut');
|
|
144
|
+
expect(ids).toContain('resetZoom');
|
|
145
|
+
expect(ids).toContain('toggleFullscreen');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('createDefaultWindowMenuTemplate', () => {
|
|
150
|
+
it('returns a submenu template with label Window and role windowMenu', () => {
|
|
151
|
+
const menu = createDefaultWindowMenuTemplate();
|
|
152
|
+
expect(menu.type).toBe('submenu');
|
|
153
|
+
expect(menu.label).toBe('Window');
|
|
154
|
+
expect(menu.role).toBe('windowMenu');
|
|
155
|
+
});
|
|
156
|
+
it('includes minimize and close items', () => {
|
|
157
|
+
const menu = createDefaultWindowMenuTemplate();
|
|
158
|
+
const ids = collectIds([menu]);
|
|
159
|
+
expect(ids).toContain('minimize');
|
|
160
|
+
expect(ids).toContain('close');
|
|
161
|
+
});
|
|
162
|
+
});
|
package/src/menu.test.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { MenuBackend } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
cloneMenuTemplate,
|
|
5
|
+
createMenuItemTemplate,
|
|
6
|
+
createWebMenuBackend,
|
|
7
|
+
enableMenuSignals,
|
|
8
|
+
getMenuBackend,
|
|
9
|
+
getMenuSignals,
|
|
10
|
+
onMenuSelect,
|
|
11
|
+
setApplicationMenu,
|
|
12
|
+
setMenuBackend,
|
|
13
|
+
showContextMenu,
|
|
14
|
+
validateMenuItemTemplate,
|
|
15
|
+
} from './menu';
|
|
16
|
+
|
|
17
|
+
function fakeBackend(overrides?: Partial<MenuBackend>): MenuBackend {
|
|
18
|
+
return {
|
|
19
|
+
setApplicationMenu: () => true,
|
|
20
|
+
popupContextMenu: () => Promise.resolve(null),
|
|
21
|
+
subscribeSelect: () => () => {},
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('cloneMenuTemplate', () => {
|
|
27
|
+
it('produces an equal but distinct tree', () => {
|
|
28
|
+
const original = createMenuItemTemplate({
|
|
29
|
+
id: 'file',
|
|
30
|
+
type: 'submenu',
|
|
31
|
+
submenu: [{ id: 'open', label: 'Open' }],
|
|
32
|
+
});
|
|
33
|
+
const clone = cloneMenuTemplate(original);
|
|
34
|
+
expect(clone).toStrictEqual(original);
|
|
35
|
+
expect(clone).not.toBe(original);
|
|
36
|
+
expect(clone.submenu).not.toBe(original.submenu);
|
|
37
|
+
expect(clone.submenu![0]).not.toBe(original.submenu![0]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('createMenuItemTemplate', () => {
|
|
42
|
+
it('fills type and enabled defaults', () => {
|
|
43
|
+
const item = createMenuItemTemplate({ id: 'copy', label: 'Copy' });
|
|
44
|
+
expect(item.type).toBe('normal');
|
|
45
|
+
expect(item.enabled).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('normalizes submenu children recursively', () => {
|
|
49
|
+
const item = createMenuItemTemplate({ type: 'submenu', submenu: [{ id: 'child' }] });
|
|
50
|
+
expect(item.submenu![0].type).toBe('normal');
|
|
51
|
+
expect(item.submenu![0].enabled).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('createWebMenuBackend', () => {
|
|
56
|
+
it('reports no native application menu', () => {
|
|
57
|
+
const backend = createWebMenuBackend();
|
|
58
|
+
expect(backend.setApplicationMenu([])).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns an unsubscribe function from subscribeSelect', () => {
|
|
62
|
+
const backend = createWebMenuBackend();
|
|
63
|
+
expect(typeof backend.subscribeSelect(() => {})).toBe('function');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('enableMenuSignals', () => {
|
|
68
|
+
it('returns a stable instance across calls', () => {
|
|
69
|
+
expect(enableMenuSignals()).toBe(enableMenuSignals());
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('getMenuBackend', () => {
|
|
74
|
+
afterEach(() => setMenuBackend(null));
|
|
75
|
+
|
|
76
|
+
it('lazily returns a web default backend', () => {
|
|
77
|
+
setMenuBackend(null);
|
|
78
|
+
expect(getMenuBackend()).not.toBeNull();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('getMenuSignals', () => {
|
|
83
|
+
it('returns the active signal group once enabled', () => {
|
|
84
|
+
const signals = enableMenuSignals();
|
|
85
|
+
expect(getMenuSignals()).toBe(signals);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('onMenuSelect', () => {
|
|
90
|
+
afterEach(() => setMenuBackend(null));
|
|
91
|
+
|
|
92
|
+
it('delivers the selected id from the backend', () => {
|
|
93
|
+
let captured: ((id: string) => void) | null = null;
|
|
94
|
+
setMenuBackend(
|
|
95
|
+
fakeBackend({
|
|
96
|
+
subscribeSelect: (l) => {
|
|
97
|
+
captured = l;
|
|
98
|
+
return () => {};
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
const received: string[] = [];
|
|
103
|
+
onMenuSelect((id) => received.push(id));
|
|
104
|
+
captured!('save');
|
|
105
|
+
expect(received).toEqual(['save']);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('setApplicationMenu', () => {
|
|
110
|
+
afterEach(() => setMenuBackend(null));
|
|
111
|
+
|
|
112
|
+
it('delegates to the active backend', () => {
|
|
113
|
+
let installed = 0;
|
|
114
|
+
setMenuBackend(
|
|
115
|
+
fakeBackend({
|
|
116
|
+
setApplicationMenu: (i) => {
|
|
117
|
+
installed = i.length;
|
|
118
|
+
return true;
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
expect(setApplicationMenu([{ id: 'a' }])).toBe(true);
|
|
123
|
+
expect(installed).toBe(1);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('setMenuBackend', () => {
|
|
128
|
+
afterEach(() => setMenuBackend(null));
|
|
129
|
+
|
|
130
|
+
it('installs an explicit backend and reverts to the web default on null', () => {
|
|
131
|
+
const backend = fakeBackend();
|
|
132
|
+
setMenuBackend(backend);
|
|
133
|
+
expect(getMenuBackend()).toBe(backend);
|
|
134
|
+
setMenuBackend(null);
|
|
135
|
+
expect(getMenuBackend()).not.toBe(backend);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('showContextMenu', () => {
|
|
140
|
+
afterEach(() => setMenuBackend(null));
|
|
141
|
+
|
|
142
|
+
it('resolves the id returned by the backend popup', async () => {
|
|
143
|
+
setMenuBackend(fakeBackend({ popupContextMenu: () => Promise.resolve('copy') }));
|
|
144
|
+
await expect(showContextMenu([], 0, 0)).resolves.toBe('copy');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('validateMenuItemTemplate', () => {
|
|
149
|
+
it('returns null for a well-formed item', () => {
|
|
150
|
+
expect(validateMenuItemTemplate(createMenuItemTemplate({ id: 'ok', label: 'Ok' }))).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('rejects a separator that carries a label', () => {
|
|
154
|
+
expect(validateMenuItemTemplate({ type: 'separator', label: 'X' })).not.toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('throws on a cyclic submenu reference', () => {
|
|
158
|
+
const node = createMenuItemTemplate({ type: 'submenu' });
|
|
159
|
+
node.submenu = [node];
|
|
160
|
+
expect(() => validateMenuItemTemplate(node)).toThrow();
|
|
161
|
+
});
|
|
162
|
+
});
|