Dynamic Extension Context Menu That Depends on Selected Text

Dynamic extension context menu that depends on selected text

The contextMenus API is used to define context menu entries. It does not need to be called right before a context menu is opened. So, instead of creating the entries on the contextmenu event, use the selectionchange event to continuously update the contextmenu entry.

I will show a simple example which just displays the selected text in the context menu entry, to show that the entries are synchronized well.

Use this content script:

document.addEventListener('selectionchange', function() {
var selection = window.getSelection().toString().trim();
chrome.runtime.sendMessage({
request: 'updateContextMenu',
selection: selection
});
});

At the background, we're going to create the contextmenu entry only once. After that, we update the contextmenu item (using the ID which we get from chrome.contextMenus.create).

When the selection is empty, we remove the context menu entry if needed.

// ID to manage the context menu entry
var cmid;
var cm_clickHandler = function(clickData, tab) {
alert('Selected ' + clickData.selectionText + ' in ' + tab.url);
};

chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
if (msg.request === 'updateContextMenu') {
var type = msg.selection;
if (type == '') {
// Remove the context menu entry
if (cmid != null) {
chrome.contextMenus.remove(cmid);
cmid = null; // Invalidate entry now to avoid race conditions
} // else: No contextmenu ID, so nothing to remove
} else { // Add/update context menu entry
var options = {
title: type,
contexts: ['selection'],
onclick: cm_clickHandler
};
if (cmid != null) {
chrome.contextMenus.update(cmid, options);
} else {
// Create new menu, and remember the ID
cmid = chrome.contextMenus.create(options);
}
}
}
});

To keep this example simple, I assumed that there's only one context menu entry. If you want to support more entries, create an array or hash to store the IDs.

Tips

  • Optimization - To reduce the number of chrome.contextMenus API calls, cache the relevant values of the parameters. Then, use a simple === comparison to check whether the contextMenu item need to be created/updated.
  • Debugging - All chrome.contextMenus methods are asynchronous. To debug your code, pass a callback function to the .create, .remove or .update methods.

How do I restrict context menus to appear only for certain selected text in a Chrome extension?

You would need to control content menu creation from a content script. Dynamic menu creation/deletion should execute pretty fast and the delay will be unnoticeable for a user.

  • Add mousedown event listener in a content script and check there whether selection satisfies your pattern.
  • Based on whether or not selection matches the patterrn, send a request to a background page asking to create or delete the menu.

Something along those lines (not tested):

content_script.js:

document.addEventListener("mousedown", function(event){
var selection = window.getSelection().toString();
if(selection.match(/^10\./)) {
chrome.extension.sendRequest({cmd: "create_menu"});
} else {
chrome.extension.sendRequest({cmd: "delete_menu"});
}
}, true);

background.html:

chrome.extension.onRequest.addListener(function(request) {
if(request.cmd == "create_menu") {
chrome.contextMenus.removeAll(function() {
chrome.contextMenus.create({
"title" : "Resolve DOI",
"type" : "normal",
"contexts" : ["selection"],
"onclick" : getClickHandler()
});
});
} else if(request.cmd == "delete_menu") {
chrome.contextMenus.removeAll();
}
});

Chrome Extension contextMenus.create title property validator?

You pass a literal string "%s" to validateSelection, so parseInt("%s") is NaN which is why the else branch is always executed. This is an immediately executing function which effectively returns Result: x > 5; (x = %s) and this string is used to create the menu item ONE TIME. Then every time you invoke the menu Chrome simply expands %s when the menu is displayed. It doesn't and can't pass this %s AGAIN into validateSelection because it wasn't a callback, it was an immeditely executed function as per javascript syntax.

The context menu title is not dynamic and there is no function callback parameter for the title.

To change it dynamically define a content script that will be injected on <all_urls> with a selectionchanged event listener that will change the context menu title before it's shown.

content script:

document.addEventListener("selectionchange", function(e) {
chrome.runtime.sendMessage({selection: getSelection().toString()});
});

background script:

chrome.contextMenus.create({id: "menuSel", title: "", contexts: ["selection"]});

chrome.contextMenus.onClicked.addListener(function(info, tab) {
.......
});

chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
if ("selection" in msg) {
chrome.contextMenus.update("menuSel", {title: validateSelection(msg.selection)});
}
});

The pitfall is that if you select something in one tab without invoking the context menu, then switch to another tab and select something, then switch back and immediately rightclick the already selected text, the result will be wrong. This can be handled, though, in a chrome.tabs.onActivated listener in the background script (may require "permissions: ["tabs"] in manifest.json):

chrome.tabs.onActivated.addListener(function(info) {
chrome.tabs.executeScript(info.tabId, {code: "getSelection().toString()"},
function(results) {
chrome.contextMenus.update("menuSel", {title: validateSelection(results[0])});
}
);
});

If you want to make it work on frames/iframes, use allFrames: true parameter in executeScript and determine which frame is active.

Render context menu depending on selection

Selection type changes context menu using chrome extension

You will have to set a listener for mouse down. There is no other way to get the selected text before the menu is created.

See this SO question:

chrome extension context menus, how to display a menu item only when there is no selection?

Here is part of the code the rest is at the link.

document.addEventListener("mousedown", function(event){
//right click
if(event.button == 2) {
if(window.getSelection().toString()) {
chrome.extension.sendRequest({cmd: "createSelectionMenu"});
} else {
chrome.extension.sendRequest({cmd: "createRegularMenu"});
}
}
}, true);

Change context menu text based on which element is clicked

No, in a Chrome extension/Firefox WebExtension, there is no way to have your JavaScript make dynamic changes to the context menu at the time the context menu opens. The only thing that dynamically changes at the time the context menu opens is: if you have added a context menu item that has the selection context for its ContextType, then you can specify %s in the title, which will be replaced with the current selection.

Change the context menu before it begins to open

Mouse events

To do what you desire, you need to make the changes to the context menu prior to the beginning of the context menu being opened. This can be accomplished by having a content script that listens to the mouseup event. You may have to end up using the mousedown event, or one or more of the mouseenter/mouseleave/mouseover/mouseout events. If you do need to use mousedown, once that event fires you should then start listening to some of the mouseenter/mouseleave/mouseover/mouseout events to tell when the element the mouse is over changes, or just assume the user releases the button on the same element on which they pressed it down.

If the mouseup/mousedown event is for button=2, then you will need to message your background script to change the context menu with contextMenus.update(). There are multiple asynchronous portions of this process. This may make for various race conditions, and may necessitate using events which give you earlier notification than mouseup (e.g. mousedown, mouseenter/mouseleave/mouseover/mouseout).

What events you need to watch for may be operating system/platform/windowing system dependent. You will need to test to determine what is needed in each environment you plan to support.

Keyboard events

The context menu can also be opened using a few keyboard sequences. You will need to listen for and perform the same messaging as is needed for mouse events. You will need to determine what these events are in all operating systems/platforms/windowing systems which you are supporting.

Linux

On Linux it appears to vary at least based on platform.

OSX

On OSX, it appears that a keyboard shortcut is only available if enabled.

Windows

On Windows, the keyboard shortcuts to open the context menu are:

Context Menu key

You will need to detect the following event sequence:

keydown { target: <body>, key: "ContextMenu", charCode: 0, keyCode: 93 }
keypress { target: <body>, key: "ContextMenu", charCode: 0, keyCode: 93 }
keyup { target: <body>, key: "ContextMenu", charCode: 0, keyCode: 93 }

The context menu does not open until around the same time the keyup event fires. I did not test to see if the keyup happens early enough for you to make changes to the context menu prior to it opening.

Keyboard combination: Shift-F10

You will need to detect the following event sequence:

keydown Shift { target: <body>, key: "Shift", charCode: 0, keyCode: 16, shiftKey: true }
keydown Shift { target: <body>, key: "F10", charCode: 0, keyCode: 121, shiftKey: true }
keypress Shift { target: <body>, key: "F10", charCode: 0, keyCode: 121, shiftKey: true }

In this case, the context menu opens at around the same time as the shifted F10 keydown/keypress events fire. No keyup event fires for this keyboard sequence.



Related Topics



Leave a reply



Submit