diff --git a/index.d.ts b/index.d.ts index 8e524af..07f8082 100644 --- a/index.d.ts +++ b/index.d.ts @@ -10,4 +10,5 @@ interface CustomEventMap { themechange: CustomEvent<{ theme: "light" | "dark" }> } +type ContentIndex = Record declare const fetchData: Promise diff --git a/quartz/components/Search.tsx b/quartz/components/Search.tsx index 8b97555..4c6664a 100644 --- a/quartz/components/Search.tsx +++ b/quartz/components/Search.tsx @@ -19,7 +19,7 @@ export default ((userOpts?: Partial) => { const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder return (
- -
-
+
+
-
+
diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts index f422d49..c9bbcce 100644 --- a/quartz/components/scripts/search.inline.ts +++ b/quartz/components/scripts/search.inline.ts @@ -143,83 +143,75 @@ function highlightHTML(searchTerm: string, el: HTMLElement) { return html.body } -document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { - const currentSlug = e.detail.url - const data = await fetchData - const container = document.getElementById("search-container") - const sidebar = container?.closest(".sidebar") as HTMLElement - const searchButton = document.getElementById("search-button") - const searchBar = document.getElementById("search-bar") as HTMLInputElement | null - const searchLayout = document.getElementById("search-layout") - const idDataMap = Object.keys(data) as FullSlug[] +async function setupSearch(searchElement: Element, currentSlug: FullSlug, data: ContentIndex) { + const container = searchElement.querySelector(".search-container") as HTMLElement + if (!container) return + const sidebar = container.closest(".sidebar") as HTMLElement + if (!sidebar) return + + const searchButton = searchElement.querySelector(".search-button") as HTMLButtonElement + if (!searchButton) return + + const searchBar = searchElement.querySelector(".search-bar") as HTMLInputElement + if (!searchBar) return + + const searchLayout = searchElement.querySelector(".search-layout") as HTMLElement + if (!searchLayout) return + + const idDataMap = Object.keys(data) as FullSlug[] const appendLayout = (el: HTMLElement) => { - if (searchLayout?.querySelector(`#${el.id}`) === null) { - searchLayout?.appendChild(el) - } + searchLayout.appendChild(el) } - const enablePreview = searchLayout?.dataset?.preview === "true" + const enablePreview = searchLayout.dataset.preview === "true" let preview: HTMLDivElement | undefined = undefined let previewInner: HTMLDivElement | undefined = undefined const results = document.createElement("div") - results.id = "results-container" + results.className = "results-container" appendLayout(results) if (enablePreview) { preview = document.createElement("div") - preview.id = "preview-container" + preview.className = "preview-container" appendLayout(preview) } function hideSearch() { - container?.classList.remove("active") - if (searchBar) { - searchBar.value = "" // clear the input when we dismiss the search - } - if (sidebar) { - sidebar.style.zIndex = "" - } - if (results) { - removeAllChildren(results) - } + container.classList.remove("active") + searchBar.value = "" // clear the input when we dismiss the search + sidebar.style.zIndex = "" + removeAllChildren(results) if (preview) { removeAllChildren(preview) } - if (searchLayout) { - searchLayout.classList.remove("display-results") - } - + searchLayout.classList.remove("display-results") searchType = "basic" // reset search type after closing - - searchButton?.focus() + searchButton.focus() } function showSearch(searchTypeNew: SearchType) { searchType = searchTypeNew - if (sidebar) { - sidebar.style.zIndex = "1" - } - container?.classList.add("active") - searchBar?.focus() + sidebar.style.zIndex = "1" + container.classList.add("active") + searchBar.focus() } let currentHover: HTMLInputElement | null = null - async function shortcutHandler(e: HTMLElementEventMap["keydown"]) { if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) { e.preventDefault() - const searchBarOpen = container?.classList.contains("active") + const searchBarOpen = container.classList.contains("active") searchBarOpen ? hideSearch() : showSearch("basic") return } else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { // Hotkey to open tag search e.preventDefault() - const searchBarOpen = container?.classList.contains("active") + const searchBarOpen = container.classList.contains("active") searchBarOpen ? hideSearch() : showSearch("tags") // add "#" prefix for tag search - if (searchBar) searchBar.value = "#" + searchBar.value = "#" return } @@ -228,23 +220,23 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { } // If search is active, then we will render the first result and display accordingly - if (!container?.classList.contains("active")) return + if (!container.classList.contains("active")) return if (e.key === "Enter") { // If result has focus, navigate to that one, otherwise pick first result - if (results?.contains(document.activeElement)) { + if (results.contains(document.activeElement)) { const active = document.activeElement as HTMLInputElement if (active.classList.contains("no-match")) return await displayPreview(active) active.click() } else { const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null - if (!anchor || anchor?.classList.contains("no-match")) return + if (!anchor || anchor.classList.contains("no-match")) return await displayPreview(anchor) anchor.click() } } else if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) { e.preventDefault() - if (results?.contains(document.activeElement)) { + if (results.contains(document.activeElement)) { // If an element in results-container already has focus, focus previous one const currentResult = currentHover ? currentHover @@ -337,8 +329,6 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { } async function displayResults(finalResults: Item[]) { - if (!results) return - removeAllChildren(results) if (finalResults.length === 0) { results.innerHTML = ` @@ -460,21 +450,23 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { document.addEventListener("keydown", shortcutHandler) window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler)) - searchButton?.addEventListener("click", () => showSearch("basic")) - window.addCleanup(() => searchButton?.removeEventListener("click", () => showSearch("basic"))) - searchBar?.addEventListener("input", onType) - window.addCleanup(() => searchBar?.removeEventListener("input", onType)) + searchButton.addEventListener("click", () => showSearch("basic")) + window.addCleanup(() => searchButton.removeEventListener("click", () => showSearch("basic"))) + searchBar.addEventListener("input", onType) + window.addCleanup(() => searchBar.removeEventListener("input", onType)) registerEscapeHandler(container, hideSearch) await fillDocument(data) -}) +} /** * Fills flexsearch document with data * @param index index to fill * @param data data to fill index with */ -async function fillDocument(data: { [key: FullSlug]: ContentDetails }) { +let indexPopulated = false +async function fillDocument(data: ContentIndex) { + if (indexPopulated) return let id = 0 const promises: Array> = [] for (const [slug, fileData] of Object.entries(data)) { @@ -489,5 +481,15 @@ async function fillDocument(data: { [key: FullSlug]: ContentDetails }) { ) } - return await Promise.all(promises) + await Promise.all(promises) + indexPopulated = true } + +document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { + const currentSlug = e.detail.url + const data = await fetchData + const searchElement = document.querySelectorAll(".search") + for (const element of searchElement) { + await setupSearch(element, currentSlug, data) + } +}) diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss index 4daefee..182b56b 100644 --- a/quartz/components/styles/search.scss +++ b/quartz/components/styles/search.scss @@ -42,7 +42,7 @@ } } - & > #search-container { + & > .search-container { position: fixed; contain: layout; z-index: 999; @@ -58,7 +58,7 @@ display: inline-block; } - & > #search-space { + & > .search-space { width: 65%; margin-top: 12vh; margin-left: auto; @@ -91,7 +91,7 @@ } } - & > #search-layout { + & > .search-layout { display: none; flex-direction: row; border: 1px solid var(--lightgray); @@ -102,7 +102,7 @@ display: flex; } - &[data-preview] > #results-container { + &[data-preview] > .results-container { flex: 0 0 min(30%, 450px); } @@ -150,7 +150,7 @@ scroll-margin-top: 2rem; } - & > #preview-container { + & > .preview-container { flex-grow: 1; display: block; overflow: hidden; @@ -171,7 +171,7 @@ } } - & > #results-container { + & > .results-container { overflow-y: auto; & .result-card {