Article
DHTML Utopia: Modern Web Design Using JavaScript & DOM
Chapter 3. Handling DOM Events
When I can't handle events, I let them handle themselves.
--Henry Ford
An event is something that happens, be it in real life, or in DHTML programming. But to those working with DHTML, events have a very specific meaning. An event is generated, or fired, when something happens to an element: a mouse clicks on a button, for example, or a change is made to a form. DHTML programming is all about event handling; your code will run in response to the firing of this or that event.
Learning which events are available, how to hook your code up to them, and how to make best use of them is a critical part of building dynamic Web applications. (It does seem that there are quite a few "critical" bits, I know!) That's what we cover in this chapter, along with a couple of real-world examples.
About Elements and Events
We're using a modern approach to DHTML, so all our DHTML code will be set to run in response to the firing of an event. If you've done any JavaScript Web programming before, you may already be using this technique without knowing it. Let's look at the procedure by which code has traditionally been hooked up to events, learn how to do it under the DOM (and why the DOM method is better), and find out exactly what these techniques make possible.
Common Events
Every page element fires a given selection of events. Some events are common to all elements; others are more specific. For example, all visible elements will fire a mouseover event when the mouse is moved over them. A change event, however, will only be fired by elements whose contents can be changed: text boxes, text areas, and drop-down lists.
You might have noticed above that I used mouseover, rather than onmouseover, for the event name. Even though the HTML attribute for handling this event is onmouseover, the modern way to describe the event itself is simply mouseover. This allows us to talk about the event (mouseover) and the event handler (onmouseover) separately. The event handler is the location at which an event handler is placed. In the bad old browser days, these concepts were all mixed up, but now we can safely think of them as separate entities.
The documents that describe the events fired by a given element are the W3C DOM specifications and HTML recommendations, which were mentioned in the last chapter, as well as the W3C DOM 2 Events specification. There's also some extra information on key events in the DOM 3 Events specification.
A summary of the events that you're likely to find useful, and that have cross-browser support, is given in Table 3.1. Note that this isn't an exhaustive survey: it's a listing of events that you're likely to use often, rather than everything under the sun.
Table 3.1. Useful Events.

