Chapter 10. Scripting BOM

A while ago, I worked a scene in the Russell Crowe and Liam Neeson film The Next Three Days. Probably the thing that surprised me most was how dynamic things were on set. Paul Haggis, who won two Oscars for Crash, kept reworking the content of the scene, especially the timing; he must have had Jason Beghe (Detective Quinn) run out of every single door in the precinct and say, "We found his son!" each time. Oftentimes Haggis would tell us what to do just moments before filming another take. Needless to say, having a good short-term memory was vital.

Now in JavaScript, those things (dynamic content, timing, short-term memory) are the purview of the Browser Object Model (BOM). For dynamic content, there's XMLHttpRequest, an object for loading data at runtime without having to refresh the page, a technique called Ajax. Then for timing things like animations, BOM provides four timer functions: setTimeout(), setInterval(), clearTimeout(), and clearInterval(). Finally, in order to give a browser a memory, BOM provides cookies. You might have heard of those.

In this chapter, Ajax, timers, and cookies are on the docket. These are pretty disparate features, but that's the way with BOM, which isn't a standard, but rather a hodgepodge of initially proprietary features that are now implemented by Internet Explorer, Firefox, Safari, and Opera. For example, XMLHttpRequest began life as a proprietary Internet Explorer feature.

Downloading the Project Files

Prior to rolling up our sleeves and coding, download the project files from www.apress.com. There are quite a few of them this time. Let's take a peek at the markup in ten.html, displayed here. Later in the chapter, we will turn the branch of the DOM tree beginning with <div class="scroller" id="s1"> into an animated gallery. Then we'll dynamically add additional galleries by way of Ajax.

<!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>Getting StartED with JavaScript</title>
<link rel="stylesheet" type="text/css" href="ten.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>
<div class="scroller" id="s1">
  <div class="wrapper">
    <ul class="slide">
      <li><a href="ten.html"><img alt="Nike LunaRacer" src="images/lunaracer.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Glide, Boston" src="images/glide_bos.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Glide, NYC" src="images/glide_nyc.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Mariah" src="images/mariah.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Fly, Orange" src="images/fly_org.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Fly, Black" src="images/fly_blk.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Elite" src="images/elite.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Zoom Vomero" src="images/vomero.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Air Max" src="images/max.jpg" /></a></li>
    </ul>
  </div>
  <div class="left arrow sprite"></div>
  <div class="right arrow sprite"></div>
</div>
<script src="ten.js" type="text/javascript"></script>
</body>
</html>

Note that the three sprites now have four arrows (as displayed in Figure 10-1). We'll need those to scroll the galleries.

The blue, fuchsia, and green sprites now have four arrows.

Figure 10-1. The blue, fuchsia, and green sprites now have four arrows.

Remembering Visitor Data with Cookies

In Chapter 9, we wrote a skin-swapping behavior. However, if you click Refresh in Firefox to reload ten.html, JavaScript totally forgets which skin you preferred. Similarly, if you close Firefox, the next time you open ten.html, the skin reverts to blue, which is the default.

So, we need to find a way to give JavaScript a memory. For this, BOM provides cookies. Our goal will be to save a visitor's skin preference to a cookie. That way, we can preset the skin to their favorite skin. We ought to make them feel welcome. However, working with cookies is not entirely straightforward.

On the one hand, writing a cookie is simple. Just assign name value pairs, separated by semicolons, to document.cookie, and you're done. Here are a couple of name-value pairs:

name=john;preference=blue

On the other hand, reading a cookie is a nightmare. JavaScript returns the name-value pair of every cookie, joined by semicolons, in one long string. It's sort of like the ticker tapes that stock exchanges used to work with. You have to search through all those cookies for the one you want. That's ridiculous, of course. But with BOM, ridiculous is the status quo.

With this in mind, it makes sense to write a helper function to read the value of a cookie. There's no need to write a second one to write the value of a cookie, since that is simple enough.

Getting the User's Preference

Open ten.js, which right now is just the final code for nine.js, in your preferred text editor. Scroll down to just past doZ(), the last helper function we coded in Chapter 9, and insert a new helper function named getCookie(). Then define a name parameter, which will contain the name of the cookie to clip from the ticker-tape string in document.cookie:

function getCookie(name) {
}

Now define some local variables:

  • batch will contain the ticker-tape string in document.cookie.

  • i will contain the return value of passing name concatenated to = to String.indexOf(). Remember from Chapter 2 that this wrapper method returns the offset from the beginning of the string or −1 to convey failure. i will enable us to clip our cookie from batch.

  • Just let firstCut and secondCut default to undefined for now.

Thus far we have this:

function getCookie(name) {
  var batch = document.cookie, i, firstCut, secondCut;
  i = batch.indexOf(name + "=");
}

So if JavaScript can find our cookie in batch, i will not contain −1. Let's write an if conditional for i!=== −1 before we go any further:

function getCookie(name) {
  var batch = document.cookie, i, firstCut, secondCut;
  i = batch.indexOf(name + "=");
if (i !== −1) {
  }
}

Now that we are sure there is a cookie named with the string in the name parameter, we want to clip its value from the ticker tape. That cut will begin at the index equivalent to i, plus the character length of the string in name, plus 1 for the = sign. Remember from Chapter 2 that String.length contains the number of characters comprising a string. So, for a cookie named skin, that would be 4. Let's assign the value of the expression indicating the offset where the value of our cookie begins to firstCut:

function getCookie(name) {
  var batch = document.cookie, i, firstCut, secondCut;
  i = batch.indexOf(name + "=");
  if (i !== −1) {
    firstCut = i + name.length + 1;
  }
}

As you might imagine, secondCut will be the offset in the document.cookie ticker tape where the value of our cookie ends. That will either be the first semicolon after firstCut or the end of the string of cookies. So to find the semicolon, we would again call indexOf() on batch. But this time, we would pass firstCut as the optional second parameter, which tells JavaScript where to begin its search. Note that i would work here, too. However, passing secondCut results in a quicker match since it's closer to the semicolon than i:

function getCookie(name) {
  var batch = document.cookie, i, firstCut, secondCut;
  i = batch.indexOf(name + "=");
  if (i !== −1) {
    firstCut = i + name.length + 1;
    secondCut = batch.indexOf(";", firstCut);
  }
}

In the event that our cookie is the last one in batch, secondCut will contain −1. If that's the case, we want to overwrite −1 with the length of the string of cookies. That is to say, we want to overwrite it with batch.length:

function getCookie(name) {
  var batch = document.cookie, i, firstCut, secondCut;
  i = batch.indexOf(name + "=");
  if (i !== −1) {
    firstCut = i + name.length + 1;
    secondCut = batch.indexOf(";", firstCut);
    if (secondCut === −1) secondCut = batch.length;
  }
}

Now for the moment of truth. Clip the value of our cookie from batch by passing String.substring() the offsets in firstCut and secondCut. However, to decode any whitespace, commas, or semicolons in the cookie value, be sure to pass the return value of substring() to decodeURIComponent(). Note that cookie values may not contain any whitespace, commas, or semicolons, so it's always best to clean them out just in case. I'll remind you of that in a bit when we write the value of our cookie.

Anyway, getCookie() has done its job at this point, so let's return the cookie value like so:

function getCookie(name) {
  var batch = document.cookie, i, firstCut, secondCut;
  i = batch.indexOf(name + "=");
  if (i !== −1) {
    firstCut = i + name.length + 1;
    secondCut = batch.indexOf(";", firstCut);
    if (secondCut === −1) secondCut = batch.length;
    return decodeURIComponent(batch.substring(firstCut, secondCut));
  }
}

Finally, if getCookie() cannot find a cookie named name, let's convey failure by appending an else clause that returns false. In just a moment, we will check for false prior to presetting the skin relative to a visitor's preference. The final code for getCookie() would be as follows:

function getCookie(name) {
  var batch = document.cookie, i, firstCut, secondCut;
  i = batch.indexOf(name + "=");
  if (i !== −1) {
    firstCut = i + name.length + 1;
    secondCut = batch.indexOf(";", firstCut);
    if (secondCut === −1) secondCut = batch.length;
    return decodeURIComponent(batch.substring(firstCut, secondCut));
  } else {
    return false;
  }
}

Setting the User's Skin Preference

Now for a function to preset the skin to blue, fuchsia, or green depending on the visitor's preference. Hmm. Let's cleverly name it presetSkin(). In its block, declare a local variable named pref, and assign to it the return value of passing "skin" to getCookie():

function presetSkin() {
  var pref = getCookie("skin");
}

So, pref will contain "blue", "fuchsia", or "green" if the visitor set their preference during a previous visit. Otherwise, pref will contain false. Note that if cookies are disabled, pref will contain false as well. With this in mind, let's make sure pref does not contain false before we do anything else. An if condition and the !== operator will do the job:

function presetSkin() {
  var pref = getCookie("skin");
  if (pref !== false) {
  }
}

Fine and dandy. Now if JavaScript runs the if block, we have a skin to set. Just set the href member of the skin style sheet to "blue.css", "fuchsia.css", or "green.css" by concatenating pref to ".css":

function presetSkin() {
  var pref = getCookie("skin");
if (pref !== false) {
    document.getElementById("skin").href = pref + ".css";
  }
}

We're done with presetSkin(). The final code for that and our getCookie() helper would be as follows:

function getCookie(name) {
  var batch = document.cookie, i, firstCut, secondCut;
  i = batch.indexOf(name + "=");
  if (i !== −1) {
    firstCut = i + name.length + 1;
    secondCut = batch.indexOf(";", firstCut);
    if (secondCut === −1) secondCut = batch.length;
    return decodeURIComponent(batch.substring(firstCut, secondCut));
  } else {
    return false;
  }
}
// some intervening code
function presetSkin() {
  var pref = getCookie("skin");
  if (pref !== false) {
    document.getElementById("skin").href = pref + ".css";
  }
}

Setting the User's Preference

We can now read the skin cookie whenever the visitor returns. But it's not very useful unless we create the skin cookie elsewhere in our script. Let's do so by rewriting swapSkinByKey(), the keypress event listener we cobbled together in Chapter 9. That's nested in prepSkinKeys(). Right now we have this:

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);
}

First declare a local variable named pref prior to the if statement. Then replace the three href assignment statements with pref ones:

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();
    var pref;
    if (e.letter === "f") {
      pref = "fuchsia";
    } else if (e.letter === "g") {
      pref = "green";
    } else if (e.letter === "b") {
      pref = "blue";
    } else {
      return;
    }
  }
  addListener(document, "keypress", swapSkinByKey, true);
}

Following the else clause, reinsert the href assignment. However, we can now do so in one fell swoop by concatenating pref to ".css":

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();
    var pref;
    if (e.letter === "f") {
      pref = "fuchsia";
    } else if (e.letter === "g") {
      pref = "green";
    } else if (e.letter === "b") {
      pref = "blue";
    } else {
      return;
    }
    sheet.href = pref + ".css";
  }
  addListener(document, "keypress", swapSkinByKey, true);
}

Now for the reason why we gutted swapSkinByKey() in the first place. Yup, it's time to create or write the skin cookie. Both of those operations work the same way. Just cobble together a string to assign to document.cookie. Note that this does not overwrite any cookies already in there. I know, it's not very intuitive. Such is the sad state of BOM. Anyway, just concatenate "skin=" to pref:

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();
    var pref;
    if (e.letter === "f") {
      pref = "fuchsia";
    } else if (e.letter === "g") {
      pref = "green";
    } else if (e.letter === "b") {
      pref = "blue";
    } else {
      return;
    }
    sheet.href = pref + ".css";
    document.cookie = "skin=" + pref;
  }
  addListener(document, "keypress", swapSkinByKey, true);
}

There's one problem with what we did. Although our cookie would survive a refresh, it would be deleted when the visitor closes their browser. That's because we created a session cookie, meaning one with no sell by date.

Let's fix that by setting the optional max-age attribute, the value for which is the life span of the cookie in seconds. Like me, you probably do not know off-hand the number of seconds in a week, month, year, and so forth. Therefore, let's let JavaScript do the math for us. Say for a 30-day cookie, we would write this:

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();
    var pref;
    if (e.letter === "f") {
      pref = "fuchsia";
    } else if (e.letter === "g") {
      pref = "green";
    } else if (e.letter === "b") {
      pref = "blue";
    } else {
      return;
    }
    sheet.href = pref + ".css";
    document.cookie = "skin=" + pref + "; max-age=" + (60*60*24*30);
  }
  addListener(document, "keypress", swapSkinByKey, true);
}

