[ Team LiB ] Previous Section Next Section

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.

Figure 10-1. A context-sensitive pop-up menu
figs/jsdc_1001.gif

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.

    [ Team LiB ] Previous Section Next Section