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.
|