In this related answer, I explained that context menu items cannot be created on the fly, because the time between a contextmenu
event and the appearance of the context menu item is not sufficient to get a chrome.contextMenus.create
call in between.
The other answer explains how to make sure that the context menu entry shows the selected text. This was done by listening to the selectionchange
event. For your purpose, we want to use an event which has the desired timing.
I'm going to use the mouseover
and mouseout
events. By depending on mouse events, the context menu will not work when you use the keyboard, e.g. by focusing an element using JavaScript or the Tab key, followed by pressing the context menu key. I did not implement it in the solution below.
The demo consists of three files (plus an HTML page to test it). I put all files in a zip file, available at https://robwu.nl/contextmenu-dom.zip.
manifest.json
Every Chrome extension requires this file in order to work. For this application, a background page and content script is used. In addition, the contextMenus
permission is required.
{
"name": "Contextmenu based on activated element",
"description": "Demo for https://stackoverflow.com/q/14829677",
"version": "1",
"manifest_version": 2,
"background": {
"scripts": ["background.js"]
},
"content_scripts": [{
"run_at": "document_idle",
"js": ["contentscript.js"],
"matches": ["<all_urls>"]
}],
"permissions": [
"contextMenus"
]
}
background.js
The background page will create the context menus on behalf of the content script. This is achieved by binding an event listener to chrome.runtime.onConnect
, which offers a simple interface to replace context menu entries.
chrome.contextMenus.create
is called whenever a message is received over this port (from the content script). All properties, except for onclick
are JSON-serializable, so only the "onclick" handler needs a special treatment. I've used a string to identify a pre-defined function in a dictionary (var clickHandlers
).
var lastTabId;
// Remove context menus for a given tab, if needed
function removeContextMenus(tabId) {
if (lastTabId === tabId) chrome.contextMenus.removeAll();
}
// chrome.contextMenus onclick handlers:
var clickHandlers = {
'example': function(info, tab) {
// This event handler receives two arguments, as defined at
// https://developer.chrome.com/extensions/contextMenus#property-onClicked-callback
// Example: Notify the tab's content script of something
// chrome.tabs.sendMessage(tab.id, ...some JSON-serializable data... );
// Example: Remove contextmenus for context
removeContextMenus(tab.id);
}
};
chrome.runtime.onConnect.addListener(function(port) {
if (!port.sender.tab || port.name != 'contextMenus') {
// Unexpected / unknown port, do not interfere with it
return;
}
var tabId = port.sender.tab.id;
port.onDisconnect.addListener(function() {
removeContextMenus(tabId);
});
// Whenever a message is posted, expect that it's identical to type
// createProperties of chrome.contextMenus.create, except for onclick.
// "onclick" should be a string which maps to a predefined function
port.onMessage.addListener(function(newEntries) {
chrome.contextMenus.removeAll(function() {
for (var i=0; i<newEntries.length; i++) {
var createProperties = newEntries[i];
createProperties.onclick = clickHandlers[createProperties.onclick];
chrome.contextMenus.create(createProperties);
}
});
});
});
// When a tab is removed, check if it added any context menu entries. If so, remove it
chrome.tabs.onRemoved.addListener(removeContextMenus);
contentscript.js
The first part of this script creates simple methods for creating context menus.
In the last 5 lines, the context menu entries are defined and bound to all current and future elements which match the given selector. Like I said in the previous section, the argument type is identical to the createProperties
argument of chrome.contextMenus.create
except for "onclick", which is a string which maps to a function in the background page.
// Port management
var _port;
var getPort = function() {
if (_port) return _port;
_port = chrome.runtime.connect({name: 'contextMenus'});
_port.onDisconnect.addListener(function() {
_port = null;
});
return _port;
}
// listOfCreateProperties is an array of createProperties, which is defined at
// https://developer.chrome.com/extensions/contextMenus#method-create
// with a single exception: "onclick" is a string which corresponds to a function
// at the background page. (Functions are not JSON-serializable, hence this approach)
function addContextMenuTo(selector, listOfCreateProperties) {
// Selector used to match an element. Match if an element, or its child is hovered
selector = selector + ', ' + selector + ' *';
var matches;
['matches', 'webkitMatchesSelector', 'webkitMatches', 'matchesSelector'].some(function(m) {
if (m in document.documentElement) {
matches = m;
return true;
}
});
// Bind a single mouseover+mouseout event to catch hovers over all current and future elements.
var isHovering = false;
document.addEventListener('mouseover', function(event) {
if (event.target && event.target[matches](selector)) {
getPort().postMessage(listOfCreateProperties);
isHovering = true;
} else if(isHovering) {
getPort().postMessage([]);
isHovering = false;
}
});
document.addEventListener('mouseout', function(event) {
if (isHovering && (!event.target || !event.target[matches](selector))) {
getPort().postMessage([]);
isHovering = false;
}
});
}
// Example: Bind the context menus to the elements which contain a class attribute starts with "story"
addContextMenuTo('[class^=story]', [
{"id": "butto1", "title": "1", "contexts":["all"], "onclick": 'example'},
{"id": "button2", "title": "2", "contexts":["all"], "onclick": 'example'},
{"id": "button3", "title": "3", "contexts":["all"], "onclick": 'example'}
]);
The previous code assumes that all context menu clicks are handled by the background page. If you want to handle the logic in the content script instead, you need to bind message events in the content script. I've shown an (commented) instance of chrome.tabs.sendMessage
in the background.js
example, to show where this event should be triggered.
If you need to identify which element triggered the event, don't use a predefined function (in a dictionary) as shown in my example, but an inline function or a factory function. To identify the element, a message needs to be paired with an unique identifier. I'll leave the task of creating this implementation to the reader (it's not difficult).