Back to the Web Developer's Journal Main Page
internet.com
side nav bar

Ever wanted to see a detailed walk-through of a sophisticated JavaScript program? Well get your hiking boots on and join us for a stroll through the entire length of Aaron Prenot's cascading menu script.
Javascript Articles:

HOW DID THEY DO THAT???

Find out in:
Amazing HTML



Site Map

Jobs at webdeveloper.com


Check out our Web-based
Discussion Groups:

Check out and join our email-based Mailing Lists for Web developers.


Developer Channel
FlashKit
Jobs.webdeveloper
JavaScript.com
JavaScriptSource
JustSMIL
ScriptSearch
Streaming Media World
WebDeveloper.com
WebReference
XMLFiles
WDVL
Discussion Groups Book Reviews Software Reviews Download Web Tools

Start that second pot of coffee.

You can refer to the full JavaScript code for HMenu.js at any time through this link.

Cascading Menu Script Explained (2)

<<< Part 1

setupMenu()


function setupMenu(menu){
	if(menu.hasChildren == true){
		for(var i=0; i < menu.subMenus.length; i++){
			setupMenu(menu.subMenus[i]);
		}
	}
	
	tMenu = eval(menu.id);
	tMenu.noWrap = true;
	tMenu.hasChildren = menu.hasChildren;
	tMenu.isChild = menu.isChild;
	tMenu.hasVisibleChild = false;
	tMenu.visibleChild = null;
	tMenu.onselectstart = returnFalse;
	tMenu.onclick = handleMenuClick;
	tMenu.currWidth = 0;
	
	for(var i=0; i < menu.items.length; i++){
		setupItem(menu.items[i]);
	}
	tMenu.style.pixelWidth += 5;
	
	for(var i=0; i < menu.items.length; i++){
		tItem = eval(menu.items[i].id);
		if(!IE4){
			tItem.style.width = "100%";
			if(tItem.hasMenu == true) {
				tItem.more.style.position = "absolute";
				tItem.more.style.pixelLeft = (tMenu.style.pixelWidth - 17);
			}
		}
	}
	
	if(menu.isChild == true){
		tMenu.parentMenu = eval(menu.parentMenu.id);
		tMenu.parentItem = eval(menu.parentItem.id);
	}
}

Boy, that's a doozie! Ok, let's start at the top. Just as with the other two recursive function, the first thing we do is look for submenus. If the menu has any submenus, setupMenu() calls itself again. But let's just get to the good part.

For every menu, the first thing that happens is we create a reference to the HTML element using eval(). Then we set a whole bunch of variables on the element. First we set the noWrap attribute to true. This prevents wrapping to the next line, forcing the menu to its full width. Then we give the element the variable hasChildren, just like its object counterpart has, to tell if the menu has any submenus. The same goes for the isChild variable.

The next two variables help us out when we are closing the menu. If the menu has a submenu hanging out, then the hasVisibleChild variable will tell us so, and the menu that is showing is pointed at by the visibleChild variable.

Next we capture some events, an integral part of DHTML. First we capture the onSelectStart event, which happens when a user tries to highlight text, and we make that event run a dummy function called returnFalse. That makes it so that the user can't highlight text in a menu. Then we set the onClick event to run a function called handleMenuClick (explained later). Then we set a dummy variable used later to reposition submenu items' arrows.

Now we start calling more functions again (uh-oh), only this time the function is not recursive (whew!). For every item that is in the menu, we have to do a set up. This is done in the function setupItem(). Like most of the function before, we are going to pass an object, only this time it's an item object.

setupItem()


