// 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 = '
  1. Introduction
  2. Getting Started
  3. Installation
  4. Shell Completion
  5. Usage
  6. Guide
  7. Why?
  8. Packaging
  9. Packages
  10. Configuration
  11. Server
  12. Design
  13. Comparisons
  14. Reference
  15. API
    ❱
    1. /api/directory/{hash}
    2. /api/file/{hash}
    3. /api/gc
    4. /api/head/{revision}
    5. /api/missing
    6. /api/number/{number}
    7. /api/numbers
    8. /api/package/{fingerprint}
    9. /api/packages
    10. /api/revision/{revision}
  16. Commands
    ❱
    1. filepack
    2. filepack archive
    3. filepack completions
    4. filepack contains
    5. filepack create
    6. filepack delete
    7. filepack download
    8. filepack files
    9. filepack fingerprint
    10. filepack gc
    11. filepack hash
    12. filepack info
    13. filepack key
    14. filepack keygen
    15. filepack languages
    16. filepack lints
    17. filepack man
    18. filepack manifest
    19. filepack metadata
    20. filepack reference
    21. filepack serve
    22. filepack sign
    23. filepack signatures
    24. filepack size
    25. filepack upload
    26. filepack verify
  17. Content Types
  18. Deco
    ❱
    1. Application
    2. Archive
    3. Audio
    4. AudioCodec
    5. AudioFormat
    6. Boolean
    7. CheckedUrl
    8. ChromaSubsampling
    9. Claims
    10. ColorType
    11. ComponentBuf
    12. Context
    13. Dimensions
    14. Directory
    15. DisplayPrivateKey
    16. Ed25519Signature
    17. Entry
    18. EntryInfo
    19. Envelope<Claims>
    20. Envelope<Statement>
    21. Fingerprint
    22. Hash
    23. Image
    24. ImageFormat
    25. Item<Audio>
    26. Item<Image>
    27. Item<Video>
    28. Language
    29. Media
    30. Metadata
    31. Mode
    32. Orientation
    33. PackageMetadata
    34. PublicKey
    35. RelativePath
    36. Revision
    37. RevisionObject
    38. Rotation
    39. Signature<Claims>
    40. Signature<Statement>
    41. SignedInteger
    42. Statement
    43. String
    44. Text
    45. Time
    46. Totals
    47. Track
    48. TrackCodec
    49. TrackInfo
    50. UnsignedInteger
    51. Video
    52. VideoFormat
  19. JSON
    ❱
    1. Audio
    2. AudioCodec
    3. AudioFormat
    4. Boolean
    5. Bytes
    6. CheckedUrl
    7. ChromaSubsampling
    8. ColorType
    9. ComponentBuf
    10. Dimensions
    11. DirectoryTree
    12. DirectoryTreeEntry
    13. File
    14. Hash
    15. Image
    16. ImageFormat
    17. Info
    18. Item<Audio>
    19. Item<Image>
    20. Item<Video>
    21. KeyName
    22. Language
    23. Lint
    24. LintGroup
    25. Manifest
    26. Media
    27. Metadata
    28. Orientation
    29. PackageMetadata
    30. PublicKey
    31. RelativePath
    32. Rotation
    33. Signature<Statement>
    34. SignatureEntry
    35. String
    36. Text
    37. Time
    38. Totals
    39. Track
    40. TrackCodec
    41. TrackInfo
    42. UnsignedInteger
    43. Utf8PathBuf
    44. Video
    45. VideoFormat
  20. Languages
  21. Lints
  22. Media
  23. Metadata
  24. Tagged Hex
    ❱
    1. hash
    2. package
    3. private
    4. public
    5. revision
    6. signature
    7. token
  25. Unstable Features
  26. Web
    ❱
    1. /
    2. /artwork/{fingerprint}
    3. /artwork/{fingerprint}/thumbnail
    4. /directory/{hash}
    5. /favicon.ico
    6. /file/{hash}
    7. /file/{hash}/{*path}
    8. /files
    9. /install.sh
    10. /media/audio/{fingerprint}/item/{item}
    11. /media/image/{fingerprint}/item/{item}
    12. /media/image/{fingerprint}/item/{item}/thumbnail
    13. /media/video/{fingerprint}/item/{item}
    14. /media/video/{fingerprint}/item/{item}/placeholder
    15. /media/video/{fingerprint}/item/{item}/placeholder/thumbnail
    16. /mount/{package}
    17. /mount/{package}/
    18. /mount/{package}/{*path}
    19. /package/{package}
    20. /package/{package}/history
    21. /package/{package}/item/{item}
    22. /package/{package}/media
    23. /packages
    24. /static/{*path}
  27. YAML
    ❱
    1. Audio
    2. CheckedUrl
    3. Image
    4. Language
    5. Media
    6. Metadata
    7. PackageMetadata
    8. RelativePath
    9. String
    10. Text
    11. Time
    12. Video
