Dynamic MenuInstead of answering questions individually, I thought I would run through how I created the menu, so others can benefit.

Here is a step by step guide. You should have an HTML page ready to add code into, and have a blank external javascript file linked in the header to add the javascript functions to.

Step 1: The HTML

The menu is just a simple unordered list with the ID set as “menu”. Each sub-level of the menu is another unordered list within the parents list item. You can create as many levels as your heart desires, as the javascript we’ll write in the next step can handle this.

Here is the HTML I’ll use for the example:


<ul id="menu">
 <li><a href="fruit.htm" title="Fruit">Fruit</a>
   <ul>
     <li><a href="apple.htm" title="Apple">Apple</a>
      <ul>
       <li><a href="grannysmith.htm" title="Granny Smith">Granny Smith</a></li>
      </ul>
     </li>
     <li><a href="orange.htm" title="Orange">Orange</a></li>
     <li><a href="banana.htm" title="Banana">Banana</a></li>
   </ul>
 </li>
 <li><a href="veg.htm" title="Fruit">Vegetables</a>
   <ul>
     <li><a href="carrot.htm" title="Carrot">Carrot</a></li>
     <li><a href="onion.htm" title="Onion">Onion</a></li>
     <li><a href="peas.htm" title="Peas">Peas</a></li>
   </ul>
 </li>
</ul>

This will produce a vanilla HTML list which you can check out in example 1.

Step 2: Collapsing the Menu

The next step is to make that vanilla HTML list into something a little less rigid. This is where we’ll dip into some fancy DOM scripting.

The first thing we want to do is collapse the menu once it’s been loaded. We’ll do this by writing a recursive function called collapseMenu(), to traverse through each nested list in your menu, and hide it.


function collapseMenu(node) {
 if (!document.getElementById) return false;
 if (!document.getElementById("menu")) return false;
 if (!node) node = document.getElementById("menu");

 if (node.childNodes.length > 0) {
  for (var i=0; i<node.childNodes.length; i++) {
   var child = node.childNodes[i];
   if (child.nodeName == "UL") {
    child.style.display = "none";
   }
   collapseMenu(child);
  }
 }
}

For those who’re interested, a recursive function is one that calls upon itself.

In this function, we start at the very first node which is the <ul id=”menu”> node. We then count how many child nodes there are, and if there’s 1 or more, we check to see if any of the children are <ul> nodes. If so, then we hide them, by setting child.style.display = “none”.

Then there’s that recursive part, we call the same function again, but this time pass in the child as the starting node. This will go through and check all the grandchildren. The recursion will continue until there are no remaining child nodes to cycle through and everything is hidden.

Step 3: Bringing Those Links to Life

As it stands right now, the HTML we created in step one has no javascript associated with it. By removing the javascript completely from the HTML and placing it in an external file we are paving the way for graceful degradation. This means that if any of your users have javascript turned off, the menu will not collapse, and they will be able to use your navigation as though it was a static list.

So, we want to create a function called prepareMenu() that will go through and find every anchor within the menu, and add an “onclick” event to each one.


function prepareMenu() {
 if (!document.getElementById || !document.getElementsByTagName) return false;
 if (!document.getElementById("menu")) return false;
 if (!menu.getElementsByTagName("a")) return false;

var links = document.getElementById("menu").getElementsByTagName("a");
 for (var i=0; i<links.length; i++) {
  links[i].onclick = function() {
   toggleMenu(this.parentNode.getElementsByTagName("UL")[0], this.href);
   return false;
  }
 }
}

For every link within the menu, we have added an “onclick” event, which calls the function toggleMenu(). To this function, we will pass the nested list which represents the sub menu items, and also the “href” (destination) of the link.

Here is the code for toggleMenu() which will handle the hiding and displaying of sub-menus when a link is clicked:


function toggleMenu(node, link) {
 if (!document.getElementById) return false;
 if (!link) return false;
 if (!node) location.href = link.href;

 if (node.style.display == "") {
  node.style.display = "none";
 } else {
  node.style.display = "";
 }
}

