1// Copyright (C) 2020 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15'use strict'; 16 17let tocAnchors = []; 18let lastMouseOffY = 0; 19let onloadFired = false; 20const postLoadActions = []; 21let tocEventHandlersInstalled = false; 22let resizeObserver = undefined; 23 24function doAfterLoadEvent(action) { 25 if (onloadFired) { 26 return action(); 27 } 28 postLoadActions.push(action); 29} 30 31function setupSandwichMenu() { 32 const header = document.querySelector('.site-header'); 33 const docsNav = document.querySelector('.nav'); 34 const menu = header.querySelector('.menu'); 35 menu.addEventListener('click', (e) => { 36 e.preventDefault(); 37 38 // If we are displaying any /docs, toggle the navbar instead (the TOC). 39 if (docsNav) { 40 // |after_first_click| is to avoid spurious transitions on page load. 41 docsNav.classList.add('after_first_click'); 42 updateNav(); 43 setTimeout(() => docsNav.classList.toggle('expanded'), 0); 44 } else { 45 header.classList.toggle('expanded'); 46 } 47 }); 48} 49 50// (Re-)Generates the Table Of Contents for docs (the right-hand-side one). 51function updateTOC() { 52 const tocContainer = document.querySelector('.docs .toc'); 53 if (!tocContainer) 54 return; 55 const toc = document.createElement('ul'); 56 const anchors = document.querySelectorAll('.doc a.anchor'); 57 tocAnchors = []; 58 for (const anchor of anchors) { 59 const li = document.createElement('li'); 60 const link = document.createElement('a'); 61 link.innerText = anchor.parentElement.innerText; 62 link.href = anchor.href; 63 link.onclick = () => { 64 onScroll(link) 65 }; 66 li.appendChild(link); 67 if (anchor.parentElement.tagName === 'H3') 68 li.style.paddingLeft = '10px'; 69 toc.appendChild(li); 70 doAfterLoadEvent(() => { 71 tocAnchors.push( 72 { top: anchor.offsetTop + anchor.offsetHeight / 2, obj: link }); 73 }); 74 } 75 tocContainer.innerHTML = ''; 76 tocContainer.appendChild(toc); 77 78 // Add event handlers on the first call (can be called more than once to 79 // recompute anchors on resize). 80 if (tocEventHandlersInstalled) 81 return; 82 tocEventHandlersInstalled = true; 83 const doc = document.querySelector('.doc'); 84 const passive = { passive: true }; 85 if (doc) { 86 const offY = doc.offsetTop; 87 doc.addEventListener('mousemove', (e) => onMouseMove(offY, e), passive); 88 doc.addEventListener('mouseleave', () => { 89 lastMouseOffY = 0; 90 }, passive); 91 } 92 window.addEventListener('scroll', () => onScroll(), passive); 93 resizeObserver = new ResizeObserver(() => requestAnimationFrame(() => { 94 updateNav(); 95 updateTOC(); 96 })); 97 resizeObserver.observe(doc); 98} 99 100// Highlights the current TOC anchor depending on the scroll offset. 101function onMouseMove(offY, e) { 102 lastMouseOffY = e.clientY - offY; 103 onScroll(); 104} 105 106function onScroll(forceHighlight) { 107 const y = document.documentElement.scrollTop + lastMouseOffY; 108 let highEl = undefined; 109 for (const x of tocAnchors) { 110 if (y < x.top) 111 continue; 112 highEl = x.obj; 113 } 114 for (const link of document.querySelectorAll('.docs .toc a')) { 115 if ((!forceHighlight && link === highEl) || (forceHighlight === link)) { 116 link.classList.add('highlighted'); 117 } else { 118 link.classList.remove('highlighted'); 119 } 120 } 121} 122 123// This function needs to be idempotent as it is called more than once (on every 124// resize). 125function updateNav() { 126 const curDoc = document.querySelector('.doc'); 127 let curFileName = ''; 128 if (curDoc) 129 curFileName = curDoc.dataset['mdFile']; 130 131 // First identify all the top-level nav entries (Quickstart, Data Sources, 132 // ...) and make them compressible. 133 const toplevelSections = document.querySelectorAll('.docs .nav > ul > li'); 134 const toplevelLinks = []; 135 for (const sec of toplevelSections) { 136 const childMenu = sec.querySelector('ul'); 137 if (!childMenu) { 138 // Don't make it compressible if it has no children (e.g. the very 139 // first 'Introduction' link). 140 continue; 141 } 142 143 // Don't make it compressible if the entry has an actual link (e.g. the very 144 // first 'Introduction' link), because otherwise it become ambiguous whether 145 // the link should toggle or open the link. 146 const link = sec.querySelector('a'); 147 if (!link || !link.href.endsWith('#')) 148 continue; 149 150 sec.classList.add('compressible'); 151 152 // Remember the compressed status as long as the page is opened, so clicking 153 // through links keeps the sidebar in a consistent visual state. 154 const memoKey = `docs.nav.compressed[${link.innerHTML}]`; 155 156 if (sessionStorage.getItem(memoKey) === '1') { 157 sec.classList.add('compressed'); 158 } 159 doAfterLoadEvent(() => { 160 childMenu.style.maxHeight = `${childMenu.scrollHeight + 40}px`; 161 }); 162 163 toplevelLinks.push(link); 164 link.onclick = (evt) => { 165 evt.preventDefault(); 166 sec.classList.toggle('compressed'); 167 if (sec.classList.contains('compressed')) { 168 sessionStorage.setItem(memoKey, '1'); 169 } else { 170 sessionStorage.removeItem(memoKey); 171 } 172 }; 173 } 174 175 const exps = document.querySelectorAll('.docs .nav ul a'); 176 let found = false; 177 for (const x of exps) { 178 // If the url of the entry matches the url of the page, mark the item as 179 // highlighted and expand all its parents. 180 if (!x.href) 181 continue; 182 const url = new URL(x.href); 183 if (x.href.endsWith('#')) { 184 // This is a non-leaf link to a menu. 185 if (toplevelLinks.indexOf(x) < 0) { 186 x.removeAttribute('href'); 187 } 188 } else if (url.pathname === curFileName && !found) { 189 x.classList.add('selected'); 190 doAfterLoadEvent(() => x.scrollIntoViewIfNeeded()); 191 found = true; // Highlight only the first occurrence. 192 } 193 } 194} 195 196// If the page contains a ```mermaid ``` block, lazily loads the plugin and 197// renders. 198function initMermaid() { 199 const graphs = document.querySelectorAll('.mermaid'); 200 201 // Skip if there are no mermaid graphs to render. 202 if (!graphs.length) 203 return; 204 205 const script = document.createElement('script'); 206 script.type = 'text/javascript'; 207 script.src = '/assets/mermaid.min.js'; 208 const themeCSS = ` 209 .cluster rect { fill: #FCFCFC; stroke: #ddd } 210 .node rect { fill: #DCEDC8; stroke: #8BC34A} 211 .edgeLabel:not(:empty) { 212 border-radius: 6px; 213 font-size: 0.9em; 214 padding: 4px; 215 background: #F5F5F5; 216 border: 1px solid #DDDDDD; 217 color: #666; 218 } 219 `; 220 script.addEventListener('load', () => { 221 mermaid.initialize({ 222 startOnLoad: false, 223 themeCSS: themeCSS, 224 securityLevel: 'loose', // To allow #in-page-links 225 }); 226 for (const graph of graphs) { 227 requestAnimationFrame(() => { 228 mermaid.init(undefined, graph); 229 graph.classList.add('rendered'); 230 }); 231 } 232 }) 233 document.body.appendChild(script); 234} 235 236function setupSearch() { 237 const URL = 238 'https://www.googleapis.com/customsearch/v1?key=AIzaSyBTD2XJkQkkuvDn76LSftsgWOkdBz9Gfwo&cx=007128963598137843411:8suis14kcmy&q=' 239 const searchContainer = document.getElementById('search'); 240 const searchBox = document.getElementById('search-box'); 241 const searchRes = document.getElementById('search-res') 242 if (!searchBox || !searchRes) return; 243 244 document.body.addEventListener('keydown', (e) => { 245 if (e.key === '/' && e.target.tagName.toLowerCase() === 'body') { 246 searchBox.setSelectionRange(0, -1); 247 searchBox.focus(); 248 e.preventDefault(); 249 } else if (e.key === 'Escape' && searchContainer.contains(e.target)) { 250 searchBox.blur(); 251 252 // Handle the case of clicking Tab and moving down to results. 253 e.target.blur(); 254 } 255 }); 256 257 let timerId = -1; 258 let lastSearchId = 0; 259 260 const doSearch = async () => { 261 timerId = -1; 262 searchRes.style.width = `${searchBox.offsetWidth}px`; 263 264 // `searchId` handles the case of two subsequent requests racing. This is to 265 // prevent older results, delivered in reverse order, to replace newer ones. 266 const searchId = ++lastSearchId; 267 const f = await fetch(URL + encodeURIComponent(searchBox.value)); 268 const jsonRes = await f.json(); 269 const results = jsonRes['items']; 270 searchRes.innerHTML = ''; 271 if (results === undefined || searchId != lastSearchId) { 272 return; 273 } 274 for (const res of results) { 275 const link = document.createElement('a'); 276 link.href = res.link; 277 const title = document.createElement('div'); 278 title.className = 'sr-title'; 279 title.innerText = res.title.replace(' - Perfetto Tracing Docs', ''); 280 link.appendChild(title); 281 282 const snippet = document.createElement('div'); 283 snippet.className = 'sr-snippet'; 284 snippet.innerText = res.snippet; 285 link.appendChild(snippet); 286 287 const div = document.createElement('div'); 288 div.appendChild(link); 289 searchRes.appendChild(div); 290 } 291 }; 292 293 searchBox.addEventListener('keyup', () => { 294 if (timerId >= 0) return; 295 timerId = setTimeout(doSearch, 200); 296 }); 297} 298 299window.addEventListener('DOMContentLoaded', () => { 300 updateNav(); 301 updateTOC(); 302}); 303 304window.addEventListener('load', () => { 305 setupSandwichMenu(); 306 initMermaid(); 307 308 // Don't smooth-scroll on pages that are too long (e.g. reference pages). 309 if (document.body.scrollHeight < 10000) { 310 document.documentElement.style.scrollBehavior = 'smooth'; 311 } else { 312 document.documentElement.style.scrollBehavior = 'initial'; 313 } 314 315 onloadFired = true; 316 while (postLoadActions.length > 0) { 317 postLoadActions.shift()(); 318 } 319 320 updateTOC(); 321 setupSearch(); 322 323 // Enable animations only after the load event. This is to prevent glitches 324 // when switching pages. 325 document.documentElement.style.setProperty('--anim-enabled', '1') 326}); 327 328// Handles redirects from the old docs.perfetto.dev. 329const legacyRedirectMap = { 330 '#/contributing': '/docs/contributing/getting-started#community', 331 '#/build-instructions': '/docs/contributing/build-instructions', 332 '#/testing': '/docs/contributing/testing', 333 '#/app-instrumentation': '/docs/instrumentation/tracing-sdk', 334 '#/recording-traces': '/docs/instrumentation/tracing-sdk#recording', 335 '#/running': '/docs/quickstart/android-tracing', 336 '#/long-traces': '/docs/concepts/config#long-traces', 337 '#/detached-mode': '/docs/concepts/detached-mode', 338 '#/heapprofd': '/docs/data-sources/native-heap-profiler', 339 '#/java-hprof': '/docs/data-sources/java-heap-profiler', 340 '#/trace-processor': '/docs/analysis/trace-processor', 341 '#/analysis': '/docs/analysis/trace-processor#annotations', 342 '#/metrics': '/docs/analysis/metrics', 343 '#/traceconv': '/docs/quickstart/traceconv', 344 '#/clock-sync': '/docs/concepts/clock-sync', 345 '#/architecture': '/docs/concepts/service-model', 346}; 347 348const fragment = location.hash.split('?')[0].replace('.md', ''); 349if (fragment in legacyRedirectMap) { 350 location.replace(legacyRedirectMap[fragment]); 351} 352 353// Pages which have been been removed/renamed/moved and need to be redirected 354// to their new home. 355const redirectMap = { 356 // stdlib docs is not a perfect replacement but is good enough until we write 357 // a proper, Android specific query codelab page. 358 // TODO(lalitm): switch to that page when it's ready. 359 '/docs/analysis/common-queries': '/docs/analysis/stdlib-docs', 360}; 361 362if (location.pathname in redirectMap) { 363 location.replace(redirectMap[location.pathname]); 364} 365