Accessibility
Icon or Spacer
   

DHTML Drop-Down Menus

By Jeremy Petersen
Manager, Web Application Engineering
TeachStream, Inc.

Click here to read or download a .zip file of this article.

Introduction

It seems one of the most in demand yet misunderstood uses for DHTML is creating a good cross-browser DHTML drop-down menu system. Many DHTML menu scripts can be found, but not all of them work in every browsers. The purpose of this article will be to shed a little light on this topic.

Building a cross-browser drop-down menu system in DHTML is no simple task. The goal of this article is to keep things as basic as possible. Be forewarned, though, that this article will present a large amount of code, yet will only be scratching the surface of what is possible. In an earlier article, "HTML Primer for Popular Web Browsers," I presented a brief introduction of the history and syntax for cross browser DHTML. If you are new to the concepts of DHTML, you may want to review the introductory article to establish a base understanding before proceeding.

The sample code used for this demonstration is based on a 1999 script called SmartMenu, written by Constantin Kuznetsov, Jr. Many changes have been made to the original script, including the simplification of both the functionality and the code, as well as the added compatibility for Netscape 6 (and - in theory - any DOM-compliant browser available). To keep things as straightforward as possible, all DHTML-related code has been placed in a single .js include file menu.js and is called from a basic .html file called index.html.

Sample Code Explanation menu.js

Browser Detect
The menu.js page begins with a simple browser-detection script. Local variables are set to 1 or 0 to mark what version of browser had been encountered, and then the fShow and fHide variables are set to the correct syntax to be used with the hide and show functions for the detected browser.

Initialize Variables
The following local variables are created and initialized to 0: rightX, leftY, and leftX. These variables will be used to hold the X Y location of the active drop-down menu so that mouse-out functions can be calculated. More information on this will be given below in the displaySubMenu function.

Function: ShowToolbar
This function is the place to set the contents of the menu. The menu contents are stored in a format that is defined in the Menu function (see below). Defining the menu content is made up of 3 basic steps:

  1. A new menu is initialized: menu = new Menu();
  2. The parent levls of the menu are defines: menu.addItem(id, text, hint, location, alternativeLocation);
  3. All of the menu sub-items are added to each parent: menu.addSubItem(idParent, text, hint, location).

Function: Menu
First, some display parameters are set. These parameters give quick access to display borders and the width of the drop-down menus. The bulk of this code dynamically creates the HTML code for both the top level of the drop-down menus and the actual sub menus. The real magic of this drop-down menu code is the fact that each menu is its own div or layer (for NS 4.x). This makes it possible to hide or show a particular div or layer on the appropriate mouse.

Function: DisplaySubMenu
This function makes the applicable menu visible (using the fShow variable from above), and then calculates and sets the rightX, leftY, and leftX variables so that a mouse-out detection is possible. Note how in the Netscape 4.x version, the Z-index must be reset so the layer displays on top. The Z-index is the stacking order of elements in DHTML. The higher the number, the closer to being on top the item is. For some reason, Netscape 4.x wants to place the menus under the main page z-index, so by setting this value to 10 in effect the code ensures that the menu will be on top of the display.

Function: HideAll
Using the fHide variable, all menus are hidden from view.

Function: CalculateSumOffset
Used to calculate the X position of a menu.

Function: UpdateIt
Used to see if the mouse X Y position is outside of the menu - if so, hide the menu.

The last portion of the JavaScript code sets the listeners for page events such as mouse click and moving the mouse to call the appropriate functions to drive the menu code.

Sample Code Explanation index.html

Style Sheet
The first block of code sets up a style sheet. Notice the different entries for each of the three browser types: all.clsMenuItemNS, clsMenuItemIE, and clsMenuItemNS6. Pay particular attention to the font. Because the different browsers render style different ways, different size fonts are used in the style sheets in order for the end-users experience to be as close to the same as possible no matter what browser they are using.

Body Tag
Next comes the body tag. Note all the settings to 0. This is important in getting the menu to display smugly against the top of the page.

Script Tags
Index.html has two script tags. The first is to include the menu.js file. The second calls the showToolbar(); function to build the menu and begin the chain of events to initialize the menu application. At this stage, you will probably notice that the script tags are inside of the body tags. This is because the DHTML writes HTML directly to the Web page, so the script tags must be inside the document body in order for the menu to function correctly.

Sample Code menu.js
//browser detection

if (document.all) {n=0;ie=1;ns6=0;fShow="visible";fHide="hidden";}//ie
else if (document.getElementById){n=0;ie=0;ns6=1;fShow="";fHide="hidden";}//ns6
else if (document.layers) {n=1;ie=0;ns6=0;fShow="show";fHide="hide";}//ns4

