Chapter 9. Listening for Events

Twenty-five hundred years ago in 490 BC, a Greek herald named Pheidippides ran several dispatches totaling 240 km (150 miles) during the final two days of the Battle of Marathon. The distance of his final run from Marathon to Athens to deliver news of the Greek victory over the Persians with the word Νενικηκαμεν (we have won) is the basis for the marathon race being 42 km (26.2 miles). Insofar as Pheidippides ran those dispatches over mountainous terrain in hot weather and would have been 40 (born in 530 BC), it is not surprising that he collapsed and died moments later.

Nowadays, dying from running a marathon is rare, but developing rigor mortis, referred to as hitting the wall, is not. Typically this happens in the final fourth of the marathon when, because of glycogen depletion, muscle fibers lock up, effectively turning a runner into a staggering corpse. So to run a fast marathon time, you must not hit the wall. During the race, this more or less comes down to making good decisions about how to respond to events as they unfold—stuff like whether to speed up or slow down relative to the display on a split clock or whether to follow a surging runner or remain in the chase pack.

In the same way, you have to be smart about how JavaScript behaviors respond to events, with things such as visitors moving their mouse or the page loading in order to prevent the browser from developing rigor mortis. Why would JavaScript freeze the browser anyway? For one, whenever some JavaScript code is running, the browser cannot do any repaints, reflows, or any other UI updates. So, a button clicked while JavaScript is executing may never look like it was clicked. For another, while a JavaScript file is downloading, a browser cannot download any other kind of file. Therefore, CSS and image downloads are temporarily blocked, both while a JavaScript file downloads and executes, typically causing a blank white page.

But not to worry—we will explore not only how to respond to events but also how to be quick about it. Insofar as JavaScript responds to events by running functions, referred to as event listeners, this typically means coding snappy functions. Some techniques for doing so, such as optimizing loops, are already in your bailiwick. Others, such as advance conditional loading, are new but well within your grasp.

Working with the Event Object

A couple of notes before start coding: first, to respond to an event, you have to tell JavaScript to listen for it as it traverses the DOM tree. In Internet Explorer 9, Firefox, Safari, Chrome, and Opera, you can tell JavaScript to listen while an event either descends (capturing phase) or ascends (bubbling phase) the DOM tree. In Internet Explorer 8 or earlier, JavaScript can listen only during the bubbling phase.

How do you know the who, what, when, where, and how of an event? Those details are provided by the members of an event object that Internet Explorer 9, Firefox, Safari, Chrome, and Opera pass as the sole parameter to an event listener function. Internet Explorer 8 or earlier, on the other hand, saves its event object to the global variable, event, that is, to window.event. As you might imagine, window.event is constantly being overwritten by Internet Explorer. However, even though this is a bit of a kludge, it works for the reason that no two events ever take place at the same moment in time. Note that INTERNET EXPLORER 9 and Opera save an event object to window.event in addition to passing it to an event listener function.

Now then, to tell JavaScript to listen for an event in Internet Explorer 9, Firefox, Safari, Chrome, and Opera, you invoke the DOM method addEventListener() on an Element or Document node. addEventListener() works with three parameters:

  • The first one is the name of the event to listen for. This can be a string literal or expression, such as a variable.

  • The second parameter is an event listener function for JavaScript to call when the event from the first parameter takes place on the node or one of its descendents. It is in this event listener function that we deal with the event that has happened. This parameter may be either a function literal or an expression, such as an identifier naming a function. So, it consists of the same two options, literal or expression, as with the first parameter.

  • The third parameter is simpler. It's just a boolean, false or true. false means call the event listener function during the bubbling phase. On the other hand, true means call the event listener function during the capturing phase. Because Internet Explorer does not implement capturing, you will nearly always pass false for the third parameter.

Internet Explorer does not implement addEventListener() or any other feature from the DOM events module. But it does have a proprietary way to tell JavaScript to listen for events (just during the bubbling phase, however). In Internet Explorer, Element and Document nodes have a proprietary method named attachEvent() that works with two parameters:

  • The first one is the name of the event to listen for as a string literal or expression. Note that this differs from the first parameter to addEventListener() in that you must prefix event names with on—for example, onclick instead of click.

  • The second parameter is the same as for addEventListener()—the event listener function as a literal or expression, such as an identifier naming a function. Because Internet Explorer can listen for events only during the bubbling phase, attachEvent() does not take a boolean indicating whether to listen during capturing or bubbling. So, there's just two parameters.

Downloading Project Files

It's time to code some snappy event listeners. However, doing so in Firebug is a bit impractical. We're going to do so with whatever plain-text editor you code your XHTML and CSS with. The only difference is that you save a JavaScript file with a .js extension rather than an .html or .css extension. With this in mind, create a plain-text file named nine.js. Then download the supporting XHTML, CSS, and image files from www.apress.com. Put the images in a subfolder (named images) of the one you put the XHTML, CSS, and JavaScript files in.

Let's take a peek at what we have in there. In nine.html, we have the following structural markup. Note that we link in nine.js just before the closing tag for the <body> element. Remember why from Chapter 1? Uh-huh, it's to prevent an initially blank page.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>JavaScript for Absolute Beginners</title>
<link rel="stylesheet" type="text/css" href="nine.css" />
<link rel="stylesheet" type="text/css" href="blue.css" id="skin" />
</head>
<body>
<div id="running">
  <h4 class="drag">Running</h4>
  <ul>
    <li><a class="sprite" id="adidas" href="http://www.adidas.com">adidas</a></li>
    <li><a class="sprite" id="asics" href="http://www.asics.com">ASICS</a></li>
    <li><a class="sprite" id="brooks" href="http://www.brooksrunning.com">Brooks</a></li>
    <li><a class="sprite" id="newBalance" href="http://www.newbalance.com">New
Balance</a></li>
    <li><a class="sprite" id="nike" href="http://www.nike.com">Nike</a></li>
    <li><a class="sprite" id="saucony" href="http://www.saucony.com">Saucony</a></li>
  </ul>
</div>
<script src="nine.js" type="text/javascript"></script>
</body>
</html>

Now the lion's share of the CSS presentation is in nine.css. It's pretty straightforward, as you can see:

* {
  margin:0;
  padding:0;
  border:0;
}
body {
  background:rgb(255,255,255);
  color:rgb(0,0,0);
  font:11px Verdana, Arial, Helvetica, sans-serif;
}
div#running {
  position:absolute;
  left:40px;
  top:40px;
  width:120px;
  height:243px;
  background:url(images/container.gif) 0 0 no-repeat;
}
div#running h4 {
  position:absolute;
  left:0px;
  top:0px;
  width:63px;
  height:25px;
  text-indent:−9999px;
  text-decoration:none;
  overflow:hidden;
}
div#running li {
  display:inline;
}
div#running li a {
  position:absolute;
  left:10px;
  width:100px;
  height:28px;
  color:rgb(0,0,0);
  text-indent:−9999px;
  text-decoration:none;
  overflow:hidden;
}
a#adidas {
  top:30px;
  background-position:0 0;
}
a#asics {
  top:65px;
  background-position:0 −27px;
}
a#brooks {
  top:100px;
  background-position:0 −54px;
}
a#newBalance {
  top:135px;
  background-position:0 −81px;
}
a#nike {
  top:170px;
  background-position:0 −108px;
}
a#saucony {
  top:205px;
  background-position:0 −135px;
}

Styles just for the blue, fuchsia, and green skins, which we will write a behavior to swap by key, are in the aptly named files blue.css, fuchsia.css, and green.css. Those are pretty simple for our project—just one rule each. For a full-blown web app, there would of course be many more. So in blue.css, we have this:

.sprite {
  background-image:url(images/blue.gif);
}

Then in fuchsia.css, just the name of the GIF differs:

.sprite {
  background-image:url(images/fuchsia.gif);
}

The same goes for green.css:

.sprite {
  background-image:url(images/green.gif);
}

Then there are the blue, fuchsia, and green sprites. Those go in the images subfolder of the one you put the XHTML, CSS, and JavaScript files in. Because this is a black-and-white book, I'm just going to reproduce the blue sprite in Figure 9-1.

An example sprite from this chapter's application

Figure 9-1. An example sprite from this chapter's application

Regardless of whether the skin is blue, fuchsia, or green, the image for the tabbed container, shown in Figure 9-2, will be the same: container.gif. Note that we will write a behavior so that we can grab the menu by the Running tab and then drag and drop it elsewhere on the page.

Download tabbed container that all three skins share.

Figure 9-2. Download tabbed container that all three skins share.

With those supporting project files in tow, let's begin by coding four helper functions to make working with events a little bit easier.

Advance Conditional Loading

The DOM and Internet Explorer methods for telling JavaScript to listen for events are similar enough that we can replace those with a helper function. In our nine.js JavaScript file, let's create a variable named addListener and then assign one of two function literals to it with the ?: conditional operator (one for Internet Explorer and one for all the other browsers). Remember from earlier in the book that JavaScript converts the first operand of the ?: operator to a boolean and that a function converts to true. So if the first operand to ?: is document.addEventListener, JavaScript converts that to true in Internet Explorer 9, Firefox, Safari, Chrome, and Opera but to false in Internet Explorer 8 or earlier, which do not implement document.addEventListener. As a result, JavaScript assigns the second operand for ?: to addListener for Internet Explorer 9, Firefox, Safari, Chrome, and Opera, but the third operand for Internet Explorer 8 or earlier, where these operands are function literals.

Since we are doing the feature testing with the ?: conditional operator prior to defining an appropriate function, this technique, which we covered in Chapter 6, is referred to as advance conditional loading. This is much snappier than running the feature test over and over every time the function is called.

Thus far, we have the following skeleton. Note that to prevent errors deriving from JavaScript's automatic semicolon insertion feature, we break lines after the ? and : tokens of the ?: operator. So, the = assignment statement is currently spread over five lines.

var addListener = (document.addEventListener) ?
  function() {
  } :
  function() {
  } ;

Let's define four parameters for the function literal intended for Internet Explorer 9, Firefox, Safari, Chrome, and Opera:

  • First, node will refer to an Element or Document node from the DOM tree.

  • In turn, type is the name of the event node should listen for.

  • listener will refer to the event listener function to run whenever the event in type occurs on node or one of its descendents.

  • phase will contain true for capturing and false or undefined for bubbling. That is to say, for bubbling, there is no need to explicitly pass false since undefined, the default value for a parameter, will do the same thing.

Now we have this:

var addListener = (document.addEventListener) ?
  function(node, type, listener, phase) {
  } :
  function() {
  } ;

Having defined the parameters for the DOM-savvy function, let's move on to the block. Just one statement in there; invoke addEventListener() as a method of the node parameter, passing in type and listener the way we found them. On the other hand, let's convert phase to a boolean with the !! idiom. Doing so converts undefined to false, which is why phase is optional. So, we're done with the first function literal:

var addListener = (document.addEventListener) ?
  function(node, type, listener, phase) {
node.addEventListener(type, listener, !! phase);
  } :
  function() {
  } ;

Note

Both the !! idiom and ! operator were covered in Chapter 3.

