From 858c029d10085171d397f8b7479b4e2c9765f86f Mon Sep 17 00:00:00 2001 From: Ivan Date: Mon, 13 Apr 2026 18:46:59 -0500 Subject: [PATCH] feat(electron): implement default context menu for editable content, links, and misspelled words in Electron app --- electron/main.js | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/electron/main.js b/electron/main.js index 9543ed3..70e21aa 100644 --- a/electron/main.js +++ b/electron/main.js @@ -10,6 +10,7 @@ const { Notification, powerSaveBlocker, session, + clipboard, } = require("electron"); const electronPrompt = require("electron-prompt"); const { spawn } = require("child_process"); @@ -308,6 +309,80 @@ ipcMain.handle("pick-directory", async () => { return filePaths[0]; }); +function attachDefaultContextMenu(browserWindow) { + const webContents = browserWindow.webContents; + webContents.on("context-menu", (event, params) => { + const template = []; + + if (params.isEditable) { + template.push( + { role: "undo" }, + { role: "redo" }, + { type: "separator" }, + { role: "cut" }, + { role: "copy" }, + { role: "paste" }, + { role: "pasteAndMatchStyle" }, + { type: "separator" }, + { role: "selectAll" } + ); + } else if (params.selectionText) { + template.push({ role: "copy" }); + } + + if (params.misspelledWord) { + const suggestions = params.dictionarySuggestions || []; + if (suggestions.length > 0) { + if (template.length > 0) { + template.push({ type: "separator" }); + } + for (const suggestion of suggestions) { + template.push({ + label: suggestion, + click: () => { + webContents.replaceMisspelling(suggestion); + }, + }); + } + } + if (template.length > 0) { + template.push({ type: "separator" }); + } + template.push({ + label: "Add to dictionary", + click: () => { + void webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord); + }, + }); + } + + if (params.linkURL) { + if (template.length > 0) { + template.push({ type: "separator" }); + } + template.push({ + label: "Open link", + click: () => { + shell.openExternal(params.linkURL); + }, + }); + template.push({ + label: "Copy link", + click: () => { + clipboard.writeText(params.linkURL); + }, + }); + } + + if (template.length === 0) { + return; + } + + const menu = Menu.buildFromTemplate(template); + menu.popup({ window: browserWindow }); + }); +} + function log(message) { // log to stdout of this process console.log(message); @@ -391,6 +466,10 @@ function createTray() { } app.whenReady().then(async () => { + app.on("browser-window-created", (event, browserWindow) => { + attachDefaultContextMenu(browserWindow); + }); + // Security: Enforce CSP for all requests as a shell-level fallback session.defaultSession.webRequest.onHeadersReceived((details, callback) => { const responseHeaders = { ...details.responseHeaders };