As it is right now, presetSkin() will never run. To fix that, simply call presetSkin() first thing in the load event listener:

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

Now let's test this in Firefox. Save ten.js, and then open ten.html with Firefox. Press f or F to swap the skin to fuchsia, and then click Refresh to make sure the skin remains fuchsia.

Now exit Firefox to end your browsing session. Then reopen ten.html to see whether our sell by date worked as planned. Skin still green?

Great. There you have it—a script with a memory!

Animating with Timers

Timers are the next BOM feature we will explore. Those are the cornerstone JavaScript animations. Scrollers are one kind of animated behavior. Let's add one of those to our script so that visitors can scroll right or left through an image gallery of running shoes. But before we do, download ten.html, ten.css, and images folder from www.apress.com (and refer to the listing of ten.html at the start of this chapter for the layout of the elements we're going to work with in this section, particularly the wrapper and scroller parts). In addition to images for the gallery, revised blue, fuchsia, and green sprites are in the images folder, too. There's no need to download ten.js, though, since we are coding that over the course of this chapter.

Preparing the Scrollers

Beneath prepSkinKeys(), add a function named prepScrollers(). Then assign to a local variable named elements the return value of passing "scroller" to the helper function, findClass(). There will be one element per scrolling gallery in there.

function prepScrollers() {
  var elements = findClass("scroller");
}

Now iterate over elements with a for loop. To make that snappier (remember, the browser is frozen while JavaScript is doing something), we will query elements.length just one time, saving that to the venerable loop variable i. Then we will decrement i in the test expression. That is to say, we will loop through elements in reverse and omit the third expression, which is traditionally where i would be incremented. Just remember to put a semicolon following i -- in order to prevent a syntax error.

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
  }
}

During each roundabout of the for loop, we'll pass elements[i] to a function literal that defines a scroller argument, which is the div that contains the scroller panel. But we cannot call it until it is defined. So, wrap the function literal in parentheses, like so:

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
    })(elements[i]);
  }
}

Now we are going to save some local variables to the call object of the function literal. Closure will make those persistent and private to some nested functions we will code in a moment or two.

The first private variable, wrapper, will contain the descendent of scroller that is of the wrapper class. Similarly, slide will contain the descendent of scroller that is of the slide class. So in both cases, we pass scroller as the value of the root parameter to findClass(). However, its return value is an array, so grab the first and only element by passing 0 to the [] operator:

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
      var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
    })(elements[i]);
  }
}

Querying computed styles from the CSS cascade forces the browser to flush its rendering queue and do any pending reflow or repaint. That's not something you want to do very often, unless of course you like to torment visitors by freezing their browser. So, let's save the width of wrapper and slide, which are values we will need to query every 15 milliseconds as the gallery is animating, to private variables named w1 and w2. Note that JavaScript can look up tersely named variables like w1 faster than more readable ones like wrapperWidth. Finally, let's clip off the "px" from w1 and w2 with parseInt() while we are here:

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
      var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
      var w2 = parseInt(queryCascade(slide, "width"));
    })(elements[i]);
  }
}

Now initialize a timer variable to null. This will later contain a timer ID that we will need in the event we want to defuse a timer before it goes off. Just picture a timer ID as the trigger code some bomb-squad guy tries frantically to crack in a movie.

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
      var w2 = parseInt(queryCascade(slide, "width"));
      var timer = null;
    })(elements[i]);
  }
}

To animate our gallery, we will increment or decrement slide.style.left. Its value is "" right now, so at some point we will have to query the CSS cascade for the computed value of left. Rather than cause an initial lurch by waiting until the beginning of an animation to query the cascade, let's do so now. Querying the cascade takes time, you know.

By the way, during the course of an animation, JavaScript will read and write slide.style.left every 15 milliseconds. Bet you couldn't do that—or anything else in .015 seconds! Don't feel bad, though. JavaScript would struggle to query left from the cascade that fast, which is why we saved it locally to slide.style.left in this step:

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
      var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
      var w2 = parseInt(queryCascade(slide, "width"));
      var timer = null;
      slide.style.left = queryCascade(slide, "left");
    })(elements[i]);
  }
}

Now we want to bind a mousedown event listener named press(), which we haven't written yet, to the arrows. So, initialize a variable named arrows to the findClass("arrow", scroller) invocation expression. Initialize the loop variable i to arrows.length and re to a regular expression for the word "right", too. We have three loop variables initialized with one var statement:

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
      var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
      var w2 = parseInt(queryCascade(slide, "width"));
      var timer = null;
      slide.style.left = queryCascade(slide, "left");
      for (var arrows = findClass("arrow", scroller), i = arrows.length, re = /right/; i -
-; ) {
        addListener(arrows[i], "mousedown", press);
      }
    })(elements[i]);
  }
}

In addition to binding press() for the mousedown event, we want to add a jump member to both <div> elements. For the one of the right class, jump will contain −10. On the other hand, jump will be 10 for the <div> of the left class. During an animation, JavaScript will add jump to slide.style.left. That is to say, pressing down on the right arrow will decrement left for slide by 10 pixels, while pressing down on the left arrow will increment left for slide by 10 pixels. That happens in just 15 milliseconds, mind you.

Anyway, this is where re earns its keep. Remember from Chapter 2 that RegExp.test() returns true if the regular expression matches the string parameter and false if not. If we pass re.test() the value of the class attribute for both arrows, it will return true for the right arrow and false for the left arrow. With this in mind, we can initialize jump to the appropriate value by making the call to RegExp.test() the boolean expression prior to the ? token of the conditional operator:

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
      var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
      var w2 = parseInt(queryCascade(slide, "width"));
      var timer = null;
      slide.style.left = queryCascade(slide, "left");
      for (var arrows = findClass("arrow", scroller), i = arrows.length, re = /right/; i -
-; ) {
        addListener(arrows[i], "mousedown", press);
        arrows[i].jump = (re.test(arrows[i].className)) ? −10 : 10;
      }
    })(elements[i]);
  }
}

Adding the Press Event Listener

Now on to the event listener press(), which will execute whenever a mousedown event fires on an arrow <div>. Define an e parameter for the DOM event object. Then if JavaScript defaults e to undefined, assign window.event to e. Remember, window.event is where Internet Explorer will save details about the mousedown event. One of those tidbits, window.event.srcElement, will refer to the left or right arrow <div>. DOM-savvy browsers call that member target. So in Internet Explorer, initialize a new window.event.target member that refers to window.event.srcElement. That way, e.target refers to an arrow <div> cross-browser:

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
      var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
      var w2 = parseInt(queryCascade(slide, "width"));
      var timer = null;
      slide.style.left = queryCascade(slide, "left");
      for (var arrows = findClass("arrow", scroller), i = arrows.length, re = /right/; i -
-; ) {
addListener(arrows[i], "mousedown", press);
        arrows[i].jump = (re.test(arrows[i].className)) ? −10 : 10;
      }

      function press(e) {
        if (!e) e = window.event;
        if (!e.target) e.target = e.srcElement;
      }
    })(elements[i]);
  }
}

Now then, the <div> that e.target refers to has a jump member containing 10 or −10. Inasmuch as JavaScript can look up a local variable faster than a nested object member, save e.target.jump to a local variable jump. While animating our particular gallery, JavaScript may need to query jump 143 times. So, saving e.target.jump to a local variable is certainly worthwhile.

Now call animate(), a function we will nest in press() in a moment. Note that animate() can query jump from the call object on press(). That's a good thing, because it will do so every 15 milliseconds! Before we code animate(), though, pass the mousedown event object to both burst() and thwart() to prevent the event from bubbling any further and a context menu from pestering Mac visitors:

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
      var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
      var w2 = parseInt(queryCascade(slide, "width"));
      var timer = null;
      slide.style.left = queryCascade(slide, "left");
      for (var arrows = findClass("arrow", scroller), i = arrows.length, re = /right/; i -
-; ) {
        addListener(arrows[i], "mousedown", press);
        arrows[i].jump = (re.test(arrows[i].className)) ? −10 : 10;
      }

      function press(e) {
        if (!e) e = window.event;
        if (!e.target) e.target = e.srcElement;
        var jump = e.target.jump;
        animate();
        burst(e);
        thwart(e);
      }
    })(elements[i]);
  }
}

Writing the Animation Function

Now nest animate() in press() so that it can query jump from the call object for press(). Note that animate() can (and will) query slide, w1, w2, and timer from the call object of the function literal press() is nested within.

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
      var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
      var w2 = parseInt(queryCascade(slide, "width"));
      var timer = null;
      slide.style.left = queryCascade(slide, "left");
      for (var arrows = findClass("arrow", scroller), i = arrows.length, re = /right/; i -
-; ) {
        addListener(arrows[i], "mousedown", press);
        arrows[i].jump = (re.test(arrows[i].className)) ? −10 : 10;
      }

      function press(e) {
        if (!e) e = window.event;
        if (!e.target) e.target = e.srcElement;
        var jump = e.target.jump;
        animate();
        burst(e);
        thwart(e);

        function animate() {
        }
      }
    })(elements[i]);
  }
}

First convert slide.style.left to a number by passing it to parseInt(). Then add jump, which will be either 10 or −10, to that number, saving the sum to a variable named x. Now we want to determine whether x is in bounds, that is, no less than w1 - w2 and no greater than 0. For our gallery that would mean an integer between −1424 and 0 inclusive. If x falls within those bounds, we want to concatenate "px" to x, which converts it to a string—remember CSS values are all of the string datatype—then assign that to slide.style.left.

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
      var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
      var w2 = parseInt(queryCascade(slide, "width"));
      var timer = null;
      slide.style.left = queryCascade(slide, "left");
for (var arrows = findClass("arrow", scroller), i = arrows.length, re = /right/; i -
-; ) {
        addListener(arrows[i], "mousedown", press);
        arrows[i].jump = (re.test(arrows[i].className)) ? −10 : 10;
      }

      function press(e) {
        if (!e) e = window.event;
        if (!e.target) e.target = e.srcElement;
        var jump = e.target.jump;
        animate();
        burst(e);
        thwart(e);

        function animate() {
          var x = parseInt(slide.style.left) + jump;
          if (x >= w1 - w2 && x <= 0) {
            slide.style.left = x + "px";
          }
        }
      }
    })(elements[i]);
  }
}

But what if x is too negative, which is to say less than −1424, or at all positive? In the former case, we want to assign "−1424px" to slide.style.left, and in the latter case we want to assign "0px". Let's make that happen by way of the else if idiom, which we explored in Chapter 4.

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
      var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
      var w2 = parseInt(queryCascade(slide, "width"));
      var timer = null;
      slide.style.left = queryCascade(slide, "left");
      for (var arrows = findClass("arrow", scroller), i = arrows.length, re = /right/; i -
-; ) {
        addListener(arrows[i], "mousedown", press);
        arrows[i].jump = (re.test(arrows[i].className)) ? −10 : 10;
      }

      function press(e) {
        if (!e) e = window.event;
        if (!e.target) e.target = e.srcElement;
        var jump = e.target.jump;
        animate();
        burst(e);
        thwart(e);

        function animate() {
var x = parseInt(slide.style.left) + jump;
          if (x >= w1 - w2 && x <= 0) {
            slide.style.left = x + "px";
          } else if (x < w1 - w2) {
            slide.style.left = w1 - w2 + "px";
          } else {
            slide.style.left = "0px";
          }
        }
      }
    })(elements[i]);
  }
}

Using the Gallery

Now let's put the gallery through the wringer. To do so, scroll down and add prepScrollers() to the load event listener:

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

Then save ten.js, refresh Firefox, and press the right arrow.

Hmm. It just nudges the gallery by 10 pixels. We'd have to press and release the right arrow 143 times to scroll to the end!

Maybe you didn't notice, but when we pressed on the right arrow, it swapped to the down version of the left arrow. And when we let go, it swapped to the up version of the left arrow. But if you press and release the left arrow, its sprite remains correct. Great googly-moogly, what's going on?

