A Bittersweet Life A Bittersweet Life - 5 months ago 18
Javascript Question

Add menu item created with the sdk/context-menu API to the top of the context menu

I'm working on a Mozilla Add-on SDK extension that provides a right-click context menu option using the

API and performs some actions on a particular website. Everything works fine except that the context menu item is added to the bottom of the list. I want it to be the first item in the context menu. I searched for a while for how to do this, but no luck.

Here is the code I use to create he context menu item (modified to obfuscate the site on which it is being used):

var cm = require("sdk/context-menu");
cm.Item({
label: "AddonName(d)",
context: [
cm.URLContext(["*.somesite.com"]),
cm.SelectorContext("a[href]")
],
contentScript: 'self.on("click", function (node, data) {' +
' self.postMessage(node.href);' +
'});',
accessKey: "d",
onMessage: function (src) {
//code
}
});

Answer

Using the Add-on SDK sdk/context-menu API, there is no way to directly add an item to anywhere but the bottom of the context menu. There is no convenient way to have it end up at the top. However, you can add it and then move it to the top, or elsewhere in the context menu, if desired. But, the Add-on SDK does not have a way for you to do so through any of the Add-on SDK APIs.

Note: Moving your context menu item to the top of the menu changes what is normally done for context menu items that are added by Firefox add-ons. If I were to load an add-on that assumed its context menu item belonged on the top of the list, I would not be pleased. If it did not, at a minimum, provide the option (simple-prefs) to have the context menu item at the bottom, or a place of my choosing, I might very well write a negative review on AMO. Placing it at the top of the context menu assumes that your add-on will be the most used option in that context menu. This being the case will significantly depend on the user, your add-on and how the user makes use of your add-on. On the other hand, if this was an option I used all the time, I would be glad to see it at the top, and separated from other menu items.

One way to have your context menu item appear at the top:

Your current code results in a context menu for links with the added entry, AddonName(d), at the bottom which looks like:
original look of link context menu after addition on bottom

The context menus are part of the XUL DOM which exists for each Firefox Window. It appears that the Add-on SDK sdk/context-menu only permits you to add items to the context menu for elements within the page content area. Thus, in the XUL DOM the context menu being affected is the menupopup that has id="contentAreaContextMenu": enter image description here

Once the menuitem for your context menu item exists, you can manipulate the XUL DOM to move it to be the firstChild which will put it at the top of the context menu. However, the SDK does not insert the menuitem for your context menu item into the XUL DOM until the first time that it will be displayed (or at least not until the first time it might be displayed). Thus, you can not just call cm.Item() and then immediately change the XUL DOM.

Further complicating this is that the XUL DOM manipulation must occur in your main code while you can only be informed that the context menu is about to be displayed (and thus your menuitem is in the XUL DOM) from a content script. We must therefore listen for the context event and pass a message to our main script to initiate the XUL DOM manipulation of the context menu.

Keep in mind that the object that is returned by let cmItem = cm.Item() is not actually a reference to the menuitem in the XUL DOM. The reason for this is that the menuitem in the XUL DOM exists as a separate object in the XUL DOM for each Firefox window. Thus, we have to search through the XUL DOM to find the correct menuitem element. Because it is a separate XUL DOM for each Firefox window we need to make sure that it happens in all windows. In this case, listening for the context event in a content script results in this change taking place each time the context menu is displayed. As a result, we do not need to specifically make the change in each Firefox window because we make the change every single time the context menu is displayed.

Adding additional complication to searching through the XUL DOM for the correct menuitem is that the Add-on SDK does not provide an id property which is applied to the menuitem created in the XUL DOM. Thus, we have to find the item based on the value of the label property. This means that you either need to not change that property, or track the changes so you are searching for the correct menuitem. In the example code below, it is assumed that the label does not change.

We now need to use the message passed from the content script for both messages indicating that the context menu is about to be displayed and to send the node.href when the context menu item has been clicked. In order to do that, what is passed is now an object with a type property indicating the type of message being passed (a click, or a context) and the data from the click.

NOTE: The Add-on SDK also adds a menuseparator in the context menu above those menu items added by the Add-on SDK. It may be that a separator is created between each context menu item that is added, or between all those added by each Add-on SDK extension. However, in brief testing it appeared to be that only one separator is added even for multiple Add-on SDK extensions. To make this look clean, you many want to add one to the top of the context menu after your entry. If you do add one, you will need to make sure you remove it upon your add-on being disabled. Moving this separator would be making the assumption that you are the only Add-on SDK extension adding to any context menu. I have not manipulated this menuseparator in the example code below.

The following code will move the single menuitem created with cm.Item() from wherever it is in the context menu to the top of the context menu:

let cm = require("sdk/context-menu");
let cmLabel = "AddonName(d)";
cm.Item({
    label: cmLabel,
    context: [ cm.SelectorContext("a[href]") ],
    contentScript: 'self.on("click", function (node, data) {' +
               '  self.postMessage({type:"click", data:node.href});' +
               '});' +
               'self.on("context", function (node) {' +
               '  self.postMessage({type:"context"});' +
               '  return true;' +
               '});',
    accessKey: "d",
    onMessage: function (message) {
        if(message.type === "click") {
            console.log("context menu selected on:" + message.data);
        } else if (message.type === "context") {
            adjustContextMenuOrder(cmLabel);
        }
     }
});

function adjustContextMenuOrder(label) {
    curWindow = require('sdk/window/utils').getMostRecentBrowserWindow();
    let contextMenulist = curWindow.document.getElementById("contentAreaContextMenu");
    //Get a list of elements with a matching label property.
    let itemList = contextMenulist.querySelectorAll('[label="' + label + '"]');
    menuitem = itemList[0]; //Assume the first one found is the one we want.
    if(menuitem === undefined) {
        return; //Did not find the menuitem.
    }
    let parent = menuitem.parentNode;
    parent.insertBefore(menuitem, parent.firstChild); //Move found element to the top.
}

The above code results in a context menu for links which looks like:
context menu moved to top

Note: Some changes were made to the code in the question for testing or to verify functionality. The context was simplified to make testing easier (removed the restriction to only display on some matching URLs). In addition, the node.href data is used in a console.log(), to verify functionality.