| // 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]); |
| } |