Show me, hide me

Sometimes you want to enable surfers to hide and show elements of a document, whether it be to reveal extra information about a subject or to allow them to get rid of right hand navigation.

In older DHTML scripts this was done by adding an ID to the element to access its visibility property

<script language="javascript" type="text/javascript">

<!--

// Evil Netscape and IE 4.x function

function show(id,trigger){

if (trigger==1){

if (document.layers) document.layers[id].visibility = "show"

else if (document.all) document.all[id].style.visibility = "visible"

}else{

if (document.layers) document.layers[id].visibility = "hide"

else if (document.all) document.all[id].style.visibility = "hidden"

}

}

//-->

</script>

<a href="javascript:show('myText',1)">Show</a>

<a href="javascript:show('myText',0)">Hide</a>

<div id="myText">Test</div>

These functions did not collapse or expand the DIV, but just hide it. Furthermore in Netscape they only worked on absolutely positioned elements. A different way of doing that for newer browsers is by accessing the display attribute of the div.

<script language="javascript" type="text/javascript">

<!--

// DOM function for >4 browsers

function show(id,trigger){

if (trigger==1){document.getElementById(id).display="block";}

else{document.getElementById(id).display="none";}

}

}

//-->

</script>

<a href="javascript:show('myText',1)">Show</a>

<a href="javascript:show('myText',0)">Hide</a>

<div id="myText">Test</div>

This way of showing and hiding actually deletes the DIV from the display, making content below it jump further up.

Now this enables us to show and hide, or in this case, collapse and expand a part of the page using its ID. However, this can be pretty tiring, as each element we add to the document needs a unique ID and a link to point to it. Furthermore we need a link for showing and one for hiding. It would be a lot handier to have a link with, for example, a "+" next to it that hints that it is an expandable element. Clicking it would turn the "+" into a "-" and show the collapsed element. Clicking the link again would hide the element and turn the "-" into a "+".

Climbing down the node tree

DOM allows us to do exactly that, if we use some straight forward HTML.

Let's say we want a menu with collapsible elements in it. The HTML is:

<style type="text/css">#menu div {display:none;}</style>

<div id="menu">

<h5><a href="javascript:show(0)">+ Test1</a></h5>

<div>Test 1</div>

<h5><a href="javascript:show(1)">+ Test2</a></h5>

<div>Test 2</div>

<h5><a href="javascript:show(2)">+ Test3</a></h5>

<div>Test3</div>

</div>

We use the H5 tags to be able to access the links later. Otherwise it gets a lot harder to access the "+" in the link text. We set the display of each DIV to none to collapse them.

To access the menu we use document.getElementById("menu"), to access the collapsible elements we use document.getElementById("menu").getElementsByTagName("div").

We can then count through the DIVs to access one of them, read its display attributes and reverse it.

<script language="javascript" type="text/javascript">

<!--

function show(which){

m=document.getElementById("menu");

trig=m.getElementsByTagName("div").item(which).style.display;

if (trig=="block") trig="none";

else if (trig=="" trig=="none") trig="block";

m.getElementsByTagName("div").item(which).style.display=trig;

}

//-->

</script>

<style type="text/css">#menu div {display:none;}</style>

<div id="menu">

<h5><a href="javascript:show(0)">+ Test1</a></h5>

<div>Test 1</div>

<h5><a href="javascript:show(1)">+ Test2</a></h5>

<div>Test 2</div>

<h5><a href="javascript:show(2)">+ Test3</a></h5>

<div>Test3</div>

</div>

Clicking the "Test 1" link (calling show(0)) now does the following:

This now enables us to show and hide the elements when clicking on the links, however, it does not change the "+" to "-" yet.

To do this we need to go through the node tree and access the link's text value

<script language="javascript" type="text/javascript">

<!--

function show(which){

m=document.getElementById("menu");

trig=m.getElementsByTagName("div").item(which).style.display;

if (trig=="block") trig="none";

else if (trig=="" trig=="none") trig="block";

m.getElementsByTagName("div").item(trigger).style.display=trig;

var highlighttext="-";

varnormaltext="+";

t=m.getElementsByTagName("h5").item(which);

h=t.getElementsByTagName("a").item(0).firstChild;

if (trig=="none"){h.nodeValue=h.nodeValue.replace(highlighttext,normaltext);}

else {h.nodeValue=h.nodeValue.replace(normaltext,highlighttext);}

}

