Summary

It is far easier to maintain content that is free from presentation and behaviour elements and attributes. Separating content, presentation, and behaviour is not only easier to maintain, it's also more accessible and search engine friendly, as the content is more likely to be machine readable.

This article walks through the steps required to build an accessible, dynamic menu, with separate content, presentation, and behavioural layers.

Author: Gez Lemon

Contents

The Behavioural Layer

There has been lots written about separating content from presentation, and most developers see the benefit of this approach (cleaner, leaner markup; faster download speeds; easier maintainability, etc). There is also a fair amount written about separating behaviour from both content and presentation, but it tends to be practiced less than separation of content from presentation. HTML (or even XHTML) should be used for content, CSS for presentation, and ECMAScript for behaviour.

ECMAScript

All the time JavaScript was a Netscape proprietary technology, its use could not be endorsed by the W3C. In the middle of 1998, the European Computer Manufacturers Association (ECMA) created a public domain specification based on JavaScript, but using a standardised Document Object Model. The third edition of the ECMAScript Language Specification was published in December 1999; Standard ECMA-262.

Collapsible Menu System

To illustrate using a completely separate behavioural layer, I'll demonstrate a collapsible menu system. All of the examples are HTML, but work equally well as XHTML, even when served with the correct MIME type.

Ideally, we want our markup to be free of any inline event handlers, to keep it as clean as possible. The following is a nested unordered list that could be used as a navigation system. The outermost list has an id, so we can locate it with our script later.

<ul id="mainnav">
  <li>
    Content
    <ul>
      <li><a href="#">HTML 4.01</a></li>
      <li><a href="#">XHTML 1.0</a></li>
    </ul>
  </li>
  <li>
    Presentation
    <ul>
      <li><a href="#">CSS 1</a></li>
      <li><a href="#">CSS 2</a></li>
    </ul>
  </li>
  <li>
    Behaviour
    <ul>
      <li><a href="#">DOM 1</a></li>
      <li><a href="#">DOM 2</a></li>
    </ul>
  </li>
  <li>
    Accessibility
    <ul>
      <li><a href="#">WCAG 1</a></li>
    </ul>
  </li>
</ul>

The plain menu without presentation or behaviour will act as the base for our script. The href attributes have been provided with proper addresses in the demonstrations, rather than a fragment identifier.

Hiding Nested Lists

The first thing we want to do, is to hide each of the nested lists. This is a relatively simple task. We just need to check we're working with a DOM compliant user agent, and if we are, gather a list of the unordered lists, and set the display property to none.

function rollup()
{
    // Check we're working with a DOM compliant browser
    if (document.getElementById && document.createElement)
    {
        var objMenu = document.getElementById('mainnav');

        var objNested = objMenu.getElementsByTagName('ul');

        // Hide each of the nested unordered list
        for (var i=0; i<objNested.length; i++)
            objNested[i].style.display = 'none';
    }
}

Registering the Event Handler

Now all we need to do, is register the rollup function as an event handler when our page has loaded. Traditionally, event handlers are specified as attributes of an element. The correct method of registering an event is to use the addEventListener method, as defined in the DOM Level 2 Events Specification. This method allows you to register as many events handlers as you like to a single event on an element, and add and remove individual handlers as and when they're required. Assigning event-handlers to a single attribute doesn't afford this kind of flexibility, but there is one problem with addEventListener; Internet Explorer does not support it. As Internet Explorer is still a dominant browser, and to avoid browser sniffing and working with Internet Explorer's HTML Components (HTC), we'll revert back to assigning our event-handler to the window's onload event.

window.onload=rollup;

So we now have a menu system that hides the sub-menus. We now require the top-level text to be links. We've deliberately marked up the menu so that the top-level text isn't a link, as without ECMAScript enabled, we'd have redundant links that don't do anything.

window.onload=rollup;

function rollup()
{
    var objNode, objAnchor;

    // Check we're working with a DOM compliant browser
    if (document.getElementById && document.createElement)
    {
        var objMenu = document.getElementById('mainnav');

        var objNested = objMenu.getElementsByTagName('ul');

        // Hide each of the nested unordered list
        for (var i=0; i<objNested.length; i++)
        {
            objNested[i].style.display = 'none';

            // Place the top-level text in an anchor tag
            objNode = objNested[i].parentNode;

            strContent = objNode.firstChild.data;

            objAnchor = document.createElement('a');
            objAnchor.href = '#';
            objAnchor.appendChild(document.createTextNode(strContent));

            objNode.replaceChild(objAnchor, objNode.firstChild);
        }
    }
}

Adding Functionality

The function now removes all the nested unordered lists, and turns our top-level text into links. All that's left now is to add the functionality to each of our newly created links. To achieve this, we create a function called, rollout. The rollout function toggles the sub-menu on and off for the appropriate link. The function returns false, to stop the browser trying to request the page, which in our case is an empty fragment identifier.

function rollout(objMenuitem)
{
    if (objMenuitem.nextSibling.style.display == 'block')
        objMenuitem.nextSibling.style.display = 'none';
    else
        objMenuitem.nextSibling.style.display = 'block';

    // Stop the browser requesting the link
    return false;
}

The function is assigned to the onclick attribute of each link we create.

objAnchor.onclick = function(){return rollout(this);}

Menu Enhancements

