In HTML the heading elements represents 6 levels of titles from h1 for the most important to h6 for the least important. Some Content Management Systems offer the possibility to automatically generate a table of contents from an article on the server side. But here I’m implementing a reusable client side JavaScript class to generate a table of contents from the heading information of a document.

About tables of contents

For example every Wikipedia article has a table of contents automatically generated from the headings and subheadings of the content. On this post I’m going to use the page The Chicago Manual of Style as an example.

Let’s generate the structure of the table of contents on the front-end side using the brand new features of JavaScript and a few interesting CSS properties.

Heading information

This is a typical HTML structure of an article with headings, paragraphs, lists and so on.

<article class="article">  
    <p>The Chicago Manual of Style is a style guide…</p>

    <h2>Availability and usage</h2>
    <p>The Chicago Manual of Style is published…</p>

    <h2>Citation Styles</h2>
    <p>Two types of citation styles are provided…</p>

    <h3>Author-date style</h3>
    <p>Using author-date style…</p>

    <h3>Notes and bibliography style</h3>
    <p>Using notes and bibliography style…</p>
</article>  

First we’ll need to alter the displayed article by adding unique id identifiers on each heading so the heading can be the target of a link. A few CMS systems like Ghost generate them automatically.

<article class="article">  
    …
    <h2 id="Availability_and_usage">Availability and usage</h2>
    …
    <h2 id="Citation_Styles">Citation Styles</h2>
    …
    <h3 id="Author-date_style">Author-date style</h3>
    …
    <h3 id="Notes_and_bibliography_style">Notes and bibliography style</h3>
    …
</article>  

Then a hierarchy of <ol> ordered lists with fragment links will be created. The numbers of the table of contents will be displayed in CSS.

<ol>  
    <li>
        <a href="#Availability_and_usage">Availability and usage</a>
    </li>
    <li>
        <a href="#Citation_Styles">Citation Styles</a>
        <ol>
            <li>
                <a href="#Author-date_style">Author-date style</a>
            </li>
            <li>
                <a href="#Notes_and_bibliography_style">Notes and bibliography style</a>
            </li>
        </ol>
    </li>
</ol>  

The main JavaScript class

I’m using the new native class syntax feature of JavaScript from the ECMAScript 6 (ES6) standard. This is working out of the box in recent browsers only but it may be compiled to older version of JavaScript to support older browsers. To do so the Babel compiler is really useful and available as an online converter. But of course it can also be used on the command line or through a build system like Gulp.

About the syntax and features of classes in JavaScript this is a detailed article.

class TableOfContents {  
    /*
        The parameters from and to must be Element objects in the DOM.
    */
    constructor({ from, to }) {
        this.fromElement = from;
        this.toElement = to;
        // Get all the ordered headings.
        this.headingElements = this.fromElement.querySelectorAll("h1, h2, h3, h4, h5, h6");
        this.tocElement = document.createElement("div");
    }

    /*
        Get the most important heading level.
        For example if the article has only <h2>, <h3> and <h4> tags
        this method will return 2.
    */
    getMostImportantHeadingLevel() {
        let mostImportantHeadingLevel = 6; // <h6> heading level
        for (let i = 0; i < this.headingElements.length; i++) {
            let headingLevel = TableOfContents.getHeadingLevel(this.headingElements[i]);
            mostImportantHeadingLevel = (headingLevel < mostImportantHeadingLevel) ?
                headingLevel : mostImportantHeadingLevel;
        }
        return mostImportantHeadingLevel;
    }

    /*
        Generate a unique id string for the heading from its text content.
    */
    static generateId(headingElement) {
        return headingElement.textContent.replace(/\s+/g, "_");
    }

    /*
        Convert <h1> to 1 … <h6> to 6.
    */
    static getHeadingLevel(headingElement) {
        switch (headingElement.tagName.toLowerCase()) {
            case "h1": return 1;
            case "h2": return 2;
            case "h3": return 3;
            case "h4": return 4;
            case "h5": return 5;
            case "h6": return 6;
            default: return 1;
        }
    }

