Recently, I had a problem where I had to make a Table of Contents out of the headings on a page. This would be a trivial task if we could just list out all the headings in one flat list:

var headings = $('h1, h2, h3, h4, h5, h6');
var listWrapper = $('#toc'); // the container where our table-of-contents goes
var counter = 0;
headings.each(function() {
    $(this).attr("id", "h" + (++counter));
    var li = $('<li></li>')
        .append($('<a></a>')
            .attr( { "href" : "#" + $(this).attr("id") } )
            .html($(this).html());
         ); // this creates the list item, and links it to the appropriate header
    listWrapper.append(li);
} );

This would generate a straight list of all the headings on the page, with no apparent hierarchical order. It would also work with any heading <h1> to <h6>. If you’re not concerned with hierarchical order, this will work beautifully.

However, what if we have to generate a hierarchical table of contents, like what Wikipedia does in its articles? This is a bit of a tricky problem, as header tags are not nested. An h2 can appear at any level in the DOM in relation to an h6 tag. With no sense of inherent hierarchical order in the heading tags, how do we make a hierarchical structure?

Instead of a straight up list, we’ll use a nested list. This list can be ordered, or unordered, but it would give the end user a sense of the structuring of the document. One example of a list could be like this:

wikipedia-toc

The numerical value of the heading tag would correspond to the depth of the list-item in the table of contents. Now, we can write a pseudocode skeleton for our table of contents generator.

var headings = $('h1, ... , h6');
var previousLevel = 0;
var list = $("#myList");
var result = list;
for(all headings) {
	var level = $(this).level; // get the numeral part of the <h_> tag.
	var li = // make the list item
	if(level &gt; previousLevel) {
		// start a new level, and add the list item
	}
	else if(level &gt; previousLevel) {
		// append a list-item to a parent list
	}
	else if(level == previousLevel) {
		// append a list-item to the current list
	}
	// update previous level
	previousLevel = level;
}

Now we can extrapolate this pseudocode to actual operations. Here’s what’s going to be tricky:

  • keeping track of the depth that we’re at
  • going back up to a previous level
  • making sure list-items are being inserted at the correct level

To solve the first problem, we use a jQuery variable, result, to keep track of the list item that we’re currently using. For every traversal through the loop, we can append a new list to result, we can append a list item after result (to the current list), or we can append a list item to a higher-level list. After this, we update result to the list-item that we added.

  • Start a new list, append li to list: result.append($('<ul></ul>').append(li));
  • Append li to the current list: result.parent().append(li);
  • Append li to a higher-level list: result.parent('ul:eq(' + level + ')').append(li);

The last item has an eq:(...) expression, which basically calculates how far up the list that we should append our list-item. To read more about eq, see the jQuery documentation.

Making the page navigable

Now that we have a rough idea of how to list out the headings on a page in a ul, how do we implement the navigation functionality? After all, we would like to be able to click an item in the TOC and navigate to that specific heading.

The solution is to introduce ids for every heading on the page, and make corresponding a href links in each list-item. The easiest way to do so is to serialize the headings by the order that they appear on the page:

var headings = $('h1, h2, h3, h4, h5, h6');
var counter = 0;
headings.each(function() {
    var htmlId = "h" + (++counter);
    $(this).attr( { 'id': htmlId; } ); // "h" in front, since IDs must start with a letter
    // create the list item and make it clickable
    var $li = $('<li></li>')
        .append($('<a></a>')
            .html( $(this).html() )
            .attr( { 'href': "#" + htmlId } ) // makes it clickable and goes to correct location
        );
} );

Putting it all together

Now that we’ve built the parts of a reliable, self-generating, self-updating TOC, we can put those parts together for our jQuery TOC. For reusability purposes, we’ll roll it into a function. We’ll also add some code to manage how deep to make the TOC and what kind of list (unordered or ordered) that it should be.

/**
 * Generates a table of contents for the document
 * @param where the selector to which to append the T.O.C
 * @param isOrdered ordered or unordered list?
 * @param depth of the TOC
 */
function generateTOC(where, isOrdered, tocDepth) {
	var $result = where;
	var list = isOrdered ? "ol" : "ul";
	var depth = tocDepth || 6;
	var curDepth = 0;
	var counter = 0;
	var select = 'h1';
	for(var i = 2; i <= Math.min(depth, 6); i++) {
		select += ", h" + i;
	}
	$(select).each(function() {
		var depth = parseInt($(this).prop('tagName').charAt(1));
		var htmlId = "h" + (++counter);
		$(this).attr( { 'id': htmlId } );
		var $li = $('<li></li>')
			.append($('<a></a>')
				.html($(this).html())
				.attr( { 'href': '#' + htmlId } )
			);
		if(depth > curDepth) {
			// going deeper
			$result.append($('<' + list + '></' + list + '>').append($li));
		} else if (depth < curDepth) {
			// going shallower
			$result.parents(list + ':eq(' + (curDepth - depth) + ')').append($li);
		} else {
			// same level
			$result.parent().append($li);
		}
		$result = $li;
		curDepth = depth;
	});
}

Usage and examples

To use this function, call in $(document).ready(), or where appropriate:

generateTOC($('#my-table-of-contents'));

You can have an ordered list by calling generateTOC($(...), true);, or unordered list by calling generateTOC($(...), false);.

Credits to http://stackoverflow.com/questions/497418/produce-heading-hierarchy-as-ordered-list for the inspiration.

Published by Geoffrey Liu

A software engineer by trade and a classical musician at heart. Currently a software engineer at Groupon getting into iOS mobile development. Recently graduated from the University of Washington, with a degree in Computer Science and a minor in Music. Web development has been my passion for many years. I am also greatly interested in UI/UX design, teaching, cooking, biking, and collecting posters.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.