feat: support non-singleton search
This commit is contained in:
		
							parent
							
								
									1cd8e7f0d5
								
							
						
					
					
						commit
						9c8fec06d2
					
				
							
								
								
									
										1
									
								
								index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								index.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -10,4 +10,5 @@ interface CustomEventMap { | ||||
|   themechange: CustomEvent<{ theme: "light" | "dark" }> | ||||
| } | ||||
| 
 | ||||
| type ContentIndex = Record<FullSlug, ContentDetails> | ||||
| declare const fetchData: Promise<ContentIndex> | ||||
|  | ||||
| @ -19,7 +19,7 @@ export default ((userOpts?: Partial<SearchOptions>) => { | ||||
|     const searchPlaceholder = i18n(cfg.locale).components.search.searchBarPlaceholder | ||||
|     return ( | ||||
|       <div class={classNames(displayClass, "search")}> | ||||
|         <button class="search-button" id="search-button"> | ||||
|         <button class="search-button"> | ||||
|           <p>{i18n(cfg.locale).components.search.title}</p> | ||||
|           <svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19.9 19.7"> | ||||
|             <title>Search</title> | ||||
| @ -29,17 +29,17 @@ export default ((userOpts?: Partial<SearchOptions>) => { | ||||
|             </g> | ||||
|           </svg> | ||||
|         </button> | ||||
|         <div id="search-container"> | ||||
|           <div id="search-space"> | ||||
|         <div class="search-container"> | ||||
|           <div class="search-space"> | ||||
|             <input | ||||
|               autocomplete="off" | ||||
|               id="search-bar" | ||||
|               class="search-bar" | ||||
|               name="search" | ||||
|               type="text" | ||||
|               aria-label={searchPlaceholder} | ||||
|               placeholder={searchPlaceholder} | ||||
|             /> | ||||
|             <div id="search-layout" data-preview={opts.enablePreview}></div> | ||||
|             <div class="search-layout" data-preview={opts.enablePreview}></div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
| @ -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 = `<a class="result-card no-match">
 | ||||
| @ -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<Promise<unknown>> = [] | ||||
|   for (const [slug, fileData] of Object.entries<ContentDetails>(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) | ||||
|   } | ||||
| }) | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user