We now have a functioning menu, but there are a couple of improvements we can make to the functionality. The menu system only links to external documents. A navigation system is more likely to link to documents within the website, in which case we would want to leave the corresponding menu item open, to indicate that the current location is within that part of the navigation system. We can do this in the rollup function by iterating though the links in each of our sub-menus, and checking if the current location matches the href attribute of the link.

function rollup()
{
    var bRollup, objLinks, objNode, objAnchor;

    // Check we're working with a DOM compliant browser
    if (document.getElementById && document.createElement)
    {
        var strLocation = window.location;

        var objMenu = document.getElementById('mainnav');

        var objNested = objMenu.getElementsByTagName('ul');

        // Hide each of the nested unordered list
        for (var i=0; i<objNested.length; i++)
        {
            // Only hide, if the current location is not found in the list
            bRollup = true;
            objLinks = objNested[i].getElementsByTagName('a');

            for (var j=0; j<objLinks.length; j++)
            {
                if (objLinks[j].href == strLocation)
                    bRollup = false;
            }

            if (bRollup == true)
                objNested[i].style.display = 'none';
            else
                objNested[i].style.display = 'block';

            // Place the top-level text in an anchor tag
            objNode = objNested[i].parentNode;

            strContent = objNode.firstChild.data;

            objAnchor = document.createElement('a');
            objAnchor.href = '#';
            objAnchor.onclick = function(){return rollout(this);}
            objAnchor.appendChild(document.createTextNode(strContent));

            objNode.replaceChild(objAnchor, objNode.firstChild);
        }
    }
}

To illustrate the effect, I've added a few internal links to our menu.

The very last feature we'll consider is for accessibility. Checkpoint 6.4 of the Web Content Accessibility Guidelines 1.0 (WCAG) requires that event handlers are input device-independent (priority 2). From a web developer's point of view, this means choosing application-level event handlers where possible, such as onblur, onsubmit, etc. When it's not possible to choose an application-level event handler, web developers are encouraged to provide redundant keyboard event handlers for each mouse event handler; therefore, we'll need to provide an onkeypress event handler for each onclick event handler.

Checkpoint 1.2 of the User Agent Accessibility Guidelines 1.0 requires that all event handlers can be activated by the keyboard alone (priority 1), which is in conflict with checkpoint 6.4 of WCAG 1.0. Recent versions of Gecko based browsers, Internet Explorer 6, and Opera, all allow mouse event handlers to be activated by the keyboard. If you try what we have so far with one of these browsers, you'll see that the script works as expected when navigating with the keyboard. This means that web developers have to take care to make sure that redundant keyboard event handlers cancel their action, to avoid activating the mouse event handler. This is taken care of in our rollout function, as it returns false to stop the browser requesting the page. To adhere to WCAG 1.0, we'll add a redundant keypress event to our script.

objAnchor.onkeypress = function(){return rollout(this);}

With our new redundant keypress event, the menu works as expected in Internet Explorer, but it's now impossible to navigate using the keyboard with Gecko based browsers or Opera. When you press a key to navigate to the next item, it triggers the onkepress event handler, which expands/collapses the menu item, and cancels any further events so as not to allow the browser to execute the link. This has the undesired effect of cancelling keyboard navigation, which means we need to catch the event to find out which key has been pressed. If it's anything other than enter or space, we need to pass control back to the browser so that it can continue to tab through the list of links. To catch the event, we need to re-write our rollout function to accept the event, and re-write the part that assigns the event handler to the onclick and onkeypress attributes. The complete script is now as follows:

window.onload=rollup;

function rollout(objMenuitem, objEvent)
{
    var iKeyCode;

    // Check if from a keyboard - non IE, but
    // irrelevant as tab doesn't trigger the 
    // keypress event in IE
    if (objEvent && objEvent.type == 'keypress')
    {
        if (objEvent.keyCode)
            iKeyCode = objEvent.keyCode;
        else if (objEvent.which)
            iKeyCode = objEvent.which;

        // If it's not the enter key or space key, 
        // pass control back to the browser
        if (iKeyCode != 13 && iKeyCode != 32)
            return true;
    }

    if (objMenuitem.nextSibling.style.display == 'block')
        objMenuitem.nextSibling.style.display = 'none';
    else
        objMenuitem.nextSibling.style.display = 'block';

    // Stop the browser requesting the link
    return false;
}

function rollup()
{
    var bRollup, objLinks, objNode, objAnchor;

    // Check we're working with a DOM compliant browser
    if (document.getElementById && document.createElement)
    {
        var strLocation = window.location;

        var objMenu = document.getElementById('mainnav');

        var objNested = objMenu.getElementsByTagName('ul');

        // Hide each of the nested unordered list
        for (var i=0; i<objNested.length; i++)
        {
            // Only hide, if the current location is not found in the list
            bRollup = true;
            objLinks = objNested[i].getElementsByTagName('a');

            for (var j=0; j<objLinks.length; j++)
            {
                if (objLinks[j].href == strLocation)
                    bRollup = false;
            }

            if (bRollup == true)
                objNested[i].style.display = 'none';
            else
                objNested[i].style.display = 'block';

            // Place the top-level text in an anchor tag
            objNode = objNested[i].parentNode;

            strContent = objNode.firstChild.data;

            objAnchor = document.createElement('a');
            objAnchor.href = '#';
            objAnchor.onclick = function(event){return rollout(this, event);}
            objAnchor.onkeypress = function(event){return rollout(this, event);}
            objAnchor.appendChild(document.createTextNode(strContent));

            objNode.replaceChild(objAnchor, objNode.firstChild);
        }
    }
}

