page-toc.js

  1let scrollTimeout;
  2
  3const listenActive = () => {
  4  const elems = document.querySelector(".pagetoc").children;
  5  [...elems].forEach((el) => {
  6    el.addEventListener("click", (event) => {
  7      clearTimeout(scrollTimeout);
  8      [...elems].forEach((el) => el.classList.remove("active"));
  9      el.classList.add("active");
 10      // Prevent scroll updates for a short period
 11      scrollTimeout = setTimeout(() => {
 12        scrollTimeout = null;
 13      }, 100); // Adjust timing as needed
 14    });
 15  });
 16};
 17
 18const getPagetoc = () =>
 19  document.querySelector(".pagetoc") || autoCreatePagetoc();
 20
 21const autoCreatePagetoc = () => {
 22  const main = document.querySelector("#content > main");
 23  const content = Object.assign(document.createElement("div"), {
 24    className: "content-wrap",
 25  });
 26  content.append(...main.childNodes);
 27  main.prepend(content);
 28  main.insertAdjacentHTML(
 29    "afterbegin",
 30    '<div class="sidetoc"><nav class="pagetoc"></nav></div>',
 31  );
 32  return document.querySelector(".pagetoc");
 33};
 34const updateFunction = () => {
 35  if (scrollTimeout) return; // Skip updates if within the cooldown period from a click
 36  const headers = [...document.getElementsByClassName("header")];
 37  const scrolledY = window.scrollY;
 38  let lastHeader = null;
 39
 40  // Find the last header that is above the current scroll position
 41  for (let i = headers.length - 1; i >= 0; i--) {
 42    if (scrolledY >= headers[i].offsetTop) {
 43      lastHeader = headers[i];
 44      break;
 45    }
 46  }
 47
 48  const pagetocLinks = [...document.querySelector(".pagetoc").children];
 49  pagetocLinks.forEach((link) => link.classList.remove("active"));
 50
 51  if (lastHeader) {
 52    const activeLink = pagetocLinks.find(
 53      (link) => lastHeader.href === link.href,
 54    );
 55    if (activeLink) activeLink.classList.add("active");
 56  }
 57};
 58
 59document.addEventListener("DOMContentLoaded", () => {
 60  const pagetoc = getPagetoc();
 61  const headers = [...document.getElementsByClassName("header")];
 62
 63  const nonH1Headers = headers.filter(
 64    (header) => !header.parentElement.tagName.toLowerCase().startsWith("h1"),
 65  );
 66  const sidetoc = document.querySelector(".sidetoc");
 67  const tocContainer = document.querySelector(".toc-container");
 68
 69  if (nonH1Headers.length === 0) {
 70    if (sidetoc) {
 71      sidetoc.style.display = "none";
 72    }
 73    if (tocContainer) {
 74      tocContainer.classList.add("no-toc");
 75    }
 76    return;
 77  }
 78
 79  if (tocContainer) {
 80    tocContainer.classList.add("has-toc");
 81  }
 82
 83  const tocTitle = Object.assign(document.createElement("p"), {
 84    className: "toc-title",
 85    textContent: "On This Page",
 86  });
 87  pagetoc.appendChild(tocTitle);
 88
 89  headers.forEach((header) => {
 90    const link = Object.assign(document.createElement("a"), {
 91      textContent: header.text,
 92      href: header.href,
 93      className: `pagetoc-${header.parentElement.tagName}`,
 94    });
 95    pagetoc.appendChild(link);
 96  });
 97  updateFunction();
 98  listenActive();
 99  window.addEventListener("scroll", updateFunction);
100});