Generating a Table of Contents

or - why should I have to do this by hand, that's what computers are for!!

by Patrick Horgan

(Back to javascript tutorials.)

It's simple to use

In order to use the table of contents generator, you need to

Don't worry that the javascript might mess up your ids. If there's an existing id on a header tag, then the script uses it. If not it will add one so we have a target for the link.

The memory footprint of the generator is small, and after the table of contents is created, the generator is deleted freeing up even that small amount.

The code works fine in javascript 'strict' mode so you don't have to worry if you (as do I) run all your code in strict mode.

Here's the structure that we'll build

We take the existing toc div and put two things inside of it:

<div> id='toc'>

<span id='toc_toggle_target'> Click to show Table of Contents </span>

<div> id='toc_box'>


<a class='toc_classH2' href='#id of some h2 header'>The text from that same header</a>

<a class='toc_classH3' href='#id of some h3 header'>The text from that same header</a>

...

<a class='toc_classH3' href='#id of some h3 header'>The text from that same header</a>

etc.

Inside the div toc_box we put a horizontal rule to separate the links and then a series of anchor links. Each one of them is built to represent one of the headers in the file, and has a class made up by taking the text toc_class and appending to it the particular header's nodeName, one of H1, H2, H3, H4, H5, or H6. The result are the class names toc_classH1, toc_classH2, toc_classH3, toc_classH4, toc_classH5, and toc_classH6. We use those later to apply styles to them.

So here's the script

This is used by the script, and gets a style value from an object