Now for the Internet Explorer 8 or earlier function literal. It's just three parameters, though. Internet Explorer 8 or earlier just implements bubbling, so there's no need to define a phase parameter. It's just node, type, and listener:

var addListener = (document.addEventListener) ?
  function(node, type, listener, phase) {
    node.addEventListener(type, listener, !! phase);
  } :
  function(node, type, listener) {
  } ;

Now in the block, invoke attachEvent() on the Element or Document node in the node parameter. Prefix the name of the event in type with on, but pass listener as is. Here it is. Note that Internet Explorer 9 and Opera implement both addEventListener() and attachEvent().

var addListener = (document.addEventListener) ?
  function(node, type, listener, phase) {
    node.addEventListener(type, listener, !! phase);
  } :
  function(node, type, listener) {
    node.attachEvent("on" + type, listener);
  } ;

Telling JavaScript to Stop Listening for an Event

Now and then you will want to tell JavaScript to stop listening for an event. In Internet Explorer 9, Firefox, Safari, Chrome, and Opera, you do so with removeEventListener(). In Internet Explorer 8 or earlier, you do so with detachEvent(). Note that Internet Explorer 9 and Opera implement both removeEventListener() and detachEvent().

Insofar as those both delete a previously added event listener, you have to pass them the same parameters you added the event listener with. Therefore, we can tweak addListener() to create a helper function to delete event listeners with.

Just cut and paste addListener, renaming the copy removeListener. Next, change addEventListener to removeEventListener in the boolean expression and first function literal. Finally, change attachEvent to detachEvent in the second function literal. Those four edits are in bold here:

var removeListener = (document.removeEventListener) ?
  function(node, type, listener, phase) {
    node.removeEventListener(type, listener, !! phase);
  } :
  function(node, type, listener) {
    node.detachEvent("on" + type, listener);
  } ;

removeListener() will come in handy when we code our drag-and-drop behavior.

Preventing Default Actions from Taking Place

Some event types have a default action associated with them. For example, if a click event occurs on an <a> element, JavaScript loads a new URL. Oftentimes you will want to cancel this default action. Say you want to load the new content with Ajax rather than by loading a new URL. To do so for Internet Explorer 9, Firefox, Safari, Chrome, and Opera, you would call preventDefault() on the event object for the click. For IE 8 or earlier, you would assign false to returnValue. Note that Internet Explorer 9 and Opera implement both preventDefault() and returnValue.

So, depending on the browser, we want to either call a method or write a member of an event object. Let's write a helper function named thwart() to do the job. Preventing a default action is not typically something JavaScript has to do right away as a page loads, so let's define thwart() by the lazy loading technique, which we explored in Chapter 6.

Now the only value thwart() needs to do its job is an event object. Traditionally, a parameter for an event object is named e, so let's not rock the boat:

function thwart(e) {
}

That's fine and dandy. Now let's write our path for Internet Explorer 9, Firefox, Safari, Chrome, and Opera with an if statement. In the event that referring to e.preventDefault does not return undefined, we will overwrite thwart with a function literal that calls preventDefault() on the event object:

function thwart(e) {
  if (e.preventDefault) {
    thwart = function(e) {
      e.preventDefault();
    };
  }
}

Referring to e.preventDefault in Internet Explorer 8 or earlier will return undefined, so those Internet Explorer versions will follow the else path. Yup, I know we don't have one yet. Let's fix that by overwriting thwart with a function literal that changes returnValue to false from true, its default value.

function thwart(e) {
  if (e.preventDefault) {
    thwart = function(e) {
      e.preventDefault();
    };
  } else {
    thwart = function(e) {
      e.returnValue = false;
    };
  }
}

