Main Page Content
An Explorer Script With No Need For Id
From the emails I got after publishing the
title="Collapsible page elements with DOM" target="_blank">collapsible page elements articleI realised that what the world needs now (apart from love, sweet love) is a clean explorer-like collapsing and expanding nested list script.Check target="_blank" title="example of a dynamic explorer navigation">this example
page to see what we are talking about.If you google for these you
find a lot, and most likely all of them fail in one way or another. They might be browser dependent, need horrid markup, are not backward compatible, whatever, for some reason most will just not do.So let's see how we can do it better...
Step one: collect underwear
Without a solid foundation, a house shows cracks (mine does), and without a
solid HTML markup to enhance, a script is likely to fail.That is why the HTML to be turned into the fancy collapse and expand script
should be a HTML list. If you haven't heard about the merits of navigationsas lists yet, read the ravings on target="_blank" title="why lists are a good markup idea for navigations">listamatic.This is what it might look like:
<ul> <li><a href="#">Link1</a></li> <li><a href="#">Link2</a> <ul> <li><a href="#">Link2_1</a></li> <li><a href="#">Link2_2</a></li> <li><a href="#">Link2_3</a></li> <li><a href="#">Link2_4</a></li> </ul> </li> <li><a href="#">Link3</a></li> <li><a href="#">Link4</a></li></ul>
And we want to be able to nest as many levels as necessary.
Ponderings: script or no script?
You don't necessarily need Javascript to achieve the functionality of a
file explorer menu, you could use CSS and some clever :hover statements.However collapsing and expanding an explorer menu when you touch it with a
mouse means you need neurosurgical skills to navigate into nested items.Furthermore you cannot keep the nested items visible once you move away from
the link.Hence, no CSS for us this time, let's do a Javascript instead.
Let us also focus on accessibility and "graceful degradation".
People with the inability to use a mouse should be able to use the script with the keyboard. People without Javascript should see the menu as a totally expanded list without bells and whistles.Mouse independence is easy: Simply add an "onkeypress" event handler
to each link you enhanced with your "onclick" one.This is common practise and a title="information about event handlers and accessibility">recommendation by the
W3C Web Accessibility Initiative(WAI).Some browsers (like some builds of Mozilla and IE on mac) have problems with
the keyboard implementation, but it is not our job to work around them. Keyboardusers that need to use it won't use these browsers anyway.There are loads of ways to collapse and expand elements on a page, let's take
a peek at a common one.Hey, I got an ID
Collapsing nested elements is easy, once you give them an ID.
Let's create one of these solutions, and, as this is not the real thing,
we won't worry about the onkeypress for the moment.<ul> <li><a href="#">Link1</a></li> <li><a href="#" onclick="d=document.getElementById('nest1');d.style.display=d.style.display=='none'?'block':'none'; return false">Link2</a> <ul id="nest1" style="display:none"> <li><a href="#">Link2_1</a></li> <li><a href="#">Link2_2</a></li> <li><a href="#">Link2_3</a></li> <li><a href="#">Link2_4</a></li> </ul> </li> <li><a href="#">Link3</a></li> <li><a href="#">Link4</a></li></ul>
Does the job, but is dependent on too many things. First of all you need an
ID for each nested element, and that could interfere with existing IDs (whoever did an ASP.NET project and has seen its way of deliberately scattering obscure IDs all over the document will have faced that problem). Secondly, if I turnoff Javascript, the nested element is hidden (unless I also turn off CSS).What we need to do is to find a function that checks for us
if there is something to collapse and does so if it exists. And that without knowing its name (ID in this case).Forget me node
The answer: DOM. This handy thing (Document Object Model) allows you to
navigate through your HTML document and access each bit of it. This can be donevia ID or tag name. And tag name is what we will use here.<ul> <li><a href="#">Link1</a></li> <li><a href="#" onclick="d=this.parentNode.getElementsByTagName('ul')[0];d.style.display=d.style.display=='none'?'block':'none'; return false">Link2</a> <ul style="display:none"> <li><a href="#">Link2_1</a></li> <li><a href="#">Link2_2</a></li> <li><a href="#">Link2_3</a></li> <li><a href="#">Link2_4</a></li> </ul> </li> <li><a href="#">Link3</a></li> <li><a href="#">Link4</a></li></ul>
What?
Ok, let's go through this one bit by bit:
d.style.display=='none'?'block':'none';return false"
is the same
More challenging is the first part, d=this.parentNode.getElementsByTagName('ul')[0];
,
We define d, and it is defined as something starting from "this". "this"
is handy, as it always is what we clicked on. In our case, "this" is the link element with the text "Link2" in it."parentNode" is the node our link is in, in our case the LI element. If we
had nested the link in a STRONG tag, that would be parentNode. This DOM variable always gets the parent element of the one we are dealing with. (Much like a "cd .." command gets you up one level in DOS or on a unix bash).Now that we are at the LI level, we get all the UL elements nested in it via
getElementsByTagName('ul')
and choose the first one getElementsByTagName('ul')[0]
(computers start counting at 0, not 1, mostprobably because they don't have any fingers).Now all we need to make this work for every link on the page,
is to check for our "d" before we tell the browser to change its display value.Let's create the fully reusable function that also checks if our browser can
do what we want it to do:<script type="text/javascript">if (document.getElementById && document.createTextNode && document.createElement){canDOM=true}function ex(n){ if(canDOM){ u=n.parentNode.getElementsByTagName('ul')[0]; if(u){u.style.display=(u.style.display=='none' u.style.display=='')?'block':'none';} }}</script><ul> <li><a href="#">Link1</a></li> <li><a href="#" onclick="ex(this);return false;" onkeypress="ex(this);return false;">Link2</a> <ul style="display:none"> <li><a href="#">Link2_1</a></li> <li><a href="#">Link2_2</a></li> <li><a href="#">Link2_3</a></li> <li><a href="#">Link2_4</a></li> </ul> </li> <li><a href="#">Link3</a></li> <li><a href="#">Link4</a></li></ul>
Gotta hide 'em all
Ok, that still leaves the non Javascript users with CSS enabled with a collapsed
list though. We need to find a way to collapse all nested ULs in the document. getElementsByTagName helps us there.<script type="text/javascript">function expinit(){ if (canDOM){ alluls=document.getElementsByTagName('UL'); for(i=0;i<alluls.length;i++){ subul=alluls[i]; if(subul.parentNode.tagName=='LI'){ subul.style.display='none'; } } }}window.onload=expinit;</script><ul> <li><a href="#">Link1</a></li> <li><a href="#" onclick="ex(this);return false;" onkeypress="ex(this);return false;">Link2</a> <ul> <li><a href="#">Link2_1</a></li> <li><a href="#">Link2_2</a></li> <li><a href="#">Link2_3</a></li> <li><a href="#">Link2_4</a></li> </ul> </li> <li><a href="#">Link3</a></li> <li><a href="#">Link4</a></li></ul>
We check if the browser supports DOM, then we get all the UL objects in the
document and store them in an array called "alluls". We then loop through this array, and check if the parentNode of the UL we are currently looking at is an LI (which defines a nested UL). If this is the case, we set the display of the UL to none. We call this script when the document is loaded and, hey, all nested lists get hidden.Two more problems though: First, all nested lists get hidden, and second, how
do I know that some of the links have sub elements and some not?Indicate left, take over
We want an indicator left of the link that tells us that there is something to
expand, and we want only lists in LIs with links in them to be hidden.<script type="text/javascript">function expinit(){ if (canDOM){ alluls=document.getElementsByTagName('UL'); for(i=0;i<alluls.length;i++){ subul=alluls[i]; if(subul.parentNode.tagName=='LI'){ mom=subul.parentNode.getElementsByTagName('A')[0] if(mom){ momlink=mom.childNodes[0]; momlink.nodeValue='+'+momlink.nodeValue; subul.style.display='none'; } } } }}window.onload=expinit;</script>
We define "mom" as the first link within the parent node of the ul we
are in. (We are at the UL level, one up is the LI, the first A is actually the link that gets clicked to expand or collapse this UL). We check if "mom" existsand if that is the case we take the first child node of the A (which is the text),and read its value via "nodeValue". Then we add a + in front of it. Voila, all links with nested elements have a + in front of them.The nice thing is that non Javascript browsers don't even see this, and it
doesn't confuse them.Now we need to change the function that does the actual collapsing to change
this + into a - when the display change happens:<script type="text/javascript">function ex(n){ if(canDOM){ u=n.parentNode.getElementsByTagName("ul")[0]; if(u){ u.style.display=(u.style.display=='none' u.style.display=='')?'block':'none'; str=n.firstChild.nodeValue; sign=str.substr(0,1)=='+'?'':'+'; n.firstChild.nodeValue= sign + str.substr(1,str.length); } } }</script>
We define "str" as the text of the link we just clicked (firstChild is
the text, nodeValue is the text data). Then we check if the first character (substr(0,1)) is a + and define "sign" as a - or a + accordingly. Remember we added this + via the expinit() function.Then we set the nodeValue of the link's text to our new sign followed by the rest
of the text in the link (substr 1 until the end of the string).You have taken your first step into a larger world...
Now we have the script we wanted. We don't need to enhance our HTML with
any IDs or extra Javascript in the document body. The functionality (and the extra text, namely the + and -) only shows up when the browser is capable of supporting it. And it works! Where to go next?It might be a bad idea to collapse all nested lists in one document. If
you want to prevent that happening, you'll have to either nest the navigation list in a DIV with an ID or to make sure you know the location of the list in the document node tree.For the ID solution, replace alluls=document.getElementsByTagName('UL');
alluls=document.getElementById('yourid').getElementsByTagName('UL');
.For the known location solution, replace alluls=document.getElementsByTagName('UL');
alluls=document.getElementsByTagName('UL')[1].getElementsByTagName('UL');
where "1" is the number of the location.Also, you might want to add an image as the indicator for collapsed elements,
or nest the indicator in some other element to add some extra styles.If you want a readymade script using this techniques here with some of these
enhancements, go and download.And look at the source code to see how these changes were implemented.