Final Version

The initial menu was only intended to demonstrate the value of separating behaviour from content. Following requests from the comment section, I've turned it into a generic menu system.

window.onload=rollup;

function rollout(objMenuitem, objEvent)
{
    var iKeyCode;

    // Check if from a keyboard - non IE, but
    // irrelevant as tab doesn't trigger the 
    // keypress event in IE
    if (objEvent && objEvent.type == 'keypress')
    {
        if (objEvent.keyCode)
            iKeyCode = objEvent.keyCode;
        else if (objEvent.which)
            iKeyCode = objEvent.which;

        // If it's not the enter key or space key, 
        // pass control back to the browser
        if (iKeyCode != 13 && iKeyCode != 32)
            return true;
    }

    // Work out what we need to do
    if (objMenuitem.nextSibling.style.display == 'block')
        var strDisplay = 'none'
    else
        var strDisplay = 'block';

    // Close any nested lists that are open
    var objMenu = document.getElementById('mainnav');
    var objNested = objMenu.getElementsByTagName('ul');

    for (var i=0; i<objNested.length; i++)
        if (objNested[i].style.display == 'block')
            objNested[i].style.display = 'none';

    objMenuitem.nextSibling.style.display = strDisplay;
    
    // Keep any parent menus for this item open
    while (objMenuitem.parentNode.parentNode.id != 'mainnav')
    {
        objMenuitem = objMenuitem.parentNode.parentNode;
        objMenuitem.style.display = 'block';
    }

    // Stop the browser requesting the link
    return false;
}

function rollup()
{
    var bRollup, objLinks, objNode, objAnchor;

    // Check we're working with a DOM compliant browser
    if (document.getElementById && document.createElement)
    {
        var strLocation = window.location;

        var objMenu = document.getElementById('mainnav');

        var objNested = objMenu.getElementsByTagName('ul');

        // Hide each of the nested unordered list
        for (var i=0; i<objNested.length; i++)
        {
            // Only hide, if the current location is not found in the list
            bRollup = true;
            objLinks = objNested[i].getElementsByTagName('a');

            for (var j=0; j<objLinks.length; j++)
            {
                if (objLinks[j].href == strLocation)
                {
                    bRollup = false;

                    // Added by John Hunter
                    // remove link for current page
                    objNode = objLinks[j];
                    strContent = objNode.firstChild.data;
                    var objCurrentPage = document.createElement('strong');
                    objCurrentPage.appendChild(document.createTextNode(strContent));
                    objNode.parentNode.replaceChild(objCurrentPage, objNode);

                    // Add an id so we can keep the parents open
                    objCurrentPage.id = 'jsKeepOpen';
                }

            }

            if (bRollup == true)
                objNested[i].style.display = 'none';
            else
                objNested[i].style.display = 'block';

            // Place the top-level text in an anchor tag
            objNode = objNested[i].parentNode;

            strContent = objNode.firstChild.data;

            objAnchor = document.createElement('a');
            objAnchor.href = '#';
            objAnchor.onclick = function(event){return rollout(this, event);}
            objAnchor.onkeypress = function(event){return rollout(this, event);}
            objAnchor.appendChild(document.createTextNode(strContent));

            objNode.replaceChild(objAnchor, objNode.firstChild);
        }

        // Keep any parent menus for the current item
        if (document.getElementById('jsKeepOpen'))
        {
            var objKeepOpen = document.getElementById('jsKeepOpen');
            while (objKeepOpen.parentNode.id != 'mainnav')
            {
                objKeepOpen = objKeepOpen.parentNode;
                objKeepOpen.style.display = 'block';
            }
        }
    }
}

I've included some rudimentary CSS with the final version. We now have a dynamic menu that degrades gracefully if ECMAScript, and/or CSS aren't available.

Category: Scripting.

