diff --git a/electron_app/src/electron-main.js b/electron_app/src/electron-main.js index ab844bd3..26f8f972 100644 --- a/electron_app/src/electron-main.js +++ b/electron_app/src/electron-main.js @@ -24,11 +24,11 @@ const check_squirrel_hooks = require('./squirrelhooks'); if (check_squirrel_hooks()) return; const electron = require('electron'); -const url = require('url'); const tray = require('./tray'); const VectorMenu = require('./vectormenu'); +const webContentsHandler = require('./webcontents-handler'); const windowStateKeeper = require('electron-window-state'); @@ -42,60 +42,12 @@ try { // Continue with the defaults (ie. an empty config) } -const PERMITTED_URL_SCHEMES = [ - 'http:', - 'https:', - 'mailto:', -]; - const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000; const INITIAL_UPDATE_DELAY_MS = 30 * 1000; let mainWindow = null; let appQuitting = false; -function safeOpenURL(target) { - // openExternal passes the target to open/start/xdg-open, - // so put fairly stringent limits on what can be opened - // (for instance, open /bin/sh does indeed open a terminal - // with a shell, albeit with no arguments) - const parsed_url = url.parse(target); - if (PERMITTED_URL_SCHEMES.indexOf(parsed_url.protocol) > -1) { - // explicitly use the URL re-assembled by the url library, - // so we know the url parser has understood all the parts - // of the input string - const new_target = url.format(parsed_url); - electron.shell.openExternal(new_target); - } -} - -function onWindowOrNavigate(ev, target) { - // always prevent the default: if something goes wrong, - // we don't want to end up opening it in the electron - // app, as we could end up opening any sort of random - // url in a window that has node scripting access. - ev.preventDefault(); - safeOpenURL(target); -} - -function onLinkContextMenu(ev, params) { - const popup_menu = new electron.Menu(); - popup_menu.append(new electron.MenuItem({ - label: params.linkURL, - click() { - safeOpenURL(params.linkURL); - }, - })); - popup_menu.append(new electron.MenuItem({ - label: 'Copy Link Address', - click() { - electron.clipboard.writeText(params.linkURL); - }, - })); - popup_menu.popup(); - ev.preventDefault(); -} - function installUpdate() { // for some reason, quitAndInstall does not fire the // before-quit event, so we need to set the flag here. @@ -259,15 +211,7 @@ electron.app.on('ready', () => { } }); - mainWindow.webContents.on('new-window', onWindowOrNavigate); - mainWindow.webContents.on('will-navigate', onWindowOrNavigate); - - mainWindow.webContents.on('context-menu', function(ev, params) { - if (params.linkURL) { - onLinkContextMenu(ev, params); - } - }); - + webContentsHandler(mainWindow.webContents); mainWindowState.manage(mainWindow); }); diff --git a/electron_app/src/webcontents-handler.js b/electron_app/src/webcontents-handler.js new file mode 100644 index 00000000..37416ebe --- /dev/null +++ b/electron_app/src/webcontents-handler.js @@ -0,0 +1,122 @@ +const {clipboard, nativeImage, Menu, MenuItem, shell} = require('electron'); +const url = require('url'); + +const PERMITTED_URL_SCHEMES = [ + 'http:', + 'https:', + 'mailto:', +]; + +function safeOpenURL(target) { + // openExternal passes the target to open/start/xdg-open, + // so put fairly stringent limits on what can be opened + // (for instance, open /bin/sh does indeed open a terminal + // with a shell, albeit with no arguments) + const parsedUrl = url.parse(target); + if (PERMITTED_URL_SCHEMES.indexOf(parsedUrl.protocol) > -1) { + // explicitly use the URL re-assembled by the url library, + // so we know the url parser has understood all the parts + // of the input string + const newTarget = url.format(parsedUrl); + shell.openExternal(newTarget); + } +} + +function onWindowOrNavigate(ev, target) { + // always prevent the default: if something goes wrong, + // we don't want to end up opening it in the electron + // app, as we could end up opening any sort of random + // url in a window that has node scripting access. + ev.preventDefault(); + safeOpenURL(target); +} + +function onLinkContextMenu(ev, params) { + const url = params.linkURL || params.srcURL; + + const popupMenu = new Menu(); + popupMenu.append(new MenuItem({ + label: url, + click() { + safeOpenURL(url); + }, + })); + + if (params.mediaType && params.mediaType === 'image' && !url.startsWith('file://')) { + popupMenu.append(new MenuItem({ + label: 'Copy Image', + click() { + if (url.startsWith('data:')) { + clipboard.writeImage(nativeImage.createFromDataURL(url)); + } else { + ev.sender.copyImageAt(params.x, params.y); + } + }, + })); + } + + popupMenu.append(new MenuItem({ + label: 'Copy Link Address', + click() { + clipboard.writeText(url); + }, + })); + popupMenu.popup(); + ev.preventDefault(); +} + +function _CutCopyPasteSelectContextMenus(params) { + return [{ + role: 'cut', + enabled: params.editFlags.canCut, + }, { + role: 'copy', + enabled: params.editFlags.canCopy, + }, { + role: 'paste', + enabled: params.editFlags.canPaste, + }, { + role: 'pasteandmatchstyle', + enabled: params.editFlags.canPaste, + }, { + role: 'selectall', + enabled: params.editFlags.canSelectAll, + }]; +} + +function onSelectedContextMenu(ev, params) { + const items = _CutCopyPasteSelectContextMenus(params); + const popupMenu = Menu.buildFromTemplate(items); + + popupMenu.popup(); + ev.preventDefault(); +} + +function onEditableContextMenu(ev, params) { + const items = [ + { role: 'undo' }, + { role: 'redo', enabled: params.editFlags.canRedo }, + { type: 'separator' }, + ].concat(_CutCopyPasteSelectContextMenus(params)); + + const popupMenu = Menu.buildFromTemplate(items); + + popupMenu.popup(); + ev.preventDefault(); +} + + +module.exports = (webContents) => { + webContents.on('new-window', onWindowOrNavigate); + webContents.on('will-navigate', onWindowOrNavigate); + + webContents.on('context-menu', function(ev, params) { + if (params.linkURL || params.srcURL) { + onLinkContextMenu(ev, params); + } else if (params.selectionText) { + onSelectedContextMenu(ev, params); + } else if (params.isEditable) { + onEditableContextMenu(ev, params); + } + }); +};