@laserware/hoverboard
Version:
Better context menus for Electron.
1 lines • 8.49 kB
Source Map (JSON)
{"version":3,"sources":["../src/main/index.ts","../src/main/configureContextMenus.ts"],"sourcesContent":["export { configureContextMenus } from \"./configureContextMenus.js\";\n","import {\n BrowserWindow,\n type IpcMainInvokeEvent,\n Menu,\n type MenuItemConstructorOptions,\n type NativeImage,\n clipboard,\n ipcMain,\n nativeImage,\n shell,\n} from \"electron\";\n\nimport {\n IpcChannel,\n type ShowContextMenuRequest,\n type ShowContextMenuResponse,\n} from \"../sandbox/globals.js\";\n\n/**\n * Options for configuring context menus in the application.\n */\nexport type ContextMenuOptions = {\n /**\n * Adds the \"Inspect Element\" menu item to the custom context menu, which\n * is very useful for development.\n */\n inspectElement?: boolean;\n\n /**\n * Adds context menu items to copy and open links if the target element clicked\n * was an anchor tag.\n */\n linkHandlers?: boolean;\n};\n\n/**\n * Electron Menu instance with an ID that enables us to find the menu and\n * dispose of it.\n *\n * @internal\n */\ninterface CustomContextMenu extends Menu {\n id: string;\n}\n\nfunction createCustomContextMenu(\n id: string,\n template: MenuItemConstructorOptions[],\n): CustomContextMenu {\n const contextMenu = Menu.buildFromTemplate(template);\n\n return Object.assign(contextMenu, { id });\n}\n\n/**\n * Configures context menus that can be created from the renderer process.\n *\n * @param options Options for building context menus.\n */\nexport function configureContextMenus(options: ContextMenuOptions): {\n dispose(): void;\n} {\n const contextMenus = new Map<string, CustomContextMenu>();\n\n const closeContextMenu = (menuId: string, window: BrowserWindow): void => {\n contextMenus.get(menuId)?.closePopup(window);\n contextMenus.delete(menuId);\n };\n\n const handleShowContextMenu = (\n event: IpcMainInvokeEvent,\n request: ShowContextMenuRequest,\n ) => {\n const browserWindow = BrowserWindow.fromWebContents(event.sender);\n if (browserWindow === null) {\n return;\n }\n\n const { menuId, position, template, linkURL } = request;\n\n return new Promise((resolve) => {\n closeContextMenu(menuId, browserWindow);\n\n for (const item of walkMenuTemplate(template)) {\n item.icon = getIconForMenuItem(item);\n\n if (item.type !== \"separator\" && item.role === undefined) {\n item.click = (menuItem, window, event) => {\n if (item.id === undefined) {\n throw new Error(\"Clickable item must have an ID\");\n }\n\n resolve({\n menuId,\n menuItemId: item.id,\n event,\n } satisfies ShowContextMenuResponse);\n };\n }\n }\n\n const zoomFactor = browserWindow.webContents.getZoomFactor();\n\n const x = Math.floor(position.x * zoomFactor);\n const y = Math.floor(position.y * zoomFactor);\n\n if (linkURL !== undefined) {\n template.push(\n { type: \"separator\" },\n {\n label: \"Copy Link\",\n type: \"normal\",\n click: () => {\n void clipboard.writeText(linkURL, \"clipboard\");\n },\n },\n {\n label: \"Open Link\",\n type: \"normal\",\n click: () => {\n void shell.openExternal(linkURL);\n },\n },\n );\n }\n\n if (options.inspectElement) {\n template.push(\n { type: \"separator\" },\n {\n label: \"Inspect Element\",\n type: \"normal\",\n click: () => {\n browserWindow.webContents?.inspectElement(x, y);\n },\n },\n );\n }\n\n const contextMenu = createCustomContextMenu(menuId, template);\n\n contextMenus.set(menuId, contextMenu);\n\n contextMenu.popup({\n window: browserWindow,\n x,\n y,\n callback() {\n contextMenus.delete(menuId);\n\n resolve({\n menuId,\n menuItemId: null,\n event: {},\n } satisfies ShowContextMenuResponse);\n },\n });\n });\n };\n\n const handleHideContextMenu = (event: IpcMainInvokeEvent, menuId: string) => {\n const browserWindow = BrowserWindow.fromWebContents(event.sender);\n if (browserWindow === null) {\n return;\n }\n\n closeContextMenu(menuId, browserWindow);\n };\n\n ipcMain.handle(IpcChannel.ForShowContextMenu, handleShowContextMenu);\n ipcMain.handle(IpcChannel.ForHideContextMenu, handleHideContextMenu);\n\n return {\n dispose(): void {\n ipcMain.removeHandler(IpcChannel.ForShowContextMenu);\n ipcMain.removeHandler(IpcChannel.ForHideContextMenu);\n },\n };\n}\n\nfunction getIconForMenuItem(\n menuItem: MenuItemConstructorOptions,\n): NativeImage | undefined {\n if (typeof menuItem.icon !== \"string\") {\n return undefined;\n }\n\n // Rather than try to determine if the icon property was a file path or a\n // data URL, we start by trying to create it from a path (since this is\n // probably the most likely scenario). If the value was a data URL, the\n // native image will be empty, so we try again:\n let image = nativeImage.createFromPath(menuItem.icon);\n\n if (image.isEmpty()) {\n image = nativeImage.createFromDataURL(menuItem.icon);\n }\n\n // We resize the image to make sure it fits in the menu:\n return image.resize({ width: 16, height: 16, quality: \"best\" });\n}\n\nfunction* walkMenuTemplate(\n template: MenuItemConstructorOptions[],\n): Generator<MenuItemConstructorOptions, void, void> {\n function* recurse(\n items: MenuItemConstructorOptions[],\n ): Generator<MenuItemConstructorOptions, void, void> {\n for (const item of items) {\n yield item;\n\n if (Array.isArray(item.submenu)) {\n yield* recurse(item.submenu);\n }\n }\n }\n\n yield* recurse(template);\n}\n"],"mappings":"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","names":["event"]}