//-->

</script>

<style type="text/css">#menu div {display:none;}</style>

<div id="menu">

<h5><a href="javascript:show(0)">+ Test1</a></h5>

<div>Test 1</div>

<h5><a href="javascript:show(1)">+ Test2</a></h5>

<div>Test 2</div>

<h5><a href="javascript:show(2)">+ Test3</a></h5>

<div>Test3</div>

</div>

We define two variables called "highlighttext" and "normaltext". These are the "-" for the expanded state and the "+" for the collapsed state.

Then we access the link elements inside the H5 on the same level as the DIV we expanded or collapsed, t=m.getElementsByTagName("h5").item(which);.

Inside this H5 we access the first A (item 0) and get its first child element, which is the text value of the link, h=t.getElementsByTagName("a").item(0).firstChild;

We get the nodeValue of this child and, depending on which display is desired, we replace "+" (normaltext) with "-"(highlighttext) or vice versa.

Voila, we have accessed our collapsible page elements without using or adding IDs to them. For each DIV we add to the menu, we simply count up its number (Test 4 would be accessed via show(3) ).

However, this function only works in IE above 4 and Mozilla / Netscape above 6. Opera does know the getElementByID functionality but does not implement it properly. We need to think about something to make the content available to every browser.

Helping the less fortunate

The first thing we have to prevent happening is Netscape 4.x, Internet Explorer 4 and Opera trying to access the function and fail to make it work.

This can be done by wrapping it in a condition that checks for a functionality neither Netscape nor Opera have, but the others have.

if (document.getElementById && document.createTextNode){},

around the main function does the trick.

Then we need to ensure that the hidden DIVs are getting displayed for these browsers. We do that with the same condition, and by adding a new style rule overwriting the old one.

if (!document.getElementById && !document.createTextNode){

document.write('<style type="text/css">#menu div{display:block;}</style>')

}

The same hack should get applied when people do have the correct browsers, but have JavaScript disabled:

<noscript><style type="text/css">#menu div{display:block;}</style></noscript>

And finally to make sure that all the horrid href="javascript:" links become working examples without javascript, we add a named anchor in each DIV, and link to that one.

<h5><a href="#test1" onclick="show(0)">+ Test1</a></h5>

<div><a name="test1"></a>Test 1</div>

That's all. We now have collapsible elements that work in every browser that supports DOM, and the others can still access all the data.

The whole code

<script language="javascript" type="text/javascript">

<!--

function show(which){

if (document.getElementById && document.createTextNode){

m=document.getElementById("menu");

trig=m.getElementsByTagName("div").item(which).style.display;

if (trig=="block") trig="none";

else if (trig=="" trig=="none") trig="block";

m.getElementsByTagName("div").item(which).style.display=trig;

var highlighttext="-";

var normaltext="+";

t=m.getElementsByTagName("h5").item(which);

h=t.getElementsByTagName("a").item(0).firstChild;

if (trig=="none"){h.nodeValue=h.nodeValue.replace(highlighttext,normaltext);}

else {h.nodeValue=h.nodeValue.replace(normaltext,highlighttext);}

}

}

//-->

</script>

<style type="text/css">#menu div {display:none;}</style>

<!-- Backward compatibility hacks -->

<noscript>

<style type="text/css">#menu div{display:block;}</style>

</noscript>

<script language="JavaScript">

<!--

if (!document.getElementById && !document.createTextNode){

document.write('<style type="text/css">#menu div{display:block;}</style>')

}

//-->

</script>

<!-- Backward compatibility hacks end -->

<div id="menu">

<h5><a href="#test1" onclick="show(0)">+ Test1</a></h5>

<div><a name="test1"></a>Test 1</div>

<h5><a href="#test2" onclick="show(1)">+ Test2</a></h5>

<div><a name="test2"></a>Test 2</div>

<h5><a href="#test3" onclick="show(2)">+ Test3</a></h5>

<div><a name="test3"></a>Test3</div>

</div>

Of course you can access some other elements of this HTML to make the effect even more obvious. You can change the colour or background of the links according to their "highlight" or "normal" state.

To see and download a fully functional and a bit enhanced version of this effect to use, go to this page.

I have to thank Scott Benish, Craig Saila and Scott Andrew LePera of the webdesign-L mailing list for testing and enhancement tips.