page-toc.js

  1let scrollTimeout;
  2
  3const listenActive = () => {
  4  const elems = document.querySelector(".pagetoc").children;
  5  [...elems].forEach((el) => {
  6    el.addEventListener("click", (_) => {
  7      clearTimeout(scrollTimeout);
  8      [...elems].forEach((el) => el.classList.remove("active"));
  9      el.classList.add("active");
 10
 11      scrollTimeout = setTimeout(() => {
 12        scrollTimeout = null;
 13      }, 100);
 14    });
 15  });
 16};
 17
 18const autoCreatePagetoc = () => {
 19  const main = document.querySelector("#content > main");
 20  const content = Object.assign(document.createElement("div"), {
 21    className: "content-wrap",
 22  });
 23  content.append(...main.childNodes);
 24  main.prepend(content);
 25  main.insertAdjacentHTML(
 26    "afterbegin",
 27    '<div class="toc-container"><nav class="pagetoc"></nav></div>',
 28  );
 29  return document.querySelector(".pagetoc");
 30};
 31
 32const getPagetoc = () =>
 33  document.querySelector(".pagetoc") || autoCreatePagetoc();
 34
 35const updateFunction = () => {
 36  if (scrollTimeout) return;
 37
 38  const headers = [...document.getElementsByClassName("header")];
 39  if (headers.length === 0) return;
 40
 41  const threshold = 100;
 42  let activeHeader = null;
 43
 44  for (const header of headers) {
 45    const rect = header.getBoundingClientRect();
 46
 47    if (rect.top <= threshold) {
 48      activeHeader = header;
 49    }
 50  }
 51
 52  if (!activeHeader && headers.length > 0) {
 53    activeHeader = headers[0];
 54  }
 55
 56  const pagetocLinks = [...document.querySelector(".pagetoc").children];
 57  pagetocLinks.forEach((link) => link.classList.remove("active"));
 58
 59  if (activeHeader) {
 60    const activeLink = pagetocLinks.find(
 61      (link) => activeHeader.href === link.href,
 62    );
 63    if (activeLink) activeLink.classList.add("active");
 64  }
 65};
 66
 67document.addEventListener("DOMContentLoaded", () => {
 68  const pagetoc = getPagetoc();
 69  const headers = [...document.getElementsByClassName("header")];
 70
 71  const nonH1Headers = headers.filter(
 72    (header) => !header.parentElement.tagName.toLowerCase().startsWith("h1"),
 73  );
 74  const tocContainer = document.querySelector(".toc-container");
 75
 76  if (nonH1Headers.length === 0) {
 77    if (tocContainer) {
 78      tocContainer.classList.add("no-toc");
 79    }
 80    return;
 81  }
 82
 83  if (tocContainer) {
 84    tocContainer.classList.add("has-toc");
 85  }
 86
 87  const tocTitle = Object.assign(document.createElement("p"), {
 88    className: "toc-title",
 89    textContent: "On This Page",
 90  });
 91
 92  pagetoc.appendChild(tocTitle);
 93
 94  headers.forEach((header) => {
 95    const link = Object.assign(document.createElement("a"), {
 96      textContent: header.text,
 97      href: header.href,
 98      className: `pagetoc-${header.parentElement.tagName}`,
 99    });
100    pagetoc.appendChild(link);
101  });
102
103  updateFunction();
104  listenActive();
105
106  const pageElement = document.querySelector(".page");
107  if (pageElement) {
108    pageElement.addEventListener("scroll", updateFunction);
109  } else {
110    window.addEventListener("scroll", updateFunction);
111  }
112});