-
Notifications
You must be signed in to change notification settings - Fork 327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add search-as-you-type (inline search results) feature #2093
base: main
Are you sure you want to change the base?
Changes from 17 commits
45b6447
1a1112b
aee0d92
ead8b4e
37143ed
bdf3385
e531e70
c9b5daa
1f522ed
1db1290
710b383
66c93ca
00bb0af
5b06319
c203489
8f3de69
f40c6ff
5132816
19bab1c
4388a76
07d4b62
49025fa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -332,6 +332,157 @@ var setupSearchButtons = () => { | |
searchDialog.addEventListener("click", closeDialogOnBackdropClick); | ||
}; | ||
|
||
/******************************************************************************* | ||
* Inline search results (search-as-you-type) | ||
* | ||
* Immediately displays search results under the search query textbox. | ||
* | ||
* The search is conducted by Sphinx's built-in search tools (searchtools.js). | ||
* Usually searchtools.js is only available on /search.html but | ||
* pydata-sphinx-theme (PST) has been modified to load searchtools.js on every | ||
* page. After the user types something into PST's search query textbox, | ||
* searchtools.js executes the search and populates the results into | ||
* the #search-results container. searchtools.js expects the results container | ||
* to have that exact ID. | ||
*/ | ||
var setupSearchAsYouType = () => { | ||
if (!DOCUMENTATION_OPTIONS.search_as_you_type) { | ||
return; | ||
} | ||
|
||
// Don't interfere with the default search UX on /search.html. | ||
if (window.location.pathname.endsWith("/search.html")) { | ||
return; | ||
} | ||
|
||
// Bail if the Search class is not available. Search-as-you-type is | ||
// impossible without that class. layout.html should ensure that | ||
// searchtools.js loads. | ||
// | ||
// Search class is defined in upstream Sphinx: | ||
// https://github.com/sphinx-doc/sphinx/blob/master/sphinx/themes/basic/static/searchtools.js#L181 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. link to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SGTM There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed now, will let you resolve |
||
if (!Search) { | ||
return; | ||
} | ||
|
||
// Destroy the previous search container and create a new one. | ||
resetSearchAsYouTypeResults(); | ||
let timeoutId = null; | ||
let lastQuery = ""; | ||
const searchInput = document.querySelector( | ||
"#pst-search-dialog input[name=q]", | ||
); | ||
|
||
// Initiate searches whenever the user types stuff in the search modal textbox. | ||
searchInput.addEventListener("keyup", () => { | ||
const query = searchInput.value; | ||
|
||
// Don't search when there's nothing in the query textbox. | ||
if (query === "") { | ||
resetSearchAsYouTypeResults(); // Remove previous results. | ||
return; | ||
} | ||
|
||
// Don't search if there is no detectable change between | ||
// the last query and the current query. E.g. the user presses | ||
// Tab to start navigating the search results. | ||
if (query === lastQuery) { | ||
return; | ||
} | ||
|
||
// The user has changed the search query. Delete the old results | ||
// and start setting up the new container. | ||
resetSearchAsYouTypeResults(); | ||
|
||
// Debounce so that the search only starts when the user stops typing. | ||
const delay_ms = 300; | ||
lastQuery = query; | ||
if (timeoutId) { | ||
window.clearTimeout(timeoutId); | ||
} | ||
timeoutId = window.setTimeout(() => { | ||
Search.performSearch(query); | ||
document.querySelector("#search-results").classList.remove("empty"); | ||
timeoutId = null; | ||
}, delay_ms); | ||
}); | ||
}; | ||
|
||
// Delete the old search results container (if it exists) and set up a new one. | ||
// | ||
// There is some complexity around ensuring that the search results links are | ||
// correct because we're extending searchtools.js past its assumed usage. | ||
// Sphinx assumes that searches are only executed from /search.html and | ||
// therefore it assumes that all search results links should be relative to | ||
// the root directory of the website. In our case the search can now execute | ||
// from any page of the website so we must fix the relative URLs that | ||
// searchtools.js generates. | ||
var resetSearchAsYouTypeResults = () => { | ||
if (!DOCUMENTATION_OPTIONS.search_as_you_type) { | ||
return; | ||
} | ||
// If a search-as-you-type results container was previously added, | ||
// remove it now. | ||
let results = document.querySelector("#search-results"); | ||
if (results) { | ||
results.remove(); | ||
} | ||
|
||
// Create a new search-as-you-type results container. | ||
results = document.createElement("section"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gabalafou would appreciate your perspective on what is the best node type for an appearing/disappearing list of search results, and how this can/should/will interact with things like tab focus. |
||
results.classList.add("search-results"); | ||
results.classList.add("empty"); | ||
// IMPORTANT: The search results container MUST have this exact ID. | ||
// searchtools.js is hardcoded to populate into the node with this ID. | ||
results.id = "search-results"; | ||
let modal = document.querySelector("#pst-search-dialog"); | ||
modal.appendChild(results); | ||
|
||
// Get the relative path back to the root of the website. | ||
const root = | ||
"URL_ROOT" in DOCUMENTATION_OPTIONS | ||
? DOCUMENTATION_OPTIONS.URL_ROOT // Sphinx v6 and earlier | ||
: document.documentElement.dataset.content_root; // Sphinx v7 and later | ||
|
||
// As Sphinx populates the search results, this observer makes sure that | ||
// each URL is correct (i.e. doesn't 404). | ||
const linkObserver = new MutationObserver(() => { | ||
const links = Array.from( | ||
document.querySelectorAll("#search-results .search a"), | ||
); | ||
// Check every link every time because the timing of when new results are | ||
// added is unpredictable and it's not an expensive operation. | ||
links.forEach((link) => { | ||
// Don't use the link.href getter because the browser computes the href | ||
// as a full URL. We need the relative URL that Sphinx generates. | ||
const href = link.getAttribute("href"); | ||
if (href.startsWith(root)) { | ||
// No work needed. The root has already been prepended to the href. | ||
return; | ||
} | ||
link.href = `${root}${href}`; | ||
}); | ||
}); | ||
|
||
// The node that linkObserver watches doesn't exist until the user types | ||
// something into the search textbox. This second observer (resultsObserver) | ||
// just waits for #search-results to exist and then registers | ||
// linkObserver on it. | ||
let isObserved = false; | ||
const resultsObserver = new MutationObserver(() => { | ||
if (isObserved) { | ||
return; | ||
} | ||
const container = document.querySelector("#search-results .search"); | ||
if (!container) { | ||
return; | ||
} | ||
linkObserver.observe(container, { childList: true }); | ||
isObserved = true; | ||
}); | ||
resultsObserver.observe(results, { childList: true }); | ||
}; | ||
|
||
/******************************************************************************* | ||
* Version Switcher | ||
* Note that this depends on two variables existing that are defined in | ||
|
@@ -857,6 +1008,7 @@ documentReady(addModeListener); | |
documentReady(scrollToActive); | ||
documentReady(addTOCInteractivity); | ||
documentReady(setupSearchButtons); | ||
documentReady(setupSearchAsYouType); | ||
documentReady(setupMobileSidebarKeyboardHandlers); | ||
|
||
// Determining whether an element has scrollable content depends on stylesheets, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should also account for dirhtml builds, which I think (?) will have a url like
https://whatever.com/en/search/
or potentiallyhttps://whatever.com/en/search/index.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dirhtml
was not on my radar. Will need to look into what that does to figure out how it affects the impl.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I introduced a temporary change to
tox.ini
in this PR to makedocs-dev
builddirhtml
instead of html. It all seems to work fine still