600 lines
22 KiB
JavaScript
600 lines
22 KiB
JavaScript
(() => {
|
|
"use strict";
|
|
|
|
const embedded = JSON.parse(document.getElementById("docslib-manifest").textContent);
|
|
const config = window.__DOCSLIB_CONFIG__ || JSON.parse(document.getElementById("docslib-config").textContent);
|
|
const mobileLayout = matchMedia("(max-width: 820px)");
|
|
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
const elements = {
|
|
desktopTitle: document.getElementById("desktop-site-title"),
|
|
mobileTitle: document.getElementById("mobile-site-title"),
|
|
desktopHeaderButtons: document.getElementById("desktop-header-buttons"),
|
|
mobileHeaderButtons: document.getElementById("mobile-header-buttons"),
|
|
desktopTree: document.getElementById("desktop-page-tree"),
|
|
mobileTree: document.getElementById("mobile-page-tree"),
|
|
toolbar: document.getElementById("desktop-toolbar"),
|
|
desktopSearchShell: document.getElementById("desktop-search-shell"),
|
|
desktopSearchInput: document.getElementById("desktop-search-input"),
|
|
desktopSearchButton: document.getElementById("desktop-search-button"),
|
|
desktopSearchResults: document.getElementById("desktop-search-results"),
|
|
mobileSearchInput: document.getElementById("mobile-search-input"),
|
|
mobileSearchButton: document.getElementById("mobile-search-button"),
|
|
mobileSearchResults: document.getElementById("mobile-search-results"),
|
|
content: document.getElementById("content"),
|
|
pageSurface: document.getElementById("page-surface"),
|
|
previous: document.getElementById("previous-page"),
|
|
next: document.getElementById("next-page"),
|
|
themeIcon: document.getElementById("theme-icon"),
|
|
refresh: document.getElementById("refresh-button"),
|
|
identity: document.getElementById("manifest-identity"),
|
|
mobileMenuButton: document.getElementById("mobile-menu-button"),
|
|
};
|
|
|
|
let manifest = window.__DOCSLIB_MANIFEST__ || embedded;
|
|
let currentNumber = null;
|
|
let syncing = false;
|
|
let mobileMode = "content";
|
|
let modeBeforeSearch = "content";
|
|
let placeholderTimer = 0;
|
|
let placeholderText = "";
|
|
let placeholderTarget = "";
|
|
let placeholderIndex = 0;
|
|
let placeholderDots = 0;
|
|
let placeholderPhase = "typing";
|
|
let lastPlaceholderPage = -1;
|
|
let customScriptLoaded = false;
|
|
let lastSyncedRevision = String(manifest.revision || "");
|
|
|
|
const dbPromise = new Promise((resolve, reject) => {
|
|
if (!window.indexedDB) return reject(new Error("IndexedDB"));
|
|
const request = indexedDB.open("docslib-cache", 1);
|
|
request.onupgradeneeded = () => {
|
|
if (!request.result.objectStoreNames.contains("manifests")) {
|
|
request.result.createObjectStore("manifests");
|
|
}
|
|
};
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
|
|
async function cachePut(value) {
|
|
const db = await dbPromise;
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction("manifests", "readwrite");
|
|
transaction.objectStore("manifests").put(value, config.cacheKey);
|
|
transaction.oncomplete = resolve;
|
|
transaction.onerror = () => reject(transaction.error);
|
|
});
|
|
}
|
|
|
|
function manifestIdentity(value = manifest) {
|
|
const updated = String(value.updated_at || "")
|
|
.replace("T", " ")
|
|
.replace(/\.\d+(?=Z|[+-])/, "")
|
|
.replace("+00:00", "Z");
|
|
const revision = String(value.revision || "").slice(0, 8);
|
|
return [updated, revision].filter(Boolean).join(" · ") || "—";
|
|
}
|
|
|
|
function isManifest(value) {
|
|
return Boolean(
|
|
value &&
|
|
value.schema_version >= 2 &&
|
|
Array.isArray(value.pages) &&
|
|
value.client &&
|
|
value.client.css &&
|
|
typeof value.client.css.system === "string" &&
|
|
value.client.js &&
|
|
typeof value.client.js.system === "string"
|
|
);
|
|
}
|
|
|
|
function updateIdentity() {
|
|
elements.identity.textContent = manifestIdentity();
|
|
elements.refresh.title = String(manifest.revision || "");
|
|
}
|
|
|
|
function setSyncing(value) {
|
|
syncing = value;
|
|
elements.refresh.classList.toggle("loading", value);
|
|
}
|
|
|
|
function siblingResourceUrl(url, name) {
|
|
const target = new URL(url, document.baseURI);
|
|
target.pathname = target.pathname.replace(/[^/]*$/, name);
|
|
target.search = "";
|
|
target.hash = "";
|
|
return target.href;
|
|
}
|
|
|
|
function noCacheUrl(url) {
|
|
const target = new URL(url, document.baseURI);
|
|
target.searchParams.set(
|
|
"no_cache",
|
|
`${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
);
|
|
return target.href;
|
|
}
|
|
|
|
function fetchNoCache(url) {
|
|
return fetch(noCacheUrl(url), { cache: "no-store" });
|
|
}
|
|
|
|
function routeNumber() {
|
|
const match = location.hash.match(/^#\/page\/(.+)$/);
|
|
return match ? decodeURIComponent(match[1]) : null;
|
|
}
|
|
|
|
function isPlainClick(event) {
|
|
return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
|
|
}
|
|
|
|
function setMobileMode(mode) {
|
|
mobileMode = mode;
|
|
document.body.classList.toggle("mobile-menu-open", mode === "menu");
|
|
document.body.classList.toggle("mobile-search-open", mode === "search");
|
|
}
|
|
|
|
function closeSearch({ clear = false } = {}) {
|
|
elements.desktopSearchShell.classList.remove("open");
|
|
if (clear) {
|
|
elements.desktopSearchInput.value = "";
|
|
elements.mobileSearchInput.value = "";
|
|
}
|
|
if (mobileMode === "search") setMobileMode(modeBeforeSearch);
|
|
}
|
|
|
|
function navigate(number, replace = false) {
|
|
const hash = `#/page/${encodeURIComponent(number)}`;
|
|
if (replace) history.replaceState(null, "", hash);
|
|
else if (location.hash !== hash) location.hash = hash;
|
|
closeSearch({ clear: true });
|
|
setMobileMode("content");
|
|
renderPage(number);
|
|
}
|
|
|
|
function buildTree(container) {
|
|
container.replaceChildren();
|
|
const root = { children: new Map(), page: null };
|
|
manifest.pages.forEach((page) => {
|
|
let node = root;
|
|
page.number.split(".").forEach((part) => {
|
|
if (!node.children.has(part)) node.children.set(part, { children: new Map(), page: null });
|
|
node = node.children.get(part);
|
|
});
|
|
node.page = page;
|
|
});
|
|
const appendNodes = (parent, node) => {
|
|
node.children.forEach((child) => {
|
|
const group = document.createElement("div");
|
|
group.className = "tree-group";
|
|
if (child.page) {
|
|
const button = document.createElement("button");
|
|
button.className = "tree-link";
|
|
button.dataset.page = child.page.number;
|
|
const number = document.createElement("span");
|
|
number.className = "tree-number";
|
|
number.textContent = `${child.page.number}.`;
|
|
const title = document.createElement("span");
|
|
title.className = "tree-title";
|
|
title.textContent = child.page.title;
|
|
button.append(number, title);
|
|
button.addEventListener("click", () => navigate(child.page.number));
|
|
group.append(button);
|
|
}
|
|
if (child.children.size) {
|
|
const nested = document.createElement("div");
|
|
nested.className = "tree-children";
|
|
appendNodes(nested, child);
|
|
group.append(nested);
|
|
}
|
|
parent.append(group);
|
|
});
|
|
};
|
|
appendNodes(container, root);
|
|
}
|
|
|
|
function buildHeaderButtons(container) {
|
|
container.replaceChildren();
|
|
const buttons = (manifest.ui && manifest.ui.header_buttons) || [];
|
|
buttons.forEach((button) => {
|
|
if (!button.label || (!button.href && !button.page)) return;
|
|
const link = document.createElement("a");
|
|
link.className = "header-link";
|
|
link.textContent = button.label;
|
|
if (button.page) {
|
|
link.href = `#/page/${encodeURIComponent(button.page)}`;
|
|
link.dataset.page = button.page;
|
|
link.addEventListener("click", (event) => {
|
|
if (!isPlainClick(event)) return;
|
|
event.preventDefault();
|
|
navigate(button.page);
|
|
});
|
|
} else {
|
|
link.href = button.href;
|
|
if (button.target === "_blank") {
|
|
link.target = "_blank";
|
|
link.rel = "noopener noreferrer";
|
|
}
|
|
}
|
|
container.append(link);
|
|
});
|
|
}
|
|
|
|
function assetDataUri(name) {
|
|
const asset = manifest.assets && manifest.assets[name];
|
|
return asset ? `data:${asset.mime};base64,${asset.data}` : "";
|
|
}
|
|
|
|
function hydrateAssets() {
|
|
elements.content.querySelectorAll("[data-docslib-asset]").forEach((element) => {
|
|
const name = element.dataset.docslibAsset;
|
|
const uri = assetDataUri(name);
|
|
if (uri && element.tagName === "IMG") element.src = uri;
|
|
else if (!uri) element.setAttribute("title", name);
|
|
});
|
|
}
|
|
|
|
function updateActiveNavigation(number) {
|
|
document.querySelectorAll(".tree-link[data-page]").forEach((button) => {
|
|
button.classList.toggle("active", button.dataset.page === number);
|
|
});
|
|
document.querySelectorAll(".header-link[data-page]").forEach((button) => {
|
|
const target = button.dataset.page;
|
|
button.classList.toggle("active", number === target || number.startsWith(`${target}.`));
|
|
});
|
|
}
|
|
|
|
function setupNavButton(button, page, direction) {
|
|
button.disabled = !page;
|
|
button.replaceChildren();
|
|
if (!page) return;
|
|
const arrow = document.createElement("span");
|
|
arrow.className = `nav-arrow nav-arrow-${direction}`;
|
|
arrow.setAttribute("aria-hidden", "true");
|
|
const title = document.createElement("span");
|
|
title.textContent = page.title;
|
|
if (direction === "previous") button.append(arrow, title);
|
|
else button.append(title, arrow);
|
|
button.onclick = () => navigate(page.number);
|
|
}
|
|
|
|
function renderPage(number) {
|
|
if (!manifest.pages.length) {
|
|
elements.content.replaceChildren();
|
|
elements.previous.disabled = elements.next.disabled = true;
|
|
return;
|
|
}
|
|
let index = manifest.pages.findIndex((page) => page.number === number);
|
|
if (index < 0) index = 0;
|
|
const page = manifest.pages[index];
|
|
currentNumber = page.number;
|
|
if (routeNumber() !== page.number) history.replaceState(null, "", `#/page/${encodeURIComponent(page.number)}`);
|
|
document.title = `${page.title} · ${manifest.title}`;
|
|
elements.content.innerHTML = page.content || "";
|
|
hydrateAssets();
|
|
elements.content.querySelectorAll("a[href^='#/page/']").forEach((link) => {
|
|
link.addEventListener("click", (event) => {
|
|
if (!isPlainClick(event)) return;
|
|
event.preventDefault();
|
|
navigate(decodeURIComponent(link.hash.slice(7)));
|
|
});
|
|
});
|
|
updateActiveNavigation(page.number);
|
|
setupNavButton(elements.previous, manifest.pages[index - 1], "previous");
|
|
setupNavButton(elements.next, manifest.pages[index + 1], "next");
|
|
elements.pageSurface.scrollTo({ top: 0, behavior: "instant" });
|
|
}
|
|
|
|
function plainText(html) {
|
|
const node = document.createElement("div");
|
|
node.innerHTML = html;
|
|
return (node.textContent || "").replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
function rankedPages(query) {
|
|
const needle = query.trim().toLocaleLowerCase();
|
|
if (!needle) return [];
|
|
const tokens = needle.split(/\s+/).filter(Boolean);
|
|
return manifest.pages
|
|
.map((page, order) => {
|
|
const title = page.title.toLocaleLowerCase();
|
|
const number = page.number.toLocaleLowerCase();
|
|
const text = plainText(page.content);
|
|
const haystack = `${number} ${title} ${text.toLocaleLowerCase()}`;
|
|
let score = 0;
|
|
if (title === needle) score += 1000;
|
|
if (title.startsWith(needle)) score += 500;
|
|
if (title.includes(needle)) score += 300;
|
|
if (number === needle) score += 600;
|
|
if (haystack.includes(needle)) score += 120;
|
|
tokens.forEach((token) => {
|
|
if (title.startsWith(token)) score += 80;
|
|
else if (title.includes(token)) score += 45;
|
|
else if (haystack.includes(token)) score += 12;
|
|
});
|
|
return { page, text, score, order };
|
|
})
|
|
.filter((item) => item.score > 0)
|
|
.sort((a, b) => b.score - a.score || a.order - b.order);
|
|
}
|
|
|
|
function renderSearchResults(container, query, limit) {
|
|
container.replaceChildren();
|
|
rankedPages(query).slice(0, limit).forEach(({ page, text }) => {
|
|
const button = document.createElement("button");
|
|
button.className = "search-result";
|
|
const title = document.createElement("strong");
|
|
title.textContent = `${page.number}. ${page.title}`;
|
|
const excerpt = document.createElement("span");
|
|
excerpt.textContent = text.slice(0, 110) || "—";
|
|
button.append(title, excerpt);
|
|
button.addEventListener("click", () => navigate(page.number));
|
|
container.append(button);
|
|
});
|
|
}
|
|
|
|
function handleDesktopSearch() {
|
|
const query = elements.desktopSearchInput.value;
|
|
renderSearchResults(elements.desktopSearchResults, query, 8);
|
|
elements.desktopSearchShell.classList.toggle("open", Boolean(query.trim()));
|
|
}
|
|
|
|
function handleMobileSearch() {
|
|
const query = elements.mobileSearchInput.value;
|
|
if (query.trim()) {
|
|
if (mobileMode !== "search") modeBeforeSearch = mobileMode;
|
|
setMobileMode("search");
|
|
renderSearchResults(elements.mobileSearchResults, query, 8);
|
|
} else if (mobileMode === "search") {
|
|
setMobileMode(modeBeforeSearch);
|
|
elements.mobileSearchResults.replaceChildren();
|
|
}
|
|
}
|
|
|
|
function choosePlaceholder() {
|
|
if (!manifest.pages.length) return "";
|
|
let index = Math.floor(Math.random() * manifest.pages.length);
|
|
if (manifest.pages.length > 1 && index === lastPlaceholderPage) index = (index + 1) % manifest.pages.length;
|
|
lastPlaceholderPage = index;
|
|
const words = manifest.pages[index].title.trim().split(/\s+/).filter(Boolean);
|
|
const count = Math.min(words.length, words.length > 2 && Math.random() > .5 ? 3 : 2);
|
|
return words.slice(0, Math.max(1, count)).join(" ");
|
|
}
|
|
|
|
function applyPlaceholder(value, dots = 0) {
|
|
const display = value ? `${value}${".".repeat(dots)}` : "";
|
|
elements.desktopSearchInput.placeholder = display;
|
|
elements.mobileSearchInput.placeholder = display;
|
|
}
|
|
|
|
function placeholderTick() {
|
|
clearTimeout(placeholderTimer);
|
|
if (elements.desktopSearchInput.value || elements.mobileSearchInput.value) {
|
|
placeholderTimer = setTimeout(placeholderTick, 400);
|
|
return;
|
|
}
|
|
if (!placeholderTarget) placeholderTarget = choosePlaceholder();
|
|
if (!placeholderTarget) return;
|
|
if (placeholderPhase === "typing") {
|
|
placeholderIndex += 1;
|
|
placeholderText = placeholderTarget.slice(0, placeholderIndex);
|
|
applyPlaceholder(placeholderText);
|
|
if (placeholderIndex >= placeholderTarget.length) {
|
|
placeholderPhase = "dots";
|
|
placeholderTimer = setTimeout(placeholderTick, 180);
|
|
} else placeholderTimer = setTimeout(placeholderTick, 95 + Math.random() * 90);
|
|
} else if (placeholderPhase === "dots") {
|
|
placeholderDots += 1;
|
|
applyPlaceholder(placeholderText, placeholderDots);
|
|
if (placeholderDots >= 3) {
|
|
placeholderPhase = "waiting";
|
|
placeholderTimer = setTimeout(placeholderTick, 5000);
|
|
} else placeholderTimer = setTimeout(placeholderTick, 220);
|
|
} else if (placeholderPhase === "waiting") {
|
|
placeholderPhase = "erasing";
|
|
placeholderDots = 0;
|
|
applyPlaceholder(placeholderText);
|
|
placeholderTimer = setTimeout(placeholderTick, 180);
|
|
} else {
|
|
placeholderIndex -= 1;
|
|
placeholderText = placeholderTarget.slice(0, Math.max(0, placeholderIndex));
|
|
applyPlaceholder(placeholderText);
|
|
if (placeholderIndex <= 0) {
|
|
placeholderTarget = choosePlaceholder();
|
|
placeholderPhase = "typing";
|
|
placeholderTimer = setTimeout(placeholderTick, 420);
|
|
} else placeholderTimer = setTimeout(placeholderTick, 55);
|
|
}
|
|
}
|
|
|
|
function startPlaceholder() {
|
|
clearTimeout(placeholderTimer);
|
|
placeholderText = "";
|
|
placeholderTarget = choosePlaceholder();
|
|
placeholderIndex = 0;
|
|
placeholderDots = 0;
|
|
placeholderPhase = "typing";
|
|
if (reducedMotion.matches) {
|
|
applyPlaceholder(placeholderTarget, 3);
|
|
return;
|
|
}
|
|
placeholderTimer = setTimeout(placeholderTick, 500);
|
|
}
|
|
|
|
function applyTheme(theme) {
|
|
document.documentElement.dataset.theme = theme;
|
|
elements.themeIcon.textContent = theme === "dark" ? "☀" : "◐";
|
|
localStorage.setItem("docslib-theme", theme);
|
|
}
|
|
|
|
function renderAll() {
|
|
window.__DOCSLIB_MANIFEST__ = manifest;
|
|
document.documentElement.lang = "und";
|
|
elements.desktopTitle.textContent = manifest.title;
|
|
elements.mobileTitle.textContent = manifest.title;
|
|
buildHeaderButtons(elements.desktopHeaderButtons);
|
|
buildHeaderButtons(elements.mobileHeaderButtons);
|
|
buildTree(elements.desktopTree);
|
|
buildTree(elements.mobileTree);
|
|
renderPage(routeNumber() || currentNumber || (manifest.pages[0] && manifest.pages[0].number));
|
|
updateIdentity();
|
|
startPlaceholder();
|
|
}
|
|
|
|
function loadCustomScript() {
|
|
if (customScriptLoaded) return;
|
|
customScriptLoaded = true;
|
|
const source = manifest.client && manifest.client.js && manifest.client.js.custom;
|
|
if (!source || !source.trim()) return;
|
|
const script = document.createElement("script");
|
|
script.id = "docslib-custom-script";
|
|
script.textContent = `${source}\n//# sourceURL=docslib-custom.js`;
|
|
document.body.append(script);
|
|
}
|
|
|
|
function announceRendered() {
|
|
try {
|
|
loadCustomScript();
|
|
} catch (error) {
|
|
console.error("SharedDocsLib custom_js", error);
|
|
} finally {
|
|
document.dispatchEvent(new CustomEvent("docslib:rendered", { detail: { manifest } }));
|
|
}
|
|
}
|
|
|
|
async function announceManifestUpdate(nextManifest, source) {
|
|
const previousRevision = lastSyncedRevision;
|
|
let cached = false;
|
|
try {
|
|
await cachePut(nextManifest);
|
|
cached = true;
|
|
} catch (_) { /* the update event still reports an unavailable cache */ }
|
|
lastSyncedRevision = String(nextManifest.revision || lastSyncedRevision);
|
|
document.dispatchEvent(new CustomEvent("on_manifest_update", {
|
|
bubbles: true,
|
|
detail: {
|
|
manifest: nextManifest,
|
|
currentManifest: manifest,
|
|
revision: nextManifest.revision,
|
|
previousRevision,
|
|
source,
|
|
cached,
|
|
applied: false,
|
|
},
|
|
}));
|
|
}
|
|
|
|
async function syncManifest() {
|
|
if (syncing) return false;
|
|
setSyncing(true);
|
|
let changed = false;
|
|
try {
|
|
if (
|
|
isManifest(embedded) &&
|
|
embedded.schema_version === manifest.schema_version &&
|
|
String(embedded.updated_at || "") > String(manifest.updated_at || "") &&
|
|
embedded.revision !== lastSyncedRevision
|
|
) {
|
|
await announceManifestUpdate(embedded, "embedded");
|
|
changed = true;
|
|
}
|
|
const versionUrl = config.versionUrl || siblingResourceUrl(config.manifestUrl, "version");
|
|
const versionResponse = await fetchNoCache(versionUrl);
|
|
if (!versionResponse.ok) throw new Error(`version:${versionResponse.status}`);
|
|
const remoteRevision = (await versionResponse.text()).trim();
|
|
if (!remoteRevision) throw new Error("version:empty");
|
|
if (remoteRevision === lastSyncedRevision) {
|
|
elements.refresh.classList.remove("error");
|
|
updateIdentity();
|
|
return changed;
|
|
}
|
|
document.dispatchEvent(new CustomEvent("on_update_available", {
|
|
bubbles: true,
|
|
detail: {
|
|
revision: remoteRevision,
|
|
currentRevision: lastSyncedRevision,
|
|
versionUrl,
|
|
manifestUrl: config.manifestUrl,
|
|
},
|
|
}));
|
|
const response = await fetchNoCache(config.manifestUrl);
|
|
if (!response.ok) throw new Error(String(response.status));
|
|
const remote = await response.json();
|
|
if (!isManifest(remote) || remote.schema_version !== embedded.schema_version) throw new Error("manifest");
|
|
if (String(remote.revision || "") !== remoteRevision) throw new Error("version:mismatch");
|
|
const remoteChanged = remote.revision !== lastSyncedRevision;
|
|
if (remoteChanged) {
|
|
await announceManifestUpdate(remote, "remote");
|
|
changed = true;
|
|
}
|
|
elements.refresh.classList.remove("error");
|
|
updateIdentity();
|
|
return changed;
|
|
} catch (error) {
|
|
elements.refresh.classList.add("error");
|
|
elements.refresh.title = String(error);
|
|
return false;
|
|
} finally {
|
|
setSyncing(false);
|
|
}
|
|
}
|
|
|
|
async function pollLive() {
|
|
try {
|
|
const response = await fetch(config.liveUrl, { cache: "no-store" });
|
|
if (!response.ok) return;
|
|
const live = await response.json();
|
|
if (live.revision && live.revision !== lastSyncedRevision) {
|
|
const changed = await syncManifest();
|
|
if (changed) location.reload();
|
|
}
|
|
} catch (_) { /* local preview may stop */ }
|
|
}
|
|
|
|
async function start() {
|
|
applyTheme(localStorage.getItem("docslib-theme") || "dark");
|
|
renderAll();
|
|
announceRendered();
|
|
syncManifest();
|
|
if (config.livePreview) setInterval(pollLive, 1200);
|
|
}
|
|
|
|
elements.desktopSearchInput.addEventListener("input", handleDesktopSearch);
|
|
elements.desktopSearchButton.addEventListener("click", () => elements.desktopSearchInput.focus());
|
|
elements.mobileSearchInput.addEventListener("input", handleMobileSearch);
|
|
elements.mobileSearchButton.addEventListener("click", () => elements.mobileSearchInput.focus());
|
|
elements.mobileMenuButton.addEventListener("click", () => {
|
|
elements.mobileSearchInput.value = "";
|
|
elements.mobileSearchResults.replaceChildren();
|
|
setMobileMode(mobileMode === "menu" ? "content" : "menu");
|
|
});
|
|
document.getElementById("theme-button").addEventListener("click", () => {
|
|
applyTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark");
|
|
});
|
|
elements.refresh.addEventListener("pointerenter", () => elements.toolbar.classList.add("refresh-expanded"));
|
|
elements.refresh.addEventListener("pointerleave", () => elements.toolbar.classList.remove("refresh-expanded"));
|
|
elements.refresh.addEventListener("focus", () => elements.toolbar.classList.add("refresh-expanded"));
|
|
elements.refresh.addEventListener("blur", () => elements.toolbar.classList.remove("refresh-expanded"));
|
|
elements.refresh.addEventListener("click", async () => {
|
|
if (syncing) return;
|
|
await syncManifest();
|
|
});
|
|
document.addEventListener("pointerdown", (event) => {
|
|
if (!elements.desktopSearchShell.contains(event.target)) elements.desktopSearchShell.classList.remove("open");
|
|
});
|
|
window.addEventListener("hashchange", () => renderPage(routeNumber()));
|
|
window.addEventListener("keydown", (event) => {
|
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
|
|
event.preventDefault();
|
|
(mobileLayout.matches ? elements.mobileSearchInput : elements.desktopSearchInput).focus();
|
|
}
|
|
if (event.key === "Escape") closeSearch({ clear: true });
|
|
});
|
|
mobileLayout.addEventListener?.("change", () => {
|
|
closeSearch({ clear: true });
|
|
setMobileMode("content");
|
|
});
|
|
|
|
start();
|
|
})();
|