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();
});
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を含んだ要素を空で用意します
