From d602b3c4c6aafa76fcfe2b21d2c86990b2f562e5 Mon Sep 17 00:00:00 2001 From: Anthony Tseng Date: Wed, 26 Oct 2016 19:01:35 -0400 Subject: [PATCH] Add chrome.contextMenus.create and chrome.contextMenus.removeAll fix #4689 requires https://github.com/brave/electron/pull/82 Auditors: @bridiver, @jonathansampson Test Plan: n/a --- app/browser/extensions/contextMenus.js | 18 +++++++ app/common/actions/extensionActions.js | 44 +++++++++++++++ app/common/constants/extensionConstants.js | 5 +- app/common/state/extensionState.js | 45 ++++++++++++++++ app/extensions.js | 2 + docs/state.md | 5 ++ js/contextMenus.js | 63 ++++++++++++++++++++++ js/stores/appStore.js | 10 ++++ 8 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 app/browser/extensions/contextMenus.js diff --git a/app/browser/extensions/contextMenus.js b/app/browser/extensions/contextMenus.js new file mode 100644 index 00000000000..bb79d311e57 --- /dev/null +++ b/app/browser/extensions/contextMenus.js @@ -0,0 +1,18 @@ +const extensionActions = require('../../common/actions/extensionActions') + +const contextMenus = { + init: () => { + process.on('chrome-context-menus-remove-all', (extensionId) => { + setImmediate(() => { + extensionActions.contextMenuAllRemoved(extensionId) + }) + }) + process.on('chrome-context-menus-create', (extensionId, menuItemId, properties) => { + setImmediate(() => { + extensionActions.contextMenuCreated(extensionId, menuItemId, properties) + }) + }) + } +} + +module.exports = contextMenus diff --git a/app/common/actions/extensionActions.js b/app/common/actions/extensionActions.js index 9f65b6e34bb..3aac2b2e6b7 100644 --- a/app/common/actions/extensionActions.js +++ b/app/common/actions/extensionActions.js @@ -77,6 +77,50 @@ const extensionActions = { actionType: ExtensionConstants.EXTENSION_DISABLED, extensionId }) + }, + + /** + * Dispatched when an extension has created item in context menu + * + * @param {string} extensionId - the extension id + * @param {string} menuItemId - the id of the menu item that was clicked + * @param {object} properties - createProperties of chrome.contextMenus.create + */ + contextMenuCreated: function (extensionId, menuItemId, properties) { + AppDispatcher.dispatch({ + actionType: ExtensionConstants.CONTEXT_MENU_CREATED, + extensionId, + menuItemId, + properties + }) + }, + + /** + * Dispatched when an extension has removed all item in context menu + * + * @param {string} extensionId - the extension id + */ + contextMenuAllRemoved: function (extensionId) { + AppDispatcher.dispatch({ + actionType: ExtensionConstants.CONTEXT_MENU_ALL_REMOVED, + extensionId + }) + }, + + /** + * Dispatched when an menu item created by extension is clicked + * + * @param {string} extensionId - the extension id + * @param {string} tabId - the tab id + * @param {object} info - the arg of onclick callback + */ + contextMenuClicked: function (extensionId, tabId, info) { + AppDispatcher.dispatch({ + actionType: ExtensionConstants.CONTEXT_MENU_CLICKED, + extensionId, + tabId, + info + }) } } diff --git a/app/common/constants/extensionConstants.js b/app/common/constants/extensionConstants.js index 64e7f18ae80..fc5c2c3a515 100644 --- a/app/common/constants/extensionConstants.js +++ b/app/common/constants/extensionConstants.js @@ -11,7 +11,10 @@ const ExtensionConstants = { EXTENSION_INSTALLED: _, EXTENSION_UNINSTALLED: _, EXTENSION_ENABLED: _, - EXTENSION_DISABLED: _ + EXTENSION_DISABLED: _, + CONTEXT_MENU_CREATED: _, + CONTEXT_MENU_ALL_REMOVED: _, + CONTEXT_MENU_CLICKED: _ } module.exports = mapValuesByKeys(ExtensionConstants) diff --git a/app/common/state/extensionState.js b/app/common/state/extensionState.js index 066b74fc19d..7f34f3c969e 100644 --- a/app/common/state/extensionState.js +++ b/app/common/state/extensionState.js @@ -124,6 +124,51 @@ const extensionState = { extension = extension.delete(field) }) return extension + }, + + contextMenuCreated: (state, action) => { + action = makeImmutable(action) + state = makeImmutable(state) + let extensionId = action.get('extensionId').toString() + let extension = extensionState.getExtensionById(state, extensionId) + if (extension) { + if (state.getIn(['extensions', action.get('extensionId'), 'contextMenus']) === undefined) { + state = state.setIn(['extensions', action.get('extensionId'), 'contextMenus'], new Immutable.List()) + } + let contextMenus = state.getIn(['extensions', action.get('extensionId'), 'contextMenus']) + return state.setIn(['extensions', action.get('extensionId'), 'contextMenus'], + contextMenus.push({ + extensionId: action.get('extensionId'), + menuItemId: action.get('menuItemId'), + properties: action.get('properties').toJS() + })) + } else { + return state + } + }, + + contextMenuAllRemoved: (state, action) => { + action = makeImmutable(action) + state = makeImmutable(state) + let extensionId = action.get('extensionId').toString() + let extension = extensionState.getExtensionById(state, extensionId) + if (extension) { + return state.deleteIn(['extensions', action.get('extensionId'), 'contextMenus']) + } else { + return state + } + }, + + getContextMenusProperties: (state) => { + let allProperties = [] + let extensions = extensionState.getEnabledExtensions(state) + extensions && extensions.forEach((extension) => { + let contextMenus = extension.get('contextMenus') + contextMenus && contextMenus.forEach((contextMenu) => { + allProperties.push(contextMenu.toJS()) + }) + }) + return allProperties } } diff --git a/app/extensions.js b/app/extensions.js index 57876ab1d34..3bf4bc3e47a 100644 --- a/app/extensions.js +++ b/app/extensions.js @@ -1,4 +1,5 @@ const browserActions = require('./browser/extensions/browserActions') +const contextMenus = require('./browser/extensions/contextMenus') const extensionActions = require('./common/actions/extensionActions') const config = require('../js/constants/config') const appConfig = require('../js/constants/appConfig') @@ -184,6 +185,7 @@ const isWidevine = (componentId) => module.exports.init = () => { browserActions.init() + contextMenus.init() const {componentUpdater, session} = require('electron') componentUpdater.on('component-checking-for-updates', () => { diff --git a/docs/state.md b/docs/state.md index 03f842653f0..6d043ca3247 100644 --- a/docs/state.md +++ b/docs/state.md @@ -26,6 +26,11 @@ AppStore [tabId]: { browserAction: {} // tab specific browser action properties } + }, + contextMenus: { + extensionId: string, + menuItemId: string, + properties: object } } }, diff --git a/js/contextMenus.js b/js/contextMenus.js index 2bd3d0a390a..673122f4ef0 100644 --- a/js/contextMenus.js +++ b/js/contextMenus.js @@ -35,6 +35,9 @@ const eventUtil = require('./lib/eventUtil') const currentWindow = require('../app/renderer/currentWindow') const config = require('./constants/config') const bookmarksToolbarMode = require('../app/common/constants/bookmarksToolbarMode') +const extensionState = require('../app/common/state/extensionState') +const extensionActions = require('../app/common/actions/extensionActions') +const appStore = require('./stores/appStoreRenderer') const isDarwin = process.platform === 'darwin' @@ -886,6 +889,8 @@ function mainTemplateInit (nodeProps, frame) { const isLink = nodeProps.linkURL && nodeProps.linkURL !== '' const isImage = nodeProps.mediaType === 'image' + const isVideo = nodeProps.mediaType === 'video' + const isAudio = nodeProps.mediaType === 'audio' const isInputField = nodeProps.isEditable || nodeProps.inputFieldType !== 'none' const isTextSelected = nodeProps.selectionText.length > 0 @@ -1099,6 +1104,64 @@ function mainTemplateInit (nodeProps, frame) { }) } + const extensionContextMenus = + extensionState.getContextMenusProperties(appStore.state) + if (extensionContextMenus.length) { + template.push(CommonMenu.separatorMenuItem) + extensionContextMenus.forEach((extensionContextMenu) => { + let info = {} + let contextsPassed = false + extensionContextMenu.properties.contexts.forEach((context) => { + if (isTextSelected && (context === 'selection' || context === 'all')) { + info['selectionText'] = nodeProps.selectionText + contextsPassed = true + } else if (isLink && (context === 'link' || context === 'all')) { + info['linkUrl'] = nodeProps.linkURL + contextsPassed = true + } else if (isImage && (context === 'image' || context === 'all')) { + info['mediaType'] = 'image' + contextsPassed = true + } else if (isInputField && (context === 'editable' || context === 'all')) { + info['editable'] = true + contextsPassed = true + } else if (nodeProps.pageURL && (context === 'page' || context === 'all')) { + info['pageUrl'] = nodeProps.pageURL + contextsPassed = true + } else if (isVideo && (context === 'video' || context === 'all')) { + info['mediaType'] = 'video' + contextsPassed = true + } else if (isAudio && (context === 'audio' || context === 'all')) { + info['mediaType'] = 'audio' + contextsPassed = true + } else if (nodeProps.frameURL && (context === 'frame' || context === 'all')) { + info['frameURL'] = nodeProps.frameURL + contextsPassed = true + } + }) + if (nodeProps.srcURL) { + info['srcURL'] = nodeProps.srcURL + } + // TODO (Anthony): Browser Action context menu + if (extensionContextMenu.properties.contexts.length === 1 && + extensionContextMenu.properties.contexts[0] === 'browser_action') { + contextsPassed = false + } + if (contextsPassed) { + info['menuItemId'] = extensionContextMenu.menuItemId + template.push( + { + label: extensionContextMenu.properties.title, + click: (item, focusedWindow) => { + if (focusedWindow) { + extensionActions.contextMenuClicked( + extensionContextMenu.extensionId, frame.get('tabId'), info) + } + } + }) + } + }) + } + if (frame.get('location') === 'about:bookmarks') { template.push( CommonMenu.separatorMenuItem, diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 9599c5a4388..dcc59509f5a 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -696,6 +696,16 @@ const handleAppAction = (action) => { case ExtensionConstants.EXTENSION_DISABLED: appState = extensionState.extensionDisabled(appState, action) break + case ExtensionConstants.CONTEXT_MENU_CREATED: + appState = extensionState.contextMenuCreated(appState, action) + break + case ExtensionConstants.CONTEXT_MENU_ALL_REMOVED: + appState = extensionState.contextMenuAllRemoved(appState, action) + break + case ExtensionConstants.CONTEXT_MENU_CLICKED: + process.emit('chrome-context-menus-clicked', + action.extensionId, action.tabId, action.info.toJS()) + break case AppConstants.APP_SET_MENUBAR_TEMPLATE: appState = appState.setIn(['menu', 'template'], action.menubarTemplate) break