Well, the two arrow <div> elements do not have an ID, which our sprite-swapping behavior relies on. Actually, that's not entirely true. The arrow <div> elements both have an ID of "", which is the empty string is the default value for the ID. Moreover, you can name an object member with any string, including "". So when prepSprites() ran, it first added a member named "" to the sprites object with offsets for the right arrow. Then it overwrote "" with offsets for the left arrow. So in the slideSprite() event listener function, sprites[e.target.id] refers to sprites[""] for both arrow <div> elements. The code is shown here for your reference:

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, we have two issues to fix. First, we want JavaScript to animate rather than nudge the gallery. Second, we want the arrow sprites to swap correctly.

Animating the Gallery

To eliminate the nudging bugaboo, we'll add a timer to move the animation along. BOM provides two kinds: window.setTimeout() runs a function after a certain number of milliseconds have elapsed, and window.setInterval() runs a function in intervals of a certain number of milliseconds. Regardless of which timer you go with, the parameters are the same. The first one is the function to run, and the second is the number of milliseconds to wait.

I tend to favor setTimeout() over setInterval() for the reason that JavaScript will not honor the call to setInterval() in the event that the last task setInterval() added to the UI queue is still in there. This behavior can result in jerky animations.

So setTimeout() it is. If x is within bounds, we will tell JavaScript to run animate() again in 15 milliseconds. It is always preferable to recurse by way of arguments.callee, which refers to the function that is running, than to do so with an identifier like animate. With this in mind, let's modify animate() like so:

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
      var w2 = parseInt(queryCascade(slide, "width"));
      var timer = null;
      slide.style.left = queryCascade(slide, "left");
      for (var arrows = findClass("arrow", scroller), i = arrows.length, re = /right/; i -
-; ) {
        addListener(arrows[i], "mousedown", press);
        arrows[i].jump = (re.test(arrows[i].className)) ? −10 : 10;
      }

      function press(e) {
        if (!e) e = window.event;
        if (!e.target) e.target = e.srcElement;
        var jump = e.target.jump;
        animate();
        burst(e);
        thwart(e);

        function animate() {
          var x = parseInt(slide.style.left) + jump;
          if (x >= w1 - w2 && x <= 0) {
            slide.style.left = x + "px";
            setTimeout(arguments.callee, 15);
          } else if (x < w1 - w2) {
            slide.style.left = w1 - w2 + "px";
          } else {
            slide.style.left = "0px";
          }
        }
      }
    })(elements[i]);
  }
}

Save ten.js, and then refresh ten.html in Firefox. Press down on the right arrow until a few images have scrolled by, and then let go.

Did the gallery keep right on rolling like a runaway train? It did so for me, too. Try the left arrow. It has the same problem.

So, we need to tell JavaScript to stop scrolling the gallery whenever a visitor releases one of the arrows. That, and we still need to fix the screwy sprites. Sigh.

OK, the sprites will have to wait. To fix the runaway gallery thingy, we need to employ the services of window.clearTimeout(). If we pass that the return value of setTimeout(), it will call off the hounds.

So, the first thing we have to do is save the return value of setTimeout() to timer, the variable we saved to the call object of the function literal that press() and in turn animate() are nested within.

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
      var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
var w2 = parseInt(queryCascade(slide, "width"));
      var timer = null;
      slide.style.left = queryCascade(slide, "left");
      for (var arrows = findClass("arrow", scroller), i = arrows.length, re = /right/; i -
-; ) {
        addListener(arrows[i], "mousedown", press);
        arrows[i].jump = (re.test(arrows[i].className)) ? −10 : 10;
      }

      function press(e) {
        if (!e) e = window.event;
        if (!e.target) e.target = e.srcElement;
        var jump = e.target.jump;
        animate();
        burst(e);
        thwart(e);

        function animate() {
          var x = parseInt(slide.style.left) + jump;
          if (x >= w1 - w2 && x <= 0) {
            slide.style.left = x + "px";
            timer = setTimeout(arguments.callee, 15);
          } else if (x < w1 - w2) {
            slide.style.left = w1 - w2 + "px";
          } else {
            slide.style.left = "0px";
          }
        }
      }
    })(elements[i]);
  }
}

If you are curious as to what the return value of setTimeout() is, don't be. It's an opaque value referred to as a timer ID. Typically this will be a number, but there's no standard saying what it should be. Anything goes. Note that you snuff out a setInterval() timer in a similar way by passing its return value to window.clearInterval(). So, BOM provides two pairs of timer functions—four in all. Don't mix and match, or you'll come to grief.

Now where were we? Right, call off the timer to fix the runaway train bug. But where? In a function named release() that we will temporarily bind to document whenever press() is called. So that the release identifier resolves faster, let's nest release() in press(). That way, it'll be on the first variable object in the scope chain.

release() will do two things. First, it will call off the hounds by passing timer to clearTimeout(). Second, it will resign its position. In other words, it will remove the mouseup event listener from document. Note that we bind the mouseup event listener to document so that if the visitor's mouse drifts off the arrow before they let go, the animation will still stop. Passing true as the optional fourth parameter puts the brakes on sooner in DOM savvy browsers. Note too that once we stop the animation, we don't want document running release() whenever subsequent mouseup events take place elsewhere on the page. This is why we have release() resign after calling clearTimeout():

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
      var w2 = parseInt(queryCascade(slide, "width"));
      var timer = null;
      slide.style.left = queryCascade(slide, "left");
      for (var arrows = findClass("arrow", scroller), i = arrows.length, re = /right/; i -
-; ) {
        addListener(arrows[i], "mousedown", press);
        arrows[i].jump = (re.test(arrows[i].className)) ? −10 : 10;
      }

      function press(e) {
        if (!e) e = window.event;
        if (!e.target) e.target = e.srcElement;
        addListener(document, "mouseup", release, true);
        var jump = e.target.jump;
        animate();
        burst(e);
        thwart(e);

        function animate() {
          var x = parseInt(slide.style.left) + jump;
          if (x >= w1 - w2 && x <= 0) {
            slide.style.left = x + "px";
            timer = setTimeout(arguments.callee, 15);
          } else if (x < w1 - w2) {
            slide.style.left = w1 - w2 + "px";
          } else {
            slide.style.left = "0px";
          }
        }

        function release(e) {
          clearTimeout(timer);
          removeListener(document, "mouseup", release, true);
        }
      }
    })(elements[i]);
  }
}

Now let's test our revision. Save ten.js, refresh ten.html in Firefox, and press down on the right arrow until a few images scroll by. Then let go. Did the gallery stop on a dime? Great.

Now press down again on the right arrow, move your mouse off of the arrow, and let go. Did it work that way too?

This is pretty good as it is. But it won't take but a moment for us to have the gallery stop scrolling whenever a visitor moves their mouse off the arrow without previously letting up on their mouse. Just duplicate the addListener() and removeListener() calls, changing just the second parameter from "mouseup" to "mouseout" like so:

function prepScrollers() {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
(function (scroller) {
      var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
      var w2 = parseInt(queryCascade(slide, "width"));
      var timer = null;
      slide.style.left = queryCascade(slide, "left");
      for (var arrows = findClass("arrow", scroller), i = arrows.length, re = /right/; i -
-; ) {
        addListener(arrows[i], "mousedown", press);
        arrows[i].jump = (re.test(arrows[i].className)) ? −10 : 10;
      }

      function press(e) {
        if (!e) e = window.event;
        if (!e.target) e.target = e.srcElement;
        addListener(document, "mouseup", release, true);
        addListener(document, "mouseout", release, true);
        var jump = e.target.jump;
        animate();
        burst(e);
        thwart(e);

        function animate() {
          var x = parseInt(slide.style.left) + jump;
          if (x >= w1 - w2 && x <= 0) {
            slide.style.left = x + "px";
            timer = setTimeout(arguments.callee, 15);
          } else if (x < w1 - w2) {
            slide.style.left = w1 - w2 + "px";
          } else {
            slide.style.left = "0px";
          }
        }

        function release(e) {
          clearTimeout(timer);
          removeListener(document, "mouseup", release, true);
          removeListener(document, "mouseout", release, true);
        }
      }
    })(elements[i]);
  }
}

Now save ten.js, refresh ten.html in Firefox, and press on the right arrow. Then after a few images scroll by, move your mouse off the right arrow without letting go of your mouse button. Did the animation halt nonetheless, just like we wanted? For me, too.

The gallery is good to go. Now let's fix those screwy sprites.

Swapping Sprites by ID or Class

Oftentimes a number of elements will share the same parts of a sprite. In an e-commerce site, for example, every Add to Cart link would share the same off and over image. If you have several JavaScript scrollers on a page, the same thing goes for the arrow sprites.

One way to fix this would be to give each element a unique ID, say add_to_cart_01 through add_to_cart_72. In addition to being error prone and inefficient, that would be fairly ridiculous.

Numbering ID values won't do. However, swapping sprites by class would be quite elegant. Identically styled elements typically are of the same class. So, swapping their sprites by class makes a good deal of sense.

That's what we'll do then. It's pretty simple to modify prepSprites() and swapSprites(). In the var bit of the for loop, declare a member variable. Then in the beginning of the for block, initialize member to id or className by way of the || operator. If id contains "", which is falsey, the || returns the value of the class attribute, which is a string. For our arrows, that would be "left arrow sprite" or "right arrow sprite". Insofar as an object member may be named with any string, including "", we'll next name a member in sprites with one of those two class strings, but only if sprites does not already have a member member. In that event, we'll calculate offsets just like in Chapter 9 except that we need to replace sprites[elements[i].id] with sprites[member] inasmuch as members are not necessarily named by ID anymore. So, there are four replacements in the DOM version and three in the Internet Explorer version:

var prepSprites = window.getComputedStyle ?
  function () {
    var elements = findClass("sprite"), sprites = {};
    for (var i = elements.length, offsets = null, member; i --; ) {
      member = elements[i].id || elements[i].className;
      if (! sprites[member]) {
        sprites[member] = [];
        sprites[member][0] = queryCascade(elements[i], "backgroundPosition");
        offsets = sprites[member][0].split(/s+/);
        sprites[member][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, member; i --; ) {
      member = elements[i].id || elements[i].className;
      if (! sprites[member]) {
        sprites[member] = [];

        offsets = [queryCascade(elements[i], "backgroundPositionX"), queryCascade(elements[i],
"backgroundPositionY")];
sprites[member][0] = offsets.join(" ");
        sprites[member][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];
      }
    }
  } ;

Why didn't we put the addListener() invocations in the if block, too? Regardless of whether we save off and over offsets for a sprite, we still want it to have a sprite-swapping behavior. For example, if you have three scrollers on a page, as we will by the end of the day, you want all three left arrows to run slideSprite() for mouseover and mouseout events. However, if we were to put the addListener() invocations in the if block, only one pair of arrows would run slideSprite() for mouseover and mouseout events.

Hmm. I don't like the sound of that either.

Now in the DOM version of slideSprite(), replace e.target.id with e.target.id || e.target.className in two places. That way, if id contains "", then JavaScript will query sprites by the string in className. Similarly renovate the Internet Explorer version, replacing e.srcElement.id with e.srcElement.id || e.srcElement.className, and you're done:

var prepSprites = window.getComputedStyle ?
  function () {
    var elements = findClass("sprite"), sprites = {};
    for (var i = elements.length, offsets = null, member; i --; ) {
      member = elements[i].id || elements[i].className;
      if (! sprites[member]) {
        sprites[member] = [];
        sprites[member][0] = queryCascade(elements[i], "backgroundPosition");
        offsets = sprites[member][0].split(/s+/);
        sprites[member][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 || e.target.className][1];
      } else {
        e.target.style.backgroundPosition = sprites[e.target.id || e.target.className][0];
      }
    }
} :
  function () {
    var elements = findClass("sprite"), sprites = {};
    for (var i = elements.length, offsets = null, member; i --; ) {
      member = elements[i].id || elements[i].className;
      if (! sprites[member]) {
        sprites[member] = [];
        offsets = [queryCascade(elements[i], "backgroundPositionX"), queryCascade(elements[i],
"backgroundPositionY")];
        sprites[member][0] = offsets.join(" ");
        sprites[member][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 ||
e.srcElement.className][1];
      } else {
        e.srcElement.style.backgroundPosition = sprites[e.srcElement.id ||
e.srcElement.className][0];
      }
    }
  } ;

Save ten.js, refresh ten.html in Firefox, and put the arrows, which are swapped by class, and the running links, which are swapped by ID, through the wringer. Verify your work with Figure 10-2.

The scroller works fine, now.

Figure 10-2. The scroller works fine, now.

Everything work fine now? Great, now on to scripting HTTP.

Writing Dynamic Pages Using Ajax

Now we come to Asynchronous JavaScript and XML (Ajax), which has had a lot of prominence over the last few years. It's a great way to add dynamic features to your web pages to make them more responsive and user friendly. Traditionally, web pages were a static lump of HTML that was delivered from the web server to the web browser; when a user interacted with the UI in any way, details of their action was sent back to the server, and a new lump of HTML was returned, even if that meant there was only a small change to make to the web page (just consider all that data and time spent waiting for not very much).

You can already see how JavaScript can help here, because actions such as button clicks and mouse over events can be handled by the browser without it having to contact the web server. In other words, user interaction can be captured by the browser without having to involve the server at all (just recall all the examples where we displayed new text on a web page without a web server being involved). However, this isn't always quite what we want. Sometimes we want to get data from a web server in response to the user's actions and display it using the JavaScript techniques we've already learned, without disturbing any content that doesn't need to change. This can certainly make a web page much more responsive and user-friendly, because there is no page refresh needed and only the data we need to send is sent.

This in essence is Ajax: updating a web page following an event, without waiting for the server to send a lump of HTML to replace the entire web page. The user can continue to view the page while JavaScript and the web server are passing data around in the background (that's the asynchronous bit in Ajax). The XML bit of Ajax is the data exchange format; it's not always XML as we'll see here, but XML is commonly used. The secret ingredient to all this is the XMLHttpRequest (XHR) object in BOM. It handles all the behind-the-scenes calls to the web server and passes any data returned to your JavaScript.

So, let's get on with it. Open ten.html in your text editor, the one you are coding ten.js in, and then delete the <div class="scroller" id="s1"> element and all its descendents. Don't worry, we'll be adding some new scrollers with Ajax. Now our markup looks like so:

<!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>Getting StartED with JavaScript</title>
<link rel="stylesheet" type="text/css" href="ten.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="ten.js" type="text/javascript"></script>
</body>
</html>

Testing XMLHttpRequest from Your Local File System

One very important thing to note before we begin scripting HTTP with an XMLHttpRequest object is that it works with the HTTP protocol. So, file URLs must begin with http://, not file://. This means you must test an Ajax script on a web server, not your local file system, which is to say your computer. In Internet Explorer, anyway. On the other hand, Firefox, Safari, and Opera relax this restriction and let you load URLs with either the http:// or file:// protocol.

What this means is that you can test the Ajax part of this script on your computer in Firefox, Safari, and Opera, but not in Internet Explorer. Of course, on a web server you can test the script in Internet Explorer, Firefox, Safari, and Opera.

Creating Tree Branches with createElem()

One helper function we'll need for the job is createElem(), which we wrote in Chapter 7 to simplify creating branches of the DOM tree. The code for createElem() is listed here. Put it with the other helper functions in ten.js.

function createElem(name, members, children) {
  var elem = document.createElement(name), m;
  if (members instanceof Object) {
    for (m in members) {
      elem[m] = members[m];
    }
  }
  if (children instanceof Array) {
    for (i = 0; i < children.length; i ++ ) {
      elem.appendChild(typeof children[i] === "object" ? children[i] :
document.createTextNode(children[i]));
    }
  }
  return elem;
}

Now we're going to conditionally define a helper function named createXHR(), which will create an XMLHttpRequest object by way of the XMLHttpRequest() constructor in Firefox, Safari, Opera, and Internet Explorer 7 or greater, and by way of the ActiveXObject() constructor in Internet Explorer 5 or 6. Note that the XMLHttpRequest object returned by XMLHttpRequest() and ActiveXObject() works the same.

Okeydokey, declare createXHR, initializing its value to null. Recall from Chapter 1 that null is preferable to undefined for representing no value on the heap, which is where function values are saved. So, right beneath createElem(), we write the following:

var createXHR = null;

Now if the identifier XMLHttpRequest is defined, we'll overwrite null with a function literal that creates an XMLHttpRequest object with the XMLHttpRequest() constructor. Because Internet Explorer and Safari return "object" for typeofXMLHttpRequest, but Firefox and Opera return "function", we'll avoid their disagreement like so:

var createXHR = null;
if (typeof XMLHttpRequest !== "undefined") {
  createXHR = function() {
    return new XMLHttpRequest();
  };
}

Now the waters muddy considerably. To create an XMLHttpRequest object in Internet Explorer 5 or 6, we have to pass a program id to the ActiveXObject() constructor. During the ten years Internet Explorer 5 and 6 were in active development, Microsoft released several of those. So in an array named versions, let's save four of the most common, ordered newest to oldest:

var createXHR = null;
if (typeof XMLHttpRequest !== "undefined") {
  createXHR = function() {
    return new XMLHttpRequest();
  };
} else if (typeof ActiveXObject !== "undefined") {
  var versions = ["MSXML2.XMLHTTP.6.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP",
"Microsoft.XMLHTTP"];
}

To figure out the newest program ID a visitor's copy of Internet Explorer supports, we'll loop through versions. Within a try block, we'll then attempt to create an XMLHttpRequest object with the program ID in versions[i]. In the event doing so does not throw an error, we'll save that program ID to version, which we initialized to "" prior to the first roundabout, and then terminate the for loop with a break statement.

Note that the empty catch block prevents an error from propagating to the nearest containing catch block. So by analogy, JavaScript errors bubble upward through a script just like events bubble upward through the DOM. Therefore, a catch clause squishes an error object in the same way that burst() squishes an event object. Note too that try must be followed by a catch or finally clause. So, our empty catch block prevents a syntax error, too.

var createXHR = null;
if (typeof XMLHttpRequest !== "undefined") {
  createXHR = function() {
    return new XMLHttpRequest();
  };
} else if (typeof ActiveXObject !== "undefined") {
  var versions = ["MSXML2.XMLHTTP.6.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP",
"Microsoft.XMLHTTP"];

  for (var i = 0, j = versions.length, version = ""; i < j; i ++) {
    try {
      new ActiveXObject(versions[i]);
      version = versions[i];
      break;
    }
    catch(e) {
    }
  }
}

If version does not contain its initial value (the "" empty string), then overwrite null with a function literal that returns an XMLHttpRequest object by passing the program ID in version to ActiveXObject(). So final code for the createXHR() advance conditional loader would be:

var createXHR = null;
if (typeof XMLHttpRequest !== "undefined") {
  createXHR = function() {
    return new XMLHttpRequest();
  };
} else if (typeof ActiveXObject !== "undefined") {
  var versions = ["MSXML2.XMLHTTP.6.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP",
"Microsoft.XMLHTTP"];
  for (var i = 0, j = versions.length, version = ""; i < j; i ++) {
    try {
      new ActiveXObject(versions[i]);
      version = versions[i];
      break;
    }
    catch(e) {
    }
  }
  if (version !== "") {
    createXHR = function() {
      return new ActiveXObject(version);
    };
  }
}

Asynchronously Requesting Data

Now that we can create an XMLHttpRequest object with the XMLHttpRequest() or ActiveXObject() constructor, we'll write a helper function named getData() to asynchronously request (GET) any kind of data with. Typically, an XMLHttpRequest object is used to fetch JSON or XML. Occasionally, you will want to fetch XHTML or plain text, too. Regardless, getData() can do the job.

getData() works with two parameters:

  • url is the URL of the data to fetch.

  • callback is a function to pass the XMLHttpRequest object containing the data to.

function getData(url, callback) {
}

Now we want to ensure createXHR was conditionally defined; that is, it does not still contain null, which would indicate the visitor has a browser from the Pleistocene epoch—a dark time predating any version of Firefox, Safari, or Opera, and version 5 or greater of Internet Explorer.

function getData(url, callback) {
  if (createXHR !== null) {
  }
}

Now we need to do four things. First, create an XMLHttpRequest object. Just call createXHR(), saving the return value to a local variable named req:

function getData(url, callback) {
  if (createXHR !== null) {
    var req = createXHR();
}
}

Second, define a readystatechange event listener for JavaScript to call whenever the number in XMLHttpRequest.readyState changes from 0 to 1 to 2 to 3 and finally to 4. More what those numbers mean in a bit.

Our readystatechange event listener will do nothing, which is to say simply return undefined, whenever readyState changes to 1, 2, or 3. But when it changes to 4, which indicates the GET request is done, we'll pass the XMLHttpRequest object to the callback function. Note that 4 means the GET request is done but not that we have the data in url. For example, if we mistyped url, the server returns a 404 "File not found" HTTP status code. But that's for the callback function to worry about. Our readystatechange event listener looks like so:

function getData(url, callback) {
  if (createXHR !== null) {
    var req = createXHR();
    req.onreadystatechange = function() {
      if (req.readyState === 4) {
        callback(req);
      }
    }
  }
}

Note that unlike event listeners bound to nodes in the DOM tree, a readystatechange event listener does not work with an event object. There's no e parameter. Note too that we bind the listener to XMLHttpRequest.onreadystatechange instead of doing so by calling our helper function addListener(). This is the DOM 0 way of binding events. Note that DOM 0, like BOM, is not a standard, which is why we did not explore binding events this way in Chapter 9. Since you are a clean slate, I didn't want to encourage bad habits. However, for binding a readystatechange event cross-browser, we have to resign ourselves to DOM 0. Just don't be doing this for DOM tree events, or I'd be most unhappy.

The third thing we need to do is clue JavaScript in to the details of the GET request. To do so, we'll pass the XMLHttpRequest.open() method three parameters:

  • The first one is a string for the type of HTTP request, typically GET or POST, to do.

  • The second one is a string for the URL to request. Note that the URL is relative to the page (ten.html in our case) making the request.

  • The third parameter is a boolean indicating whether to do an asynchronous request (true) or a synchronous request (false). More plainly, true means do not freeze the browser until the HTTP request is done, and false means go right ahead and freeze the browser. Note that the default is true.

function getData(url, callback) {
  if (createXHR !== null) {
    var req = createXHR();
    req.onreadystatechange = function() {
      if (req.readyState === 4) {
        callback(req);
      }
    }
    req.open("GET", url, true);
  }
}

Calling open() prepares an HTTP request to be sent but doesn't send it. So, there's a fourth step to do—call XMLHttpRequest.send(). This method takes one parameter: null for a GET request and a query string like "sport=running&brand=Nike&shoe=LunaRacer" for a POST request. Note that for a synchronous request, JavaScript blocks until send() returns. So, this is why asynchronous requests are preferred. We don't want to freeze the visitor's browser, right?

Anyway, we're done coding getData(), which looks like so:

function getData(url, callback) {
  if (createXHR !== null) {
    var req = createXHR();
    req.onreadystatechange = function() {
      if (req.readyState === 4) {
        callback(req);
      }
    }
    req.open("GET", url, true);
    req.send(null);
  }
}

Before moving on, let's recap what the readyState numbers mean, since the details are now comprehensible to you:

  • 0—A new XMLHttpRequest object has been created by calling XMLHttpRequest() or ActiveXObject(). Insofar as 0 is the initial value for readyState, the readystatechange event listener is not invoked.

  • 1—XMLHttpRequest.open() has been called.

  • 2—XMLHttpRequest.send() has been called. For things to work cross-browser, you need to bind the readystatechange event listener prior to calling open() and send().

  • 3—HTTP response headers have been received and the body is beginning to load. Note that if the XHR was created by passing "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", or "Microsoft.XMLHTTP" to ActiveXObject(), the readystatechange event listener is not invoked.

  • 4—The response is complete, so if the HTTP status code is 200 "OK" or 304 "Not modified," there's data for the callback to add to the page.

Parsing an HTML Response

The first callback function will parse the HTML markup in data/s2.html, shown here. It contains the slide<ul>:

<ul class="slide">
  <li><a href="ten.html"><img alt="Nike LunaRacer" src="images/lunaracer.jpg" /></a></li>
  <li><a href="ten.html"><img alt="Nike Lunar Glide, Boston" src="images/glide_bos.jpg"
/></a></li>
  <li><a href="ten.html"><img alt="Nike Lunar Glide, NYC" src="images/glide_nyc.jpg"
/></a></li>
  <li><a href="ten.html"><img alt="Nike Mariah" src="images/mariah.jpg" /></a></li>
  <li><a href="ten.html"><img alt="Nike Lunar Fly, Orange" src="images/fly_org.jpg"
/></a></li>
<li><a href="ten.html"><img alt="Nike Lunar Fly, Black" src="images/fly_blk.jpg" /></a></li>
  <li><a href="ten.html"><img alt="Nike Lunar Elite" src="images/elite.jpg" /></a></li>
  <li><a href="ten.html"><img alt="Nike Zoom Vomero" src="images/vomero.jpg" /></a></li>
  <li><a href="ten.html"><img alt="Nike Air Max" src="images/max.jpg" /></a></li>
</ul>

However, we receive this as a string:

"<ul class="slide">
  <li><a href="ten.html"><img alt="Nike LunaRacer" src="images/lunaracer.jpg" /></a></li>
  <li><a href="ten.html"><img alt="Nike Lunar Glide, Boston" src="images/glide_bos.jpg"
/></a></li>
  <li><a href="ten.html"><img alt="Nike Lunar Glide, NYC" src="images/glide_nyc.jpg"
/></a></li>
  <li><a href="ten.html"><img alt="Nike Mariah" src="images/mariah.jpg" /></a></li>
  <li><a href="ten.html"><img alt="Nike Lunar Fly, Orange" src="images/fly_org.jpg"
/></a></li>
  <li><a href="ten.html"><img alt="Nike Lunar Fly, Black" src="images/fly_blk.jpg" /></a></li>
  <li><a href="ten.html"><img alt="Nike Lunar Elite" src="images/elite.jpg" /></a></li>
  <li><a href="ten.html"><img alt="Nike Zoom Vomero" src="images/vomero.jpg" /></a></li>
  <li><a href="ten.html"><img alt="Nike Air Max" src="images/max.jpg" /></a></li>
</ul>"

So, we're going to have to write a function to search through the string and create the <ul>, <li>, <a>, and <img>Element nodes before we can place them on the page, making sure of course to get the nesting right.

Because this is the final chapter in the book, you probably can roll a helper function to do the job by yourself. So I'll leave that, as they say, as an exercise for the reader.

Just kidding. You'd likely gnaw off a finger or two in frustration trying to code that.

It'd be pretty dull to explain, too. Turns out, I won't have to.

Internet Explorer 4 gave every Element node a proprietary innerHTML member. If you assign a string to innerHTML, JavaScript parses it into HTML and then replaces all descendents of the Element node with that DOM branch. That may be draconian, but it's practical, too. It's so much so that Firefox, Safari, and Opera have always implemented innerHTML, even though it's not part of any DOM standard.

Anyway, innerHTML is totally perfect for parsing an HTML response, quietly converting it from a string value to a branch of the DOM tree. I guess we'll use it then.

Here's how: XMLHttpRequest.responseText contains the string equivalent of the HTML in data/s2.html. parseHTML() will assign that to innerHTML for <div class="wrapper">. However, we're going to need to create that and the other elements of the scroller first with our helper function, createElem()—but only if we received data/s2.html all right from the server or browser cache. To make sure of that we test whether XMLHttpRequest.status is 200 (received data/s2.html from the server) or 304 (received data/s2.html from the cache).

With all those details spilling out of mind, let's begin coding parseHTML() like so. Note that the req parameter will contain the XMLHttpRequest object passed in by getData() when XMLHttpRequest.readyState changes to 4:

function parseHTML(req) {
  if (req.status === 200 || req.status === 304) {
  }
}

Note that if you are testing this script on your computer, which is to say loading URLs with the file:// protocol, there obviously will not be an http:// status code. So, XMLHttpRequest.status will always be 0, no matter what. With this in mind, if you are testing the script on your computer, you must replace 200 or 304 with 0. Otherwise, the if block will never run!

function parseHTML(req) {
  if (req.status === 0 || req.status === 304) {
  }
}

Within the block of the if conditional, we then want to create the HTML for the scroller, less <ul class="slide">, by calling createElem() like so.

function parseHTML(req) {
  if (req.status === 200 || req.status === 304) {
    var div = createElem("div", {className: "scroller", id: "s2"}, [
      createElem("div", {className: "wrapper"}),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
  }
}

Doing so creates the following DOM branch in memory:

<div class="scroller" id="s2">
  <div class="wrapper">
  </div>
  <div class="left arrow sprite"></div>
  <div class="right arrow sprite"></div>
</div>

Now we want to parse the string of text in XMLHttpRequest.responseText into HTML. Then attach that branch to the DOM tree limb, <div class="wrapper">, which we refer to as div.firstChild. One simple assignment to innerHTML does that all in one fell swoop:

function parseHTML(req) {
  if (req.status === 200 || req.status === 304) {
    var div = createElem("div", {className: "scroller", id: "s2"}, [
      createElem("div", {className: "wrapper"}),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
    div.firstChild.innerHTML = req.responseText;
  }
}

I've given Internet Explorer some grief in this book. Deservedly so, too. However, innerHTML is a good idea. I wish it were added to the DOM standard.

Enough with the compliments. The local variable <div> now contains the following HTML:

<div class="scroller" id="s2">
  <div class="wrapper">
    <ul class="slide">
      <li><a href="ten.html"><img alt="Nike LunaRacer" src="images/lunaracer.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Glide, Boston" src="images/glide_bos.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Glide, NYC" src="images/glide_nyc.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Mariah" src="images/mariah.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Fly, Orange" src="images/fly_org.jpg"
/></a></li>
<li><a href="ten.html"><img alt="Nike Lunar Fly, Black" src="images/fly_blk.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Elite" src="images/elite.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Zoom Vomero" src="images/vomero.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Air Max" src="images/max.jpg" /></a></li>
    </ul>
  </div>
  <div class="left arrow sprite"></div>
  <div class="right arrow sprite"></div>
</div>

The only problem is it's floating around in memory, totally invisible to visitors. So, we want to insert it into the DOM tree with appendChild(), a method we covered in Chapter 7:

function parseHTML(req) {
  if (req.status === 200 || req.status === 304) {
    var div = createElem("div", {className: "scroller", id: "s2"}, [
      createElem("div", {className: "wrapper"}),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
    div.firstChild.innerHTML = req.responseText;
    document.body.appendChild(div);
  }
}

Regardless of the HTTP status code for our GET request for data/s2.html, we want to call a function named prep(), which will replace the function literal we currently have for the load event. That way, if we get an undesirable status code, say a 404 "Not found" for mistyping the URL, prep() will still run, adding the drag and drop, sprite, and other behaviors elements on our page.

function parseHTML(req) {
  if (req.status === 200 || req.status === 304) {
    var div = createElem("div", {className: "scroller", id: "s2"}, [
      createElem("div", {className: "wrapper"}),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
    div.firstChild.innerHTML = req.responseText;
    document.body.appendChild(div);
  }
  prep();
}

Underneath parseHTML(), define a function named prep(). In its body, call prepSprites(), prepDrag(), prepSkinKeys(), and prepScrollers(). Finally, in the body of the load event listener, which now contains just a call to presetSkin(), append a call to getData() to fetch data/s2.html. Our script now ends like so:

function prep() {
  prepSprites();
  prepDrag();
  prepSkinKeys();
  prepScrollers();
}
addListener(window, "load", function(e) {
    presetSkin();
    getData("data/s2.html", parseHTML);
});

Why bother making those changes? Wouldn't adding a call to getData() to the old load event listener, say like in the following code, work just as well?

addListener(window, "load", function(e) {
    presetSkin();
    getData("data/s2.html", parseHTML);
    prepSprites();
    prepDrag();
    prepSkinKeys();
    prepScrollers();
  });

Well no, it wouldn't. Because we requested data/s2.html asynchronously, JavaScript does not block until data/s2.html has loaded. That is to say, prepSprites() and prepScrollers() would very likely run before parseHTML() added the new scroller to the page. Therefore, pressing on the arrows for the new scroller would do nothing whatsoever.

That would be bad, so let's add the prep() function, save ten.js, and reload ten.html in Firefox. Put the new scroller through the wringer, verifying you work with Figure 10-3.

Testing the HTML scroller

Figure 10-3. Testing the HTML scroller

Parsing an XML Response

For the first few years, XML was the preferred data exchange format for Ajax. Although JSON (which we will cover in a bit) has overtaken XML in popularity, you will likely need to work with XML for years to come.

With this in mind, let's write a function named parseXML() to parse the contents of the XML file, data/s3.xml, code for which appears in this section. This is the data from the original ten.html, marked up as data:

<?xml version="1.0" encoding="utf-8"?>
<gallery>
<shoe>
  <href>ten.html</href>
  <src>images/lunaracer.jpg</src>
  <alt>Nike LunaRacer</alt>
</shoe>
<shoe>
  <href>ten.html</href>
  <src>images/glide_bos.jpg</src>
  <alt>Nike Lunar Glide, Boston</alt>
</shoe>
<shoe>
  <href>ten.html</href>
  <src>images/glide_nyc.jpg</src>
  <alt>Nike Lunar Gllide, NYC</alt>
</shoe>
<shoe>
  <href>ten.html</href>
  <src>images/mariah.jpg</src>
  <alt>Nike Mariah</alt>
</shoe>
<shoe>
  <href>ten.html</href>
  <src>images/fly_org.jpg</src>
  <alt>Nike Lunar Fly, Orange</alt>
</shoe>
<shoe>
  <href>ten.html</href>
  <src>images/fly_blk.jpg</src>
  <alt>Nike Lunar Fly, Black</alt>
</shoe>
<shoe>
  <href>ten.html</href>
  <src>images/elite.jpg</src>
  <alt>Nike Lunar Elite</alt>
</shoe>
<shoe>
  <href>ten.html</href>
  <src>images/vomero.jpg</src>
  <alt>Nike Zoom Vomero</alt>
</shoe>
<shoe>
  <href>ten.html</href>
  <src>images/max.jpg</src>
  <alt>Nike Air Max</alt>
</shoe>
</gallery>

We'll use this data in a second scroller below the one we created in the previous example. The first thing we need to do is move the prep() invocation from parseHTML() to parseXML(), replacing it with a call to getData() for data/s3.xml. Doing so ensures the two new scrollers are in the DOM tree prior to JavaScript running prepSprites() and prepScrollers().

function parseHTML(req) {
  if (req.status === 200 || req.status === 304) {
var div = createElem("div", {className: "scroller", id: "s2"}, [
      createElem("div", {className: "wrapper"}),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
    div.firstChild.innerHTML = req.responseText;
    document.body.appendChild(div);
  }
  getData("data/s3.xml", parseXML);
}
function parseXML(req) {
  prep();
}

An XML response differs from an HTML one in that it is a Document node containing Element and Text nodes rather than a string of plain text. So you query XML data the same way as a DOM tree. Yup, with the methods we covered in Chapter 7.

So as you might guess, the DOM tree representing data/s3.xml isn't in XMLHttpRequest.responseText. That value is a string not an object. Rather, the DOM tree for our XML is in XMLHttpRequest.responseXML. So after we make sure XMLHttpRequest.status is either 200 or 304, same as we did for HTML, we'll save the DOM tree for our XML to a local variable named domTree.

function parseXML(req) {
  if (req.status === 200 || req.status === 304) {
    var domTree = req.responseXML;
  }
  prep();
}

Note that if you are testing this script on your computer, which is to say loading URLs with the file:// protocol, there obviously will not be an http:// status code. So XMLHttpRequest.status will always be 0, no matter what. With this in mind, if you are testing the script on your computer, you must replace 200 or 304 with 0. Otherwise, the if block will never run!

function parseXML(req) {
  if (req.status === 0 || req.status === 304) {
    var domTree = req.responseXML;
  }
  prep();
}

Now we want to save the nine <shoe> Element nodes to a local variable named elements. Fetch those as if they were <div> or <li> elements—with Document.getElementsByTagName(), which we covered in Chapter 7:

function parseXML(req) {
  if (req.status === 200 || req.status === 304) {
    var domTree = req.responseXML;
    var elements = domTree.getElementsByTagName("shoe");
  }
  prep();
}

Now for any of those <shoe> elements, we can query the Text node in a child <href> like so:

elements[i].getElementsByTagName("href")[0].firstChild.data

Same thing works for a child <src> or <alt>:

elements[i].getElementsByTagName("src")[0].firstChild.data
elements[i].getElementsByTagName("alt")[0].firstChild.data

With this in mind, we can cobble together a scroller with our helper function, createElem(), like so:

function parseXML(req) {
  if (req.status === 200 || req.status === 304) {
    var domTree = req.responseXML;
    var elements = domTree.getElementsByTagName("shoe");
    var div, ul = createElem("ul", {className: "slide"}), li;
    for (var i = 0, j = elements.length; i < j; i ++) {
      li = createElem("li", null, [
        createElem("a", {href: elements[i].getElementsByTagName("href")[0].firstChild.data}, [
        createElem("img", {src: elements[i].getElementsByTagName("src")[0].firstChild.data,
          alt: elements[i].getElementsByTagName("alt")[0].firstChild.data})])]);
      ul.appendChild(li);
    }
    div = createElem("div", {className: "scroller", id: "s3"}, [
      createElem("div", {className: "wrapper"}, [ul]),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
  }
  prep();
}

Now the local variable <div> contains the following DOM branch:

<div class="scroller" id="s3">
  <div class="wrapper">
    <ul class="slide">
      <li><a href="ten.html"><img alt="Nike LunaRacer" src="images/lunaracer.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Glide, Boston" src="images/glide_bos.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Glide, NYC" src="images/glide_nyc.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Mariah" src="images/mariah.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Fly, Orange" src="images/fly_org.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Fly, Black" src="images/fly_blk.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Elite" src="images/elite.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Zoom Vomero" src="images/vomero.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Air Max" src="images/max.jpg" /></a></li>
    </ul>
  </div>
  <div class="left arrow sprite"></div>
  <div class="right arrow sprite"></div>
</div>

But it's floating around in memory invisible to visitors. So, we need attach the DOM branch to the tree, the same way we did in parseHTML():

function parseXML(req) {
if (req.status === 200 || req.status === 304) {
    var domTree = req.responseXML;
    var elements = domTree.getElementsByTagName("shoe");
    var div, ul = createElem("ul", {className: "slide"}), li;
    for (var i = 0, j = elements.length; i < j; i ++) {
      li = createElem("li", null, [
        createElem("a", {href: elements[i].getElementsByTagName("href")[0].firstChild.data}, [
        createElem("img", {src: elements[i].getElementsByTagName("src")[0].firstChild.data,
          alt: elements[i].getElementsByTagName("alt")[0].firstChild.data})])]);
      ul.appendChild(li);
    }
    div = createElem("div", {className: "scroller", id: "s3"}, [
      createElem("div", {className: "wrapper"}, [ul]),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
    document.body.appendChild(div);
  }
  prep();
}

Okeydokey, save ten.js, and reload ten.html in Firefox, comparing its display to Figure 10-4:

Testing the XML scroller

Figure 10-4. Testing the XML scroller

Parsing Simple XML

Like HTML, XML is fairly bloated. That is to say, the ratio of data to structure (tags) is quite low. For this reason, encoding data in XML tag attributes has gained favor over doing so with child nodes. Take a peek at the XML file data/s4.xml to see what I mean by that. Compare its code, displayed here, to that of data/s3.xml. We'll put this into a third scroller.

<?xml version="1.0" encoding="utf-8"?>
<gallery>
  <shoe href="ten.html" src ="images/lunaracer.jpg" alt="Nike LunaRacer"></shoe>
  <shoe href="ten.html" src ="images/glide_bos.jpg" alt="Nike Lunar Glide, Boston"></shoe>
  <shoe href="ten.html" src ="images/glide_nyc.jpg" alt="Nike Lunar Glide, NYC"></shoe>
  <shoe href="ten.html" src ="images/mariah.jpg" alt="Nike Mariah"></shoe>
  <shoe href="ten.html" src ="images/fly_org.jpg" alt="Nike Lunar Fly, Orange"></shoe>
  <shoe href="ten.html" src ="images/fly_blk.jpg" alt="Nike Lunar Fly, Black"></shoe>
  <shoe href="ten.html" src ="images/elite.jpg" alt="Nike Lunar Elite"></shoe>
  <shoe href="ten.html" src ="images/vomero.jpg" alt="Nike Zoom Vomero"></shoe>
  <shoe href="ten.html" src ="images/max.jpg" alt="Nike  Air Max"></shoe>
</gallery>

Because XML encoded this way is referred to as Simple XML, let's name the function that will parse data/s4.xml, parseSimpleXML(). Like before, the first thing we want to do is move the prep() invocation from parseXML() to parseSimpleXML(), replacing it with a call to getData() for data/s4.xml. You know, so that the three new scrollers are in the DOM tree prior to JavaScript running prepSprites() and prepScrollers():

function parseXML(req) {
  if (req.status === 200 || req.status === 304) {
    var domTree = req.responseXML;
    var elements = domTree.getElementsByTagName("shoe");
    var div, ul = createElem("ul", {className: "slide"}), li;
    for (var i = 0, j = elements.length; i < j; i ++) {
      li = createElem("li", null, [
        createElem("a", {href: elements[i].getElementsByTagName("href")[0].firstChild.data}, [
        createElem("img", {src: elements[i].getElementsByTagName("src")[0].firstChild.data,
          alt: elements[i].getElementsByTagName("alt")[0].firstChild.data})])]);
      ul.appendChild(li);
    }
    div = createElem("div", {className: "scroller", id: "s3"}, [
      createElem("div", {className: "wrapper"}, [ul]),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
    document.body.appendChild(div);
  }

  getData("data/s4.xml", parseSimpleXML);
}
function parseSimpleXML(req) {
  prep();
}

Now what would you do next in parseSimpleXML()?

Make sure XMLHttpRequest.status is either 200 or 304, the same as we did for parseHTML() and parseXML().

function parseSimpleXML(req) {
  if (req.status === 200 || req.status === 304) {
  }
  prep();
}

Next?

Right again. Save the DOM tree for our XML to a local variable named domTree, just like we did in parseXML().

function parseSimpleXML(req) {
  if (req.status === 200 || req.status === 304) {
    var domTree = req.responseXML;
  }
  prep();
}

And now?

Yup, save the nine <shoe> elements to a local variable, the same as for parseXML().

function parseSimpleXML(req) {
  if (req.status === 200 || req.status === 304) {
    var domTree = req.responseXML;
    var elements = domTree.getElementsByTagName("shoe");
  }
  prep();
}

Note that if you are testing this script on your computer, which is to say loading URLs with the file:// protocol, there obviously will not be an http:// status code. XMLHttpRequest.status will always be 0, no matter what. With this in mind, if you are testing the script on your computer, you must replace 200 or 304 with 0. Otherwise, the if block will never run!

function parseSimpleXML(req) {
  if (req.status === 0 || req.status === 304) {
    var domTree = req.responseXML;
    var elements = domTree.getElementsByTagName("shoe");
  }
  prep();
}

Do you remember how to query a custom attribute for an element?

We can't use the . or [] operators. Rather, we need to call Element.getAttribute(), a method defined by each <shoe> element. For example, to query the href attribute, we'd write this:

elements[i].getAttribute("href")

The same thing goes for the src and alt attributes:

elements[i].getAttribute("src")
elements[i].getAttribute("alt")

Even that is less verbose than for traditional XML. Anyway, with this in mind, we can cobble together a scroller with our helper function, createElem(), like so:

function parseSimpleXML(req) {
  if (req.status === 200 || req.status === 304) {
    var domTree = req.responseXML;
var elements = domTree.getElementsByTagName("shoe");
    var div, ul = createElem("ul", {className: "slide"}), li;
    for (var i = 0, j = elements.length; i < j; i ++) {
      li = createElem("li", null, [
        createElem("a", {href: elements[i].getAttribute("href")}, [
        createElem("img", {src: elements[i].getAttribute("src"), alt:
elements[i].getAttribute("alt")})])]);
      ul.appendChild(li);
    }

    div = createElem("div", {className: "scroller", id: "s4"}, [

      createElem("div", {className: "wrapper"}, [ul]),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
  }
  prep();
}

Now the local variable <div> contains the following DOM branch:

<div class="scroller" id="s4">
  <div class="wrapper">
    <ul class="slide">
      <li><a href="ten.html"><img alt="Nike LunaRacer" src="images/lunaracer.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Glide, Boston" src="images/glide_bos.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Glide, NYC" src="images/glide_nyc.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Mariah" src="images/mariah.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Fly, Orange" src="images/fly_org.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Fly, Black" src="images/fly_blk.jpg"
/></a></li>
      <li><a href="ten.html"><img alt="Nike Lunar Elite" src="images/elite.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Zoom Vomero" src="images/vomero.jpg" /></a></li>
      <li><a href="ten.html"><img alt="Nike Air Max" src="images/max.jpg" /></a></li>
     </ul>
  </div>
  <div class="left arrow sprite"></div>
  <div class="right arrow sprite"></div>
</div>

But it's in memory, invisible to visitors. How would you fix that?

Yep, put the DOM branch on the tree:

function parseSimpleXML(req) {
  if (req.status === 200 || req.status === 304) {
    var domTree = req.responseXML;
    var elements = domTree.getElementsByTagName("shoe");
    var div, ul = createElem("ul", {className: "slide"}), li;
    for (var i = 0, j = elements.length; i < j; i ++) {
      li = createElem("li", null, [
createElem("a", {href: elements[i].getAttribute("href")}, [
        createElem("img", {src: elements[i].getAttribute("src"), alt:
elements[i].getAttribute("alt")})])]);
      ul.appendChild(li);
    }
    div = createElem("div", {className: "scroller", id: "s4"}, [
      createElem("div", {className: "wrapper"}, [ul]),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
    document.body.appendChild(div);
  }
  prep();
}

It's time to give parseSimpleXML() a whirl. So, save ten.js and reload ten.html in Firefox, comparing its display to Figure 10-5:

Testing the Simple XML scroller

Figure 10-5. Testing the Simple XML scroller

Parsing JSON

More and more, XML is being supplanted by JSON, a data exchange format derived from JavaScript object and array literal syntax. JSON downloads snappy and is simple to parse. Just pass JSON data to window.eval(), and you have a JavaScript array or object. However, passing third-party JSON data, which may be malformed or malicious, to eval() is a horrible idea.

Warning

The eval() method is a powerful and dangerous tool. You should not pass any third-party data to it, because that third-party data could well contain malicious code for your users' browsers to run. This could lead to all sorts of attacks and discomforts.

For this reason, Internet Explorer 8, Firefox 3.5, and Safari 4 define a method, JSON.parse(), for you to use instead of eval(). For other versions and browsers, download the free JSON parser maintained by Douglas Crockford, JSON's creator, from http://json.org/json2.js. Delete the first line:

alert('IMPORTANT: Remove this line from json2.js before deployment.');

Save the file as json2.js to the same directory as your other JavaScript files. Then link it in to your XHTML page. json2.js will define window.JSON only if it is missing. So for ten.html, we would link in json2.js like so:

<!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>Getting StartED with JavaScript</title>
<link rel="stylesheet" type="text/css" href="ten.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="ten.js" type="text/javascript"></script>

<script src="json2.js" type="text/javascript"></script>

</body>
</html>

Note that http://json.org/json2.js is the very same JSON parser that Internet Explorer 8, Firefox 3.5, Safari 4, and Opera 10.5 natively define. The only difference is that the native version is compiled rather than interpreted. So, it runs faster.

JSON in a Nutshell

Okeydokey, JSON differs from JavaScript object and array literals in a few ways. First, JSON object members may only be named with strings. So no identifiers. Second, JSON does not permit values to be functions or undefined. That is to say, a JSON value may be a string, number, boolean, null, object literal, or array literal.

Pretty simple, don't you think?

We're going to be doing exactly the same as we did in the previous examples, except we're using JSON to pass the data, rather than HTML or XML. We'll add another scroller to the page, using the JSON data. To encode data for a scroller with JSON, we would write the following, which is what data/s5.js contains:

[
   {
    "href": "ten.html",
    "src": "images/lunaracer.jpg",
    "alt": "Nike LunaRacer"
  },
   {
    "href": "ten.html",
    "src": "images/glide_bos.jpg",
    "alt": "Nike Lunar Glide, Boston"
  },
   {
    "href": "ten.html",
    "src": "images/glide_nyc.jpg",
    "alt": "Nike Lunar Glide, NYC"
  },
   {
    "href": "ten.html",
    "src": "images/mariah.jpg",
    "alt": "Nike Mariah"
  },
   {
    "href": "ten.html",
    "src": "images/fly_org.jpg",
    "alt": "Nike Lunar Fly, Orange"
  },
   {
    "href": "ten.html",
    "src": "images/fly_blk.jpg",
    "alt": "Nike Lunar Fly, Black"
  },
   {
    "href": "ten.html",
    "src": "images/elite.jpg",
    "alt": "Nike Lunar Elite"
  },
   {
"href": "ten.html",
    "src": "images/vomero.jpg",
    "alt": "Nike Zoom Vomero"
  },
   {
    "href": "ten.html",
    "src": "images/max.jpg",
    "alt": "Nike Air Max"
  }
]

With JSON data and parser in hand, let's create a new scroller. The function to do so will, oddly enough, be named parseJSON(). But before I forget, relocate prep() and have getData() go GET the JSON data. Hmm. I've seen this fish before!

function parseSimpleXML(req) {
  if (req.status === 200 || req.status === 304) {
    var domTree = req.responseXML;
    var elements = domTree.getElementsByTagName("shoe");
    var div, ul = createElem("ul", {className: "slide"}), li;
    for (var i = 0, j = elements.length; i < j; i ++) {
      li = createElem("li", null, [
        createElem("a", {href: elements[i].getAttribute("href")}, [
        createElem("img", {src: elements[i].getAttribute("src"), alt:
 elements[i].getAttribute("alt")})])]);
      ul.appendChild(li);
    }
    div = createElem("div", {className: "scroller", id: "s4"}, [
      createElem("div", {className: "wrapper"}, [ul]),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
    document.body.appendChild(div);
  }
  getData("data/s5.js", parseJSON);
}
function parseJSON(req) {
  prep();
}

Now then, eval() and JSON.parse() work with a string. Moreover, an XMLHttpRequest has but two hiding places for data, XMLHttpRequest.responseText or XMLHttpRequest.responseXML.

So, where would our JSON data be?

Right, XMLHttpRequest.responseText. I'd have fallen into despair had you missed that one.

After making sure the HTTP request did not fail, create a local variable named data containing the return value of passing XMLHttpRequest.responseText to JSON.parse(). Remember, that'll be a compiled or interpreted version of Crockford's JSON parser. So no worries; this works perfectly cross-browser.

function parseJSON(req) {
  if (req.status === 200 || req.status === 304) {

    var data = JSON.parse(req.responseText);

  }
  prep();
}

Note that if you are testing this script on your computer, which is to say loading URLs with the file:// protocol, there obviously will not be an http:// status code. XMLHttpRequest.status will always be 0, no matter what. With this in mind, if you are testing the script on your computer, you must replace 200 or 304 with 0. Otherwise, the if block will never run!

function parseJSON(req) {
  if (req.status === 0 || req.status === 304) {
    var data = JSON.parse(req.responseText);
  }
  prep();
}

The local variable data now contains an array of objects, just like if we had written this:

function parseJSON(req) {
  if (req.status === 200 || req.status === 304) {
    var data = [
      {
        "href": "ten.html",
        "src": "images/lunaracer.jpg",
        "alt": "Nike LunaRacer"
      },
      {
        "href": "ten.html",
        "src": "images/glide_bos.jpg",
        "alt": "Nike Lunar Glide, Boston"
      },
      {
        "href": "ten.html",
        "src": "images/glide_nyc.jpg",
        "alt": "Nike Lunar Glide, NYC"
      },
      {
        "href": "ten.html",
        "src": "images/mariah.jpg",
        "alt": "Nike Mariah"
      },
      {
        "href": "ten.html",
        "src": "images/fly_org.jpg",
        "alt": "Nike Lunar Fly, Orange"
      },
      {
        "href": "ten.html",
        "src": "images/fly_blk.jpg",
        "alt": "Nike Lunar Fly, Black"
      },
      {
        "href": "ten.html",
        "src": "images/elite.jpg",
        "alt": "Nike Lunar Elite"
      },
      {
        "href": "ten.html",
        "src": "images/vomero.jpg",
"alt": "Nike Zoom Vomero"
      },
      {
        "href": "ten.html",
        "src": "images/max.jpg",
        "alt": "Nike Air Max"
      }
    ];
  }
  prep();
}

To query say the src member of the third element in data, we would write one of the following:

data[2].src
data[2]["src"]

Those would both return the string, images/glide_nyc.jpg. Remember from Chapter 5 you may query a member named with a string with an identifier so long as the string, src in our case, is a valid identifier.

With this in mind, we can create a new scroller from our JSON data and createElem() helper function like so:

function parseJSON(req) {
  if (req.status === 200 || req.status === 304) {
    var data = JSON.parse(req.responseText);

    var div, ul = createElem("ul", {className: "slide"}), li;

    for (var i = 0, j = data.length; i < j; i ++) {
      li = createElem("li", null, [
        createElem("a", {href: data[i].href}, [
        createElem("img", {src: data[i].src, alt: data[i].alt})])]);
      ul.appendChild(li);
    }
    div = createElem("div", {className: "scroller", id: "s5"}, [
      createElem("div", {className: "wrapper"}, [ul]),
      createElem("div", {className: "left arrow sprite"}),

      createElem("div", {className: "right arrow sprite"})]);
  }
  prep();
}

Then add the DOM branch to the tree. Note that displaying our new scroller to the visitor is a UI update. Just like rendering the down image for a sprite. Remember that if the UI thread is running JavaScript at the time, those have to take a number and wait in the UI queue. I'm just trying to reinforce why it's vital to write JavaScript that runs snappy. UI rigor mortis is unpleasant for the visitor.

function parseJSON(req) {
  if (req.status === 200 || req.status === 304) {
    var data = JSON.parse(req.responseText);
    var div, ul = createElem("ul", {className: "slide"}), li;
    for (var i = 0, j = data.length; i < j; i ++) {
      li = createElem("li", null, [
createElem("a", {href: data[i].href}, [
        createElem("img", {src: data[i].src, alt: data[i].alt})])]);
      ul.appendChild(li);
    }
    div = createElem("div", {className: "scroller", id: "s5"}, [
      createElem("div", {className: "wrapper"}, [ul]),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
    document.body.appendChild(div);
  }
  prep();
}

Now let's throw parseJSON() into the pool and see whether it sinks or swims; save ten.js and reload ten.html in Firefox, comparing its display to Figure 10-6.

Testing the JSON scroller

Figure 10-6. Testing the JSON scroller

Padding JSON

Since JSON is valid JavaScript, you do not need an XMLHttpRequest object to retrieve JSON data. This is referred to as JSON-P, JSON with Padding.

JSON-P works like this:

  • The JSON data created by a PHP or some other server-side script is wrapped in a callback function. So in data/s6.js, displayed next, the JSON array from the previous gallery is wrapped in a callback function named padJSON(), which we'll define in a bit.

  • A <script> element with an src set to the URL of the JSON-P file is dynamically inserted into the page, into ten.html in our case. Note that for cross-browser compatibility, the <script> has to go into the <head>, not the <body>. The browser then executes the JSON-P data file like any other JavaScript file. So, the JSON array gets passed to the callback function it is wrapped in.

padJSON([
   {
    "href": "ten.html",
    "src": "images/lunaracer.jpg",
    "alt": "Nike LunaRacer"
  },
   {
    "href": "ten.html",
    "src": "images/glide_bos.jpg",
    "alt": "Nike Lunar Glide, Boston"
  },
   {
    "href": "ten.html",
    "src": "images/glide_nyc.jpg",
    "alt": "Nike Lunar Glide, NYC"
  },
   {
    "href": "ten.html",
    "src": "images/mariah.jpg",
    "alt": "Nike Mariah"
  },
   {
    "href": "ten.html",
    "src": "images/fly_org.jpg",
    "alt": "Nike Lunar Fly, Orange"
  },
   {
    "href": "ten.html",
    "src": "images/fly_blk.jpg",
    "alt": "Nike Lunar Fly, Black"
  },
   {
    "href": "ten.html",
    "src": "images/elite.jpg",
    "alt": "Nike Lunar Elite"
  },
{
    "href": "ten.html",
    "src": "images/vomero.jpg",
    "alt": "Nike Zoom Vomero"
  },
   {
    "href": "ten.html",
    "src": "images/max.jpg",
    "alt": "Nike Air Max"
  }
]);

Let's define a function named parseJSONP. Insofar as we're not making an parseJSONP request, let's name the parameter data, not req. As we've done several times before, move prep() from parseJSON() to parseJSONP(). However, rather than call getData() for data/s6.js, simply call parseJSONP() instead. Remember, we're bypassing XMLHttpRequest entirely.

function parseJSON(req) {
  if (req.status === 200 || req.status === 304) {
    var data = JSON.parse(req.responseText);
    var div, ul = createElem("ul", {className: "slide"}), li;
    for (var i = 0, j = data.length; i < j; i ++) {
      li = createElem("li", null, [
        createElem("a", {href: data[i].href}, [
        createElem("img", {src: data[i].src, alt: data[i].alt})])]);
      ul.appendChild(li);
    }
    div = createElem("div", {className: "scroller", id: "s5"}, [
      createElem("div", {className: "wrapper"}, [ul]),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
    document.body.appendChild(div);
  }

  parseJSONP();
}

function parseJSONP(data) {
  prep();
}

Now when parseJSON() calls parseJSONP(), data will be undefined. In that event, we want to define padJSON(), the JSON-P callback function. But it has to be global so that it is callable from data/s6.js. Remember, none of the functions in ten.js is global. They're all saved to the call object of the self-invoking function wrapping the script.

So by way of a closure, we'll make parseJSONP() callable from the global scope like so. Remember from Chapter 6 that arguments.callee refers to the function that is running—parseJSONP().

function parseJSONP(data) {

  if (typeof data === "undefined") {
    var f = arguments.callee;
    window.padJSON = function(d) {
        f(d);
      };
}
  prep();
}

Now we'll dynamically insert a <script> with an src of data/s6.js, the URL of our JSON-P file. Then return to terminate parseJSONP(). This is when the <script> element is added to the DOM tree. Our JSON array then gets passed to padJSON(), which in turn passes it on to parseJSONP(). The second time parseJSONP() is called, data contains the JSON array not undefined. But a JSON array is a valid JavaScript array, too. So, we can create a new scroller from data just like we did in parseJSON():

function parseJSONP(data) {
  if (typeof data === "undefined") {
    var f = arguments.callee;
    window.padJSON = function(d) {
        f(d);
      };
    var script = document.createElement("script");
    script.src = "data/s6.js";
    document.getElementsByTagName("head")[0].appendChild(script);
    return;
  }
  var div, ul = createElem("ul", {className: "slide"}), li;
  for (var i = 0, j = data.length; i < j; i ++) {
    li = createElem("li", null, [
      createElem("a", {href: data[i].href}, [
      createElem("img", {src: data[i].src, alt: data[i].alt})])]);
    ul.appendChild(li);
  }
  div = createElem("div", {className: "scroller", id: "s6"}, [
    createElem("div", {className: "wrapper"}, [ul]),
    createElem("div", {className: "left arrow sprite"}),
    createElem("div", {className: "right arrow sprite"})]);

  prep();
}

Finally, it's time to put the branch on the DOM tree. You know what to do:

function parseJSONP(data) {
  if (typeof data === "undefined") {
    var f = arguments.callee;
    window.padJSON = function(d) {
        f(d);
      };
    var script = document.createElement("script");
    script.src = "data/s6.js";
    document.getElementsByTagName("head")[0].appendChild(script);
    return;
  }
  var div, ul = createElem("ul", {className: "slide"}), li;
  for (var i = 0, j = data.length; i < j; i ++) {
    li = createElem("li", null, [
      createElem("a", {href: data[i].href}, [
createElem("img", {src: data[i].src, alt: data[i].alt})])]);
    ul.appendChild(li);
  }
  div = createElem("div", {className: "scroller", id: "s6"}, [
    createElem("div", {className: "wrapper"}, [ul]),
    createElem("div", {className: "left arrow sprite"}),
    createElem("div", {className: "right arrow sprite"})]);

  document.body.appendChild(div);

  prep();
}

Now let's test parseJSONP(); save ten.js, and reload ten.html in Firefox, comparing its display to Figure 10-7.

Testing the JSON-P scroller

Figure 10-7. Testing the JSON-P scroller

Yielding with Timers

In addition to animations, one other use for timers is to yield control of the UI thread so that the browser can update its display. Doing so prevents browser rigormortis.

At the moment, JavaScript blocks while prep() is running. So, until prepSprites(), prepDrag(), prepSkinKeys(), and prepScrollers() have all returned, a visitor's browser will be frozen.

To fix that, we'll create an array named mojo containing those four functions. Then yield the UI thread for 30 milliseconds, long enough for most UI updates, between each call:

function prep() {
  var mojo = [prepSprites, prepDrag, prepSkinKeys, prepScrollers];
  setTimeout(function() {
    (mojo.shift())();
    if (mojo.length !== 0) {
      setTimeout(arguments.callee, 30);
    }
  }, 30);
}

Converting function declarations to expressions

Other than the conditional advance loaders, the functions in our script are defined by way of declarations rather than expressions. Doing so is helpful while initially coding a script insofar as you can invoke declared functions prior to defining them. So you can be bit messy while you are trying to get things working if you code your functions with declarations. Moreover, a debugger like Firebug can use a declared function's nonstandard name member as an indicator to convey errors with.

However, function expressions are preferred over declarations insofar as function expressions require you to use functions as values, which is the key to unlocking the power of JavaScript. Moreover, function expressions require you to define a function prior to invoking it, which is good programming practice. This is why we explored functions with expressions rather than declarations in Chapter 6.

With this in mind, let's now go through and recode our function declarations as expressions. Doing so is fairly simple for the most part. Just declare a variable named with the identifier from the declaration and then assign an unnamed function expression to it. The body of the expression is the identical to the body of the declaration. So we're pretty much just moving the identifier. Finally, follow the } curly brace at the end of the function body with a ; semicolon to end the var statement.

Except for the nested functions, we were careful not to invoke functions prior to their declaration. So in addition to converting the nested function declarations to expressions, we will need to move them higher in their parent function's body so that we are invoking a function rather than undefined.

Furthermore, let's rework our conditional advance loaders for createXHR() and findClass() with the ?: operator rather than with an if else statement. In this way, our script will contain 24 function expressions followed by 1 invocation expression (of addListener()). Yup, pretty elegant.

ECMAScript 5 adds a strict mode that tells a JavaScript interpreter to throw errors if you try to use deprecated features such as argument.callee. To trigger strict mode, simply put "use strict"; on the very first line of a script. Internet Explorer 9, Firefox 4, and other ECMAScript 5 compliant browsers will then parse our script in strict mode, while older browsers will simply ignore the string literal, which is not saved to any variable. So let's insert "use strict"; on the very first line of our script.

Now for the final moment of truth. Save ten.js, and then reload ten.html in Firefox. Put all the behaviors we coded in Chapters 9 and 10 through the wringer.

Final code for ten.js appears here. Note that in the downloads for this chapter at www.apress.com; this is tenFinal.js:

"use strict";
(function () {

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);
  } ;

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

var burst = function (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 = document.getElementsByClassName ?
  function (name, root) {
    root = root || document.documentElement;
    return root.getElementsByClassName(name);
  } :
  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;
  } ;

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 ++;
  };
}();

var getCookie = function (name) {
  var batch = document.cookie, i, firstCut, secondCut;
  i = batch.indexOf(name + "=");
  if (i !== −1) {
    firstCut = i + name.length + 1;
secondCut = batch.indexOf(";", firstCut);
    if (secondCut === −1) secondCut = batch.length;
    return decodeURIComponent(batch.substring(firstCut, secondCut));
  } else {
    return false;
  }
};

var createElem = function (name, members, children) {
  var elem = document.createElement(name), m;
  if (members instanceof Object) {
    for (m in members) {
      elem[m] = members[m];
    }
  }
  if (children instanceof Array) {
    for (i = 0; i < children.length; i ++ ) {
      elem.appendChild(typeof children[i] === "object" ?
        children[i] : document.createTextNode(children[i]));
    }
  }
  return elem;
};

var createXHR = typeof XMLHttpRequest !== "undefined" ?
  function () {
    return new XMLHttpRequest();
  } :
  typeof ActiveXObject !== "undefined" ?
  function () {
    var versions = ["MSXML2.XMLHTTP.6.0", "MSXML2.XMLHTTP.3.0",
      "MSXML2.XMLHTTP", "Microsoft.XMLHTTP"];
    for (var i = 0, j = versions.length, version = ""; i < j; i ++) {
      try {
        new ActiveXObject(versions[i]);
        version = versions[i];
        break;
      }
      catch(e) {
      }
    }
    if (version !== "") {
      return function () {
        return new ActiveXObject(version);
      };
    } else {
      return null;
    }
  }() :
  null ;

var getData = function (url, callback) {
  if (createXHR !== null) {
    var req = createXHR();
req.onreadystatechange = function () {
      if (req.readyState === 4) {
        callback(req);
      }
    }
    req.open("GET", url, true);
    req.send(null);
  }
};

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

var drag = function (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();
  var move = function (e) {
    if (!e) e = window.event;
    wrapper.style.left = left + e.clientX - clientX + "px";
    wrapper.style.top = top + e.clientY - clientY + "px";
    burst(e);
  };
  var drop = function (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);
  };
  addListener(document, "mousemove", move, true);
  addListener(document, "mouseup", drop, true);
  burst(e);
  thwart(e);
};

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

var presetSkin = function () {
  var pref = getCookie("skin");
  if (pref !== false) {
    document.getElementById("skin").href = pref + ".css";
  }
};

var prepSkinKeys = function () {
  var sheet = document.getElementById("skin");
var swapSkinByKey = function (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();
    var pref;
    if (e.letter === "f") {
      pref = "fuchsia";
    } else if (e.letter === "g") {
      pref = "green";
    } else if (e.letter === "b") {
      pref = "blue";
    } else {
      return;
    }
    sheet.href = pref + ".css";
    document.cookie = "skin=" + pref + "; max-age=" + (60*60*24*30);
  };
  addListener(document, "keypress", swapSkinByKey, true);
};

var prepScrollers = function () {
  var elements = findClass("scroller");
  for (var i = elements.length; i --; ) {
    (function (scroller) {
      var wrapper = findClass("wrapper", scroller)[0];
      var slide = findClass("slide", scroller)[0];
      var w1 = parseInt(queryCascade(wrapper, "width"));
      var w2 = parseInt(queryCascade(slide, "width"));
      var timer = null;
      slide.style.left = queryCascade(slide, "left");

      var press = function (e) {
        if (!e) e = window.event;
        if (!e.target) e.target = e.srcElement;
        var jump = e.target.jump;

        var animate = function animate () {
          var x = parseInt(slide.style.left) + jump;
          if (x >= w1 - w2 && x <= 0) {
            slide.style.left = x + "px";
            timer = setTimeout(animate, 15);
          } else if (x < w1 - w2) {
            slide.style.left = w1 - w2 + "px";
          } else {
            slide.style.left = "0px";
          }
        };

        var release = function (e) {
          clearTimeout(timer);
          removeListener(document, "mouseup", release, true);
removeListener(document, "mouseout", release, true);
        };
        addListener(document, "mouseup", release, true);
        addListener(document, "mouseout", release, true);
        animate();
        burst(e);
        thwart(e);
      };
      for (var arrows = findClass("arrow", scroller),
        i = arrows.length, re = /right/; i --; ) {
          addListener(arrows[i], "mousedown", press);
          arrows[i].jump = (re.test(arrows[i].className)) ? −10 : 10;
      }
    })(elements[i]);
  }
};

var parseHTML = function (req) {
  if (req.status === 200 || req.status === 304) {
    var div = createElem("div", {className: "scroller", id: "s2"}, [
      createElem("div", {className: "wrapper"}),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
    div.firstChild.innerHTML = req.responseText;
    document.body.appendChild(div);
  }
  getData("data/s3.xml", parseXML);
};

var parseXML = function (req) {
  if (req.status === 200 || req.status === 304) {
    var domTree = req.responseXML,
      m = "getElementsByTagName";
    var elements = domTree[m]("shoe");
    var div, ul = createElem("ul", {className: "slide"}), li;
    for (var i = 0, j = elements.length; i < j; i ++) {
      li = createElem("li", null, [
        createElem("a", {href: elements[i][m]("href")[0].firstChild.data}, [
        createElem("img", {src: elements[i][m]("src")[0].firstChild.data,
        alt: elements[i][m]("alt")[0].firstChild.data})])]);
      ul.appendChild(li);
    }
    div = createElem("div", {className: "scroller", id: "s3"}, [
      createElem("div", {className: "wrapper"}, [ul]),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
    document.body.appendChild(div);
  }
  getData("data/s4.xml", parseSimpleXML);
};

var parseSimpleXML = function (req) {
  if (req.status === 200 || req.status === 304) {
    var domTree = req.responseXML;
var elements = domTree.getElementsByTagName("shoe");
    var div, ul = createElem("ul", {className: "slide"}), li;
    for (var i = 0, j = elements.length; i < j; i ++) {
      li = createElem("li", null, [
        createElem("a", {href: elements[i].getAttribute("href")}, [
        createElem("img", {src: elements[i].getAttribute("src"),
        alt: elements[i].getAttribute("alt")})])]);
      ul.appendChild(li);
    }
    div = createElem("div", {className: "scroller", id: "s4"}, [
      createElem("div", {className: "wrapper"}, [ul]),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
    document.body.appendChild(div);
  }
  getData("data/s5.js", parseJSON);
};

var parseJSON = function (req) {
  if (req.status === 200 || req.status === 304) {
    var data = JSON.parse(req.responseText);
    var div, ul = createElem("ul", {className: "slide"}), li;
    for (var i = 0, j = data.length; i < j; i ++) {
      li = createElem("li", null, [
        createElem("a", {href: data[i].href}, [
        createElem("img", {src: data[i].src, alt: data[i].alt})])]);
      ul.appendChild(li);
    }
    div = createElem("div", {className: "scroller", id: "s5"}, [
      createElem("div", {className: "wrapper"}, [ul]),
      createElem("div", {className: "left arrow sprite"}),
      createElem("div", {className: "right arrow sprite"})]);
    document.body.appendChild(div);
  }
  parseJSONP();
};

var parseJSONP = function parseJSONP (data) {
  if (typeof data === "undefined") {
    var f = parseJSONP;
    window.padJSON = function (d) {
        f(d);
      };
    var script = document.createElement("script");
    script.src = "data/s6.js";
    document.getElementsByTagName("head")[0].appendChild(script);
    return;
  }
  var div, ul = createElem("ul", {className: "slide"}), li;
  for (var i = 0, j = data.length; i < j; i ++) {
    li = createElem("li", null, [
      createElem("a", {href: data[i].href}, [
      createElem("img", {src: data[i].src, alt: data[i].alt})])]);
    ul.appendChild(li);
}
  div = createElem("div", {className: "scroller", id: "s6"}, [
    createElem("div", {className: "wrapper"}, [ul]),
    createElem("div", {className: "left arrow sprite"}),
    createElem("div", {className: "right arrow sprite"})]);
  document.body.appendChild(div);
  prep();
};

var prep = function () {
  var mojo = [prepSprites, prepDrag, prepSkinKeys, prepScrollers];
  setTimeout(function yield () {
    (mojo.shift())();
    if (mojo.length !== 0) {
      setTimeout(yield, 30);
    }
  }, 30);
};

addListener(window, "load", function (e) {
    presetSkin();
    getData("data/s2.html", parseHTML);
  });

})();

Summary

In this chapter we explored how to save visitor data in a cookie in order to remember their preference for a blue, fuchsia, or green skin, animate part of the DOM tree and yield the UI thread with timers, and dynamically add content with Ajax or JSON-P. Finally, we explored how to recode our function declarations as function expressions and to have JavaScript interpret our script in strict mode, which is new in ECMAScript 5.

Though function declarations are simpler to work with while writing and debugging a script, function expressions are preferred insofar as those require you to use functions as values. Doing so is the key to unlocking the power of JavaScript and to your becoming a JavaScript wizard.

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

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