A List Apart

Menu
Issue № 73

DOM Design Tricks II

by Published in CSS, HTML, JavaScript

In this tutorial, we’ll use the event-handling capabilities of the Document Object Model (DOM) to make some minor improvements to the expanding menu that we developed in a previous tutorial. [Truth in advertising: these improvements are borderline cute; their real purpose is to serve as a vehicle to introduce the concepts of events and nodes.]In the previous tutorial, we discussed using the CSS display property to make an expanding menu like the one below. In that tutorial, only the small plus or minus sign images were clickable. However, if you try the menu below, you’ll see that all the words in the main headings are active and clickable; not just the small plus or minus signs. This won’t work in Navigator 4.

plus sign Search Engines

plus sign Web Design

plus sign Digital Photography

The original code used the onMouseOver and onMouseOut event handlers to detect when the mouse moved over the images.Note: the code that we’ll show in the text of this article is designed to work with Mozilla, a DOM-compliant browser.  If you want code that will work on other browsers, you can use the cross-platform DOM utilities include file, which was developed in another article. Using the View Page Source option in your browser will, of course, show the actual code.Before we go into the code, though, we have to discuss the two ways that the DOM handles events:

Capturing and Bubbling Events

Aside from using onMouseOver and onMouseOut, the DOM lets you attach an event listener to the objects in your document.  These listeners wait for events to occur, and they can be told to listen for events in one of two ways. The first method is known as event capture; the second is called event bubbling.

Event Capture

diagram showing arrows moving downwardsLet’s say that your document contains a <div> which contains a <p> which contains an <img>. Further, let’s say you’ve added an event listener to all of them using the capture method. When a user clicks on the image, a mouseclick event occurs.Even though the user clicked the image, the image doesn’t get the event first.  Instead, the event listener attached to the document grabs the event first and processes it.  (That is, it captures the event before it gets to its intended target.) The event is then passed down to the <div>’s event listener.  The event then goes to the <p>, and finally to the <img>. That is, all of the clicked-on object’s “ancestors” higher up in the document capture the event for processing before sending it down the chain to its intended target. You can try event capture in a pop-up window. (This only works in Mozilla.) [Ed. Note: Since this article was originally published, the event capture pop-up now works in most browsers.]

Event Bubbling

diagram showing arrows moving upwardsNow let’s look at the same situation from the inside out.  You have an <img> inside a <p>, which is inside a <div>, which is inside your document. We’ve attached event listeners to all the objects, specifying the bubble method. When a user clicks the image, this time the events rise like a bubble in a glass of water.  The click’s original target, the <img>, gets to see the event first, and then passes it upwards to the <p> for further processing, which passes it on to the <div>, which finally passes it up to the document.  You can try event bubbling in a pop-up window. (This only works in Mozilla.)  [Ed. Note: Refer to previous Editor’s Note.]Here are the relevant parts of the source code for both demos.

Setting Events

Now that we know how the DOM handles events, we have to find out how to add event listeners to objects. we’ll tell each <div> in the menu headings to listen for mouse click, mouse over, and mouse out events.To make it easier to set the event listeners, we’ll need a utility routine that gives us back a reference to an object in a document when we give the object’s name. This function uses variable isNav6, which is part of the DOM Utilities package introduced in an earlier tutorial.
function getObject( objName )
{
    if (isNav6)
    {
       return document.getElementById( objName );
    }
}
Once we have the object, we can add an event listener with a statement of this form:
obj.addEventListener( "name", functionName, useCapture );
...where:
name
is the name of the event you want the object to listen for
functionName
is the name of the function that will process that event
useCapture
is a boolean that tells whether you want to use event capturing (true) or event bubbling (false).
So, what looks like this in HTML:
<div id="m0"
  onMouseOver="highlight()">
...is achieved like this when using the DOM:
var obj = getObject("m0");
obj.addEventListener( "mouseover", highlight, false );
We use the getObject function from above to write a setup() function to add the event listeners to each of the menu <div>s, which have ids of m0, m1, and m2.
var nMenus = 3;function setup( )
{
    var i;
    var obj;    for (i=0; i < nmenus; i++)
    {
        obj = getObject("m" + i);
        if (isNav6)
        {
            obj.addEventListener("mouseover", highlight, false);
            obj.addEventListener("mouseout", dim, false);
            obj.addEventListener("click", showMenu, false);
        }
    }
}
Now, we have to write the highlight(), dim(), and showMenu() functions. In order to do this, we must first understand the concept of a Node.

Nodes

As we detailed in a previous article, your documents are made up of “spare parts” that are nested inside one another. This HTML…
<body>
<p>
   First Text
   <img src="picture.png">
   <i>More Text</i>
</p>
</body>
...has this structure:
  • A <body>, which contains
    • A paragraph (<p>), which contains
      • the First Text
      • an image (<img>)
      • and an italic (<i>) element which contains
        • More Text
