public/assets/js/search.js (133 lines of code) (raw):
import lunr from "lunr";
const searchButton = document.getElementById("search");
const searchDropdown = document.getElementById("search-dropdown");
const searchInput = document.getElementById("searchbox-input");
const searchResults = document.getElementById("search-results");
let lunrIndex;
let documents;
if (searchButton) {
document.addEventListener("click", function (event) {
// If user clicks inside the element, do nothing
if (event.target.closest(`#${searchDropdown.id}`)) return;
// If user clicks outside the element, hide it!
searchDropdown.classList.remove("is-active");
});
let shiftKeyPressed = false;
let focusedResultIndex = -1;
document.addEventListener("keydown", async (e) => {
// Handle Escape
if (e.key === 'Escape' && searchDropdown) {
searchDropdown.classList.remove("is-active");
}
// Handle double-shift
if (e.key === 'Shift' && !searchDropdown.classList.contains("is-active")) {
if (shiftKeyPressed) {
searchButton.dispatchEvent(new CustomEvent('click'));
searchButton.scrollIntoView();
shiftKeyPressed = false;
} else {
shiftKeyPressed = true;
setTimeout(() => shiftKeyPressed = false, 400);
}
}
// Select item with down/up key
if (searchDropdown.classList.contains("is-active") && searchResults) {
const resultElements = searchResults.getElementsByTagName("a");
if (e.key === 'ArrowDown' && resultElements.length > 0 && focusedResultIndex < resultElements.length - 1) { // down
resultElements[++focusedResultIndex].focus();
resultElements[focusedResultIndex].classList.add('has-background-info-light');
if (focusedResultIndex > 0) {
resultElements[focusedResultIndex - 1].classList.remove('has-background-info-light');
}
e.preventDefault();
} else if (e.key === 'ArrowUp' && resultElements.length > 0 && focusedResultIndex >= 0) { // up
if (focusedResultIndex <= 0) {
resultElements[0].classList.remove('has-background-info-light');
searchResults.getElementsByTagName("div")[0].scrollIntoView();
searchInput.focus();
searchInput.select();
focusedResultIndex = -1;
// move caret to end
if (searchInput.setSelectionRange) {
setTimeout(() => searchInput.setSelectionRange(searchInput.value.length, searchInput.value.length), 50);
}
} else {
resultElements[--focusedResultIndex].focus();
resultElements[focusedResultIndex].classList.add('has-background-info-light');
if (focusedResultIndex < resultElements.length - 1) {
resultElements[focusedResultIndex + 1].classList.remove('has-background-info-light');
}
}
e.preventDefault();
}
}
});
searchButton.addEventListener("click", async () => {
if (searchDropdown) {
searchDropdown.classList.toggle("is-active");
if (searchDropdown.classList.contains("is-active")) {
searchInput.focus();
focusedResultIndex = -1;
}
}
if (documents === undefined) {
const jsonUrl = new URL("/lunr.json", import.meta.url).href;
const response = await fetch(jsonUrl);
const json = await response.json();
documents = json.results;
console.log("Loaded " + documents.length + " document(s).");
// load lunr search index
lunrIndex = lunr(function () {
this.ref("url");
this.field("title", { boost: 1.3 });
this.field("subtitle", { boost: 1.2 });
this.field("resourceType", { boost: 0.1 });
this.field("channelTitle", { boost: 0.5 });
documents.forEach(function (doc) {
let documentBoost = 1;
if (!doc.channel) documentBoost = 1.5;
if (doc.resourceType === "tip") documentBoost = 2.0;
if (doc.resourceType === "tutorial") documentBoost = 1.8;
if (doc.resourceType === "topic") documentBoost = 1.6;
if (doc.resourceType === "article") documentBoost = 1.5;
if (doc.resourceType === "link") documentBoost = 1.3;
if (doc.resourceType === "tutorialstep") documentBoost = 1.1;
this.add(doc, { boost: documentBoost });
}, this);
});
}
});
}
if (searchInput) {
searchInput.addEventListener("keyup", () => {
let query = searchInput.value;
searchResults.innerHTML = "";
if (query !== '') {
searchResults.innerHTML = findSearchResults(query);
}
});
}
function findSearchResults(query) {
const limit = 250;
let results = lunrIndex.search(query).map(function (result) {
return documents.find(function (page) {
return page.url === result.ref;
});
});
if (results.length === 0 && query.indexOf("*") < 0) {
return findSearchResults(query + "*");
}
const description =
results.length === 0
? "<p>No results have been found</p>"
: `<p>Showing ${Math.min(limit, results.length)} results</p>
`;
const list = results.slice(0, limit).map((result) => {
let tags = [];
if (result.channelTitle) {
tags.push(`<span class="tag is-small is-light">${result.channelTitle}</span>`);
} else {
tags.push(`<span class="tag is-small is-light">All</span>`);
}
//if (result.resourceType) tags.push(`<span class="tag is-small is-light">${getContentType(result.resourceType, undefined)}</span>`);
return `
<a class="panel-block is-block is-active" href="${result.url}">
<div class="has-text-left is-fullwidth">
<b>${result.title}</b>
${tags.length ? `<div class="is-pulled-right is-hidden-touch">${tags.join("<span> </span>")}</div>`: ""}<br/>
${result.subtitle ? `<small class="is-small">${result.subtitle}</small>` : ''}
</div> </a>`;
});
return `<div class="panel-block">${description}</div>\n${list.join("")}`;
}
if (!Element.prototype.matches)
Element.prototype.matches = Element.prototype.msMatchesSelector;
if (!Element.prototype.closest)
Element.prototype.closest = function (selector) {
let el = this;
while (el) {
if (el.matches(selector)) {
return el;
}
el = el.parentElement;
}
};