table of contents

// "DOMContentLoaded" event handler (on document)
// - automatically generates a table of contents for the document.
document.addEventListener("DOMContentLoaded", () => {

    // find the TOC container element.
    // if there isn't one, create one at the start of the document.
    let toc = document.querySelector("#toc") || (() => {
        let div = `<div id="toc"></div>`.htmlToElement();
        document.body.prepend(div);
        return div;
    })();

    // find all section headings, assuming: 
    // - title: <h1>
    // - sections: <h2> - <h6>
    let headings = document.querySelectorAll("h2,h3,h4,h5,h6");

    // keeps track of section numbers. 
    const sectionNumberManager = {
        
        _numbers: [0, 0, 0, 0, 0],    // h2, h3, h4, h5, h6
        
        // generate section number for current heading in for-of loop
        sectionNumberForHeading(heading){
            
            // h2 -> 0, h3 -> 1, ...
            let i = level(heading) - 2;
    
            // increment the section number for this heading level 
            this._numbers[i] += 1; 
            
            // reset all lower heading level numbers to zero. 
            for (let j = i + 1; j < this._numbers.length; j++) {
                this._numbers[j] = 0;
            }
    
            // combine section numbers for all heading levels 
            // to produce a section number like 2.3.1.
            return this._numbers.slice(0, i+1).join(".");
        },
    };

    // h2 -> 2, h3 -> 3, ...
    function level(heading){
        return parseInt(heading.tagName.charAt(1));
    }

    // loop through section headings. 
    for (let heading of headings) {

        // skip if it's inside the TOC container. 
        if (heading.parentNode === toc) continue;

        // generate section number for current `heading`
        let sectionNumber = sectionNumberManager.sectionNumberForHeading(heading);

        // add section number to `heading`. 
        heading.insertAdjacentHTML(
            'afterbegin', 
            `<span class="TOCSectNum">${sectionNumber}</span>`    // place in a <span> to make it styleable.
        );

        // wrap `heading` in "named anchor" <a> so we can link to it.
        let anchorID = `TOC${sectionNumber}`;
        heading.wrappedWithHTML(`<a id="${anchorID}"></a>`);
        
        // add link (to `heading`) to TOC container.
        toc.insertAdjacentHTML('beforeend', 
            `<div class="TOCEntry TOCLevel${level(heading)-1}">
               <a href="#${anchorID}">${heading.innerHTML}</a>
             </div>`
        );
    }

});

Last updated