@zappdev/runtime 0.1.0 → 0.6.0-alpha.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/app.ts +28 -81
- package/bridge.ts +23 -0
- package/context-menu.ts +84 -0
- package/dialog.ts +29 -103
- package/dock.ts +76 -0
- package/events.ts +104 -226
- package/index.ts +17 -25
- package/menu.ts +59 -68
- package/notification.ts +199 -0
- package/package.json +7 -5
- package/services.ts +46 -81
- package/sync.ts +60 -44
- package/window.ts +125 -0
- package/worker-globals.ts +47 -0
- package/worker.ts +104 -248
- package/README.md +0 -51
- package/bindings-contract.ts +0 -31
- package/protocol.ts +0 -94
- package/windows.ts +0 -222
package/events.ts
CHANGED
|
@@ -1,249 +1,127 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Events — global event bus for cross-context communication.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { Events } from "@zappdev/runtime";
|
|
7
|
+
*
|
|
8
|
+
* // Listen for events
|
|
9
|
+
* const off = Events.on("my-event", (data) => console.log(data));
|
|
10
|
+
*
|
|
11
|
+
* // Emit events (fire-and-forget, all listeners receive)
|
|
12
|
+
* Events.emit("my-event", { hello: "world" });
|
|
13
|
+
*
|
|
14
|
+
* // Unsubscribe
|
|
15
|
+
* off();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { getBridge } from "./bridge";
|
|
20
|
+
|
|
21
|
+
/** Numeric identifiers for window events. */
|
|
2
22
|
export enum WindowEvent {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
CLOSE = 5,
|
|
15
|
-
/** The window was minimized. */
|
|
16
|
-
MINIMIZE = 6,
|
|
17
|
-
/** The window was maximized. */
|
|
18
|
-
MAXIMIZE = 7,
|
|
19
|
-
/** The window was restored from minimize or maximize. */
|
|
20
|
-
RESTORE = 8,
|
|
21
|
-
/** The window entered fullscreen mode. */
|
|
22
|
-
FULLSCREEN = 9,
|
|
23
|
-
/** The window exited fullscreen mode. */
|
|
24
|
-
UNFULLSCREEN = 10,
|
|
23
|
+
READY = 0,
|
|
24
|
+
FOCUS = 1,
|
|
25
|
+
BLUR = 2,
|
|
26
|
+
RESIZE = 3,
|
|
27
|
+
MOVE = 4,
|
|
28
|
+
CLOSE = 5,
|
|
29
|
+
MINIMIZE = 6,
|
|
30
|
+
MAXIMIZE = 7,
|
|
31
|
+
RESTORE = 8,
|
|
32
|
+
FULLSCREEN = 9,
|
|
33
|
+
UNFULLSCREEN = 10,
|
|
25
34
|
}
|
|
26
35
|
|
|
27
|
-
/**
|
|
36
|
+
/** App lifecycle events.
|
|
37
|
+
* STARTED/SHUTDOWN are backend-only (fire before/after WebViews exist).
|
|
38
|
+
* The rest are available in both frontend and backend. */
|
|
28
39
|
export enum AppEvent {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
STARTED = 100, // backend-only
|
|
41
|
+
SHUTDOWN = 101, // backend-only
|
|
42
|
+
REOPEN = 104, // dock icon clicked
|
|
43
|
+
OPEN_URL = 105, // deep link opened
|
|
44
|
+
DID_BECOME_ACTIVE = 106,
|
|
45
|
+
DID_RESIGN_ACTIVE = 107,
|
|
33
46
|
}
|
|
34
47
|
|
|
48
|
+
/** Map WindowEvent enum to string event names. */
|
|
35
49
|
const WINDOW_EVENT_NAMES: Record<number, string> = {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
[WindowEvent.READY]: "window:ready",
|
|
51
|
+
[WindowEvent.FOCUS]: "window:focus",
|
|
52
|
+
[WindowEvent.BLUR]: "window:blur",
|
|
53
|
+
[WindowEvent.RESIZE]: "window:resize",
|
|
54
|
+
[WindowEvent.MOVE]: "window:move",
|
|
55
|
+
[WindowEvent.CLOSE]: "window:close",
|
|
56
|
+
[WindowEvent.MINIMIZE]: "window:minimize",
|
|
57
|
+
[WindowEvent.MAXIMIZE]: "window:maximize",
|
|
58
|
+
[WindowEvent.RESTORE]: "window:restore",
|
|
59
|
+
[WindowEvent.FULLSCREEN]: "window:fullscreen",
|
|
60
|
+
[WindowEvent.UNFULLSCREEN]: "window:unfullscreen",
|
|
47
61
|
};
|
|
48
62
|
|
|
49
63
|
const APP_EVENT_NAMES: Record<number, string> = {
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
[AppEvent.STARTED]: "app:started",
|
|
65
|
+
[AppEvent.SHUTDOWN]: "app:shutdown",
|
|
66
|
+
[AppEvent.REOPEN]: "app:reopen",
|
|
67
|
+
[AppEvent.OPEN_URL]: "app:open-url",
|
|
68
|
+
[AppEvent.DID_BECOME_ACTIVE]: "app:active",
|
|
69
|
+
[AppEvent.DID_RESIGN_ACTIVE]: "app:inactive",
|
|
52
70
|
};
|
|
53
71
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
_onEvent?: (name: string, handler: EventHandler) => number;
|
|
61
|
-
_offEvent?: (name: string, id: number) => void;
|
|
62
|
-
_onceEvent?: (name: string, handler: EventHandler) => number;
|
|
63
|
-
_offAllEvents?: (name?: string) => void;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const BRIDGE_SYMBOL = Symbol.for("zapp.bridge");
|
|
67
|
-
|
|
68
|
-
const getBridge = (): ZappBridge => {
|
|
69
|
-
const bridge = (globalThis as unknown as Record<symbol, unknown>)[BRIDGE_SYMBOL] as ZappBridge | undefined;
|
|
70
|
-
if (!bridge) {
|
|
71
|
-
throw new Error("Zapp bridge is unavailable. Is the bootstrap loaded?");
|
|
72
|
-
}
|
|
73
|
-
return bridge;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const ensureBridge = (): ZappBridge => {
|
|
77
|
-
const symbolStore = globalThis as unknown as Record<symbol, unknown>;
|
|
78
|
-
let bridge = symbolStore[BRIDGE_SYMBOL] as ZappBridge | undefined;
|
|
79
|
-
if (!bridge) {
|
|
80
|
-
bridge = { _listeners: {}, _lastId: 0 };
|
|
81
|
-
try {
|
|
82
|
-
Object.defineProperty(symbolStore, BRIDGE_SYMBOL, {
|
|
83
|
-
value: bridge, enumerable: false, configurable: true, writable: false,
|
|
84
|
-
});
|
|
85
|
-
} catch {
|
|
86
|
-
// @ts-ignore -- fallback for non-configurable
|
|
87
|
-
symbolStore[BRIDGE_SYMBOL] = bridge;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (!bridge._listeners) bridge._listeners = {};
|
|
91
|
-
return bridge;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
/** Base payload delivered with window events. */
|
|
95
|
-
export interface WindowEventPayload {
|
|
96
|
-
/** The ID of the window that emitted the event. */
|
|
97
|
-
windowId: string;
|
|
98
|
-
/** Unix-epoch millisecond timestamp of when the event occurred. */
|
|
99
|
-
timestamp: number;
|
|
100
|
-
/** Window dimensions, present on size-related events. */
|
|
101
|
-
size?: { width: number; height: number };
|
|
102
|
-
/** Window screen coordinates, present on position-related events. */
|
|
103
|
-
position?: { x: number; y: number };
|
|
72
|
+
/** Payload for window events that include size and position. */
|
|
73
|
+
export interface WindowSizePayload {
|
|
74
|
+
windowId: string;
|
|
75
|
+
timestamp: number;
|
|
76
|
+
size: { width: number; height: number };
|
|
77
|
+
position: { x: number; y: number };
|
|
104
78
|
}
|
|
105
79
|
|
|
106
|
-
/** Payload for
|
|
107
|
-
export interface
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
size: { width: number; height: number };
|
|
111
|
-
position: { x: number; y: number };
|
|
80
|
+
/** Payload for simple window events (focus, blur, minimize, etc). */
|
|
81
|
+
export interface WindowPayload {
|
|
82
|
+
windowId: string;
|
|
83
|
+
timestamp: number;
|
|
112
84
|
}
|
|
113
85
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
117
|
-
|
|
118
|
-
/** Window events that carry size + position data */
|
|
119
|
-
type WindowSizeEvents =
|
|
120
|
-
| "window:resize"
|
|
121
|
-
| "window:move"
|
|
122
|
-
| "window:maximize"
|
|
123
|
-
| "window:restore";
|
|
124
|
-
|
|
125
|
-
/** Window events without size/position data */
|
|
126
|
-
type WindowSimpleEvents =
|
|
127
|
-
| "window:ready"
|
|
128
|
-
| "window:focus"
|
|
129
|
-
| "window:blur"
|
|
130
|
-
| "window:close"
|
|
131
|
-
| "window:minimize"
|
|
132
|
-
| "window:fullscreen"
|
|
133
|
-
| "window:unfullscreen";
|
|
134
|
-
|
|
135
|
-
/** App lifecycle events */
|
|
136
|
-
type AppEvents =
|
|
137
|
-
| "app:started"
|
|
138
|
-
| "app:shutdown";
|
|
139
|
-
|
|
140
|
-
/** All known Zapp event names */
|
|
141
|
-
export type KnownEventName = WindowSizeEvents | WindowSimpleEvents | AppEvents;
|
|
86
|
+
/** Known window events that carry size+position data. */
|
|
87
|
+
type SizeEvents = "window:resize" | "window:move" | "window:maximize" | "window:restore";
|
|
142
88
|
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
T extends WindowSimpleEvents ? WindowEventPayload :
|
|
147
|
-
T extends AppEvents ? undefined :
|
|
148
|
-
unknown;
|
|
89
|
+
/** Known window events without size data. */
|
|
90
|
+
type SimpleEvents = "window:ready" | "window:focus" | "window:blur" | "window:close"
|
|
91
|
+
| "window:minimize" | "window:fullscreen" | "window:unfullscreen";
|
|
149
92
|
|
|
150
|
-
/**
|
|
151
|
-
|
|
93
|
+
/** App event string names. */
|
|
94
|
+
type AppEvents = "app:started" | "app:shutdown" | "app:reopen" | "app:open-url" | "app:active" | "app:inactive";
|
|
152
95
|
|
|
153
|
-
/**
|
|
154
|
-
export
|
|
155
|
-
/** Emit a named event with an optional payload. */
|
|
156
|
-
emit(name: string, payload?: unknown): unknown;
|
|
96
|
+
/** All known event names. Arbitrary strings also work. */
|
|
97
|
+
export type EventName = SizeEvents | SimpleEvents | AppEvents | (string & {});
|
|
157
98
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
on(name: WindowSimpleEvents, handler: (payload: WindowEventPayload) => void): () => void;
|
|
162
|
-
/** Subscribe to an app lifecycle event. Returns an unsubscribe function. */
|
|
163
|
-
on(name: AppEvents, handler: () => void): () => void;
|
|
164
|
-
/** Subscribe to a custom event. Returns an unsubscribe function. */
|
|
165
|
-
on(name: string & {}, handler: (payload?: unknown) => void): () => void;
|
|
166
|
-
|
|
167
|
-
/** Subscribe to a window size/position event once. Returns an unsubscribe function. */
|
|
168
|
-
once(name: WindowSizeEvents, handler: (payload: WindowSizeEventPayload) => void): () => void;
|
|
169
|
-
/** Subscribe to a simple window event once. Returns an unsubscribe function. */
|
|
170
|
-
once(name: WindowSimpleEvents, handler: (payload: WindowEventPayload) => void): () => void;
|
|
171
|
-
/** Subscribe to an app lifecycle event once. Returns an unsubscribe function. */
|
|
172
|
-
once(name: AppEvents, handler: () => void): () => void;
|
|
173
|
-
/** Subscribe to a custom event once. Returns an unsubscribe function. */
|
|
174
|
-
once(name: string & {}, handler: (payload?: unknown) => void): () => void;
|
|
175
|
-
|
|
176
|
-
/** Remove a specific handler, or all handlers, for the given event name. */
|
|
177
|
-
off(name: EventName, handler?: EventHandler): void;
|
|
178
|
-
/** Remove all handlers for a given event name, or all events if no name is provided. */
|
|
179
|
-
offAll(name?: EventName): void;
|
|
99
|
+
/** Resolve event name from enum to string. */
|
|
100
|
+
export function eventName(event: WindowEvent | AppEvent): string {
|
|
101
|
+
return WINDOW_EVENT_NAMES[event] ?? APP_EVENT_NAMES[event] ?? `unknown:${event}`;
|
|
180
102
|
}
|
|
181
103
|
|
|
182
|
-
|
|
183
|
-
export const Events = {
|
|
184
|
-
emit(name: string, payload?: unknown): unknown {
|
|
185
|
-
return getBridge()._emit?.(name, payload);
|
|
186
|
-
},
|
|
187
|
-
|
|
188
|
-
on(name: string, handler: EventHandler): () => void {
|
|
189
|
-
const bridge = ensureBridge();
|
|
190
|
-
if (bridge._onEvent) {
|
|
191
|
-
const id = bridge._onEvent(name, handler);
|
|
192
|
-
return () => bridge._offEvent?.(name, id);
|
|
193
|
-
}
|
|
194
|
-
const listeners = bridge._listeners!;
|
|
195
|
-
const id = (bridge._lastId = (bridge._lastId ?? 0) + 1);
|
|
196
|
-
(listeners[name] ??= []).push({ id, fn: handler });
|
|
197
|
-
return () => {
|
|
198
|
-
listeners[name] = (listeners[name] ?? []).filter((e) => e.id !== id);
|
|
199
|
-
};
|
|
200
|
-
},
|
|
201
|
-
|
|
202
|
-
once(name: string, handler: EventHandler): () => void {
|
|
203
|
-
const bridge = ensureBridge();
|
|
204
|
-
if (bridge._onceEvent) {
|
|
205
|
-
const id = bridge._onceEvent(name, handler);
|
|
206
|
-
return () => bridge._offEvent?.(name, id);
|
|
207
|
-
}
|
|
208
|
-
const listeners = bridge._listeners!;
|
|
209
|
-
const id = (bridge._lastId = (bridge._lastId ?? 0) + 1);
|
|
210
|
-
(listeners[name] ??= []).push({ id, fn: handler, once: true });
|
|
211
|
-
return () => {
|
|
212
|
-
listeners[name] = (listeners[name] ?? []).filter((e) => e.id !== id);
|
|
213
|
-
};
|
|
214
|
-
},
|
|
215
|
-
|
|
216
|
-
off(name: string, handler?: EventHandler): void {
|
|
217
|
-
const bridge = getBridge();
|
|
218
|
-
if (!handler) {
|
|
219
|
-
bridge._offAllEvents?.(name) ??
|
|
220
|
-
((bridge._listeners ?? {})[name] = []);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
const listeners = bridge._listeners ?? {};
|
|
224
|
-
listeners[name] = (listeners[name] ?? []).filter((e) => e.fn !== handler);
|
|
225
|
-
},
|
|
104
|
+
type EventHandler = (payload: any) => void;
|
|
226
105
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
106
|
+
export const Events = {
|
|
107
|
+
/**
|
|
108
|
+
* Subscribe to an event. Returns an unsubscribe function.
|
|
109
|
+
*/
|
|
110
|
+
on(name: EventName, handler: EventHandler): () => void {
|
|
111
|
+
return getBridge().on(name, handler);
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Emit a fire-and-forget event.
|
|
116
|
+
*
|
|
117
|
+
* - From a **webview**: dispatched locally to listeners in the same window
|
|
118
|
+
* (and to native listeners). Does not cross window boundaries.
|
|
119
|
+
* - From the **backend** worker: broadcast to *every* open webview's
|
|
120
|
+
* listeners. This is the canonical pattern for pushing backend-owned
|
|
121
|
+
* state to all windows without per-window polling.
|
|
122
|
+
* - From a **regular worker**: same as backend — broadcast to every webview.
|
|
123
|
+
*/
|
|
124
|
+
emit(name: string, payload?: Record<string, unknown>): void {
|
|
125
|
+
getBridge().emit(name, payload);
|
|
126
|
+
},
|
|
127
|
+
};
|
package/index.ts
CHANGED
|
@@ -1,39 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Provides window management, events, dialogs, menus, workers, sync primitives,
|
|
6
|
-
* and service invocation for building cross-platform desktop applications.
|
|
3
|
+
* Zapp Runtime — frontend API for Zapp desktop apps.
|
|
7
4
|
*
|
|
8
5
|
* @example
|
|
9
6
|
* ```ts
|
|
10
|
-
* import { App, Window, WindowEvent,
|
|
7
|
+
* import { App, Window, WindowEvent, Events, Services } from "@zappdev/runtime";
|
|
11
8
|
*
|
|
12
9
|
* Window.current().on(WindowEvent.READY, () => {
|
|
13
10
|
* Window.current().show();
|
|
14
11
|
* });
|
|
15
12
|
*
|
|
16
|
-
* const result = await
|
|
17
|
-
* message: "Hello from Zapp!",
|
|
18
|
-
* buttons: ["OK"],
|
|
19
|
-
* });
|
|
13
|
+
* const result = await Services.invoke("greet", { name: "World" });
|
|
20
14
|
* ```
|
|
21
15
|
*/
|
|
22
16
|
|
|
23
17
|
export { App } from "./app";
|
|
24
|
-
export
|
|
25
|
-
export { Events, WindowEvent, AppEvent,
|
|
26
|
-
export
|
|
27
|
-
export {
|
|
28
|
-
export type
|
|
29
|
-
export {
|
|
30
|
-
export {
|
|
31
|
-
export type
|
|
32
|
-
export { Sync } from "./sync";
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export * from "./protocol";
|
|
39
|
-
export * from "./bindings-contract";
|
|
18
|
+
export { Window, type WindowHandle, type WindowOptions } from "./window";
|
|
19
|
+
export { Events, WindowEvent, AppEvent, eventName, type WindowPayload, type WindowSizePayload, type EventName } from "./events";
|
|
20
|
+
export { Services, type InvokeOptions, type CancellablePromise } from "./services";
|
|
21
|
+
export { Worker, SharedWorker, SharedWorkerPort, type WorkerMessageEvent } from "./worker";
|
|
22
|
+
export { Dialog, type OpenFileOptions, type SaveFileOptions, type MessageOptions, type OpenFileResult, type SaveFileResult, type MessageResult } from "./dialog";
|
|
23
|
+
export { Menu, type MenuItemDef, type MenuHandle } from "./menu";
|
|
24
|
+
export { ContextMenu, type ContextMenuOptions } from "./context-menu";
|
|
25
|
+
export { Notification, type NotificationOptions, type ScheduleOptions, type PermissionStatus } from "./notification";
|
|
26
|
+
export { Sync, type SyncWaitOptions } from "./sync";
|
|
27
|
+
export { Dock } from "./dock";
|
|
28
|
+
|
|
29
|
+
// Re-export worker globals type declarations.
|
|
30
|
+
// Workers should add: import "@zappdev/runtime/worker-globals";
|
|
31
|
+
// This registers send/receive/postMessage/onmessage on the global scope.
|
package/menu.ts
CHANGED
|
@@ -1,98 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menu — application menu bar.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { Menu, App } from "@zappdev/runtime";
|
|
7
|
+
*
|
|
8
|
+
* Menu.build([
|
|
9
|
+
* { role: "appMenu" },
|
|
10
|
+
* { label: "File", submenu: [
|
|
11
|
+
* { label: "New", accelerator: "CmdOrCtrl+N", action: () => console.log("New!") },
|
|
12
|
+
* { type: "separator" },
|
|
13
|
+
* { label: "Quit", accelerator: "CmdOrCtrl+Q", action: () => App.quit() },
|
|
14
|
+
* ]},
|
|
15
|
+
* { label: "Edit", role: "editMenu" },
|
|
16
|
+
* { label: "Window", role: "windowMenu" },
|
|
17
|
+
* ]);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { getBridge } from "./bridge";
|
|
1
22
|
import { Events } from "./events";
|
|
2
23
|
|
|
3
|
-
/** Definition of a single menu item, including separators, checkboxes, and submenus. */
|
|
4
24
|
export interface MenuItemDef {
|
|
5
|
-
/** Unique ID for click events. Auto-generated if action is provided without id. */
|
|
6
25
|
id?: string;
|
|
7
|
-
/** Display text. Required unless type is "separator" or role is set. */
|
|
8
26
|
label?: string;
|
|
9
|
-
/** Item type. Default: "normal" */
|
|
10
27
|
type?: "normal" | "separator" | "checkbox";
|
|
11
|
-
/** Whether the item is clickable. Default: true */
|
|
12
28
|
enabled?: boolean;
|
|
13
|
-
/** For checkbox items. Default: false */
|
|
14
29
|
checked?: boolean;
|
|
15
|
-
/** Keyboard shortcut. Use "CmdOrCtrl" for cross-platform Cmd/Ctrl. */
|
|
16
30
|
accelerator?: string;
|
|
17
|
-
|
|
18
|
-
role?: "editMenu" | "windowMenu" | "appMenu";
|
|
19
|
-
/** Click handler. Sugar for menu.on(id, handler). */
|
|
31
|
+
role?: "editMenu" | "windowMenu" | "appMenu" | "copy" | "cut" | "paste" | "selectAll" | "undo" | "redo" | "quit";
|
|
20
32
|
action?: () => void;
|
|
21
|
-
/** Nested submenu items. */
|
|
22
33
|
submenu?: MenuItemDef[];
|
|
23
34
|
}
|
|
24
35
|
|
|
25
|
-
/** Handle returned after building a menu, used to listen for item clicks. */
|
|
26
36
|
export interface MenuHandle {
|
|
27
|
-
/** Listen for a menu item click by its ID */
|
|
28
|
-
on(itemId: string, handler: () => void): () => void;
|
|
29
|
-
/** The raw menu definition (for re-serialization) */
|
|
30
37
|
readonly items: MenuItemDef[];
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
export interface MenuAPI {
|
|
35
|
-
/** Build a menu from a definition array */
|
|
36
|
-
build(items: MenuItemDef[]): MenuHandle;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let menuIdSeq = 0;
|
|
40
|
+
let menuActionCounter = 0;
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
function collectActions(items: MenuItemDef[]): Map<string, () => void> {
|
|
43
|
+
const actions = new Map<string, () => void>();
|
|
42
44
|
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
function walk(items: MenuItemDef[]) {
|
|
46
|
+
for (const item of items) {
|
|
47
|
+
if (item.action) {
|
|
48
|
+
if (!item.id) {
|
|
49
|
+
item.id = `__menu_${++menuActionCounter}`;
|
|
50
|
+
}
|
|
51
|
+
actions.set(item.id, item.action);
|
|
52
|
+
}
|
|
53
|
+
if (item.submenu) walk(item.submenu);
|
|
51
54
|
}
|
|
52
|
-
|
|
53
|
-
delete copy.action;
|
|
54
|
-
if (copy.submenu) {
|
|
55
|
-
copy.submenu = collectActions(copy.submenu, actions);
|
|
56
|
-
}
|
|
57
|
-
return copy;
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function postMenu(items: MenuItemDef[]): void {
|
|
62
|
-
const handler = (globalThis as unknown as Record<string, Record<string, Record<string, { postMessage?: (m: string) => void }>>>)
|
|
63
|
-
.webkit?.messageHandlers?.zapp;
|
|
64
|
-
const chromeWebview = (globalThis as unknown as Record<string, Record<string, { postMessage?: (m: string) => void }>>)
|
|
65
|
-
.chrome?.webview;
|
|
55
|
+
}
|
|
66
56
|
|
|
67
|
-
|
|
57
|
+
walk(items);
|
|
58
|
+
return actions;
|
|
59
|
+
}
|
|
68
60
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
function stripActions(items: MenuItemDef[]): any[] {
|
|
62
|
+
return items.map(item => {
|
|
63
|
+
const clean: any = { ...item };
|
|
64
|
+
delete clean.action;
|
|
65
|
+
if (clean.submenu) clean.submenu = stripActions(clean.submenu);
|
|
66
|
+
return clean;
|
|
67
|
+
});
|
|
74
68
|
}
|
|
75
69
|
|
|
76
|
-
|
|
77
|
-
export const Menu: MenuAPI = {
|
|
70
|
+
export const Menu = {
|
|
78
71
|
build(items: MenuItemDef[]): MenuHandle {
|
|
79
|
-
const actions
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
// Post the cleaned (no functions) menu to native
|
|
83
|
-
postMenu(cleanItems);
|
|
72
|
+
const actions = collectActions(items);
|
|
73
|
+
const clean = stripActions(items);
|
|
84
74
|
|
|
85
|
-
// Wire up
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
75
|
+
// Wire up action event listeners
|
|
76
|
+
if (actions.size > 0) {
|
|
77
|
+
Events.on("__menu:click", (payload: any) => {
|
|
78
|
+
const id = typeof payload === "string" ? JSON.parse(payload).id : payload?.id;
|
|
79
|
+
const handler = actions.get(id);
|
|
80
|
+
if (handler) handler();
|
|
81
|
+
});
|
|
89
82
|
}
|
|
90
83
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
},
|
|
96
|
-
};
|
|
84
|
+
// Send to native
|
|
85
|
+
(getBridge() as any).post(JSON.stringify({ t: 4, m: "setMenu", a: { items: clean } }));
|
|
86
|
+
|
|
87
|
+
return { items };
|
|
97
88
|
},
|
|
98
89
|
};
|