{"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<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\r\nvar headings = $('h1, h2, h3, h4, h5, h6');\r\nvar listWrapper = $('#toc'); \/\/ the container where our table-of-contents goes\r\nvar counter = 0;\r\n\r\nheadings.each(function() {\r\n    $(this).attr(&quot;id&quot;, &quot;h&quot; + (++counter));\r\n    var li = $('&lt;li&gt;&lt;\/li&gt;')\r\n        .append($('&lt;a&gt;&lt;\/a&gt;')\r\n            .attr( { &quot;href&quot; : &quot;#&quot; + $(this).attr(&quot;id&quot;) } )\r\n            .html($(this).html());\r\n         ); \/\/ this creates the list item, and links it to the appropriate header\r\n    \r\n    listWrapper.append(li);\r\n} );\r\n<\/pre>\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-medium-file=\"https:\/\/g-liu.com\/blog\/wp-content\/uploads\/2014\/04\/wikipedia-toc-234x300.png\" 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=\"(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<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\r\nvar headings = $('h1, ... , h6');\r\nvar previousLevel = 0;\r\nvar list = $(&quot;#myList&quot;);\r\nvar result = list;\r\nfor(all headings) {\r\n\tvar level = $(this).level; \/\/ get the numeral part of the &lt;h_&gt; tag.\r\n\tvar li = \/\/ make the list item\r\n\tif(level &amp;gt; previousLevel) {\r\n\t\t\/\/ start a new level, and add the list item\r\n\t}\r\n\telse if(level &amp;gt; previousLevel) {\r\n\t\t\/\/ append a list-item to a parent list\r\n\t}\r\n\telse if(level == previousLevel) {\r\n\t\t\/\/ append a list-item to the current list\r\n\t}\r\n\t\/\/ update previous level\r\n\tpreviousLevel = level;\r\n}\r\n<\/pre>\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<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\r\nvar headings = $('h1, h2, h3, h4, h5, h6');\r\nvar counter = 0;\r\n\r\nheadings.each(function() {\r\n    var htmlId = &quot;h&quot; + (++counter);\r\n    $(this).attr( { 'id': htmlId; } ); \/\/ &quot;h&quot; in front, since IDs must start with a letter\r\n    \r\n    \/\/ create the list item and make it clickable\r\n    var $li = $('&lt;li&gt;&lt;\/li&gt;')\r\n        .append($('&lt;a&gt;&lt;\/a&gt;')\r\n            .html( $(this).html() )\r\n            .attr( { 'href': &quot;#&quot; + htmlId } ) \/\/ makes it clickable and goes to correct location\r\n        );\r\n} );\r\n<\/pre>\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<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\r\n\/** \r\n * Generates a table of contents for the document\r\n * @param where the selector to which to append the T.O.C\r\n * @param isOrdered ordered or unordered list?\r\n * @param depth of the TOC\r\n *\/\r\nfunction generateTOC(where, isOrdered, tocDepth) {\r\n\tvar $result = where;\r\n\tvar list = isOrdered ? &quot;ol&quot; : &quot;ul&quot;;\r\n\tvar depth = tocDepth || 6;\r\n\tvar curDepth = 0;\r\n\t\r\n\tvar counter = 0;\r\n\t\r\n\tvar select = 'h1';\r\n\t\r\n\tfor(var i = 2; i &lt;= Math.min(depth, 6); i++) {\r\n\t\tselect += &quot;, h&quot; + i;\r\n\t}\r\n\r\n\t$(select).each(function() {\r\n\t\tvar depth = parseInt($(this).prop('tagName').charAt(1));\r\n\t\tvar htmlId = &quot;h&quot; + (++counter);\r\n\t\t\r\n\t\t$(this).attr( { 'id': htmlId } );\r\n\t\tvar $li = $('&lt;li&gt;&lt;\/li&gt;')\r\n\t\t\t.append($('&lt;a&gt;&lt;\/a&gt;')\r\n\t\t\t\t.html($(this).html())\r\n\t\t\t\t.attr( { 'href': '#' + htmlId } )\r\n\t\t\t);\r\n\r\n\t\tif(depth &gt; curDepth) {\r\n\t\t\t\/\/ going deeper\r\n\t\t\t$result.append($('&lt;' + list + '&gt;&lt;\/' + list + '&gt;').append($li));\r\n\t\t} else if (depth &lt; curDepth) {\r\n\t\t\t\/\/ going shallower\r\n\t\t\t$result.parents(list + ':eq(' + (curDepth - depth) + ')').append($li);\r\n\t\t} else {\r\n\t\t\t\/\/ same level\r\n\t\t\t$result.parent().append($li);\r\n\t\t}\r\n\t\t\r\n\t\t$result = $li;\r\n\t\tcurDepth = depth;\r\n\t});\r\n}\r\n<\/pre>\n<h2>Usage and examples<\/h2>\n<p>To use this function, call in <code>$(document).ready()<\/code>, or where appropriate:<\/p>\n<pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\r\ngenerateTOC($('#my-table-of-contents'));\r\n<\/pre>\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\" class=\"broken_link\" rel=\"nofollow\">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: var headings = $(&#8216;h1, h2, h3, h4, h5, h6&#8217;); var listWrapper = $(&#8216;#toc&#8217;); \/\/ the container where our table-of-contents goes &#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_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"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","enabled":false}}},"categories":[830],"tags":[],"jetpack_publicize_connections":[],"aioseo_notices":[],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"https:\/\/g-liu.com\/blog\/wp-content\/uploads\/2014\/04\/wikipedia-toc.png","jetpack_shortlink":"https:\/\/wp.me\/p2Zt3y-1db","_links":{"self":[{"href":"https:\/\/g-liu.com\/blog\/wp-json\/wp\/v2\/posts\/4661"}],"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}]}}