Share what you are working on and using!

Ahh! SO cool that you saw that and thought to share it here! Thanks so much @cjeller1592!

My code’s a little bit spaghetti-ish and might be a little buggy! I’m 100% sure someone here could find a much cleaner solution! I wrote it so people using my tutorials/class could jump to the parts they need or go back to where they left off.

One more heads up before I put the code and explain it: you need to add <div id="toc"></div> to posts where you want the table of contents to appear. I also recommend putting this element after the fold so that the script doesn’t generate a TOC for your home page. Again, I’m sure there’s a cleaner solution!

The code is:

CSS

#post-body {
    max-width: 40rem;
}

body#post article ul, body#post article ul ul, body#post article li {
    margin: 0;
}

#post nav a:not(.home), header nav a {
    margin: 0;
}


#post nav a:not(.home):not(.sub-nav), header nav a {
    margin: 0 0 0 1em;
}

@media only screen and (min-width: 60rem) {
    #post .tutorial {
        max-width: 60rem;
    }
    
    #article-container {
        display: flex;
        justify-content: space-between;
    }
    
    #table-of-contents, #spacer {
        min-width: 18rem;
        max-width: 18rem;
    }
    
    .sticky {
      position: fixed;
      top: 10px;
      width: 100%;
    }
}

JavaScript

var toc = document.createElement("nav");

if (document.getElementById("toc")) {
    window.onscroll = function() {onScroll()};
    document.getElementsByTagName("article")[0].classList.add("tutorial");
    
    var body = document.getElementsByClassName("e-content")[0];
    body.id = "article-container";
    
    var article = document.createElement("div");
    article.innerHTML = body.innerHTML;
    document.getElementById("post-body").id = "";
    article.id = "post-body";
    article.classList = body.classList;
    
    var contents = document.getElementById("toc");
    toc.id = "table-of-contents";
    toc.innerHTML = "<h3>Contents</h3>";
    
    body.innerHTML = "";
    body.appendChild(toc);
    body.appendChild(article);
    
    document.getElementById("toc").remove();
    makeTableOfContents();
}

function makeTableOfContents() {
    var headers = Array.from(document.querySelectorAll("h1,h2,h3,h4,h5,h6"));    
    var currentLevel = 3;
    
    var lists = [];
    var rootList = document.createElement("ul");
    lists.push(rootList);
    var currentList = 0;
    toc.appendChild(rootList);
    
    for (var i = 3; i < headers.length; i++) {
        var level = headers[i].tagName.charAt(1);
        if (level > currentLevel) {
            var list = document.createElement("ul");
            lists.push(list);
            lists[currentList].appendChild(list);
            currentList = lists.length - 1;
            currentLevel = level;
        }
        else if (level < currentLevel) {
            currentList -= 1;
            currentLevel = level;
        }
        var listItem = document.createElement("li");
        listItem.innerHTML = "<a class=\"sub-nav\" href=\"#" + headers[i].id + "\">"  + headers[i].innerHTML + "</a>";
        lists[currentList].appendChild(listItem);
    }
}

var sticky = 150;

function onScroll() {
  if (document.documentElement.scrollTop >= sticky) {
    if (document.getElementById("spacer") === null) {
        toc.classList.add("sticky");
        var body = document.getElementsByClassName("e-content")[0];
        var spacer = document.createElement("div");
        spacer.id = "spacer";
        body.insertBefore(spacer, document.getElementById("post-body"));
    }
  } else if (document.getElementById("spacer") !== null) {
    toc.classList.remove("sticky");
    document.getElementById("spacer").remove();
  }
}

The general approach is:

Making the table of contents

  1. Make the original article element essentially a container for both the table of contents and a new div, which I fill with the article element’s contents (the post itself). I then empty the original article element and fill it with both the table of contents element and the new div.
  2. Fill the table of contents element. Start by creating a root list ul element and add it to the list of ul elements. Then, iterate through every header element. (I think there could be a recursive solution here instead?) (I start at h3 elements and only care about the headers starting at the third one to avoid getting the blog name, the post title, and the “Contents” header.) For each header, if the header level (like h3, h4, etc.) is greater than the current level, so if this is a subheader of the previous header, create a new ul and add it to the list. Then add the new header link li that links to the associated header id (only possible because Write.as headers come with their own ids! Perfect!) to the current ul. If the new header level is less, so this is a larger header than the last, just go to the previous ul in the list and add this new header link to that one. If it’s the same, just add it to the current ul.

Making the table of contents sticky
edit: whoops, I didn’t know about this – https://davidwalsh.name/detect-sticky

  1. Bind the scroll event to a function called onScroll, so the onScroll method will be called whenever the user scrolls.
  2. In your onScroll function, if the current scroll position is lower on the page than your table of contents, add the sticky CSS class to anchor it to the top of the page. But because the sticky class anchors the TOC to the top of the page, you need a spacer element to fill its position in the article; otherwise the post contents would shift to center in the whole 60rem (when it’s supposed to stay shifted to the right). So create a new spacer element with the CSS id spacer (associated with a width of 18rem, allowing up to 2rem of space between.) and put it before the new post div. If the user scrolls back up, just undo that.

Design

  1. Make CSS that adjusts the page so that the TOC + content takes 60rem instead of the usual 40 to accommodate a TOC. Make sure the post header covers the whole 60rem. Ensure that the sticky element keeps the same width as the TOC so that the transition to the TOC being positioned fixed is relatively seamless. On mobile, keep the TOC in place for now. Style the lists and list elements to look more natural as a table of contents (remove/adjust margins).

Really hope this is helpful for someone! Please let me know if you have a nicer solution! I’m happy to answer any questions y’all might have about the code.

Thanks again @cjeller1592! Was really cool to see that you liked it :smile:

2 Likes