//Initialize variables used in displaySubMenu function
rightX = 0;
leftY = 0;
leftX = 0;

//menu contents
function showToolbar(){
//addItem(id, text, hint, location, alternativeLocation);
	menu = new Menu();
	menu.addItem("allaireid", "Allaire", "Allaire links",  null, null);
	menu.addItem("macromediaid", "Macromedia", "Macromedia links",  null, null);
	menu.addItem("newsid", "News", "News Links",  null, null);	
	menu.addItem("searchengineid", "Search Engines", "Search Engines",  null, null);

// addSubItem(idParent, text, hint, location);	
	menu.addSubItem("allaireid", "Home", "http://www.allaire.com/",  "http://www.allaire.com/");
	menu.addSubItem("allaireid", "CF Forums", "ColdFusion Support Conference",  "http://forums.allaire.com/DevConf/index.cfm");
	menu.addSubItem("allaireid", "knowledgebase", "Allaire Knowledgebase ",  "http://www.allaire.com/Support/KnowledgeBase/SearchForm.cfm");
	menu.addSubItem("allaireid", "Products", "Allaire Products",  "http://www.allaire.com/Products/index.cfm");
	menu.addSubItem("allaireid", "Allaire Alive", "Allaire Alive",  "http://www.allaire.com/allairealive/index.cfm");
	menu.addSubItem("allaireid", "Developers Exchange", "Allaire Developers Exchange",  "http://devex.allaire.com/developer/gallery/index.cfm");

	menu.addSubItem("macromediaid", "Home", "http://www.macromedia.com/",  "http://www.macromedia.com/");
	menu.addSubItem("macromediaid", "Products", "Macromedia Products",  "http://www.macromedia.com/software/");
	menu.addSubItem("macromediaid", "Showcase", "Macromedia Showcase",  "http://www.macromedia.com/showcase/");

	menu.addSubItem("newsid", "CBS news", "CBS News",  "http://www.cbsnews.com");
	menu.addSubItem("newsid", "Cnet", "Cnet",  "http://www.cnet.com/");
	menu.addSubItem("newsid", "News.com", "News.com",  "http://news.com");
	menu.addSubItem("newsid", "Wired News", "Wired News",  "http://www.wired.com");
	
	menu.addSubItem("searchengineid", "Yahoo", "Yahoo",  "http://www.yahoo.com/");
	menu.addSubItem("searchengineid", "Infoseek", "Infoseek",  "http://www.infoseek.com/");
	menu.addSubItem("searchengineid", "Excite", "Excite", "http://www.excite.com");
	menu.addSubItem("searchengineid", "HotBot", "HotBot",  "http://www.hotbot.com");

	menu.showMenu();
}

//create the menu
function Menu(){
	this.addItem    = addItem;
	this.addSubItem = addSubItem;
	this.showMenu   = showMenu;
	this.bgColor     = "teal";	
	this.mainPaneBorder = 1;
	this.subMenuPaneBorder = 1;
	this.subMenuPaneWidth = 150;
	lastMenu = null;
	
	HTMLstr = "";
	HTMLstr += "<!-- MENU PANE DECLARATION BEGINS -->\n";
	HTMLstr += "\n";
	HTMLstr += "<div id='MainTable' style='position:relative'>\n";
	HTMLstr += "<table width='100%' cellpadding='0' cellspacing='0' bgcolor='"+this.bgColor+"' border='"+this.mainPaneBorder+"'>\n";
	HTMLstr += "<tr>";
	if (n) HTMLstr += "<td>&nbsp;";
	HTMLstr += "<!-- MAIN MENU STARTS -->\n";
	HTMLstr += "<!-- MAIN_MENU -->\n";
	HTMLstr += "<!-- MAIN MENU ENDS -->\n";
	if (n) HTMLstr += "</td>";
	HTMLstr += "</tr>\n";
	HTMLstr += "</table>\n";
	HTMLstr += "\n";
	HTMLstr += "<!-- SUB MENU STARTS -->\n";
	HTMLstr += "<!-- SUB_MENU -->\n";
	HTMLstr += "<!-- SUB MENU ENDS -->\n";
	HTMLstr += "\n";
 	HTMLstr+= "</div>\n";
	HTMLstr += "<!-- MENU PANE DECALARATION ENDS -->\n";
}

