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
- Make the original
article
element essentially a container for both the table of contents and a newdiv
, which I fill with thearticle
element’s contents (the post itself). I then empty the originalarticle
element and fill it with both the table of contents element and the new div. - Fill the table of contents element. Start by creating a root list
ul
element and add it to the list oful
elements. Then, iterate through every header element. (I think there could be a recursive solution here instead?) (I start ath3
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 newul
and add it to the list. Then add the new header linkli
that links to the associated header id (only possible because Write.as headers come with their own ids! Perfect!) to the currentul
. If the new header level is less, so this is a larger header than the last, just go to the previousul
in the list and add this new header link to that one. If it’s the same, just add it to the currentul
.
Making the table of contents sticky
edit: whoops, I didn’t know about this – https://davidwalsh.name/detect-sticky
- Bind the scroll event to a function called
onScroll
, so theonScroll
method will be called whenever the user scrolls. - In your
onScroll
function, if the current scroll position is lower on the page than your table of contents, add thesticky
CSS class to anchor it to the top of the page. But because thesticky
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 idspacer
(associated with a width of 18rem, allowing up to 2rem of space between.) and put it before the new postdiv
. If the user scrolls back up, just undo that.
Design
- 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 positionedfixed
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