The Holy Grail of TOC Scripts
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
<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>
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 =)
Leave a comment
4 years, 2 months ago
ehmm very interesting
how to create auto-toc for xml files?
9 years, 1 month ago
[…] } } Get the full TOC Code including this addition. A more detailed discussion of my original TOC script is also available. –> Comments &r […]
3 years, 6 months ago
Szymon, the best way to create a TOC with XML would use an XSLT transform. However, IE does not yet support XSLT… so we’re stuck traversing the tree with JS :(
3 years, 4 months ago
In Combo
Okay, my JavaScript Table of Contents doesn’t work in Internet Explorer or Konqueror (which means it probably also breaks in Safari). Firefox and Galeon both work, so presumably just about any Gecko-based browser will work (Gecko is Mozilla’s HTML re…
3 years, 4 months ago
Great script! I have some nitpicks with the generated markup though; it’s not semantic and is difficult to style with CSS…
I suggest the following changes:
1. Loose the table structure, it’s totally unnecessary. Instead, use a div with an id.
2. The content is marked up with b(old). I’d suggest a header instead (e.g. h2).
3. I’d suggest to add an id to the hide/show text to remove it in print stylesheets
4. Instead of adding the numbers and using padding-left for indentation, why not use a nested ordered list? This gives more flexibility for styling, and allows the numbering to be set via CSS as well (e.g. IV.3.a )
I’ve changed 1-3 and put it on my website at http://www.jeroencoumans.nl/js/toc.js
Unfortunately, my coding skills really suck, so I’ve left #4 for anyone who feels up for it! :)
3 years, 4 months ago
@Joren:
Good comments. The markup I used really does suck and I would be happy to get rid of it. It just happened to be what I got working first, and as we all know; sometimes that’s just good enough.
If I can i’ll tinker with your changes and work on some of the others soon; then post an updated copy on this site.
3 years, 4 months ago
That’d be really great! I’m also looking forward to the auto-numbering of the headers, if that’s still in the planning. By the way, what license, if any, is the script under?
3 years, 4 months ago
@Jeroen: I guess I did negelct to specify a “license”. I normally release things under GPL; but my intent is pretty much just to put it in the public domain (if such a thing still exists)
3 years, 4 months ago
As far as I know, public domain still exists, but it never hurts to add a license reference to avoid confusion.
1 year, 5 months ago
Hi, regarding anchor links not working onload, putting the following at the end of a function, registered to the onload event, solved the problem in a custom toc script I’ve made - haven’t tried it in this one, but it should work just as well:
if( window.location.hash.length > 0 ) {
window.location.hash = window.location.hash;
}