1/** 2 * Sidebar navigation: an accordion over the toctree captions, plus the mobile 3 * drawer. 4 * 5 * The Envoy tree has nine top-level areas and thousands of pages behind them. 6 * sphinx_rtd_theme renders every caption and its subtree at the same visual 7 * weight, which gives no sense of place. Here each caption becomes a 8 * <details> and only the section containing the current page starts open. 9 */ 10 11const DESKTOP_QUERY = '(max-width: 960px)'; 12 13/** 14 * A line icon for each top-level area. 15 * 16 * Keyed on the link target rather than the label: the real tree has no toctree 17 * captions, just nine `toctree-l1` links, and their titles are prose that gets 18 * reworded far more often than their paths move. Anything unmatched simply has 19 * no icon, which is why the fallback is nothing rather than a generic glyph. 20 */ 21const AREA_ICONS = { 22 about_docs: '<path d="M8 4.6v8"/><path d="M2.5 3.4h4A1.5 1.5 0 0 1 8 4.6v8a1.5 1.5 0 0 0-1.5-1.1h-4z"/><path d="M13.5 3.4h-4A1.5 1.5 0 0 0 8 4.6v8a1.5 1.5 0 0 1 1.5-1.1h4z"/>', 23 intro: '<circle cx="8" cy="8" r="6"/><path d="M8 7.3v4.1M8 4.8v.2"/>', 24 start: '<circle cx="8" cy="8" r="6"/><path d="M6.6 5.5l4.1 2.5-4.1 2.5z"/>', 25 configuration: '<path d="M2 5.2h12M2 10.8h12"/><circle cx="6" cy="5.2" r="1.6"/><circle cx="10.4" cy="10.8" r="1.6"/>', 26 operations: '<path d="M2.4 12a5.9 5.9 0 1 1 11.2 0"/><path d="M8 12l2.9-3.4"/>', 27 extending: '<path d="M2.6 2.6h4.6v4.6H2.6z"/><path d="M8.8 2.6h4.6v4.6H8.8z"/><path d="M8.8 8.8h4.6v4.6H8.8z"/>', 28 api: '<path d="M6.1 2.6C4.6 2.6 4.6 8 3.1 8c1.5 0 1.5 5.4 3 5.4"/><path d="M9.9 2.6c1.5 0 1.5 5.4 3 5.4-1.5 0-1.5 5.4-3 5.4"/>', 29 faq: '<circle cx="8" cy="8" r="6"/><path d="M6.3 6.4a1.75 1.75 0 1 1 2.3 1.7c-.4.2-.6.5-.6.9v.3"/><path d="M8 11.6v.1"/>', 30 version_history: '<circle cx="8" cy="8" r="6"/><path d="M8 4.7V8l2.4 1.5"/>', 31}; 32 33/** 34 * The first meaningful path segment of a top-level link. 35 * 36 * Sidebar hrefs are relative to the current page, so from deep in the API tree 37 * they arrive as `../../start/start.html`; the leading hops have to come off 38 * before the segment means anything. 39 */ 40function areaOf(href) { 41 const path = href.split(/[?#]/)[0].replace(/^(?:\.\.?\/)+/, ''); 42 const [first, ...rest] = path.split('/'); 43 return rest.length ? first : first.replace(/\.html$/, ''); 44} 45 46/** Prefixes each top-level entry with its area icon. */ 47function addIcons(menu) { 48 menu.querySelectorAll(':scope > ul > li.toctree-l1 > a[href]').forEach((link) => { 49 const paths = AREA_ICONS[areaOf(link.getAttribute('href'))]; 50 if (!paths || link.querySelector('.envoy-nav-icon')) { 51 return; 52 } 53 54 const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 55 icon.setAttribute('class', 'envoy-nav-icon'); 56 icon.setAttribute('viewBox', '0 0 16 16'); 57 icon.setAttribute('width', '14'); 58 icon.setAttribute('height', '14'); 59 icon.setAttribute('fill', 'none'); 60 icon.setAttribute('stroke', 'currentColor'); 61 icon.setAttribute('stroke-width', '1.4'); 62 icon.setAttribute('stroke-linecap', 'round'); 63 icon.setAttribute('aria-hidden', 'true'); 64 icon.innerHTML = paths; 65 66 link.prepend(icon); 67 link.classList.add('envoy-nav-area'); 68 }); 69} 70 71/** 72 * Every entry in the API tree ends in "(proto)", which distinguishes none of 73 * them and costs a wrapped line on the long ones. The full title stays in the 74 * tooltip; the page's own heading is untouched. 75 */ 76function trimTitles(menu) { 77 menu.querySelectorAll('a[href]').forEach((link) => { 78 const text = link.textContent.trim(); 79 if (!text.endsWith('(proto)')) { 80 return; 81 } 82 83 link.title = text; 84 const trimmed = text.replace(/\s*\(proto\)$/, ''); 85 const node = Array.from(link.childNodes) 86 .reverse() 87 .find((child) => child.nodeType === Node.TEXT_NODE && child.textContent.includes('(proto)')); 88 89 if (node) { 90 node.textContent = node.textContent.replace(/\s*\(proto\)\s*$/, ''); 91 } else { 92 link.textContent = trimmed; 93 } 94 }); 95} 96 97/** Groups each `p.caption` and the `ul` that follows it into a disclosure. */ 98function buildSections(menu) { 99 const captions = Array.from(menu.querySelectorAll(':scope > p.caption')); 100 if (!captions.length) { 101 return; 102 } 103 104 captions.forEach((caption) => { 105 const list = caption.nextElementSibling; 106 if (!list || list.tagName !== 'UL') { 107 return; 108 } 109 110 const label = caption.textContent.trim(); 111 const details = document.createElement('details'); 112 details.className = 'envoy-nav-section'; 113 114 const summary = document.createElement('summary'); 115 summary.textContent = label; 116 details.append(summary); 117 118 caption.replaceWith(details); 119 details.append(list); 120 121 // Open the section holding the current page; if the reader is on a page 122 // that is in no toctree, leave the first section open as a
122starting point. 123 if (list.querySelector('.current')) { 124 details.open = true; 125 } 126 }); 127 128 const sections = Array.from(menu.querySelectorAll('.envoy-nav-section')); 129 if (sections.length && !sections.some((section) => section.open)) { 130 sections[0].open = true; 131 } 132} 133 134function focusable(container) { 135 return Array.from(container.querySelectorAll( 136 'a[href], button:not([disabled]), input:not([disabled]), summary, ' + 137 '[tabindex]:not([tabindex="-1"])' 138 )).filter((element) => !element.hidden && element.offsetParent !== null); 139} 140 141export function init() { 142 const menu = document.querySelector('.wy-menu-vertical'); 143 if (menu) { 144 // The published tree has no toctree captions â it is a flat list of nine 145 // `toctree-l1` links â so this is a no-op there and only groups builds that 146 // do use captions. 147 buildSections(menu); 148 addIcons(menu); 149 trimTitles(menu); 150 151 // Keep the current page in view when the sidebar is taller than the pane. 152 const current = menu.querySelector('a.current'); 153 if (current) { 154 window.requestAnimationFrame(() => { 155 current.scrollIntoView({block: 'nearest'}); 156 }); 157 } 158 } 159 160 const toggle = document.querySelector('[data-envoy-nav-toggle]'); 161 const sidebar = document.querySelector('.wy-nav-side'); 162 const backdrop = document.querySelector('.envoy-nav-backdrop'); 163 const content = document.querySelector('.wy-nav-content-wrap'); 164 const topbar = document.querySelector('.envoy-doc-topbar'); 165 const mobile = window.matchMedia(DESKTOP_QUERY); 166 167 if (!toggle || !sidebar || !backdrop) { 168 return; 169 } 170 171 sidebar.id = sidebar.id || 'envoy-doc-sidebar'; 172 173 let open = false; 174 let returnFocus = null; 175 176 function setOpen(next, restoreFocus) { 177 open = next && mobile.matches; 178 document.body.classList.toggle('envoy-nav-open', open); 179 toggle.setAttribute('aria-expanded', String(open)); 180 toggle.setAttribute( 181 'aria-label', open ? 'Close documentation navigation' : 182 'Open documentation navigation'); 183 backdrop.hidden = !open; 184 185 const hidden = mobile.matches && !open; 186 sidebar.inert = hidden; 187 sidebar.toggleAttribute('aria-hidden', hidden); 188 189 [content, topbar].forEach((element) => { 190 if (!element) { 191 return; 192 } 193 element.inert = open; 194 element.toggleAttribute('aria-hidden', open); 195 }); 196 197 if (open) { 198 returnFocus = document.activeElement; 199 const targets = focusable(sidebar); 200 if (targets.length) { 201 window.requestAnimationFrame(() => targets[0].focus()); 202 } 203 } else if (restoreFocus && returnFocus instanceof HTMLElement) { 204 returnFocus.focus(); 205 } 206 } 207 208 setOpen(false, false); 209 210 toggle.addEventListener('click', () => setOpen(!open, true)); 211 212 document.querySelectorAll('[data-envoy-nav-close]').forEach((button) => { 213 button.addEventListener('click', () => setOpen(false, true)); 214 }); 215 216 sidebar.addEventListener('click', (event) => { 217 if (event.target.closest('a[href]') && mobile.matches) { 218 setOpen(false, false); 219 } 220 }); 221 222 document.addEventListener('keydown', (event) => { 223 if (!open) { 224 return; 225 } 226 227 if (event.key === 'Escape') { 228 event.preventDefault(); 229 setOpen(false, true); 230 return; 231 } 232 233 if (event.key === 'Tab') { 234 const targets = focusable(sidebar); 235 if (!targets.length) { 236 return; 237 } 238 239 const first = targets[0]; 240 const last = targets[targets.length - 1]; 241 if (event.shiftKey && document.activeElement === first) { 242 event.preventDefault(); 243 last.focus(); 244 } else if (!event.shiftKey && document.activeElement === last) { 245 event.preventDefault(); 246 first.focus(); 247 } 248 } 249 }); 250 251 mobile.addEventListener('change', () => setOpen(false, false)); 252}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.