ContextMenus

Context Menus

A context menu is a menu where the items on the menu are specific to the context in which the menu was opened. A typical usage is for the user to right-click on an element to display a menu of commands pertaining to what was clicked on.

Context Menu Events

There are various ways in which a context menu can be opened. The most common is by clicking on an element with the right mouse button. On Macintosh systems with only one mouse button, a context menu may be opened by either holding down the mouse button or by pressing the Control key and clicking the mouse button. On Windows, the context menu can also be opened by pressing the menu key on the keyboard (the key on many keyboards next to Control with a menu image) or by pressing Shift+F10. You should not assume that a user has used the mouse to open a context menu.

When using the mouse to open a context menu, the context is the element that was clicked. When using the keyboard, the context is the element in the window that is currently focused.

Although there are several ways to open a context menu, a single event may be used to listen for any of these situations; the 'contextmenu' event is fired in any case.

<hbox id="container" align="center" oncontextmenu="...">
  <label value="Name:"/>
  <textbox id="name"/>
</hbox>

In this example, an attempt to open a context menu anywhere inside the hbox will call the event listener attached using the oncontextmenu attribute. As textboxes have a context menu built in, the event will fire before an attempt to open the context menu. However, the event will also fire when the label is right-clicked for instance, so the event handler should check to make sure that the target of the contextmenu event is what is desired.

To cancel a context menu event, you can use the preventDefault method of the event object.

function checkContextMenu(event) {
  if (event.target.localName == "textbox")
    event.preventDefault();
}

function init() {
  var container = document.getElementById("container");
  container.addEventListener("contextmenu", checkContextMenu, true);
}

The 'checkContextMenu' function checks to see if the textbox was the target of the context menu and, if so, cancels the event using the preventDefault method. This has the effect of disabling the context menu. The event listener is attached using the addEventListener method within the 'init' function. This might be called while initializing a window during a load event for example.

Attaching Context Menus

A context menu may be attached to an element using the context attribute, which may be applied to any XUL element. The value of the context attribute should be the id of a context menu within the same document.

In the example, a context menu is attached to a grid.

Image:Popupguide-contextmenu.png

<window id="main-window">
  <popupset>
    <menupopup id="ins-del-menu">
      <menuitem label="Insert"/>
      <menuitem label="Delete"/>
    </menupopup>
  </popupset>
</window>

<grid context="ins-del-menu">
  <columns>
    <column/>
    <column flex="1"/>
  </columns>
  <rows id="rows">
    <row align="center">
      <label value="Name:"/>
      <textbox/>
    </row>
  </rows>
</grid>

The same context menu can be attached to multiple elements. Just set the context attribute on those elements you want the context menu to apply to.

One technique is to attach a context menu to an entire document area. For instance, the Firefox browser applies a single context menu to the browser area which displays web pages. For example, the following will attach a context menu with the id 'contentAreaContextMenu' to a browser element:

<browser context="contentAreaContextMenu">

Indicating the Default Item

On some platforms, one of the items in a context menu is marked as being a default operation. Typically, this will cause the menuitem's label to appear in the context menu in bold. What constitutes the default item isn't specified, but is usually the operation carried out when the item was left-clicked or pressed normally, as opposed to context clicked. You can indicate this item, if it applies, using the default attribute.

<menupopup id="link-menu">
  <menuitem label="Open Link" default="true"/>
  <menuitem label="Open In New Window"/>
  <menuitem label="Properties"/>
</menupopup>

The default attribute has been set to true on the first menu item ('Open Link'). On platforms that support it, it will be marked as a default item. On other platforms, the attribute will just be ignored and the menuitem will be displayed normally. Naturally, it only makes sense to have one default item per context menu.

Note that the default attribute just affects how the item is displayed, it performs no other function itself. You will still need to associate code with the element to handle the default operation.

To group menu items together for better visual clarity, use a menu separator.

<menuseparator/>

Child Context Menus