So far, so good. The only problem with this is that the first time we call thwart(), it overwrites itself without canceling the default action (because we assign a new function to the thwart identifier but don't actually call the new function). Good grief!

To fix this, we need for the initial version of thwart() to call the new version of thwart(), passing the event object in:

function thwart(e) {
  if (e.preventDefault) {
    thwart = function(e) {
      e.preventDefault();
    };
  } else {
    thwart = function(e) {
      e.returnValue = false;
    };
  }
  thwart(e);
}

The first time thwart() runs, it does a lot of work. But thereafter, thwart() runs snappily since its block contains but one statement. In Internet Explorer 9, Firefox, Safari, Chrome, and Opera, that would be e.preventDefault(), and in Internet Exploreror earlier that would be e.returnValue = false.

Preventing an Event from Traversing the DOM Tree

Now then, in Internet Explorer 9, Firefox, Safari, Chrome, and Opera, an event object descends the DOM tree to the element an event happened on. Then turns tail and ascends the DOM tree. So, the event object passes by any ancestor of the target of the event two times. On the other hand, in Internet Explorer 8 or earlier, the event object passes by any ancestor one time, that is, while bubbling upward through the DOM tree. Therefore, if you register event listeners for a certain kind of event, say a click, on nodes that are on different tiers of the DOM tree, all of those could potentially run when a click event occurs.

There ought to be a way to prevent an event object from traversing any further through the DOM treeto avoid triggering additional event listeners, right? And there is. Just as with preventing default actions, you do so by calling a method for Internet Explorer 9, Firefox, Safari, Chrome, and Opera and by writing a member for Internet Explorer 8 or earlier. The DOM method is named stopPropagation(), and the Internet Explorer 8 or earlier member is named cancelBubble. Note that Internet Explorer 9 and Opera implement both stopPropagation() and cancelBubble. With this in mind, we can write a lazy loader to do the job by changing some identifiers in thwart().

Now cut and paste thwart(). Then change the identifier thwart to burst in four places:

function burst(e) {
  if (e.preventDefault) {
    burst = function(e) {
      e.preventDefault();
    };
  } else {
    burst = function(e) {
      e.returnValue = false;
    };
  }
  burst(e);
}

Next, change the identifier for the DOM method from preventDefault to stopPropagation in two places:

function burst(e) {
  if (e.stopPropagation) {
burst = function(e) {
      e.stopPropagation();
    };
  } else {
    burst = function(e) {
      e.returnValue = false;
    };
  }
  burst(e);
}

Now change the identifier for the proprietary Internet Explorer member from returnValue to cancelBubble. However, cancelBubble has to be set to true to prevent an event object from bubbling further. Change false to true, and we're done:

function burst(e) {
  if (e.stopPropagation) {
    burst = function(e) {
      e.stopPropagation();
    };
  } else {
    burst = function(e) {
      e.cancelBubble = true;
    };
  }
  burst(e);
}

We're done coding helper functions for working with events. This is the final code for them:

var addListener = (document.addEventListener) ?
  function(node, type, listener, phase) {
    node.addEventListener(type, listener, !! phase);
  } :
  function(node, type, listener) {
    node.attachEvent("on" + type, listener);
  } ;
var removeListener = (document.removeEventListener) ?
  function(node, type, listener, phase) {
    node.removeEventListener(type, listener, !! phase);
  } :
  function(node, type, listener) {
    node.detachEvent("on" + type, listener);
  } ;
function thwart(e) {
  if (e.preventDefault) {
    thwart = function(e) {
      e.preventDefault();
    };
  } else {
    thwart = function(e) {
      e.returnValue = false;
    };
  }
  thwart(e);
}
function burst(e) {
  if (e.stopPropagation) {
    burst = function(e) {
      e.stopPropagation();
    };
  } else {
    burst = function(e) {
      e.cancelBubble = true;
    };
  }
  burst(e);
}

Writing Helper Functions

Now that you have seen advance conditional loading, let's rework a few helper functions from the past two chapters with it. This will improve their performance immensely and make sure we're using the best technique in each case.

Crawling the DOM Tree

In Chapter 7, we wrote a helper function named traverseTree() to crawl the DOM tree, which we'll have to do again in this chapter. The code for traverseTree() appears here:

function traverseTree(node, func) {
  func(node);
  node = node.firstChild;
  while (node !== null) {
    arguments.callee(node, func);
    node = node.nextSibling;
  }
}

Coding traverseTree() to crawl the DOM by way of firstChild and nextSibling rather than iterating over childNodes is more than 100 times faster in Internet Explorer. So, our take on traverseTree() is already optimized relative tothat Internet Explorer bug. Note that Firefox, Safari, and Opera crawl the DOM just as fast by iterating over childNodes.

However, in Internet Explorer 9, Firefox, Safari, Chrome, and Opera, traverseTree() has to crawl through Text nodes representing formatting whitespace in our XHTML markup. So if we could eliminate that ridiculous bit of work, traverseTree() would be much snappier in Internet Explorer 9, Firefox, Safari, Chrome, and Opera.

DOM 3 defines an ElementTraversal interface that enables us to do just that. ElementTraversal provides the following members that we can use in place of firstChild, lastChild, previousSibling, nextSibling, and childNodes.length:

firstElementChild
lastElementChild
previousElementSibling
nextElementSibling
childElementCount

As its name implies, ElementTraversal is designed for traversing Element nodes. So, firstElementChild, lastElementChild, previousElementSibling, and nextElementSibling will contain an Element node or null (and never a Text, Comment, or any other kind of node). Note that childElementCount differs from childNodes.length in that it contains just the number of child Element nodes rather than the overall number of child nodes.

Therefore, for Internet Explorer 9, Firefox, Safari, Chrome, and Opera, we can rework traverseTree() with firstElementChild and nextElementSibling to replace firstChild and nextSibling. The two function literals in the ?: expression differ only by those identifiers:

var traverseTree = document.documentElement.firstElementChild ?
  function traverseTree (node, func) {
    func(node);
    node = node.firstElementChild;
    while (node !== null) {
      traverseTree(node, func);
      node = node.nextElementSibling;
    }
  } :
  function traverseTree (node, func) {
    func(node);
    node = node.firstChild;
    while (node !== null) {
      traverseTree(node, func);
      node = node.nextSibling;
    }
  } ;

Note that the boolean expression, document.documentElement.firstElementChild, queries firstElementChild for the <html>Element node. That would be the <head>Element node.

Why not simply query the firstElementChild of the Document node? We could see whether document.firstElementChild refers to the <html>Element node. That's a bad idea: ElementTraversal is implemented only by Element nodes. So although all 12 node types have a firstChild member, only an Element node has a firstElementChild member, too.

Hmm. If it were up to me, a Document node would implement ElementTraversal, too. But I do not write the standards, just about them.

Note

If you want to wade through the details of the ElementTraversal interface, visit

www.w3.org/TR/ElementTraversal/.

Finding an Element by Class

The next helper function we want to rework is findClass(), which we wrote in Chapter 7 to help us find an Element node by its class attribute. DOM 3 adds a NodeSelector interface that defines two methods, querySelectorAll() and querySelector(). Those provide a way to query Element nodes with CSS selectors. That is to say, querySelectorAll() and querySelector() enable you to query elements in the same way that you would target them in a CSS rule. So for example, to query the <h4> element in our markup, we could invoke querySelectorAll() in any of the following ways in Firebug:

document.querySelectorAll("h4.drag")[0];
// <h4 class="drag">
document.querySelectorAll("div#running h4")[0];
// <h4 class="drag">
document.getElementById("running").querySelectorAll("h4")[0];
// <h4 class="drag">

As the previous samples illustrate, NodeSelector is implemented by both Document and Element nodes. So you may query the whole DOM tree or just a branch, just like with getElementsByTagName().

Note

DocumentFragment nodes also implement NodeSelector.

Like getElementsByTagName(), querySelectorAll() returns a NodeList. But unlike getElementsByTagName(), querySelectorAll() does not return a live DOM query, just a copy of the matching Element nodes. This is a terrific feature as far as script speed is concerned. It's sort of like optimizing some code by copying the Element nodes returned by getElementsByTagName() into an array so that you can work solely in ECMAScript.

By the way, querySelector() returns the first Element node in the DOM tree matching a CSS selector. Since this is totally dependent on XHTML content, which is always being updated, I'd discourage you from using querySelector(). If the element you want is no longer the first one in source code order months down the road, your script won't work.

Just after the var statement for traverseTree, write one for findClass. The first operand to the ?: operator will be document.querySelectorAll, which will convert to true in Internet Explorer 8, Firefox 3.5, Safari 3.1, Chrome 4, and Opera 10. So, those versions or later will assign the first function literal to findClass. This one takes two parameters:

  • name is the name of the class to find.

  • root is where we begin descending the DOM tree from. root may be a Document or Element node. However, root is optional. In the event that root is undefined, we will simply assign document.documentElement to the parameter, which is the <html> element.

var findClass = document.querySelectorAll ?
  function (name, root) {
    root = root || document.documentElement;
  } :
  function() {
  } ;

Next, call querySelectorAll() on the root parameter. Insofar as querySelectorAll() works with a CSS selector, we need to prefix a . on the class string in name. Let's do so and then return the value of calling querySelectorAll():

var findClass = document.querySelectorAll ?
  function (name, root) {
    root = root || document.documentElement;
    return root.querySelectorAll("." + name);
  } :
  function() {
  } ;

The second function literal, the one for older browsers, is just the findClass() function we wrote in Chapter 7 but as a function literal. Remember that function literals are values, so those can be an operand to an operator like ?:. Our completed advance conditional loader for findClass looks as follows. Just be sure to remember the ? and : tokens and the terminating semicolon.

var findClass = document.querySelectorAll ?
  function (name, root) {
    root = root || document.documentElement;
    return root.querySelectorAll("." + name);
  } :
  function (name, root) {
    var found = [];
    root = root || document.documentElement;
    traverseTree(root, function(node) {
      if (!! node.className) {
        for (var names = node.className.split(/s+/), i = names.length; i --; ) {
          if (names[i] === name) {
            found.push(node);
          }
        }
      }
    });
    return found;
  } ;

Testing for getElementsByClassName()

As of this writing, the fourth working draft of HTML 5 defines a getElementsByClassName() method for Document and Element nodes to query elements by their class attribute. Even though HTML 5 is not yet a W3C recommendation, Explorer 9, Firefox 3, Safari 3.1, Chrome 4, and Opera 9.62 already implement getElementsByClassName().

getElementsByClassName() works with one parameter, a string of one or more class names separated by spaces, just like the class attribute for an XHTML tag. The return value is a NodeList containing any matching Element nodes. However, this NodeList differs from the one returned by querySelectorAll() in that it is a live DOM query.

Now let's rework our advanced conditional loader for findClass() so that getElementsByClassName() is the preferred option. Rather than nest ?: expressions, let's go with the else if idiom. This is a little more readable for a three-option advance conditional loader.

Because an if statement cannot be the right operand to an = operator (only an expression or literal can be an operand), we are going to have to put the var statement before the if. Then in the if statement, we will assign one of three function literals to findClass. Reworking our previous take on findClass(), we have the following:

var findClass;
if () {
} else if (document.querySelectorAll) {
  findClass = function (name, root) {
    root = root || document.documentElement;
    return root.querySelectorAll("." + name);
  };
} else {
  findClass = function (name, root) {
    var found = [];
root = root || document.documentElement;
    traverseTree(root, function(node) {
      if (!! node.className) {
        for (var names = node.className.split(/s+/), i = names.length; i --; ) {
          if (names[i] === name) {
            found.push(node);
          }
        }
      }
    });
    return found;
  };
}

Now put document.getElementsByClassName in the empty () following the if keyword. This will return either a function, which converts to true, or undefined, which converts to false:

var findClass;
if (document.getElementsByClassName) {
} else if (document.querySelectorAll) {
  findClass = function (name, root) {
    root = root || document.documentElement;
    return root.querySelectorAll("." + name);
  };
} else {
  findClass = function (name, root) {
    var found = [];
    root = root || document.documentElement;
    traverseTree(root, function(node) {
      if (!! node.className) {
        for (var names = node.className.split(/s+/), i = names.length; i --; ) {
          if (names[i] === name) {
            found.push(node);
          }
        }
      }
    });
    return found;
  };
}

Now for the empty if block. Just as with the other two blocks, we want to assign a function literal to findClass there, since at the moment findClass contains undefined. This one is almost identical to the one for querySelectorAll(). Just change the identifier to getElementsByClassName and pass in name as is.

var findClass;
if (document.getElementsByClassName) {
  findClass = function (name, root) {
    root = root || document.documentElement;
    return root.getElementsByClassName(name);
  };
} else if (document.querySelectorAll) {
  findClass = function (name, root) {
    root = root || document.documentElement;
    return root.querySelectorAll("." + name);
};
} else {
  findClass = function (name, root) {
    var found = [];
    root = root || document.documentElement;
    traverseTree(root, function(node) {
      if (!! node.className) {
        for (var names = node.className.split(/s+/), i = names.length; i --; ) {
          if (names[i] === name) {
            found.push(node);
          }
        }
      }
    });
    return found;
  };
}

So there it is. findClass will be one of three function literals that return elements of the same class from the overall DOM tree or a branch of it. Note that native browser functions such as getElementsByClassName() or querySelectorAll() are compiled not interpreted. This is why they are snappier than ones you write yourself.

Note

If you want to view W3 documentation on querySelectorAll() or querySelector(), visit:

www.w3.org/TR/selectors-api/

If you want to view W3 documentation on getElementsByClassName() or HTML 5, visit:

www.w3.org/TR/html5/

Querying the Cascade

In Chapter 8, we wrote the following function to query CSS values from the cascade by either the DOM getComputedStyle() method or the Internet Explorer currentStyle property.

function queryCascade(element, property) {
  if (typeof getComputedStyle === "function") {
    return getComputedStyle(element, null)[property];
  } else if (element.currentStyle) {
    return element.currentStyle[property];
  }
}

Regardless of whether JavaScript queries the CSS cascade in the DOM or Internet Explorer way, doing so is quite a speed bump. With this in mind, let's at least eliminate the redundant feature testing by recoding queryCascade() as an advance conditional loader. Note that we will still have to try to avoid querying the cascade, which remains slow as a turtle.

The first thing we need to do is declare queryCascade with a var statement, rather than a function statement:

var queryCascade;

Next, initialize queryCascade to a ?: expression that returns one of two function literals. The boolean expression prior to the ? token can be the one from the if else shown earlier, typeof getComputedStyle === "function". Or more simply, it can be window.getComputedStyle:

var queryCascade = window.getComputedStyle ?
  function() {
  } :
  function() {
  } ;

Now between the ? and : tokens, code a function literal to do the job of the if clause shown previously. That is to say, just cut and paste the return statement. But remember to define the element and property parameters:

var queryCascade = window.getComputedStyle ?
  function(element, property) {
    return getComputedStyle(element, null)[property];
  } :
  function() {
  } ;

Finally, between the : and ; tokens, code a function literal containing the return statement from the else clause shown previously. Yup, cut and paste. And again, don't forget the element and property parameters:

var queryCascade = window.getComputedStyle ?
  function(element, property) {
    return getComputedStyle(element, null)[property];
  } :
  function(element, property) {
    return element.currentStyle[property];
  } ;

So there it is. In Explorer 9, Firefox, Safari, Chrome, and Opera, JavaScript assigns the first function literal to queryCascade, and in Internet Explorer 8 or earlier, it assigns the second one. There's no feature testing to do whenever JavaScript calls queryCascade(). Not even the first time!

We're done reworking our helper functions for working with markup and CSS, so our JavaScript file now looks like so:

var addListener = document.addEventListener ?
  function(node, type, listener, phase) {
    node.addEventListener(type, listener, !! phase);
  } :
  function(node, type, listener) {
    node.attachEvent("on" + type, listener);
  } ;
var removeListener = document.removeEventListener ?
  function(node, type, listener, phase) {
    node.removeEventListener(type, listener, !! phase);
  } :
  function(node, type, listener) {
    node.detachEvent("on" + type, listener);
  } ;
function thwart(e) {
if (e.preventDefault) {
    thwart = function(e) {
      e.preventDefault();
    };
  } else {
    thwart = function(e) {
      e.returnValue = false;
    };
  }
  thwart(e);
}
function burst(e) {
  if (e.stopPropagation) {
    burst = function(e) {
      e.stopPropagation();
    };
  } else {
    burst = function(e) {
      e.cancelBubble = true;
    };
  }
  burst(e);
}
var traverseTree = document.documentElement.firstElementChild ?
  function traverseTree (node, func) {
    func(node);
    node = node.firstElementChild;
    while (node !== null) {
      traverseTree(node, func);
      node = node.nextElementSibling;
    }
  } :
  function traverseTree (node, func) {
    func(node);
    node = node.firstChild;
    while (node !== null) {
      traverseTree(node, func);
      node = node.nextSibling;
    }
  } ;

var findClass;
if (document.getElementsByClassName) {
  findClass = function (name, root) {
    root = root || document.documentElement;
    return root.getElementsByClassName(name);
  };
} else if (document.querySelectorAll) {
  findClass = function (name, root) {
    root = root || document.documentElement;
    return root.querySelectorAll("." + name);
  };
} else {
  findClass = function (name, root) {
var found = [];
    root = root || document.documentElement;
    traverseTree(root, function(node) {
      if (!! node.className) {
        for (var names = node.className.split(/s+/), i = names.length; i --; ) {
          if (names[i] === name) {
            found.push(node);
          }
        }
      }
    });
    return found;
  };
}
var queryCascade = window.getComputedStyle ?
  function(element, property) {
    return getComputedStyle(element, null)[property];
  } :
  function(element, property) {
    return element.currentStyle[property];
  } ;

Sliding Sprites

Now for the first of our behaviors. This one will slide a sprite whenever a visitor rolls their mouse over or off an element of the sprite class. JavaScript will be listening for mouseover and mouseout events.

The rough skeleton for our sprite behavior will have an event listener function named slideSprite() nested within a preparatory function named prepSprites(). Doing so means the event listener function may query the call object of prepSprites() even though prepSprites() will be invoked just one time, shortly after the page loads. Thus far we have this:

function prepSprites() {
  function slideSprite() {
  }
}

Preparing the Ground

What are these secret variables we want slideSprite() to be able to query even after prepSprites() has returned? The first one, elements, will contain the return value of passing "sprite" to our helper function, findClass(). So, any elements of the sprite class will be in there. The second one, sprites, will for now contain an empty object but later will contain details of each sprite. We'll add members to sprites in a moment. Note that creating an object with literal notation is snappier than doing so with new and the Object() constructor. Therefore, {} it is:

function prepSprites() {
  var elements = findClass("sprite"), sprites = {};
  function slideSprite() {
  }
}

Now let's iterate over the Element nodes in elements. Remember from Chapter 4 that looping in reverse is snappier in that we can test and update the loop variable i in a single expression. Note too that we want to initialize i to elements.length so that we don't slow things down by querying elements.length every roundabout of the loop. Finally, initialize a variable named offsets to null since it will later contain an array of offsets identifying where the sprites are:

function prepSprites() {
  var elements = findClass("sprite"), sprites = {};
  for (var i = elements.length, offsets = null; i --; ) {
  }
  function slideSprite() {
  }
}

Now for every Element node in elements, we want to name a member in sprites with the value of its id attribute. So, relative to our XHTML markup, sprites will contain members named "adidas", "asics", "brooks", and so on. Those will initially contain an empty array, which we will create with array literal notation, since that is snappier than doing so with new and Array():

function prepSprites() {
  var elements = findClass("sprite"), sprites = {};
  for (var i = elements.length, offsets = null; i --; ) {
    sprites[elements[i].id] = [];
  }
  function slideSprite() {
  }
}

Now we have to work around some Internet Explorer 8 or earlier skullduggery. Querying currentStyle.backgroundPosition for an element returns undefined even though querying style.backgroundPosition for the very same element returns the horizontal and vertical offsets of the background image. I know, that's preposterous.

Are those offsets simply missing in currentStyle?

No, they're just in a different drawer. Internet Explorer 8 or earlier separates them into members named backgroundPositionX and backgroundPositionY. We will have to code one path for Explorer 9, Firefox, Safari, Chrome, and Opera and another for Internet Explorer 8 or earlier. Let's do the former first. Test for the DOM method getComputedStyle(), and then query backgroundPosition, saving that to sprites[elements[i].id][0]. So for example, sprites.saucony[0] will contain "0px −135px", which is the off position for the sprite (the position when the mouse is off the sprite).

Now we need to separate the horizontal and vertical offsets. To do so, call String.split() on the off position that we just saved to elements[i].id][0]. Remember from Chapter 2 that String.split() returns an array of smaller strings created by separating the larger string relative to its parameter. So if we divide the off string based on whitespace, we get an array with two elements. So for the Saucony sprite, the array would be as follows:

