xref: /aosp_15_r20/external/perfetto/infra/perfetto.dev/src/assets/script.js (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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