UNPKG

electron-context-menu

Version:
429 lines (376 loc) 13.1 kB
import process from 'node:process'; import electron from 'electron'; import cliTruncate from 'cli-truncate'; import {download} from 'electron-dl'; import isDev from 'electron-is-dev'; const webContents = win => win.webContents ?? (win.id && win); const decorateMenuItem = menuItem => (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 && menuItem.visible !== '') .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) => { const handleContextMenu = (event, properties) => { if (typeof options.shouldShowMenu === 'function' && options.shouldShowMenu(event, properties) === false) { return; } const {editFlags} = properties; const hasText = properties.selectionText.length > 0; const isLink = Boolean(properties.linkURL); const can = type => editFlags[`can${type}`] && hasText; const defaultActions = { separator: () => ({type: 'separator'}), learnSpelling: decorateMenuItem({ id: 'learnSpelling', label: '&Learn Spelling', visible: Boolean(properties.isEditable && hasText && properties.misspelledWord), click() { const target = webContents(win); target.session.addWordToSpellCheckerDictionary(properties.misspelledWord); }, }), lookUpSelection: decorateMenuItem({ id: 'lookUpSelection', label: 'Look Up “{selection}”', visible: process.platform === 'darwin' && hasText && !isLink, click() { if (process.platform === 'darwin') { webContents(win).showDefinitionForSelection(); } }, }), searchWithGoogle: decorateMenuItem({ id: 'searchWithGoogle', label: '&Search with Google', visible: hasText, click() { const url = new URL('https://www.google.com/search'); url.searchParams.set('q', properties.selectionText); electron.shell.openExternal(url.toString()); }, }), cut: decorateMenuItem({ id: 'cut', label: 'Cu&t', enabled: can('Cut'), visible: properties.isEditable, click(menuItem) { const target = webContents(win); if (!menuItem.transform && target) { target.cut(); } else { properties.selectionText = menuItem.transform ? menuItem.transform(properties.selectionText) : properties.selectionText; electron.clipboard.writeText(properties.selectionText); } }, }), copy: decorateMenuItem({ id: 'copy', label: '&Copy', enabled: can('Copy'), visible: properties.isEditable || hasText, click(menuItem) { const target = webContents(win); if (!menuItem.transform && target) { target.copy(); } else { properties.selectionText = menuItem.transform ? menuItem.transform(properties.selectionText) : properties.selectionText; electron.clipboard.writeText(properties.selectionText); } }, }), paste: decorateMenuItem({ id: 'paste', label: '&Paste', enabled: editFlags.canPaste, visible: properties.isEditable, click(menuItem) { const target = webContents(win); if (menuItem.transform) { let clipboardContent = electron.clipboard.readText(properties.selectionText); clipboardContent = menuItem.transform ? menuItem.transform(clipboardContent) : clipboardContent; target.insertText(clipboardContent); } else { target.paste(); } }, }), selectAll: decorateMenuItem({ id: 'selectAll', label: 'Select &All', click() { webContents(win).selectAll(); }, }), saveImage: decorateMenuItem({ id: 'saveImage', label: 'Save I&mage', visible: properties.mediaType === 'image', click(menuItem) { properties.srcURL = menuItem.transform ? menuItem.transform(properties.srcURL) : properties.srcURL; download(win, properties.srcURL); }, }), saveImageAs: decorateMenuItem({ id: 'saveImageAs', label: 'Sa&ve Image As…', visible: properties.mediaType === 'image', click(menuItem) { properties.srcURL = menuItem.transform ? menuItem.transform(properties.srcURL) : properties.srcURL; download(win, properties.srcURL, {saveAs: true}); }, }), saveVideo: decorateMenuItem({ id: 'saveVideo', label: 'Save Vide&o', visible: properties.mediaType === 'video', click(menuItem) { properties.srcURL = menuItem.transform ? menuItem.transform(properties.srcURL) : properties.srcURL; download(win, properties.srcURL); }, }), saveVideoAs: decorateMenuItem({ id: 'saveVideoAs', label: 'Save Video& As…', visible: properties.mediaType === 'video', click(menuItem) { properties.srcURL = menuItem.transform ? menuItem.transform(properties.srcURL) : properties.srcURL; download(win, properties.srcURL, {saveAs: true}); }, }), copyLink: decorateMenuItem({ id: 'copyLink', label: 'Copy Lin&k', visible: properties.linkURL.length > 0 && properties.mediaType === 'none', click(menuItem) { properties.linkURL = menuItem.transform ? menuItem.transform(properties.linkURL) : properties.linkURL; electron.clipboard.write({ bookmark: properties.linkText, text: properties.linkURL, }); }, }), saveLinkAs: decorateMenuItem({ id: 'saveLinkAs', label: 'Save Link As…', visible: properties.linkURL.length > 0 && properties.mediaType === 'none', click(menuItem) { properties.linkURL = menuItem.transform ? menuItem.transform(properties.linkURL) : properties.linkURL; download(win, properties.linkURL, {saveAs: true}); }, }), copyImage: decorateMenuItem({ id: 'copyImage', label: 'Cop&y Image', visible: properties.mediaType === 'image', click() { webContents(win).copyImageAt(properties.x, properties.y); }, }), copyImageAddress: decorateMenuItem({ id: 'copyImageAddress', label: 'C&opy Image Address', visible: properties.mediaType === 'image', click(menuItem) { properties.srcURL = menuItem.transform ? menuItem.transform(properties.srcURL) : properties.srcURL; electron.clipboard.write({ bookmark: properties.srcURL, text: properties.srcURL, }); }, }), copyVideoAddress: decorateMenuItem({ id: 'copyVideoAddress', label: 'Copy Video Ad&dress', visible: properties.mediaType === 'video', click(menuItem) { properties.srcURL = menuItem.transform ? menuItem.transform(properties.srcURL) : properties.srcURL; electron.clipboard.write({ bookmark: properties.srcURL, text: properties.srcURL, }); }, }), inspect: () => ({ id: 'inspect', label: 'I&nspect Element', click() { webContents(win).inspectElement(properties.x, properties.y); if (webContents(win).isDevToolsOpened()) { webContents(win).devToolsWebContents.focus(); } }, }), services: () => ({ id: 'services', label: 'Services', role: 'services', visible: process.platform === 'darwin' && (properties.isEditable || hasText), }), }; const shouldShowInspectElement = typeof options.showInspectElement === 'boolean' ? options.showInspectElement : isDev; const shouldShowSelectAll = options.showSelectAll || (options.showSelectAll !== false && process.platform !== 'darwin'); function word(suggestion) { return { id: 'dictionarySuggestions', label: suggestion, visible: Boolean(properties.isEditable && hasText && properties.misspelledWord), click(menuItem) { const target = webContents(win); target.replaceMisspelling(menuItem.label); }, }; } let dictionarySuggestions = []; if (hasText && properties.misspelledWord && properties.dictionarySuggestions.length > 0) { dictionarySuggestions = properties.dictionarySuggestions.map(suggestion => word(suggestion)); } else { dictionarySuggestions.push( { id: 'dictionarySuggestions', label: 'No Guesses Found', visible: Boolean(hasText && properties.misspelledWord), enabled: false, }, ); } let menuTemplate = [ dictionarySuggestions.length > 0 && defaultActions.separator(), ...dictionarySuggestions, defaultActions.separator(), options.showLearnSpelling !== false && defaultActions.learnSpelling(), defaultActions.separator(), options.showLookUpSelection !== false && defaultActions.lookUpSelection(), defaultActions.separator(), options.showSearchWithGoogle !== false && defaultActions.searchWithGoogle(), defaultActions.separator(), defaultActions.cut(), defaultActions.copy(), defaultActions.paste(), shouldShowSelectAll && defaultActions.selectAll(), defaultActions.separator(), options.showSaveImage && defaultActions.saveImage(), options.showSaveImageAs && defaultActions.saveImageAs(), options.showCopyImage !== false && defaultActions.copyImage(), options.showCopyImageAddress && defaultActions.copyImageAddress(), options.showSaveVideo && defaultActions.saveVideo(), options.showSaveVideoAs && defaultActions.saveVideoAs(), options.showCopyVideoAddress && defaultActions.copyVideoAddress(), defaultActions.separator(), options.showCopyLink !== false && defaultActions.copyLink(), options.showSaveLinkAs && defaultActions.saveLinkAs(), defaultActions.separator(), shouldShowInspectElement && defaultActions.inspect(), options.showServices && defaultActions.services(), defaultActions.separator(), ]; if (options.menu) { menuTemplate = options.menu(defaultActions, properties, win, dictionarySuggestions, event); } if (options.prepend) { const result = options.prepend(defaultActions, properties, win, event); if (Array.isArray(result)) { menuTemplate.unshift(...result); } } if (options.append) { const result = options.append(defaultActions, properties, win, event); 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 properties.selectionText === 'string' ? properties.selectionText.trim() : ''; menuItem.label = menuItem.label.replace('{selection}', cliTruncate(selectionString, 25).replaceAll('&', '&&')); } } if (menuTemplate.length > 0) { const menu = electron.Menu.buildFromTemplate(menuTemplate); if (typeof options.onShow === 'function') { menu.on('menu-will-show', options.onShow); } if (typeof options.onClose === 'function') { menu.on('menu-will-close', options.onClose); } menu.popup(win); } }; const currentWebContents = webContents(win); currentWebContents.on('context-menu', handleContextMenu); return () => { currentWebContents.removeListener('context-menu', handleContextMenu); }; }; export default function contextMenu(options = {}) { if (process.type === 'renderer') { throw new Error('Cannot use electron-context-menu in the renderer process!'); } let isDisposed = false; const disposables = []; const init = win => { if (isDisposed) { return; } const disposeMenu = create(win, options); const disposable = () => { disposeMenu(); }; webContents(win).once('destroyed', disposable); disposables.push(disposable); }; const dispose = () => { for (const dispose of disposables) { dispose(); } disposables.length = 0; isDisposed = true; }; 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) { const onDomReady = () => { init(win); }; const listenerFunction = win.addEventListener ?? win.addListener; listenerFunction('dom-ready', onDomReady, {once: true}); disposables.push(() => { win.removeEventListener('dom-ready', onDomReady, {once: true}); }); return dispose; } init(win); return dispose; } for (const win of electron.BrowserWindow.getAllWindows()) { init(win); } const onWindowCreated = (event, win) => { init(win); }; electron.app.on('browser-window-created', onWindowCreated); disposables.push(() => { electron.app.removeListener('browser-window-created', onWindowCreated); }); return dispose; }