["0px", "−135px"]

Save that to the offsets variable that we initialized to null a moment ago. So we now have this:

function prepSprites() {
  var elements = findClass("sprite"), sprites = {};
  for (var i = elements.length, offsets = null; i --; ) {
    sprites[elements[i].id] = [];
    if (typeof getComputedStyle === "function") {
      sprites[elements[i].id][0] = queryCascade(elements[i], "backgroundPosition");
      offsets = sprites[elements[i].id][0].split(/s+/);
}
  }
  function slideSprite() {
  }
}

Now for the else clause for Internet Explorer 8 or earlier. Insofar as its offsets are already separated, we will do things in reverse. Create the offsets array from backgroundPositionX and backgroundPositionY. Then call Array.join() on offsets, passing " " as the parameter, and save the return value to sprites[elements[i].id][0]. For example, sprites.nike would now contain "0px −108px":

function prepSprites() {
  var elements = findClass("sprite"), sprites = {};
  for (var i = elements.length, offsets = null; i --; ) {
    sprites[elements[i].id] = [];
    if (typeof getComputedStyle === "function") {
      sprites[elements[i].id][0] = queryCascade(elements[i], "backgroundPosition");
      offsets = sprites[elements[i].id][0].split(/s+/);
    } else {
      offsets = [
        queryCascade(elements[i], "backgroundPositionX"),
        queryCascade(elements[i], "backgroundPositionY")
      ];
      sprites[elements[i].id][0] = offsets.join(" ");
    }
  }
  function slideSprite() {
  }
}

Now that sprites[elements[i].id][0] and offsets have the same values in Firefox, Safari, Chrome, Opera, and Internet Explorer, we can calculate the over position by subtracting the width of the element from 1 and concatenating that to "px " and the vertical offset, which remains the same in the over position (the over position being for when the mouse is over the button). Internet Explorer doesn't make up a different name for width, so we can simply call queryCascade() this time. However, we need to remove the "px" from the width value with parseInt() before subtracting it from 1:

function prepSprites() {
  var elements = findClass("sprite"), sprites = {};
  for (var i = elements.length, offsets = null; i --; ) {
    sprites[elements[i].id] = [];
    if (typeof getComputedStyle === "function") {
      sprites[elements[i].id][0] = queryCascade(elements[i], "backgroundPosition");
      offsets = sprites[elements[i].id][0].split(/s+/);
    } else {
      offsets = [
        queryCascade(elements[i], "backgroundPositionX"),
        queryCascade(elements[i], "backgroundPositionY")
      ];
      sprites[elements[i].id][0] = offsets.join(" ");
    }
    sprites[elements[i].id][1] = 1 - parseInt(queryCascade(elements[i], "width")) + "px " +
offsets[1];
}
  function slideSprite() {
  }
}

Now JavaScript has created the following object that our nested event listener function, slideSprite(), can query even after prepSprites() returns:

var sprites = {
  "adidas": ["0px 0px", "−99px 0px"],
  "asics": ["0px −27px", "−99px −27px"],
  "brooks": ["0px −54px", "−99px −54px"],
  "newBalance": ["0px −81px", "−99px −81px"],
  "nike": ["0px −108px", "−99px −108px"],
  "saucony": ["0px −135px", "−99px −135px"]
}

Now we want to tell JavaScript to run slideSprite() whenever mouseover and mouseout events occur on the <a> in elements[i]. This is where our helper function, addListener(), earns its keep. Note that only the second parameter differs in our two calls to addListener(). Feel free to cut and paste:

function prepSprites() {
  var elements = findClass("sprite"), sprites = {};
  for (var i = elements.length, offsets = null; i --; ) {
    sprites[elements[i].id] = [];
    if (typeof getComputedStyle === "function") {
      sprites[elements[i].id][0] = queryCascade(elements[i], "backgroundPosition");
      offsets = sprites[elements[i].id][0].split(/s+/);
    } else {
      offsets = [
        queryCascade(elements[i], "backgroundPositionX"),
        queryCascade(elements[i], "backgroundPositionY")
      ];
      sprites[elements[i].id][0] = offsets.join(" ");
    }
    sprites[elements[i].id][1] = 1 - parseInt(queryCascade(elements[i], "width")) + "px " +
      offsets[1];
    addListener(elements[i], "mouseover", slideSprite);
    addListener(elements[i], "mouseout", slideSprite);
  }
  function slideSprite() {
  }
}

That ends the for loop, and prepSprites() would now return. Note that only our nested event listener function, slideSprite(), can query the offsets in sprites, which lives on in a closure. With this in mind, let's go ahead and fill in slideSprite().

Moving the Sprites

The first thing we need to do is define a parameter for the event object that Internet Explorer 9, Firefox, Safari, Chrome, and Opera will pass to slideSprite() whenever a mouseover or mouseout event takes place on our sprites. By convention, this parameter is named e. It doesn't have to be. We could name it brownCow if we wanted. But let's not rock the boat.

Internet Explorer 8 or earlier does not pass an event object to event listener functions. Rather, it continually overwrites the global variable, event, with the latest event object. Insofar as only one event can ever take place at a time, this works. It's a bit of a kludge, but it works. Note that for interoperability, Internet Explorer 9 and Opera implement window.event, too.

Anyway, if e contains undefined, as it would in Internet Explorer 8 or earlier, let's overwrite that value with window.event:

function prepSprites() {
  var elements = findClass("sprite"), sprites = {};
  for (var i = elements.length, offsets = null; i --; ) {
    sprites[elements[i].id] = [];
    if (typeof getComputedStyle === "function") {
      sprites[elements[i].id][0] = queryCascade(elements[i], "backgroundPosition");
      offsets = sprites[elements[i].id][0].split(/s+/);
    } else {
      offsets = [
        queryCascade(elements[i], "backgroundPositionX"),
        queryCascade(elements[i], "backgroundPositionY")
      ];
      sprites[elements[i].id][0] = offsets.join(" ");
    }
    sprites[elements[i].id][1] = 1 - parseInt(queryCascade(elements[i], "width")) + "px " +
      offsets[1];
    addListener(elements[i], "mouseover", slideSprite);
    addListener(elements[i], "mouseout", slideSprite);
  }
  function slideSprite(e) {
    if (!e) e = window.event;
  }
}

e now contains an object with members that detail what the visitor did. One of those members refers to the node in the DOM tree that the event took place on. For Internet Explorer 9, Firefox, Safari, Chrome, and Opera, the member is named target. For Internet Explorer 8 or earlier, the member is named srcElement. Note that Internet Explorer 9 and Opera implement both target and srcElement.

Anyway, if e does not have a target member, referring to e.target returns undefined. In that case, we want to add a target member to e that refers to window.event.srcElement:

function prepSprites() {
  var elements = findClass("sprite"), sprites = {};
  for (var i = elements.length, offsets = null; i --; ) {
    sprites[elements[i].id] = [];
    if (typeof getComputedStyle === "function") {
      sprites[elements[i].id][0] = queryCascade(elements[i], "backgroundPosition");
      offsets = sprites[elements[i].id][0].split(/s+/);
    } else {
      offsets = [
        queryCascade(elements[i], "backgroundPositionX"),
        queryCascade(elements[i], "backgroundPositionY")
      ];
      sprites[elements[i].id][0] = offsets.join(" ");
    }
    sprites[elements[i].id][1] = 1 - parseInt(queryCascade(elements[i], "width")) + "px " +
      offsets[1];
    addListener(elements[i], "mouseover", slideSprite);
addListener(elements[i], "mouseout", slideSprite);
  }
  function slideSprite(e) {
    if (!e) e = window.event;
    if (!e.target) e.target = e.srcElement;
  }
}

To know whether to slide the sprite to the over or off position, we need to know whether a mouseover or mouseout event took place. The answer to our query is in e.type. Even in Internet Explorer 8 or earlier! So if e.type contains "mouseover", we want to slide the sprite to sprites[e.target.id][1]. Otherwise, we want to slide it to sprites[e.target.id][0]. That sounds like a job for an if else statement:

function prepSprites() {
  var elements = findClass("sprite"), sprites = {};
  for (var i = elements.length, offsets = null; i --; ) {
    sprites[elements[i].id] = [];
    if (typeof getComputedStyle === "function") {
      sprites[elements[i].id][0] = queryCascade(elements[i], "backgroundPosition");
      offsets = sprites[elements[i].id][0].split(/s+/);
    } else {
      offsets = [
        queryCascade(elements[i], "backgroundPositionX"),
        queryCascade(elements[i], "backgroundPositionY")
      ];
      sprites[elements[i].id][0] = offsets.join(" ");
    }
    sprites[elements[i].id][1] = 1 - parseInt(queryCascade(elements[i], "width")) + "px " +
      offsets[1];
    addListener(elements[i], "mouseover", slideSprite);
    addListener(elements[i], "mouseout", slideSprite);
  }
  function slideSprite(e) {
    if (!e) e = window.event;
    if (!e.target) e.target = e.srcElement;
    if (e.type == "mouseover") {
      e.target.style.backgroundPosition = sprites[e.target.id][1];
    } else {
      e.target.style.backgroundPosition = sprites[e.target.id][0];
    }
  }
}

So there it is. Now to verify that this all works as planned, load nine.html in Firefox, enable Firebug, and then call prepSprites():

prepSprites();

Now roll your mouse over and off the sprites to test the swaps. In Figure 9-3, I rolled my mouse over the New Balance sprite, which has the shading at the top now, rather than at the bottom:

Testing the sprites by manually calling prepSprites() with the Firebug console

Figure 9-3. Testing the sprites by manually calling prepSprites() with the Firebug console

Snappier Sprites

So prepSprites() and slideSprite() are written to work for Internet Explorer 9, Firefox, Safari, Chrome, and Opera or for Internet Explorer 8 or earlier. Even though lumping DOM-savvy and DOM-dummy paths together in event listener functions is commonplace (most scripts you maintain will do so), all that feature testing makes them run slower. With this in mind, let's rework prepSprites() and slideSprite() as advance conditional loaders so that our sprites are snappier for visitors.

First, we want to define prepSprites with a var statement rather than a function statement. Then, we initialize its value to one of two function literals relative to whether getComputedStyle() is defined:

var prepSprites = window.getComputedStyle ?
  function () {
  } :
  function () {
  } ;

Now let's fill in the function literal for Internet Explorer 9, Firefox, Safari, Chrome, and Operaby eliminating all the workarounds for Internet Explorer 8 or earlier:

var prepSprites = window.getComputedStyle ?
  function () {
    var elements = findClass("sprite"), sprites = {};
    for (var i = elements.length, offsets = null; i --; ) {
      sprites[elements[i].id] = [];
      sprites[elements[i].id][0] = queryCascade(elements[i], "backgroundPosition");
      offsets = sprites[elements[i].id][0].split(/s+/);
      sprites[elements[i].id][1] = 1 - parseInt(queryCascade(elements[i], "width")) +
      "px " + offsets[1];
      addListener(elements[i], "mouseover", slideSprite);
      addListener(elements[i], "mouseout", slideSprite);
    }
    function slideSprite(e) {
      if (e.type == "mouseover") {
        e.target.style.backgroundPosition = sprites[e.target.id][1];
      } else {
        e.target.style.backgroundPosition = sprites[e.target.id][0];
      }
    }
  } :
  function () {
  } ;

Now for the one that Internet Explorer 8 or earlier can palate. Just remove the code intelligible only to Internet Explorer 9, Firefox, Safari, Chrome, and Opera. Note that in slideSprite(), we save window.event to a local variable e. Caching any global variable that you query more than one time in a function to a local variable makes the lookup snappier. Remember from earlier in the book that global variables reside on the very last variable object in a function's execution context. So, caching the global variable to a local one prevents JavaScript from fruitlessly querying activation objects for a global variable.

With those things in mind, let's fill in the block of the function literal for Internet Explorer 8 or earlier like so:

var prepSprites = window.getComputedStyle ?
  function () {
    var elements = findClass("sprite"), sprites = {};
    for (var i = elements.length, offsets = null; i --; ) {
      sprites[elements[i].id] = [];
      sprites[elements[i].id][0] = queryCascade(elements[i], "backgroundPosition");
      offsets = sprites[elements[i].id][0].split(/s+/);
      sprites[elements[i].id][1] = 1 - parseInt(queryCascade(elements[i], "width")) +
        "px " + offsets[1];
      addListener(elements[i], "mouseover", slideSprite);
      addListener(elements[i], "mouseout", slideSprite);
    }
    function slideSprite(e) {
      if (e.type == "mouseover") {
        e.target.style.backgroundPosition = sprites[e.target.id][1];
      } else {
        e.target.style.backgroundPosition = sprites[e.target.id][0];
}
    }
  } :
  function () {
    var elements = findClass("sprite"), sprites = {};
    for (var i = elements.length, offsets = null; i --; ) {
      sprites[elements[i].id] = [];
      offsets = [queryCascade(elements[i], "backgroundPositionX"),
        queryCascade(elements[i], "backgroundPositionY")];
      sprites[elements[i].id][0] = offsets.join(" ");
      sprites[elements[i].id][1] = 1 - parseInt(queryCascade(elements[i], "width")) +
        "px " + offsets[1];
      addListener(elements[i], "mouseover", slideSprite);
      addListener(elements[i], "mouseout", slideSprite);
    }
    function slideSprite() {
      var e = window.event;
      if (e.type == "mouseover") {
        e.srcElement.style.backgroundPosition = sprites[e.srcElement.id][1];
      } else {
        e.srcElement.style.backgroundPosition = sprites[e.srcElement.id][0];
      }
    }
  } ;

So there it is. Neither Internet Explorer 9, Firefox, Safari, Chrome, and Opera nor Internet Explorer 8 or earlier have to do any feature testing whenever prepSprites() or slideSprite() run. Not even the first time. All in all, JavaScript has less work to do, and our visitors get snappier sprites.

Before moving on to the drag-and-drop behavior, verify your work by refreshing Firefox and calling prepSprites() via Firebug:

prepSprites();

Then roll your mouse over and off the sprites to test the swaps.

Does it work for you, too? Great. But if not, verify that your script is just like the rest of ours:

var addListener = document.addEventListener ?
  function(node, type, listener, phase) {
    node.addEventListener(type, listener, !! phase);
  } :
  function(node, type, listener) {
    node.attachEvent("on" + type, listener);
  } ;

var removeListener = document.removeEventListener ?
  function(node, type, listener, phase) {
    node.removeEventListener(type, listener, !! phase);
  } :
  function(node, type, listener) {
    node.detachEvent("on" + type, listener);
  } ;

function thwart(e) {
  if (e.preventDefault) {
    thwart = function(e) {
e.preventDefault();
    };
  } else {
    thwart = function(e) {
      e.returnValue = false;
    };
  }
  thwart(e);
}

function burst(e) {
  if (e.stopPropagation) {
    burst = function(e) {
      e.stopPropagation();
    };
  } else {
    burst = function(e) {
      e.cancelBubble = true;
    };
  }
  burst(e);
}

var traverseTree = document.documentElement.firstElementChild ?
  function traverseTree (node, func) {
    func(node);
    node = node.firstElementChild;
    while (node !== null) {
      traverseTree(node, func);
      node = node.nextElementSibling;
    }
  } :
  function traverseTree (node, func) {
    func(node);
    node = node.firstChild;
    while (node !== null) {
      traverseTree(node, func);
      node = node.nextSibling;
    }
  } ;

var findClass;

if (document.getElementsByClassName) {
  findClass = function (name, root) {
    root = root || document.documentElement;
    return root.getElementsByClassName(name);
  };
} else if (document.querySelectorAll) {
  findClass = function (name, root) {
    root = root || document.documentElement;
    return root.querySelectorAll("." + name);
  };
} else {
findClass = function (name, root) {
    var found = [];
    root = root || document.documentElement;
    traverseTree(root, function(node) {
      if (!! node.className) {
        for (var names = node.className.split(/s+/), i = names.length; i --; ) {
          if (names[i] === name) {
            found.push(node);
          }
        }
      }
    });
    return found;
  };
}

var queryCascade = window.getComputedStyle ?
  function(element, property) {
    return getComputedStyle(element, null)[property];
  } :
  function(element, property) {
    return element.currentStyle[property];
  } ;

var doZ = function() {
  var z = 400;
  return function() {
    return z ++;
  };
}();

// sprite swaps

var prepSprites = window.getComputedStyle ?
  function () {
    var elements = findClass("sprite"), sprites = {};
    for (var i = elements.length, offsets = null; i --; ) {
      sprites[elements[i].id] = [];
      sprites[elements[i].id][0] = queryCascade(elements[i], "backgroundPosition");
      offsets = sprites[elements[i].id][0].split(/s+/);
      sprites[elements[i].id][1] = 1 - parseInt(queryCascade(elements[i], "width")) +
        "px " + offsets[1];
      addListener(elements[i], "mouseover", slideSprite);
      addListener(elements[i], "mouseout", slideSprite);
    }
    function slideSprite(e) {
      if (e.type == "mouseover") {
        e.target.style.backgroundPosition = sprites[e.target.id][1];
      } else {
        e.target.style.backgroundPosition = sprites[e.target.id][0];
      }
    }
  } :
  function () {
var elements = findClass("sprite"), sprites = {};
    for (var i = elements.length, offsets = null; i --; ) {
      sprites[elements[i].id] = [];
      offsets = [queryCascade(elements[i], "backgroundPositionX"),
        queryCascade(elements[i], "backgroundPositionY")];
      sprites[elements[i].id][0] = offsets.join(" ");
      sprites[elements[i].id][1] = 1 - parseInt(queryCascade(elements[i], "width")) +
        "px " + offsets[1];
      addListener(elements[i], "mouseover", slideSprite);
      addListener(elements[i], "mouseout", slideSprite);
    }
    function slideSprite() {
      var e = window.event;
      if (e.type == "mouseover") {
        e.srcElement.style.backgroundPosition = sprites[e.srcElement.id][1];
      } else {
        e.srcElement.style.backgroundPosition = sprites[e.srcElement.id][0];
      }
    }
  } ;

Drag-and-Drop Behavior

Now it's time for the drag-and-drop behavior, where we can move the panel of buttons around the page. For this one to work, we need to have JavaScript listen for mousedown, mousemove, and mouseup events. Those occur whenever a visitor presses down on their mouse button, moves their mouse, and releases their mouse button. Who'd have thought?

Writing the Mousedown Event Listener

The rough skeleton will be a mousedown event listener named drag() containing the mousemove and mouseup event listeners, named move() and drop(). Those two nested functions can then query the call object for drag(), which is where we will store several coordinates for later (the position of the moveable panel and the position of the mouse when the event occurs). Note that all three event listeners define an e parameter for the event object that Internet Explorer 9, Firefox, Safari, Chrome, and Opera send their way. Then assign window.event to e if it contains undefined, if the browser is Internet Explorer 8 or earlier. Thus far we have the following:

function drag(e) {
  if (!e) e = window.event;
  function move(e) {
    if (!e) e = window.event;
  }
  function drop(e) {
    if (!e) e = window.event;
  }
}

Now let's fill in the block for drag(). This is the only one of the three event listeners that needs to query e.target, because we can then use the results of this query elsewhere. Remember that e.target is the node the event happened to. For drag(), that would a mousedown event on an element of the drag class. Taking a peek at our markup, that would be the <h4> element. So fine, if e does not have a target member, add one that refers to srcElement for Internet Explorer 8 or earlier:

