10.7 Creating a Contextual (Right-Click) Menu
NN 6, IE 5(Win)
10.7.1 Problem
You want to display a customized menu of navigation
or other options when the user right-clicks (Windows) or holds down
the mouse button (in Netscape for the Mac)—actions that
normally trigger the browser's internal context
menu.
10.7.2 Solution
Use the
oncontextmenu event handler that is part of
newer browsers to intercept the normal browser action and display a
menu of your own design. The example page described in the Discussion
demonstrates one way to create a menu out of standard HTML elements
as well as the script code that controls each menu's
visibility, positioning, and interactivity. Figure 10-1 shows the finished results.
To deploy this recipe on a page of your own design, you need to
customize the following items:
The HTML for the div context menus, following the
model shown in the solution
IDs for the span elements surrounding the words
and phrases to be highlighted
Data in the cMenu object, particularly the string
indexes of the object entries (identical to the IDs of the
highlighted spans), the IDs of the div context
menus for the menuID properties, and the
href properties' URLs to which
you want each menu entry to lead
Style sheets for the highlighted colors (and style sheets in general)
can be modified to suit your design tastes.
All activity begins by assigning mouse-related event handlers after
the page loads. The following initContextMenus(
) initialization function runs when the
page's onload event handler
fires:
function initContextMenus( ) {
if (document.body.addEventListener) {
// W3C DOM Event model
document.body.addEventListener("contextmenu", showContextMenu, true);
document.body.addEventListener("click", hideContextMenus, true);
} else {
// IE Event model
document.body.oncontextmenu = showContextMenu;
}
// set intelligent tool tips
setContextTitles( );
}
The scripting includes a way for browsers that don't
support the oncontextmenu event handler to degrade
gracefully.
10.7.3 Discussion
Example 10-1 shows the HTML page and embedded CSS
style sheet that uses custom context menus powered by the scripts in
Example 10-2. The page is designed around body text
containing highlighted words or phrases for which you want to offer
two or more navigation links per entry in a context-sensitive pop-up
menu. Figure 10-1 shows a contextual menu for this
solution.
Example 10-1. HTML and CSS portions of the contextual menu recipe
<html>
<head>
<title>Contextual Menus</title>
<style type="text/css">
.contextMenus {position:absolute; background-color:#cfcfcf;
border-style:solid; border-width:1px;
border-color:#EFEFEF #505050 #505050 #EFEFEF;
visibility:hidden}
.menuItem {cursor:pointer; font-size:9pt;
font-family:Arial, Helvetica, sans-serif;
padding-left:5px; color:black;
background-color:transparent;
text-decoration:none}
.menuItemOn {cursor:pointer; font-size:9pt;
font-family:Arial, Helvetica, sans-serif;
padding-left:5px; color:red;
background-color:yellow;
text-decoration:underline}
.contextEntry {font-weight:bold; color:darkred; cursor:pointer}
</style>
<script type="text/javascript" src="contextMenus.js"></script>
</head>
<body onload="initContextMenus( )">
<h1>Custom Contextual Menu</h1>
<hr />
<p>This sentence has at least one
<span id="lookup1" class="contextEntry">sesquipedalian</span> word
and mention of the state of <span id="lookup2" class="contextEntry">Wyoming</span>,
both of which could have additional lookups.</p>
<div id="contextMenu1" class="contextMenus" onclick="hideContextMenus( )"
onmouseup="execMenu(event)" onmouseover="toggleHighlight(event)"
onmouseout="toggleHighlight(event)">
<table><tbody>
<tr><td class="menuItem">Merriam-Webster Dictionary</td></tr>
<tr><td class="menuItem">Merriam-Webster Thesaurus</td></tr>
</tbody></table>
</div>
<div id="contextMenu2" class="contextMenus" onclick="hideContextMenus( )"
onmouseup="execMenu(event)" onmouseover="toggleHighlight(event)"
onmouseout="toggleHighlight(event)">
<table><tbody>
<tr><td class="menuItem">Wyoming Tourist Info</td></tr>
<tr><td class="menuItem">State Map</td></tr>
<tr><td class="menuItem">cnn.com</td></tr>
<tr><td class="menuItem">Google</td></tr>
<tr><td class="menuItem">Yahoo Search</td></tr>
</tbody></table>
</div>
</body>
</html>
A sample HTML paragraph contains a couple of span
elements that surround the highlighted words. The class name assigned
to the span elements is used to associate a style
sheet rule (the
contextEntry class), as well as to assist with the
display of context menus in the scripts.
Two hardwired context menus are created the quick and dirty way:
wrapping small tables inside positioned div
elements. The context menus, initially hidden from view, are governed
by the style sheet rules for three classes:
contextMenus, menuItem, and
menuItemOn. Each div element
contains event handlers for mouse actions other than the context menu
display, such as mouse rollover and navigating in response to a click
on a menu item.
Scripts for the contextual menus are contained in the
contextMenus.js library, shown in Example 10-2.
Example 10-2. contextMenus.js library
// context menu data objects
var cMenu = new Object( );
cMenu["lookup1"] = {menuID:"contextMenu1",
hrefs:["http://www.m-w.com/cgi-bin/dictionary?book=Dictionary&va=sesquipedalian",
"http://www.m-w.com/cgi-bin/dictionary?book=Thesaurus&va=sesquipedalian"]};
cMenu["lookup2"] = {menuID:"contextMenu2",
hrefs:["http://www.wyomingtourism.org/",
"http://www.pbs.org/weta/thewest/places/states/wyoming/",
"http://cnn.looksmart.com/r_search?l&izch&
pin=020821x36b42f8a561537f36a1&qc=&col=cnni&qm=0&st=1&nh=10&
rf=1&venue=all&keyword=&qp=&search=0&key=wyoming",
"http://google.com","http://search.yahoo.com"]};
// position and display context menu
function showContextMenu(evt) {
// hide any existing menu just in case
hideContextMenus( );
evt = (evt) ? evt : ((event) ? event : null);
if (evt) {
var elem = (evt.target) ? evt.target : evt.srcElement;
if (elem.nodeType = = 3) {
elem = elem.parentNode;
}
if (elem.className = = "contextEntry") {
var menu = document.getElementById(cMenu[elem.id].menuID);
// turn on IE mouse capture
if (menu.setCapture) {
menu.setCapture( );
}
// position menu at mouse event location
var left, top;
if (evt.pageX) {
left = evt.pageX;
top = evt.pageY;
} else if (evt.offsetX || evt.offsetY) {
left = evt.offsetX;
top = evt.offsetY;
} else if (evt.clientX) {
left = evt.clientX;
top = evt.clientY;
}
menu.style.left = left + "px";
menu.style.top = top + "px";
menu.style.visibility = "visible";
if (evt.preventDefault) {
evt.preventDefault( );
}
evt.returnValue = false;
}
}
}
// retrieve URL from cMenu object related to chosen item
function getHref(tdElem) {
var div = tdElem.parentNode.parentNode.parentNode.parentNode;
var index = tdElem.parentNode.rowIndex;
for (var i in cMenu) {
if (cMenu[i].menuID = = div.id) {
return cMenu[i].hrefs[index];
}
}
return "";
}
// navigate to chosen menu item
function execMenu(evt) {
evt = (evt) ? evt : ((event) ? event : null);
if (evt) {
var elem = (evt.target) ? evt.target : evt.srcElement;
if (elem.nodeType = = 3) {
elem = elem.parentNode;
}
if (elem.className = = "menuItemOn") {
location.href = getHref(elem);
}
hideContextMenus( );
}
}
// hide all context menus
function hideContextMenus( ) {
if (document.releaseCapture) {
// turn off IE mouse event capture
document.releaseCapture( );
}
for (var i in cMenu) {
var div = document.getElementById(cMenu[i].menuID)
div.style.visibility = "hidden";
}
}
// rollover highlights of context menu items
function toggleHighlight(evt) {
evt = (evt) ? evt :
((event) ? event : null);
if (evt) {
var elem = (evt.target) ? evt.target : evt.srcElement;
if (elem.nodeType = = 3) {
elem = elem.parentNode;
}
if (elem.className.indexOf("menuItem") != -1) {
elem.className = (evt.type = = "mouseover") ? "menuItemOn" : "menuItem";
}
}
}
// set tooltips for menu-capable and lesser browsers
function setContextTitles( ) {
var cMenuReady = (document.body.addEventListener ||
typeof document.oncontextmenu != "undefined")
var spans = document.body.getElementsByTagName("span");
for (var i = 0; i < spans.length; i++) {
if (spans[i].className = = "contextEntry") {
if (cMenuReady) {
var menuAction = (navigator.userAgent.indexOf("Mac") != -1) ?
"Click and hold " : "Right click ";
spans[i].title = menuAction + "to view relevant links"
} else {
spans[i].title = "Relevant links available with other browsers " +
"(IE5+/Windows, Netscape 6+)."
spans[i].style.cursor = "default";
}
}
}
}
// bind events and initialize tooltips
function initContextMenus( ) {
if (document.body.addEventListener) {
// W3C DOM event model
document.body.addEventListener("contextmenu", showContextMenu, true);
document.body.addEventListener("click", hideContextMenus, true);
} else {
// IE event model
document.body.oncontextmenu = showContextMenu;
}
// set intelligent tooltips
setContextTitles( );
}
At the start of the script, data for the context menu actions
(primarily the URL for each link) is assigned to the
cMenu object using the context-sensitive
span elements' IDs as index
values. The rest of the scripting is divided into three categories:
event assignment, displaying or hiding the context menu, and acting
on a menu selection.
Event assignment occurs in the initContextMenus(
) function, which is invoked by an
onload event handler in the
<body> tag. The initialization function
invokes setContextTitles(
), which assigns browser-appropriate
title attribute strings to the context-sensitive
entries. The function to show the context menu,
showContextMenu(
), filters out events so that only those
from the context-sensitive spans are heeded. Hiding the context menu
with hideContextMenus(
) is far simpler because it hides all
menu items, visible or not.
When the user rolls the mouse pointer over a visible context menu,
the usual rollover effects for each table cell visually reinforce the
choice about to be made. The event handlers are specified for the
div element that contains the table and table
cells where the rollover events occur, thus the events rely on event
bubbling.
When a user makes a selection from the menu, the execMenu(
) function needs to detect which item
was chosen from which menu and dig out the associated URL from the
cMenu object. A key utility function,
getHref( ), starts when the ID of the
td element receives the mouseup
event, after which getHref() retrieves the
corresponding URL. The getHref( ) function is
invoked from within the execMenu( ) function,
which is called to act from a mouseup event that
bubbles up from a td element (or its text node in
NN 6 and later) within each context menu div
element.
An important stylistic point to address in this example is the use of
tables within the context
menus themselves. There are two significant advantages to this
approach, both of which have to do with simplicity and aesthetics. By
specifying a table inside the div element, you do
not have to worry about the width of the positioned
div element. The div determines
its width based on the width occupied by the table, which, in turn,
depends on the widest cell and its text content. You could get the
same auto-sizing effect by simply stuffing the div
elements with a series of nested div or
p elements for each item in the menu. The problem
with this technique, however, is that each such
element's background area is only as wide as its
text. If you attempt to perform the rollover technique to change the
background color on each item, the colored background is an uneven
width up and down the menu, unless the text for each item (magically)
has identical widths. By using the table, each td
element's background space is the same width as the
others. On the downside, there are schools of HTML thought that urge
against using tables for formatting purposes only. They would rather
you use positioning and style sheets to control visual aspects of
content.
You can see an example of using style sheets for pop-up menu creation
in Recipe 10.9, if you prefer that technique. It may require some
trial and error to achieve the desired dimensions, unless you use
even more sophisticated techniques found in the commercial pop-up
menu libraries available from several developer-oriented web sites.
Just because the recipe shown here uses the
oncontextmenu event handler to display the menu
doesn't mean that you are limited to employing that
event as the trigger. A rollover (onmouseover
event handler) for the highlighted entries works just as well, and,
in fact, operates on more browsers.
One of the challenges facing the Solution is how to correlate the
context menu choices with a parallel set of URLs. The recipe utilizes
a custom script object
(cMenu), which is defined with string index
values. For performance reasons (i.e., snappy response to
right-clicking on a highlighted entry), the cMenu
object string indexes correspond to the IDs of the highlighted spans,
speeding the determination (in the showContextMenu(
) function) of the ID of the div context
menu associated with the highlighted entry
(cMenu["spanID"].menuID).
Each indexed entry of the cMenu object is an
object in itself, which has two properties. The first,
menuID, is the ID of the div
context menu associated with the highlighted span
entry. The second property is an array of URLs for each item in the
context menu. The order of URL values in the array is the same as the
order of the td elements in the menu. Given a user
choice of a td element (as detected by the
onmouseup event targeted at one of the
td elements), the getHref( )
function can calculate the key information it needs to retrieve the
URL from the cMenu object: the ID of the context
menu holding that td element and the row in which
the td element lives.
There is actually an easier way to accomplish this other than the
indirect approach using the cMenu object, but it
entails embedding more page-specific data within the HTML code,
including the application of a custom attribute. I elected to avoid
this technique because, without getting into XML namespaces and XHTML
coding, the custom attribute would not validate against standard
DTDs. Of course, not every developer is concerned with validation, in
which case, adding an attribute to the td elements
of the menus pointing to the associated URL simplifies the activity
taking place in the execMenu( ) function. For
example, if the td elements have an
href attribute whose value is the URL for the
entry, the deeply nested statement in execMenu( )
doesn't need the getHref( )
function at all. Instead, it simply reads the custom attribute of the
td element:
location.href = elem.getAttribute("href");
Event processing is a vital aspect of this recipe. The code
successfully works with both the W3C and IE concepts of event
capture, which have little to do with each other. In the
initContextMenus( ) function, a W3C DOM event
model browser binds oncontextmenu (supported in NN
6 and later, although not a sanctioned W3C DOM Level 2 event) and
onclick events to the body in the capture phase of
event propagation. This lets these events get processed before they
reach their targets, making it easier to curb their default actions
when we don't want them to occur.
On the IE side, event capture is for mouse events only, and is
intended to be a temporary state. In capture mode, mouse events are
directed to the element for which the setCapture(
) method is invoked. The browser goes into a kind of modal
state, during which the user cannot access other page elements by the
mouse because the events on elements outside of the invoking element
automatically go to the invoking element. Thus, each
div menu element has an onclick
event handler assigned that hides the context menus. With a context
menu showing, if the user clicks anywhere outside of the menus, they
disappear, just like a browser-based context menu. Hiding the context
menus disengages capture mode in IE, returning mouse activity to
normal.
10.7.4 See Also
Recipe 13.1 for positioning an element on the page; Recipe 12.7 for
changing the visibility of an element.
|