views:

38

answers:

1

Say I have a basic page like so:

<custom:TableOfContents />
<h1>Some Heading</h1>
  <h2>Foo</h2>
    <p>Lorem ipsum</p>
  <h2>Bar</h2>
    <p>Lorem ipsum</p>
  <h2>Baz</h2>
    <p>Lorem ipsum</p>
<h1>Another Heading</h2>
  <h2>Qux</h2>
    <p>Lorem ipsum</p>
  <h2>Quux</h2>
    <p>Lorem ipsum</p>

Assume all the header tags exist as server side controls. Is there some web control <custom:TableOfContents /> for ASP.NET webforms that will dynamically generate a table of contents that looks something like the following (when rendered to the screen):

1. Some Heading
1.1. Foo
1.2. Bar
1.3. Baz
2. Another Heading
2.1. Qux
2.2. Quux

Ideally, each entry in the table of contents would be a hyperlink to a dynamically generated anchor at the appropriate place on the page. Also, it would be nice if the text of each header tag could be prefixed with its section number.

If not a web control, is there some easier way of doing this? Keep in mind that many of the header tags are going to be created by data bound controls, so manually maintaining the table of contents is not an option. It seems like the webforms model is ideally suited to creating such a control, which is why I'm surprised I haven't yet found one.

+1  A: 

I needed to do a similar thing a few days ago and, though not a webcontrol, used jQuery.

$(document).ready(buildTableOfContents);

function buildTableOfContents() {
    var headers = $('#content').find('h1,h2,h3,h4,h5,h6');
    var root, list;
    var previousLevel = 1;
    var depths = [0, 0, 0, 0, 0, 0];

    root = list = $('<ol />');

    for (var i = 0; i < headers.length; i++) {
        var header = headers.eq(i);
        var level = parseInt(header.get(0).nodeName.substring(1));

        if (previousLevel > level) {
            // Move up the tree
            for (var L = level; L < previousLevel; L++) {
                list = list.parent().parent();
                depths[L] = 0;
            }
        } else if (previousLevel < level) {
            // A sub-item
            for (var L = previousLevel; L < level; L++) {
                var lastItem = list.children().last();

                // Create an empty list item if we're skipping a level (e.g., h1 -> h3)
                if (lastItem.length == 0)
                    lastItem = $('<li />').appendTo(list);

                list = $('<ol />').appendTo(lastItem);
            }
        }

        depths[level - 1]++;

        // Grab the ID for the anchor
        var id = header.attr('id');
        if (id == '') {
            // If there is no ID, make a random one
            id = header.get(0).nodeName + '-' + Math.round(Math.random() * 1e10);
            header.attr('id', id);
        }

        var sectionNumber = depths.slice(0, level).join('.');

        list.append(
            $('<li />').append(
                $('<a />')
                    .text(sectionNumber + ' '+ header.text())
                    .attr('href', '#' + id)));

        previousLevel = level;
    }

    $('#table-of-contents').append(root);
}

This will make an ordered list and append it to #table-of-contents with appropriate numbering (e.g., 1.1). Just a little bit of CSS is needed to hide the lists' built in numbering: #table-of-contents ol { list-style:none; }.

Matthew Jacobs
Clean code. I like it. A server side control to generate a table of contents would still be useful in certain cases... but for what I'm doing right now, doing it on the client side works perfectly. Thanks!
Michael Kropat