asp.netnavigationweb-controls

Is there a web control to dynamically generate a table of contents?


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.


Solution

  • 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; }.