简体   繁体   中英

Recursive JS Function to List Hx HTML tags?

I have some code:

$('section').each(function() {
    var the_id;
    var list_item;
    $(this).find('h2').each(function() {
        the_id = $(this).attr('id');
        $('nav > ol').append('<li><a href="#' + the_id + '">' + $(this).text() + '</a></li>');
        $(this).siblings('h3').each(function(i) {
            the_id = $(this).attr('id');
            if (i == 0) {
                $('nav > ol > li:last').append('<ol></ol>');
            }
            $('nav > ol > li:last > ol').append('<li><a href="#' + the_id + '">' + $(this).text() + '</a></li>');
        });
    });
});

It generates some HTML like:

<li>
   <a href="#example-1">Example 1</a>
   <ol>
      <li>
         <a href="#example-1a">Example 1a</a>
     </li>
     <li>
         <a href="#example-1b">Example 1b</a>
     </li>
   </ol>
</li>
<li>
   <a href="#another-example">Another Example</a>
</li>
<li>
  <a href="#last-one">Last One</a>
</li>
<li>
   <a href="#just-kidding--another">Just kidding, another</a>
   <ol>
      <li>
         <a href="#this-is-a-sub-header">This is a sub header</a>
      </li>
   </ol>
</li>

My issue is, my JS only goes as far as i write it (h2, then looks for h3s, and i'd have to write another callback for doing h4's then another for h5's and h6's. How can I write a recursive function that does this?

How about another approach to building the list HTML? I really suck at writing/understanding recursive code. Iteration is usually easier for me.

function buildToc(container) {
    var html = "", lastLevel = -1;
    container.find('h1, h2, h3, h4, h5, h6, h7, h8, h9').each(function() {
        var $this = $(this);
        var level = this.tagName.match(/H(\d)/)[1];
        if (lastLevel < level) {
            html += "<ol>";
        }
        if (lastLevel > level)  {
            html += "</ol>";
        }
        html += "<li><a href='"+ this.id + "' >" + $this.text() + "</a></li>";
        lastLevel = level;
    });
    return html;
}
$('nav').append( buildToc( $('article section') ) );

I ran that on your page and it duplicated your existing TOC. And you don't need custom code for each level; Quick and dirty.

A bit of staring at this and I think I figured out what you're going for. Each level isn't actually different, you've just explained the minimum in levels to get it all. Like Matt's comment says, you need to declare the callbacks separately. But you also need to pass your current level of list into it, so you can append to the right place. We can try an approach that uses a method for constructing the callbacks, like this:

function Generator(e, level) {
    var list = e; // I think you need this for the closure to work, but my js is rusty so I may be wrong.

    return function() {
        var new_list = $('<ol></ol>');
        list.append(new_list); // you might want to make sure there's more than 0 siblings?

        $(this).siblings('h' + level).each(function(i) {
            the_id = $(this).attr('id');
            new_list.append('<li><a href="#' + the_id + '">' + $(this).text() + '</a></li>');
            if (level < 6) { // your max h tag level
                Generator(new_list, level + 1)();
            }
        });
    }
}

I'm not sure if I implemented the thing you're trying to do correctly, but the idea is that you can keep creating sub lists and pass them back into the function recursively. One of your original problems is that you have to make your jQuery selector deeper each time. This approach fixes that. You call it simply:

$('section').each(Generator($('nav'), 2)); // maybe not nav, something close to that

Obviously your code starts out by using $(this).find('h2') instead of $(this).siblings('h2') , so some adjustment is needed in that area. But I'm sure that's not a big deal.

I didn't test this, so I probably made at least one mistake somewhere.

A quick Google search revealed this script ; it's plain 'ole javascript, but you should be able to adapt it to your needs (or, just borrow the idea and write your own code).

After playing around with this some, I think Juan is right: the iterative approach seems easier. I put together a quick jQuery plugin that uses a similar approach:

(function($) {
    $.fn.buildTOC = function(options) {
        var opts = $.extend({
                scan: $(document) // By default, search the entire page for headings
            }, options),
            $toc = $(this),       // This is where we'll place our TOC links

            /*
            * Get the current level from the Heading tag.
            */
            getLevel = function(h) {
                return parseInt(h.substring(1));
            },

            /*
             * Creates a new sublist and returns it.
             * The randomly-generated ID just makes it easier to find the new list.
             */
            pushLevel = function(toc) {
                var id = 'node' + Math.round(Math.random() * 50000 + 1);
                toc.append($('<ol id="' + id + '"></ol>'));
                return $('#' + id, toc);
            },

            /*
             * Returns the last sublist containing an element at the current level;
             * otherwise, returns the parent list (for top-level items).
             */
            popLevel = function(toc, level) {
                var sub = $('.toc-level-' + level + ':last', toc);
                if (sub.length) {
                    return sub.parent();
                } else {
                    return $toc.children('ol');
                }
            },

            /*
             * Appends a link for the current tag to the current list.
             * Also adds a class for the current level (handy for styling), so it's easy
             * to find items at this level.
             */
            appendLink = function(toc, tag, level) {
                toc.append($('<li class="toc-level-' + level + '"><a href="#' + tag.id + '">' + tag.innerHTML + '</a></li>'))
            },

            buildTOC = function(toc) {
                var headings = $('h1,h2,h3,h4,h5,h6', opts.scan),
                    lastLevel = 0;
                for (var i=0, len=headings.length; i<len; i++) {
                    var currTag = headings[i],
                        currLevel = getLevel(currTag.tagName);
                    if (lastLevel == currLevel) {
                        // Siblings: just add a link for this item
                        appendLink(toc, currTag, currLevel);
                    } else if (lastLevel < currLevel) {
                        // Child: create a new list and append to that
                        toc = pushLevel(toc);
                        appendLink(toc, currTag, currLevel);
                    } else {
                        // Parent: move back out to the appropriate list
                        toc = popLevel(toc, currLevel);
                        appendLink(toc, currTag, currLevel);
                    }
                    lastLevel = currLevel;
                }
            };

        buildTOC($toc);
    };
})(jQuery);

You'd use it like so:

$(function() {
    $('#toc').buildTOC();
})

Where toc is the ID of the container where the links should go.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM