138 lines
4.8 KiB
JavaScript
138 lines
4.8 KiB
JavaScript
|
"use strict";
|
||
|
|
||
|
|
||
|
if (document.getElementsByTagName("html")[0].classList.contains("single-page")) {
|
||
|
const title = document.body.querySelector(".sidebar .link.title");
|
||
|
const content = document.getElementsByClassName("content")[0];
|
||
|
const toc = document.getElementsByClassName("toc")[0];
|
||
|
const sidebar = document.getElementsByClassName("sidebar")[0];
|
||
|
|
||
|
let height = 0;
|
||
|
|
||
|
const anchors = Array.prototype.reduce.call(document.body.querySelectorAll(".content a"), function(accum, anchor) {
|
||
|
const name = anchor.getAttribute("name");
|
||
|
if (name && !accum[name]) {
|
||
|
accum[name] = anchor;
|
||
|
}
|
||
|
return accum;
|
||
|
}, { });
|
||
|
|
||
|
const sidebarLinks = Array.prototype.map.call(document.body.querySelectorAll(".sidebar .toc .link a"), function(anchor, index) {
|
||
|
const name = anchor.getAttribute("href").substring(1);
|
||
|
const link = anchor.parentNode;
|
||
|
|
||
|
// If the click, set the class directly, so it doesn't "jump"
|
||
|
anchor.onclick = function() {
|
||
|
const current = document.body.querySelector(".link.selected");
|
||
|
if (current) { current.classList.remove("selected"); }
|
||
|
|
||
|
link.classList.add("selected");
|
||
|
};
|
||
|
|
||
|
height += link.offsetHeight;
|
||
|
|
||
|
// Get the offset in the content of the target
|
||
|
let offset = 0;
|
||
|
anchor = anchors[name];
|
||
|
if (anchor) { offset = anchor.offsetTop; }
|
||
|
|
||
|
return { index, link, name, offset };
|
||
|
});
|
||
|
sidebarLinks.forEach(function(link, index) {
|
||
|
const next = sidebarLinks[index + 1];
|
||
|
link.height = (next ? next.offset: content.offsetHeight) - link.offset;
|
||
|
});
|
||
|
|
||
|
function getScrollTop(link) {
|
||
|
const maxScrollY = (toc.offsetHeight + 160 - window.innerHeight);
|
||
|
if (link == null) { return maxScrollY; }
|
||
|
|
||
|
const percentLinks = link.index / sidebarLinks.length;
|
||
|
return percentLinks * maxScrollY;
|
||
|
}
|
||
|
|
||
|
function getTime() { return (new Date()).getTime(); }
|
||
|
|
||
|
let scrollPaused = false;
|
||
|
|
||
|
function highlightToc(scrollVisible, animate) {
|
||
|
// What percent through the content are we?
|
||
|
let percentContent = window.scrollY / (content.offsetHeight - window.innerHeight);
|
||
|
if (percentContent < 0) {
|
||
|
percentContent = 0;
|
||
|
} else if (percentContent > 1) {
|
||
|
percentContent = 1;
|
||
|
}
|
||
|
|
||
|
// Map to on-screen w/ a smooth gradient from [ 0, content.offsetHeight ]
|
||
|
let y = window.scrollY + window.innerHeight * percentContent;
|
||
|
if (y < 0) { y = 0; }
|
||
|
|
||
|
// Find the link that is at before location y
|
||
|
let last = null;
|
||
|
for (let i = 0; i < sidebarLinks.length; i++) {
|
||
|
const link = sidebarLinks[i];
|
||
|
if (link.offset > y) { break; }
|
||
|
last = link;
|
||
|
}
|
||
|
|
||
|
// If the link is not already selected...
|
||
|
if (!last.link.classList.contains("selected")) {
|
||
|
// ...unselcted the currently seclected link
|
||
|
const current = document.body.querySelector(".link.selected");
|
||
|
if (current) { current.classList.remove("selected"); }
|
||
|
|
||
|
// ...select the new link
|
||
|
last.link.classList.add("selected");
|
||
|
}
|
||
|
|
||
|
// ...and scroll it to be visible
|
||
|
if (scrollVisible && !scrollPaused) {
|
||
|
const scrollTarget = getScrollTop(last);
|
||
|
const percentFragment = (last.offset - y) / last.height;
|
||
|
const delta = (getScrollTop(sidebarLinks[last.index + 1]) - scrollTarget) * percentFragment;
|
||
|
const scrollTop = scrollTarget - delta;
|
||
|
|
||
|
if (animate) {
|
||
|
const pi_2 = Math.PI / 2;
|
||
|
const totalDuration = 200;
|
||
|
const shift = scrollTop - sidebar.scrollTop;
|
||
|
const start = getTime();
|
||
|
const timer = setInterval(function() {
|
||
|
const duration = getTime() - start;
|
||
|
if (duration > totalDuration) {
|
||
|
clearInterval(timer);
|
||
|
sidebar.scrollTop = scrollTop;
|
||
|
return;
|
||
|
}
|
||
|
// linear: 0 -> 1
|
||
|
let i = duration / totalDuration;
|
||
|
// ease out: 1 -> 0
|
||
|
i = 1 - Math.sin(i * pi_2);
|
||
|
sidebar.scrollTop = scrollTop - i * shift;
|
||
|
}, 5);
|
||
|
} else {
|
||
|
sidebar.scrollTop = scrollTop;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sidebar.onmouseenter = function() {
|
||
|
scrollPaused = true;
|
||
|
}
|
||
|
|
||
|
sidebar.onmouseleave = function() {
|
||
|
scrollPaused = false;
|
||
|
highlightToc(true, true);
|
||
|
}
|
||
|
|
||
|
// Wehenver we scroll, highlight the TOC
|
||
|
window.onscroll = function() { highlightToc(true); }
|
||
|
|
||
|
// Poll occassionally to highlight the TOC (but don't auto-scroll)
|
||
|
setInterval(function() { highlightToc(false); }, 1000);
|
||
|
|
||
|
// Set up the initial TOC highlight
|
||
|
highlightToc(true);
|
||
|
}
|