Table of Contents script

See section 8E of the book for an explanation of creating HTML elements.

This script crashes Explorer 5.2 on Mac.

The h3s and h4s are not in the correct order in Safari 1.3.2, because it doesn't support my getElementsByTagNames() function.

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.

If you want to use it yourself you also need my getElementsByTagNames() function.

The script

function createTOC() {
	var y = document.createElement('div');
	y.id = 'innertoc';
	var a = y.appendChild(document.createElement('span'));
	a.onclick = showhideTOC;
	a.id = 'contentheader';
	a.innerHTML = 'show page contents';
	var z = y.appendChild(document.createElement('div'));
	z.onclick = showhideTOC;
	var toBeTOCced = getElementsByTagNames('h2,h3,h4,h5');
	if (toBeTOCced.length < 2) return false;

	for (var i=0;i<toBeTOCced.length;i++) {
		var tmp = document.createElement('a');
		tmp.innerHTML = toBeTOCced[i].innerHTML;
		tmp.className = 'page';
		z.appendChild(tmp);
		if (toBeTOCced[i].nodeName == 'H4')
			tmp.className += ' indent';
		if (toBeTOCced[i].nodeName == 'H5')
			tmp.className += ' extraindent';
		var headerId = toBeTOCced[i].id || 'link' + i;
		tmp.href = '#' + headerId;
		toBeTOCced[i].id = headerId;
		if (toBeTOCced[i].nodeName == 'H2') {
			tmp.innerHTML = 'Top';
			tmp.href = '#top';
			toBeTOCced[i].id = 'top';
		}
	}
	return y;
}

var TOCstate = 'none';

function showhideTOC() {
	TOCstate = (TOCstate == 'none') ? 'block' : 'none';
	var newText = (TOCstate == 'none') ? 'show page contents' : 'hide page contents';
	document.getElementById('contentheader').innerHTML = newText;
	document.getElementById('innertoc').lastChild.style.display = TOCstate;
}

Explanation

The script works as follows:

Preparations

First I create a <div id="innertoc"> to hold the table of contents.

function createTOC() {
	var y = document.createElement('div');
	y.id = 'innertoc';

I append a <span> to it with the text 'show page contents'. Clicking on this element runs the showhideTOC() script I explain below.

	var a = y.appendChild(document.createElement('span'));
	a.onclick = showhideTOC;
	a.id = 'contentheader';
	a.innerHTML = 'show page contents';

The I create yet another <div> to contain the actual links. Clicking on this <div> (which really means: clicking on any link in this <div>) also calls showhideTOC().

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

Getting the headers

Then I create a new array toBeTOCced and use my getElementsByTagNames() function to get all headers in the document.

	var toBeTOCced = getElementsByTagNames('h2,h3,h4,h5');

If there's only one item in this array (i.e. the page only contains the <h2> page title) I stop the function. I don't want a ToC with only one item.

	if (toBeTOCced.length < 2) return false;

Creating the ToC

Now I start creating the ToC. I go through the toBeTOCced array. For each element I create a link that has the same text as the header. Note the use of innerHTML: a few headers on this site contain HTML tags like <code>, and I want these to show up in the ToC, too. I append these newly created links to the ToC's inner <div>.

	for (var i=0;i<toBeTOCced.length;i++) {
		var tmp = document.createElement('a');
		tmp.innerHTML = toBeTOCced[i].innerHTML;
		tmp.className = 'page';
		z.appendChild(tmp);

I add an extra class to the link if the current header is an <h4> or <h5>. These classes create the fake nested lists.

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

Now we have to link the <a> element I just created with the header it refers to. This requires the creation of a unique ID. However, the header may already have an ID, since on a few old pages on this site I use internal links (<a href="#somewhere>/<h3 id="somewhere">). I don't want to break these internal links, so I preferably use the ID the header already has. Only when the header has no ID yet do I create one by appending the number of the current header to the text 'link'.

		var headerId = toBeTOCced[i].id || 'link' + i;

The link we just created gets an href of '#' plus the newly created ID, and the header itselfs also receives the correct ID.

		tmp.href = '#' + headerId;
		toBeTOCced[i].id = headerId;

Finally a special case: if the header is the <h2> that heads the page, it's called 'top' and also gets that ID.

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

The Table of Contents is now ready; we return it to whichever function asked for it.

	return y;
}

Showing and hiding the ToC

Finally, the function that shows and hides the ToC. It's very simple: it first checks the current ToC state, and then generates a new text and a new display value based on that information.

var TOCstate = 'none';

function showhideTOC() {
	TOCstate = (TOCstate == 'none') ? 'block' : 'none';
	var newText = (TOCstate == 'none') ? 'show page contents' : 'hide page contents';
	document.getElementById('contentheader').innerHTML = newText;
	document.getElementById('innertoc').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.