function drag(e) {
  if (!e) e = window.event;
  if (!e.target) e.target = e.srcElement;
  function move(e) {
    if (!e) e = window.event;
  }
  function drop(e) {
    if (!e) e = window.event;
  }
}

By the way, if you are wondering whether we will be recoding this as an advance conditional loader, don't. Other than redefining e and its members, there's no workarounds for Internet Explorer 8 or earlier. So our first cut will be our final one.

Where were we? Right. Now we don't want to move the running tab, which is to say the <h4> element. Rather, we want to move the <div> wrapping the whole shebang, which is e.target.parentNode. We'll save that to a local variable aptly named wrapper. That way, both move() and drop() can manipulate the <div> later. But one thing we need to do straightaway is make sure wrapper displays in front of any other content in the documentso the user can always see it when dragging. To do so, set its z-index to the return value of doZ(), a helper function we will write later (remind me if I forget). We now have this:

function drag(e) {
  if (!e) e = window.event;
  if (!e.target) e.target = e.srcElement;
  var wrapper = e.target.parentNode;
  wrapper.style.zIndex = doZ();
  function move(e) {
    if (!e) e = window.event;
  }
  function drop(e) {
    if (!e) e = window.event;
  }
}

We just need to jot down some coordinates for move() to do calculations with later. First, we want to save CSS values for left and top (stripped of their units of measure) to local variables named, well, left and top, which we will use later to recalculate the new position of the panel. Then we want to save clientX and clientY for the mousedown event to local variables named, you guessed it, clientX and clientY. The clientX and clientY members of the event object provide those coordinates in pixels. However, those are numbers, not strings. So, no "px" suffix to strip away with parseInt(). Note that clientX and clientY are not in document coordinates because CSS values for left and top would need to be when we reposition wrapper (in other words, clientX and clientY are the coordinates on the current window rather than the position in the document). Still, we need clientX and clientY to calculate left and top as well as to keep the visitors mouse pinned to the same spot on the Running tab (you'll see the actual calculations in the next section). We now have this:

function drag(e) {
  if (!e) e = window.event;
  if (!e.target) e.target = e.srcElement;
  var wrapper = e.target.parentNode;
  var left = parseInt(queryCascade(wrapper, "left"));
var top = parseInt(queryCascade(wrapper, "top"));
  var clientX = e.clientX;
  var clientY = e.clientY;
  wrapper.style.zIndex = doZ();
  function move(e) {
    if (!e) e = window.event;
  }
  function drop(e) {
    if (!e) e = window.event;
  }
}

Observant readers will notice that we put those last four var statements before the wrapper.style.zIndex = doZ() statement. Any ideas as to why?

Right. It's good programming practice to put all local variable declarations at the top of their function's block. Smiley cookie for remembering.

Now we want to have document listen for every mousemove event. Why do we want to run an event listener function for every mousemove event? Why not just listen for those that happen on the <h4> element?

It turns out a visitor can move their mouse faster than an event listener on just the <h4> can respond to them. So, if we just run move() for mousemove events on the <h4>, the visitor's mouse will leave its confines. Consequently, after an initial nudge, the <div> will not follow their mouse. Sort of like a stubborn mule.

Not wanting that to be the case, let's bind move() to document rather than the <h4>. And to improve performance in Internet Explorer 9, Firefox, Safari, Chrome, and Opera, let's do so for the capture phase. Note that in Internet Explorer 8 or earlier, document will listen during the bubble phase instead. So, pass true as the optional fourth parameter to addListener():

function drag(e) {
  if (!e) e = window.event;
  if (!e.target) e.target = e.srcElement;
  var wrapper = e.target.parentNode;
  var left = parseInt(queryCascade(wrapper, "left"));
  var top = parseInt(queryCascade(wrapper, "top"));
  var clientX = e.clientX;
  var clientY = e.clientY;
  wrapper.style.zIndex = doZ();
  addListener(document, "mousemove", move, true);
  function move(e) {
    if (!e) e = window.event;
  }
  function drop(e) {
    if (!e) e = window.event;
  }
}

Just as we registered move() on document in order to keep up with the visitor's mouse, we also want to register drop() on document so as not to miss the mouseup event, which tells us where to drop the Running <div>, that is, where the visitor wanted to drag it to.

Could you tell me what would happen if we did miss that vital mouseup event?

This one is worth a couple Smiley cookies.

What do you think?

Sort of. If the visitor stopped moving their mouse, the <div> would drop pretty much where they wanted it to. However, whenever they started moving their mouse again, say from the <h4> tab down to the Brooks <a>, the <div> would follow their mouse. They'd never be able to click the Brooks <a>—or any other link on the page. Great googly moogly, we can't have that!

Let's register drop() on document, too. Pass true for the fourth parameter again to improve performance in Internet Explorer 9, Firefox, Safari, Chrome, and Opera. Rather than wait for the mouseup event to descend and ascend the DOM tree, just nip it in the bud:

function drag(e) {
  if (!e) e = window.event;
  if (!e.target) e.target = e.srcElement;
  var wrapper = e.target.parentNode;
  var left = parseInt(queryCascade(wrapper, "left"));
  var top = parseInt(queryCascade(wrapper, "top"));
  var clientX = e.clientX;
  var clientY = e.clientY;
  wrapper.style.zIndex = doZ();
  addListener(document, "mousemove", move, true);
  addListener(document, "mouseup", drop, true);
  function move(e) {
    if (!e) e = window.event;
  }
  function drop(e) {
    if (!e) e = window.event;
  }
}

Now call burst() and thwart(), remembering to pass in e, which contains the mousedown event object. Doing so prevents any mousedown event listeners bound to ancestors of the <h4> from running and a context menu from appearing for Mac visitors:

function drag(e) {
  if (!e) e = window.event;
  if (!e.target) e.target = e.srcElement;
  var wrapper = e.target.parentNode;
  var left = parseInt(queryCascade(wrapper, "left"));
  var top = parseInt(queryCascade(wrapper, "top"));
  var clientX = e.clientX;
  var clientY = e.clientY;
  wrapper.style.zIndex = doZ();
  addListener(document, "mousemove", move, true);
  addListener(document, "mouseup", drop, true);
  burst(e);
  thwart(e);
  function move(e) {
    if (!e) e = window.event;
  }
  function drop(e) {
    if (!e) e = window.event;
  }
}

We're done with the mousedown event listener. Now let's move on to move(), mousemove event listener. Sorry for the pun.

Writing the Mousemove Event Listener

One note of caution for those who are bad at math. There is some possibility that your head will explode trying to comprehend how move() calculates left and top during the drag. So, maybe cover your ears and close your eyes while we work on move(). Someone will give you a poke when we're done to let you know it's safe to follow along again.

Now even though drag() will have returned prior to JavaScript ever calling move(), which it will do rapid-fire during a drag, wrapper continues to refer to the Running <div> by way of a closure. Therefore, we can reposition the <div> by changing wrapper.style.left and wrapper.style.top.

This is where things get tricky. Even though the clientX and clientY members of an event object are in window coordinates, but CSS values for left and top are in document coordinates, we can use the former to calculate the latter for the reason that the document does not scroll during the drag, while move() is running.

With this in mind, we can calculate the CSS value for wrapper.style.left by adding the X coordinate of the mousemove event in e.clientX to the local variable left and then subtracting the X coordinate of the mousedown event. Finally, we concatenate "px" to that number:

function drag(e) {
  if (!e) e = window.event;
  if (!e.target) e.target = e.srcElement;
  var wrapper = e.target.parentNode;
  var left = parseInt(queryCascade(wrapper, "left"));
  var top = parseInt(queryCascade(wrapper, "top"));
  var clientX = e.clientX;
  var clientY = e.clientY;
  wrapper.style.zIndex = doZ();
  addListener(document, "mousemove", move, true);
  addListener(document, "mouseup", drop, true);
  burst(e);
  thwart(e);
  function move(e) {
    if (!e) e = window.event;
    wrapper.style.left = left + e.clientX - clientX + "px";
  }
  function drop(e) {
    if (!e) e = window.event;
  }
}

In the same way, we can calculate the CSS value for wrapper.style.top by adding the Y coordinate of the mousemove event in e.clientY to the local variable top and then subtracting the Y coordinate of the mousedown event. Finally, we concatenate "px" to that number:

function drag(e) {
  if (!e) e = window.event;
  if (!e.target) e.target = e.srcElement;
  var wrapper = e.target.parentNode;
  var left = parseInt(queryCascade(wrapper, "left"));
  var top = parseInt(queryCascade(wrapper, "top"));
  var clientX = e.clientX;
  var clientY = e.clientY;
  wrapper.style.zIndex = doZ();
  addListener(document, "mousemove", move, true);
  addListener(document, "mouseup", drop, true);
burst(e);
  thwart(e);
  function move(e) {
    if (!e) e = window.event;
    wrapper.style.left = left + e.clientX - clientX + "px";
    wrapper.style.top = top + e.clientY - clientY + "px";
  }
  function drop(e) {
    if (!e) e = window.event;
  }
}

Now to prevent the mousemove event object from traversing the DOM tree any further, pass e to burst(). Note that this is just for Internet Explorer 9, Firefox, Safari, Chrome, and Opera since in Internet Explorer 8 or earlier the mousemove event has already ended its journey by bubbling up to document. Note too that there would be no point in passing e to thwart() inasmuch as there is no default action for a mousemove event.

And with that, we're done coding move(). So if any one next to you has their ears covered and eyes closed, give them a poke. Then have them cut and paste our code for move(), which appears here:

function drag(e) {
  if (!e) e = window.event;
  if (!e.target) e.target = e.srcElement;
  var wrapper = e.target.parentNode;
  var left = parseInt(queryCascade(wrapper, "left"));
  var top = parseInt(queryCascade(wrapper, "top"));
  var clientX = e.clientX;
  var clientY = e.clientY;
  wrapper.style.zIndex = doZ();
  addListener(document, "mousemove", move, true);
  addListener(document, "mouseup", drop, true);
  burst(e);
  thwart(e);
  function move(e) {
    if (!e) e = window.event;
    wrapper.style.left = left + e.clientX - clientX + "px";
    wrapper.style.top = top + e.clientY - clientY + "px";
    burst(e);
  }
  function drop(e) {
    if (!e) e = window.event;
  }
}

Writing the Mouseup Event Listener

Now for the mouseup event listener, drop(). First, we want to tell JavaScript not to listen for mousemove or mouseup events on document. This is where our helper function removeListener() earns its keep. Remember that to remove an event listener with removeEventListener() or detachEvent(), you must pass the same parameters that you added the event listener with. With this in mind, let's cut and paste our addListener calls within drag(). Then just change addListener to removeListener:

function drag(e) {
  if (!e) e = window.event;
  if (!e.target) e.target = e.srcElement;
  var wrapper = e.target.parentNode;
  var left = parseInt(queryCascade(wrapper, "left"));
  var top = parseInt(queryCascade(wrapper, "top"));
  var clientX = e.clientX;
  var clientY = e.clientY;
  wrapper.style.zIndex = doZ();
  addListener(document, "mousemove", move, true);
  addListener(document, "mouseup", drop, true);
  burst(e);
  thwart(e);
  function move(e) {
    if (!e) e = window.event;
    wrapper.style.left = left + e.clientX - clientX + "px";
    wrapper.style.top = top + e.clientY - clientY + "px";
    burst(e);
  }
  function drop(e) {
    if (!e) e = window.event;
    removeListener(document, "mousemove", move, true);
    removeListener(document, "mouseup", drop, true);
  }
}

Now then, if an element has negative left or top CSS values, browsers do not render scrollbars to make the element accessible to visitors. For this reason, if a visitor drags and drops the Running <div> beyond the browser window to the left or top, there will be no way for them to view the <div>. It's sort of like dropping the <div> down a black hole.

What to do? Well, simply query left and top, and if their values are negative, change them to 0, which will snap the <div> back into view, flush to edge of the window:

function drag(e) {
  if (!e) e = window.event;
  if (!e.target) e.target = e.srcElement;
  var wrapper = e.target.parentNode;
  var left = parseInt(queryCascade(wrapper, "left"));
  var top = parseInt(queryCascade(wrapper, "top"));
  var clientX = e.clientX;
  var clientY = e.clientY;
  wrapper.style.zIndex = doZ();
  addListener(document, "mousemove", move, true);
  addListener(document, "mouseup", drop, true);
  burst(e);
  thwart(e);
  function move(e) {
    if (!e) e = window.event;
    wrapper.style.left = left + e.clientX - clientX + "px";
    wrapper.style.top = top + e.clientY - clientY + "px";
    burst(e);
  }
  function drop(e) {
    if (!e) e = window.event;
removeListener(document, "mousemove", move, true);
    removeListener(document, "mouseup", drop, true);
    if (parseInt(wrapper.style.left) < 0) wrapper.style.left = "0px";
    if (parseInt(wrapper.style.top) < 0) wrapper.style.top = "0px";
  }
}

Finally, pass the mouseup event object in e to burst() and thwart(), and we're done:

function drag(e) {
  if (!e) e = window.event;
  if (!e.target) e.target = e.srcElement;
  var wrapper = e.target.parentNode;
  var left = parseInt(queryCascade(wrapper, "left"));
  var top = parseInt(queryCascade(wrapper, "top"));
  var clientX = e.clientX;
  var clientY = e.clientY;
  wrapper.style.zIndex = doZ();
  addListener(document, "mousemove", move, true);
  addListener(document, "mouseup", drop, true);
  burst(e);
  thwart(e);
  function move(e) {
    if (!e) e = window.event;
    wrapper.style.left = left + e.clientX - clientX + "px";
    wrapper.style.top = top + e.clientY - clientY + "px";
    burst(e);
  }
  function drop(e) {
    if (!e) e = window.event;
    removeListener(document, "mousemove", move, true);
    removeListener(document, "mouseup", drop, true);
    if (parseInt(wrapper.style.left) < 0) wrapper.style.left = "0px";
    if (parseInt(wrapper.style.top) < 0) wrapper.style.top = "0px";
    burst(e);
    thwart(e);
  }
}

The doZ() Helper Function

Now for doZ()—thanks for reminding me. This helper function will always return the next highest integer after the one it last returned. So if we set the z-index of the element the visitor is dragging to the return value of doZ(), we can be sure they will never drag it underneath another element. Yup, good idea.

Declare a variable doZ containing the return value of a self-invoking function literal. This will create a closure to save the z-index within from one doZ() call to the next:

var doZ = function() {
}();

Now initialize a private variable z to an integer greater than any z-index on your page. I dun no, say 400:

var doZ = function() {
var z = 400;
}();

Right now, doZ evaluates to undefined since the self-invoking function literal does not explicitly return a value. We have some work to do; return a function literal for JavaScript to initialize doZ to, that is, the helper function we want doZ to refer to.

var doZ = function() {
  var z = 400;
  return function() {
  };
}();

Now return the unincremented value of the private z variable. Then increment z to the value doZ() ought to return the next time we call it. To do so, place the ++ operator in the post-increment position. So the first call of doZ() returns 400 and saves 401 to z, the second call of doZ() returns 401 and saves 402 to z, the third call of doZ() returns 402 and saves 403 to z, and so on. doZ() returns z from the closure and then remembers what to return the next time.

Note

The ++ operator and the pre- and post-increment positions were covered in Chapter 3.

var doZ = function() {
  var z = 400;
  return function() {
    return z ++;
  };
}();

Before moving on, let's put doZ() up with the other helper functions, say right before prepSprites(). Yup, cut and paste.

Prepping the Drag

Now we want to tell JavaScript to listen for mousedown events on any element of the drag class. We'll do so within a function named prepDrag(), which as you might imagine will prep the drag-and-drop behavior:

function prepDrag() {
}

Now in a local variable elements, let's save any element in our markup that is a member of the drag class by passing "drag" to our findClass() helper function:

function prepDrag() {
  var elements = findClass("drag");
}

Then write an optimized for loop to iterate over elements. During each roundabout of the for loop, tell JavaScript to listen for mousedown events on elements[i], that is, every element that is a member of the drag class. To do so, pass our helper function addListener() the DOM node that elements[i] refers to, the string "mousedown", and the identifier for our drag() function.

Warning

It bears repeating that the third parameter to addListener() is an identifier naming a function, not a function invocation expression. That is to say, we are telling JavaScript the name of a function to run rather than running the function by appending the () operator.

With those words of caution, we're done:

function prepDrag() {
  var elements = findClass("drag");
  for (var i = elements.length; i --; ) {
    addListener(elements[i], "mousedown", drag);
  }
}

Now let's put our drag-and-drop behavior through the wringer. Save our JavaScript file. Then refresh Firefox, and call prepDrag() from the Firebug console. Optionally, call prepSprites() again, too; refreshing Firefox KO'd the sprite event listeners.

prepSprites();
prepDrag();

Now grab the Running tab with your mouse, and drag and drop the menu to a different spot on the page. Figure 9-4 the Running menu after I moved it from its usual starting position. Note that as you drag, your mouse stays pinned to wherever you grabbed hold of the tab.

Grabbing the menu by the tab and dragging it elsewhere on the page

Figure 9-4. Grabbing the menu by the tab and dragging it elsewhere on the page

Now drag the menu beyond the bounds of the Firefox window, either to the left or top. Once it has disappeared from view, drop it and watch JavaScript snap it flush to the edge of the Firefox window. Figure 9-5 illustrates where I dragged the Running menu to and where JavaScript snapped it back into view.

Dragging the Running menu beyond the bounds of the Firefox window.

Figure 9-5. Dragging the Running menu beyond the bounds of the Firefox window.

The Running menu snaps back into view

Figure 9-6. The Running menu snaps back into view

How did it go? I hope you're smiling like a butcher's dog. But if not, carefully compare your script to that of your fearless leader:

var addListener = document.addEventListener ?
  function(node, type, listener, phase) {
    node.addEventListener(type, listener, !! phase);
  } :
  function(node, type, listener) {
    node.attachEvent("on" + type, listener);
  } ;

var removeListener = document.removeEventListener ?
  function(node, type, listener, phase) {
    node.removeEventListener(type, listener, !! phase);
  } :
  function(node, type, listener) {
    node.detachEvent("on" + type, listener);
  } ;
function thwart(e) {
  if (e.preventDefault) {
    thwart = function(e) {
      e.preventDefault();
    };
  } else {
    thwart = function(e) {
      e.returnValue = false;
    };
  }
  thwart(e);
}

function burst(e) {
  if (e.stopPropagation) {
    burst = function(e) {
      e.stopPropagation();
    };
  } else {
    burst = function(e) {
      e.cancelBubble = true;
    };
  }
  burst(e);
}

var traverseTree = document.documentElement.firstElementChild ?
  function traverseTree (node, func) {
    func(node);
    node = node.firstElementChild;
    while (node !== null) {
      traverseTree(node, func);
      node = node.nextElementSibling;
    }
  } :
  function traverseTree (node, func) {
    func(node);
    node = node.firstChild;
    while (node !== null) {
      traverseTree(node, func);
      node = node.nextSibling;
    }
  } ;

var findClass;

if (document.getElementsByClassName) {
  findClass = function (name, root) {
    root = root || document.documentElement;
    return root.getElementsByClassName(name);
  };
} else if (document.querySelectorAll) {
  findClass = function (name, root) {
    root = root || document.documentElement;
return root.querySelectorAll("." + name);
  };
} else {
  findClass = function (name, root) {
    var found = [];
    root = root || document.documentElement;
    traverseTree(root, function(node) {
      if (!! node.className) {
        for (var names = node.className.split(/s+/), i = names.length; i --; ) {
          if (names[i] === name) {
            found.push(node);
          }
        }
      }
    });
    return found;
  };
}

var queryCascade = window.getComputedStyle ?
  function(element, property) {
    return getComputedStyle(element, null)[property];
  } :
  function(element, property) {
    return element.currentStyle[property];
  } ;

var doZ = function() {
  var z = 400;
  return function() {
    return z ++;
  };
}();

// sprite swaps

var prepSprites = window.getComputedStyle ?
  function () {
    var elements = findClass("sprite"), sprites = {};
    for (var i = elements.length, offsets = null; i --; ) {
      sprites[elements[i].id] = [];
      sprites[elements[i].id][0] = queryCascade(elements[i], "backgroundPosition");
      offsets = sprites[elements[i].id][0].split(/s+/);
      sprites[elements[i].id][1] = 1 - parseInt(queryCascade(elements[i], "width")) +
        "px " + offsets[1];
      addListener(elements[i], "mouseover", slideSprite);
      addListener(elements[i], "mouseout", slideSprite);
    }
    function slideSprite(e) {
      if (e.type == "mouseover") {
        e.target.style.backgroundPosition = sprites[e.target.id][1];
      } else {
        e.target.style.backgroundPosition = sprites[e.target.id][0];
      }
}
  } :
  function () {
    var elements = findClass("sprite"), sprites = {};
    for (var i = elements.length, offsets = null; i --; ) {
      sprites[elements[i].id] = [];
      offsets = [queryCascade(elements[i], "backgroundPositionX"),
        queryCascade(elements[i], "backgroundPositionY")];
      sprites[elements[i].id][0] = offsets.join(" ");
      sprites[elements[i].id][1] = 1 - parseInt(queryCascade(elements[i], "width")) +
        "px " + offsets[1];
      addListener(elements[i], "mouseover", slideSprite);
      addListener(elements[i], "mouseout", slideSprite);
    }
    function slideSprite() {
      var e = window.event;
      if (e.type == "mouseover") {
        e.srcElement.style.backgroundPosition = sprites[e.srcElement.id][1];
      } else {
        e.srcElement.style.backgroundPosition = sprites[e.srcElement.id][0];
      }
    }
  } ;

// drag and drop

function drag(e) {
  if (!e) e = window.event;
  if (!e.target) e.target = e.srcElement;
  var wrapper = e.target.parentNode;
  var left = parseInt(queryCascade(wrapper, "left"));
  var top = parseInt(queryCascade(wrapper, "top"));
  var clientX = e.clientX;
  var clientY = e.clientY;
  wrapper.style.zIndex = doZ();
  addListener(document, "mousemove", move, true);
  addListener(document, "mouseup", drop, true);
  burst(e);
  thwart(e);

  function move(e) {
    if (!e) e = window.event;
    wrapper.style.left = left + e.clientX - clientX + "px";
    wrapper.style.top = top + e.clientY - clientY + "px";
    burst(e);
  }

  function drop(e) {
    if (!e) e = window.event;
    removeListener(document, "mousemove", move, true);
    removeListener(document, "mouseup", drop, true);
    if (parseInt(wrapper.style.left) < 0) wrapper.style.left = "0px";
    if (parseInt(wrapper.style.top) < 0) wrapper.style.top = "0px";
    burst(e);
thwart(e);
  }
}

function prepDrag() {
  var elements = findClass("drag");
  for (var i = elements.length; i --; ) {
    addListener(elements[i], "mousedown", drag);
  }
}

Swapping Skins by Key

Now for a skin-swapping behavior. This one will swap the skin to fuchsia if the visitor presses f on their keyboard, to green if they press g, and back to blue if they press b. Note that case does not matter. So for example, f or F will swap the skin to fuchsia. To implement this behavior, we want to have JavaScript listen for keypress events. They can tell you the character that would normally print in response to pressing a key or combination of keys. That is to say, a keypress event can differentiate between an r and an R even though they share the same key on a typical keyboard.

Insofar as we want to respond to every keypress event, we will bind the skin-swapping event listener to document, which as you know is omniscient. For Internet Explorer 9, Firefox, Safari, Chrome, and Opera, JavaScript will nip the keypress in the bud by listening during the capturing phase. On the other hand, Internet Explorer 8 or earlier will listen during the bubbling phase. So, Internet Explorer 8 or earlier will have to wait for the event to traverse the DOM tree, but Internet Explorer 9, Firefox, Safari, Chrome, and Opera will not.

The rough framework for this behavior will be a keypress event listener named swapSkinByKey() nested inside a preparatory function named prepSkinKeys(). In this way, swapSkinByKey() can query the local variables of prepSkinKeys() even after prepSkinKeys() has returned. Note that prepSkinKeys() defines no parameters, but swapSkinByKey() defines e for the keypress event object. Thus far we have this:

function prepSkinKeys() {
  function swapSkinByKey(e) {
  }
}

Now declare a local variable named sheet referring to the skin style sheet— the one containing the CSS rules that vary among the blue, fuchsia, and green skins. swapSkinByKey() may then query sheet even after prepSkinKeys() has returned. That's a good thing, since prepSkinKeys() will only run one time, right after the page loads:

function prepSkinKeys() {
  var  sheet = document.getElementById("skin");
  function swapSkinByKey(e) {
  }
}

On to the nested event listener swapSkinByKey(). Begin with a couple of statements to ensure e refers to the keypress event object and that e.target refers to the DOM node the keypress took place on:

function prepSkinKeys() {
  var  sheet = document.getElementById("skin");
  function swapSkinByKey(e) {
    if (!e) e = window.event;
if (!e.target) e.target = e.srcElement;
  }
}

Since we do not want to swap skins if the visitor is typing some text in a form (wouldn't that be bizarre!), let's terminate swapSkinByKey() if e.target is an <input> or <textarea> element:

function prepSkinKeys() {
  var   sheet = document.getElementById("skin");
  function swapSkinByKey(e) {
    if (!e) e = window.event;
    if (!e.target) e.target = e.srcElement;
    if (e.target.nodeName.toLowerCase() === "input" ||
      e.target.nodeName.toLowerCase() === "textarea") return;
  }
}

If JavaScript gets this far, we really do want to swap skins. To do so, we will convert the ASCII value of the key the visitor pressed to a string by way of String.fromCharCode(). In turn, we will then convert that string to lowercase with String.toLowerCase(). Flip back to Chapter 2 if you have forgotten how those two methods work.

Like me, you probably do not know the ASCII values for b, f, g, B, F, and G off hand. Table 9-1 contains the lowdown.

Table 9-1. ASCII Values for b, f, g, B, F, and G

ASCII Value

Printable Character

66

B

70

F

71

G

98

b

102

f

103

g

Now we just need to query the keypress event object for the ASCII value of the key the visitor pressed. It's in e.charCode for Firefox and Safari but e.keyCode for Internet Explorer, Opera, and Safari, so either one will do for Safari! On the other hand, e.keyCode will be 0 in Firefox while e.charCode will be undefined in Internet Explorer and Opera. Since 0 and undefined are both falsy, we can grock the ASCII value of the keypress cross-browser with the expression e.charCode || e.keyCode. We will get the ASCII value from charCode in Firefox and Safari, but from keyCode in Internet Explorer and Opera. As to why things have to be so muddled, I have no idea.

Let's add a letter member to the keypress event object. Then save the converted ASCII value to e.letter:

function prepSkinKeys() {
  var  sheet = document.getElementById("skin");
  function swapSkinByKey(e) {
    if (!e) e = window.event;
    if (!e.target) e.target = e.srcElement;
if (e.target.nodeName.toLowerCase() === "input" ||
      e.target.nodeName.toLowerCase() === "textarea") return;
    e.letter = String.fromCharCode(e.charCode || e.keyCode).toLowerCase();
  }
}

Now we want to swap skins (or do nothing) relative to the string in e.letter. Let's go with the else if idiom for the job. Insofar as the initial skin is blue, let's optimize things by making b the third choice. Don't want to make it the default, though. Otherwise, every key other than f or g would be a shortcut for the blue skin!

I dun no, of the other two, fuchsia probably would be more popular. So, let's put that before green. Then in the event the visitor did not press f, g, or b, we want to do nothing. To do so, put a naked return in an else clause or omit the else clause entirely. Let's go with the former to make our intentions clear:

function prepSkinKeys() {
  var  sheet = document.getElementById("skin");
  function swapSkinByKey(e) {
    if (!e) e = window.event;
    if (!e.target) e.target = e.srcElement;
    if (e.target.nodeName.toLowerCase() === "input" ||
      e.target.nodeName.toLowerCase() === "textarea") return;
    e.letter = String.fromCharCode(e.charCode || e.keyCode).toLowerCase();
    if (e.letter === "f") {
      sheet.href = "fuchsia.css";
    } else if (e.letter === "g") {
      sheet.href = "green.css";
    } else if (e.letter === "b") {
      sheet.href = "blue.css";
    } else {
      return;
    }
  }
}

We're done with swapSkinByKey(). Now we want to tell JavaScript to run it whenever a keypress occurs:

function prepSkinKeys() {
  var  sheet = document.getElementById("skin");
  function swapSkinByKey(e) {
    if (!e) e = window.event;
    if (!e.target) e.target = e.srcElement;
    if (e.target.nodeName.toLowerCase() === "input" ||
      e.target.nodeName.toLowerCase() === "textarea") return;
    e.letter = String.fromCharCode(e.charCode || e.keyCode).toLowerCase();
    if (e.letter === "f") {
      sheet.href = "fuchsia.css";
    } else if (e.letter === "g") {
      sheet.href = "green.css";
    } else if (e.letter === "b") {
      sheet.href = "blue.css";
    } else {
      return;
    }
}
  addListener(document, "keypress", swapSkinByKey, true);
}

Insofar as a skin swap requires a repaint and possibly a reflow, optimizing swapSkinByKey() as either a lazy loader or advance conditional loader is not worthwhile. Compared to the time a browser takes to reflow the render tree and repaint the page, the time JavaScript takes to run swapSkinByKey() is insignificant. Let's leave swapSkinByKey() the way it is and move on to testing.

You know what to do. Refresh Firefox, and then use the Firebug console to run the three prepatory functions like so:

prepSprites();
prepDrag();
prepSkinKeys();

Now click somewhere within the Firefox window (so that you do not continuing typing in the Firebug console), and press f or F to swap the skin from blue to fuchsia, verifying your work with Figure 9-7. Then press g or G to change the skin to green and b or B to revert to the initial blue skin.

Pressing f or F swaps the skin from blue to fuchsia (this may not be so obvious in a black-and-white book, but take my word for it).

Figure 9-7. Pressing f or F swaps the skin from blue to fuchsia (this may not be so obvious in a black-and-white book, but take my word for it).

In the unlikely event that things did not go according to plan, take a deep breath, and verify your script with that of the fella next to you.

Initiating Behaviors When the DOM Tree Is Available

To load our behaviors, we want JavaScript to run prepSprites(), prepDrag(), and prepSkinKeys(). However, to prevent errors, we don't want to run these functions until the DOM tree is fully available. How do we know when that is?

Simple, window will be the target of a load event when the DOM tree is fully available. Moreover, when that load event takes place, you know that the browser has loaded and parsed all markup, CSS, JavaScript, and images.

Insofar as the load event listener for window will simply invoke prepSprites(), prepDrag(), and prepSkinKeys(), a function literal will do. So in our JavaScript file, let's call addListener() like so:

addListener(window, "load", function() {
    prepSprites();
    prepDrag();
    prepSkinKeys();
  });

Save our JavaScript file, and then refresh Firefox. This time around there's no need manually call prepSprites(), prepDrag(), and prepSkinKeys() from the Firebug console.

Fighting Global Evil

One final touch to wrap things up. Rather than risk that another script will overwrite our functions, or vice versa, let's paste our script (yup, the whole enchilada) into the body of a self-invoking function literal that is wrapped with parentheses. Doing so creates a module, a global abatement technique covered in Chapter 6. Pretty simple to do, too. Just take a look at the first and last lines, which are in bold, of the final code for our script below.

Then save our JavaScript file, refresh Firefox, and test the behaviors one last time.

(function() {
var addListener = document.addEventListener ?
  ...

var removeListener = document.removeEventListener ?
  ...

function thwart(e) {
  ...
}
function burst(e) {
  ...
}

var traverseTree = document.documentElement.firstElementChild ?
  ...

var findClass;
if (document.getElementsByClassName) {
...
} else if (document.querySelectorAll) {
  ...
} else {
  ...
}

var queryCascade = window.getComputedStyle ?
  ...

var doZ = function() {
  ...
}();

// sprite swaps
var prepSprites = window.getComputedStyle ?
  ...

// drag and drop
function drag(e) {
  ...
}
function prepDrag() {
  ...
}

// swap skins by key
function prepSkinKeys() {
  ...
}

// load behaviors when the DOM tree is fully available
addListener(window, "load", function() {
    prepSprites();
    prepDrag();
    prepSkinKeys();
  });
})();

Summary

In this chapter, we explored how to have JavaScript listen for an event that occurs on an element or its ancestors and in turn respond by running a function referred to as an event listener. Moreover, we covered a couple of techniques to make an event listener or its supporting functions snappier. Those that would run straightaway when a page loads were rewritten as advance conditional loaders, while those that run later in time were rewritten as lazy loaders.

In the next chapter, we will add some BOM features to this script—things like timers for animations and Ajax for dynamic content. It's going to be a real hootenanny!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.28.179