//add drop-down container
function addItem(idItem, text, hint, location, altLocation){
	var Lookup = "<!-- ITEM "+idItem+" -->";
	if (HTMLstr.indexOf(Lookup) != -1){
		alert(idParent + " already exist");
		return;
	}
	var MENUitem = "";
	MENUitem += "\n<!-- ITEM "+idItem+" -->\n";
	if (n){
		MENUitem += "<ilayer name="+idItem+">";
		MENUitem += "<a href='.' class=clsMenuItemNS onmouseover=\"displaySubMenu('"+idItem+"')\" onclick=\"return false;\">";
		MENUitem += "|&nbsp;";
		MENUitem += text;
		MENUitem += "</a>";
		MENUitem += "</ilayer>";
	}
	else{//IE or NS6
		MENUitem += "<td>\n";
		MENUitem += "<div id='"+idItem+"' style='position:relative; font: "+this.menuFont+";'>\n";
		MENUitem += "<a ";
		
		if(ie)
			MENUitem += "class=clsMenuItemIE ";
		if(ns6)
			MENUitem += "class=clsMenuItemNS6 ";
			
		if (hint != null)
			MENUitem += "title='"+hint+"' ";
		if (location != null){
			MENUitem += "href='"+location+"' ";
			MENUitem += "onmouseover=\"hideAll()\" ";
		}
		else{
			if (altLocation != null)
				MENUitem += "href='"+altLocation+"' ";
			else
				MENUitem += "href='.' ";
			MENUitem += "onmouseover=\"displaySubMenu('"+idItem+"')\" ";
			MENUitem += "onclick=\"return false;\" "
		}
		MENUitem += ">";
		MENUitem += text;
		MENUitem += "</a>\n";
		MENUitem += "</div>\n";
		MENUitem += "</td>\n";
	}
	MENUitem += "<!-- END OF ITEM "+idItem+" -->\n\n";
	MENUitem += "<!-- MAIN_MENU -->\n";

	HTMLstr = HTMLstr.replace("<!-- MAIN_MENU -->\n", MENUitem);
}

//add sub-menu items to applicable drop-down containor
function addSubItem(idParent, text, hint, location){
	var MENUitem = "";
	Lookup = "<!-- ITEM "+idParent+" -->";
	if (HTMLstr.indexOf(Lookup) == -1){
		alert(idParent + " not found");
		return;
	}
	Lookup = "<!-- NEXT ITEM OF SUB MENU "+ idParent +" -->";
	if (HTMLstr.indexOf(Lookup) == -1){
		if (n){
			MENUitem += "\n";
			MENUitem += "<layer id='"+idParent+"submenu' visibility=hide bgcolor='"+this.bgColor+"'>\n";
			MENUitem += "<table border='"+this.subMenuPaneBorder+"' bgcolor='"+this.bgColor+"' width="+this.subMenuPaneWidth+">\n";
			MENUitem += "<!-- NEXT ITEM OF SUB MENU "+ idParent +" -->\n";
			MENUitem += "</table>\n";
			MENUitem += "</layer>\n";
			MENUitem += "\n";
		}
		else{//IE or NS6
			MENUitem += "\n";
			MENUitem += "<div id='"+idParent+"submenu' style='position:absolute; background-color: "+this.bgColor+"; visibility: hidden; width: "+this.subMenuPaneWidth+"; top: -300;'>\n";
			MENUitem += "<table border='"+this.subMenuPaneBorder+"' bgcolor='"+this.bgColor+"' width="+this.subMenuPaneWidth+">\n";
			MENUitem += "<!-- NEXT ITEM OF SUB MENU "+ idParent +" -->\n";
			MENUitem += "</table>\n";
			MENUitem += "</div>\n";
			MENUitem += "\n";
		}
		MENUitem += "<!-- SUB_MENU -->\n";
		HTMLstr = HTMLstr.replace("<!-- SUB_MENU -->\n", MENUitem);
	}

	Lookup = "<!-- NEXT ITEM OF SUB MENU "+ idParent +" -->\n";
	if (n)  MENUitem = "<tr><td><a class=clsMenuItemNS title='"+hint+"' href='"+location+"'>"+text+"</a><br></td></tr>\n";
	else //IE or NS6
		if(ie)
			MENUitem = "<tr><td><a class=clsMenuItemIE title='"+hint+"' href='"+location+"'>"+text+"</a><br></td></tr>\n";
		if(ns6)
			MENUitem = "<tr><td><a class=clsMenuItemNS6 title='"+hint+"' href='"+location+"'>"+text+"</a><br></td></tr>\n";
	MENUitem += Lookup;
	HTMLstr = HTMLstr.replace(Lookup, MENUitem);

}

//write out main menu bar
function showMenu(){
	document.writeln(HTMLstr);
}

