The site navigation works only if the browser supports XMLHttpRequest.
The site navigation is an example of what Jeremy Keith calls Hijax.
On this page I explain how I import the site navigation that appears on every page of this site.
It's best to start with a general overview of the problem the site navigation is supposed to solve. That way, you'll easily understand why I coded it as I did.
QuirksMode.org is an old, old site. Among other things, this means that the HTML structure of the pages was already well determined by the time I worked on the 2006 redesign that introduced the site navigation. During that redesign, I adhered to a number of principles set out in the Redesign: the CSS QuirksBlog entry. For the site navigation, two are important:
As a result, the site navigation would not be hard-coded into any page, and it would not form a separate file, either.
Then how would I get the site navigation information? Which page contains an ordered list of all pages on this site? The answer is obvious: the sitemap page. The site navigation would have to use the sitemap as its source file. If I imported the sitemap and changed its presentation a bit, I'd have the perfect site navigation.
In the 2003 design I loaded the sitemap into a navigation frame (and remember that frames can be viewed as precursors of XMLHttp). When in 2006 I decided to ditch the frames, I had to find another solution. The obvious one was XMLHttpRequest.
Before treating the code, we first have to consider the source file: the sitemap. This sitemap is absolutely crucial for this site's continuing accessibility. Although I don't think I'm visited by large flocks of noscript users (why would they visit a JavaScript site?), I still had to make a determined effort to practice what I preached. I made the key accessibility decisions back in 2003. Summary:
The sitemap remains a normal HTML page that contains lots of links as well as a few headers to make sense of them. It was this normal, accessible HTML page that I had to import and somehow groom into an acceptable high-end site navigation.
This way of handling Ajax is an important progressive enhancement technique. As long as you make sure that anything you import can be viewed as a separate, static HTML page, a small amount of nifty scripting imports the page for users with modern browsers, while users without JavaScript will simply move on to the plain HTML pages. (See Jeremy Keith's Hijax definition for more information on this important point.)
When a page has loaded, I import nav.txt that prepares the
way for both the site navigation and the Table of Contents. It creates the HTML structures
necessary to insert these two link lists into the page. It contains a "show site navigation" link (well, really a
<span>
for CSS reasons), clicking on which imports and shows the site navigation.
Clicking on the "show site navigation" imports the sitemap. The importing bit is easy. I took my trusty XMLHttpRequest functions and wrote a single line of code:
sendRequest('/sitemap.html',setMainNav);
That took care of the importing. I had the complete sitemap ready for action. Note: the complete
sitemap, including links to the CSS and JavaScript files, the <title>
, the doctype, and so on.
However, I just want to show the actual list of links. I'm not sure what effect inserting another doctype declaration
into a page will have, but it's sure to be illegal as hell. Therefore I had to get rid of the overhead.
Besides, the sitemap also contains links to all archived pages, but I didn't want to show these links in the site navigation. It should consist of links to the active pages only. Therefore I had to somehow separate the archived links from the active ones.
First things first: I created a <div id="mainMenu">
to delimit
the chunk of the sitemap that contains the site navigation information. This solved both problems:
the overhead and the archived links were excluded from this area and would never end up in the site navigation.
Besides, moving this <div>
around a bit allows me change my definition at need.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html lang="en"> <head> <title>QuirksMode - sitemap</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <link rel="stylesheet" href="quirksmode.css" /> <link rel="stylesheet" href="sitemap.css" /> <script src="quirksmode.js" type="text/javascript"></script> <script src="sitemap.js" type="text/javascript"></script> </head> <body> <h2>Sitemap</h2> <div id="header"></div> <p class="intro">All pages on my site.</p> <div id="controls"></div> <h3>General</h3> <div id="mainMenu"> <a href="/">Home</a><br /> [... all links to active pages ...] </div><!-- mainMenu --> <hr /> <h2 id="archive">Archives</h2> <p>The pages below are only retained for historical reasons; the information they contain is outdated.</p> [.. all links to archived pages ...] </body> </html>
Now the site navigation area was adequately defined. However, I still had to find it in the imported sitemap.
Whenever you've XMLHttpRequested a file, you can read out either its responseXML
or its responseText
to get to its actual content. Unsurprisingly, the first treats the file as an XML file, while the second treats it as
plain text. When importing HTML, responseText
is the best option, since it allows you to immediately
paste the result into your page. Conversely, when you use responseXML
you're required to first fire up
a DOM script that goes through the response and converts it to HTML. Therefore I decided to use responseText
.
However, this is wrong:
function setMainNav(req) { document.getElementById('siteNav').innerHTML = req.responseText; }
Now, the entire sitemap is made into a site navigation, and as we just saw that's not what I want. I only want the
content of the <div id="mainMenu">
to form the site navigation. Therefore I first have to
extract the <div id="mainMenu">
from the response somehow.
At first sight it might seem easiest
to take the responseXML
and use getElementById('mainMenu')
. Unfortunately that doesn't work.
In XML files, the getElementById()
method works only if the DTD declares that the id
attribute has
type ID; a mere name ID is not enough. I could have solved this by creating a custom DTD, but I
don't understand them well enough, and besides I'm not sure if creating one would satisfy all browsers (chances are it won't).
The trick is to first load the
responseText
into a generated HTML element that floats around in 'DOM hyperspace' (see section 8K of
the book). This gives you the best of both worlds: DOM methods become available to the sitemap,
but it's not yet present in the actual HTML page.
function setMainNav(req) { var container = document.createElement('div'); container.innerHTML = req.responseText; // container is NOT inserted into the document!
Unfortunately document.getElementById()
still doesn't work: the sitemap is not (yet) part of the
document, so any method of the document won't work. Fortunately getElementsByTagName()
can work on
any DOM node, whether it's present in the document or not. Therefore I go through all <div>
tags
in the sitemap until I encounter the one with id="mainMenu". If I don't find it, something is wrong and the function
ends.
var x = container.getElementsByTagName('div'); var siteMap; for (var i=0;i<x.length;i++) { if (x[i].id == 'mainMenu') { siteMap = x[i]; break; } } if (!siteMap) return;
Now the siteMap
variable refers to the <div id="mainMenu">
and I can insert it.
Afterwards I clean up my mess: container
is made empty.
document.getElementById('siteNav').appendChild(siteMap); container.innerHTML = ''; }
As a result the part of the sitemap delimited by the <div id="mainMenu">
now becomes
the site navigation.
(I left out quite a few other tasks of the setMainNav()
function. For instance, it moves the text
after the links to the links' title
attributes, it searches for the link that leads to the current
page in order to highlight it and make it unclickable, and it sets onclick event handlers that show blocks of links
when the user clicks on a header. All that is relatively straightforward DOM scripting, though,
and it has nothing to do with the importing process. Take a look at the details in quirksmode.js
around line 300.)