Comments

  1. [ecmascriptmenu.php#comment1]

    I hate to be the bearer of bad tidings, but the final example doesn't appear to be working in Firefox (works ok in IE). It's probably a typo somewhere as the other examples work fine.

    Great article non-the-less *smile*

    Posted by Richard@Home on

  2. [ecmascriptmenu.php#comment2]

    I hate to be the bearer of bad tidings, but the final example doesn't appear to be working in Firefox (works ok in IE). It's probably a typo somewhere as the other examples work fine.

    Thank you for the feedback. Apart from adding CSS, the only difference in the script on the final version is the part where it adds the event. I've got Firefox 1.0, and it seems to be working OK. It also works in Mozilla 1.2.1, so I'm not sure what could be going wrong. What version of Firefox are you running?

    Great article non-the-less *smile*

    Thank you *smile*

    Posted by Gez on

  3. [ecmascriptmenu.php#comment3]

    Fantastic write up - thank you for sharing this! The final example works in Firefox for me. I've seen similar techniques - but not as detailed and they haven't managed to implement onkeypress properly. I have one question ...

    
    objLinks = objNested[i].getElementsByTagName('a');
    

    Should that be an uppercase 'A'? It obviously works but I was under the impression that content delivered as text/html is held in the DOM in uppercase. I have copied and changed the final version and can confirm it works with uppercase 'A'. Why have you chose lowercase?

    Posted by Michael on

  4. [ecmascriptmenu.php#comment4]

    Fantastic write up - thank you for sharing this!

    Thank you *smile*

    I was under the impression that content delivered as text/html is held in the DOM in uppercase.

    That is the case, but functions like getElementsByTagName work regardless of case. The only time you have to take care is with string comparison functions.

    
    if (objNode.tagName == 'a')
    

    The above code would not match an anchor in content delivered as text/html, but would match an anchor in an XHTML document served as application/xhtml+xml. To ensure I'm forward compatible, I tend to write everything in lowercase, and convert strings to lowercase when doing string comparison.

    
    if (objNode.tagName.toLowerCase() == 'a')
    

    Posted by Gez on

  5. [ecmascriptmenu.php#comment6]

    gez, did you have a snoop around my experiments?

    I've just got in, so I'll take a proper look at them tomorrow. I just had a quick look, but they popped up in a tiny window making it difficult to read the code - probably me just having too much to drink *smile*

    Posted by Gez on

  6. [ecmascriptmenu.php#comment7]

    That's a bloody good read, Jez. Trouble is, you've now given me additional work on what is already an overloaded Sunday: full spec keyboard functionality. Thanks for nothing ;o)

    Posted by Mike Pepper on

  7. [ecmascriptmenu.php#comment8]

    Excellent tutorial Gez *smile* Good work Patrick, the Javascript window does not show all the code, I had to copy and paste it into notepad.

    While were on the DOM, is there a way to have a tab control in a textarea like that in a TextBox in VB?

    Posted by Gaz on

  8. [ecmascriptmenu.php#comment9]

    Thank you, Mike and Gaz *smile*

    Trouble is, you've now given me additional work on what is already an overloaded Sunday

    Sundays are always overloaded. We should start a campaign, write to our local MP, and demand that a Sunday is to include Monday, Tuesday, and Wednesday.

    is there a way to have a tab control in a textarea like that in a TextBox in VB?

    There's not a standard way of doing it, but it could be achieved in a similar way to how the nested lists are rolled up in this tutorial. Use appropriately labelled textareas, roll them up so it looks like one textarea, and make the label the tab target; making sure it degrades gracefully with ECMAScript and/or CSS disabled, and keyboard navigable when ECMAScript is present.

    Posted by Gez on

  9. [ecmascriptmenu.php#comment10]

    Yes!

    Thank you for building this out. I had been in event model heck, trying to stop 'onclick's from percolating or falling through.

    Interpolating the 'A' elements when the page load makes an elegant solution and our menu generation code becomes that much simpler.

    Posted by Bill Humphries on

  10. [ecmascriptmenu.php#comment11]

    All I can say is wow! Great tutorial. I really don't have a good enough handle on javascript at the moment but working on it, hope you don't mind me using it on a project in the near future as its just what I need when its needed. Will definately blog this page later today when I get a chance, too. *smile* Ok I'll shut up now coffee's ready ....

    Posted by Steven Clark aka Norty Pig on

  11. [ecmascriptmenu.php#comment12]

    As in Richard's comment, I'm having the same issue with Firefox 1.0 on WinXP. Javascript's enabled, adblock disabled, no errors. Everthing works fine until the final version.

    As everyone else has said-- thank you for shaing your work!

    Posted by Rob on

  12. [ecmascriptmenu.php#comment13]

    Could the menu work with links "<a href" already being in place, not adding the link function with some scripting? How can that be done?

    Posted by Asking on

  13. [ecmascriptmenu.php#comment14]

    Hi, thanks for the great tutorial, is an interesting read.

    Would it be possible to have the section headings act as links to actual pages, then have an expand/collapse button next to them. I've tried altering the code, but my DOM traversal is not quite up to scratch!!

    so

    
    <ul id="mainnav">
       <li>
          <h3><a href="">Section Heading</a></h3>
          expand
          <ul>
            <li>menu item</li>
            <li>menu item</li>
          </ul>
       </li>
    </ul>
    

    Posted by Steven Urmston on

  14. [ecmascriptmenu.php#comment15]

    Thank you everyone who's given feedback. This is a rushed response, as I've just got in work. I'll give a more considered response when I get back this evening in case I have anything wrong *smile*

    Could the menu work with links "<a href" already being in place, not adding the link function with some scripting? How can that be done?

    The examples here haven't been marked up as a link, so that it degrades without a redundant link if ECMAScript isn't available. If the top-level title takes you to an overview of that section, then it would be appropriate to use a link. In that scenario, it's just a case of assigning the onclick and onkeypress events to the link. This can be done using previousSibling.previousSibling for each unordered list found in the loop of the rollup function.

    
    objNested[i].previousSibling.previousSibling.onclick=function(event){return rollout(this, event);}
    objNested[i].previousSibling.previousSibling.onkeypress=function(event){return rollout(this, event);}
    

    The rollout function would require nextSibling.nextSibling.

    
    if (objMenuitem.nextSibling.nextSibling.style.display == 'block')
    	objMenuitem.nextSibling.nextSibling.style.display = 'none';
    else
    	objMenuitem.nextSibling.nextSibling.style.display = 'block';
    

    That would work without amendment for <hn><a href="#">link phrase</a></hn>.

    Thank you to everyone who's provided feedback about a problem with some versions of Firefox. I've tested it on a few more machines, but still haven't been able to reproduce the problem. I'll keep looking into it, but if anyone else can shed any light on it (like what happens: Is anything rendered? does it work with styles disabled?), I would be most grateful *smile*

    Posted by Gez on

  15. [ecmascriptmenu.php#comment18]

    Works fine for me in Firefox 1.0 (on Win 2K)
    Disabling Javascript simply results in an expanded group of nested lists displaying all the menu items.

    My only reservation is the onclick action that's required to activate the menu. These days, the browsing public are familiar with menus using a simple mouseover action they may overlook the need to actually click on a link (unless prompted).

    Anyways, a minor consideration! *shocked* )

    Great job Gez!

    Posted by Paul on

  16. [ecmascriptmenu.php#comment19]

    Hi Gez,
    Just to re-re-iterate what everyone else has said great article.
    I've been struggling with this aspect of design for some time, and the need to do somthing for our own web site is getting ever greater. UDM4 hasn't delivered the solution as far as I'm concerned, this looks much more promising. I'd like to experiment more with it before shouting yeeehaaa !
    My biggest problem was always getting section headings and separators between sections, UDM could not do it without some horrible kludge in the css.
    Can you drop me a line outside of ,I seem to have lost your email address.

    Robert

    Posted by robert campbell on

  17. [ecmascriptmenu.php#comment20]

    Gez - great menu system. Thanks for being so generous with your knowledge!

    I've been playing with the script since you first posted it, and I'm trying to amend it so that only one nested list can be expanded at any given time. Unfortunately, I'm fairly DOM-challenged. Any ideas on how I might implement that feature?

    Thanks!

    Posted by Alexa on

  18. [ecmascriptmenu.php#comment21]

    Hi Alexa, and thank you for your kind words.

    I'm trying to amend it so that only one nested list can be expanded at any given time.

    It's a similar technique to the rollup function, except there's no need to turn top-level text into anchor elements. As we would need to know what to do with the current action, assign the property value to a variable. We can then close any nested lists that are open, then set the current value so that it toggles on and off. The following is the complete rollout function to replace the existing rollout function that should only allow one nested list to be toggled at a time.

    
    function rollout(objMenuitem, objEvent)
    {
        var iKeyCode;
    
        // Check if from a keyboard - non IE, but
        // irrelevant as tab doesn't trigger the 
        // keypress event in IE
        if (objEvent && objEvent.type == 'keypress')
        {
            if (objEvent.keyCode)
                iKeyCode = objEvent.keyCode;
            else if (objEvent.which)
                iKeyCode = objEvent.which;
    
            // If it's not the enter key or space key, 
            // pass control back to the browser
            if (iKeyCode != 13 && iKeyCode != 32)
                return true;
        }
    
        // Work out what we need to do
        if (objMenuitem.nextSibling.style.display == 'block')
            var strDisplay = 'none'
        else
            var strDisplay = 'block';
    
        // Close any nested lists that are open
        var objMenu = document.getElementById('mainnav');
        var objNested = objMenu.getElementsByTagName('ul');
    
        for (var i=0; i<objNested.length; i++)
            if (objNested[i].style.display == 'block')
                objNested[i].style.display = 'none';
    
        objMenuitem.nextSibling.style.display = strDisplay;
    
        // Stop the browser requesting the link
        return false;
    }
    

    Posted by Gez on

  19. [ecmascriptmenu.php#comment22]

    Hi there,

    Nice solution, and well written up.

    Works for me on Firefox 1.0 Mac OS X* and on Safari 1.1

    One issue with the menu is that the current page is represented in the menu as a link. Its good practice (and an accessibility requirement) to avoid having links to the current page. The following amendment to the menu script replaces the current page link with the text wrapped in a strong tag.

    Add the commented section to the rollup() function:

    
    ...
    // Hide each of the nested unordered list
    for (var i=0; i<objNested.length; i++)
    {
        // Only hide, if the current location is not found in the list
        bRollup = true;
        objLinks = objNested[i].getElementsByTagName('a');
    
        for (var j=0; j<objLinks.length; j++)
        {
    		
            if (objLinks[j].href == strLocation) {
                bRollup = false;
    			
    			// 
    			// remove link for current page
    			//
    			objNode = objLinks[j];
    			strContent = objNode.firstChild.data;
    			var objCurrentPage = document.createElement('strong');
    			objCurrentPage.appendChild(
    					document.createTextNode(strContent));
    			objNode.parentNode.replaceChild(objCurrentPage, objNode);
    			
    			// end remove link for current page
    			
    		}
        }
    
        if (bRollup == true)
        ...
    

    It won't modify the original html so self-referencing links will still effect non-script browsers and accessibility auditing tools, but it will benefit most users.

    Cheers.

    * = Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.7.5) Gecko/20041107 Firefox/1.0

    Posted by John Hunter on

  20. [ecmascriptmenu.php#comment23]

    Hi John,

    Thank you for your kind words, and enhancement to the script.

    One issue with the menu is that the current page is represented in the menu as a link. Its good practice (and an accessibility requirement) to avoid having links to the current page.

    It's not an accessibility requirement, but self-targetting links can cause usability problems.

    Thanks again for the enhancement *smile*

    Posted by Gez on

  21. [ecmascriptmenu.php#comment24]

    If I contain this menu (which is great - thankyou) in a floated div, there are problems in Safari. The problem occurs when one of the main menu items is clicked, revealing the sub-menu: then, anything that's pushed below the original depth of the DIV disappears. Brushing the mouse over them reveals them, but it's not ideal. Just thought you should know.

    Posted by Virginia on

  22. [ecmascriptmenu.php#comment25]

    Re the above comment: after some mucking around, it appears that it's not as simple as I made out. I'll keep tinkering and see if I come up with anything.

    Posted by Virginia on

  23. [ecmascriptmenu.php#comment26]

    Ah-hah!

    Finally. I figured out how to use this menu, but horizontally. You have to float the elements.

    
    #menu>li{
    float: left;
    width: 100px;
    }
    

    I'm loving this. Because it's barebones in the HTML, I can use pure CSS to style it. Thank goodness for Chef Juicy! *grins*

    Posted by Xaxio on

  24. [ecmascriptmenu.php#comment27]

    Hi Gez, thanks for the great code!

    In reading your reply regarding the ability to only allow one nested list open at a time, the dropin replacement rollup function does not support nested lists that are more than one level deep. When selecting a second level list, it collapses everything.

    Also, I'm trying to extend what you've done to allow the first child element of a list to be a link to a summary as well as expand it's list. You discuss this in reply #19 where you replied to comment #15.

    Unfortunately I don't quite follow what you are attempting to do here, and can't get it to work. I understand that you are suggesting not making the firstChild content into an anchor but adding the onClick/OnKeyPress event... can you clarify?

    Thanks a bunch & thanks for sharing!

    Posted by Fishysan on

  25. [ecmascriptmenu.php#comment28]

    Finally. I figured out how to use this menu, but horizontally. You have to float the elements.

    Thank you for posting your solution, Xaxio; Nice work *smile*

    Posted by Gez on

  26. [ecmascriptmenu.php#comment29]

    Hi Gez, thanks for the great code!

    Thank you, Fishysan.

    the dropin replacement rollup function does not support nested lists that are more than one level deep.

    If I find some free time over the weekend, I'll look into it.

    Unfortunately I don't quite follow what you are attempting to do here, and can't get it to work. I understand that you are suggesting not making the firstChild content into an anchor but adding the onClick/OnKeyPress event... can you clarify?

    I've added an example of a menu system that already has links in place, and the supporting ECMAScript that would be required to make it work.

    I hope that helps. Thank you for your positive comments :-)

    Posted by Gez on

  27. [ecmascriptmenu.php#comment30]

    This code is just unbelievable useful. Thank you very much for sharing your work, Gez, as so many people have expressed before I found this resource.

    I have one last question considering Xaxio's comment.
    I would not mind using the script horizontally as well, but I do not get the script to work, although I've floated the elements.

    Do I understand you guys correctly that the final navigation could look like "Content | Presentation | etc"
    instead of

    Content
    Presentation
    etc.

    Where do I have to fill in the new parameters to show navigation horizontally?

    Posted by Michael on

  28. [ecmascriptmenu.php#comment31]

    Hi there, just a brief line to thank you for the excellent job.
    I've been googling around (instead of studying it properly, shame shame) for some good nested menu code, and had found some with lots of id's for the UL's and LI's, your code is FAR cleaner. I'll be implementing it in a production site (Siena University Sciences Faculty Libray, in Italy), what sort of reference to you shall I put in the code?
    Cheers.
    Sacha

    Posted by Sacha on

  29. [ecmascriptmenu.php#comment32]

    Thank you for your kind comment, Sacha.

    I'll be implementing it in a production site (Siena University Sciences Faculty Libray, in Italy), what sort of reference to you shall I put in the code?

    A link to this page in the code would be very kind of you, but there's no obligation.

    Thanks again for your kind words.

    Posted by Gez on

  30. [ecmascriptmenu.php#comment33]

    This code is just unbelievable useful.

    Thank you very much :-)

    I would not mind using the script horizontally as well, but I do not get the script to work, although I've floated the elements ... Where do I have to fill in the new parameters to show navigation horizontally?

    The script only relates to the behaviour, and has nothing to do with how it will be presented. The CSS in the final version was just an example of styling it, and to demonstrate separating presentation, content, and behaviour. For completeness, I've included a very crude inline version of the menu system. A better version could be created with more effort, but it should give you a rough idea.

    Posted by Gez on

  31. [ecmascriptmenu.php#comment34]

    Jipp, your quick implementation helped me out. Thanks a lot!
    I am now wondering whether I have to allocate the size of the undocked nest downwards! Or can I directly start with my content underneith your navigation?

    I will refer to this resource in my source as well.
    Nice that is not mandatory, but it would be a shame if developers would not mention its code's origin...

    Cheers
    Michael

    Posted by Michael on

  32. [ecmascriptmenu.php#comment35]

    No questions, just praise. This was just what I was looking for. It works great, and every time I check back it's gotten better. Thank you for sharing it.

    Posted by Anthony on

  33. [ecmascriptmenu.php#comment36]

    I am now wondering whether I have to allocate the size of the undocked nest downwards! Or can I directly start with my content underneith your navigation?

    It depends on how you want it to display. You could start directly with your content underneath the navigation, but if you have many items in a list, the gap between the navigation and the content maybe too large. You could have it so that the content displays immediately beneath the unopened list, and expands over the top of your content.

    I will refer to this resource in my source as well.
    Nice that is not mandatory, but it would be a shame if developers would not mention its code's origin...

    Thank you, I appreciate that *smile*

    Posted by Gez on

  34. [ecmascriptmenu.php#comment37]

    Hi Gez. Great article, I must say!

    I do have a question. Not being an expert on ECMAScript (but feeling that I am in CSS design) I know that I currently use a common method of indicating to the user what page you are currently on. Specifically:

    
    ---------------------------
    
    HTML
    ---------------------------
    <body id="presentation">
    ...
    <ul id="mainnav">
    	...
    	<li id="navpresentation">Presentation
    		<ul>
    			<li><a href="http://www.w3.org/TR/REC-CSS1">CSS 1</a></li>
    			<li><a href="http://www.w3.org/TR/REC-CSS2/">CSS 2</a></li>
    		</ul>
    	</li>
            ...
    </ul>
    ---------------------------
    
    
    ---------------------------
    
    CSS
    ---------------------------
    body#presentation #navpresentation {
            background: green; /*as an example*/
    }
    ---------------------------
    

    Seeing as we are seperating content from presentation and behaviour, I wonder if there is a simpler way to take this behaviour and incorporate it into the ECMAScript. Would there still be a need for the IDs on the list items? I would think that would be the only way to uniquely identify the menu items. That seems to make sense to me.

    Many thanks. Marco

    Posted by Marco on

  35. [ecmascriptmenu.php#comment38]

    Hi Marco,

    Thank you for your praise of the menu system.

    I'm sorry, but I don't really follow your question. The usual method of highlighting the current page in a menu system to the visitor is to not make the entry a link. In that case, non-link text (li) could be styled differently to the rest of the menu. An ID on a menu item isn't behavioural; it's a hook that can be used for CSS and/or ECMAScript. If the ID is required regardless, then it should be included in the content. If it's an ID that could be assigned to a list item depending on some condition, then it could be assigned in the script:

    
    objNode.id = 'currentitem';
    

    If that's not what you meant, please ask again and I'll try and get my brain into gear *smile*

    Posted by Gez on

  36. [ecmascriptmenu.php#comment39]

    Hi Gez. Thanks for the response. I apologize. Allow me to clarify what I said.

    What I meant to say was, I wonder if there is a simpler way to take this CSS hook from my example above and incorporate it into the ECMAScript?

    So, if I assigned an ID to each navigation item, could the script be used in a way to indicate where you are.

    I do see the method that was suggested by John Hunter that removes the current link. I guess I am expanding on that with a visual cue. I hope this is clear. If not, I'll try my best to elabourate.

    Thanks again. Marco.

    Posted by Marco on

  37. [ecmascriptmenu.php#comment40]

    Sorry, my brain must have been dead. I just read my last message and gave my head a shake. I am going to try to explain this properly this time. Here we go.

    Let us assume that each top level link has a default page. So, for example, I am starting on mysite.com. when I click on 'Presentation', I go to mysite.com/presentation/index.html

    User clicks top level link and goes to the above default page.

    On mysite.com/presentation/index.html, the sub navigation is open for "presentation' with a visual cue that tells the user they are in the particular section. So, 'Presentation' would be highlighted in a different colour of some sort. The same behaviour would also happen for any sub navigation as well.

    So, back to my question. If I did assign a unique ID to each main nav and sub nav, can the script mimic this? I think that perhaps I'm asking for some different functionality here. But, I'm hoping this is clear.

    Sorry for the confusion. Many thanks again! Marco


    Posted by Marco on

  38. [ecmascriptmenu.php#comment41]

    Hi Marco,

    I do see the method that was suggested by John Hunter that removes the current link. I guess I am expanding on that with a visual cue.

    With the example you describe, you could specify a selector directly without using a redundant id:

    
    #mainnav ul li
    

    Each of the links would be overriden with the #mainnav a selector, leaving the text to be styled using the selector above.

    Alternatively, you could add an id to the relevant node, which would be necessary if you wanted to do more than just change the background and foreground colour of text, in which case you would refer to the dynamic id in your CSS.

    Hope that helps.

    Posted by Gez on

  39. [ecmascriptmenu.php#comment42]

    Oops, I accidentally deleted this comment.

    In reading your reply regarding the ability to only allow one nested list open at a time, the dropin replacement rollup function does not support nested lists that are more than one level deep. When selecting a second level list, it collapses everything.

    I managed to find a few minutes between meetings. The following would keep the parent lists for multiple sub-menus open:

    
    // Keep any parent menus for this item open
    while (objMenuitem.parentNode.parentNode.id != 'mainnav')
    {
        objMenuitem = objMenuitem.parentNode.parentNode;
        objMenuitem.style.display = 'block';
    } 
    

    I've added a new nested list as an example, and incorporated John Hunter's suggestion for not making the current page a link.

    Posted by Gez on

  40. [ecmascriptmenu.php#comment43]

    Hi Marco,

    I've just noticed your reply - we must have posted at the same time. In the example above (which I accidentally deleted while trying to find on the page), you could add a class directly instead of changing .style.display to each of the parent links so you can style them individually. Obviously, the class would require display: block;.

    
    // Keep any parent menus for this item open
    while (objMenuitem.parentNode.parentNode.id != 'mainnav')
    {
        objMenuitem = objMenuitem.parentNode.parentNode;
        objMenuitem.class = 'openmenu';
    } 
    

    Posted by Gez on

  41. [ecmascriptmenu.php#comment44]

    This is a great menu! Thank you for all the fine work you've done.

    On the latest version of the menu that keeps multiple submenus open, I'm finding that internal links under that submenu do not stay open on the subsequent page. For example:

    Content
    HTML
    XHTML
    XHTML1.0 (linked to an internal page)

    When XHTML1.0 is clicked, the resultant page's menu will show only:

    Content
    HTML
    XHTML

    When you click XHTML, you then see the items nested under it and the link properly removed on XHTML1.0 - I'd think that the menu should be open to the level that contains the current page, no?

    Am I missing something?

    Posted by Peter on

  42. [ecmascriptmenu.php#comment45]

    Hi Gez. Thank you for your patience and your feedback.

    I will digest and have a go at your points above. If I have any major issues, well, you know where I'll be posting *smile*

    Many thanks! Marco

    Posted by Marco on

  43. [ecmascriptmenu.php#comment46]

    The menu was not working for me with Firefox 1.0.
    Turns out TBE (Tabbrowser Extension) was the culprit.
    I disabled a lot of the settings to see if I could make it work, but had no luck; uninstalling TBE was the only remedy.

    Nice job on the menu, though! I may try to use it, along with cookies for keeping the menus expanded (I have sub-pages that are not in the menu, so the strLocation matching won't work).

    Posted by Chuck Norris on

  44. [ecmascriptmenu.php#comment47]

    Hi Peter,

    Am I missing something?

    You're not, but I was :-) When I wrote the system, I only intended it to work with two-levels as the purpose was to show the separation of behaviour, presentation, and content; it was never intended to be a full-blown, plug and play menu system. As I've added bits and pieces, I haven't tested it properly through lack of time. I'm really grateful for your comment, as it's prompted me to test it properly. Hopefully, it should now be a full-blown menu system.

    I've updated the files, and added extra levels under "Firefox Extensions" to demonstrate it:

    Thank you for your comment.

    Posted by Gez on

  45. [ecmascriptmenu.php#comment48]

    Hi Chuck,

    Turns out TBE (Tabbrowser Extension) was the culprit.

    Thank you for letting me know - I suspect that must be the same problem that a few others have experienced with Firefox. If it's a clash with that extension, it could be that they're using the id 'mainnav'. If that is the problem, then anyone using TBE could try replacing mainnav with some other id that's likely to be unique to see if that works.

    Thank you very much for reporting it, Chuck. I've never seen any of your films, but I hear they're good *smile*

    Posted by Gez on

  46. [ecmascriptmenu.php#comment49]

    I haven't had time to do anything with your code to test the problem with TBE, but it works with some of your demo menus posted in your article, until you get past the redundant demo.

    We had a discussion about it here:
    http://www.dslreports.com/forum/remark,12687171~mode=flat

    If I have some time this weekend, I'll do some investigating.

    Posted by Chuck Norris on

  47. [ecmascriptmenu.php#comment50]

    Nicely done Gez - very similar vein (and code structure) to easymenu.co.uk. I like the idea of attaching the anchor to the parent "starter" items - mind if I nick that for a future release of mine?

    Keep up the good work *shocked* )

    Sam

    Posted by SamHS on

  48. [ecmascriptmenu.php#comment51]

    Thanks, Sam *smile*

    I like the idea of attaching the anchor to the parent "starter" items - mind if I nick that for a future release of mine?

    Feel free to use it.

    Cheers

    Posted by Gez on

  49. [ecmascriptmenu.php#comment52]

    I've come back to the site and seen that the menu has moved on in leaps and bounds. As Gez modestly puts it

    Hopefully, it should now be a full-blown menu system.

    .

    I'll offer up my 10p worth for comment.

    In the standard menu we can not tell by looking at the menu which items have sub menus attached. We have no indication that there is any more information beyond what's presented.

    Taking a usability standpoint (and congnitive perception too) we need some indication of 'more'.

    By adding a IMG object as a child node to the anchor at the top of each collapsible section we can have a 'collapsible indicator'

    Create the image object

    
        var objImg = document.createElement('img');
        objImg.setAttribute('src', 'right-red.gif');
        objImg.setAttribute('width', '12');
        objImg.setAttribute('height', '12');
        objImg.align="right";
        objImg.border="0";
        objImg.alt="click for submenu";
    

    and now attach it to the dom in the right place..within the rollup function

    
        objAnchor = document.createElement('a');
        objAnchor.href = '#';
        objAnchor.onclick = function(event){return rollout(this, event);}
        objAnchor.onkeypress = function(event){return rollout(this, event);}
        objAnchor.appendChild(objImg);
        objAnchor.appendChild(document.createTextNode(strContent));
    
    

    You can see the example at http://www.ecommnet.co.uk/test/juicymenu/testthis4.asp
    I've also managed to mangle the CSS to give
    Section headings, and section separators, I personally don't like section headings as links but that's just taste. Note the color scheme is just for test, or maybe you like it?

    I've tested in FF, IE, Safari on Mac and PC where appropriate and I believe its functional in all but IE5 on the Mac.

    Posted by robert campbell on

  50. [ecmascriptmenu.php#comment53]

    Hi Robert,

    Good work *smile* Someone at work brought up the issue about sub-content indicators on their personal website, and I helped them add a right pointing double angle quotation mark (&#187 *wink* . I like the idea of using images, though. I would avoid adding presentation attributes with ECMAScript, as the purpose was to keep content, presentation, and behaviour separate:

    
        objImg.align="right";
        objImg.border="0";
    

    A better solution would be to add a class, and put the rules for the styles in a style sheet.

    Good work though, and thank you for reporting back with your findings.

    Best regards,

    Posted by Gez on

  51. [ecmascriptmenu.php#comment54]

    I would avoid adding presentation attributes with ECMAScript, as the purpose was to keep content, presentation, and behaviour separate:

    Point taken, however I think the image is part of the behaviour, well its a grey area, and more to the point I couldn't get the class thing to work ! :--{

    Robert

    Posted by robert campbell on

Comments are closed for this entry.