250 lines
6.5 KiB
TypeScript
250 lines
6.5 KiB
TypeScript
import { app, BrowserWindow, clipboard, Menu, MenuItem } from 'electron'
|
|
|
|
const LABELS = {
|
|
openInNewTab: (type: 'link' | Electron.ContextMenuParams['mediaType']) =>
|
|
`Open ${type} in new tab`,
|
|
openInNewWindow: (type: 'link' | Electron.ContextMenuParams['mediaType']) =>
|
|
`Open ${type} in new window`,
|
|
copyAddress: (type: 'link' | Electron.ContextMenuParams['mediaType']) => `Copy ${type} address`,
|
|
undo: 'Undo',
|
|
redo: 'Redo',
|
|
cut: 'Cut',
|
|
copy: 'Copy',
|
|
delete: 'Delete',
|
|
paste: 'Paste',
|
|
selectAll: 'Select All',
|
|
back: 'Back',
|
|
forward: 'Forward',
|
|
reload: 'Reload',
|
|
inspect: 'Inspect',
|
|
addToDictionary: 'Add to dictionary',
|
|
exitFullScreen: 'Exit full screen',
|
|
emoji: 'Emoji',
|
|
}
|
|
|
|
const getBrowserWindowFromWebContents = (webContents: Electron.WebContents) => {
|
|
return BrowserWindow.getAllWindows().find((win) => {
|
|
if (win.webContents === webContents) return true
|
|
|
|
let browserViews: Electron.BrowserView[]
|
|
|
|
if ('getBrowserViews' in win) {
|
|
browserViews = win.getBrowserViews()
|
|
} else if ('getBrowserView' in win) {
|
|
// @ts-ignore
|
|
browserViews = [win.getBrowserView()]
|
|
} else {
|
|
browserViews = []
|
|
}
|
|
|
|
return browserViews.some((view) => view.webContents === webContents)
|
|
})
|
|
}
|
|
|
|
type ChromeContextMenuLabels = typeof LABELS
|
|
|
|
interface ChromeContextMenuOptions {
|
|
/** Context menu parameters emitted from the WebContents 'context-menu' event. */
|
|
params: Electron.ContextMenuParams
|
|
|
|
/** WebContents which emitted the 'context-menu' event. */
|
|
webContents: Electron.WebContents
|
|
|
|
/** Handler for opening links. */
|
|
openLink: (
|
|
url: string,
|
|
disposition: 'default' | 'foreground-tab' | 'background-tab' | 'new-window',
|
|
params: Electron.ContextMenuParams,
|
|
) => void
|
|
|
|
/** Chrome extension menu items. */
|
|
extensionMenuItems?: MenuItem[]
|
|
|
|
/** Labels used to create menu items. Replace this if localization is needed. */
|
|
labels?: ChromeContextMenuLabels
|
|
|
|
/**
|
|
* @deprecated Use 'labels' instead.
|
|
*/
|
|
strings?: ChromeContextMenuLabels
|
|
}
|
|
|
|
export const buildChromeContextMenu = (opts: ChromeContextMenuOptions): Menu => {
|
|
const { params, webContents, openLink, extensionMenuItems } = opts
|
|
|
|
const labels = opts.labels || opts.strings || LABELS
|
|
|
|
const menu = new Menu()
|
|
const append = (opts: Electron.MenuItemConstructorOptions) => menu.append(new MenuItem(opts))
|
|
const appendSeparator = () => menu.append(new MenuItem({ type: 'separator' }))
|
|
|
|
if (params.linkURL) {
|
|
append({
|
|
label: labels.openInNewTab('link'),
|
|
click: () => {
|
|
openLink(params.linkURL, 'default', params)
|
|
},
|
|
})
|
|
append({
|
|
label: labels.openInNewWindow('link'),
|
|
click: () => {
|
|
openLink(params.linkURL, 'new-window', params)
|
|
},
|
|
})
|
|
appendSeparator()
|
|
append({
|
|
label: labels.copyAddress('link'),
|
|
click: () => {
|
|
clipboard.writeText(params.linkURL)
|
|
},
|
|
})
|
|
appendSeparator()
|
|
} else if (params.mediaType !== 'none') {
|
|
// TODO: Loop, Show controls
|
|
append({
|
|
label: labels.openInNewTab(params.mediaType),
|
|
click: () => {
|
|
openLink(params.srcURL, 'default', params)
|
|
},
|
|
})
|
|
append({
|
|
label: labels.copyAddress(params.mediaType),
|
|
click: () => {
|
|
clipboard.writeText(params.srcURL)
|
|
},
|
|
})
|
|
appendSeparator()
|
|
}
|
|
|
|
if (params.isEditable) {
|
|
if (params.misspelledWord) {
|
|
for (const suggestion of params.dictionarySuggestions) {
|
|
append({
|
|
label: suggestion,
|
|
click: () => webContents.replaceMisspelling(suggestion),
|
|
})
|
|
}
|
|
|
|
if (params.dictionarySuggestions.length > 0) appendSeparator()
|
|
|
|
append({
|
|
label: labels.addToDictionary,
|
|
click: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord),
|
|
})
|
|
} else {
|
|
if (
|
|
app.isEmojiPanelSupported() &&
|
|
!['input-number', 'input-telephone'].includes(params.formControlType)
|
|
) {
|
|
append({
|
|
label: labels.emoji,
|
|
click: () => app.showEmojiPanel(),
|
|
})
|
|
appendSeparator()
|
|
}
|
|
|
|
append({
|
|
label: labels.redo,
|
|
enabled: params.editFlags.canRedo,
|
|
click: () => webContents.redo(),
|
|
})
|
|
append({
|
|
label: labels.undo,
|
|
enabled: params.editFlags.canUndo,
|
|
click: () => webContents.undo(),
|
|
})
|
|
}
|
|
|
|
appendSeparator()
|
|
|
|
append({
|
|
label: labels.cut,
|
|
enabled: params.editFlags.canCut,
|
|
click: () => webContents.cut(),
|
|
})
|
|
append({
|
|
label: labels.copy,
|
|
enabled: params.editFlags.canCopy,
|
|
click: () => webContents.copy(),
|
|
})
|
|
append({
|
|
label: labels.paste,
|
|
enabled: params.editFlags.canPaste,
|
|
click: () => webContents.paste(),
|
|
})
|
|
append({
|
|
label: labels.delete,
|
|
enabled: params.editFlags.canDelete,
|
|
click: () => webContents.delete(),
|
|
})
|
|
appendSeparator()
|
|
if (params.editFlags.canSelectAll) {
|
|
append({
|
|
label: labels.selectAll,
|
|
click: () => webContents.selectAll(),
|
|
})
|
|
appendSeparator()
|
|
}
|
|
} else if (params.selectionText) {
|
|
append({
|
|
label: labels.copy,
|
|
click: () => {
|
|
clipboard.writeText(params.selectionText)
|
|
},
|
|
})
|
|
appendSeparator()
|
|
}
|
|
|
|
if (menu.items.length === 0) {
|
|
const browserWindow = getBrowserWindowFromWebContents(webContents)
|
|
|
|
// TODO: Electron needs a way to detect whether we're in HTML5 full screen.
|
|
// Also need to properly exit full screen in Blink rather than just exiting
|
|
// the Electron BrowserWindow.
|
|
if (browserWindow?.fullScreen) {
|
|
append({
|
|
label: labels.exitFullScreen,
|
|
click: () => browserWindow.setFullScreen(false),
|
|
})
|
|
|
|
appendSeparator()
|
|
}
|
|
|
|
append({
|
|
label: labels.back,
|
|
enabled: webContents.navigationHistory.canGoBack(),
|
|
click: () => webContents.navigationHistory.goBack(),
|
|
})
|
|
append({
|
|
label: labels.forward,
|
|
enabled: webContents.navigationHistory.canGoForward(),
|
|
click: () => webContents.navigationHistory.goForward(),
|
|
})
|
|
append({
|
|
label: labels.reload,
|
|
click: () => webContents.reload(),
|
|
})
|
|
appendSeparator()
|
|
}
|
|
|
|
if (extensionMenuItems) {
|
|
extensionMenuItems.forEach((item) => menu.append(item))
|
|
if (extensionMenuItems.length > 0) appendSeparator()
|
|
}
|
|
|
|
append({
|
|
label: labels.inspect,
|
|
click: () => {
|
|
webContents.inspectElement(params.x, params.y)
|
|
|
|
if (!webContents.isDevToolsFocused()) {
|
|
webContents.devToolsWebContents?.focus()
|
|
}
|
|
},
|
|
})
|
|
|
|
return menu
|
|
}
|
|
|
|
export default buildChromeContextMenu
|