Labs/Jetpack/Reboot/JEP/112

From MozillaWiki
< Labs‎ | Jetpack‎ | Reboot‎ | JEP
Jump to: navigation, search

JEP 112 - Context Menu

  • Champion: Drew Willcoxon - adw at mozilla.com
  • Status: Accepted/Initial implementation introduced in Jetpack SDK 0.3
  • Bug Ticket: bug 548590
  • Type: API

Proposal

Provide access to the page's context menu through a module named context-menu.

This proposal recognizes the following principles:

  1. The domain of browser extensions is not a single page but the set of all the user's pages.
  2. The page's context menu is for UI related to the page and its content. It should not be used for contexts external to the page. It should not be used simply because it's handy.

Multiple extensions may simultaneously modify the context menu, but their modifications should not conflict. For example, if two extensions both replace a menuitem M, the net effect should be that M is removed from the menu and both extensions' items are present in place of M. Modifications to the menu are always performed on the unmodified menu and then merged to form the modified menu.

We mention briefly that HTML5 includes a specification for menus, but no browser implements it yet, and its scope is not that of browser extensions anyway.

Use Cases

  • Adding an item that acts on a portion of the page -- a node, selection, etc. Examples:
    • An image-editing extension adds an "Edit Image" item when the context menu is invoked on an image.
    • A search extension adds a "Search" item when the context menu is invoked while a selection exists.
    • An extension adds an "Open Text Link" item when the selection contains a URL outside of an anchor.
    • A DOM inspection extension adds an "Inspect This Node" item when the context menu is invoked anywhere in the page.
  • Adding an item that acts on the entire page. Examples:
    • An extension that makes it easy to edit page source adds an "Edit Page Source" item when the context menu is invoked anywhere on the page.
    • An image-editing extension adds an "Edit Page Images" item when the context menu is invoked anywhere on a page that has at least one image.

Non-Use Cases

  • Adding an item unrelated to the page and its content. Examples:
    • A tab-related extension adds an item that provides access to all the user's tabs.
    • An extension adds an item when it's three o'clock.
  • Radio and checkbox items.
  • The ability to modify the menu just before it's shown, and the ability to modify individual items already created by the extension.
  • The ability to modify existing items other than to replace them with new items.

Dependencies & Requirements

  • Access to content pages and their DOMs.
  • Access to the browser's DOM, including popupshowing and popuphiding events.
  • Menuitem icons should be package-able in XPIs, reference-able from code.

API Methods

add

add(menuitem)

Adds an item to the context menu. The position at which the item is inserted is at the discretion of Jetpack.

menuitem
The menuitem to add. See #Creating New Menuitems. Separators, however, can't be added.

insertBefore

This part of the proposal is under review.

insertBefore(target, menuitem)

Inserts a new item before an existing one. If the target item does not exist, this function silently fails, and the context menu not modified.

target
A value describing the existing item before which to insert the new item. See #Specifying Existing Menuitems.
menuitem
The menuitem to add. See #Creating New Menuitems.

remove

remove(item)

Permanently removes an item previously added to the context menu. This method should not be used to temporarily remove an item under particular contexts; for that, set an appropriate context when creating the item. See #Specifying Contexts.

item
A menuitem that was previously created and added to the menu. See #Creating New Menuitems.

replace

This part of the proposal is under review.

replace(target, menuitem)

Replaces an existing item in the context menu. If the target item does not exist, this function silently fails, and the context menu is not modified.

target
A value describing the existing item to replace. See #Specifying Existing Menuitems.
menuitem
The menuitem to add. See #Creating New Menuitems.

Specifying Contexts

