The Holy Grail of TOC Scripts

posted September 25th 2004 at 1302 EDT in 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



    <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 =)

10 Responses

  1. #1 Szymon
    4 years, 2 months ago

    ehmm very interesting
    how to create auto-toc for xml files?

  2. #2 Jump to Page Anchor
    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. #3 Andy Wirtanen
    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 :(

  4. #4 Mike Hicks
    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…

  5. #5 Jeroen Coumans
    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! :)

  6. #6 jehiah
    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.

  7. #7 Jeroen Coumans
    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?

  8. #8 jehiah
    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)

  9. #9 Jeroen Coumans
    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.

  10. #10 Björn Þór Jónsson
    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;
    }

Leave a comment