const getPageToc = () => document.getElementsByClassName('pagetoc')[0]; const pageToc = getPageToc(); const pageTocChildren = [...pageToc.children]; const headers = [...document.getElementsByClassName('header')]; // Select highlighted item in ToC when clicking an item pageTocChildren.forEach(child => { child.addEventHandler('click', () => { pageTocChildren.forEach(child => { child.classList.remove('active'); }); child.classList.add('active'); }); }); /** * Test whether a node is in the viewport */ function isInViewport(node) { const rect = node.getBoundingClientRect(); return rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth); } /** * Set a new ToC entry. * Clear any previously highlighted ToC items, set the new one, * and adjust the ToC scroll position. */ function setTocEntry() { let activeEntry; const pageTocChildren = [...getPageToc().children]; // Calculate which header is the current one at the top of screen headers.forEach(header => { if (window.pageYOffset >= header.offsetTop) { activeEntry = header; } }); // Update selected item in ToC when scrolling pageTocChildren.forEach(child => { if (activeEntry.href.localeCompare(child.href) === 0) { child.classList.add('active'); } else { child.classList.remove('active'); } }); let tocEntryForLocation = document.querySelector(`nav a[href="${activeEntry.href}"]`); if (tocEntryForLocation) { const headingForLocation = document.querySelector(activeEntry.hash); if (headingForLocation && isInViewport(headingForLocation)) { // Update ToC scroll const nav = getPageToc(); const content = document.querySelector('html'); if (content.scrollTop !== 0) { nav.scrollTo({ top: tocEntryForLocation.offsetTop - 100, left: 0, behavior: 'smooth', }); } else { nav.scrollTop = 0; } } } } /** * Populate sidebar on load */ window.addEventListener('load', () => { // Prevent rendering the table of contents of the "print book" page, as it // will end up being rendered into the output (in a broken-looking way) // Get the name of the current page (i.e. 'print.html') const pageNameExtension = window.location.pathname.split('/').pop(); // Split off the extension (as '.../print' is also a valid page name), which // should result in 'print' const pageName = pageNameExtension.split('.')[0]; if (pageName === "print") { // Don't render the table of contents on this page return; } // Only create table of contents if there is more than one header on the page if (headers.length <= 1) { return; } // Create an entry in the page table of contents for each header in the document headers.forEach((header, index) => { const link = document.createElement('a'); // Indent shows hierarchy let indent = '0px'; switch (header.parentElement.tagName) { case 'H1': indent = '5px'; break; case 'H2': indent = '20px'; break; case 'H3': indent = '30px'; break; case 'H4': indent = '40px'; break; case 'H5': indent = '50px'; break; case 'H6': indent = '60px'; break; default: break; } let tocEntry; if (index == 0) { // Create a bolded title for the first element tocEntry = document.createElement("strong"); tocEntry.innerHTML = header.text; } else { // All other elements are non-bold tocEntry = document.createTextNode(header.text); } link.appendChild(tocEntry); link.style.paddingLeft = indent; link.href = header.href; pageToc.appendChild(link); }); setTocEntry.call(); }); // Handle active headers on scroll, if there is more than one header on the page if (headers.length > 1) { window.addEventListener('scroll', setTocEntry); }