Items should be added to the context menu, as its name implies, only when some particular context arises. Context can be related to page content or the page itself, but it should never be external to the page. (See #Non-Use Cases for examples.)

Instead of requiring consumers to manually add and remove items when particular contexts arise, this proposal introduces the notion that items are bound to one or more contexts, much as event listeners are bound to events. When the context menu is invoked, all of the items bound to the context giving rise to the menu are added to the menu. If no items are bound to the context, no items are added to the menu. This binding occurs through an item's context property.

Contexts may be specified with any of the following types:

string
A CSS selector. The context arises when the menu is invoked on a node that either matches this selector or has an ancestor that matches.
undefined or null
The page context. The context arises when the menu is invoked on a non-interactive portion of the page. The definition of "non-interactive" is at Jetpack's discretion.
function
An arbitrary predicate. The context arises when the function returns true. The function is passed an object describing the current context. See #Context_Descriptions.
array
An array of any of the other types. The context arises when any context in the array does.

An item's context property is a collection, similar to event listener collections common throughout Jetpack's various APIs. A single context may be bound by assigning one of the above types to the context property either on construction or after:

var item = contextMenu.Item({ context: "img" });
item.context = "img";

Multiple contexts may be bound by assigning an array of contexts either on construction or after:

var item = contextMenu.Item({ context: ["img", "a[href]"] });
item.context = ["img", "a[href]"];

Contexts may also be bound by calling the collection's add method, which takes a single context or array of contexts:

item.context.add("img");
item.context.add(["img", "a[href]"]);

Finally, contexts may be unbound by calling the collection's remove method, which takes a single context or array of contexts :

item.context.remove("img");
item.context.remove(["img", "a[href]"]);

When an item is bound to more than one context, it is added to menus arising from any of those contexts.

Creating New Menuitems

New menuitems are created using one of the constructors exported by the module, Item, Menu, and Separator.

Item

Item(options)

The Item constructor creates simple menuitems.

options
An object that defines the following properties:
context
If the item is added to the top-level context menu, this specifies the context under which the item will appear. See #Specifying Contexts. If undefined, the page context is assumed. This property is ignored if the item is contained in a submenu.
data
An optional, arbitrary string that the extension may associate with the menuitem.
icon
The URL of an icon to display in the menuitem. Optional. Note that some environments, notably Gnome 2.28, do not support menuitem icons either by default or at all.
label
The label of the menuitem, a string. Required.
onClick
An optional function that will be called when the menuitem is clicked. It is called as onClick(contextObj, item). contextObj is an object describing the context in which the menu was invoked. See #Context Descriptions. item is the item itself.

Menu

Menu(options)

The Menu constructor creates items that expand into submenus.

options
An object that defines the following properties:
context
If the item is added to the top-level context menu, this specifies the context under which the item will appear. See #Specifying Contexts. If undefined, the page context is assumed. This property is ignored if the item is contained in a submenu.
icon
The URL of an icon to display in the menuitem. Optional. Note that some environments, notably Gnome 2.28, do not support menuitem icons either by default or at all.
items
An array of menuitems that the submenu will contain.
label
The label of the menuitem, a string.
onClick
An optional function that will be called when any of the item's descendants is clicked. (The commands of descendants are invoked first, in a bottom-up, bubbling manner.) It is called as onClick(contextObj, item). contextObj is an object describing the context in which the menu was invoked. See #Context Descriptions. item is the item that was clicked.

Separator

Separator()

The Separator constructor creates menu separators. Separators can only be contained in Menus; they can't be added to the top-level context menu.

Specifying Existing Menuitems

This part of the proposal is under review.

Items that are part of the context menu by default are identified by case-sensitive strings IDs.

TODO: Table of IDs, making up our own where XUL IDs don't exist, are inconsistent, or are ugly. What to do about non-Firefox apps?

Context Descriptions

It would be useful if menuitem callbacks had a way of examining the context in which they are invoked. For example, a command that downloads images needs to know the URL of the image that the user right-clicked when she opened the context menu.

When a menuitem's callback is called, it is passed an object describing the context in which the context menu was invoked. This object has the following properties:

node
The node the user clicked to invoke the menu.
document
The document containing node, i.e., node.ownerDocument.
window
The window containing document, i.e., node.ownerDocument.defaultView.

Example Usage

var contextMenu = require("context-menu");

// Add "Edit Image" on images.
var imgCssSelector = "img";
var editImageItem = contextMenu.Item({
  label: "Edit Image",
  onClick: function (context) {
    editImage(context.node.src);
  },
  context: imgCssSelector
});
contextMenu.add(editImageItem);

// Replace the "Search" menuitem with a menu that searches Google or
// Wikipedia when there is selected text.
//
// NOTE: The API used by this example is under review.
var selection = require("selection");
function selectionExists() {
  return !!selection.text;
}
var searchMenu = contextMenu.Menu({
  label: "Search",
  onClick: function (context, item) {
    context.window.location.href = item.data + selection.text;
  },
  context: selectionExists,
  items: [
    contextMenu.Item({
      label: "Google",
      data: "http://www.google.com/search?q="
    }),
    contextMenu.Item({
      label: "Wikipedia",
      data: "http://en.wikipedia.org/wiki/"
    })
  ]
});
contextMenu.replace("context-searchselect", searchMenu);

// Add "Edit Page Source" when the menu is invoked on a
// non-interactive portion of the page.
var pageSourceItem = contextMenu.Item({
  label: "Edit Page Source",
  onClick: function (context) {
    editSource(context.document.URL);
  },
  context: null
});
contextMenu.add(pageSourceItem);

// Add "Edit Page Images" when the page contains at least one image.
function pageHasImages(context) {
  return !!context.document.querySelector("img");
}
var editImagesItem = contextMenu.Item({
  label: "Edit Page Images",
  onClick: function (context) {
    var imgNodes = context.document.querySelectorAll("img");
    editImages(imgNodes);
  },
  context: pageHasImages
});
contextMenu.add(editImagesItem);