surwiki/assets/search.js

140 lines
4.7 KiB
JavaScript

function debouncer(interval, callback) {
let currentTimeout = null;
function trigger() {
currentTimeout = null;
callback();
}
return function () {
clearTimeout(currentTimeout);
currentTimeout = setTimeout(trigger, interval);
};
}
(function () {
const form = document.querySelector('form.search');
const input = form.querySelector('input');
const results = form.querySelector('.live-results');
const resultPrototype = document.getElementById('search-result-prototype').firstChild;
function clearChildren(element) {
while (element.lastChild) {
element.removeChild(results.lastChild);
}
}
let ongoing = false;
function submit() {
if (input.value === "") {
results.classList.remove("show");
clearChildren(results);
return;
}
if (ongoing) return;
ongoing = true;
const query = input.value;
fetch(
"_search?snippet_size=10&limit=4&q=" + encodeURIComponent(query),
{
headers: {
"Accept": "application/json",
},
credentials: "same-origin",
}
).then(response => {
if (!response.ok) throw new Error("Unexpected status code (" + response.status + ")");
return response.json();
}).then(result => {
clearChildren(results);
result.hits.forEach((hit, index) => {
const item = resultPrototype.cloneNode(true);
item.querySelector('.link').href = hit.slug || ".";
item.querySelector('.link').setAttribute("data-focusindex", index + 1);
item.querySelector('.title').textContent = hit.title;
item.querySelector('.snippet').innerHTML = hit.snippet;
results.appendChild(item);
})
if (result.next) {
const item = resultPrototype.cloneNode(true);
item.querySelector('.link').href = "_search?q=" + encodeURIComponent(query);
item.querySelector('.link').setAttribute("data-focusindex", result.hits.length + 1);
item.querySelector('.link').innerHTML = "See more results\u2026";
results.appendChild(item);
}
results.classList.add("show");
if (input.value !== query) submitter();
ongoing = false;
}).catch(err => {
console.error(err);
clearChildren(results);
results.classList.add("show");
const item = document.createElement("li");
item.classList.add("search-result");
item.classList.add("error");
item.textContent = "Live search unavailable";
results.appendChild(item);
ongoing = false;
});
}
const submitter = debouncer(300, submit);
input.addEventListener('input', submitter);
form.addEventListener('focusin', () => form.classList.add("focus"));
form.addEventListener('focusout', function (ev) {
for (let ancestor = ev.relatedTarget; ancestor; ancestor = ancestor.parentElement) {
if (ancestor === form) return;
}
// We are now actually losing focus from the form:
form.classList.remove("focus");
});
function moveFocus(element, delta) {
const focusIndexText = document.activeElement.getAttribute("data-focusindex");
const nextIndex = focusIndexText ? parseInt(focusIndexText, 10) + delta : 0;
const candidate = element.querySelector("[data-focusindex=\"" + nextIndex + "\"]");
if (candidate) candidate.focus();
}
function focusControl(element, ev) {
if (ev.key === 'ArrowUp') {
ev.preventDefault();
ev.stopPropagation();
moveFocus(element, -1);
} else if (ev.key === 'ArrowDown') {
ev.preventDefault();
ev.stopPropagation();
moveFocus(element, 1);
} else if (ev.key === 'Escape') {
ev.preventDefault();
ev.stopPropagation();
document.activeElement && document.activeElement.blur();
}
}
for (let element of document.querySelectorAll(".keyboard-focus-control")) {
const captureElement = element;
element.addEventListener('keydown', ev => focusControl(captureElement, ev));
}
const defaultKeyboardFocusControl = document.querySelector(".default-keyboard-focus-control");
if (defaultKeyboardFocusControl) {
document.addEventListener('keydown', ev => {
focusControl(defaultKeyboardFocusControl, ev);
});
}
})();