javascripthtmlcssdomhtml-lists

Expand the Current List in Table of Contents


I have a basic table of contents on each webpage, and there is CSS which adds a border around the current section the user is on. This is just a snippet to show how it works:

<ol>
    ...
</ol>
<ol>
    <li><a href="#">Public Host Server</a>
        <ol class="toc-collapse-level">
            <li><a href="#">Host Server</a>
                <ol>
                    <li class="toc-current-section"><a href="#">Hyper-V Configuration</a>
                        <ol>
                            <li><a href="#s1">Host Network Settings and Status</a></li>
                            <li><a href="#s2">Disk and VM Configuration Locations</a></li>
                            <li><a href="#s3">Virtual Switch Configuration</a></li>
                            <li><a href="#s4">Installed Virtual Servers</a></li>
                            <li><a href="#s5">Web Servers and Reverse Proxy Server VM Network Adapter Settings</a></li>
                        </ol>
                    </li>
                </ol>
            </li>   
            <li>
                <ol>
                    <li><a>...</a>
                        <ol>
                            <li><a>...</a></li>
                            <li><a>...</a></li>
                            <li><a>...</a></li>
                        </ol>
                    </li>                 
                </ol>
            </li>
        </ol>
    </li>
</ol>
<ol>
    ...
</ol>

**The class toc-current-section signifies the current page that is open in the table of contents.
**Sometimes toc-current-section is on the preceding TOC level, which is why the script must check both levels for this property.

I am using the follow script to make the list collapsible.

(t => {
const e = "faq-list",
    n = "faq-clicked";

    function l(l, o) {
        const i = l.children;
        if (0 === i.length) return;
        for (let t of i)
            if (t.childElementCount < 2) return;
            let c = l.classList;
            if (c.length > 0 && !c.contains(e)) return;
            c.add(e);
            const s = t.createElement("span");
            let a = !1;
            s.className = "faq-button", s.innerText = "⊕", s.onclick = function() {
                a = !a, this.innerText = a ? "⊖" : "⊕";
                for (const t of l.children) t.classList.toggle(n, a)
            }, l.before(s);
            for (let t = 0; t < i.length; t++) {
                let e = i[t];
                e.id = "faq-" + (o ? o + "-" : "") + (t + 1);
                let l = e.firstElementChild;
                l.insertAdjacentHTML("beforeend", ` <span class="anchor"><a href="#${e.id}">#</a></    span>`), location.hash === "#" + e.id && (e.scrollIntoView(), e.classList.add(n)), l.onclick = function(t) {
                e.classList.toggle(n)
            }
        }
    }

    ///////////////////////////////////////////////////////////////////////////////
    /////////////////CODE THAT WAS ADDED///////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////////

    function isElementVisible(el) {
        var rect = el.getBoundingClientRect(),
            vWidth = window.innerWidth || document.documentElement.clientWidth,
            vHeight = window.innerHeight || document.documentElement.clientHeight,
            efp = function (x, y) {
                return document.elementFromPoint(x, y)
            };

        // Return false if not in the viewport
        if (rect.right < 0 || rect.bottom < 0
            || rect.left > vWidth || rect.top > vHeight)
            return false;

        // Return true if any of its four corners are visible
        return (
            el.contains(efp(rect.left, rect.top))
            || el.contains(efp(rect.right, rect.top))
            || el.contains(efp(rect.right, rect.bottom))
            || el.contains(efp(rect.left, rect.bottom))
        );
    }

    const o = t.querySelectorAll(["div", "main", "section", "article"].map((t => t + ":not(.footnotes) > ol")).join(","));

    for (let t = 0; t < o.length; t++) {
        l(o[t], o.length > 1 ? t + 1 : 0)
        if(isElementVisible(o[t])){

            //Get the first level ol element
            tocSectionL1_ol = o[t].firstElementChild.childNodes[2];  

            //Get the li element within the first level ol element
            tocSectionL1_li = tocSectionL1_ol.firstElementChild;

            //Get the second level ol element
            tocSectionL2_ol = tocSectionL1_li.childNodes[2];

            //Get the li element within the second level ol element
            tocSectionL2_li = tocSectionL2_ol.firstElementChild;

            //Get the class name of each li element
            tocSectionL1_li_class = tocSectionL1_li.className;
            tocSectionL2_li_class = tocSectionL2_li.className;

            //Check if either li element contains the class toc-current-section   
            if((tocSectionL1_li_class || tocSectionL2_li_class) == "toc-current-section") {

                //Toggle the *faq-clicked* class on the element
                o[t].classList.toggle("faq-clicked");
            }
        }
    }
})(document);

And the CSS for the collapsible list to work:

.faq-button {
    cursor: pointer
}

.faq-list>li>:first-child {
    display: block;
    cursor: pointer;
    background: #fafafa;
    margin: -.5em;
    padding: .5em
}

.faq-list>:not(.faq-clicked)>* {
    display: none
}

.faq-button {
    float: right;
    margin-left: 1em
}

.faq-list .anchor {
    display: none
}

.faq-list>li:hover .anchor {
    display: inline
}

.faq-list>li {
    border: 1px solid #eee;
    padding: .5em
}

The problem is I am unsure how to make the current section be expanded when the page loads.

EDIT 1: Updated the script to include functionality by myself and @learndev. So far it adds the class to the proper element that must be expanded, but is still remainined collapsed when the page loads.. Also modified the HTML snipped for better clarity.
This is a screenshot of the inspector for my webpage, showing that the element does have the class "faq-clicked" toggled, however the inner contents remain greyed out and invisible.

enter image description here



EDIT 2: SOLVED Upon close inspection, the original collapse script toggles the class faq-clicked on the first child li element, not the ol element. The recommended code toggled the class on the object o[t], which represented the ol element with the class name faq-list. By toggling the class on o[t].firstElementChild, the script then toggles the class *faq-cliked on the inner li element, thus providing the proper functionality.

enter image description here


Thanks to @learndev for getting me in the right direction!

Solution

  • to make the current section expand on page load, first you have to determine which is the current section.

    I think there can be multiple sections expanded at the same time (e.g when they are visible on screen).

    So to find all visible sections we can use the following script:

    function isElementVisible(el) {
        var rect = el.getBoundingClientRect(),
            vWidth = window.innerWidth || document.documentElement.clientWidth,
            vHeight = window.innerHeight || document.documentElement.clientHeight,
            efp = function (x, y) {
                return document.elementFromPoint(x, y)
            };
    
        // Return false if it's not in the viewport
        if (rect.right < 0 || rect.bottom < 0
            || rect.left > vWidth || rect.top > vHeight)
            return false;
    
        // Return true if any of its four corners are visible
        return (
            el.contains(efp(rect.left, rect.top))
            || el.contains(efp(rect.right, rect.top))
            || el.contains(efp(rect.right, rect.bottom))
            || el.contains(efp(rect.left, rect.bottom))
        );
    }
    

    source: How can I tell if a DOM element is visible in the current viewport?

    Now you can extend your script and check for each section if it is visible and toggle "faq-clicked" if it is.

    ...
    const o = t.querySelectorAll(["div", "main", "section", "article"].map((t => t + ":not(.footnotes) > ol")).join(","));
    for (let t = 0; t < o.length; t++) {
        l(o[t], o.length > 1 ? t + 1 : 0)
    
        // This is the new code
        if(isElementVisible(o[t])){
            o[t].classList.toggle("faq-clicked");
        }
    
    }