Importing the site navigation

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.

Overview

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:

  1. The HTML structure of the content pages would not change.
  2. I add links to new pages in two spots: the overall sitemap, and the table-of-contents page of the relevant section (JavaScript, CSS, etc.)

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.

The source file and accessibility

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:

  1. Every content page (is supposed to) contain a hard-coded link to the sitemap.
  2. Therefore, every noscript visitor is able to go to the sitemap from any page, and use this sitemap to go to any other page. That's less usable than the site navigation, but QuirksMode.org would remain accessible.

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.)

Preparations

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.

Importing the sitemap

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.

Markup

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.

responseXML and responseText

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

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.)