function setupItem(item){
	tItem = eval(item.id);
	tItem.highlight = highlight;
	tItem.onmouseover = tItem.highlight;
	tItem.onclick = handleItemClick;
	tItem.ondragstart = returnFalse;
	tItem.parentMenu = eval(item.parentMenu.id);
	tItem.hasMenu = false;
	tItem.menu = null;
	tItem.noWrap = true;
	
	if(item.hasMenu == true){
		tItem.innerHTML += "4";
		tItem.more = eval(item.id + "_more");
		tItem.menu = eval(item.menu.id); 
		tItem.hasMenu = true;
	}
	
	if(!IE4) {
		tItem.parentMenu.style.pixelWidth = Math.max(tItem.parentMenu.currWidth, 
								tItem.offsetWidth);
	}
}

setupItem() starts out by referencing the actual HTML element, again using eval() with the id. Then we give the element a function - highlight - which will change the color of the item as the mouse is moved over it, and open a submenu if the item is a submenu item. But in order for the highlight function to be called when the mouse moves over the item, we have to capture the onMouseOver event for the item and set it to run highlight. It's important to notice, however, that we don't just use highlight, but tItem.highlight. That is because highlight uses the this object, so in order for that to work, you have to use the object that highlight is attached to and not the the function. Then we capture the onClick event and make it run handleItemClick, which will close the menu. Also, we capture the onDragStart event, which happens when the user tries to select text. onDragStart could cause a lot of problems, so we make it run the dummy function to do nothing.

Next we set the item's parentMenu variable. This is just a reference to the menu that the item is in. Now, we'll move on to the if statement which determines whether or not this item is a submenu item or not. If it is, then the item's hasMenu has to be set to indicate that it is a submenu item. Also, the menu variable has to be set to point to the submenu to open. Notice that we add something to the HTML inside the element. That is a span that uses Webdings font to display the little arrow. Then we add a reference to the little arrow so that we can position it later.

Lastly, and only in IE5, we set that item's parent menu's width so that everything fits right.

Back to setupMenu()


function setupMenu(menu){
	if(menu.hasChildren == true){
		for(var i=0; i < menu.subMenus.length; i++){
			setupMenu(menu.subMenus[i]);
		}
	}
	
	tMenu = eval(menu.id);
	tMenu.noWrap = true;
	tMenu.hasChildren = menu.hasChildren;
	tMenu.isChild = menu.isChild;
	tMenu.hasVisibleChild = false;
	tMenu.visibleChild = null;
	tMenu.onselectstart = returnFalse;
	tMenu.onclick = handleMenuClick;
	tMenu.currWidth = 0;
	
	for(var i=0; i < menu.items.length; i++){
		setupItem(menu.items[i]);
	}
	tMenu.style.pixelWidth += 5;
	
	for(var i=0; i < menu.items.length; i++){
		tItem = eval(menu.items[i].id);
		if(!IE4){
			tItem.style.width = "100%";
			if(tItem.hasMenu == true) {
				tItem.more.style.position = "absolute";
				tItem.more.style.pixelLeft = (tMenu.style.pixelWidth - 17);
			}
		}
	}
	
	if(menu.isChild == true){
		tMenu.parentMenu = eval(menu.parentMenu.id);
		tMenu.parentItem = eval(menu.parentItem.id);
	}
}

After we get done setting up all of the menu's items, we make the menu an extra 5 pixels wider give the little arrow for submenus a little extra room. Then we loop through all the items in the menu again, this time to set their width to take up the whole menu, so that you can move the mouse over any part of the item and still set off the highlight function. We only do this in IE5 though, because IE4 does a better job of taking care of things than IE5. After we set the item to take up the full width of the menu, we check to see if the item is a submenu. If it is, then we position the little arrow to be right near the edge of the menu. We only have to change the arrow's x position, because it will automatically be lined up in the right row. An element's x and y positions are stored in style.pixelLeft and style.pixelTop.

Lastly, if this menu is a submenu, we need to make a reference the menu that it is coming from, and a reference to the submenu item that makes it appear.

Way Back to initMenu()

Once all the menus have been setup in the final loop of findMenus(), execution goes back to where we started, initMenu(), where we call the last function needed to setup the menus.

attachMenus()


function attachMenus(){
	for(var i in document.all){
		if(document.all[i].menu){
			document.all[i].onclick = showMenu;
		}
	}
}

To make adding menus as painless as possible, webmasters can add a menu to any item on the page, simply by adding the menu="" property to the HTML element. However, on its own, that property does nothing. That's where attachMenus() comes into play. All it does is loop through every element on the page, checking to see if there is a property on that element called menu. If there is, then we capture the onClick event for that element and set it to run showMenu, which as we'll explain in a moment, displays the menu held in the menu variable of the item that called it. What's really cool is that when showMenu is called for a submenu item, the submenu item has a variable called menu, and if it is called from an HTML element, that element has a menu variable too! Isn't it grand how some things work out?

The Leftovers

Ok, so we have gotten though how the menus are setup, but what actually makes the menus appear and disappear? What makes the menu items become highlighted and unhighlighted? What makes Mountain Dew taste so funny at 4 a.m.? We'll answer all of those questions (well, most of them) right now.

showMenu()


function showMenu(menu, x, y){
	event.cancelBubble = true;
	if(menu){
		if(menu.isChild == true){
			menu.style.pixelTop = menu.parentItem.offsetTop + 
						menu.parentMenu.offsetTop + 4;
						
			menu.style.pixelLeft = menu.parentMenu.offsetLeft + 
						menu.parentMenu.offsetWidth - 4;
			menu.parentMenu.hasChildVisible = true;
			menu.parentMenu.visibleChild = menu;
			menu.style.zIndex = menu.parentMenu.style.zIndex + 1;
		} else if(x && y){
			menu.style.pixelTop = y;
			menu.style.pixelLeft = x;
			menuContainer.activeMenu = menu;
			document.onclick = menuContainer.closeAll;
		} 
	} else {
		menu = eval(this.menu);
		menu.style.pixelTop = event.clientY;
		menu.style.pixelLeft = event.clientX;
		menuContainer.activeMenu = menu;
		document.onclick = menuContainer.closeAll;
	}
	menu.className = "visibleMenu";
	return false;
}

showMenu() is the first function called to interact with a menu. It just shows a menu. It can work in two different ways. First, it can be called as a function from a script (which is how it is called when a submenu item is moused over), in which case you pass a reference to the menu to be displayed, and x and y location on the page for it to be displayed at if the menu is a top level menu. Second, it can be called as the result of an element being clicked, like when the user clicks on an element that has had the onClick event captured.

