
You can refer to the full JavaScript code for HMenu.js at any time through this link.
The Script
Let'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.
function initMenu(){
// test for IE4+, it won't work otherwise
if(!document.all) return false;
menuContainer.activeMenu = null;
menuContainer.closeAll = closeAll;
findMenus();
attachMenus();
}
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()
function findMenus(){
var cTag = menuContainer.children;
for(var i=0; i < cTag.length; i++){
tcTag = cTag[i];
if(tcTag.className == "menu"){
var tMenu = findSubMenus(tcTag);
menus[menus.length] = tMenu;
}
}
for(var i=0; i < menus.length; i++){
var tcTag = menus[i]
moveHTML(tcTag);
}
for(var i=0; i < menus.length; i++){
var tcTag = menus[i];
setupMenu(tcTag);
}
}
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
function Menu(){
this.id = "";
this.subMenus = new Array();
this.items = new Array();
this.hasChildren = false;
this.isChild = false;
this.parentMenu = null;
this.parentItem = null;
}
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:
var menuObject = new Menu();
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()
function findSubMenus(menu){
var cMenu = menu.children;
var tMenu = new Menu();
tMenu.id = menu.id;
for(var i=0; i < cMenu.length; i++){
var tcMenu = new Item();
tcMenu.id = cMenu[i].id;
if(tcMenu.id.indexOf("subMenu") != -1){
++i;
var subMenu = cMenu[i];
tMenu.subMenus[tMenu.subMenus.length] = findSubMenus(subMenu);
tMenu.subMenus[(tMenu.subMenus.length - 1)].isChild = true;
tMenu.subMenus[(tMenu.subMenus.length - 1)].parentMenu = tMenu;
tMenu.subMenus[(tMenu.subMenus.length - 1)].parentItem = tcMenu;
tMenu.hasChildren = true;
tcMenu.hasMenu = true;
tcMenu.menu = tMenu.subMenus[(tMenu.subMenus.length - 1)];
}
tcMenu.parentMenu = tMenu;
tMenu.items[tMenu.items.length] = tcMenu;
}
return tMenu;
}
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().
function findMenus(){
var cTag = menuContainer.children;
for(var i=0; i < cTag.length; i++){
tcTag = cTag[i];
if(tcTag.className == "menu"){
var tMenu = findSubMenus(tcTag);
menus[menus.length] = tMenu;
}
}
for(var i=0; i < menus.length; i++){
var tcTag = menus[i];
moveHTML(tcTag);
}
for(var i=0; i < menus.length; i++){
var tcTag = menus[i];
setupMenu(tcTag);
}
}
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()
function moveHTML(menu){
if(menu.hasChildren == true){
for(var i=0; i < menu.subMenus.length; i++){
moveHTML(menu.subMenus[i]);
}
}
var tMenu = eval(menu.id);
var tMenuHTML = tMenu.outerHTML;
tMenu.outerHTML = "";
menuContainer.innerHTML += tMenuHTML;
}
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...Again
Now 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.
function findMenus(){
var cTag = menuContainer.children;
for(var i=0; i < cTag.length; i++){
tcTag = cTag[i];
if(tcTag.className == "menu"){
var tMenu = findSubMenus(tcTag);
menus[menus.length] = tMenu;
}
}
for(var i=0; i < menus.length; i++){
var tcTag = menus[i]
moveHTML(tcTag);
}
for(var i=0; i < menus.length; i++){
var tcTag = menus[i];
setupMenu(tcTag);
}
}
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.