The Trans Directory
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: support non-singleton search

+67 -64
+1
index.d.ts
··· 10 10 themechange: CustomEvent<{ theme: "light" | "dark" }> 11 11 } 12 12 13 + type ContentIndex = Record<FullSlug, ContentDetails> 13 14 declare const fetchData: Promise<ContentIndex>
+5 -5
quartz/components/Search.tsx
··· 19 19 const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder 20 20 return ( 21 21 <div class={classNames(displayClass, "search")}> 22 - <button class="search-button" id="search-button"> 22 + <button class="search-button"> 23 23 <p>{i18n(cfg.locale).components.search.title}</p> 24 24 <svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7"> 25 25 <title>Search</title> ··· 29 29 </g> 30 30 </svg> 31 31 </button> 32 - <div id="search-container"> 33 - <div id="search-space"> 32 + <div class="search-container"> 33 + <div class="search-space"> 34 34 <input 35 35 autocomplete="off" 36 - id="search-bar" 36 + class="search-bar" 37 37 name="search" 38 38 type="text" 39 39 aria-label={searchPlaceholder} 40 40 placeholder={searchPlaceholder} 41 41 /> 42 - <div id="search-layout" data-preview={opts.enablePreview}></div> 42 + <div class="search-layout" data-preview={opts.enablePreview}></div> 43 43 </div> 44 44 </div> 45 45 </div>
+55 -53
quartz/components/scripts/search.inline.ts
··· 143 143 return html.body 144 144 } 145 145 146 - document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { 147 - const currentSlug = e.detail.url 148 - const data = await fetchData 149 - const container = document.getElementById("search-container") 150 - const sidebar = container?.closest(".sidebar") as HTMLElement 151 - const searchButton = document.getElementById("search-button") 152 - const searchBar = document.getElementById("search-bar") as HTMLInputElement | null 153 - const searchLayout = document.getElementById("search-layout") 146 + async function setupSearch(searchElement: Element, currentSlug: FullSlug, data: ContentIndex) { 147 + const container = searchElement.querySelector(".search-container") as HTMLElement 148 + if (!container) return 149 + 150 + const sidebar = container.closest(".sidebar") as HTMLElement 151 + if (!sidebar) return 152 + 153 + const searchButton = searchElement.querySelector(".search-button") as HTMLButtonElement 154 + if (!searchButton) return 155 + 156 + const searchBar = searchElement.querySelector(".search-bar") as HTMLInputElement 157 + if (!searchBar) return 158 + 159 + const searchLayout = searchElement.querySelector(".search-layout") as HTMLElement 160 + if (!searchLayout) return 161 + 154 162 const idDataMap = Object.keys(data) as FullSlug[] 155 - 156 163 const appendLayout = (el: HTMLElement) => { 157 - if (searchLayout?.querySelector(`#${el.id}`) === null) { 158 - searchLayout?.appendChild(el) 159 - } 164 + searchLayout.appendChild(el) 160 165 } 161 166 162 - const enablePreview = searchLayout?.dataset?.preview === "true" 167 + const enablePreview = searchLayout.dataset.preview === "true" 163 168 let preview: HTMLDivElement | undefined = undefined 164 169 let previewInner: HTMLDivElement | undefined = undefined 165 170 const results = document.createElement("div") 166 - results.id = "results-container" 171 + results.className = "results-container" 167 172 appendLayout(results) 168 173 169 174 if (enablePreview) { 170 175 preview = document.createElement("div") 171 - preview.id = "preview-container" 176 + preview.className = "preview-container" 172 177 appendLayout(preview) 173 178 } 174 179 175 180 function hideSearch() { 176 - container?.classList.remove("active") 177 - if (searchBar) { 178 - searchBar.value = "" // clear the input when we dismiss the search 179 - } 180 - if (sidebar) { 181 - sidebar.style.zIndex = "" 182 - } 183 - if (results) { 184 - removeAllChildren(results) 185 - } 181 + container.classList.remove("active") 182 + searchBar.value = "" // clear the input when we dismiss the search 183 + sidebar.style.zIndex = "" 184 + removeAllChildren(results) 186 185 if (preview) { 187 186 removeAllChildren(preview) 188 187 } 189 - if (searchLayout) { 190 - searchLayout.classList.remove("display-results") 191 - } 192 - 188 + searchLayout.classList.remove("display-results") 193 189 searchType = "basic" // reset search type after closing 194 - 195 - searchButton?.focus() 190 + searchButton.focus() 196 191 } 197 192 198 193 function showSearch(searchTypeNew: SearchType) { 199 194 searchType = searchTypeNew 200 - if (sidebar) { 201 - sidebar.style.zIndex = "1" 202 - } 203 - container?.classList.add("active") 204 - searchBar?.focus() 195 + sidebar.style.zIndex = "1" 196 + container.classList.add("active") 197 + searchBar.focus() 205 198 } 206 199 207 200 let currentHover: HTMLInputElement | null = null 208 - 209 201 async function shortcutHandler(e: HTMLElementEventMap["keydown"]) { 210 202 if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) { 211 203 e.preventDefault() 212 - const searchBarOpen = container?.classList.contains("active") 204 + const searchBarOpen = container.classList.contains("active") 213 205 searchBarOpen ? hideSearch() : showSearch("basic") 214 206 return 215 207 } else if (e.shiftKey && (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { 216 208 // Hotkey to open tag search 217 209 e.preventDefault() 218 - const searchBarOpen = container?.classList.contains("active") 210 + const searchBarOpen = container.classList.contains("active") 219 211 searchBarOpen ? hideSearch() : showSearch("tags") 220 212 221 213 // add "#" prefix for tag search 222 - if (searchBar) searchBar.value = "#" 214 + searchBar.value = "#" 223 215 return 224 216 } 225 217 ··· 228 220 } 229 221 230 222 // If search is active, then we will render the first result and display accordingly 231 - if (!container?.classList.contains("active")) return 223 + if (!container.classList.contains("active")) return 232 224 if (e.key === "Enter") { 233 225 // If result has focus, navigate to that one, otherwise pick first result 234 - if (results?.contains(document.activeElement)) { 226 + if (results.contains(document.activeElement)) { 235 227 const active = document.activeElement as HTMLInputElement 236 228 if (active.classList.contains("no-match")) return 237 229 await displayPreview(active) 238 230 active.click() 239 231 } else { 240 232 const anchor = document.getElementsByClassName("result-card")[0] as HTMLInputElement | null 241 - if (!anchor || anchor?.classList.contains("no-match")) return 233 + if (!anchor || anchor.classList.contains("no-match")) return 242 234 await displayPreview(anchor) 243 235 anchor.click() 244 236 } 245 237 } else if (e.key === "ArrowUp" || (e.shiftKey && e.key === "Tab")) { 246 238 e.preventDefault() 247 - if (results?.contains(document.activeElement)) { 239 + if (results.contains(document.activeElement)) { 248 240 // If an element in results-container already has focus, focus previous one 249 241 const currentResult = currentHover 250 242 ? currentHover ··· 337 329 } 338 330 339 331 async function displayResults(finalResults: Item[]) { 340 - if (!results) return 341 - 342 332 removeAllChildren(results) 343 333 if (finalResults.length === 0) { 344 334 results.innerHTML = `<a class="result-card no-match"> ··· 460 450 461 451 document.addEventListener("keydown", shortcutHandler) 462 452 window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler)) 463 - searchButton?.addEventListener("click", () => showSearch("basic")) 464 - window.addCleanup(() => searchButton?.removeEventListener("click", () => showSearch("basic"))) 465 - searchBar?.addEventListener("input", onType) 466 - window.addCleanup(() => searchBar?.removeEventListener("input", onType)) 453 + searchButton.addEventListener("click", () => showSearch("basic")) 454 + window.addCleanup(() => searchButton.removeEventListener("click", () => showSearch("basic"))) 455 + searchBar.addEventListener("input", onType) 456 + window.addCleanup(() => searchBar.removeEventListener("input", onType)) 467 457 468 458 registerEscapeHandler(container, hideSearch) 469 459 await fillDocument(data) 470 - }) 460 + } 471 461 472 462 /** 473 463 * Fills flexsearch document with data 474 464 * @param index index to fill 475 465 * @param data data to fill index with 476 466 */ 477 - async function fillDocument(data: { [key: FullSlug]: ContentDetails }) { 467 + let indexPopulated = false 468 + async function fillDocument(data: ContentIndex) { 469 + if (indexPopulated) return 478 470 let id = 0 479 471 const promises: Array<Promise<unknown>> = [] 480 472 for (const [slug, fileData] of Object.entries<ContentDetails>(data)) { ··· 489 481 ) 490 482 } 491 483 492 - return await Promise.all(promises) 484 + await Promise.all(promises) 485 + indexPopulated = true 493 486 } 487 + 488 + document.addEventListener("nav", async (e: CustomEventMap["nav"]) => { 489 + const currentSlug = e.detail.url 490 + const data = await fetchData 491 + const searchElement = document.querySelectorAll(".search") 492 + for (const element of searchElement) { 493 + await setupSearch(element, currentSlug, data) 494 + } 495 + })
+6 -6
quartz/components/styles/search.scss
··· 42 42 } 43 43 } 44 44 45 - & > #search-container { 45 + & > .search-container { 46 46 position: fixed; 47 47 contain: layout; 48 48 z-index: 999; ··· 58 58 display: inline-block; 59 59 } 60 60 61 - & > #search-space { 61 + & > .search-space { 62 62 width: 65%; 63 63 margin-top: 12vh; 64 64 margin-left: auto; ··· 91 91 } 92 92 } 93 93 94 - & > #search-layout { 94 + & > .search-layout { 95 95 display: none; 96 96 flex-direction: row; 97 97 border: 1px solid var(--lightgray); ··· 102 102 display: flex; 103 103 } 104 104 105 - &[data-preview] > #results-container { 105 + &[data-preview] > .results-container { 106 106 flex: 0 0 min(30%, 450px); 107 107 } 108 108 ··· 150 150 scroll-margin-top: 2rem; 151 151 } 152 152 153 - & > #preview-container { 153 + & > .preview-container { 154 154 flex-grow: 1; 155 155 display: block; 156 156 overflow: hidden; ··· 171 171 } 172 172 } 173 173 174 - & > #results-container { 174 + & > .results-container { 175 175 overflow-y: auto; 176 176 177 177 & .result-card {