<!--
    Demo for this article:
    http://blustemy.io/creating-a-table-of-contents-in-javascript/
-->

<nav class="table-of-contents">
    <h2>Contents</h2>
</nav>

<h1>The Chicago Manual of Style</h1>

<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 in hardcover and online…</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>
    
    <h2>History</h2>
    <p>What now is known as The Chicago Manual of Style…</p>
    
    <h3>History of editions</h3>
    <ul>
        <li>First Edition, 1906</li>
        <li>…</li>
    </ul>
    
    <h2>Recent printed editions</h2>
    <ul>
        <li>University of Chicago (2003)</li>
        <li>…</li>
    </ul>
    
    <h2>See also</h2>
    <ul>
        <li>Linguistic prescription</li>
        <li>…</li>
    </ul>
</article>
/*
    Demo for this article:
    http://blustemy.io/creating-a-table-of-contents-in-javascript/
*/

body {
    font-family: "Arial", sans-serif;
}

.table-of-contents {
    display: inline-block;
    padding: 0 1em;
    background: #f2f2f2;
    border: 1px solid #ddd;
    font-size: .8em;
}

/* https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Lists_and_Counters/Using_CSS_counters#Nesting_counters */
.table-of-contents ol {
    list-style: none;
    padding: 0;
    counter-reset: counter-table-of-contents;
}

.table-of-contents ol ol {
    padding-left: 2em;
}

.table-of-contents ol li {
    margin: .5em 0;
}

.table-of-contents ol li::before {
    counter-increment: counter-table-of-contents;
    content: counters(counter-table-of-contents, ".") " ";
    opacity: .7;
}
/*
    Demo for this article:
    http://blustemy.io/creating-a-table-of-contents-in-javascript/
*/

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);
    }
}

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

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.