@laserware/hoverboard
Version:
Better context menus for Electron.
1 lines • 9.44 kB
Source Map (JSON)
{"version":3,"sources":["../src/main/configureContextMenus.ts"],"names":["event"],"mappings":"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","file":"main.mjs","sourcesContent":["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"]}