When CSS-compliant browsers read your HTML files, they analyze this structure and create a structure of nodes, which you can think of as little chunks of information that describe your document.  The nodes for the HTML above can be diagrammed as shown below.  The top half of each node shows the nodeName, and the bottom half shows the nodeValue. You can click the diagram to open up a window that shows a larger version.Tree DiagramNodes also contain information about their relationship to each other. Let’s look at these nodes as if they were members of a family tree.
  • Node B’s parentNode is node A.
  • Node B’s firstChild is node C, and its lastChild is node E.
  • Node B also contains a childNodes array that lists all its immediate children, C, D, and E).
  • Node D has a previousSibling, which is node C, and a nextSibling, node E.
  • Since node C has nobody to its left and nobody immediately below it, its previousSibling and firstChild properties will both be null.
You may explore these relationships more fully in a new pop-up window.We’re going to use the ability to move through this “family tree” of a document’s nodes to find out which <div> a user clicked in the expanding menu.For a quick quiz on nodes, see if you can answer these questions:
  1. Who is node F’s parentNode?
  2. Who is node A’s firstChild?
  3. Who are node B’s nextSibling and previousSibling?

See the answers

  1. Node E is node F’s parent.
  2. Node B is A’s firstChild (and also its lastChild).
  3. Node B is an only child, so its previousSibling and nextSibling are null.

Finding the <div>

When an event occurs, the corresponding event listener function is invoked.  This function should be written to expect one argument – an Event object. The event’s target property is the one we’re interested in; it tell us which Node is the event’s intended target.
When we click the words or the image in a <div>, the target that gets returned is actually the text or the image inside the <div>, not the <div> itself. [The event’s currentNode property is the one we really want, but it’s not implemented in Mozilla release 15.]
Therefore, we have to write a function which checks to see if the target of the event we pass to it is a DIV element (tag).  If not, the function keeps looking at that node’s parentNode, and its parent, and so forth, until it finds a DIV element.

function findOwner(evt)
{
    var node = evt.target;
    while (node)
    {
        if (node.nodeType == Node.ELEMENT_NODE &&
                 node.nodeName == "DIV")
        {
                return node;
        }
        node = node.parentNode;
    }
    return null;
}
Once we have the <div>’s node, we can change its properties, which we’ll need to do for the highlight() and dim() functions. Notice that we change the shape of the cursor when it moves over the <div>. [This does not work in Internet Explorer.]
function highlight(evt)
{
    var obj = findOwner(evt);
    if (isNav6) { obj.style.cursor = "pointer"; }
    obj.style.color= "#ff0000"; // red
}function dim(evt)
{
    var obj = findOwner(evt);
    if (isNav6) { obj.style.cursor = "default"; }
    obj.style.color = "#000000"; // black
}

Implementing showMenu()

All that remains now is to implement the showMenu() function, which expands and contracts the menu. The line numbers in the code below are for reference only.
 1 function showMenu(evt)
 2 {
 3        var owner = findOwner(evt);
 4 
 5        var divNum;
 6 
 7        divNum = owner.attributes.getNamedItem("id").nodeValue;
 8 
 9        divNum = parseInt(divNum.substr(1));
10 
11        if (getIdProperty("s" + divNum, "display") != "block")
12        {
13                setIdProperty("s" + divNum, "display", "block");
14                document.images["i" + divNum].src = "minus.png";
15        }
16        else
17        {
18                setIdProperty("s" + divNum, "display", "none");
19                document.images["i" + divNum].src = "plus.png";
20        }
21 }
Here are the lines of interest:
Line 3
  As in the highlight() and dim() functions,   we first have to find the “owner” of the click   event.
Line 7
  A node’s attributes property gives a list of   all the tag’s attribute=value pairs. It’s   indexed by string, so we need to use the getNamedItem()   method to find the id attribute’s value (given by   the nodeValue property).
Line 9
  Since we’ve cleverly named all the <div> tags   with ids like m0, m1, etc., we use   the substr method to extract the digit.
Line 11
  Checks to see if a submenu is already displayed by   looking at its display property.
Lines 13-14, 18-19
  We’ve also numbered the ids of the submenus and   the images, so it’s easy to change their properties and   image sources.
It’s a lot of code, but once you understand it, you’ll be prepared to manipulate nodes for very powerful effects. Let’s summarize:

Summary

You use the addEventListener() function to make a document object react to events rather than having to use onEvent= handlers in the tags. You may look at these events as they are passed down from the document to the the target (capture), or as they are passed up from the target to its enclosing elements (bubbling).When the event listener function is invoked, you get an Event object that tells you the target for which the event was destined.  This target may not always be the object whose properties you wish to affect, so it might be necessary to walk through the target node’s “family tree” until you find the node you want.Being able to attach event listeners to a document’s objects is a great convenience; knowing how to exploit the hierarchy of a document’s nodes gives us immense power.  we’ll learn more about that in the next tutorial.

No Comments

  1. Sorry, commenting is closed on this article.