186 lines
4.5 KiB
TypeScript
186 lines
4.5 KiB
TypeScript
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<WebContextMenuOptions>,
|
|
): 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<WebContextMenuOptions> = {
|
|
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,
|
|
})
|
|
})
|
|
}
|