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 (
|
return (
|
||||||
<Pressable
|
<a href={href} onClick={preventAnchorNavigation} style={MARKDOWN_LINK_ANCHOR_STYLE}>
|
||||||
accessibilityRole="link"
|
<Pressable
|
||||||
onPress={handlePress}
|
accessibilityRole="link"
|
||||||
onHoverIn={handleHoverIn}
|
onPress={handlePress}
|
||||||
onHoverOut={handleHoverOut}
|
onHoverIn={handleHoverIn}
|
||||||
>
|
onHoverOut={handleHoverOut}
|
||||||
<Text style={hoveredTextStyle}>{children}</Text>
|
>
|
||||||
</Pressable>
|
<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(
|
function getInlineCodeAutoLinkUrl(
|
||||||
markdownParser: ReturnType<typeof MarkdownIt>,
|
markdownParser: ReturnType<typeof MarkdownIt>,
|
||||||
content: string,
|
content: string,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
setupWindowResizeEvents,
|
setupWindowResizeEvents,
|
||||||
setupDefaultContextMenu,
|
setupDefaultContextMenu,
|
||||||
setupDragDropPrevention,
|
setupDragDropPrevention,
|
||||||
|
buildStandardContextMenuItems,
|
||||||
} from "./window/window-manager.js";
|
} from "./window/window-manager.js";
|
||||||
import { registerDialogHandlers } from "./features/dialogs.js";
|
import { registerDialogHandlers } from "./features/dialogs.js";
|
||||||
import {
|
import {
|
||||||
@@ -150,10 +151,7 @@ function showBrowserWebviewContextMenu(
|
|||||||
params: Electron.ContextMenuParams,
|
params: Electron.ContextMenuParams,
|
||||||
): void {
|
): void {
|
||||||
const menu = Menu.buildFromTemplate([
|
const menu = Menu.buildFromTemplate([
|
||||||
{ role: "copy", enabled: params.selectionText.length > 0 },
|
...buildStandardContextMenuItems(contents, params),
|
||||||
{ role: "paste" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "selectAll" },
|
|
||||||
...(app.isPackaged
|
...(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 {
|
export function readBadgeCount(input: unknown): number {
|
||||||
if (typeof input !== "number" || !Number.isSafeInteger(input) || input < 0) {
|
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 {
|
export function setupDefaultContextMenu(win: BrowserWindow): void {
|
||||||
win.webContents.on("context-menu", (_event, params) => {
|
win.webContents.on("context-menu", (_event, params) => {
|
||||||
const menu = Menu.buildFromTemplate([
|
const menu = Menu.buildFromTemplate(buildStandardContextMenuItems(win.webContents, params));
|
||||||
{ role: "copy", enabled: params.selectionText.length > 0 },
|
|
||||||
{ role: "paste" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "selectAll" },
|
|
||||||
]);
|
|
||||||
menu.popup({ window: win });
|
menu.popup({ window: win });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user