Table of Contents script

This script crashes Explorer 5.2 on Mac.

In other Explorers on Mac clicking on the 'Contents' bar is only possible with the page scrolled completely up.

The script works in Explorer 5.0 Windows, but on my site this browser subsequently hides all floated elements (like this one) and most long code examples. Therefore I disabled the script in this browser. It might work perfectly in other situations, though.

On this page I explain the Table of Contents script that runs in all pages on this site. It generates a list of all h3's and h4's on the page and offers links to them.

Principles

The main problem was getting both sorts of headers in the order they appear in the source code. I couldn't use getElementsByTagName(), since that would have destroyed this order. Therefore the script searches for all h3's and h4's that are children of the body.

On this site I use h3's for main headers and h4's for sub-headers. In the generated table of contents I want to show this hierarchy. Initially I wanted to use nested uls, but that would have made the script much more complicated. Therefore I decided to fake them.

The script

function createTOC()
{
	if (top.bugRiddenCrashPronePieceOfJunk) return;
	var x = document.body.childNodes;
	var y = document.createElement('div');
	y.id = 'toc';
	var a = y.appendChild(document.createElement('span'));
	a.onclick = showhideTOC;
	a.innerHTML = 'Contents';
	var z = y.appendChild(document.createElement('div'));
	z.onclick = showhideTOC;
	var toBeTOCced = new Array();
	for (var i=0;i<x.length;i++)
	{
		if (x[i].nodeName.indexOf('H') != -1)
			toBeTOCced.push(x[i])
	}

	if (toBeTOCced.length < 2) return;

	for (var i=0;i<toBeTOCced.length;i++)
	{
		var tmp = document.createElement('a');
		tmp.innerHTML = toBeTOCced[i].innerHTML;
		tmp.href = '#link' + i;
		tmp.className = 'page';
		z.appendChild(tmp);
		if (toBeTOCced[i].nodeName == 'H4')
			tmp.className += ' indent';
		var tmp2 = document.createElement('a');
		tmp2.id = 'link' + i;
		if (toBeTOCced[i].nodeName == 'H2')
		{
			tmp.innerHTML = 'Top';
			tmp.href = '#top';
			tmp2.id = 'top';
		}
		toBeTOCced[i].parentNode.insertBefore(tmp2,toBeTOCced[i]);
	}
	document.body.insertBefore(y,document.body.childNodes[2]);
}

var TOCstate = 'none';

function showhideTOC()
{
	TOCstate = (TOCstate == 'none') ? 'block' : 'none';
	document.getElementById('toc').lastChild.style.display = TOCstate;

}

Explanation

First of all I detect Explorer 5.2 Mac by a special variable bugRiddenCrashPronePieceOfJunk. See the making of QuirksMode.org page for more sordid details.

function createTOC()
{
	if (top.bugRiddenCrashPronePieceOfJunk) return;

Preparation

Then I take all children of the body and create a div id="toc" to contain the table of contents.

	var x = document.body.childNodes;
	var y = document.createElement('div');
	y.id = 'toc';

I append a span to it with the text 'Contents'. Clicking on this element runs the showhideTOC() script I explain below.

	var a = y.appendChild(document.createElement('span'));
	a.onclick = showhideTOC;
	a.innerHTML = 'Contents';

The I create yet another div to contain the actual links. This div is initially hidden but can be made visible. Clicking on this div (which really means: clicking on any link) also calls showhideTOC().

	var z = y.appendChild(document.createElement('div'));
	z.onclick = showhideTOC;

Finding the headers

Then I create a new array toBeTOCced and go through all children of the body tag.

	var toBeTOCced = new Array();
	for (var i=0;i<x.length;i++)
	{

I want to insert an internal anchor (<a name>) before every header as a target for the link in the table of contents. It is very important to first create an array with all headers, before doing anything else. If I'd immediately start adding internal anchors my function would create an infinite loop:

  1. The script finds a header. Say it is the 12th child of the body.
  2. Create an internal anchor just before this header. Now this anchor becomes the 12th child of the body.
  3. Go on to the 13th child. Hey, this is a header! In fact, it's the same header as in step 1. The creation of the anchor has moved it up one place.
  4. Create an internal anchor just before this header.
  5. Go on to the 14th child. Again the same header.
  6. etc. etc. The browser never wakes up.

So I push all headers into the array toBeTOCced first. I create a list of all headers in the page before changing the page.

		if (x[i].nodeName.indexOf('H') != -1)
			toBeTOCced.push(x[i])
	}

Now toBeTOCced contains all headers in the order they appear in the source code.

The very first element of the array is the h2 that contains the page title. This is deliberate: I want a 'Top' link as the very first link in my table of contents, and linking it to the page title is the easiest way of doing this.

On the other hand, if the page doesn't contain any h3 or h4 I don't want to create a table of contents at all. So if the array contains 0 or only 1 element, I end the function. (That one element is of course the h2, which doesn't count).

	if (toBeTOCced.length < 2) return;

Creating the table of contents

I can finally start creating my table of contents. Go through toBeTOCced.

	for (var i=0;i<toBeTOCced.length;i++)
	{

Create a new a class="page" element. Its innerHTML becomes the content of the current header. Its href becomes '#link' plus the current value of i. This ensures a unique link name for each TOC link.

Why innerHTML? A few of my headers contain HTML, and I want this HTML to appear in the TOC link, too.

		var tmp = document.createElement('a');
		tmp.innerHTML = toBeTOCced[i].innerHTML;
		tmp.href = '#link' + i;
		tmp.className = 'page';

Append the new TOC link to the TOC.

		z.appendChild(tmp);

If the current element is an h4 I add a class 'indent'. It is meant for faking nested lis.

		if (toBeTOCced[i].nodeName == 'H4')
			tmp.className += ' indent';

Create another a element and give it the same id as the TOC link, but of course without the '#'. This is the internal anchor the link in the TOC jumps to.

		var tmp2 = document.createElement('a');
		tmp2.id = 'link' + i;

If the current element is the h2 page header I use the special value 'Top' for its innerHTML and internal anchor. I don't want to see the page title repeated in the TOC.

		if (toBeTOCced[i].nodeName == 'H2')
		{
			tmp.innerHTML = 'Top';
			tmp.href = '#top';
			tmp2.id = 'top';
		}

Finally insert the internal anchor into the document, just before the header it belongs to.

		toBeTOCced[i].parentNode.insertBefore(tmp2,toBeTOCced[i]);
	}

When we've gone through all headers the TOC is ready. Append the entire TOC block to the document as its third child.

	document.body.insertBefore(y,document.body.childNodes[2]);
}

Why the third child? Because of Explorer on Windows. I give the entire TOC a position: fixed and place it in the upper right corner. Unfortunately Explorer on Windows doesn't support this, so it shows the TOC at the place I insert it into the document.

I want Explorer on Windows to show it directly after the page title. Although initially the page title is the first element in the page, I insert an internal anchor before it, causing it to become the second element. Therefore the TOC should become the third one.

The other browsers don't care where I insert it: they'll show it fixed in the upper right corner anyway.

showhideTOC

Finally a very simple function to show and hide the TOC content. A variable to remember its current state, which initially is 'none'.

var TOCstate = 'none';

Then a function to switch its state from 'none' to 'block' or vice versa.

function showhideTOC()
{
	TOCstate = (TOCstate == 'none') ? 'block' : 'none';
	document.getElementById('toc').lastChild.style.display = TOCstate;

}

This function is called whenever the user clicks on the 'Contents' span, so he can toggle the display of the TOC. In addition, it's called whenever the user clicks on a link. This immediately hides the TOC.