Webページのサイドに自動で目次を作る

Ads

Result

左ペインの目次を自動で生成する、というスクリプトです

vanillaで書かれてますので他のライブラリは不要です

javascript

let tocId = "toc";

let headings;
let headingIds = [];
let headingIntersectionData = {};
let headerObserver;

function setLinkActive(link) {
  const links = document.querySelectorAll(`#${tocId} a`);
  links.forEach((link) => link.classList.remove("active"));
  if (link) {
    link.classList.add("active");
  }
}

function getProperListSection(heading, previousHeading, currentListElement) {
  let listSection = currentListElement;
  if (previousHeading) {
    if (heading.tagName.slice(-1) > previousHeading.tagName.slice(-1)) {
      let nextSection = document.createElement("ul");
      listSection.appendChild(nextSection);
      return nextSection;
    } else if (heading.tagName.slice(-1) < previousHeading.tagName.slice(-1)) {
      let indentationDiff =
        parseInt(previousHeading.tagName.slice(-1)) -
        parseInt(heading.tagName.slice(-1));
      while (indentationDiff > 0) {
        listSection = listSection.parentElement;
        indentationDiff--;
      }
    }
  }
  return listSection;
}

function setIdFromContent(element, appendedId) {
  if (!element.id) {
    element.id = `${element.innerHTML
      .replace(/:/g, "")
      .trim()
      .toLowerCase()
      .split(" ")
      .join("-")}-${appendedId}`;
  }
}

function addNavigationLinkForHeading(heading, currentSectionList) {
  let listItem = document.createElement("li");
  let anchor = document.createElement("a");
  anchor.innerHTML = heading.innerHTML;
  anchor.id = `${heading.id}-link`;
  anchor.href = `#${heading.id}`;
  anchor.onclick = (e) => {
    setTimeout(() => {
      setLinkActive(anchor);
    });
  };
  listItem.appendChild(anchor);
  currentSectionList.appendChild(listItem);
}

function buildTableOfContentsFromHeadings() {
  const tocElement = document.querySelector(`#${tocId}`);
  const main = document.querySelector("main");
  if (!main) {
    throw Error("A `main` tag section is required to query headings from.");
  }
  headings = main.querySelectorAll("h1, h2, h3, h4, h5, h6");
  let previousHeading;
  let currentSectionList = document.createElement("ul");
  tocElement.appendChild(currentSectionList);

  headings.forEach((heading, index) => {
    currentSectionList = getProperListSection(
      heading,
      previousHeading,
      currentSectionList
    );
    setIdFromContent(heading, index);
    addNavigationLinkForHeading(heading, currentSectionList);

    headingIds.push(heading.id);
    headingIntersectionData[heading.id] = {
      y: 0
    };
    previousHeading = heading;
  });
}

function updateActiveHeadingOnIntersection(entry) {
  const previousY = headingIntersectionData[entry.target.id].y;
  const currentY = entry.boundingClientRect.y;
  const id = `#${entry.target.id}`;
  const link = document.querySelector(id + "-link");
  const index = headingIds.indexOf(entry.target.id);

  if (entry.isIntersecting) {
    if (currentY > previousY && index !== 0) {
      console.log(id + ":1 enter top");
    } else {
      console.log(id + ":2 enter bottom");
      setLinkActive(link);
    }
  } else {
    if (currentY > previousY) {
      console.log(id + ":3 leave bottom");
      const lastLink = document.querySelector(`#${headingIds[index - 1]}-link`);
      setLinkActive(lastLink);
    } else {
      console.log(id + ":4 leave top");
    }
  }

  headingIntersectionData[entry.target.id].y = currentY;
}

function observeHeadings() {
  let options = {
    root: document.querySelector("main"),
    threshold: 0.1
  };
  headerObserver = new IntersectionObserver(
    (entries) => entries.forEach(updateActiveHeadingOnIntersection),
    options
  );
  Array.from(headings)
    .reverse()
    .forEach((heading) => headerObserver.observe(heading));
}

window.addEventListener("load", (event) => {
  buildTableOfContentsFromHeadings();
  if ("IntersectionObserver" in window) {
    observeHeadings();
  }
});

window.addEventListener("unload", (event) => {
  headerObserver.disconnect();
});
Ads

css

nav {
  flex: 1 1 300px;
  background-color: #111;
  min-width: 240px;
  paddings: 1rem;
  overflow-y: auto;
}


.active {
  color: yellow;
  font-weight: bold;
}

ul {
  list-style: none;
  padding-left: 1.25rem;
}

ul li::before {
  content: "\2022";
  color: #fff;
  font-weight: bold;
  display: inline-block;
  width: 1em;
  margin-left: -1em;
}

スタイルはCSSで設定します

html

 <nav id="toc">
  </nav>

tocIdに設定したidを含んだ要素を空で用意します

via

Dynamic Active Table of Contents