This is pretty straightforward, the function will check the display property of the node, if it is hidden then display it, if not then hide it. One small detail however is that if the node passed to this function is null, then the links href is followed. This means that if the link clicked has no submenu, then the function will direct the user to the links location rather than trying to display a non-exisistent sub-menu.

Step 4: Add the OnLoad Events

Right now we have our functions ready to go, but they are not being called at all by our code. This means they will quite happily sit in your javascript file doing absolutely nothing. We need to add two “onload” events so that when the page has finished loading, both collapseMenu() and prepareMenu() are executed.

We can add multiple “onload” events using the excellent addLoadEvent() function written by Simon Willison:


function addLoadEvent(func) {
 var oldonload = window.onload;
 if (typeof window.onload != 'function') {
  window.onload = func;
 } else {
  window.onload = function() {
   oldonload();
   func();
  }
 }
}

You can add this function directly into your main javascript file, or you can create a separate javascript file with just this function, and include it with a <script> tag in the header of your HTML.

Now that we have Simon’s function to use, we can add two lines to the top of our javascript file:


addLoadEvent(collapseMenu);
addLoadEvent(prepareMenu);

This will ensure that when the page has finished loading, both the collapse and prepare menu functions are executed.

Now that we have most of the javascript and HTML in place, you should see something like example 2 when you load your page and click around the menu.

Although this is a perfectly adequate menu, it’s a little boring. Now is the time to snazz up the menu with a little extra javascript and some CSS love.

Step 5: Bring on the Goodies

For the menu I made for the Durable WordPress Theme, whenever you clicked a top level node of the menu, all the other levels would collapse. This meant that the menu wouldn’t ever get too big, and would only allow the user to see one level at a time.

To collapse the top level nodes when any top level node is clicked, we need to add a check to toggleMenu() and create another function hideTopLevels():


function hideTopLevels() {
 if (!document.getElementById) return false;
 if (!(node = document.getElementById("menu"))) return false;

 if (node.childNodes.length > 0) {
  for (var i=0; i<node.childNodes.length; i++) {
   var child = node.childNodes[i];
   for(var j=0; j<child.childNodes.length; j++) {
    var grandchild = child.childNodes[j];
    if (grandchild.nodeName == "UL") {
     if (grandchild.style.display == '') {
      grandchild.style.display = "none";
     }
    }
   }
  }
 }
}

Now we must add a quick check in toggleMenu() that will call this function if we have clicked a top level node:

Add in these lines after if (!node) return false;


if (node.parentNode.parentNode.id == "menu") {
 hideTopLevels();
}

Now you should have something like example 3 where top level nodes will collapse when another is clicked.

Finally, we can really spice up the menu by adding transitional effects using the Script.aculo.us javascript library and adding some nice CSS styles.

Download Script.aculo.us and link the javascript files in the <head> section of your HTML. If you are unsure how to do this, follow the Scriptaculous installation instructions.

Once you’ve got Scriptaculous linked and ready for use, we can change a couple of lines in toggleMenu() and hideTopLevels() so that the effects are used:

In toggleMenu() change:


if (node.style.display == "") {
 node.style.display = "none";
} else {
 node.style.display = "";
}

To this:


if (node.style.display == "") {
 Effect.BlindUp(node, {duration: 0.2});
} else {
 Effect.BlindDown(node, {duration: 0.2});
}

Then in hideTopLevels() change:


grandchild.style.display = "none";

To this:


Effect.BlindUp(grandchild, {duration: 0.2});

Your list should now be displaying some pretty cool looking transitions, rather than jumping from hiding to displaying.

I’ve added some CSS to my final example, but you are free to style the list any way you like to suit your own taste. Here is the final version with the transitions added, some new content, and my own example CSS linked above.

You are free to edit whatever you like and take this code away to use on your own site. I hope you enjoyed the walk through!

A zipped up version of the menu is availiable for download.

UPDATE: Now includes latest version of Prototype and Scriptaculous which fixes the drop down effects bug mentioned in the comments below.

Technorati Tags: , , , ,