feat(desktop): rich context menu — copy link, image, spellcheck
Replaces the hardcoded Copy/Paste/Select All menu with one driven by ContextMenuParams: link items (Open in Browser, Copy Link Address), image items (Copy Image, Save Image As), spellcheck suggestions, and proper Cut/Copy/Paste in editable inputs. Both the main window and the browser webview menus go through the shared helper. MarkdownLink (web branch) now wraps its Pressable+Text in a real <a href> with `display: contents` so Electron exposes linkURL on right-click. Native branch is untouched.
This commit is contained in:
@@ -908,17 +908,29 @@ function MarkdownLink({
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="link"
|
||||
onPress={handlePress}
|
||||
onHoverIn={handleHoverIn}
|
||||
onHoverOut={handleHoverOut}
|
||||
>
|
||||
<Text style={hoveredTextStyle}>{children}</Text>
|
||||
</Pressable>
|
||||
<a href={href} onClick={preventAnchorNavigation} style={MARKDOWN_LINK_ANCHOR_STYLE}>
|
||||
<Pressable
|
||||
accessibilityRole="link"
|
||||
onPress={handlePress}
|
||||
onHoverIn={handleHoverIn}
|
||||
onHoverOut={handleHoverOut}
|
||||
>
|
||||
<Text style={hoveredTextStyle}>{children}</Text>
|
||||
</Pressable>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const MARKDOWN_LINK_ANCHOR_STYLE: React.CSSProperties = {
|
||||
display: "contents",
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
};
|
||||
|
||||
function preventAnchorNavigation(event: React.MouseEvent<HTMLAnchorElement>): void {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function getInlineCodeAutoLinkUrl(
|
||||
markdownParser: ReturnType<typeof MarkdownIt>,
|
||||
content: string,
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
setupWindowResizeEvents,
|
||||
setupDefaultContextMenu,
|
||||
setupDragDropPrevention,
|
||||
buildStandardContextMenuItems,
|
||||
} from "./window/window-manager.js";
|
||||
import { registerDialogHandlers } from "./features/dialogs.js";
|
||||
import {
|
||||
@@ -150,10 +151,7 @@ function showBrowserWebviewContextMenu(
|
||||
params: Electron.ContextMenuParams,
|
||||
): void {
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{ role: "copy", enabled: params.selectionText.length > 0 },
|
||||
{ role: "paste" },
|
||||
{ type: "separator" },
|
||||
{ role: "selectAll" },
|
||||
...buildStandardContextMenuItems(contents, params),
|
||||
...(app.isPackaged
|
||||
? []
|
||||
: [
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { app, BrowserWindow, Menu, ipcMain, nativeTheme } from "electron";
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
Menu,
|
||||
type MenuItemConstructorOptions,
|
||||
type WebContents,
|
||||
clipboard,
|
||||
ipcMain,
|
||||
nativeTheme,
|
||||
shell,
|
||||
} from "electron";
|
||||
|
||||
export function readBadgeCount(input: unknown): number {
|
||||
if (typeof input !== "number" || !Number.isSafeInteger(input) || input < 0) {
|
||||
@@ -272,14 +282,76 @@ export function setupDarwinPaintRefresh(win: BrowserWindow): void {
|
||||
});
|
||||
}
|
||||
|
||||
export function buildStandardContextMenuItems(
|
||||
contents: WebContents,
|
||||
params: Electron.ContextMenuParams,
|
||||
): MenuItemConstructorOptions[] {
|
||||
const items: MenuItemConstructorOptions[] = [];
|
||||
|
||||
if (params.misspelledWord) {
|
||||
if (params.dictionarySuggestions.length > 0) {
|
||||
for (const suggestion of params.dictionarySuggestions) {
|
||||
items.push({
|
||||
label: suggestion,
|
||||
click: () => contents.replaceMisspelling(suggestion),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
items.push({ label: "No suggestions", enabled: false });
|
||||
}
|
||||
items.push({ type: "separator" });
|
||||
items.push({
|
||||
label: "Add to Dictionary",
|
||||
click: () => contents.session.addWordToSpellCheckerDictionary(params.misspelledWord),
|
||||
});
|
||||
items.push({ type: "separator" });
|
||||
}
|
||||
|
||||
if (params.linkURL && /^https?:/i.test(params.linkURL)) {
|
||||
items.push({
|
||||
label: "Open Link in Browser",
|
||||
click: () => {
|
||||
void shell.openExternal(params.linkURL);
|
||||
},
|
||||
});
|
||||
items.push({
|
||||
label: "Copy Link Address",
|
||||
click: () => clipboard.writeText(params.linkURL),
|
||||
});
|
||||
items.push({ type: "separator" });
|
||||
}
|
||||
|
||||
if (params.hasImageContents && params.srcURL) {
|
||||
items.push({
|
||||
label: "Copy Image",
|
||||
click: () => contents.copyImageAt(params.x, params.y),
|
||||
});
|
||||
items.push({
|
||||
label: "Save Image As…",
|
||||
click: () => contents.downloadURL(params.srcURL),
|
||||
});
|
||||
items.push({ type: "separator" });
|
||||
}
|
||||
|
||||
if (params.isEditable) {
|
||||
items.push({ role: "cut", enabled: params.editFlags.canCut });
|
||||
items.push({ role: "copy", enabled: params.editFlags.canCopy });
|
||||
items.push({ role: "paste", enabled: params.editFlags.canPaste });
|
||||
items.push({ type: "separator" });
|
||||
items.push({ role: "selectAll" });
|
||||
} else {
|
||||
items.push({ role: "copy", enabled: params.selectionText.length > 0 });
|
||||
items.push({ role: "paste" });
|
||||
items.push({ type: "separator" });
|
||||
items.push({ role: "selectAll" });
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export function setupDefaultContextMenu(win: BrowserWindow): void {
|
||||
win.webContents.on("context-menu", (_event, params) => {
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{ role: "copy", enabled: params.selectionText.length > 0 },
|
||||
{ role: "paste" },
|
||||
{ type: "separator" },
|
||||
{ role: "selectAll" },
|
||||
]);
|
||||
const menu = Menu.buildFromTemplate(buildStandardContextMenuItems(win.webContents, params));
|
||||
menu.popup({ window: win });
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user