'; // 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. // Check both with and without the '.html' suffix to be robust against pretty URLs if (link.href.replace(/\.html$/, '') === current_page.replace(/\.html$/, '') || 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') { const clientRect = e.target.getBoundingClientRect(); const sidebarRect = this.getBoundingClientRect(); sessionStorage.setItem('sidebar-scroll-offset', clientRect.top - sidebarRect.top); } }, { passive: true }); const sidebarScrollOffset = sessionStorage.getItem('sidebar-scroll-offset'); sessionStorage.removeItem('sidebar-scroll-offset'); if (sidebarScrollOffset !== null) { // preserve sidebar scroll position when navigating via links within sidebar const activeSection = this.querySelector('.active'); if (activeSection) { const clientRect = activeSection.getBoundingClientRect(); const sidebarRect = this.getBoundingClientRect(); const currentOffset = clientRect.top - sidebarRect.top; this.scrollTop += currentOffset - parseFloat(sidebarScrollOffset); } } 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); // --------------------------------------------------------------------------- // 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)}
`; drawDebugLine(); } lastKnownScrollPosition = scrollTop; } function drawDebugLine() { if (!document.body) { return; } const id = 'mdbook-threshold-debug-line'; const existingLine = document.getElementById(id); if (existingLine) { existingLine.remove(); } const line = document.createElement('div'); line.id = id; line.style.cssText = ` position: fixed; top: ${threshold}px; left: 0; width: 100vw; height: 2px; background-color: red; z-index: 9999; pointer-events: none; `; document.body.appendChild(line); } function mdbookEnableThresholdDebug() { thresholdDebug = true; updateThreshold(); drawDebugLine(); } window.mdbookEnableThresholdDebug = mdbookEnableThresholdDebug; // Updates which headers in the sidebar should be expanded. If the current // header is inside a collapsed group, then it, and all its parents should // be expanded. function updateHeaderExpanded(currentA) { // Add expanded to all header-item li ancestors. let current = currentA.parentElement; while (current) { if (current.tagName === 'LI' && current.classList.contains('header-item')) { current.classList.add('expanded'); } current = current.parentElement; } } // Updates which header is marked as the "current" header in the sidebar. // This is done with a virtual Y threshold, where headers at or below // that line will be considered the current one. function updateCurrentHeader() { if (!headers || !headers.length) { return; } // Reset the classes, which will be rebuilt below. const els = document.getElementsByClassName('current-header'); for (const el of els) { el.classList.remove('current-header'); } for (const toggle of headerToggles) { toggle.classList.remove('expanded'); } // Find the last header that is above the threshold. let lastHeader = null; for (const header of headers) { const rect = header.getBoundingClientRect(); if (rect.top <= threshold) { lastHeader = header; } else { break; } } if (lastHeader === null) { lastHeader = headers[0]; const rect = lastHeader.getBoundingClientRect(); const windowHeight = window.innerHeight; if (rect.top >= windowHeight) { return; } } // Get the anchor in the summary. const href = '#' + lastHeader.id; const a = [...document.querySelectorAll('.header-in-summary')] .find(element => element.getAttribute('href') === href); if (!a) { return; } a.classList.add('current-header'); updateHeaderExpanded(a); } // Updates which header is "current" based on the threshold line. function reloadCurrentHeader() { if (disableScroll) { return; } updateThreshold(); updateCurrentHeader(); } // When clicking on a header in the sidebar, this adjusts the threshold so // that it is located next to the header. This is so that header becomes // "current". function headerThresholdClick(event) { // See disableScroll description why this is done. disableScroll = true; setTimeout(() => { disableScroll = false; }, 100); // requestAnimationFrame is used to delay the update of the "current" // header until after the scroll is done, and the header is in the new // position. requestAnimationFrame(() => { requestAnimationFrame(() => { // Closest is needed because if it has child elements like . 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 }); })();