Sometimes it may be convenient to place the menupopup for the context menu as a child of the element the menu is associated with instead of referring to the menupopup by id. This may be useful, for example, when a context menu is associated with only 1 element. It is also useful in XBL bindings, as you wouldn't want to refer to a popup by id in this case. If the context attribute is set to the value '_child', the context menu is found by examining the children of the element instead of looking for a menupopup with the given id. For example:

<vbox context="_child">
  <label value="Hello"/>
  <menupopup>
    <menuitem label="Cut"/>
    <menuitem label="Copy"/>
    <menuitem label="Paste"/>
  </menupopup>
</vbox>

In this sample, the context menu is found as the child of the vbox, because the context attribute is set to '_child'. Note that this special value must begin with an underscore to distinguish it from an id of 'child'. The menupopup does not have to be the last child of the element as in this example, but usually this will be a convenient place to put it.

Hiding and Showing Menu Items based on Context

When the context menu is opened, a popupshowing event will fire before the popup is displayed. This may be used to adjust the items displayed on the menu. Typically, this will vary based on what the context is. For example, right clicking on an image might display items pertaining to images, whereas clicking on a link would display items pertaining to links.

In many cases, it may be easier to use a single context menu containing every possible item, and show and hide the necessary items when needed.

To hide an item, set its hidden property to true. To show an item, set its hidden property to false.

<script>
function showHideDeleteItem()
{
  var deleteItem = document.getElementById("delete");

  var rows = document.getElementById("rows");
  deleteItem.hidden = (rows.childNodes.length == 0);
}
</script>

<menupopup id="inssel-menu" onpopupshowing="showHideDeleteItem()">
  <menuitem label="Insert"/>
  <menuitem id="delete" label="Delete"/>
</menupopup>

In this example, the showHideDeleteItem function is called when the popupshowing event is fired. This function checks if the element with the id 'rows' has no children and, if so, hides the 'delete' menu item by adjusting the item's hidden property. When the user attempts to open the context menu, the delete menu item will either be shown or hidden depending on the the number of children.

Another alternative is to disable the menuitem instead of hiding it, by setting the item's disabled property.

deleteItem.disabled = (rows.childNodes.length == 0);

For this example, using the disabled property is more suitable. Items should be disabled when they don't apply in certain circumstances, but should be hidden when they would never apply to the element that was clicked on.

Determining what was Context Clicked

It is important to remember that a context menu may be opened in other ways than just with the mouse. However, even when using the keyboard, there will still be a node (the context) that the menu applies to, which will be the element that is currently focused. When the mouse is used, the context will be the element that is clicked.

You can retrieve this element using the popup's triggerNode property. In Firefox 3.6 and earlier, the document's popupNode property was used; this is now deprecated. In this example, the triggerNode property is checked for a popup attached to a browser to determine whether an image is clicked:

<script>
function showHideItems(aEvent)
{
  // aEvent.target is the popup for which the popupshowing event fired.
  var element = aEvent.target.triggerNode;
  var isImage = (element instanceof Components.interfaces.nsIImageLoadingContent &&
                 element.currentURI);
  document.getElementById("enlarge").hidden = !isImage;
  document.getElementById("details").hidden = !isImage;
}
</script>

<menupopup id="contentAreaContextMenu" onpopupshowing="showHideItems(event)">
  <menuitem label="Copy"/>
  <menuitem id="enlarge" label="Enlarge Image"/>
  <menuitem id="details" label="Image Details"/>
</menupopup>

<browser src="http://www.mozilla.org" context="contentAreaContextMenu"/>

When the popupshowing event is fired, the showHideItems function is called. The triggerNode is retrieved and examined to see if it is an image. The nsIImageLoadingContent interface is implemented by all types of images. This is useful instead of comparing the tag, as several different tags support images. The test also checks to ensure that the element has a uri set.

Finally, the hidden state of two menuitems are adjusted based on whether the context is an image or not. The result is that context clicking on images will show a menu with three items, but will only show one item for other types of elements.