Include the updated desktop screenshot, active-monitor screenshot capture and editor locking, and Settings window drag areas.
131 lines
4.3 KiB
JavaScript
131 lines
4.3 KiB
JavaScript
// Progressive enhancement for the handbook. Navigation and content work without JS.
|
|
const search = document.querySelector("#docs-search");
|
|
const results = document.querySelector("#search-results");
|
|
const status = document.querySelector("#search-status");
|
|
let indexPromise;
|
|
if (search) {
|
|
search.closest(".docs-search").hidden = false;
|
|
const loadIndex = () =>
|
|
(indexPromise ||= fetch("search-index.json")
|
|
.then((response) => {
|
|
if (!response.ok) throw new Error("Search index unavailable");
|
|
return response.json();
|
|
})
|
|
.catch((error) => {
|
|
indexPromise = null;
|
|
throw error;
|
|
}));
|
|
search.addEventListener("focus", () => {
|
|
loadIndex().catch(() => {});
|
|
});
|
|
search.addEventListener("input", async () => {
|
|
const query = search.value.trim().toLocaleLowerCase();
|
|
if (!query) {
|
|
results.hidden = true;
|
|
results.replaceChildren();
|
|
status.textContent = "";
|
|
return;
|
|
}
|
|
try {
|
|
const index = await loadIndex();
|
|
if (search.value.trim().toLocaleLowerCase() !== query) return;
|
|
const words = query.split(/\s+/);
|
|
const matches = index
|
|
.filter((item) =>
|
|
words.every((word) =>
|
|
`${item.page} ${item.heading} ${item.text}`
|
|
.toLocaleLowerCase()
|
|
.includes(word),
|
|
),
|
|
)
|
|
.sort(
|
|
(a, b) =>
|
|
Number(b.heading.toLocaleLowerCase().includes(query)) -
|
|
Number(a.heading.toLocaleLowerCase().includes(query)),
|
|
)
|
|
.slice(0, 10);
|
|
results.replaceChildren();
|
|
for (const item of matches) {
|
|
const link = document.createElement("a");
|
|
link.href = item.url;
|
|
const title = document.createElement("strong");
|
|
title.textContent = `${item.page} / ${item.heading}`;
|
|
const snippet = document.createElement("span");
|
|
const position = Math.max(
|
|
0,
|
|
item.text.toLocaleLowerCase().indexOf(words[0]) - 45,
|
|
);
|
|
snippet.textContent = `${position ? "…" : ""}${item.text.slice(position, position + 165)}${item.text.length > position + 165 ? "…" : ""}`;
|
|
link.append(title, snippet);
|
|
results.append(link);
|
|
}
|
|
if (!matches.length) {
|
|
const empty = document.createElement("p");
|
|
empty.textContent =
|
|
"No matching sections. Try “wallpaper”, “shortcuts”, or “notifications”.";
|
|
results.append(empty);
|
|
}
|
|
results.hidden = false;
|
|
status.textContent = `${matches.length} matching sections.`;
|
|
} catch {
|
|
if (search.value.trim().toLocaleLowerCase() !== query) return;
|
|
results.hidden = true;
|
|
status.textContent = "Search is unavailable. Browse the guides below.";
|
|
}
|
|
});
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape") {
|
|
results.hidden = true;
|
|
search.focus();
|
|
}
|
|
if (
|
|
event.key === "/" &&
|
|
!["INPUT", "TEXTAREA"].includes(document.activeElement.tagName) &&
|
|
!event.ctrlKey &&
|
|
!event.metaKey &&
|
|
!event.altKey
|
|
) {
|
|
event.preventDefault();
|
|
search.focus();
|
|
}
|
|
if (
|
|
event.key === "ArrowDown" &&
|
|
document.activeElement === search &&
|
|
!results.hidden
|
|
) {
|
|
const first = results.querySelector("a");
|
|
if (first) {
|
|
event.preventDefault();
|
|
first.focus();
|
|
}
|
|
}
|
|
});
|
|
document.addEventListener("click", (event) => {
|
|
if (!event.target.closest(".docs-search")) results.hidden = true;
|
|
});
|
|
}
|
|
if (navigator.clipboard?.writeText) {
|
|
for (const block of document.querySelectorAll(".prose pre")) {
|
|
const button = document.createElement("button");
|
|
button.type = "button";
|
|
button.className = "code-copy";
|
|
button.textContent = "Copy";
|
|
button.setAttribute("aria-label", "Copy code example");
|
|
button.addEventListener("click", async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(
|
|
block.querySelector("code").textContent,
|
|
);
|
|
button.textContent = "Copied";
|
|
if (status) status.textContent = "Code example copied.";
|
|
} catch {
|
|
button.textContent = "Select to copy";
|
|
if (status)
|
|
status.textContent =
|
|
"Copy failed. Select the code to copy it manually.";
|
|
}
|
|
});
|
|
block.append(button);
|
|
}
|
|
}
|