Click to popup a larger image of this table.
Hooking Code to Events
So, now you know some common events, and when they fire. But how do you make your code run in response to those events?
Hooking up the Old Way
If you've done any JavaScript coding before, you'll probably have written something like this:
<a href="somewhere.html"
onclick="myJavaScriptFunction(); return false;"
>click me!</a>
That onclick attribute connects some JavaScript code to that link's click event. When the link is clicked, it will fire a click event, and that code will run. No problem! Notice, though, that the code never actually mentions "click," which is the actual name of the event.
What if we wanted to detect a keypress? Here's the equivalent script:
function aKeyWasPressed() {
// put event handler code here ...
}
And here's the matching snippet of HTML:
<textarea id="myta" onkeypress="aKeyWasPressed()"></textarea>
In this case, how does our aKeyWasPressed function know which key was pressed? Well, it doesn't. That's a major limitation of the old-fashioned approach. But we can improve on that!
Hooking up the DOM Way
The DOM specifications enlarge the idea of event handlers by providing event targets and event listeners. An event target is the thing at which an event is aimed—an element, essentially. An event listener is the thing that grabs the event when it appears, and responds to it. Where do events come from in the first place? They come from the user. The browser software captures the user action and sends the event to the right event target.
A given event source can be relevant to more than one event listener. Using the old-fashioned method above, only one piece of code could be run in response to any event. For example, an element could have only one onclick attribute. (Actually, you could have as many as you liked, but each one would overwrite the one before it, so, effectively, you have only one. Alternatively, you could string JavaScript statements together, using semicolons in the attribute, but this makes the HTML code even more cluttered.) Using the modern method, you can run as many pieces of code as you want upon the firing of an event or events. Listeners get to share events, and events get to share listeners. To facilitate this, we must move our "hookup" code from the HTML to a separate script section: as noted above, no element can have more than one onclick attribute.
Event handling works in different ways, depending on the browser. We'll examine the W3C-approved way first, before we look at event handling in Internet Explorer. Here's the W3C approach.
Example 3.1. keycodedetect.html (excerpt)
function aKeyWasPressed(e) {
// put event listener code here...
}
var textarea = document.getElementById('myta');
textarea.addEventListener('keyup', aKeyWasPressed, false);
And here's the matching bit of HTML:
Example 3.2. keycodedetect.html (excerpt)
<textarea id="myta"></textarea>
HTML Before Script... for Now
If you're working through this example in your HTML editor of choice, be sure to place the JavaScript code after the HTML in this and the next few examples in this chapter. The textarea must exist before the JavaScript code can assign an event listener to it.
If you're used to placing JavaScript at the top of your HTML files, don't fret. We'll discuss an elegant way around this restriction at the end of the section.
Those few lines of code contain a number of complex concepts. Consider this snippet:
Example 3.3. keycodedetect.html (excerpt)
var textarea = document.getElementById('myta');
Here, we see a familiar reference to the <textarea>. Next, there's something new:
Example 3.4. keycodedetect.html (excerpt)
textarea.addEventListener('keyup', aKeyWasPressed, false);
This is the crucial line that sets everything up. Each element has an addEventListener method, which allows you to hook a function to any event that the element receives. (We've used the keyup event here, rather than the more commonly expected keypress, because, at the time of writing, Safari on Macintosh does not support the assigning of keypress events using addEventListener. Perhaps more importantly, the DOM3 recommendation does not mention a keypress event.) The method takes three arguments: the event, the function that should be called, and a true-or-false value for useCapture. This last item relates to a rarely-used feature of DOM events called event capture. For the moment, we'll just set it to false, to indicate that we don't want to use event capture. If you'd like to get the full story, see the DOM Level 3 Events specification (not for the faint of heart!).
The event is specified as a string, which is the (modern) name of the event (i.e. without the "on" prefix). The function is specified using only the name of the function; do not place brackets after it, as in aKeyWasPressed(), as this would call the function. We don't want to call it now; we want to call it later, when the event is fired. (If you have worked in other languages, you may recognize that this means that functions are first-class objects in JavaScript; we can pass around references to a function using its name, but without calling it. This procedure doesn't work in all languages, but it's a very useful feature of JavaScript.)
Now, when a key is pressed in our <textarea>, our aKeyWasPressed function will be called. Note that JavaScript no longer clutters up our HTML; much like the separation of design and content facilitated by CSS, we've separated our page content (HTML) from our page behavior (JavaScript). This is an important benefit of the new technique: we can switch new event listeners in and out without altering the HTML in our page. It's the modern way!
We still haven't addressed the question we posed earlier, though: how does the aKeyWasPressed function know which key was pressed?
Getting Event Information
A subtle change that we made in the above code was to give the aKeyWasPressed function an argument, e.
Example 3.5. keycodedetect.html (excerpt)
function aKeyWasPressed(e) {
...
When a function is called as an event listener, it is passed, in the case of a W3C events-compliant browser, to an event object, which holds details of the event. This object has a number of properties containing useful information, such as target, and a reference to the element that fired the event. The precise properties that are available will depend on the type of event in question, but the most useful properties are listed in Table 3.2.
Table 3.2. Useful Properties.

Click to popup a larger image of this table.
[a] Don't use charCode here, even though some Websites tell you to. keyCode has good cross-browser support, and charCode does not. Key codes in the DOM are a standards mess! There are three ways to get the code: keyCode (IE), charCode (Mozilla/Netscape) and data (the official DOM 3 Events way). Fortunately, all major browsers support the nonstandard keyCode. So always use this, at least until the data property is widespread (in about 2010!).
Code that identifies which key was pressed would look like this:
Example 3.6. keycodedetect.html (excerpt)
function aKeyWasPressed(e) {
var key = e.keyCode;
alert('You pressed the key: ' + String.fromCharCode(key));
}
var textarea = document.getElementById('myta');
textarea.addEventListener('keyup', aKeyWasPressed, false);
When a key is pressed, our function will pop up a dialog box to tell us so. (Note that we use the String.fromCharCode method to convert the keyboard code provided by keyCode to a human-readable string.)
Re-using Listeners Across Targets
The target attribute might not seem very useful; after all, we know that it will be a reference to the <textarea>. But we can hook up the same function as an event listener on more than one element. We can, for example, attach one single function as an event listener for click events to every link in our page. When any link is clicked, our function will be called; we can then tell which link was clicked by examining the function's e.target. We'll come back to this in later examples in this chapter.
For now, all we need to know is that we don't have to write a separate event listener for every single tag in which we're interested.
What Happens After an Event Fires?
Events have two further important properties: bubbling and default actions. Think about an HTML document. It's hierarchical: elements are contained by other elements. Consider this HTML snippet:
<div>
<p>
<a href="">a link</a>
</p>
</div>
Clicking on the link will cause that link to fire a click event. But the link is contained within the paragraph, and the paragraph is contained within the <div>. So clicking the link will also cause both the paragraph and the <div> to see the click event. This is called event bubbling; an event "bubbles" up through the DOM tree, starting with the target element, until it reaches the top. Not all events bubble; for example, focus and blur events do not. Bubbling can often be ignored, but there are times when you'll want to prevent a specific event from bubbling. (There are a lot of complex rules about event bubbling and event capturing, the phase of event propagation that occurs before event bubbling. In practice, we don't need to know much beyond how to stop it happening, but a complete write-up is available at for those who would like to know more of the theory underlying this aspect of the DOM.)
Once you've got an event, the DOM Events specification says that you can stop any further bubbling like this:
function aKeyWasPressed(e) {
var key = e.keyCode;
e.stopPropagation();
...
}
Once the call to topPropagation is in place, the event will occur on the <a> tag only: any listeners on the <p> or <div> tags will miss out. If there are no listeners on those other tags, there's no need to stop bubbling. In this case, the event silently passes through the parent tags, having no extra effect.
Some events have a default action. The most obvious example is clicking a link: the default action for this event is to navigate the current window or frame to the link's destination. If we wanted to handle clicks on a link entirely within our JavaScript code, we might want to prevent that default action from being taken.
In our examples so far, we have handled the keyup event, which is fired when a key is released. As it turns out, this event has no default action. A closely-related event that does have a default action is keypress, which occurs whenever a character is typed using the combination of keydown and keyup. The keypress event is nonstandard (i.e. it is not described by the W3C DOM standard), which is why I have avoided mentioning it until now, but it is well supported by the major browsers.
Let's say we want to prevent keypress events from inputting text into our textarea. We could do this by setting up an event listener that cancelled the default action of that type of event. The DOM standard specifies a method, named preventDefault, that achieves this, but again, Internet Explorer implements its own proprietary technique. Here's the DOM approach:
function aKeyWasPressed(e) {
e.preventDefault();
}
var textarea = document.getElementById('myta');
textarea.addEventListener('keypress', aKeyWasPressed, false);
Assigning Event Listeners on Page Load
In all of the examples we've seen so far in this chapter, the JavaScript code has had to follow the HTML code to which it assigns event listeners. If the JavaScript code were to come first, it would be unable to find the HTML elements in question, as they would not yet exist.
A solution to this problem is to assign event listeners for specific document elements in a listener assigned to the window's load event. As a result, event listeners will only be assigned once the document has finished loading, and all elements are available.
Here's the complete listing for our keystroke detection example, restructured in this way:
Example 3.7. keycodedetect.html
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Detect keystrokes</title>
<script type="text/javascript">
function aKeyWasPressed(e) {
var key = e.keyCode;
alert('You pressed the key: ' + String.fromCharCode(key));
}
function addListeners(e) {
var textarea = document.getElementById('myta');
textarea.addEventListener('keyup', aKeyWasPressed, false);
}
window.addEventListener('load', addListeners, false);
</script>
</head>
<body>
<form>
<textarea id="myta"></textarea>
</form>
</body>
</html>
Our main event listener, aKeyWasPressed, has not been changed. What has changed is the way in which this listener is assigned. The code that assigns it has been placed inside a new function, addListeners:
Example 3.8. keycodedetect.html (excerpt)
function addListeners(e) {
var textarea = document.getElementById('myta');
textarea.addEventListener('keyup', aKeyWasPressed, false);
}
This function is itself an event listener, which we assign to the window object's load event:
Example 3.9. keycodedetect.html (excerpt)
window.addEventListener('load', addListeners, false);
This event is fired once the document has finished loading, to signal that all HTML elements are now available. The addListeners function takes this opportunity to assign listeners to elements as required.
We'll continue to use this structure as we move forward through this chapter, and the rest of the book.
Making Events Work Cross-Browser
Naturally, making events work cross-browser is not as easy as just following the DOM standard. Internet Explorer doesn't implement the DOM Events model very well. Instead, it offers a proprietary and different way to hook up event listeners and gain access to event data.
Adding Event Listeners Portably
Instead of using an addEventListener method on an element, IE has an attachEvent method, and instead of passing an event object to each event listener, it has a global event object in window.event. This is inconvenient but not catastrophic; it just means that you have to take different actions for different browsers. In practice, what this means is that you have a small number of standard functions and techniques that you use to carry out event handling actions. One of these is the addEvent function, created by Scott Andrew:
Example 3.10. portabledetect.php (excerpt)
function addEvent(elm, evType, fn, useCapture)
// cross-browser event handling for IE5+, NS6+ and Mozilla/Gecko
// By Scott Andrew
{
if (elm.addEventListener) {
elm.addEventListener(evType, fn, useCapture);
return true;
} else if (elm.attachEvent) {
var r = elm.attachEvent('on' + evType, fn);
return r;
} else {
elm['on' + evType] = fn;
}
}
IE's attachEvent method is called, with an event name and a function to be the listener, but the event name should have "on" at the beginning. The addEvent function above takes care of the cross-browser differences (Note that if the browser doesn't support either addEventListener or attachEvent, which is the case for IE5 for Macintosh, the code assigns the event listener directly to the element as an event handler using its onevent property. This will overwrite any previous event handler that was attached to that event, which isn't good, but it's an interim solution (and better than it not working at all). There is a way around this issue, which, though it makes the code significantly more complex, does avoid this problem; details can be found in this Stylish Scripting blog post; simply include it in your code, then use it to attach events. As such, the code above becomes:
function aKeyWasPressed(e) {
var key = e.keyCode;
alert('You pressed the key: ' + String.fromCharCode(key));
}
function addListeners(e) {
var textarea = document.getElementById('myta');
addEvent(textarea, 'keyup', aKeyWasPressed, false);
}
addEvent(window, 'load', addListeners, false);
function addEvent(elm, evType, fn, useCapture)
// cross-browser event handling for IE5+, NS6+ and Mozilla/Gecko
// By Scott Andrew
{
if (elm.addEventListener) {
elm.addEventListener(evType, fn, useCapture);
return true;
} else if (elm.attachEvent) {
var r = elm.attachEvent('on' + evType, fn);
return r;
} else {
elm['on' + evType] = fn;
}
}
We're now using the addEvent function to make aKeyWasPressed listen for keyup events on the textarea.
Inspecting Event Objects Portably
This is not the only change that's required; we also have to take into account the fact that IE doesn't pass an event object to our event listener, but instead stores the event object in the window object. Just to make our lives as DHTML developers a little more complex, it also uses slightly different properties on the event object that it creates. These are shown in Table 3.3.
Table 3.3. W3C Event Object Properties.

Click to popup a larger image of this table.
[a] 0 = left button; 2 = right button; 1 = middle button.
[b] 1 = left button; 2 = right button; 4 = middle button. For combinations, add numbers: 7 means all three buttons pressed.
[c] As previously noted, the standard data property is not well supported.
Taking all this into consideration, our portable code becomes:
Example 3.11. portabledetect.html (excerpt)
function aKeyWasPressed(e) {
if (window.event) {
var key = window.event.keyCode;
} else {
var key = e.keyCode;
}
alert('You pressed the key: ' + String.fromCharCode(key));
}
function addListeners(e) {
var textarea = document.getElementById('myta');
addEvent(textarea, 'keyup', aKeyWasPressed, false);
}
addEvent(window, 'load', addListeners, false);
function addEvent(elm, evType, fn, useCapture)
// cross-browser event handling for IE5+, NS6+ and Mozilla/Gecko
// By Scott Andrew
{
if (elm.addEventListener) {
elm.addEventListener(evType, fn, useCapture);
return true;
} else if (elm.attachEvent) {
var r = elm.attachEvent('on' + evType, fn);
return r;
} else {
elm['on' + evType] = fn;
}
}
This updated version of aKeyWasPressed first checks whether a window.event object exists:
Example 3.12. portabledetect.html (excerpt)
if (window.event) {
If it does, then it and its corresponding window.event.keyCode property, are used to obtain the code of the pressed key. (This technique for checking that something exists is called feature sniffing, and will be explained in more detail in the next chapter.) If not, the event object passed to the function (as e), which also has a keyCode property, is used.
Stopping Propagation and Default Actions Portably
Halting bubbling can be done in two ways, as is the case with much event handling: via the DOM approach and the Internet Explorer approach. In DOM-compliant browsers, we can prevent an event from bubbling by calling the event object's stopPropagation method inside the event listener.
In Internet Explorer (where there is a global window.event object), we set window.event.cancelBubble to true inside the event listener. In practice, the usual technique is to use feature sniffing to Do The Right Thing:
if (window.event && window.event.cancelBubble) {
window.event.cancelBubble = true;
}
if (e && e.stopPropagation) {
// e is the event object passed to this listener
e.stopPropagation();
}
Unfortunately, even this doesn't cover all the major browsers. Arguably a worse offender even than Internet Explorer, Apple's Safari browser provides the stopPropagation method, but doesn't actually do anything when it is called. There is no easy way around this, but since event bubbling will not significantly affect any of the examples in this book, we'll just ignore this problem for now.
We also need to feature-sniff to stop default actions. With the DOM, we use the passed event object's preventDefault method; with Internet Explorer, we set the global event object's returnValue property to false.
if (window.event && window.event.returnValue) {
window.event.returnValue = false;
}
if (e && e.preventDefault) {
e.preventDefault();
}
Again, Safari appears to support preventDefault, but doesn't actually do anything when it is called. Unfortunately, preventing the default action associated with an event is a rather vital feature for many of the examples we'll look at in this book. The only way to do it in Safari (at least until Apple fixes its DOM standard event support) is to use an old-style event handler that returns false.
For example, to prevent the click event of a link from navigating to the target of the link, we would normally just use an event listener that prevented the default action of the link:
function cancelClick(e) {
if (window.event && window.event.returnValue) {
window.event.returnValue = false;
}
if (e && e.preventDefault) {
e.preventDefault();
}
}
addEvent(myLink, 'click', cancelClick, false);
To make this work in Safari, we need a second function, which will return false to cancel the event, and which we will assign as the onclick event handler of the link:
function cancelClick(e) {
if (window.event && window.event.returnValue) {
window.event.returnValue = false;
}
if (e && e.preventDefault) {
e.preventDefault();
}
}
function cancelClickSafari() {
return false;
}
addEvent(myLink, 'click', cancelClick, false);
myLink.onclick = cancelClickSafari;
This is actually quite an ugly solution, as it will overwrite any onclick event handler that another script may have installed. This kind of inter-script conflict is what modern event listeners are designed to avoid. Unfortunately, there is simply no better way around the problem in Safari. We'll see an example of this solution in practice later in this chapter.
This sort of cross-browser coding is obviated to a large extent by browser manufacturers coming together to implement the W3C DOM, but for event handling it's still required.
Smart Uses of Events
That's enough about how events work. Let's see a couple of practical examples. You should also know enough now to fully understand the image rollover code we saw in Chapter 2, The Document Object Model.
Creating Smarter Links
Some Websites open all clicked links in a new window. Often, they do this with the intention that the user will return to their site more readily if it's still open in another browser window. Some users find this useful; others find it heartily annoying. It would be possible, given our event-handling techniques above, to give them the choice.
Imagine we placed a checkbox on the page, which, initially unchecked, was accompanied by the label Open links in new window. Clicking any link will open that link in a new window if the box is checked.
We could implement this functionality using a combination of event listeners: we attach to each link on the page a click listener, which investigates the checkbox and opens the corresponding link in a new window if the box is checked. We also need a listener to run upon page load, to actually attach the listener to each link.
First, here's the HTML page we'll work on:
Example 3.13. smartlinks.html
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Smart Links</title>
<script type="text/javascript" src="smartlink.js"></script>
<style type="text/css">
form {
float: right;
width: 25em;
height: 5em;
border: 1px solid blue;
padding: 1em;
}
</style>
</head>
<body>
<h1>Smart Links</h1>
<form action=""><p>
<label for="newwin">Open links in new window?
<input type="checkbox" id="newwin">
</label>
</p></form>
<p>This page contains several links, such as
<a href="http://www.sitepoint.com/">SitePoint</a>,
<a href="http://www.yahoo.com/">Yahoo!</a>, and
<a href="http://www.google.com/">Google</a>.
These links should ordinarily open in the same window when
clicked, unless the checkbox is checked; this will make them
open in a new window.
</p>
</body>
</html>
As you can see, this page is quite simple, and contains no JavaScript except for the file that the <script> tag brings in. Figure 3.1 shows how the code displays:
Figure 3.1. The example "smart links" Web page.

Next, let's look at the content of smartlink.js. This code has been assembled from our earlier discussions, although it contains some extra code for this particular page. First, here's an outline of what the script holds:
Example 3.14. smartlink.js (excerpt)
function addEvent(elm, evType, fn, useCapture) { ... }
function handleLink(e) { ... }
function cancelClick() { ... }
function addListeners(e) { ... }
addEvent(window, 'load', addListeners, false);
And here are those four items in detail:
Example 3.15. smartlink.js
function addEvent(elm, evType, fn, useCapture) {
// cross-browser event handling for IE5+, NS6+ and Mozilla/Gecko
// By Scott Andrew
if (elm.addEventListener) {
elm.addEventListener(evType, fn, useCapture);
return true;
} else if (elm.attachEvent) {
var r = elm.attachEvent('on' + evType, fn);
return r;
} else {
elm['on' + evType] = fn;
}
}
function handleLink(e) {
var el;
if (window.event && window.event.srcElement)
el = window.event.srcElement;
if (e && e.target)
el = e.target;
if (!el)
return;
while (el.nodeName.toLowerCase() != 'a' &&
el.nodeName.toLowerCase() != 'body')
el = el.parentNode;
if (document.getElementById('newwin') &&
document.getElementById('newwin').checked) {
window.open(el.href);
if (window.event) {
window.event.cancelBubble = true;
window.event.returnValue = false;
}
if (e && e.stopPropagation && e.preventDefault) {
e.stopPropagation();
e.preventDefault();
}
}
}
function cancelClick() {
if (document.getElementById('newwin') &&
document.getElementById('newwin').checked) {
return false;
}
return true;
}
function addListeners() {
if (!document.getElementById)
return;
var all_links = document.getElementsByTagName('a');
for (var i = 0; i < all_links.length; i++) {
addEvent(all_links[i], 'click', handleLink, false);
all_links[i].onclick = cancelClick;
}
}
addEvent(window, 'load', addListeners, false);
Our code includes the now-familiar addEvent function to carry out cross-browser event hookups. We use it to call the addListeners function once the page has loaded.
The addListeners function uses another familiar technique; it iterates through all the links on the page and does something to them. In this case, it attaches the handleLink function as a click event listener for each link, so that when a link is clicked, that function will be called. It also attaches the cancelClick function as the old-style click event listener for each link—this will permit us to cancel the default action of each link in Safari.
When we click a link, that link fires a click event, and handleLink is run. The function does the following:
Example 3.16. smartlink.js (excerpt)
if (window.event && window.event.srcElement)
el = window.event.srcElement;
if (e && e.target)
el = e.target;
if (!el)
return;
This is the cross-browser approach to identifying which link was clicked; we check for a window.event object and, if it exists, use it to get window.event.srcElement, the clicked link. Alternatively, if e, the passed-in parameter, exists, and e.target exists, then we use that as the clicked link. If we've checked for both e and e.target, but neither exists, we give up and exit the function (with return).
Next up, we want to make sure that we have a reference to our link element:
Example 3.17. smartlink.js (excerpt)
while (el.nodeName.toLowerCase() != 'a' &&
el.nodeName.toLowerCase() != 'body')
el = el.parentNode;
if (el.nodeName.toLowerCase() == 'body')
return;
Some browsers may pass the text node inside a link as the clicked-on node, instead of the link itself. If the clicked element is not an <a> tag, we ascend the DOM tree, getting its parent (and that node's parent, and so on) until we get to the a element. (We also check for body, to prevent an infinite loop; if we get as far up the tree as the document body, we give up.)
Note that we also use toLowerCase on the nodeName of the element. This is the easiest way to ensure that a browser that returns a nodeName of A, and one that returns a nodeName of a, will both be handled correctly by the function.
Next, we check our checkbox:
Example 3.18. smartlink.js (excerpt)
if (document.getElementById('newwin') &&
document.getElementById('newwin').checked) {
We first confirm (for paranoia's sake) that there is an element with id newwin (which is the checkbox). Then, if that checkbox is checked, we open the link in a new window:
Example 3.19. smartlink.js (excerpt)
window.open(el.href);
We know that el, the clicked link, is a link object, and that link objects have an href property. The window.open method creates a new window and navigates it to the specified URL.
Finally, we take care of what happens afterward:
Example 3.20. smartlink.js (excerpt)
if (window.event) {
window.event.cancelBubble = true;
window.event.returnValue = false;
}
if (e && e.stopPropagation && e.preventDefault) {
e.stopPropagation();
e.preventDefault();
}
}
We don't want the link to have its normal effect of navigating the current window to the link's destination. So, in a cross-browser fashion, we stop the link's normal action from taking place.
As previously mentioned, Safari doesn't support the standard method of cancelling the link's default action, so we have an old-style event listener, cancelClick, that will cancel the event in that browser:
Example 3.21. smartlink.js (excerpt)
function cancelClick() {
if (document.getElementById('newwin') &&
document.getElementById('newwin').checked) {
return false;
}
return true;
}
You can see that some of this code is likely to appear in every project we attempt, particularly those parts that have to do with listener installation.
Making Tables More Readable
A handy trick that many applications use to display tables of data is to highlight the individual row and column that the viewer is looking at; paper-based tables often shade table rows and columns alternately to provide a similar (although non-dynamic(…until paper technology gets a lot cooler than it is now, at any rate!) effect.
Here's a screenshot of this effect in action. Note the location of the cursor. If we had another cursor, you could see that the second table is highlighted differently. But we don't, so you'll just have to try the example code for yourself...
Figure 3.2. Example of table highlighting in a Web page.

We can apply this effect to tables in an HTML document using event listeners. We'll attach a mouseover listener to each cell in a table, and have that listener highlight all the other cells located in that cell's row and column. We'll also attach a mouseout listener that turns the highlight off again.
The techniques we have explored in this chapter are at their most powerful when we combine the dynamic capabilities of DHTML with the page styling of CSS. Instead of specifically applying a highlight to each cell we wish to illuminate, we'll just apply a new class, hi, to those cells; our CSS will define exactly how table cells with class hi should be displayed. To change the highlight, simply change the CSS. For a more powerful effect still, use CSS's selectors to apply different styles to highlighted cells depending on the table in which they appear.
Here's an example page that contains tables:
Example 3.22. tableHighlight.html
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Highlighted Tables</title>
<script type="text/javascript" src="tableHighlight.js">
</script>
<style type="text/css">
tr.hi td, td.hi {
background-color: #ccc;
}
table.extra tr.hi td, table.extra td.hi {
color: red;
text-decoration: underline overline;
background-color: transparent;
}
</style>
</head>
<body>
<h1>Highlighted Tables</h1>
<h2>A table with highlighting</h2>
<table>
<tr>
<td></td>
<td>Column 1</td>
<td>Column 2</td>
<td>Column 3</td>
<td>Column 4</td>
</tr>
<tr>
<td>Row 1</td>
<td>1,1</td><td>1,2</td><td>1,3</td><td>1,4</td>
</tr>
<tr>
<td>Row 2</td>
<td>2,1</td><td>2,2</td><td>2,3</td><td>2,4</td>
</tr>
<tr>
<td>Row 3</td>
<td>3,1</td><td>3,2</td><td>3,3</td><td>3,4</td>
</tr>
<tr>
<td>Row 4</td>
<td>4,1</td><td>4,2</td><td>4,3</td><td>4,4</td>
</tr>
</table>
<h2>A table with different highlighting</h2>
<table class="extra">
<tr>
<td></td>
<td>Column 1</td>
<td>Column 2</td>
<td>Column 3</td>
<td>Column 4</td>
</tr>
<tr>
<td>Row 1</td>
<td>1,1</td><td>1,2</td><td>1,3</td><td>1,4</td>
</tr>
<tr>
<td>Row 2</td>
<td>2,1</td><td>2,2</td><td>2,3</td><td>2,4</td>
</tr>
<tr>
<td>Row 3</td>
<td>3,1</td><td>3,2</td><td>3,3</td><td>3,4</td>
</tr>
<tr>
<td>Row 4</td>
<td>4,1</td><td>4,2</td><td>4,3</td><td>4,4</td>
</tr>
</table>
</body>
</html>
That code creates two four-by-four tables, each with column and row headings (so each table contains five rows and five columns in total). Notice that none of the styles have any effect because, as yet, there are no elements with class="hi".
Let's look at the matching tableHighlight.js script. Its structure reflects our earlier discussions, but it contains some additional code for this particular technique. Here's an outline of the script:
Example 3.23. tableHighlight.js (excerpt)
function addEvent(elm, evType, fn, useCapture) { ... }
function ascendDOM(e, target) { ... }
function hi_cell(e) { ... }
function lo_cell(e) { ... }
function addListeners() { ... }
addEvent(window, 'load', addListeners, false);
Notice how similar the function outline is to the smart links example. Here are the six items in all their detail.
Example 3.24. tableHighlight.js
function addEvent(elm, evType, fn, useCapture)
// cross-browser event handling for IE5+, NS6+ and Mozilla/Gecko
// By Scott Andrew
{
if (elm.addEventListener) {
elm.addEventListener(evType, fn, useCapture);
return true;
} else if (elm.attachEvent) {
var r = elm.attachEvent('on' + evType, fn);
return r;
} else {
elm['on' + evType] = fn;
}
}
// climb up the tree to the supplied tag.
function ascendDOM(e, target) {
while (e.nodeName.toLowerCase() != target &&
e.nodeName.toLowerCase() != 'html')
e = e.parentNode;
return (e.nodeName.toLowerCase() == 'html') ? null : e;
}
// turn on highlighting
function hi_cell(e) {
var el;
if (window.event && window.event.srcElement)
el = window.event.srcElement;
if (e && e.target)
el = e.target;
if (!el) return;
el = ascendDOM(el, 'td');
if (el == null) return;
var parent_row = ascendDOM(el, 'tr');
if (parent_row == null) return;
var parent_table = ascendDOM(parent_row, 'table');
if (parent_table == null) return;
// row styling
parent_row.className += ' hi';
// column styling
var ci = -1;
for (var i = 0; i < parent_row.cells.length; i++) {
if (el === parent_row.cells[i]) {
ci = i;
}
}
if (ci == -1) return; // this should never happen
for (var i = 0; i < parent_table.rows.length; i++) {
var cell = parent_table.rows[i].cells[ci];
cell.className += ' hi';
}
}
// turn off highlighting
function lo_cell(e) {
var el;
if (window.event && window.event.srcElement)
el = window.event.srcElement;
if (e && e.target)
el = e.target;
if (!el) return;
el = ascendDOM(el, 'td');
if (el == null) return;
var parent_row = ascendDOM(el, 'tr');
if (parent_row == null) return;
var parent_table = ascendDOM(parent_row, 'table');
if (parent_table == null) return;
// row de-styling
parent_row.className =
parent_row.className.replace(/\b ?hi\b/, '');
// column de-styling
var ci = el.cellIndex;
for (var i = 0; i < parent_table.rows.length; i++) {
var cell = parent_table.rows[i].cells[ci];
cell.className = cell.className.replace(/\b ?hi\b/, '');
}
}
function addListeners() {
if (!document.getElementsByTagName) return;
var all_cells = document.getElementsByTagName('td');
for (var i = 0; i < all_cells.length; i++) {
addEvent(all_cells[i], 'mouseover', hi_cell, false);
addEvent(all_cells[i], 'mouseout', lo_cell, false);
}
}
addEvent(window, 'load', addListeners, false);
We add our mouseover and mouseout event listeners using the standard approach. The addListeners function sets up our hi_cell and lo_cell functions as mouseover and mouseout event listeners, respectively.
To minimize duplicate code, we've added a handy little utility function called ascendDOM. This marches up the tree from the element supplied in the first argument to find the first enclosing tag whose name matches the second argument.
Processing happens as follows. Mousing over a table cell triggers the hi_cell function. This finds the moused-over cell, then calculates the row and the table in which that cell appears. The ascendDOM function is called quite often in the code, so you can see the benefit of putting that code into a function. In hi_cell, the lines that actually do the styling work are these:
Example 3.25. tableHighlight.js (excerpt)
parent_row.className += ' hi';
Example 3.26. tableHighlight.js (excerpt)
cell.className += ' hi';
The rest of the code is simply concerned with picking out the right elements for these lines to work on.
Our intention here is to apply the class hi to the other cells in the row that contains the moused-over cell, and its column. The first line above executes the first task. The second line applies the class to a given cell, but our script needs to find the appropriate cells first.
This is where things get a little complicated. The row is a simple <tr> tag, whereas the column is a list of cells scattered across all the rows in the table. According to the DOM Level 2 specification, table cell elements have a cellIndex property, which indicates the cell's index in the row. To find the other cells in this column, we could iterate through all the rows in the table and find within each row the cell that has the same cellIndex.
Sadly, Safari doesn't properly support cellIndex—it is always set to 0, no matter what the actual index should be. If Safari supported cellIndex, the process could have been simple:
var ci = el.cellIndex;
In fact, this concise snippet must be replaced with the much longer section below:
Example 3.27. tableHighlight.js (excerpt)
var ci = -1;
for (var i = 0; i < parent_row.cells.length; i++) {
if (el === parent_row.cells[i]) {
ci = i;
}
}
if (ci == -1) return; // this should never happen
ci is the cellIndex, and can be used to highlight other cells with the same cellIndex in the other rows in the table:
Example 3.28. tableHighlight.js (excerpt)
for (var i = 0; i < parent_table.rows.length; i++) {
var cell = parent_table.rows[i].cells[ci];
cell.className += ' hi';
}
All the table's rows are held in the table's rows array. We walk through that array, applying the hi class to the cell in each row that has the same index as the moused-over cell.
The upshot of this exercise is that all the cells in the same column as the moused-over cell will have class hi; the table row containing the cell will also have class hi.
Our CSS code takes care of the appearance of these cells:
Example 3.29. tableHighlight.html (excerpt)
tr.hi td, td.hi {
background-color: #ccc;
}
We've applied a background color of class hi to both tds, and tds in a tr of class hi; thus, these cells will be highlighted. The lo_cell function works similarly, except that it removes the class hi from the row and column rather than applying it. The removal is done with the following lines:
Example 3.30. tableHighlight.js (excerpt)
parent_row.className =
parent_row.className.replace(/\b ?hi\b/, '');
Example 3.31. tableHighlight.js (excerpt)
cell.className = cell.className.replace(/\b ?hi\b/, '');
Since a className is a string, it has all the methods of a string, one of which is replace; we can call the replace method with a regular expression (first parameter) and a substitute string (second parameter). If a match for the regular expression is found in the string, it is replaced by the substitute string. In our example, we look for matches to the expression \b ?hi\b (note that regular expressions are delimited by slashes, not quotes)—that is, a word boundary followed by an optional space, the word 'hi', and another word boundary—and replace it with a blank string, thus removing it from the className.
An added bonus of using CSS to provide the style information is that we can apply different highlighting to different tables on the page without changing the script. For example, the HTML of the page contains two tables, one with a class of extra. We apply some CSS specifically to tables with class extra:
Example 3.32. tableHighlight.html (excerpt)
table.extra tr.hi td, table.extra td.hi {
color: red;
text-decoration: underline overline;
background-color: transparent;
}
As a result, the highlighted cells in that particular table will be highlighted differently. CSS makes achieving this kind of effect very easy.
Summary
Understanding the processes by which events are fired, and by which code is hooked to those events, is vital to DHTML programming. Almost everything you do in DHTML will involve attaching code to events, as described in this chapter. We've examined some common events and the two browser models for listening to them. We have also covered what happens when an event fires, and how you can interrupt or alter that process. Finally, we looked at a few events in detail, and saw some simple examples of how code can attach to those events and improve the user experience on sites that employ these techniques.