//show submenu, and then calculate its x and y coordinates
function displaySubMenu(idMainMenu){
	var menu;
	var submenu;
	if (n){
		submenu = document.layers[idMainMenu+"submenu"];
		if (lastMenu != null && lastMenu != submenu) hideAll();
		submenu.left = document.layers[idMainMenu].pageX;
		submenu.top  = document.layers[idMainMenu].pageY + document.layers[idMainMenu].clip.height;
		submenu.visibility = fShow;
		submenu.zIndex=10
		leftX  = document.layers[idMainMenu+"submenu"].left;
		rightX = leftX + document.layers[idMainMenu+"submenu"].clip.width;
		leftY  = document.layers[idMainMenu+"submenu"].top+document.layers[idMainMenu+"submenu"].clip.height;
	} else if (ns6) {
	
	menu = document.getElementById(idMainMenu);		
		submenu = document.getElementById(idMainMenu+"submenu");
		if (lastMenu != null && lastMenu != submenu) hideAll();
		submenu.style.left = calculateSumOffset(menu, 'offsetLeft');
		submenu.style.top  = document.getElementById(idMainMenu).offsetHeight + 4;
		submenu.style.visibility = fShow;
		leftX  = calculateSumOffset(menu, 'offsetLeft');
		rightX =leftX + document.getElementById(idMainMenu+"submenu").offsetWidth;
		leftY  = document.getElementById(idMainMenu+"submenu").offsetHeight + window.pageYOffset + 4;
	
	} else if (ie) { 
		menu = eval(idMainMenu);
		submenu = eval(idMainMenu+"submenu.style");
		if (lastMenu != null && lastMenu != submenu) hideAll();			
		submenu.left = calculateSumOffset(menu, 'offsetLeft');
		submenu.top  = document.all[idMainMenu].offsetHeight + 4;
		submenu.visibility = fShow;
		leftX  = document.all[idMainMenu+"submenu"].style.posLeft;
		rightX = leftX + document.all[idMainMenu+"submenu"].offsetWidth;
		leftY  = document.all[idMainMenu+"submenu"].style.posTop+document.all[idMainMenu+"submenu"].offsetHeight + 4;
	}
	lastMenu = submenu;
}

//hide the submenu
function hideAll(){
	if (ns6){
		if (lastMenu != null) 
			{lastMenu.style.visibility = fHide;}
	}else{
		if (lastMenu != null) 
			{lastMenu.visibility = fHide;}
	}		
}

//used to calculate position of a submenu
function calculateSumOffset(idItem, offsetName){
	var totalOffset = 0;
	var item = eval('idItem');
	do{
		totalOffset += eval('item.'+offsetName);
		item = eval('item.offsetParent');
	} while (item != null);
	return totalOffset;
}

//close menu on mouse out of menu containor
function updateIt(e){
	if (ns6){
		var x = e.pageX;
		var y = e.pageY;
		if (x > rightX || x < leftX) hideAll();
		else if (y > leftY) hideAll();
	}
	 else if (ie){
		var x = window.event.clientX;
		var y = window.event.clientY;
		if (x > rightX || x < leftX) hideAll();
		else if (y > leftY) hideAll();
	}
	else if (n){
		var x = e.pageX;
		var y = e.pageY;
		if (x > rightX || x < leftX) hideAll();
		else if (y > leftY) hideAll();
	}
}

//set page to hide menus on a mouse click or on mouseout of menu containor
if (ns6)
{
	document.body.onclick=hideAll;
	document.body.onmousemove=updateIt;
}
else if (ie)
{
	document.body.onclick=hideAll;
	document.body.onscroll=hideAll;
	document.body.onmousemove=updateIt;
}
else if (n)
{
	document.onmousedown=hideAll;
	window.captureEvents(Event.MOUSEMOVE);
	window.onmousemove=updateIt;
}

Sample Code index.html
<html>
<head>

<style>
all.clsMenuItemNS{font: bold x-small Verdana; color: white; text-decoration: none;}
.clsMenuItemIE{text-decoration: none; font: bold xx-small Verdana; color: white; cursor: hand;}
.clsMenuItemNS6{text-decoration: none; font: bold x-small Verdana; color: white; cursor: hand;}
A:hover {color: yellow;}
</style>

<title>Menu Test page</title>
</head>

<body marginwidth="0" marginheight="0" leftmargin="0" topmargin="0" bgcolor="white">

<script language="JavaScript" src="menu.js"></script>
<script language="JavaScript">
//start the menu
showToolbar();
</script>

<div align="center"><h1>Test Page</h1></div>

</body>
</html>
Conclusion

This article has given a very basic example of how to construct a cross-browser DHTML drop-down menu system. By digging into this code, you can see many examples of the ways DHTML can interact with a page. Also - and perhaps more importantly - you can get an idea for the syntax in using DHTML to work with different browsers. Modify and improve, and let's fill Allaire's Developers Exchange up with DHTML powered custom tags!