| // Copyright (C) 2020 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| 'use strict'; |
| |
| let tocAnchors = []; |
| let lastMouseOffY = 0; |
| let onloadFired = false; |
| const postLoadActions = []; |
| let tocEventHandlersInstalled = false; |
| let resizeObserver = undefined; |
| |
| function doAfterLoadEvent(action) { |
| if (onloadFired) { |
| return action(); |
| } |
| postLoadActions.push(action); |
| } |
| |
| function setupSandwichMenu() { |
| const header = document.querySelector('.site-header'); |
| const docsNav = document.querySelector('.nav'); |
| const menu = header.querySelector('.menu'); |
| menu.addEventListener('click', (e) => { |
| e.preventDefault(); |
| |
| // If we are displaying any /docs, toggle the navbar instead (the TOC). |
| if (docsNav) { |
| // |after_first_click| is to avoid spurious transitions on page load. |
| docsNav.classList.add('after_first_click'); |
| updateNav(); |
| setTimeout(() => docsNav.classList.toggle('expanded'), 0); |
| } else { |
| header.classList.toggle('expanded'); |
| } |
| }); |
| } |
| |
| // (Re-)Generates the Table Of Contents for docs (the right-hand-side one). |
| function updateTOC() { |
| const tocContainer = document.querySelector('.docs .toc'); |
| if (!tocContainer) |
| return; |
| const toc = document.createElement('ul'); |
| const anchors = document.querySelectorAll('.doc a.anchor'); |
| tocAnchors = []; |
| for (const anchor of anchors) { |
| const li = document.createElement('li'); |
| const link = document.createElement('a'); |
| link.innerText = anchor.parentElement.innerText; |
| link.href = anchor.href; |
| link.onclick = () => { |
| onScroll(link) |
| }; |
| li.appendChild(link); |
| if (anchor.parentElement.tagName === 'H3') |
| li.style.paddingLeft = '10px'; |
| toc.appendChild(li); |
| doAfterLoadEvent(() => { |
| tocAnchors.push( |
| { top: anchor.offsetTop + anchor.offsetHeight / 2, obj: link }); |
| }); |
| } |
| tocContainer.innerHTML = ''; |
| tocContainer.appendChild(toc); |
| |
| // Add event handlers on the first call (can be called more than once to |
| // recompute anchors on resize). |
| if (tocEventHandlersInstalled) |
| return; |
| tocEventHandlersInstalled = true; |
| const doc = document.querySelector('.doc'); |
| const passive = { passive: true }; |
| if (doc) { |
| const offY = doc.offsetTop; |
| doc.addEventListener('mousemove', (e) => onMouseMove(offY, e), passive); |
| doc.addEventListener('mouseleave', () => { |
| lastMouseOffY = 0; |
| }, passive); |
| } |
| window.addEventListener('scroll', () => onScroll(), passive); |
| resizeObserver = new ResizeObserver(() => requestAnimationFrame(() => { |
| updateNav(); |
| updateTOC(); |
| })); |
| resizeObserver.observe(doc); |
| } |
| |
| // Highlights the current TOC anchor depending on the scroll offset. |
| function onMouseMove(offY, e) { |
| lastMouseOffY = e.clientY - offY; |
| onScroll(); |
| } |
| |
| function onScroll(forceHighlight) { |
| const y = document.documentElement.scrollTop + lastMouseOffY; |
| let highEl = undefined; |
| for (const x of tocAnchors) { |
| if (y < x.top) |
| continue; |
| highEl = x.obj; |
| } |
| for (const link of document.querySelectorAll('.docs .toc a')) { |
| if ((!forceHighlight && link === highEl) || (forceHighlight === link)) { |
| link.classList.add('highlighted'); |
| } else { |
| link.classList.remove('highlighted'); |
| } |
| } |
| } |
| |
| // This function needs to be idempotent as it is called more than once (on every |
| // resize). |
| function updateNav() { |
| const curDoc = document.querySelector('.doc'); |
| let curFileName = ''; |
| if (curDoc) |
| curFileName = curDoc.dataset['mdFile']; |
| |
| // First identify all the top-level nav entries (Quickstart, Data Sources, |
| // ...) and make them compressible. |
| const toplevelSections = document.querySelectorAll('.docs .nav > ul > li'); |
| const toplevelLinks = []; |
| for (const sec of toplevelSections) { |
| const childMenu = sec.querySelector('ul'); |
| if (!childMenu) { |
| // Don't make it compressible if it has no children (e.g. the very |
| // first 'Introduction' link). |
| continue; |
| } |
| |
| // Don't make it compressible if the entry has an actual link (e.g. the very |
| // first 'Introduction' link), because otherwise it become ambiguous whether |
| // the link should toggle or open the link. |
| const link = sec.querySelector('a'); |
| if (!link || !link.href.endsWith('#')) |
| continue; |
| |
| sec.classList.add('compressible'); |
| |
| // Remember the compressed status as long as the page is opened, so clicking |
| // through links keeps the sidebar in a consistent visual state. |
| const memoKey = `docs.nav.compressed[${link.innerHTML}]`; |
| |
| if (sessionStorage.getItem(memoKey) === '1') { |
| sec.classList.add('compressed'); |
| } |
| doAfterLoadEvent(() => { |
| childMenu.style.maxHeight = `${childMenu.scrollHeight + 40}px`; |
| }); |
| |
| toplevelLinks.push(link); |
| link.onclick = (evt) => { |
| evt.preventDefault(); |
| sec.classList.toggle('compressed'); |
| if (sec.classList.contains('compressed')) { |
| sessionStorage.setItem(memoKey, '1'); |
| } else { |
| sessionStorage.removeItem(memoKey); |
| } |
| }; |
| } |
| |
| const exps = document.querySelectorAll('.docs .nav ul a'); |
| let found = false; |
| for (const x of exps) { |
| // If the url of the entry matches the url of the page, mark the item as |
| // highlighted and expand all its parents. |
| if (!x.href) |
| continue; |
| const url = new URL(x.href); |
| if (x.href.endsWith('#')) { |
| // This is a non-leaf link to a menu. |
| if (toplevelLinks.indexOf(x) < 0) { |
| x.removeAttribute('href'); |
| } |
| } else if (url.pathname === curFileName && !found) { |
| x.classList.add('selected'); |
| doAfterLoadEvent(() => x.scrollIntoViewIfNeeded()); |
| found = true; // Highlight only the first occurrence. |
| } |
| } |
| } |
| |
| // If the page contains a ```mermaid ``` block, lazily loads the plugin and |
| // renders. |
| function initMermaid() { |
| const graphs = document.querySelectorAll('.mermaid'); |
| |
| // Skip if there are no mermaid graphs to render. |
| if (!graphs.length) |
| return; |
| |
| const script = document.createElement('script'); |
| script.type = 'text/javascript'; |
| script.src = '/assets/mermaid.min.js'; |
| const themeCSS = ` |
| .cluster rect { fill: #FCFCFC; stroke: #ddd } |
| .node rect { fill: #DCEDC8; stroke: #8BC34A} |
| .edgeLabel:not(:empty) { |
| border-radius: 6px; |
| font-size: 0.9em; |
| padding: 4px; |
| background: #F5F5F5; |
| border: 1px solid #DDDDDD; |
| color: #666; |
| } |
| `; |
| script.addEventListener('load', () => { |
| mermaid.initialize({ |
| startOnLoad: false, |
| themeCSS: themeCSS, |
| securityLevel: 'loose', // To allow #in-page-links |
| }); |
| for (const graph of graphs) { |
| requestAnimationFrame(() => { |
| mermaid.init(undefined, graph); |
| graph.classList.add('rendered'); |
| }); |
| } |
| }) |
| document.body.appendChild(script); |
| } |
| |
| function setupSearch() { |
| const URL = |
| 'https://www.googleapis.com/customsearch/v1?key=AIzaSyBTD2XJkQkkuvDn76LSftsgWOkdBz9Gfwo&cx=007128963598137843411:8suis14kcmy&q=' |
| const searchContainer = document.getElementById('search'); |
| const searchBox = document.getElementById('search-box'); |
| const searchRes = document.getElementById('search-res') |
| if (!searchBox || !searchRes) return; |
| |
| document.body.addEventListener('keydown', (e) => { |
| if (e.key === '/' && e.target.tagName.toLowerCase() === 'body') { |
| searchBox.setSelectionRange(0, -1); |
| searchBox.focus(); |
| e.preventDefault(); |
| } else if (e.key === 'Escape' && searchContainer.contains(e.target)) { |
| searchBox.blur(); |
| |
| // Handle the case of clicking Tab and moving down to results. |
| e.target.blur(); |
| } |
| }); |
| |
| let timerId = -1; |
| let lastSearchId = 0; |
| |
| const doSearch = async () => { |
| timerId = -1; |
| searchRes.style.width = `${searchBox.offsetWidth}px`; |
| |
| // `searchId` handles the case of two subsequent requests racing. This is to |
| // prevent older results, delivered in reverse order, to replace newer ones. |
| const searchId = ++lastSearchId; |
| const f = await fetch(URL + encodeURIComponent(searchBox.value)); |
| const jsonRes = await f.json(); |
| const results = jsonRes['items']; |
| searchRes.innerHTML = ''; |
| if (results === undefined || searchId != lastSearchId) { |
| return; |
| } |
| for (const res of results) { |
| const link = document.createElement('a'); |
| link.href = res.link; |
| const title = document.createElement('div'); |
| title.className = 'sr-title'; |
| title.innerText = res.title.replace(' - Perfetto Tracing Docs', ''); |
| link.appendChild(title); |
| |
| const snippet = document.createElement('div'); |
| snippet.className = 'sr-snippet'; |
| snippet.innerText = res.snippet; |
| link.appendChild(snippet); |
| |
| const div = document.createElement('div'); |
| div.appendChild(link); |
| searchRes.appendChild(div); |
| } |
| }; |
| |
| searchBox.addEventListener('keyup', () => { |
| if (timerId >= 0) return; |
| timerId = setTimeout(doSearch, 200); |
| }); |
| } |
| |
| window.addEventListener('DOMContentLoaded', () => { |
| updateNav(); |
| updateTOC(); |
| }); |
| |
| window.addEventListener('load', () => { |
| setupSandwichMenu(); |
| initMermaid(); |
| |
| // Don't smooth-scroll on pages that are too long (e.g. reference pages). |
| if (document.body.scrollHeight < 10000) { |
| document.documentElement.style.scrollBehavior = 'smooth'; |
| } else { |
| document.documentElement.style.scrollBehavior = 'initial'; |
| } |
| |
| onloadFired = true; |
| while (postLoadActions.length > 0) { |
| postLoadActions.shift()(); |
| } |
| |
| updateTOC(); |
| setupSearch(); |
| |
| // Enable animations only after the load event. This is to prevent glitches |
| // when switching pages. |
| document.documentElement.style.setProperty('--anim-enabled', '1') |
| }); |
| |
| // Handles redirects from the old docs.perfetto.dev. |
| const legacyRedirectMap = { |
| '#/contributing': '/docs/contributing/getting-started#community', |
| '#/build-instructions': '/docs/contributing/build-instructions', |
| '#/testing': '/docs/contributing/testing', |
| '#/app-instrumentation': '/docs/instrumentation/tracing-sdk', |
| '#/recording-traces': '/docs/instrumentation/tracing-sdk#recording', |
| '#/running': '/docs/quickstart/android-tracing', |
| '#/long-traces': '/docs/concepts/config#long-traces', |
| '#/detached-mode': '/docs/concepts/detached-mode', |
| '#/heapprofd': '/docs/data-sources/native-heap-profiler', |
| '#/java-hprof': '/docs/data-sources/java-heap-profiler', |
| '#/trace-processor': '/docs/analysis/trace-processor', |
| '#/analysis': '/docs/analysis/trace-processor#annotations', |
| '#/metrics': '/docs/analysis/metrics', |
| '#/traceconv': '/docs/quickstart/traceconv', |
| '#/clock-sync': '/docs/concepts/clock-sync', |
| '#/architecture': '/docs/concepts/service-model', |
| }; |
| |
| const fragment = location.hash.split('?')[0].replace('.md', ''); |
| if (fragment in legacyRedirectMap) { |
| location.replace(legacyRedirectMap[fragment]); |
| } |
| |
| // Pages which have been been removed/renamed/moved and need to be redirected |
| // to their new home. |
| const redirectMap = { |
| // stdlib docs is not a perfect replacement but is good enough until we write |
| // a proper, Android specific query codelab page. |
| // TODO(lalitm): switch to that page when it's ready. |
| '/docs/analysis/common-queries': '/docs/analysis/stdlib-docs', |
| }; |
| |
| if (location.pathname in redirectMap) { |
| location.replace(redirectMap[location.pathname]); |
| } |