{"id":4661,"date":"2014-04-14T23:11:03","date_gmt":"2014-04-15T06:11:03","guid":{"rendered":"http:\/\/g-liu.com\/blog\/?p=4661"},"modified":"2014-04-16T15:33:27","modified_gmt":"2014-04-16T22:33:27","slug":"walkthrough-making-a-self-updating-table-of-contents-with-jquery","status":"publish","type":"post","link":"https:\/\/g-liu.com\/blog\/2014\/04\/walkthrough-making-a-self-updating-table-of-contents-with-jquery\/","title":{"rendered":"Walkthrough: Making a self-updating table of contents with jQuery"},"content":{"rendered":"<p>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:<\/p>\n[code language=&#8221;javascript&#8221;]\nvar headings = $(&#8216;h1, h2, h3, h4, h5, h6&#8217;);<br \/>\nvar listWrapper = $(&#8216;#toc&#8217;); \/\/ the container where our table-of-contents goes<br \/>\nvar counter = 0;<\/p>\n<p>headings.each(function() {<br \/>\n    $(this).attr(&quot;id&quot;, &quot;h&quot; + (++counter));<br \/>\n    var li = $(&#8216;&lt;li&gt;&lt;\/li&gt;&#8217;)<br \/>\n        .append($(&#8216;&lt;a&gt;&lt;\/a&gt;&#8217;)<br \/>\n            .attr( { &quot;href&quot; : &quot;#&quot; + $(this).attr(&quot;id&quot;) } )<br \/>\n            .html($(this).html());<br \/>\n         ); \/\/ this creates the list item, and links it to the appropriate header<\/p>\n<p>    listWrapper.append(li);<br \/>\n} );<br \/>\n[\/code]\n<p>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 <code>&lt;h1&gt;<\/code> to <code>&lt;h6&gt;<\/code>. If you&#8217;re not concerned with hierarchical order, this will work beautifully.<\/p>\n<p>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 <code>h2<\/code> can appear at any level in the DOM in relation to an <code>h6<\/code> tag. With no sense of inherent hierarchical order in the heading tags, how do we make a hierarchical structure?<br \/>\n<!--more--><br \/>\nInstead of a straight up list, we&#8217;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:<\/p>\n<p><a href=\"http:\/\/g-liu.com\/blog\/wp-content\/uploads\/2014\/04\/wikipedia-toc.png\"><img loading=\"lazy\" decoding=\"async\" data-attachment-id=\"4662\" data-permalink=\"https:\/\/g-liu.com\/blog\/2014\/04\/walkthrough-making-a-self-updating-table-of-contents-with-jquery\/wikipedia-toc\/\" data-orig-file=\"https:\/\/g-liu.com\/blog\/wp-content\/uploads\/2014\/04\/wikipedia-toc.png\" data-orig-size=\"320,409\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;}\" data-image-title=\"wikipedia-toc\" data-image-description=\"&lt;p&gt;The Wikipedia Table of Contents for an article.&lt;\/p&gt;\n\" data-image-caption=\"\" data-large-file=\"https:\/\/g-liu.com\/blog\/wp-content\/uploads\/2014\/04\/wikipedia-toc.png\" src=\"http:\/\/g-liu.com\/blog\/wp-content\/uploads\/2014\/04\/wikipedia-toc.png\" alt=\"wikipedia-toc\" width=\"320\" height=\"409\" class=\"alignnone size-full wp-image-4662\" srcset=\"https:\/\/g-liu.com\/blog\/wp-content\/uploads\/2014\/04\/wikipedia-toc.png 320w, https:\/\/g-liu.com\/blog\/wp-content\/uploads\/2014\/04\/wikipedia-toc-234x300.png 234w\" sizes=\"auto, (max-width: 320px) 100vw, 320px\" \/><\/a><\/p>\n<p>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.<\/p>\n[code language=&#8221;javascript&#8221;]\nvar headings = $(&#8216;h1, &#8230; , h6&#8217;);<br \/>\nvar previousLevel = 0;<br \/>\nvar list = $(&quot;#myList&quot;);<br \/>\nvar result = list;<br \/>\nfor(all headings) {<br \/>\n\tvar level = $(this).level; \/\/ get the numeral part of the &lt;h_&gt; tag.<br \/>\n\tvar li = \/\/ make the list item<br \/>\n\tif(level &amp;gt; previousLevel) {<br \/>\n\t\t\/\/ start a new level, and add the list item<br \/>\n\t}<br \/>\n\telse if(level &amp;gt; previousLevel) {<br \/>\n\t\t\/\/ append a list-item to a parent list<br \/>\n\t}<br \/>\n\telse if(level == previousLevel) {<br \/>\n\t\t\/\/ append a list-item to the current list<br \/>\n\t}<br \/>\n\t\/\/ update previous level<br \/>\n\tpreviousLevel = level;<br \/>\n}<br \/>\n[\/code]\n<p>Now we can extrapolate this pseudocode to actual operations. Here&#8217;s what&#8217;s going to be tricky:<\/p>\n<ul>\n<li>keeping track of the depth that we&#8217;re at<\/li>\n<li>going back\u00a0<em>up<\/em> to a previous level<\/li>\n<li>making sure list-items are being inserted at the correct level<\/li>\n<\/ul>\n<p>To solve the first problem, we use a jQuery variable, <code>result<\/code>, to keep track of the list item that we&#8217;re currently using. For every traversal through the loop, we can append a new list to <code>result<\/code>, we can append a list item after <code>result<\/code> (to the current list), or we can append a list item to a higher-level list. After this, we update <code>result<\/code> to the list-item that we added.<\/p>\n<ul>\n<li><strong>Start a new list, append <code>li<\/code> to list:<\/strong> <code>result.append($('&lt;ul&gt;&lt;\/ul&gt;').append(li));<\/code><\/li>\n<li><strong>Append <code>li<\/code> to the current list:<\/strong> <code>result.parent().append(li);<\/code><\/li>\n<li><strong>Append <code>li<\/code> to a higher-level list:<\/strong> <code>result.parent('ul:eq(' + level + ')').append(li);<\/code><\/li>\n<\/ul>\n<p>The last item has an <code>eq:(...)<\/code> expression, which basically calculates how far up the list that we should append our list-item. To read more about <code>eq<\/code>, see the <a href=\"http:\/\/api.jquery.com\/eq\/\" target=\"_blank\">jQuery documentation<\/a>.<\/p>\n<h1>Making the page navigable<\/h1>\n<p>Now that we have a rough idea of how to list out the headings on a page in a <code>ul<\/code>, 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.<\/p>\n<p>The solution is to introduce <code>id<\/code>s for every heading on the page, and make corresponding <code>a href<\/code> 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:<\/p>\n[code language=&#8221;javascript&#8221;]\nvar headings = $(&#8216;h1, h2, h3, h4, h5, h6&#8217;);<br \/>\nvar counter = 0;<\/p>\n<p>headings.each(function() {<br \/>\n    var htmlId = &quot;h&quot; + (++counter);<br \/>\n    $(this).attr( { &#8216;id&#8217;: htmlId; } ); \/\/ &quot;h&quot; in front, since IDs must start with a letter<\/p>\n<p>    \/\/ create the list item and make it clickable<br \/>\n    var $li = $(&#8216;&lt;li&gt;&lt;\/li&gt;&#8217;)<br \/>\n        .append($(&#8216;&lt;a&gt;&lt;\/a&gt;&#8217;)<br \/>\n            .html( $(this).html() )<br \/>\n            .attr( { &#8216;href&#8217;: &quot;#&quot; + htmlId } ) \/\/ makes it clickable and goes to correct location<br \/>\n        );<br \/>\n} );<br \/>\n[\/code]\n<h1>Putting it all together<\/h1>\n<p>Now that we&#8217;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&#8217;ll roll it into a function. We&#8217;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.<\/p>\n[code language=&#8221;javascript&#8221;]\n\/**<br \/>\n * Generates a table of contents for the document<br \/>\n * @param where the selector to which to append the T.O.C<br \/>\n * @param isOrdered ordered or unordered list?<br \/>\n * @param depth of the TOC<br \/>\n *\/<br \/>\nfunction generateTOC(where, isOrdered, tocDepth) {<br \/>\n\tvar $result = where;<br \/>\n\tvar list = isOrdered ? &quot;ol&quot; : &quot;ul&quot;;<br \/>\n\tvar depth = tocDepth || 6;<br \/>\n\tvar curDepth = 0;<\/p>\n<p>\tvar counter = 0;<\/p>\n<p>\tvar select = &#8216;h1&#8217;;<\/p>\n<p>\tfor(var i = 2; i &lt;= Math.min(depth, 6); i++) {<br \/>\n\t\tselect += &quot;, h&quot; + i;<br \/>\n\t}<\/p>\n<p>\t$(select).each(function() {<br \/>\n\t\tvar depth = parseInt($(this).prop(&#8216;tagName&#8217;).charAt(1));<br \/>\n\t\tvar htmlId = &quot;h&quot; + (++counter);<\/p>\n<p>\t\t$(this).attr( { &#8216;id&#8217;: htmlId } );<br \/>\n\t\tvar $li = $(&#8216;&lt;li&gt;&lt;\/li&gt;&#8217;)<br \/>\n\t\t\t.append($(&#8216;&lt;a&gt;&lt;\/a&gt;&#8217;)<br \/>\n\t\t\t\t.html($(this).html())<br \/>\n\t\t\t\t.attr( { &#8216;href&#8217;: &#8216;#&#8217; + htmlId } )<br \/>\n\t\t\t);<\/p>\n<p>\t\tif(depth &gt; curDepth) {<br \/>\n\t\t\t\/\/ going deeper<br \/>\n\t\t\t$result.append($(&#8216;&lt;&#8216; + list + &#8216;&gt;&lt;\/&#8217; + list + &#8216;&gt;&#8217;).append($li));<br \/>\n\t\t} else if (depth &lt; curDepth) {<br \/>\n\t\t\t\/\/ going shallower<br \/>\n\t\t\t$result.parents(list + &#8216;:eq(&#8216; + (curDepth &#8211; depth) + &#8216;)&#8217;).append($li);<br \/>\n\t\t} else {<br \/>\n\t\t\t\/\/ same level<br \/>\n\t\t\t$result.parent().append($li);<br \/>\n\t\t}<\/p>\n<p>\t\t$result = $li;<br \/>\n\t\tcurDepth = depth;<br \/>\n\t});<br \/>\n}<br \/>\n[\/code]\n<h2>Usage and examples<\/h2>\n<p>To use this function, call in <code>$(document).ready()<\/code>, or where appropriate:<\/p>\n[code language=&#8221;javascript&#8221;]\ngenerateTOC($(&#8216;#my-table-of-contents&#8217;));<br \/>\n[\/code]\n<p>You can have an ordered list by calling <code>generateTOC($(...), true);<\/code>, or unordered list by calling <code>generateTOC($(...), false);<\/code>.<\/p>\n<p><em>Credits to <a href=\"http:\/\/stackoverflow.com\/questions\/497418\/produce-heading-hierarchy-as-ordered-list\" target=\"_blank\">http:\/\/stackoverflow.com\/questions\/497418\/produce-heading-hierarchy-as-ordered-list<\/a> for the inspiration.<\/em><\/p>\n<!-- AddThis Advanced Settings generic via filter on the_content --><!-- AddThis Share Buttons generic via filter on the_content --><!-- AddThis Related Posts generic via filter on the_content -->","protected":false},"excerpt":{"rendered":"<p>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: [code language=&#8221;javascript&#8221;] var headings = $(&#8216;h1, h2, h3, h4, h5, h6&#8217;); var listWrapper = $(&#8216;#toc&#8217;); \/\/ the container where our &#8230;<!-- AddThis Advanced Settings generic via filter on wp_trim_excerpt --><!-- AddThis Share Buttons generic via filter on wp_trim_excerpt --><!-- AddThis Related Posts generic via filter on wp_trim_excerpt --><\/p>\n","protected":false},"author":2,"featured_media":4662,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"#Walkthrough: Making a self-updating table of contents with #jQuery http:\/\/wp.me\/p2Zt3y-1db #javascript #coding","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"jetpack_post_was_ever_published":false},"categories":[830],"tags":[],"class_list":["post-4661","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-code-web-design"],"jetpack_publicize_connections":[],"jetpack_shortlink":"https:\/\/wp.me\/p2Zt3y-1db","jetpack_sharing_enabled":true,"jetpack_featured_media_url":"https:\/\/g-liu.com\/blog\/wp-content\/uploads\/2014\/04\/wikipedia-toc.png","_links":{"self":[{"href":"https:\/\/g-liu.com\/blog\/wp-json\/wp\/v2\/posts\/4661","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/g-liu.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/g-liu.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/g-liu.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/g-liu.com\/blog\/wp-json\/wp\/v2\/comments?post=4661"}],"version-history":[{"count":24,"href":"https:\/\/g-liu.com\/blog\/wp-json\/wp\/v2\/posts\/4661\/revisions"}],"predecessor-version":[{"id":4688,"href":"https:\/\/g-liu.com\/blog\/wp-json\/wp\/v2\/posts\/4661\/revisions\/4688"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/g-liu.com\/blog\/wp-json\/wp\/v2\/media\/4662"}],"wp:attachment":[{"href":"https:\/\/g-liu.com\/blog\/wp-json\/wp\/v2\/media?parent=4661"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/g-liu.com\/blog\/wp-json\/wp\/v2\/categories?post=4661"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/g-liu.com\/blog\/wp-json\/wp\/v2\/tags?post=4661"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}