In the first case, we check to see if the menu is a child menu. If it is, we have to make sure that parent menu knows it has a menu being displayed by setting parentMenu.hasVisibleChild to true, and then tell the parent menu which menu is being displayed, by setting parentMenu.visibleChild to a reference to this menu. Then we also have to set its x and y location. Its x location is easy - it is the parent menu's x location plus the parent menu's width (stored in style.pixelWidth). The y location is a bit trickier - it is the parent menu's y location plus the parent item's y location (ok, so it's not so tricky). In addition to all that, we modify its z-Index, which determines whether or not it is displayed above or below something else. Since a submenu layers over the parent menu, we set its z-Index to be one greater that the parent menu.

If, however, the menu is not a child menu, we simply set the x and y to the locations passed to showMenu(), and then set the menuContainer.activeMenu to be this menu. We also have to capture the mouse clicks on the document, because if the user clicks out of the menu, we want to close the menu.

In the second case, the menu variable is going to be from the element as a string, so we need to reference the menu with eval(). Then we set the menu's x and y location to be the location of the mouse, stored in the event object that gets passed. We also have to remember to set the menuContainer.activeMenu, and capture the mouse clicks on the document.

And in either case, we always have to make the menu visible. After all, that's what this function does right. We do that by changing the menu class from menu to visibleMenu

highlight()


function highlight(){
	if(activeItem != null){
		if(activeItem != this){
			unhighlight(activeItem);	
		} else {
			return;
		}
	}

	event.cancelBubble = true;

	this.className = "menuItemOver";
	activeItem = this;

	// don't open a menu thats already open
	if((this.hasMenu == true) 
		&& (this.parentMenu.hasVisibleChild == true) 
		&& (this.parentMenu.visibleChild == this.menu)) return;

	// if there is a menu open, close it
	if(this.parentMenu.hasChildVisible == true){
		hideMenu(this.parentMenu.visibleChild);
	}

	// if this item has a menu, show it
	if(this.hasMenu){
		showMenu(this.menu);
	}
}

highlight() is a very important function in this script. It does a lot of work. Whenever the user moves the mouse over a menu item, this function gets called. The first thing it does is to check and see if there is an item already highlight, stored in the global variable activeItem, which is created at the beginning of the script. If there is an item highlight, it checks to see if the item is the right one. If it is, then the function has already done its job and quits. If it isn't, then it unhighlights the one it's just found, and goes on to highlight the correct item by changing its class from menuItem to menuItemOver. It then takes its place on the throne by naming itself as the active item.

In the subsequent, very complicated if statement, we determine whether or not this item is a submenu item. If it is, we check to see if the parent menu has a submenu open already. If so, then we check to see if it is the menu that goes with this item. If it is then we have nothing else to do, and we quit. If not, we keep on truckin'. Next, whether this is a submenu item or not, we check to see if the parent menu has a submenu open, and if so, we close it by calling hideMenu(). Then, if this is a submenu item, we show the menu for this item, and quit.

hideMenu()


function hideMenu(menu){
	// to handle the careless child menu hiding down below
	if(menu == null) return false;
	event.cancelBubble = true;

	// i do this kind of carelessly.  i was having trouble otherwise
	hideMenu(menu.visibleChild);
		if(menu.isChild == true){
		menu.parentMenu.hasChildVisible = false;
		menu.parentMenu.visibleChild = null;
	} else {
	  document.onclick = "";
	  menuContainer.activeMenu = null;
	}
	menu.className = "menu";
}

Sorry, this is another one of those recursive functions. But this one is a bit wacky. The first thing we do in this function is make sure that the menu passed to it really exists. If not, then we don't do anything. hideMenu() could potentially have trouble hiding open submenus, so it tries to close a visible menu even if there isn't one. If there is one, hideMenu() calls itself to hide the visible submenu, until there isn't a submenu to hide. Then, if this is a submenu, we set the parent menu's information regarding whether or not there is a submenu visible to denote that there isn't. If this isn't a submenu, then we have to let the world know that there isn't a menu visible anymore by clearing menuContainer.activeMenu and we also stop capturing mouse clicks from the document because we don't need them. Finally, we make the menu actually disappear by setting class name back to menu.

unhighlight()


function unhighlight(menu){
	event.cancelBubble = true;
	menu.className = "menuItem";
}

Finally! This is the last function for making the menus work. unhighlight() simply returns the item back to its normal state by resetting its class name back to menuItem. And thats it! Its that simple. You probably never thought there would be a simple function within this script. Well, there it is.

<<< Part 1

If you have any further questions, want to report a bug, or even if you want to tell Aaron what a good job he did, email him at aprenot@hotmail.com. Or you can try to contact him in the Yahoo! JavaScript club, as Caff Addict. He frequently makes submissions there. Also, check out his web page at http://aprenot.brinkster.net.
Back to the Web Developer's Journal
Contact WDJ   •    Suits!   •    Propheads!   •    Ponytails!
Discuss   •    Subscribe   •    Search


internet.com

IT | Developer | Internet News | Small Business | Personal Technology | International | Search internet.com | Advertise | Corporate Info
Newsletters | Tech Jobs | E-mail Offers

internet.commerce
Be a Commerce Partner                                
  


The Network for Technology Professionals

Search:

About Internet.com

Legal Notices, Licensing, Permissions, Privacy Policy.
Advertise | Newsletters | E-mail Offers