The Holy Grail of TOC Scripts

by @jehiah on 2004-09-25 13:02UTC
Filed under: All , HTML , Javascript

I have attempted to improve upon Peter-Paul Koch’s Table Of Contents script which (thankfully) he does a good job of describing how it works so I don’t need to do that here. My TOC source code is also available.

Read on for an overview of my changes, important notes, and the code you need.

TOC Code

[js]

    <script language="javascript">
	function createTOC()
	{
		// to do : gracefully handle if h2 is top level id and not h1

		// configuration options
		var page_block_id = 'mainContent'; // this is the id which contains our h1's etc
		var toc_page_position =-1; // used later to remember where in the page to put the final TOC
		var top_level ="H1";// default top level.. shouldn't matter what is here it is set at line 50 anyway
		var skip_first = true;

		var w = document.getElementById(page_block_id);
		var x = w.childNodes;
		//build our table tbody tr td - structure
		y = document.createElement('table');
		y.id='toc';
		mytablebody = document.createElement('TBODY');
		myrow = document.createElement('TR');
		mycell = document.createElement('TD');
		myrow.appendChild(mycell);
		mytablebody.appendChild(myrow);
		y.appendChild(mytablebody);

		// create the two title strings so we can switch between the two later via the id
		var a = mycell.appendChild(document.createElement('span'));
		a.id = 'toc_hide';
		a.innerHTML = '<b>Contents</b> <small>[<a href="" onclick="javascript:showhideTOC();return false;">hide</a>]</small>';
		a.style.textAlign='center';
		var a = mycell.appendChild(document.createElement('span'));
		a.id = 'toc_show';
		a.style.display='none'

		a.innerHTML = '<b>Contents</b> <small>[<a href="" onclick="javascript:showhideTOC();return false;">show</a>]</small>';
		a.style.textAlign='center';

		var z = mycell.appendChild(document.createElement('div'));

		// set the id so we can show/hide this div block later
		z.id ='toc_contents';

		var toBeTOCced = new Array();
		for (var i=0;i<x.length;i++)
		{
			if (x[i].nodeName.indexOf('H') != -1 && x[i].nodeName != "HR") // added check for hr tags
			{
				toBeTOCced.push(x[i])
				if (toc_page_position == -1)
				{
					// get the first one.. don't care which level it is
					toc_page_position = 0; 
					// we should also remember which level is top of the page
					top_level = x[i].nodeName;
				}
				else if (toc_page_position == 0)
				{
					toc_page_position = i-1; // we want the toc before the first subheading
				}
			}
		}
		// array to store numeric toc prefixes
		var counterArray = new Array();
		for (var i=0;i<=7;i++)
			{counterArray[i]=0;}

		// quit if it is a small toc
		if (toBeTOCced.length <= 2) return;

		for (var i=0;i<toBeTOCced.length;i++)
		{
			// put the link item in the toc
			var tmp_indent =0;
			// tmp is link in toc
			var tmp = document.createElement('a');
			// tmp2 is name link for this heading ancor
			var tmp2 = document.createElement('a');	

			// we need to prefix with a number
			var level = toBeTOCced[i].nodeName.charAt(1);
			// we need to put in the upper numbers ie: 4.2 etc.
			++counterArray[level];

			tmp.href = '#header_' + i;
			tmp2.id = 'header_' + i;

			for (var j=2;j<=level;j++)
				if (counterArray[j] > 0)
				{
					tmp.innerHTML += counterArray[j]+'.' // add numbering before this toc entry
					tmp_indent +=10;
				}
			tmp.innerHTML +=  ' ' + toBeTOCced[i].innerHTML;

			// if counterArray[+1] != 1 .. reset it and all the above
			level++; // counterArray[level+1] was giving me issues... stupid javascript
			if (counterArray[level] > 0) // if we dropped back down, clear out the upper numbers
			{
				for (var j=level; j < 7; j++)
				{counterArray[j]=0;}
			}

			if (tmp_indent > 10)
				tmp.style.paddingLeft=tmp_indent -10+'px';

			// if NOT h1 tag, add to toc
			if (!skip_first)
			{
				z.appendChild(tmp);
				// put in a br tag after the link
				var tmp_br = document.createElement('br');
				z.appendChild(tmp_br);
			}
			else // else, act as if this item was never created.
			{
				skip_first=false;	
				// this is so the toc prefixes stay proper if the page starts with a h2 instead of a h1... we just reset the first heading to 0
				--level;
				--counterArray[level];
			}



			if (toBeTOCced[i].nodeName == 'H1')
			{
				tmp.innerHTML = 'Top';
				tmp.href = '#top';
				tmp2.id = 'top';
			}
			// put the a name tag right before the heading
			toBeTOCced[i].parentNode.insertBefore(tmp2,toBeTOCced[i]);
		}
		w.insertBefore(y,w.childNodes[toc_page_position+2]); // why is this +2 and not +1?
	}

	var TOCstate = 'block';

	function showhideTOC()
	{
		TOCstate = (TOCstate == 'none') ? 'block' : 'none';
		// flip the toc contents
		document.getElementById('toc_contents').style.display = TOCstate;
		// now flip the headings
		if (TOCstate == 'none')
		{
			document.getElementById('toc_show').style.display = 'inline';
			document.getElementById('toc_hide').style.display = 'none';
		}
		else
		{
			document.getElementById('toc_show').style.display = 'none';
			document.getElementById('toc_hide').style.display = 'inline';
		}
	}

	// now attache the createTOC() to the onload
	window.onload = createTOC;


    </script>

[/js]

Wikipedia Styling

Many kudos to Wikipedia for their styling of contents boxes. While they are not dynamically generated by the browser, I believe they are still dynamically generated on page edit. There are a few changes I made to the script to allow it to follow wikipedia styling in a fairly cross-browser implementation.

The biggest change involves using a table structure instead of a div tag so that it does nto bump out to the full width of the containing element (which is only needed because Internet Explorer does not understand the display:table; property for div tags).

<style>
/* table of contents stuff */
#toc {
    padding:5px;
    background-color:#f0f0f0;
    border:1px solid #aaaaaa;
    margin-right:auto;
    text-align:center;
}
#toc div {text-align:left;}

</style>

Auto Numbering

I believe it is simply easier to follow a table of contents that is auto-numbered. I may eventually automagically add the numbering to the actuall page headings, but for now this works.

Headings from div tag

Peter-Paul’s script made a table of contents for all headings the fell under the root <body> tag of the page, however very few sites have actuall page content directly under the root. Most sites follow a html structure closer to this:

root
|-- contentdiv
|   |-- page
|   |-- paragraph
|   |-- paragraph2
|   `-- subheading
|-- headerdiv
|   |-- links
|   `-- title
`-- leftnavdiv
    |-- link
    |-- link2
    `-- section title

In fact, the only case I believe this doesn’t really happen is when your site is using frames (like Peter-Paul’s). So I changed the script so it pulls all the headings from a specific div instead of the root <html> tag.

Onload Event Handler

I added a slice of code to automatically run the toc generator by onload event.

Downfalls

the friggin anchor links don’t work when you open them from a bookmark etc.. only when they are pulled up from within the page. This might be a good thing though because it adds the usability of a visitor always landing at the top of the page where they have your site navigation (ie: they don’t automatically get lost somewhere towards the bottom of the page). The anchors don’t work because they are not added to the page till after the onload event fires. If someone wants to write some code to run in the onload event that would check to see if a visitor is trying to go to a specific part of the page, and take them there.. I’d be happy to accept it =)

Subscribe via RSS ı Email
Jehiah Czebotar