'use strict'; // This is a fork of electron-context-menu@0.15.0. We need to fork it because // the latest version only runs from the main process and we need it in the // renderer process. It also has a dependency to electron-is-dev which also only // runs in the main process. // // In fact we almost don't use any features of electron-context-menu, which is // just a wrapper over Electron's own native context menu but with more bugs, so // we should get rid of it, but for now this is good enough as a quick fix. const electron = require('electron'); const electronRemote = require('@electron/remote'); const webContents = win => win.webContents || (win.getWebContents && win.getWebContents()); const decorateMenuItem = menuItem => { return (options = {}) => { if (options.transform && !options.click) { menuItem.transform = options.transform; } return menuItem; }; }; const removeUnusedMenuItems = menuTemplate => { let notDeletedPreviousElement; return menuTemplate .filter(menuItem => menuItem !== undefined && menuItem !== false && menuItem.visible !== false) .filter((menuItem, index, array) => { const toDelete = menuItem.type === 'separator' && (!notDeletedPreviousElement || index === array.length - 1 || array[index + 1].type === 'separator'); notDeletedPreviousElement = toDelete ? notDeletedPreviousElement : menuItem; return !toDelete; }); }; const create = (win, options) => { webContents(win).on('context-menu', (event, props) => { if (typeof options.shouldShowMenu === 'function' && options.shouldShowMenu(event, props) === false) { return; } // If another listener has called .preventDefault, don't show the default context menu. if (event.defaultPrevented) { return; } const { editFlags } = props; const hasText = props.selectionText.trim().length > 0; const isLink = Boolean(props.linkURL); const can = type => editFlags[`can${type}`] && hasText; const defaultActions = { separator: () => ({ type: 'separator' }), lookUpSelection: decorateMenuItem({ id: 'lookUpSelection', label: 'Look Up “{selection}”', visible: process.platform === 'darwin' && hasText && !isLink, click() { if (process.platform === 'darwin') { webContents(win).showDefinitionForSelection(); } }, }), cut: decorateMenuItem({ id: 'cut', label: 'Cut', enabled: can('Cut'), visible: props.isEditable, click(menuItem) { props.selectionText = menuItem.transform ? menuItem.transform(props.selectionText) : props.selectionText; electron.clipboard.writeText(props.selectionText); webContents(win).delete(); }, }), copy: decorateMenuItem({ id: 'copy', label: 'Copy', enabled: can('Copy'), visible: props.isEditable || hasText, click(menuItem) { props.selectionText = menuItem.transform ? menuItem.transform(props.selectionText) : props.selectionText; electron.clipboard.writeText(props.selectionText); }, }), paste: decorateMenuItem({ id: 'paste', label: 'Paste', enabled: editFlags.canPaste, visible: props.isEditable, click(menuItem) { let clipboardContent = electron.clipboard.readText(props.selectionText); clipboardContent = menuItem.transform ? menuItem.transform(clipboardContent) : clipboardContent; webContents(win).insertText(clipboardContent); }, }), saveImage: decorateMenuItem({ id: 'saveImage', label: 'Save Image', visible: props.mediaType === 'image', click(menuItem) { props.srcURL = menuItem.transform ? menuItem.transform(props.srcURL) : props.srcURL; // download(win, props.srcURL); }, }), saveImageAs: decorateMenuItem({ id: 'saveImageAs', label: 'Save Image As…', visible: props.mediaType === 'image', click(menuItem) { props.srcURL = menuItem.transform ? menuItem.transform(props.srcURL) : props.srcURL; // download(win, props.srcURL, {saveAs: true}); }, }), copyLink: decorateMenuItem({ id: 'copyLink', label: 'Copy Link', visible: props.linkURL.length !== 0 && props.mediaType === 'none', click(menuItem) { props.linkURL = menuItem.transform ? menuItem.transform(props.linkURL) : props.linkURL; electron.clipboard.write({ bookmark: props.linkText, text: props.linkURL, }); }, }), copyImage: decorateMenuItem({ id: 'copyImage', label: 'Copy Image', visible: props.mediaType === 'image', click() { webContents(win).copyImageAt(props.x, props.y); }, }), copyImageAddress: decorateMenuItem({ id: 'copyImageAddress', label: 'Copy Image Address', visible: props.mediaType === 'image', click(menuItem) { props.srcURL = menuItem.transform ? menuItem.transform(props.srcURL) : props.srcURL; electron.clipboard.write({ bookmark: props.srcURL, text: props.srcURL, }); }, }), inspect: () => ({ id: 'inspect', label: 'Inspect Element', click() { win.inspectElement(props.x, props.y); if (webContents(win).isDevToolsOpened()) { webContents(win).devToolsWebContents.focus(); } }, }), services: () => ({ id: 'services', label: 'Services', role: 'services', visible: process.platform === 'darwin' && (props.isEditable || hasText), }), }; const shouldShowInspectElement = typeof options.showInspectElement === 'boolean' ? options.showInspectElement : false; let menuTemplate = [ defaultActions.separator(), options.showLookUpSelection !== false && defaultActions.lookUpSelection(), defaultActions.separator(), defaultActions.cut(), defaultActions.copy(), defaultActions.paste(), defaultActions.separator(), defaultActions.saveImage(), options.showSaveImageAs && defaultActions.saveImageAs(), options.showCopyImage !== false && defaultActions.copyImage(), options.showCopyImageAddress && defaultActions.copyImageAddress(), defaultActions.separator(), defaultActions.copyLink(), defaultActions.separator(), shouldShowInspectElement && defaultActions.inspect(), options.showServices && defaultActions.services(), defaultActions.separator(), ]; if (options.menu) { menuTemplate = options.menu(defaultActions, props, win); } if (options.prepend) { const result = options.prepend(defaultActions, props, win); if (Array.isArray(result)) { menuTemplate.unshift(...result); } } if (options.append) { const result = options.append(defaultActions, props, win); if (Array.isArray(result)) { menuTemplate.push(...result); } } // Filter out leading/trailing separators // TODO: https://github.com/electron/electron/issues/5869 menuTemplate = removeUnusedMenuItems(menuTemplate); for (const menuItem of menuTemplate) { // Apply custom labels for default menu items if (options.labels && options.labels[menuItem.id]) { menuItem.label = options.labels[menuItem.id]; } // Replace placeholders in menu item labels // if (typeof menuItem.label === 'string' && menuItem.label.includes('{selection}')) { // const selectionString = typeof props.selectionText === 'string' ? props.selectionText.trim() : ''; // menuItem.label = menuItem.label.replace('{selection}', cliTruncate(selectionString, 25)); // } } if (menuTemplate.length > 0) { const menu = (electronRemote ? electronRemote.Menu : electron.Menu).buildFromTemplate(menuTemplate); // // When `electronRemote` is not available, this runs in the browser process. // // We can safely use `win` in this case as it refers to the window the // context-menu should open in. // // When this is being called from a web view, we can't use `win` as this // would refer to the web view which is not allowed to render a popup menu. // menu.popup({ window: electronRemote ? electronRemote.getCurrentWindow() : win }); } }); }; module.exports = (options = {}) => { if (options.window) { const win = options.window; // When window is a webview that has not yet finished loading webContents is not available if (webContents(win) === undefined) { win.addEventListener('dom-ready', () => { create(win, options); }, { once: true }); return; } return create(win, options); } for (const win of (electron.BrowserWindow || electronRemote.BrowserWindow).getAllWindows()) { create(win, options); } (electron.app || electronRemote.app).on('browser-window-created', (event, win) => { create(win, options); }); };