Automatic Page Contents

It's been so long since I last posted a tutorial here that I'm afraid everyone might have forgotten about the place. For the past few months, there has been a little "Page Contents" menu at the top-right corner of some of the pages on this site — actually, any page that has more than one <h2> elements in the main content area. In this entry, I'd like to demonstrate how to create an automatic page contents list using jQuery.

The Plugin Option

Before we begin, I should let you know that there are a number of plugins that do what I'm about to show you. If you want to go the plug-in route, take a look at one of these:

But if you want to see how to do this sort of thing on your own, read on...

Let's Begin

Here is what we need to do:

  1. Find out if there is more than one <h2> in the main content area of the page (on this site, the home page and most listing pages will have more than one). If so, we start building the table of contents...
  2. Create a new container <div> for the table of contents and insert a heading and an empty div inside.
  3. Copy the <h2> elements and the <a> elements inside them.
  4. Change the attributes of the copied links.
  5. Insert each copied <h2> along with its contained link into the page-contents <div>.

When we're finished, we should have something that looks like the image to the right:

Create the container

Okay, so our first step is to create the container. We'll wrap our code in a $(document).ready(), as we do nearly every time, so that the script loads when the DOM (but not necessarily image, etc.) has loaded. Then we make sure that we're dealing with more than one <h2> and, if so, start building:

[js]$(document).ready(function() { if ( $('#content h2').length> 1) { $('
') .prepend('

Page Contents

') .append('
') .prependTo('body'); } });[/js]

Okay, maybe a little explanation is warranted here. The first line inside our if condition (line 3) creates a new <div> element with an id of "page-contents." Line 4 inserts an <h3> at the beginning of that new <div> and line 5 inserts an empty <div> at the end of the page-contents <div>. Finally, line 5 inserts the whole group of newly created elements at the beginning of the <body>.

Copy the headings

The next part involves copying all of the <h2> elements, and while we're at it, changing the href of the copied <a> elements to point to the original <h2>s. How do we know where to point them? Each of the original <h2>s has an id associated with it. We just take the id, prepend a "#", and attach it to the href:

[js]$(document).ready(function() { if ( $('#content h2').length> 1) { // ... etc. ... $('#content h2').each(function(index) { var $this = $(this); var thisId = this.id; $this .clone() .find('a') .attr({ 'title': 'jump to ' + $this.text(), 'href': '#' + thisId }) .end() .attr('id', 'pc-' + index) .appendTo('#page-contents div'); }); } });[/js]

We use a .each() method here so that we can apply a number of things to each <h2> in turn. Inside this method, we start by declaring a couple variables—one for $(this), because we use it twice, and one for each id, because we use it to set the hrefs.

So, for each <h2>, we make a copy using .clone() and find the <a> inside of it. Then we set each link's title and href attribute.

When we're finished with the links, we back up to the cloned <h2> elements by using .end(). We want to ensure that we don't duplicate the id along with the element, so we prepend "pc-" to it, keeping each one unique.

When all is ready, we append each <h2> to the <div>inside the page-contents container.

A Little Extra Flair

Now, for an optional addition to our script, we can hide the page-contents <div> in a style sheet (by setting it to display: none). Then we attach a click handler to the page-contents heading with a .toggleClass() to change the background image and a .slideToggle() to show and hide the same-page links.

Here is the complete script, with the additional click handler at the end:

[js]$(document).ready(function() { if ( $('#content h2').length> 1) { $('
') .prepend('

Page Contents

') .append('
') .prependTo('body'); $('#content h2').each(function(index) { var $this = $(this); var thisId = this.id; $this .clone() .find('a') .attr({ 'title': 'jump to ' + $this.text(), 'href': '#' + thisId }) .end() .attr('id', 'pc-' + index) .appendTo('#page-contents div'); }); $('#page-contents h3').click(function() { $(this).toggleClass('arrow-down') .next().slideToggle('fast'); }); } });[/js]