frontend/static/search.js (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | const search = document.getElementById("search");
let results = document.getElementById("results");
let pagination = document.getElementById("pagination");
const range = new Range();
range.setStartAfter(search);
range.setEndAfter(search.parentNode.lastChild);
let urlLocation = new URL(location);
let state = history.state || {
url: urlLocation.toString(),
input: urlLocation.searchParams.get("query"),
fragment: range.cloneContents().innerHTML || "",
opened: [],
};
let escapePolicy = null;
if (window.trustedTypes && trustedTypes.createPolicy) {
escapePolicy = trustedTypes.createPolicy("fetch", {
createHTML: (string) => string,
});
}
function detailsToggled(ev) {
const nextURL = new URL(location);
if (ev.newState == "open" || ev.target.open === true) {
state.opened.push(this.id);
nextURL.hash = this.id;
} else {
state.opened = state.opened.filter((x) => x != this.id);
nextURL.hash = "";
}
state.url = nextURL.toJSON();
history.replaceState(state, "", nextURL);
}
function addToggleEventListeners(results) {
results.querySelectorAll("details").forEach((details) =>
// toggle event doesn't bubble :(
details.addEventListener("toggle", detailsToggled, { passive: true }),
);
}
function paginationLinkClicked(ev) {
const url = new URL(ev.target.href);
getResults(url);
ev.preventDefault();
}
function addPaginationEventListeners(pagination) {
pagination.addEventListener("click", paginationLinkClicked);
}
function renderFragmentHTML(html) {
const fragment = range.createContextualFragment(
escapePolicy !== null ? escapePolicy.createHTML(html) : html,
);
results = fragment.querySelector("#results");
pagination = fragment.querySelector("#pagination");
range.deleteContents();
range.insertNode(fragment);
if (results !== null) {
addToggleEventListeners(results);
}
if (pagination !== null) {
addPaginationEventListeners(pagination);
}
}
async function getResults(url) {
try {
state.url = url.toJSON();
history.pushState(state, null, url);
const res = await fetch(url, {
headers: {
fetch: "true",
},
});
if (res.ok) {
state.fragment = await res.text();
state.opened = [];
history.replaceState(state, null, url);
renderFragmentHTML(state.fragment);
} else {
throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`);
}
} catch (error) {
range.deleteContents();
range.insertNode(new Text(error.message));
console.error("fetch failed", error);
}
}
search.addEventListener("submit", function (ev) {
const url = new URL(this.action);
const formData = new FormData(this);
url.search = new URLSearchParams(formData).toString();
state.input = formData.get("query");
getResults(url);
ev.preventDefault();
});
if (results !== null) {
addToggleEventListeners(results);
}
if (pagination !== null) {
addPaginationEventListeners(pagination);
}
if (state.opened.length > 0) {
state.opened.forEach((id) =>
document.getElementById(id).setAttribute("open", "open"),
);
} else if (location.hash) {
document.getElementById(location.hash.slice(1)).setAttribute("open", "open");
}
addEventListener("popstate", function (ev) {
if (ev.state != null) {
url = new URL(ev.state.url);
if (!url.pathname.endsWith("/search") && ev.state.fragment !== null) {
document.getElementById("query").value = ev.state.input;
renderFragmentHTML(ev.state.fragment);
return;
}
}
range.deleteContents();
search.reset();
});
|