This article follows on from Cascading Menus and fully explains how the JavaScript in that menu program works. It's also a crash course in Dynamic HTML (DHTML), and to understand it you will need to know basic JavaScript.
August 16, 2000
You can refer to the full JavaScript code for HMenu.js at any time through this link. The ScriptLet's recap on the elements dealt with in Cascading Menus. A menu is made up of four basic items - the menu itself, menu items, submenu items, and submenus. A menu item is a single entry in a menu. A submenu item opens a submenu when the mouse is moved over it. The menu is the container for all the menu items and submenu items. All menus that are not submenus are referred to as top level menus. The menus themselves are defined using HTML and a lot of DIV elements, one placed inside the other, and all placed inside the grandaddy DIV element, menuContainer. initMenu()To make the menus come to life, the webmaster has to add onload="initMenu()" to their document's BODY tag. initMenu() is the function that gets the ball rolling. Which brings us on to the file where all the real code is contained, HMenu.js. The rest of the article will walk you through it, section by section. initMenu() looks to be a very plain function, and it is. First, it makes sure that the browser is Internet Explorer 4 or above. This is done with if(!document.all) return false;. What this does is check to see if the object document.all exists. document.all is the object that Internet Explorer uses to keep track of all the information in a web page. Netscape Navigator and others don't use this object, so if it doesn't exist then we aren't using Internet Explorer and we exit the function. Next we initialize the variable menuContainer.activeMenu. menuContainer is the DIV element that we use to hold all the menu definitions. We just give it a variable activeMenu so that we can always tell which, if any, menu is open. Then we add a function to the menuContainer element, closeAll. This function will close the active menu, and all its submenus.
After we attach the variable and the function to menuContainer, we then run the function findMenus(). This function sorts though all the HTML that we created to find all the real menu definitions, including the submenus. findMenus() It does this by using the special collection (a collection is an array of objects or elements) children. This is all of the elements immediately inside of any given HTML element. I say immediate because, like real children, you don't count the ones that belong to one of your children (grandchildren). It stores all of the children of the menuContainer element to the variable cTag. Then, we loop through each of the children, storing the actual child element in the variable tcTag. You don't have to do this, but it makes it easier to type later. Then we check to see if it is a menu. Since all menus, including submenus, have the class="menu" inside their DIV element, we can check for that by examining the child's className property. If the className property is set to menu, then bingo, we have a menu! We must also check to see if there are any submenus. That is done with the function findSubMenus(). We pass findSubMenus() the menu element, and then it gives us back a genuine menu object. Now, before we delve into this new function, let's explain what a menu object is. The Menu Object A menu object is a special type of function that doesn't really do anything. Instead, it just stores information into whatever variable is used to create the object using this. To create a menu object, you use the new operator: So in our case, anything stored using this, would actually be stored in menuObject. In other programming languages, it would be called a class. A menu object contains its id (set later), a collection of all its subMenus (if it has any), a collection of the items in the menu, and information regarding its location in the menu hierarchy. hasChildren tells us whether or not this menu has any submenus and isChild tells us if this is a submenu. If this is a submenu, parentMenu will point to the menu that this sprouts from, and parentItem will point to the item that this sprouts from. findSubMenus() findSubMenus() starts like findMenus() by storing the children for the element in the variable cMenu. Then we create a menu object, which we discussed a moment ago, in tMenu. Now we set the menu object's id to be the same as the element's id. Once again we loop through all the of element's children, only this time we create a new item object in tcMenu. An item object stores the item element's id, a variable that points to the menu it is in (parentMenu), a variable to tell us if the item is a submenu item (hasMenu), and if it is a submenu item the variable menu will point to the submenu. We are trying to see if the item's class is set to subMenu. As we already know, a submenu item comes just before the definition for the submenu, so when we find a submenu item we automatically jump to the next child (++i). Then we search this new submenu for more more submenus by calling findSubMenus() again, passing the newly found submenu. The menu object returned by findSubMenus() is then added to the current menu's subMenus collection, and we set the properties in the menu object that pertain only to a submenu. Confusing? Functions like this (called recursive functions, because they call themselves) usually are. Let's try a practical example from the first article, with the editors. First findMenus() finds menu1, the only menu we have. It calls findSubMenus() and passes menu1. findSubMenus() then starts looping through menu1 and immediately finds the first submenu item, the item for Bruce Morris (submenu1_1). It then jumps to the next child in menu1, which is menu1_1, the submenu for Bruce Morris. We call findSubMenus() again, this passing menu1_1. When we loop though menu1_1's children, we never find any items with subMenu in className, so instead of calling findSubMenus() again, we create all the menu items, and then pass back the new menu object. Now we are back in the original findSubMenus() call, and in the next loop we find the submenu item for Charlie Morris, and the process starts again. After we loop through all the submenu items and menus, we pass back the menu object for menu1 to findMenus() and we try to find another menu, but we don't, so findMenus() continues in a different way. Ok, back to findSubMenus(). As we are looping though the menu's items, if the item is not a submenu item then we don't need to call findSubMenus() again. After we check to see if an item is a submenu item, whether it is or not, we add the item to the menus items collection. Once we exhaust all the items in the menu, we will pass the menu object back to findMenus()... findMenus() - Continued...As we search menuContainer for menus, the menu objects that are returned from findSubMenus() are stored in a global array called simply enough menus. This array will contain all of the top level menus for the page. After we find all the menus, it's time to do the complicated stuff. For convenience, content for the menus is written in HTML, but to make all the menus separate boxes, we have first to move the HTML around. This is accomplished in the second loop in findMenus(). With this loop, instead of cycling through all the children of the menu elements, we use the menu objects created by findSubMenus(). The loop goes though menus, the array containing all of the top level menu objects. We store the menu object in the variable tcTag, and then we call a new function, moveHTML(). We pass the menu object to moveHTML(), and now we brace ourselves, because this is another one of those recursive functions. moveHTML() As simple as this code looks, it does a bundle. First we start by checking to see if this menu has any submenus, because if it does, we will need to move that HTML first. This is where the recursiveness comes into play. If the current menu (right now the top menu) has any submenus, we will loop through all of the submenus. For every submenu, we will call moveHTML() again, and again check for submenus, until we get to the very last (bottom) submenu. For every menu that we find, the code after the loop will execute. First we reference the actual HTML element. Since an element is referenced by its id, we use the eval() function. This will turn the string contained in the menu object's id variable (say "menu1") into the the code for referencing the element (menu1). Next we temporarily store the element's HTML, contained in its outerHTML variable. Then we erase the element from the document by setting its HTML (in outerHTML) to nothing. Then we recreate it by adding the HTML inside the menuContainer element. The HTML inside an element is stored in innerHTML, hence the inner. And now we have the menu element restored, good as new, only now, it isn't nested inside of another menu any more. In case you're wondering why we can't move the HTML as soon the menu is found, it's because we can't move the child of an element that we've already referenced, without creating errors. findMenus() - Continued...AgainNow that we have moved all the HTML around so that the menus will look right, we have to make the menus actually do something. Right now, they are just a bunch or boxes (at least now they're pretty) that do nothing. They don't even appear yet. The last loop in findMenus() takes care of that (but not all by itself) and is the most complicated part of the script. This last loop, once again, goes through the menus array, only this time it calls the latest greatest function setupMenu(). Once again we pass the menu object to the function, and once again we will brace ourselves for a recursive ride. |
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.
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.
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.
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.
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?
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.
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
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.
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.
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.