Skip to page content or skip to Accesskey List.
Search evolt.org
evolt.org login: or register

Work

Main Page Content

An explorer script with no need for ID

Rated 4.04 (Ratings: 6) (Add your rating)

Log in to add a comment
(13 comments so far)

Want more?

 
Picture of codepo8

Chris Heilmann

Member info | Full bio

User since: July 29, 2002

Last login: April 27, 2006

Articles written: 17

From the emails I got after publishing the 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 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 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 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.

Currently employed in London as a Lead Front End Developer, Chris has been bouncing around the globe working for several agencies and companies. A web enthusiast from 1997 on workplaces include Munich, London, Santa Monica and San Francisco. More of Chris' writings can be found at http://icant.co.uk and he blogs at http://wait-till-i.com

A few enhancements

Submitted by luminosity on October 31, 2003 - 17:13.

That would make this a great script, rather than a good one: Have a class which you tag any lists with, which are wanted to be collapsible. This is exactly what the class attribute is meant for.. subclassing lists (ie, this would be a collapsible subclass of the unordered list superclass.

In addition, if all you had to do apart from tagging those lists with classes, was include the script in the page, not having to add any code to the page itself, it'd be fantastic. Seperation of script and structure is as important as style and structure, and I don't think it'd be too hard to get the script to add these event handlers itself.

login or register to post comments

Lines

Submitted by asjo on November 7, 2003 - 10:42.

In explorer-type treeviews, there are usually lines connecting the items (and the +/--marks). How do you add them to the nice semantically clean markup?

login or register to post comments

a touch of class

Submitted by codepo8 on November 8, 2003 - 00:55.

Luminocity to give the script that behaviour, all you need to do is to filter the main UL array a bit. For example if the class is colul:

function expinit(){
 if (canDOM){
  alluls=document.getElementsByTagName('UL');
  for(i=0;i<alluls.length;i++){
   subul=alluls[i];
   if(subul.parentNode.tagName=='LI' && subul.className=='colul'){
[...]

As for the handlers, that was a bit tricky, as IE doesn't allow you to write an onclick attribute with a string only. Nevertheless check the next post :-)

ASJO: The idea is to replicate the behaviour of the explorer, not the complete look and feel. I suppose clever use of background images in CSS makes it possible though.

login or register to post comments

Ah, so much easier and a real navigation

Submitted by codepo8 on November 8, 2003 - 01:10.

Two caveats of this technique bugged me:

  • There was no possibility for the link to point to a landing page in addition to collapse and expand the node (you could add an extra a before the other one, but Mozilla needs it to have something in it, and it clutters up the markup).
  • you had to add the event handlers by hand

Don't despair, a solution was found:

Click here to see and download the solution .

Also comes with a nice PHP script to generate and highlight the necessary HTML :-)

login or register to post comments

Re: Links

Submitted by asjo on November 11, 2003 - 07:35.

> The idea is to replicate the behaviour of the explorer,
> not the complete look and feel.
> I suppose clever use of background
> images in CSS makes it possible though.

I've been using a bunch of images and a large table, which is unhandy; if anybody has any links to something like the above described, i'd love to see them.

login or register to post comments

I'd like to see this with def lists

Submitted by slholmes on November 12, 2003 - 14:22.

I've spent several years trying to justify in my head the use of bulleted lists for links. Yes, yes. I agree totally that navigation elements are by nature lists. My issue is: should the list be marked up ul in XHTML? I think not and the reason is simply on account of the darn bullets. The bullets that are displayed in every single browser I've come across seem to semantically destroy the very idea of using an unordered list for the semantic rendering of purely navigational elements. This is simply just not what uls were designed for. The definition list seems to me to be much more appropriate. No browser displays dls using bullets. The HTML spec at w3c indicates that a defined term (dt) can have multiple definitions. There doesn't appear to be any accessibility restrictions on nested definition lists. There is a one to one correlation between the defined term and a "folder" and between the definition description (dd) and the actual links (one link per dd element). Perfect rendering machinery for a hiearchical description of explorer type tables of contents. Please view the source for http://www.polyphonicstudios.net/contents.html for an example of what I'm talking about. The gist of this post is that links should be marked up using definition lists and not unordered lists. I'd love it if there existed unobtrusive DHTML for dl based navigation.

login or register to post comments

slholmes

Submitted by codepo8 on November 12, 2003 - 14:27.

Although I tend to disagree with your argumentation (how can links of subelements be a definition of a term and the main link be the term) all you need to do to use this script is to replace ul with dt and li with dd... Netscape 4.x's bookmark file was a DL, btw...

login or register to post comments

lists versus lists

Submitted by aardvark on November 12, 2003 - 15:59.

I tend to believe that an unordered list is more appropriate than a definition list. Granted, a definition list could still be argued as not semantically incorrect, I feel that an unordered list is a closer match to the contents we're discussing:

Lists
Lists may contain:
  • Unordered information
  • Ordered information
  • Definitions
An ordered list, created using the <ol> element, should contain information where order should be emphasized, as in a recipe.

Definition List
Definition lists vary only slightly from other types of lists in that list items consist of two parts: a term and a description.

After all, isn't a list of links simply a list of links? Is each sub-link really a definition of the parent link? Isn't there a reason the W3C took the time to differentiate between the two?

As for the argument that bullets don't look right, it is easy enough to use CSS to remove them altogether. Then your concern over appearance goes away. And given that we're talking about the semantic/structural merit of a tag, shouldn't its appearance when rendered be irrelevant?

login or register to post comments

Thanks for the info

Submitted by slholmes on November 13, 2003 - 07:32.

codepo8 - I'll definitely give what you suggest a try. aardvark, my reasoning has more to do with common useage and as I'm thinking of it, my reasoning will have a very western language bias. When reading documents on the web I encounter almost zero cases of links listed in a bulleted form (except in examples such as these). It just has no precendent. And therefore I'm thinking if a reader views a page with no CSS, the markup will be confusing - "why would they put a list of links in with a bunch of bullets" in otherwords, it looks highly unusual. Because this is the only reasoning for not using lists - and admittedly it is not a very convincing one - I should concentrate on the list of links is a def list argument. I'll only ask the question "do the files in a folder define the folder?" If you agree with me that they do, then the dl seems appropriate and correct. An aside: Thanks for making me think this through. I desire to become an artist at this stuff. I have a personal digital tennet and that is "program on purpose".

login or register to post comments

unordered lists

Submitted by mynameismonkey on November 16, 2003 - 16:16.

We made a PHP class that generates the nested lists, then outputs valid XHTML ul blocks styled with CSS. We have expanding and contracting menus although not in the style being taught here. Each nav block will display regular nested lists when CSS is not available. Comments welcome, we're still working on it. Example page.

login or register to post comments

Expand Collapse All

Submitted by systemaddict on April 3, 2004 - 00:26.

Hello, It would be nice with a Expand / Collapse All function. I tried to make one but could only get it to expand the first child. Thanks, Soren

login or register to post comments

systemaddict

Submitted by codepo8 on April 5, 2004 - 15:57.

Check my link above to the other solution

login or register to post comments

I agree

Submitted by nainil on April 6, 2004 - 20:12.

I agree with aardvark that "an unordered list is more appropriate than a definition list.". Also for mynameismonkey, the example page depicts an illustrative way of defining "Web Dynamics".

login or register to post comments

The access keys for this page are: ALT (Control on a Mac) plus:

evolt.orgEvolt.org is an all-volunteer resource for web developers made up of a discussion list, a browser archive, and member-submitted articles. This article is the property of its author, please do not redistribute or use elsewhere without checking with the author.