Summary: I’d like to create a vertical navigation that will expand and collapse to show and hide sub-navigation using only unordered lists and as few class/id names as possible. In the course of Dissecting Code, I’ve seen some many unnecessary classes and I’d like to make sure that the core xHTML is as clean as possible and still give a reasonable navigation in the absence of JavaScript. Check out the finished demo.
Nested Unordered Lists
In an effort to keep my xHTML as simple as possible, I’m going to create this navigation with some nested lists and attach a single ID to the top level ul like so:
<ul id="nav"> <li><a href="#">Home</a></li> <li><a href="#">Portfolio</a> <ul> <li><a href="#">Web Design</a></li> <li><a href="#">Illustration</a></li> <li><a href="#">Drawings</a></li> <li><a href="#">Photography</a> <ul> <li><a href="#">Landscapes</a></li> <li><a href="#">Portrait</a></li> <li><a href="#">Abstract</a></li> </ul> </li> </ul> </li> ... </ul>
You get the point. Notice that I have links for the li’s that have sub navigation, so this assumes there is a page for all of these. Had I not had a link and just used them to categorize, I could have done this a little differently.
The Plan
So how am I going to approach this. Let me first plan out what I’d like my script to do:
- Grab the navigation ul and get all the underlying ul’s.
- Hide all the secondary navigation
- Attach an Image with an onClick to the li’s that have sub-navigation (onClick will do show/hid)
- Add in an Expand All and Collapse All
That’s not too complicated.
Check the Browser!
My first step in writing code usually involves some checking of the DOM to make sure my script will work in the first place. I’m going to be getting elements by id and class name and also creating elements, so lets see if we’re able to do that first. If not, we’ll abandon the script:
if(!document.getElementById) return; if(!document.getElementsByTagName) return; if(!document.createElement) return;
The Script
The first step is to set up the onclick events for the list items that have sub-navigation and inject in an image to control it. So lets start going through my list. Note: Unnatural line breaks, I mark line breaks with a ‘«’.
Grab the navigation and the underlying ul’s:
var navigation = document.getElementById('nav'); var navSub = navigation.getElementsByTagName('ul');
We’re going to start a for loop on all the subnavs to do a number of things:
for (i=0; i<navSub.length; i++){
We’ll create the expand image:
var toggleImage = document.createElement('img'); toggleImage.setAttribute('src', 'img/expand.gif');
There isn’t an a tag around this image so when you hover over it, there won’t be a hand to indicate you can click it. I’m going to change the style to accommodate this:
toggleImage.style.cursor = "pointer";
I’m going to create the onclick for the image to call the toggleNav function:
toggleImage.onclick = function() { toggleNav(this); }
This will seem a bit tricky because of how I want to transverse the nodes so let me explain. I’m going to use ‘insertBefore’ here. I want to get the parent node of the sub-navigation that we’re on (the top li) and insert the ‘toggleImage’ just before the sub-navigation:
navSub[i].parentNode.insertBefore(toggleImage, navSub[i].parentNode.firstChild);
You might think that I could just do:
navSub[i].parentNode.insertBefore(toggleImage, navSub[i]);
But this would put the expand/collapse image to the right of the link.
The last step in this loop is to assign the classes to this sub-navigation to hide it and set the style of the li to assign it an image and some padding and close the for loop:
navSub[i].style.display="none"; navSub[i].parentNode.className = "expandable"; }
The final ‘for’ loop looks like this:
for (i=0; i<navSub.length; i++){ var toggleImage = document.createElement('img'); toggleImage.setAttribute('src', 'img/expand.gif'); toggleImage.style.cursor = "pointer"; toggleImage.onclick = function() { toggleNav(this); } navSub[i].parentNode.insertBefore(toggleImage, navSub[i].parentNode.firstChild); navSub[i].style.display="none"; navSub[i].parentNode.className = "expandable"; }
The last thing we need to do to set up the navigation is to add in an expand and contract link for an easy shortcut. I’m going to append the list with two list items, and to make this real easy I’ll use some ‘innerHTML’. We’ll need an onClick event to call the function (I’m going to use the same function as before using an id to tell the function what to do:
var expandLink = document.createElement('li'); expandLink.innerHTML = "<a href='#' onclick='toggleNav(this)' « id='expandAll'>Expand All</a>" var collapseLink = document.createElement('li'); collapseLink.innerHTML = "<a href='#' onclick='toggleNav(this)' « id='collapseAll'>Collapse All</a>"
Those are created, now I’ll append them to the list:
navigation.appendChild(expandLink); navigation.appendChild(collapseLink);
The Magical Function
That’s the last step in setting up the page for the interaction and we’ll jump into the function. Basically I want to test for three different things using the id of what was clicked to figure out how to handle the page – we’ll do an if/else if/else statement. The if and else if check that the id is present to handle all of the navigation and the last else statement will handle the individual list items.
Let’s start this function:
function toggleNav(whichOne){
The ‘whichOne’ variable could possible be “this”, “collapseAll” or “expandAll”. Let’s test for the first:
if (whichOne.getAttribute('id') == "expandAll") {
If it tests correctly, we’ll grab the navigation, it’s underlying ul’s and the images (so we can change them):
var navigation = document.getElementById('nav'); var navigationULs = navigation.getElementsByTagName('ul'); var allImages = navigation.getElementsByTagName('img');
Now we’ll loop through the underlying unordered lists so we can change their style to be show and change the image to indicate that they’ve been expanded:
for (i = 0; i < navigationULs.length; i++) { navigationULs[i].style.display = "block"; allImages[i].setAttribute('src', 'img/contract.gif') }
Lets do the same thing for Collapsing the navigation, but this time just the opposite and do it as an else in this function:
else if (whichOne.getAttribute('id') == "collapseAll"){ var navigation = document.getElementById('nav'); var navigationULs = navigation.getElementsByTagName('ul'); var allImages = navigation.getElementsByTagName('img'); for (i = 0; i < navigationULs.length; i++) { navigationULs[i].style.display = "none"; allImages[i].setAttribute('src', 'img/expand.gif') } }
The last step in this large if/elseif/else statement is to handle the individual navigation clicks – I won’t test for anything and use a basic toggle to accomplish this. Remember that what’s pass to this function is “this”. In this we’ll grab the ul that hides under “this” list item by grabbing the parent and getting the uls:
var theParent = whichOne.parentNode; var theParentULs = theParent.getElementsByTagName('ul');
Of course we’ll grab the image too so we can change it up:
var theParentImage = theParent.getElementsByTagName('img');
Then we’ll do an if/else toggle to check what the display style is, change it along with the image. This is probably self explanatory (we target [0] so that any underlying third or forth level ul’s aren’t affected):
if (theParentULs[0].style.display == "none") { theParentULs[0].style.display = "block"; theParentImage[0].setAttribute('src', 'img/contract.gif'); } else { theParentULs[0].style.display = "none"; theParentImage[0].setAttribute('src', 'img/expand.gif'); }
So now in it’s entirety, here’s the complete function:
function toggleNav(whichOne){ if (whichOne.getAttribute('id') == "expandAll") { var navigation = document.getElementById('nav'); var navigationULs = navigation.getElementsByTagName('ul'); var allImages = navigation.getElementsByTagName('img'); for (i = 0; i < navigationULs.length; i++) { navigationULs[i].style.display = "block"; allImages[i].setAttribute('src', 'img/contract.gif') } } else if (whichOne.getAttribute('id') == "collapseAll"){ var navigation = document.getElementById('nav'); var navigationULs = navigation.getElementsByTagName('ul'); var allImages = navigation.getElementsByTagName('img'); for (i = 0; i < navigationULs.length; i++) { navigationULs[i].style.display = "none"; allImages[i].setAttribute('src', 'img/expand.gif') } } else { var theParent = whichOne.parentNode; var theParentULs = theParent.getElementsByTagName('ul'); var theParentImage = theParent.getElementsByTagName('img'); if (theParentULs[0].style.display == "none") { theParentULs[0].style.display = "block"; theParentImage[0].setAttribute('src', 'img/contract.gif'); } else { theParentULs[0].style.display = "none"; theParentImage[0].setAttribute('src', 'img/expand.gif'); } } }
I also did a little styling to pretty it up a bit:
#nav, #nav ul{ list-style:none; margin-left:20px; } #nav ul{ padding-top:.5em; } #nav li{ padding-left:17px; background: url(../img/arrow.gif) left top no-repeat; padding-bottom:.45em; } #nav a{ text-decoration:none; color:#960000; } #nav a:hover{ color:#1b53b9; } //This gets rid of the default bullet #nav .expandable{ padding-left:0px; background-image:none; } //Gives a little padding between the new image and the text #nav .expandable img{ margin-right:5px; } //To make the expand/collapse stand out #expandAll, #collapseAll { font-weight:bold; }
Pretty complicated, but it gets the job done. Throw the init() function into an onload event, make sure your top level ul has an id of nav, put a link to the script in the xHTML and you’re ready to go. For all the code, check out the demo.
I like it. Simple and very customizable.