From the emails I got after publishing the

title="Collapsible page elements with DOM" target="_blank">collapsible page elements article

I 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 navigations

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

users 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 turn

off 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 done

via 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

as above, we take an object(d) and check its display value. If that value is none, we set it

to block and vice versa.

More challenging is the first part, d=this.parentNode.getElementsByTagName('ul')[0];,

especially when you haven't used DOM before, or anything that also works with

traversing through node trees (XSLT for example).

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, most

probably 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" exists

and 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');

in expinit() with alluls=document.getElementById('yourid').getElementsByTagName('UL');.

For the known location solution, replace alluls=document.getElementsByTagName('UL');

in expinit() with 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.

pureDom explorer

And look at the source code to see how these changes were implemented.