mirror of
https://github.com/alrayyes/wiki.git
synced 2024-11-22 11:36:23 +00:00
* feat(search): telescope-style search Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * chore(search): cleanup some basis and borders Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * fix(search): make sure to set overflow-y Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * feat(search): shows preview on desktop only search Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * perf: add options to control layout through config cache memoize results to avoid fetching Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * chore: use the default configuration * fix: correct minor type for search Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * fix: use datasets to query for preview Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * chore: layout changes show preview on normal layout, and only show previous layout in list page. * fix(type): annotate search with types Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * chore: apply jacky's suggestion Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com> * chore: using map API and scss Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * fix: styling on search container view on phones Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> * Update quartz.layout.ts Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com> --------- Signed-off-by: Aaron <29749331+aarnphm@users.noreply.github.com> Co-authored-by: Jacky Zhao <j.zhao2k19@gmail.com>
This commit is contained in:
parent
4e5643fb49
commit
a29fadb046
3 changed files with 208 additions and 84 deletions
|
@ -4,8 +4,18 @@ import style from "./styles/search.scss"
|
|||
import script from "./scripts/search.inline"
|
||||
import { classNames } from "../util/lang"
|
||||
|
||||
export default (() => {
|
||||
export interface SearchOptions {
|
||||
enablePreview: boolean
|
||||
}
|
||||
|
||||
const defaultOptions: SearchOptions = {
|
||||
enablePreview: true,
|
||||
}
|
||||
|
||||
export default ((userOpts?: Partial<SearchOptions>) => {
|
||||
function Search({ displayClass }: QuartzComponentProps) {
|
||||
const opts = { ...defaultOptions, ...userOpts }
|
||||
|
||||
return (
|
||||
<div class={classNames(displayClass, "search")}>
|
||||
<div id="search-icon">
|
||||
|
@ -36,7 +46,7 @@ export default (() => {
|
|||
aria-label="Search for something"
|
||||
placeholder="Search for something"
|
||||
/>
|
||||
<div id="results-container"></div>
|
||||
<div id="search-layout" data-preview={opts.enablePreview}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import FlexSearch from "flexsearch"
|
||||
import { ContentDetails } from "../../plugins/emitters/contentIndex"
|
||||
import { registerEscapeHandler, removeAllChildren } from "./util"
|
||||
import { FullSlug, resolveRelative } from "../../util/path"
|
||||
import { FullSlug, normalizeRelativeURLs, resolveRelative } from "../../util/path"
|
||||
|
||||
interface Item {
|
||||
id: number
|
||||
|
@ -71,20 +71,44 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
|
|||
}`
|
||||
}
|
||||
|
||||
const p = new DOMParser()
|
||||
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
||||
let prevShortcutHandler: ((e: HTMLElementEventMap["keydown"]) => void) | undefined = undefined
|
||||
document.addEventListener("nav", async (e: unknown) => {
|
||||
const currentSlug = (e as CustomEventMap["nav"]).detail.url
|
||||
|
||||
const fetchContentCache: Map<FullSlug, Element[]> = new Map()
|
||||
|
||||
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 searchIcon = document.getElementById("search-icon")
|
||||
const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
|
||||
const results = document.getElementById("results-container")
|
||||
const searchLayout = document.getElementById("search-layout")
|
||||
const resultCards = document.getElementsByClassName("result-card")
|
||||
const idDataMap = Object.keys(data) as FullSlug[]
|
||||
|
||||
const appendLayout = (el: HTMLElement) => {
|
||||
if (searchLayout?.querySelector(`#${el.id}`) === null) {
|
||||
searchLayout?.appendChild(el)
|
||||
}
|
||||
}
|
||||
|
||||
const enablePreview = searchLayout?.dataset?.preview === "true"
|
||||
let preview: HTMLDivElement | undefined = undefined
|
||||
const results = document.createElement("div")
|
||||
results.id = "results-container"
|
||||
results.style.flexBasis = enablePreview ? "30%" : "100%"
|
||||
appendLayout(results)
|
||||
|
||||
if (enablePreview) {
|
||||
preview = document.createElement("div")
|
||||
preview.id = "preview-container"
|
||||
preview.style.flexBasis = "70%"
|
||||
appendLayout(preview)
|
||||
}
|
||||
|
||||
function hideSearch() {
|
||||
container?.classList.remove("active")
|
||||
if (searchBar) {
|
||||
|
@ -96,6 +120,9 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||
if (results) {
|
||||
removeAllChildren(results)
|
||||
}
|
||||
if (preview) {
|
||||
removeAllChildren(preview)
|
||||
}
|
||||
|
||||
searchType = "basic" // reset search type after closing
|
||||
}
|
||||
|
@ -109,7 +136,7 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||
searchBar?.focus()
|
||||
}
|
||||
|
||||
function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
||||
async function shortcutHandler(e: HTMLElementEventMap["keydown"]) {
|
||||
if (e.key === "k" && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
const searchBarOpen = container?.classList.contains("active")
|
||||
|
@ -139,6 +166,9 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||
if (results?.contains(document.activeElement)) {
|
||||
// If an element in results-container already has focus, focus previous one
|
||||
const prevResult = document.activeElement?.previousElementSibling as HTMLInputElement | null
|
||||
if (enablePreview && prevResult?.id) {
|
||||
await displayPreview(prevResult?.id as FullSlug)
|
||||
}
|
||||
prevResult?.focus()
|
||||
}
|
||||
} else if (e.key === "ArrowDown" || e.key === "Tab") {
|
||||
|
@ -146,10 +176,16 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||
// When first pressing ArrowDown, results wont contain the active element, so focus first element
|
||||
if (!results?.contains(document.activeElement)) {
|
||||
const firstResult = resultCards[0] as HTMLInputElement | null
|
||||
if (enablePreview && firstResult?.id) {
|
||||
await displayPreview(firstResult?.id as FullSlug)
|
||||
}
|
||||
firstResult?.focus()
|
||||
} else {
|
||||
// If an element in results-container already has focus, focus next one
|
||||
const nextResult = document.activeElement?.nextElementSibling as HTMLInputElement | null
|
||||
if (enablePreview && nextResult?.id) {
|
||||
await displayPreview(nextResult?.id as FullSlug)
|
||||
}
|
||||
nextResult?.focus()
|
||||
}
|
||||
}
|
||||
|
@ -220,13 +256,17 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||
}
|
||||
}
|
||||
|
||||
function resolveUrl(slug: FullSlug): URL {
|
||||
return new URL(resolveRelative(currentSlug, slug), location.toString())
|
||||
}
|
||||
|
||||
const resultToHTML = ({ slug, title, content, tags }: Item) => {
|
||||
const htmlTags = tags.length > 0 ? `<ul>${tags.join("")}</ul>` : ``
|
||||
const itemTile = document.createElement("a")
|
||||
itemTile.classList.add("result-card")
|
||||
itemTile.id = slug
|
||||
itemTile.href = new URL(resolveRelative(currentSlug, slug), location.toString()).toString()
|
||||
itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}<p>${content}</p>`
|
||||
itemTile.href = resolveUrl(slug).toString()
|
||||
itemTile.innerHTML = `<h3>${title}</h3>${htmlTags}${enablePreview && window.innerWidth > 600 ? "" : `<p>${content}</p>`}`
|
||||
itemTile.addEventListener("click", (event) => {
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
|
||||
hideSearch()
|
||||
|
@ -248,10 +288,47 @@ document.addEventListener("nav", async (e: unknown) => {
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchContent(slug: FullSlug): Promise<Element[]> {
|
||||
if (fetchContentCache.has(slug)) {
|
||||
return fetchContentCache.get(slug) as Element[]
|
||||
}
|
||||
|
||||
const targetUrl = resolveUrl(slug).toString()
|
||||
const contents = await fetch(targetUrl)
|
||||
.then((res) => res.text())
|
||||
.then((contents) => {
|
||||
if (contents === undefined) {
|
||||
throw new Error(`Could not fetch ${targetUrl}`)
|
||||
}
|
||||
const html = p.parseFromString(contents ?? "", "text/html")
|
||||
normalizeRelativeURLs(html, targetUrl)
|
||||
return [...html.getElementsByClassName("popover-hint")]
|
||||
})
|
||||
|
||||
fetchContentCache.set(slug, contents)
|
||||
return contents
|
||||
}
|
||||
|
||||
async function displayPreview(slug: FullSlug) {
|
||||
if (!searchLayout || !enablePreview) return
|
||||
|
||||
removeAllChildren(preview as HTMLElement)
|
||||
const contentDetails = await fetchContent(slug)
|
||||
|
||||
const previewInner = document.createElement("div")
|
||||
previewInner.classList.add("preview-inner")
|
||||
preview?.appendChild(previewInner)
|
||||
contentDetails?.forEach((elt) => previewInner.appendChild(elt))
|
||||
}
|
||||
|
||||
async function onType(e: HTMLElementEventMap["input"]) {
|
||||
let term = (e.target as HTMLInputElement).value
|
||||
let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[]
|
||||
|
||||
if (searchLayout) {
|
||||
searchLayout.style.opacity = "1"
|
||||
}
|
||||
|
||||
if (term.toLowerCase().startsWith("#")) {
|
||||
searchType = "tags"
|
||||
} else {
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
|
||||
& > #search-space {
|
||||
width: 50%;
|
||||
margin-top: 15vh;
|
||||
margin-top: 12vh;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
|
@ -86,93 +86,130 @@
|
|||
}
|
||||
}
|
||||
|
||||
& > #results-container {
|
||||
& .result-card {
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border: 1px solid var(--lightgray);
|
||||
border-bottom: none;
|
||||
width: 100%;
|
||||
& > #search-layout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
opacity: 0;
|
||||
|
||||
& > * {
|
||||
height: calc(75vh - 20em);
|
||||
background: none;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--lightgray); // Border to define the box
|
||||
}
|
||||
|
||||
@media all and (max-width: $mobileBreakpoint) {
|
||||
display: block;
|
||||
& > *:not(#results-container) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
& > #results-container {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
& > #preview-container {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
|
||||
// normalize card props
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
text-transform: none;
|
||||
text-align: left;
|
||||
background: var(--light);
|
||||
outline: none;
|
||||
font-weight: inherit;
|
||||
|
||||
& .highlight {
|
||||
color: var(--secondary);
|
||||
font-weight: 700;
|
||||
& .preview-inner {
|
||||
padding: 1em;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
font-family: inherit;
|
||||
font-size: 1.1em;
|
||||
color: var(--dark);
|
||||
line-height: 1.5em;
|
||||
font-weight: 400;
|
||||
background: var(--light);
|
||||
border-radius: 5px;
|
||||
box-shadow:
|
||||
0 14px 50px rgba(27, 33, 48, 0.12),
|
||||
0 10px 30px rgba(27, 33, 48, 0.16);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: var(--lightgray);
|
||||
}
|
||||
& > #results-container {
|
||||
overflow-y: auto;
|
||||
|
||||
&:first-of-type {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
& .result-card {
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
width: 100%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom: 1px solid var(--lightgray);
|
||||
}
|
||||
|
||||
& > h3 {
|
||||
// normalize card props
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
}
|
||||
text-transform: none;
|
||||
text-align: left;
|
||||
background: var(--light);
|
||||
outline: none;
|
||||
font-weight: inherit;
|
||||
|
||||
& > ul > li {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
& .highlight {
|
||||
color: var(--secondary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
& > ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
padding-left: 0;
|
||||
gap: 0.4rem;
|
||||
margin: 0;
|
||||
margin-top: 0.45rem;
|
||||
// Offset border radius
|
||||
margin-left: -2px;
|
||||
overflow: hidden;
|
||||
background-clip: border-box;
|
||||
}
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: var(--lightgray);
|
||||
}
|
||||
|
||||
& > ul > li > p {
|
||||
border-radius: 8px;
|
||||
background-color: var(--highlight);
|
||||
overflow: hidden;
|
||||
background-clip: border-box;
|
||||
padding: 0.03rem 0.4rem;
|
||||
margin: 0;
|
||||
color: var(--secondary);
|
||||
opacity: 0.85;
|
||||
}
|
||||
& > h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& > ul > li > .match-tag {
|
||||
color: var(--tertiary);
|
||||
font-weight: bold;
|
||||
opacity: 1;
|
||||
}
|
||||
& > ul > li {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
|
||||
& > p {
|
||||
margin-bottom: 0;
|
||||
& > ul {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
padding-left: 0;
|
||||
gap: 0.4rem;
|
||||
margin: 0;
|
||||
margin-top: 0.45rem;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
background-clip: border-box;
|
||||
}
|
||||
|
||||
& > ul > li > p {
|
||||
border-radius: 8px;
|
||||
background-color: var(--highlight);
|
||||
overflow: hidden;
|
||||
background-clip: border-box;
|
||||
padding: 0.03rem 0.4rem;
|
||||
margin: 0;
|
||||
color: var(--secondary);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
& > ul > li > .match-tag {
|
||||
color: var(--tertiary);
|
||||
font-weight: bold;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
& > p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue