// Populate the sidebar // // This is a script, and not included directly in the page, to control the total size of the book. // The TOC contains an entry for each page, so if each page includes a copy of the TOC, // the total size of the page becomes O(n**2). class MDBookSidebarScrollbox extends HTMLElement { constructor() { super(); } connectedCallback() { this.innerHTML = '{{#toc}}{{/toc}}'; // Set the current, active page, and reveal it if it's hidden let current_page = document.location.href.toString().split('#')[0].split('?')[0]; if (current_page.endsWith('/')) { current_page += 'index.html'; } const links = Array.prototype.slice.call(this.querySelectorAll('a')); const l = links.length; for (let i = 0; i < l; ++i) { const link = links[i]; const href = link.getAttribute('href'); if (href && !href.startsWith('#') && !/^(?:[a-z+]+:)?\/\//.test(href)) { link.href = path_to_root + href; } // The 'index' page is supposed to alias the first chapter in the book. if (link.href === current_page || i === 0 && path_to_root === '' && current_page.endsWith('/index.html')) { link.classList.add('active'); let parent = link.parentElement; while (parent) { if (parent.tagName === 'LI' && parent.classList.contains('chapter-item')) { parent.classList.add('expanded'); } parent = parent.parentElement; } } } // Track and set sidebar scroll position this.addEventListener('click', e => { if (e.target.tagName === 'A') { sessionStorage.setItem('sidebar-scroll', this.scrollTop); } }, { passive: true }); const sidebarScrollTop = sessionStorage.getItem('sidebar-scroll'); sessionStorage.removeItem('sidebar-scroll'); if (sidebarScrollTop) { // preserve sidebar scroll position when navigating via links within sidebar this.scrollTop = sidebarScrollTop; } else { // scroll sidebar to current active section when navigating via // 'next/previous chapter' buttons const activeSection = document.querySelector('#mdbook-sidebar .active'); if (activeSection) { activeSection.scrollIntoView({ block: 'center' }); } } // Toggle buttons const sidebarAnchorToggles = document.querySelectorAll('.chapter-fold-toggle'); function toggleSection(ev) { ev.currentTarget.parentElement.parentElement.classList.toggle('expanded'); } Array.from(sidebarAnchorToggles).forEach(el => { el.addEventListener('click', toggleSection); }); } } window.customElements.define('mdbook-sidebar-scrollbox', MDBookSidebarScrollbox); {{#if sidebar_header_nav}} // --------------------------------------------------------------------------- // Support for dynamically adding headers to the sidebar. (function() { // This is used to detect which direction the page has scrolled since the // last scroll event. let lastKnownScrollPosition = 0; // This is the threshold in px from the top of the screen where it will // consider a header the "current" header when scrolling down. const defaultDownThreshold = 150; // Same as defaultDownThreshold, except when scrolling up. const defaultUpThreshold = 300; // The threshold is a virtual horizontal line on the screen where it // considers the "current" header to be above the line. The threshold is // modified dynamically to handle headers that are near the bottom of the // screen, and to slightly offset the behavior when scrolling up vs down. let threshold = defaultDownThreshold; // This is used to disable updates while scrolling. This is needed when // clicking the header in the sidebar, which triggers a scroll event. It // is somewhat finicky to detect when the scroll has finished, so this // uses a relatively dumb system of disabling scroll updates for a short // time after the click. let disableScroll = false; // Array of header elements on the page. let headers; // Array of li elements that are initially collapsed headers in the sidebar. // I'm not sure why eslint seems to have a false positive here. // eslint-disable-next-line prefer-const let headerToggles = []; // This is a debugging tool for the threshold which you can enable in the console. let thresholdDebug = false; // Updates the threshold based on the scroll position. function updateThreshold() { const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const windowHeight = window.innerHeight; const documentHeight = document.documentElement.scrollHeight; // The number of pixels below the viewport, at most documentHeight. // This is used to push the threshold down to the bottom of the page // as the user scrolls towards the bottom. const pixelsBelow = Math.max(0, documentHeight - (scrollTop + windowHeight)); // The number of pixels above the viewport, at least defaultDownThreshold. // Similar to pixelsBelow, this is used to push the threshold back towards // the top when reaching the top of the page. const pixelsAbove = Math.max(0, defaultDownThreshold - scrollTop); // How much the threshold should be offset once it gets close to the // bottom of the page. const bottomAdd = Math.max(0, windowHeight - pixelsBelow - defaultDownThreshold); let adjustedBottomAdd = bottomAdd; // Adjusts bottomAdd for a small document. The calculation above // assumes the document is at least twice the windowheight in size. If // it is less than that, then bottomAdd needs to be shrunk // proportional to the difference in size. if (documentHeight < windowHeight * 2) { const maxPixelsBelow = documentHeight - windowHeight; const t = 1 - pixelsBelow / Math.max(1, maxPixelsBelow); const clamp = Math.max(0, Math.min(1, t)); adjustedBottomAdd *= clamp; } let scrollingDown = true; if (scrollTop < lastKnownScrollPosition) { scrollingDown = false; } if (scrollingDown) { // When scrolling down, move the threshold up towards the default // downwards threshold position. If near the bottom of the page, // adjustedBottomAdd will offset the threshold towards the bottom // of the page. const amountScrolledDown = scrollTop - lastKnownScrollPosition; const adjustedDefault = defaultDownThreshold + adjustedBottomAdd; threshold = Math.max(adjustedDefault, threshold - amountScrolledDown); } else { // When scrolling up, move the threshold down towards the default // upwards threshold position. If near the bottom of the page, // quickly transition the threshold back up where it normally // belongs. const amountScrolledUp = lastKnownScrollPosition - scrollTop; const adjustedDefault = defaultUpThreshold - pixelsAbove + Math.max(0, adjustedBottomAdd - defaultDownThreshold); threshold = Math.min(adjustedDefault, threshold + amountScrolledUp); } if (documentHeight <= windowHeight) { threshold = 0; } if (thresholdDebug) { const id = 'mdbook-threshold-debug-data'; let data = document.getElementById(id); if (data === null) { data = document.createElement('div'); data.id = id; data.style.cssText = ` position: fixed; top: 50px; right: 10px; background-color: 0xeeeeee; z-index: 9999; pointer-events: none; `; document.body.appendChild(data); } data.innerHTML = `
| documentHeight | ${documentHeight.toFixed(1)} |
| windowHeight | ${windowHeight.toFixed(1)} |
| scrollTop | ${scrollTop.toFixed(1)} |
| pixelsAbove | ${pixelsAbove.toFixed(1)} |
| pixelsBelow | ${pixelsBelow.toFixed(1)} |
| bottomAdd | ${bottomAdd.toFixed(1)} |
| adjustedBottomAdd | ${adjustedBottomAdd.toFixed(1)} |
| scrollingDown | ${scrollingDown} |
| threshold | ${threshold.toFixed(1)} |
.
const a = event.target.closest('a');
const href = a.getAttribute('href');
const targetId = href.substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
threshold = targetElement.getBoundingClientRect().bottom;
updateCurrentHeader();
}
});
});
}
// Takes the nodes from the given head and copies them over to the
// destination, along with some filtering.
function filterHeader(source, dest) {
const clone = source.cloneNode(true);
clone.querySelectorAll('mark').forEach(mark => {
mark.replaceWith(...mark.childNodes);
});
dest.append(...clone.childNodes);
}
// Scans page for headers and adds them to the sidebar.
document.addEventListener('DOMContentLoaded', function() {
const activeSection = document.querySelector('#mdbook-sidebar .active');
if (activeSection === null) {
return;
}
const main = document.getElementsByTagName('main')[0];
headers = Array.from(main.querySelectorAll('h2, h3, h4, h5, h6'))
.filter(h => h.id !== '' && h.children.length && h.children[0].tagName === 'A');
if (headers.length === 0) {
return;
}
// Build a tree of headers in the sidebar.
const stack = [];
const firstLevel = parseInt(headers[0].tagName.charAt(1));
for (let i = 1; i < firstLevel; i++) {
const ol = document.createElement('ol');
ol.classList.add('section');
if (stack.length > 0) {
stack[stack.length - 1].ol.appendChild(ol);
}
stack.push({level: i + 1, ol: ol});
}
// The level where it will start folding deeply nested headers.
const foldLevel = 3;
for (let i = 0; i < headers.length; i++) {
const header = headers[i];
const level = parseInt(header.tagName.charAt(1));
const currentLevel = stack[stack.length - 1].level;
if (level > currentLevel) {
// Begin nesting to this level.
for (let nextLevel = currentLevel + 1; nextLevel <= level; nextLevel++) {
const ol = document.createElement('ol');
ol.classList.add('section');
const last = stack[stack.length - 1];
const lastChild = last.ol.lastChild;
// Handle the case where jumping more than one nesting
// level, which doesn't have a list item to place this new
// list inside of.
if (lastChild) {
lastChild.appendChild(ol);
} else {
last.ol.appendChild(ol);
}
stack.push({level: nextLevel, ol: ol});
}
} else if (level < currentLevel) {
while (stack.length > 1 && stack[stack.length - 1].level > level) {
stack.pop();
}
}
const li = document.createElement('li');
li.classList.add('header-item');
li.classList.add('expanded');
if (level < foldLevel) {
li.classList.add('expanded');
}
const span = document.createElement('span');
span.classList.add('chapter-link-wrapper');
const a = document.createElement('a');
span.appendChild(a);
a.href = '#' + header.id;
a.classList.add('header-in-summary');
filterHeader(header.children[0], a);
a.addEventListener('click', headerThresholdClick);
const nextHeader = headers[i + 1];
if (nextHeader !== undefined) {
const nextLevel = parseInt(nextHeader.tagName.charAt(1));
if (nextLevel > level && level >= foldLevel) {
const toggle = document.createElement('a');
toggle.classList.add('chapter-fold-toggle');
toggle.classList.add('header-toggle');
toggle.addEventListener('click', () => {
li.classList.toggle('expanded');
});
const toggleDiv = document.createElement('div');
toggleDiv.textContent = '❱';
toggle.appendChild(toggleDiv);
span.appendChild(toggle);
headerToggles.push(li);
}
}
li.appendChild(span);
const currentParent = stack[stack.length - 1];
currentParent.ol.appendChild(li);
}
const onThisPage = document.createElement('div');
onThisPage.classList.add('on-this-page');
onThisPage.append(stack[0].ol);
const activeItemSpan = activeSection.parentElement;
activeItemSpan.after(onThisPage);
});
document.addEventListener('DOMContentLoaded', reloadCurrentHeader);
document.addEventListener('scroll', reloadCurrentHeader, { passive: true });
})();
{{/if}}