10.8 Creating Drop-Down Navigation Menus
NN 6, IE 5
10.8.1 Problem
You want navigation menus to
drop down from a menu bar on the page.
10.8.2 Solution
This solution demonstrates one of dozens of ways to implement
drop-down menus. It relies on some simple images for the always
visible menu headers, a single external style sheet called
menus.css (shown in Example 10-3
in the Discussion), and an external JavaScript library called
menus.js (shown in Example 10-4
in the Discussion). You can see the results in Figure 10-2.
To implement this solution, you must create (or borrow) menu header
images for normal and highlighted versions. In several places within
the menus.js library, you fill in the text and
links for the menu items. The library does the rest to assemble the
DHTML components for the menus, under guidance of the
menus.css style sheets.
10.8.3 Discussion
The menu bar is hardwired into the page's HTML as a
single div containing three img
elements. Each img element is surrounded by a
hyperlink (a) element containing basic navigation
action for use by drop-down menu users and simple clicking. Mouse
event handlers for the img elements are assigned
by a script:
<div id="menubar">
<a href="index.html"><img id="menuImg_1" class="menuImg"
src="home_off.jpg" border="0" height="20"
width="80"></a><a href="catalog.html"><img id="menuImg_2"
class="menuImg" src="catalog_off.jpg" border="0" height="20"
width="80"></a><a href="about.html"><img id="menuImg_3"
class="menuImg" src="about_off.jpg" border="0"
height="20" width="80"></a>
</div>
An imported style sheet, menus.css, shown in
Example 10-3, contains specifications for the
drop-down menu containers (class menuWrapper,
which is assigned by script during initialization) and the individual
items in the menus in both normal and highlighted states.
Example 10-3. menus.css style sheet
.menuWrapper {
position:absolute;
width:162px;
background-color:#ff9933;
visibility:hidden;
border-style:solid;
border-width:2px;
border-color:#efefef #505050 #505050 #efefef;
padding:3px;
}
.menuItem {
cursor:pointer;
font-size:16px;
font-family:Arial, Helvetica, sans-serif;
border-bottom:1px solid #505050;
border-top:1px solid #efefef;
padding-left:10px;
color:black;
background-color:#ff9933;
text-decoration:none;
position:absolute;
left:0px;
width:159px;
height:22px;
line-height:1.4em
}
.menuItemOn {
cursor:pointer;
font-size:16px;
font-family:Arial, Helvetica, sans-serif;
border-bottom:1px solid #505050;
border-top:1px solid #efefef;
padding-left:10px;
color:#0099ff;
background-color:#ffcc99;
text-decoration:underline;
position:absolute;
left:0px;
width:159px;
height:22px;
line-height:1.4em
}
Example 10-4 shows the
menus.js library for all the scripts used with
the menus.
Example 10-4. menus.js drop-down menu library
// global menu state
var menuReady = false;
// precache menubar image pairs
if (document.images) {
var imagesNormal = new Array( );
imagesNormal["home"] = new Image(20, 80);
imagesNormal["home"].src="home_off.jpg";
imagesNormal["catalog"] = new Image(20, 80);
imagesNormal["catalog"].src = "catalog_off.jpg";
imagesNormal["about"] = new Image(20, 80);
imagesNormal["about"].src = "about_off.jpg";
var imagesHilite = new Array( );
imagesHilite["home"] = new Image(20, 80);
imagesHilite["home"].src="home_on.jpg";
imagesHilite["catalog"] = new Image(20, 80);
imagesHilite["catalog"].src = "catalog_on.jpg";
imagesHilite["about"] = new Image(20, 80);
imagesHilite["about"].src = "about_on.jpg";
}
function getElementStyle(elem, IEStyleProp, CSSStyleProp) {
if (elem.currentStyle) {
return elem.currentStyle[IEStyleProp];
} else if (window.getComputedStyle) {
var compStyle = window.getComputedStyle(elem, "");
return compStyle.getPropertyValue(CSSStyleProp);
}
return "";
}
// carry over some critical menu style sheet attribute values
var CSSRuleValues = {menuItemHeight:"18px",
menuItemLineHeight:"1.4em",
menuWrapperBorderWidth:"2px",
menuWrapperPadding:"3px",
defaultBodyFontSize:"12px"
};
// specifications for menu contents and menubar image associations
var menus = new Array( );
menus[0] = {mBarImgId:"menuImg_1",
mBarImgNormal:imagesNormal["home"],
mBarImgHilite:imagesHilite["home"],
menuItems:[ ],
elemId:""
};
menus[1] = {mBarImgId:"menuImg_2",
mBarImgNormal:imagesNormal["catalog"],
mBarImgHilite:imagesHilite["catalog"],
menuItems:[ {text:"Deluxe Line", href:"catalog_deluxe.html"},
{text:"Budget Line", href:"catalog_budget.html"},
{text:"Export", href:"catalog_export.html"},
{text:"Order Print Catalog", href:"catalog_order.html"}
],
elemId:""
};
menus[2] = {mBarImgId:"menuImg_3",
mBarImgNormal:imagesNormal["about"],
mBarImgHilite:imagesHilite["about"],
menuItems:[ {text:"Press Releases", href:"press.html"},
{text:"Executive Staff", href:"staff.html"},
{text:"Map to Our Offices", href:"map.html"},
{text:"Company History", href:"history.html"},
{text:"Job Postings", href:"jobs.html"},
{text:"Contact Us", href:"contact.html"}
],
elemId:""
};
// create hash table-like lookup for menu objects with id string indexes
function makeHashes( ) {
for (var i = 0; i < menus.length; i++) {
menus[menus[i].elemId] = menus[i];
menus[menus[i].mBarImgId] = menus[i];
}
}
// assign menu label image event handlers
function assignLabelEvents( ) {
var elem;
for (var i = 0; i < menus.length; i++) {
elem = document.getElementById(menus[i].mBarImgId);
elem.onmouseover = swap;
elem.onmouseout = swap;
}
}
// invoked from initMenu( ), generates the menu div elements and their contents.
// all this action is invisible to user during construction
function makeMenus( ) {
var menuDiv, menuItem, itemLink, mbarImg, textNode, offsetLeft, offsetTop;
// determine key adjustment factors for the total height of menu divs
var menuItemH = 0;
var bodyFontSize = parseInt(getElementStyle(document.body, "fontSize", "font-size"));
// test to see if browser's font size has been adjusted by the user
// and that the new size registers as an applied style property
if (bodyFontSize = = parseInt(CSSRuleValues.defaultBodyFontSize)) {
menuItemH = (parseFloat(CSSRuleValues.menuItemHeight));
} else {
// works nicely in Netscape 7
menuItemH = parseInt(parseFloat(CSSRuleValues.menuItemLineHeight) * bodyFontSize);
}
var heightAdjust = parseInt(CSSRuleValues.menuWrapperPadding) +
parseInt(CSSRuleValues.menuWrapperBorderWidth);
if (navigator.appName = = "Microsoft Internet Explorer" &&
navigator.userAgent.indexOf("Win") != -1 &&
(typeof document.compatMode = = "undefined" ||
document.compatMode = = "BackCompat")) {
heightAdjust = -heightAdjust;
}
// use menus array to drive div creation loop
for (var i = 0; i < menus.length; i++) {
menuDiv = document.createElement("div");
menuDiv.id = "popupmenu" + i;
// preserve menu's ID as property of the menus array item
menus[i].elemId = "popupmenu" + i;
menuDiv.className = "menuWrapper";
if (menus[i].menuItems.length > 0) {
menuDiv.style.height = (menuItemH * menus[i].menuItems.length) -
heightAdjust + "px";
} else {
// don't display any menu div lacking menu items
menuDiv.style.display = "none";
}
// define event handlers
menuDiv.onmouseover = keepMenu;
menuDiv.onmouseout = requestHide;
// set stacking order in case other layers are around the page
menuDiv.style.zIndex = 1000;
// assemble menu item elements for inside menu div
for (var j = 0; j < menus[i].menuItems.length; j++) {
menuItem = document.createElement("div");
menuItem.id = "popupmenuItem_" + i + "_" + j;
menuItem.className = "menuItem";
menuItem.onmouseover = toggleHighlight;
menuItem.onmouseout = toggleHighlight;
menuItem.onclick = hideMenus;
menuItem.style.top = menuItemH * j + "px";
itemLink = document.createElement("a");
itemLink.href = menus[i].menuItems[j].href;
itemLink.className = "menuItem";
itemLink.onmouseover = toggleHighlight;
itemLink.onmouseout = toggleHighlight;
textNode = document.createTextNode(menus[i].menuItems[j].text);
itemLink.appendChild(textNode);
menuItem.appendChild(itemLink);
menuDiv.appendChild(menuItem);
}
// append each menu div to the body
document.body.appendChild(menuDiv);
}
makeHashes( );
assignLabelEvents( );
// pre-position menu
for (i = 0; i < menus.length; i++) {
positionMenu(menus[i].elemId);
}
menuReady = true;
}
// initialize global that helps manage menu hiding
var timer;
// invoked from mouseovers inside menus to cancel hide
// request from mouseout of menu bar image et al.
function keepMenu( ) {
clearTimeout(timer);
}
function cancelAll( ) {
keepMenu( );
menuReady = false;
}
// invoked from mouseouts to request hiding all menus
// in 1/4 second, unless cancelled
function requestHide( ) {
timer = setTimeout("hideMenus( )", 250);
}
// "brute force" hiding of all menus and restoration
// of normal menu bar images
function hideMenus( ) {
for (var i = 0; i < menus.length; i++) {
document.getElementById(menus[i].mBarImgId).src = menus[i].mBarImgNormal.src;
var menu = document.getElementById(menus[i].elemId)
menu.style.visibility = "hidden";
}
}
// set menu position just before displaying it
function positionMenu(menuId){
// use the menu bar image for position reference of related div
var mBarImg = document.getElementById(menus[menuId].mBarImgId);
var offsetTrail = mBarImg;
var offsetLeft = 0;
var offsetTop = 0;
while (offsetTrail) {
offsetLeft += offsetTrail.offsetLeft;
offsetTop += offsetTrail.offsetTop;
offsetTrail = offsetTrail.offsetParent;
}
if (navigator.userAgent.indexOf("Mac") != -1 &&
typeof document.body.leftMargin != "undefined") {
offsetLeft += document.body.leftMargin;
offsetTop += document.body.topMargin;
}
var menuDiv = document.getElementById(menuId);
menuDiv.style.left = offsetLeft + "px";
menuDiv.style.top = offsetTop + mBarImg.height + "px";
}
// display a particular menu div
function showMenu(menuId) {
if (menuReady) {
keepMenu( );
hideMenus( );
positionMenu(menuId);
var menu = document.getElementById(menuId);
menu.style.visibility = "visible";
}
}
// menu bar image swapping, invoked from mouse events in menu bar
// swap style sheets for menu items during rollovers
function toggleHighlight(evt) {
evt = (evt) ? evt : ((event) ? event : null);
if (typeof menuReady != "undefined") {
if (menuReady && evt) {
var elem = (evt.target) ? evt.target : evt.srcElement;
if (elem.nodeType = = 3) {
elem = elem.parentNode;
}
if (evt.type = = "mouseover") {
keepMenu( );
elem.className ="menuItemOn";
} else {
elem.className ="menuItem";
requestHide( );
}
evt.cancelBubble = true;
}
}
}
function swap(evt) {
evt = (evt) ? evt : ((event) ? event : null);
if (typeof menuReady != "undefined") {
if (evt && (document.getElementById && document.styleSheets) && menuReady) {
var elem = (evt.target) ? evt.target : evt.srcElement;
if (elem.className = = "menuImg") {
if (evt.type = = "mouseover") {
showMenu(menus[elem.id].elemId);
elem.src = menus[elem.id].mBarImgHilite.src;
} else if (evt.type = = "mouseout") {
requestHide( );
}
evt.cancelBubble = true;
}
}
}
}
// create menus only if key items are supported
function initMenus( ) {
if (document.getElementById && document.styleSheets) {
setTimeout("makeMenus( )", 5);
window.onunload=cancelAll;
}
}
Scripts begin with one global variable declaration,
menuReady, that is ultimately used as a flag to
let other functions know when the menus are available for animation.
Next is code for precaching all menu button images in two states
(á là Recipe 12.1). A utility function,
getElementStyle(
), is a variation of the function from
Recipe 11.12, which this script uses to keep menu item font sizes in
sync with user-selected font sizes in Mozilla-based browsers.
Because browser security restrictions (at least in IE 6 and Netscape
6 and 7) prevent scripts from reading rule
property values of style sheets, the script includes a global object
that replicates some key style sheet values that the scripts use for
help with menu positioning and sizing. You can get these values from
the style sheet settings.
Next is the creation of objects (inside an array called
menus) that contain vital menu details needed
later when they are built. Each object has five properties, which are
described in more detail later.
Scripts for hiding and showing the menus frequently require that a
menu's reference be capable of pointing to the
swappable image at the top of the menu, and vice versa. To speed this
process along (i.e., to avoid looping through all of the
menu's array items in search of properties that
match either the image or menu IDs), it is more convenient during
initialization to create a one time, simulated hash table (in the
makeHashes( ) function), whose string index values
consist of both the image and menu element IDs (see Recipe 3.9). The
result is a hash table that has two pointers to each
menus array entry—one for the image ID and
one for the menu ID. Another function invoked during initialization,
assignLabelEvents(
), assigns the mouse rollover event
handlers to the image elements at the top of each menu.
The biggest function of the recipe, makeMenus(
), assembles the menu elements, using
W3C DOM node creation syntax. This routine is invoked at
initialization time and depends on the menus
array, defined earlier, to help populate each menu with the text and
link for each menu item.
There are two rollover concerns for this application: the menu bar
image swapping and the display of the menus. While their actions are
different, the actions work with each other. But first, some support
code to do the dirty work is needed. Because of the interaction
between menu bar image and menus, a setTimeout(
) timer is used to assist in cleaning up
menus that are no longer necessary. The timer identifier (created
when setTimeout( ) is invoked) is preserved as a
global variable called timer.
You can't just hide the menu when a
mouseout event occurs in one of the menu bar
images, since the mouse may be headed to the currently displayed menu
and you want that menu to remain in place. To assist with this task
is the keepMenu( ) function, which cancels a timer set in
the requestHide( ) function, and thus makes sure the
menu stays visible.
A related function, cancelAll(
), which is invoked by an
onunload event handler, guards against potential
problems (particularly in Netscape 6 and later) in states between
page loadings, while the cursor may still be rolling around a
swappable image. Global variables are in transient states and may be
valid when a function begins, but be gone by the time the value is
needed. The cancelAll( ) function puts the page in
a quiet state during the transition.
All mouseout events from the menu bar and menus
start the 1/4-second delay clock ticking in the requestHide(
) function. If the timer is still valid in 1/4 second, the
hideMenus( ) function runs. The
hideMenus( ) function performs a blanket
restoration of menu bar images and menu display.
A separate positioning function, positionMenu(
), is invoked before any menu is
displayed. Therefore, if the menu bar changes position on the page
(perhaps due to dynamic content or a resized browser window), the
menu is displayed correctly with respect to the menu title image.
Invoked by all mouseover events in menu bar images
and menu components, the showMenu(
) function turns off the timer so that
any pending hideMenu( ) call
won't occur. Then it immediately hides all menus
(rather than waiting for the timer) and shows the menu
div element whose ID is accessed from the
menus array (index passed as a parameter)
elemId property.
As the user rolls the mouse pointer over items within a displayed
menu, the mouseover and
mouseout events trigger style sheet changes to the
entries (by changing the element's class assignment
in the toggleHighlight( ) function). In the case
of a mouseover, the keepMenu( )
function fires to make sure any pending menu hiding gets cancelled.
For a mouseout, the hide request is made, in case
the mouseout motion is toward some other region on
the page.
The swap( ) function, invoked by the mouse event
handlers of the menu bar images, is the main trigger to display a
drop-down menu. In response to a mouseover event,
the menu is displayed, and the menu bar image changes to the
highlighted version. For a mouseout event, a hide
request is made. If the hide timer isn't cancelled
in time, the menu disappears and the menu bar image returns to its
default image.
Invoked by the onload event handler, the
initMenus( ) function certifies that the browser
has the right stuff for the menuing system. It looks not only for
basic W3C DOM support, but also for the more esoteric
document.styleSheets property (lacking in Opera
through Version 6). If the browser supports the necessary facilities,
the makeMenus( ) function generates the menu
div elements. It also assigns an
onunload event handler to the window so that any
pending menu-hide request is cancelled before the page goes away.
One of the goals of the design shown in this recipe is to minimize
the amount of custom work needed to implement the drop-down menu in a
variety of visual contexts. Most of the menuing libraries available
from other sources go to even further extremes in this regard,
building very complex and thorough systems of custom objects and
dynamically written HTML. General-purpose libraries, especially those
designed to work with outdated object models (e.g., the Navigator 4
DOM), need the extra complexity to accommodate as wide a range of
deployment scenarios as possible. This example, on the other hand, is
pared to a smaller size, and might require a bit more work to blend
into your design (particularly around the images for the menu bar).
But there should plenty of ideas here that you can use as-is for a
largely automatic menuing systems compatible with IE 5 or later and
NN 6 or later.
Reliance on style
sheets for the visual aspects of the menus simplifies your
experimentation with different looks, such as color combinations,
font specifications, and sizes. The JavaScript code supporting the
style sheets makes only a few assumptions:
All drop-down menus are the same width, regardless of the widths of
their menu title images (which may vary as you see fit).
Widths of menu items are determined by subtracting the menu
wrapper's padding from the
wrapper's width (162 pixels for the wrapper and 159
pixels for menu items in this recipe).
Style sheets for the two states of menu item (normal and highlighted)
should specify the same dimensions and font sizes.
Several menu item height and menu wrapper border style sheet
properties must be replicated in the
CSSRuleValues object defined in the menu JavaScript
code. If you modify your style sheet, duplicate the changes in
CSSRuleValues.
You need to customize a few parts of the JavaScript code to fit the
menu system to your graphical menus. First, establish the image
precaching for the individual graphics in your menu bar (your menu
bar may be a vertical list of menu titles or other element
arrangement). Image array string index values and
src properties are set just like they are in
Recipe 12.1.
Next are the specifications for the content of the menus. The
menus array contains custom objects
corresponding to the menus that get created elsewhere. Each custom
object has numerous properties that get used through various stages
of creating, displaying, and hiding the menus:
- mBarImgId
-
String containing the ID of the img element for
the menu bar menu title image associated with this menu.
- mBarImgNormal
-
Reference to the precached image object for the normal state of the
menu title image.
- mBarImgHilite
-
Reference to the precached image object for the highlighted state of
the menu title image.
- menuItems
-
Array of objects, one for each item in the menu (or an empty array if
the menu title has no drop-down menu items). Each object consists of
two properties: one for the text that shows in the drop-down menu;
the other with the URL to which the user will navigate when selecting
the menu item.
- elemId
-
Initialized as an empty string, this property gets its value filled
in automatically when the menus are created. Do nothing with this
property.
The final item to customize is the menu bar. Each menu title must be
its own img element (and have two
states—normal and highlighted—as fed to the precaching
code earlier). You must add onmouseover and
onmouseout event handlers to each
img element's HTML code. Both
event handlers invoke the same method (swap( )),
and pass as the first parameter the zero-based index integer
corresponding to the index of the menus array entry bearing the menu
specifications for this menu title.
Although it is not a requirement for the menuing system, I recommend
that each menu title image also be surrounded by a hyperlink. This is
so that underpowered browsers and search engines are able to follow
paths to the next lower levels of your web site, even if the
destinations of those links are simple pages offering traditional
links to the same items in your drop-down menus. Users of your menus
will be able to bypass those pages.
All of the other code in the recipe works on its own to build the
menus and handle their display activities. Items in the menus are
created as traditional links so that users who expect to see URLs of
links in the status bar will be right at home.
Code in the makeMenus(
) function assumes that the menus are to
be deployed as true drop-down menus by positioning the menus flush
left and just below the images in the menu bar. If you need the menus
to push to the right or upward, you'll need to
adjust the statements that set values of the
menuDiv.style.left and
menuDiv.style.top properties. Notice that in the
drop-down recipe, the left position is lined up with the
mBarImg.offsetLeft value, and that the top is
pushed to the bottom of the image by the addition of
mBarImg.height. If you want to make the menu push
to the right, the top would be flush with the
mBarImg.offsetTop, and the left would be extended
to the right by the amount of mBarImg.width. To
make the menu appear to pop upward, the top of the menu would be at
the mBarImg.offsetTop minus the height of the
menuDiv (set earlier in this function). In all
cases, leave the plain offsetLeft and
offsetTop variable adjustments in the formulas
because they take care of some nonstandard position alignment
behaviors of Internet Explorer.
It's unfortunate that misplaced security
restrictions prevent scripts from reading style sheet rule attributes
directly. As a workaround, we have to duplicate some important values
in the script code (in the CSSRuleValues global
variable) to refer to them while sizing the menus. If, in the future,
browsers are able to access style sheet rules without offending
security restrictions, you should be able to let the definitions in
the style sheet rules govern the menu positioning and sizing. For
future reference, the following function adheres to the W3C DOM (and
IE syntax idiosyncrasies) to read individual rule values from a style
sheet embedded in a style or
link element bearing an ID. While the function
works as-is from a local hard disk, it generates security-related
errors when the page is accessed from a server:
// utility function invoked from makeMenus( )
// returns style sheet attribute value.
// parameters are: ID of <style> element, selector name, and attribute name
function getCSSRuleValue(styleID, selector, attr) {
var sheet, styleElem, i;
for (i = 0; i < document.styleSheets.length; i++) {
sheet = document.styleSheets[i];
styleElem = (sheet.ownerNode) ? sheet.ownerNode :
((sheet.owningElement) ? sheet.owningElement : null);
if (styleElem) {
if (styleElem.id = = styleID) {
break;
}
}
}
var rules = (sheet.cssRules) ? sheet.cssRules : ((sheet.rules) ?
sheet.rules : null);
if (rules) {
for (i = 0; i < rules.length; i++) {
if (rules[i].selectorText = = selector || rules[i].selectorText = =
"*" + selector) {
return rules[i].style[attr];
}
}
}
return null;
}
If you design your menu bar to live in one frame and expect the
pop-up menu to appear in another frame, you have some more coding to
do because div elements exist within the context
of a browser window and do not extend into adjacent frames. To begin,
the code that generates the div elements in the
changeable frame has to be incorporated into each document that loads
into that other frame. But that code needs to be modified to look to
the menu bar title images in the navigation frame for position
information. You also have to take into account any possible
scrolling that occurs in the changeable frame, since it influences
the position of the menu within the page, even though the menu title
is static. Additional cross-frame communication is needed to
synchronize the image swapping and menu showing/hiding actions.
As a last note, you may be interested in the rationale behind the
requestHide( ) function and the use of the
setTimeout( ) method to hide a menu. Notice that two
different elements' states change: the menu title
image's src and the visibility of
the associated menu. A simple onmouseout event
handler on the image to swap the image and hide the menu works only
if the user moves the pointer in a direction other than downward into
the menu. In the latter case, you don't want the
menu title to change back to its original state or hide the menu.
Instead, the image's onmouseout
event handler sets the timer to execute the restoration process in
250 milliseconds via setTimeout( ). However, the
onmouseover event handlers of menu components,
which fire before the 250 milliseconds are up, clear the timer so
that the state stays the same, all while the cursor is in the menu
(or goes back up to the menu title).
If the user slides over to another menu title, the time-out timer is
also cleared so that the hideMenus(
) method can restore initial state
instantly (which is a little faster than 250 milliseconds, so
response feels quicker). Only when the user slides the pointer out
and away from the menu bar or a visible menu does the timer have a
chance to invoke hideMenus( ), which puts
everything back the way it was when the page loaded.
With the requestHide( ) function setting a timer
to go off in the future (no matter how soon the future will be), you
must set the onunload event handler for the page
to invoke cancelHide( ). Failure to do so will
allow the hideMenus( ) function call in the timer
queue to execute after the page has gone away — taking its
scripts with it. The result is a script error. If your pages assign
another onunload event handler via script
properties for other purposes, you need to define yet another
function that invokes both cancelHide( ) and your
other function, so that the onunload event can
invoke both functions.
10.8.4 See Also
Recipe 3.9 for simulating a hash table; Recipe 12.1 for precaching
images; Recipe 12.7 for hiding and showing elements; Recipe 13.1 for
creating a positioned element.
|