function getStyleObj(obj,styleProp) { // Got this from David Cramer // davidcramer.posterous.com/code/84/get-offsets-xy-for-an-object-javascript.html if (obj.currentStyle){ var s = obj.currentStyle[styleProp]; } else if (window.getComputedStyle){ var s = document.defaultView.getComputedStyle(obj,null). getPropertyValue(styleProp); } return s; }

The getStyleObj function, given a reference to an object, and the name of the style, will fetch the value of the style for you. I use it to get the min-width and max-width for the table of contents so that the user can choose the minimized and opened widths for the table of contents.

toc_generator object

I've made the table of contents generator an object so that none of its variables and methods pollute the global namespace. That means that we have to declare one before you can use it, and we'll see that at the bottom of the script.

In case you don't know, you make an object in javascript by declaring a function. Anything declared with this. a part of the name is a public class variable, and anything declared with var is a private class variable. Making it an object puts everything together in one tidy package.

begin toc_generator initialization

function toc_generator() { var toc_clicktoshow='<strong>Click to show Table of Contents</strong>'; var toc_clicktohide='<strong>Click to hide Table of Contents</strong>'; var toc_show_width='65em'; var toc_hide_width='25em';

I set up the strings to be shown to ask the user to expand and contract the table of contents, and also set the default widths that the table of contents will be expanded and contracted to. Later I might very well make the strings and the widths arguments to the class function, but as of yet I haven't had a need. Feel free to change it if you wish. As long as you put in the code that you got it from me, I'm happy for you to use it for any purposes at all.

Keep track of the html elements

var toc=document.getElementById('toc'); var toc_toggle_target; var toc_box;

Here you see that we declare a local variable toc, and use it to hold a reference to the toc div. We also declare two variables, toc_toggle_target and toc_box which we'll use in a minute when we create them.

Try to get widths from the style sheet

var minWidth=getStyleObj(toc,'min-width'); if(minWidth!='0px'){ toc_hide_width=minWidth; } var maxWidth=getStyleObj(toc,'max-width'); if(maxWidth!='none'){ toc_show_width=maxWidth; }

Here I check to see if someone has applied a min-width or a max-width style to the toc div. Annoyingly enough, if min-width doesn't exist the DOM returns '0px' but if max-width doesn't exist the DOM returns 'none'. The CSS2.1 spec lists the initial value of min-width as 0, and of max-width of 'none' so it's probably portable. If anyone knows more about this please feel free to email me. In any case, if I can tell that they're set, I override the default values with the ones from the style sheet.

callback to toggle visibility

var toggleTOCVisibility=function() { if(toc_toggle_target.innerHTML==toc_clicktoshow){ toc_toggle_target.innerHTML=toc_clicktohide; toc_box.hidden=false; toc_box.style.display='block'; toc.style.width=toc_show_width; }else{ toc_toggle_target.innerHTML=toc_clicktoshow; toc_box.hidden=true; toc_box.style.display='none'; toc.style.width=toc_hide_width; } }

This private method is the callback that's called when we click on the table of contents. Remember the two strings that ask the user to click to hide or show the table of contents, as appropriate? Their presence as the html to display in the toc_toggle_target is used as my signal to know whether the table of contents is now displayed or hidden. If toc_toggle_target's innerHTML has the same value as the toc_clicktoshow string, then we're currently hidden and preparing to show. Otherwise, we're currently showing and preparing to hide.

So when we change state, we currently change four things:

Get a list of all the headers in the order they occur in the page

var getHeaders=function() { var headers=new Array(); var hdr_names=[ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ]; for(var i=0;i<hdr_names.length;i++){ var elems=document.getElementsByTagName(hdr_names[i]); for(var j=0;j<elems.length;j++){ headers.push(elems[j]); } } // Now sort them // I learned how to do this from Peter-Paul Koch from here: // http://www.quirksmode.org/dom/getElementsByTagNames.html if (headers[0].sourceIndex) { headers.sort(function (a,b) { return a.sourceIndex - b.sourceIndex; }); } else if (headers[0].compareDocumentPosition){ headers.sort(function (a,b) { return 3-(a.compareDocumentPosition(b) & 6);}); } return headers; }

We create an Array called headers, fetch all of the headers of each type in turn, and push everything onto the end of the headers array. The problem with this of course is that now we have all of the h1s, then the h2s, through the h6s, surely not the same as the order they occured in the page. We could have iterated through all document elements and inserted the headers in the array as we found them and then they'd be in order. It would be slower though.

So next we need to sort. We're going to call the sort() method on the headers array, which expects a comparison function as its argument. The comparison function which when passed two array elements, a and b, will return:

So the only hard part is coming up with a comparison function for sort that will return what we need. There are two choices, neither one of which works for all browsers.

Headers have a sourceIndex attribute

We look at the first element of the array, (it could have been any) to see if if headers[0].sourceIndex exists. If it does, (IE and Opera), then it would be the index value of the element in the array we would get if we hypothetically called getElementsById('*');.

subtracting the sourceIndex of two elements will do what we want. If, for example, one header had a sourceIndex of 17, and the other 42, then subtracting 42-17 is a number > 0 so we know that 17 preceeds 42. If we were comparing them in the opposite order, we'd have 17-42 is a number < 0 so we know that 42 follows 17.

Headers have a compareDocumentPosition method

compareDocumentPosition is a DOM Level 3 method. If it exists (IE>8 or Firefox, Safari, Chrome, Opera), then we'll use it. It returns a bitmask made up of the following values:

DOCUMENT_POSITION_DISCONNECTED = 0x01; DOCUMENT_POSITION_PRECEDING = 0x02; DOCUMENT_POSITION_FOLLOWING = 0x04; DOCUMENT_POSITION_CONTAINS = 0x08; DOCUMENT_POSITION_CONTAINED_BY = 0x10;

Our job is to turn this into something that the sort method will want. First we see that we only care about two values, DOCUMENT_POSITION_PRECEEDING, or DOCUMENT_POSITION_FOLLOWING. There bit values add up to 6, so we take the return value, and do a logical AND with 6 to get rid of anything we don't care about. We're left with either a 2 if the second one preceeds (we want this case to turn into a postitive number) the first, or a 4 if the second one follows the first (we want this one to turn into a negative number). If we subtract the value from 3, i.e. either 3 - 2, or 3 - 4, we get just what we want.

Generate the content for the table of contents

You'll notice that the method for this function is not assigned to a var, but rather to this.genTOC. That means that genTOC is a public method, and users of this object can see and use it. (In point of fact it is the only public method.)

Provide an early out

this.genTOC=function() { if(!toc){ return; }

First, if there was no <div id='toc'></div> in the document, then we return right away. This way if the script file containing and instantiating this class is included in a file that doesn't want a table of contents we bail out early, no harm, no foul. As you'll see, after we generate the table of contents (or decide that we aren't going to in this case), we throw away the instantiated toc_generator so that the memory used in the instantiation can be reclaimed by the javascript garbage collector.

Early initialization and create the toc_toggle_target span

toc.onclick=toggleTOCVisibility; toc.style.width=toc_hide_width; toc_toggle_target=toc.appendChild(document.createElement('span')); toc_toggle_target.id='toc_toggle_target'; toc_toggle_target.innerHTML=toc_clicktoshow;

We set the click callback for the table of contents, and, assuming that we're going to start off by hiding the table of contents, set the width to the narrow width.

Then we create the span, toc_toggle_target the only part that remains visible when the table of contents is hidden. We set its id so we can style it later, and set its innerHTML to ask the user to click it to show the table of contents.

Now create toc_box to hold all the contents of the TOC

toc_box=toc.appendChild(document.createElement('div')); toc_box.id='toc_box'; toc_box.hidden=true; toc_box.style.display='none'; // hr just to separate the toggle target toc_box.appendChild(document.createElement('hr'));

We create the toc_box div and set its id so we can style it, make it hidden with no display as discussed above in the discussion of the toggleTOCVisibility method, and as a first child of the div, append a horizontal rule.

Get the headers and use them to build the table of contents

this.headers=getHeaders(); // get all headers in order // below, /\bnotoc\b/ is a literal regular expression. It's compiled at // the time the script is instantiated rather than when it's run. // The regular expression is \bnotoc\b. \b means word boundary // so it would match 'foo notoc' or 'notoc foo' // but not 'foonotoc' or 'notocfoo' for(var i=0;i<this.headers.length;i++){ if(!/\bnotoc\b/.test(this.headers[i].className)){ var toc_line=toc_box.appendChild(document.createElement('a')); toc_line.innerHTML=this.headers[i].innerHTML; toc_line.className='toc_class'+this.headers[i].nodeName; if(!this.headers[i].id){ this.headers[i].id='toc_link'+i; } toc_line.href='#'+this.headers[i].id; } delete this.headers; this.headers=null; }

We loop through all the headers, and for each one we first check via regular expression if its class contain 'notoc'. If not, we'll put it into the table of contents. We create an anchor link and append it to the toc_box div. For each one, we:

At the end we free up the headers since we used new to allocate the Array.

Now we're done. Instantiate the object, call its genTOC method and then free up the memory from creating it

} } this.toc=new toc_generator(); this.toc.genTOC(); // Automagically try to generate toc. delete this.toc; this.toc=null;

So far we've been declaring the constructor for an object called toc_generator(). Now we want one, so we assign it to toc, call the method to create the table of contents (remember it returns without doing anything if the toc div doesn't exist), and then delete the object. We're done.

The CSS for the Table of Contents

If we ran it for this page, without any styling, the result would be an unholy mess. The anchors we'd created would all be on the same line. And the Click to show Table of Contents line wouldn't stand out at all as a target to be clicked on.

Style that applies to the whole table of contents

#toc { text-align: center; min-width: 9em; max-width: 50em; background-color: rgb(250,250,250); font-family: "Century Gothic","Apple Gothic",sans-serif; -webkit-box-shadow: inset 1.5px .7px 1px 1.5px rgba(100, 100, 100, 10); -moz-box-shadow: 1.5px .7px 1px 1.5px rgba(100, 100, 100, 10);; box-shadow: 1.5px .7px 1px 1.5px rgba(100, 100, 100, 10);; }

We center the text because we want the toc_toggle_target span text to be centered. We can't style the span to do it, because that would just center inside the span, not center the span in the table of contents.

Here's where we can set how narrow and wide the table of contents is with min-width and max-width. We make the background color slightly greyer than the white so it will show up, set the font we want, and for newer browsers add a drop shadow.

styling the toggle target

span#toc_toggle_target{ font-size: 55%; cursor: pointer; }

We make the font smaller for the part we click on, and set the cursor. Without setting the cursor, we'd get the I-Beam cursor like you would for text, and it wouldn't communicate to the user that they should click on it.

Style for all the anchors.

#toc a{ display: block; text-align: left; text-decoration: none; color: rgb(0,0,0); }

We set display to block so we get one anchor per line, without having to insert <br /> from the script. It seems more elegant. We align left to get rid of the center text-align inherited from the parent div, set the text decoration to none so links are not underlined, and set the color to black for the text.

Set style for anchors when hovered or visited

#toc a:hover{ color: rgb(0,0,255); text-decoration: underline; } #toc a:hover:visited{ color: rgb(255,130,205); text-decoration: underline; }

When people put their cursor over a link we underline them to make it clear that these are links.

If they haven't visited them we turn them blue, and if they have, we turn them purple.

Now we set the indents for the anchors

a.toc_classH1{ margin-left: 0em; } a.toc_classH2{ margin-left: 1em; } a.toc_classH3{ margin-left: 2em; } a.toc_classH4{ margin-left: 3em; } a.toc_classH5{ margin-left: 4em; } a.toc_classH6{ margin-left: 5.0em; }

This simply allows us to make higher header numbers be indented more.

And that's all there is

I hope this helps you learn more about javascript. Click for the lastest version of my utilities.js script

(Back to javascript tutorials.)