import type { ContextMenuParams, MenuItemConstructorOptions, WebContents, } from 'electron/main' import { clipboard, shell } from 'electron' import { Menu } from 'electron/main' type WebContextMenuOptions = { allowNavigation?: boolean } function isRemoteURL(url: string | undefined): boolean { return /^https?:\/\//i.test(url ?? '') } function hasVisibleText(value: string | undefined): boolean { return Boolean(value?.trim()) } function pushSeparator(items: MenuItemConstructorOptions[]): void { if (items.length > 0 && items.at(-1)?.type !== 'separator') { items.push({ type: 'separator' }) } } function trimTrailingSeparators(items: MenuItemConstructorOptions[]): MenuItemConstructorOptions[] { while (items.at(-1)?.type === 'separator') { items.pop() } return items } function writeClipboardText(text: string): void { if (text) { clipboard.writeText(text) } } function buildContextMenuTemplate( webContents: WebContents, params: ContextMenuParams, options: Required, ): MenuItemConstructorOptions[] { const items: MenuItemConstructorOptions[] = [] const editFlags = params.editFlags const canCopy = editFlags.canCopy || hasVisibleText(params.selectionText) const canSelectAll = editFlags.canSelectAll || params.isEditable if (params.isEditable) { items.push( { label: '撤销', enabled: editFlags.canUndo, click: () => webContents.undo(), }, { label: '重做', enabled: editFlags.canRedo, click: () => webContents.redo(), }, { type: 'separator' }, { label: '剪切', enabled: editFlags.canCut, click: () => webContents.cut(), }, { label: '复制', enabled: canCopy, click: () => webContents.copy(), }, { label: '粘贴', enabled: editFlags.canPaste, click: () => webContents.paste(), }, { label: '删除', enabled: editFlags.canDelete, click: () => webContents.delete(), }, { type: 'separator' }, { label: '全选', enabled: canSelectAll, click: () => webContents.selectAll(), }, ) } else if (canCopy) { items.push({ label: '复制', click: () => webContents.copy(), }) } if (hasVisibleText(params.selectionText)) { pushSeparator(items) items.push({ label: '复制所选文字', click: () => writeClipboardText(params.selectionText), }) } if (isRemoteURL(params.linkURL)) { pushSeparator(items) items.push( { label: '复制链接', click: () => writeClipboardText(params.linkURL), }, { label: '在浏览器中打开链接', click: () => { void shell.openExternal(params.linkURL) }, }, ) } if (params.mediaType === 'image' && params.hasImageContents) { pushSeparator(items) items.push({ label: '复制图片', click: () => webContents.copyImageAt(params.x, params.y), }) if (isRemoteURL(params.srcURL)) { items.push({ label: '复制图片地址', click: () => writeClipboardText(params.srcURL), }) } } else if (isRemoteURL(params.srcURL)) { pushSeparator(items) items.push({ label: '复制媒体地址', click: () => writeClipboardText(params.srcURL), }) } if (options.allowNavigation) { pushSeparator(items) items.push( { label: '后退', enabled: webContents.navigationHistory.canGoBack(), click: () => webContents.navigationHistory.goBack(), }, { label: '前进', enabled: webContents.navigationHistory.canGoForward(), click: () => webContents.navigationHistory.goForward(), }, { label: '重新加载', enabled: !webContents.isDestroyed(), click: () => webContents.reload(), }, ) } return trimTrailingSeparators(items) } export function attachWebContextMenu( webContents: WebContents, options: WebContextMenuOptions = {}, ): void { const resolvedOptions: Required = { allowNavigation: options.allowNavigation ?? true, } webContents.on('context-menu', (event, params) => { const template = buildContextMenuTemplate(webContents, params, resolvedOptions) if (template.length === 0) { return } event.preventDefault() Menu.buildFromTemplate(template).popup({ frame: params.frame ?? undefined, sourceType: params.menuSourceType, }) }) }