bookshelf-doc/static/search.js

165 lines
4.9 KiB
JavaScript

/**
* This module is used to handle user's interaction with the search form.
*/
import { DeclarationDataCenter } from "./declaration-data.js";
// Search form and input in the upper right toolbar
const SEARCH_FORM = document.querySelector("#search_form");
const SEARCH_INPUT = SEARCH_FORM.querySelector("input[name=q]");
// Search form on the /search.html_page. These may be null.
const SEARCH_PAGE_INPUT = document.querySelector("#search_page_query")
const SEARCH_RESULTS = document.querySelector("#search_results")
// Max results to show for autocomplete or /search.html page.
const AC_MAX_RESULTS = 30
const SEARCH_PAGE_MAX_RESULTS = undefined
// Create an `div#autocomplete_results` to hold all autocomplete results.
let ac_results = document.createElement("div");
ac_results.id = "autocomplete_results";
SEARCH_FORM.appendChild(ac_results);
/**
* Attach `selected` class to the the selected autocomplete result.
*/
function handleSearchCursorUpDown(down) {
const sel = ac_results.querySelector(`.selected`);
if (sel) {
sel.classList.remove("selected");
const toSelect = down
? sel.nextSibling
: sel.previousSibling;
toSelect && toSelect.classList.add("selected");
} else {
const toSelect = down ? ac_results.firstChild : ac_results.lastChild;
toSelect && toSelect.classList.add("selected");
}
}
/**
* Perform search (when enter is pressed).
*/
function handleSearchEnter() {
const sel = ac_results.querySelector(`.selected .result_link a`) || document.querySelector(`#search_button`);
sel.click();
}
/**
* Allow user to navigate autocomplete results with up/down arrow keys, and choose with enter.
*/
SEARCH_INPUT.addEventListener("keydown", (ev) => {
switch (ev.key) {
case "Down":
case "ArrowDown":
ev.preventDefault();
handleSearchCursorUpDown(true);
break;
case "Up":
case "ArrowUp":
ev.preventDefault();
handleSearchCursorUpDown(false);
break;
case "Enter":
ev.preventDefault();
handleSearchEnter();
break;
}
});
/**
* Remove all children of a DOM node.
*/
function removeAllChildren(node) {
while (node.firstChild) {
node.removeChild(node.lastChild);
}
}
/**
* Handle user input and perform search.
*/
function handleSearch(dataCenter, err, ev, sr, maxResults, autocomplete) {
const text = ev.target.value;
// If no input clear all.
if (!text) {
sr.removeAttribute("state");
removeAllChildren(sr);
return;
}
// searching
sr.setAttribute("state", "loading");
if (dataCenter) {
var allowedKinds;
if (!autocomplete) {
allowedKinds = new Set();
document.querySelectorAll(".kind_checkbox").forEach((checkbox) =>
{
if (checkbox.checked) {
allowedKinds.add(checkbox.value);
}
}
);
}
const result = dataCenter.search(text, false, allowedKinds, maxResults);
// in case user has updated the input.
if (ev.target.value != text) return;
// update autocomplete results
removeAllChildren(sr);
for (const { name, kind, docLink } of result) {
const row = sr.appendChild(document.createElement("div"));
row.classList.add("search_result")
const linkdiv = row.appendChild(document.createElement("div"))
linkdiv.classList.add("result_link")
const link = linkdiv.appendChild(document.createElement("a"));
link.innerText = name;
link.title = name;
link.href = SITE_ROOT + docLink;
}
}
// handle error
else {
removeAllChildren(sr);
const d = sr.appendChild(document.createElement("a"));
d.innerText = `Cannot fetch data, please check your network connection.\n${err}`;
}
sr.setAttribute("state", "done");
}
// https://www.joshwcomeau.com/snippets/javascript/debounce/
const debounce = (callback, wait) => {
let timeoutId = null;
return (...args) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
callback.apply(null, args);
}, wait);
};
}
DeclarationDataCenter.init()
.then((dataCenter) => {
// Search autocompletion.
SEARCH_INPUT.addEventListener("input", debounce(ev => handleSearch(dataCenter, null, ev, ac_results, AC_MAX_RESULTS, true), 300));
if(SEARCH_PAGE_INPUT) {
SEARCH_PAGE_INPUT.addEventListener("input", ev => handleSearch(dataCenter, null, ev, SEARCH_RESULTS, SEARCH_PAGE_MAX_RESULTS, false))
document.querySelectorAll(".kind_checkbox").forEach((checkbox) =>
checkbox.addEventListener("input", ev => SEARCH_PAGE_INPUT.dispatchEvent(new Event("input")))
);
SEARCH_PAGE_INPUT.dispatchEvent(new Event("input"))
};
SEARCH_INPUT.dispatchEvent(new Event("input"))
})
.catch(e => {
SEARCH_INPUT.addEventListener("input", debounce(ev => handleSearch(null, e, ev, ac_results, AC_MAX_RESULTS, true), 300));
if(SEARCH_PAGE_INPUT) {
SEARCH_PAGE_INPUT.addEventListener("input", ev => handleSearch(null, e, ev, SEARCH_RESULTS, SEARCH_PAGE_MAX_RESULTS, false));
}
});