1*61c4878aSAndroid Build Coastguard Worker// Copyright 2023 The Pigweed Authors 2*61c4878aSAndroid Build Coastguard Worker// 3*61c4878aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4*61c4878aSAndroid Build Coastguard Worker// use this file except in compliance with the License. You may obtain a copy of 5*61c4878aSAndroid Build Coastguard Worker// the License at 6*61c4878aSAndroid Build Coastguard Worker// 7*61c4878aSAndroid Build Coastguard Worker// https://www.apache.org/licenses/LICENSE-2.0 8*61c4878aSAndroid Build Coastguard Worker// 9*61c4878aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software 10*61c4878aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11*61c4878aSAndroid Build Coastguard Worker// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12*61c4878aSAndroid Build Coastguard Worker// License for the specific language governing permissions and limitations under 13*61c4878aSAndroid Build Coastguard Worker// the License. 14*61c4878aSAndroid Build Coastguard Worker 15*61c4878aSAndroid Build Coastguard Workerwindow.pw = {}; 16*61c4878aSAndroid Build Coastguard Worker 17*61c4878aSAndroid Build Coastguard Worker// Display inline search results under the search modal. After the user types 18*61c4878aSAndroid Build Coastguard Worker// text in the search box, results are shown underneath the text input box. 19*61c4878aSAndroid Build Coastguard Worker// The search is restarted after each keypress. 20*61c4878aSAndroid Build Coastguard Worker// 21*61c4878aSAndroid Build Coastguard Worker// TODO: b/363034219 - Try to upstream this code into pydata-sphinx-theme. 22*61c4878aSAndroid Build Coastguard Workerwindow.pw.initSearch = () => { 23*61c4878aSAndroid Build Coastguard Worker // Don't interfere with the default search UX on /search.html. 24*61c4878aSAndroid Build Coastguard Worker if (window.location.pathname.endsWith('/search.html')) { 25*61c4878aSAndroid Build Coastguard Worker return; 26*61c4878aSAndroid Build Coastguard Worker } 27*61c4878aSAndroid Build Coastguard Worker // The template //docs/layout/page.html ensures that Search is always 28*61c4878aSAndroid Build Coastguard Worker // loaded at this point. 29*61c4878aSAndroid Build Coastguard Worker // eslint-disable-next-line no-undef 30*61c4878aSAndroid Build Coastguard Worker if (!Search) { 31*61c4878aSAndroid Build Coastguard Worker return; 32*61c4878aSAndroid Build Coastguard Worker } 33*61c4878aSAndroid Build Coastguard Worker // Destroy the previous search container and create a new one. 34*61c4878aSAndroid Build Coastguard Worker window.pw.resetSearchResults(); 35*61c4878aSAndroid Build Coastguard Worker let timeoutId = null; 36*61c4878aSAndroid Build Coastguard Worker let lastQuery = ''; 37*61c4878aSAndroid Build Coastguard Worker const searchInput = document.querySelector('#search-input'); 38*61c4878aSAndroid Build Coastguard Worker // Set up the event handler to initiate searches whenever the user 39*61c4878aSAndroid Build Coastguard Worker // types stuff in the search modal textbox. 40*61c4878aSAndroid Build Coastguard Worker searchInput.addEventListener('keyup', () => { 41*61c4878aSAndroid Build Coastguard Worker const query = searchInput.value; 42*61c4878aSAndroid Build Coastguard Worker // Don't search when there's nothing in the query textbox. 43*61c4878aSAndroid Build Coastguard Worker if (query === '') { 44*61c4878aSAndroid Build Coastguard Worker return; 45*61c4878aSAndroid Build Coastguard Worker } 46*61c4878aSAndroid Build Coastguard Worker // Don't search if there is no detectable change between 47*61c4878aSAndroid Build Coastguard Worker // the last query and the current query. E.g. user presses 48*61c4878aSAndroid Build Coastguard Worker // Tab to start navigating the search results. 49*61c4878aSAndroid Build Coastguard Worker if (query === lastQuery) { 50*61c4878aSAndroid Build Coastguard Worker return; 51*61c4878aSAndroid Build Coastguard Worker } 52*61c4878aSAndroid Build Coastguard Worker // The user has changed the search query. Delete the old results 53*61c4878aSAndroid Build Coastguard Worker // and start setting up the new container. 54*61c4878aSAndroid Build Coastguard Worker window.pw.resetSearchResults(); 55*61c4878aSAndroid Build Coastguard Worker // Debounce so that the search only starts only when the 56*61c4878aSAndroid Build Coastguard Worker // user stops typing. 57*61c4878aSAndroid Build Coastguard Worker const delay_ms = 500; 58*61c4878aSAndroid Build Coastguard Worker lastQuery = query; 59*61c4878aSAndroid Build Coastguard Worker if (timeoutId) { 60*61c4878aSAndroid Build Coastguard Worker window.clearTimeout(timeoutId); 61*61c4878aSAndroid Build Coastguard Worker } 62*61c4878aSAndroid Build Coastguard Worker timeoutId = window.setTimeout(() => { 63*61c4878aSAndroid Build Coastguard Worker // eslint-disable-next-line no-undef 64*61c4878aSAndroid Build Coastguard Worker Search.performSearch(query); 65*61c4878aSAndroid Build Coastguard Worker timeoutId = null; 66*61c4878aSAndroid Build Coastguard Worker }, delay_ms); 67*61c4878aSAndroid Build Coastguard Worker }); 68*61c4878aSAndroid Build Coastguard Worker}; 69*61c4878aSAndroid Build Coastguard Worker 70*61c4878aSAndroid Build Coastguard Worker// Deletes the old custom search results container and recreates a 71*61c4878aSAndroid Build Coastguard Worker// new, empty one. 72*61c4878aSAndroid Build Coastguard Worker// 73*61c4878aSAndroid Build Coastguard Worker// Note that Sphinx assumes that searches are always made from /search.html 74*61c4878aSAndroid Build Coastguard Worker// so there's some safeguard logic to make sure the inline search always 75*61c4878aSAndroid Build Coastguard Worker// works no matter what pigweed.dev page you're on. b/365179592 76*61c4878aSAndroid Build Coastguard Worker// 77*61c4878aSAndroid Build Coastguard Worker// TODO: b/363034219 - Try to upstream this code into pydata-sphinx-theme. 78*61c4878aSAndroid Build Coastguard Workerwindow.pw.resetSearchResults = () => { 79*61c4878aSAndroid Build Coastguard Worker let results = document.querySelector('#search-results'); 80*61c4878aSAndroid Build Coastguard Worker if (results) { 81*61c4878aSAndroid Build Coastguard Worker results.remove(); 82*61c4878aSAndroid Build Coastguard Worker } 83*61c4878aSAndroid Build Coastguard Worker results = document.createElement('section'); 84*61c4878aSAndroid Build Coastguard Worker results.classList.add('pw-search-results'); 85*61c4878aSAndroid Build Coastguard Worker results.id = 'search-results'; 86*61c4878aSAndroid Build Coastguard Worker // Add the new results container to the DOM. 87*61c4878aSAndroid Build Coastguard Worker let modal = document.querySelector('.search-button__search-container'); 88*61c4878aSAndroid Build Coastguard Worker modal.appendChild(results); 89*61c4878aSAndroid Build Coastguard Worker // Relative path to the root directory of the site. Sphinx's 90*61c4878aSAndroid Build Coastguard Worker // HTML builder auto-inserts this metadata on every page. 91*61c4878aSAndroid Build Coastguard Worker const root = document.documentElement.dataset.content_root; 92*61c4878aSAndroid Build Coastguard Worker // As Sphinx populates the search results, this observer makes sure that 93*61c4878aSAndroid Build Coastguard Worker // each URL is correct (i.e. doesn't 404). b/365179592 94*61c4878aSAndroid Build Coastguard Worker const linkObserver = new MutationObserver(() => { 95*61c4878aSAndroid Build Coastguard Worker const links = Array.from( 96*61c4878aSAndroid Build Coastguard Worker document.querySelectorAll('#search-results .search a'), 97*61c4878aSAndroid Build Coastguard Worker ); 98*61c4878aSAndroid Build Coastguard Worker // Check every link every time because the timing of when new results are 99*61c4878aSAndroid Build Coastguard Worker // added is unpredictable and it's not an expensive operation. 100*61c4878aSAndroid Build Coastguard Worker links.forEach((link) => { 101*61c4878aSAndroid Build Coastguard Worker // Don't use the link.href getter because the browser computes the href 102*61c4878aSAndroid Build Coastguard Worker // as a full URL. We need the relative URL that Sphinx generates. 103*61c4878aSAndroid Build Coastguard Worker const href = link.getAttribute('href'); 104*61c4878aSAndroid Build Coastguard Worker if (href.startsWith(root)) { 105*61c4878aSAndroid Build Coastguard Worker // No work needed. The root has already been prepended to the href. 106*61c4878aSAndroid Build Coastguard Worker return; 107*61c4878aSAndroid Build Coastguard Worker } 108*61c4878aSAndroid Build Coastguard Worker link.href = `${root}${href}`; 109*61c4878aSAndroid Build Coastguard Worker }); 110*61c4878aSAndroid Build Coastguard Worker }); 111*61c4878aSAndroid Build Coastguard Worker // The node that linkObserver watches doesn't exist until the user types 112*61c4878aSAndroid Build Coastguard Worker // something into the search textbox. resultsObserver just waits for 113*61c4878aSAndroid Build Coastguard Worker // that container to exist and then registers linkObserver on it. 114*61c4878aSAndroid Build Coastguard Worker let isObserved = false; 115*61c4878aSAndroid Build Coastguard Worker const resultsObserver = new MutationObserver(() => { 116*61c4878aSAndroid Build Coastguard Worker if (isObserved) { 117*61c4878aSAndroid Build Coastguard Worker return; 118*61c4878aSAndroid Build Coastguard Worker } 119*61c4878aSAndroid Build Coastguard Worker const container = document.querySelector('#search-results .search'); 120*61c4878aSAndroid Build Coastguard Worker if (!container) { 121*61c4878aSAndroid Build Coastguard Worker return; 122*61c4878aSAndroid Build Coastguard Worker } 123*61c4878aSAndroid Build Coastguard Worker linkObserver.observe(container, { childList: true }); 124*61c4878aSAndroid Build Coastguard Worker isObserved = true; 125*61c4878aSAndroid Build Coastguard Worker }); 126*61c4878aSAndroid Build Coastguard Worker resultsObserver.observe(results, { childList: true }); 127*61c4878aSAndroid Build Coastguard Worker}; 128*61c4878aSAndroid Build Coastguard Worker 129*61c4878aSAndroid Build Coastguard Workerwindow.addEventListener('DOMContentLoaded', () => { 130*61c4878aSAndroid Build Coastguard Worker // Manually control when Mermaid diagrams render to prevent scrolling issues. 131*61c4878aSAndroid Build Coastguard Worker // Context: https://pigweed.dev/docs/style_guide.html#site-nav-scrolling 132*61c4878aSAndroid Build Coastguard Worker if (window.mermaid) { 133*61c4878aSAndroid Build Coastguard Worker // https://mermaid.js.org/config/usage.html#using-mermaid-run 134*61c4878aSAndroid Build Coastguard Worker window.mermaid.run(); 135*61c4878aSAndroid Build Coastguard Worker } 136*61c4878aSAndroid Build Coastguard Worker window.pw.initSearch(); 137*61c4878aSAndroid Build Coastguard Worker}); 138