    generateToc() {
        let currentLevel = this.getMostImportantHeadingLevel() - 1,
            currentElement = this.tocElement;

        for (let i = 0; i < this.headingElements.length; i++) {
            let headingElement = this.headingElements[i],
                headingLevel = TableOfContents.getHeadingLevel(headingElement),
                headingLevelDifference = headingLevel - currentLevel,
                linkElement = document.createElement("a");

            if (!headingElement.id) {
                headingElement.id = TableOfContents.generateId(headingElement);
            }
            linkElement.href = `#${headingElement.id}`;
            linkElement.textContent = headingElement.textContent;

            if (headingLevelDifference > 0) {
                // Go down the DOM by adding list elements.
                for (let j = 0; j < headingLevelDifference; j++) {
                    let listElement = document.createElement("ol"),
                        listItemElement = document.createElement("li");
                    listElement.appendChild(listItemElement);
                    currentElement.appendChild(listElement);
                    currentElement = listItemElement;
                }
                currentElement.appendChild(linkElement);
            } else {
                // Go up the DOM.
                for (let j = 0; j < -headingLevelDifference; j++) {
                    currentElement = currentElement.parentNode.parentNode;
                }
                let listItemElement = document.createElement("li");
                listItemElement.appendChild(linkElement);
                currentElement.parentNode.appendChild(listItemElement);
                currentElement = listItemElement;
            }

            currentLevel = headingLevel;
        }

        this.toElement.appendChild(this.tocElement.firstChild);
    }
}

To pass the parameters to the constructor of the class I’ve used a specific ES6 feature called object destructuring and assignment. I think this is a better way to instantiate a class without having to remember the order of the parameters of the constructor. And particularly when we could have a lot of parameters. Here’s a simplified version:

class MyClass {  
    constructor({ thing, another, again }) {
        this.thing = thing;
        this.another = another;
        this.again = again;
    }
}

Then to instantiate this class:

new MyClass({  
    thing: "Hello!",
    another: "Hi!",
    again: "Bye!"
});

A nice read about the destructuring assignment syntax.

Generate the table of contents

The class is instantiated when the page is loaded as soon as the browser triggers the DOMContentLoaded event.

document.addEventListener("DOMContentLoaded", () =>  
    new TableOfContents({
        from: document.querySelector(".article"),
        to: document.querySelector(".table-of-contents")
    }).generateToc()
);

Here again I’ve used a nice feature of the ES6 version of JavaScript, the arrow functions.

Adding the list numbers

I could have added the list numbers with JavaScript but why not doing it with pure CSS styles. To do so we must use the counter properties. Even if this is a rarely known and used feature of CSS browser support is excellent according to Can I use. I got the idea from this documentation page on using CSS counters.

.table-of-contents ol {
    list-style: none;
    padding: 0;
    /* This is an arbitrary name as the value. */
    counter-reset: counter-table-of-contents;
}

.table-of-contents ol ol {
    /* Indent the nested lists. */
    padding-left: 2em;
}

.table-of-contents ol li::before {
    counter-increment: counter-table-of-contents;
    /* Concatenate the nested counters. */
    content: counters(counter-table-of-contents, ".") " ";
}

Thanks to the cascading feature of CSS these properties behave like recursive counters. They are recursively incremented and concatenated. In the following code sample I’ve indicated the numbers that will be generated by the counter properties and added by the ::before selector:

<ol>  
    <li>
        <!-- 1 -->
        <a href="#Availability_and_usage">Availability and usage</a>
    </li>
    <li>
        <!-- 2 -->
        <a href="#Citation_Styles">Citation Styles</a>
        <ol>
            <li>
                <!-- 2.1 -->
                <a href="#Author-date_style">Author-date style</a>
            </li>
            <li>
                <!-- 2.2 -->
                <a href="#Notes_and_bibliography_style">Notes and bibliography style</a>
            </li>
        </ol>
    </li>
</ol>  

Demo

Here’s the preview, if you want to dig into with the code the demo is here on CodePen.

See the Pen Creating a Table of Contents in JavaScript by Frederic Perrin (@blustemy) on CodePen.