Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34a8dfcd55 | ||
|
|
44da82467e | ||
|
|
3231ce6e79 | ||
|
|
a0b927da4a | ||
|
|
5ab922f316 | ||
|
|
d11a0e71a8 | ||
|
|
2b57a68e1f | ||
|
|
18cd58617d | ||
|
|
ee868b2d79 | ||
|
|
5a36e5b68d | ||
|
|
0416c03ae6 | ||
|
|
3b596c9311 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"version": "4.2.0",
|
"version": "4.2.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"version": "4.2.0",
|
"version": "4.2.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.7.0",
|
"@clack/prompts": "^0.7.0",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "@jackyzha0/quartz",
|
"name": "@jackyzha0/quartz",
|
||||||
"description": "🌱 publish your digital garden and notes as a website",
|
"description": "🌱 publish your digital garden and notes as a website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "4.2.0",
|
"version": "4.2.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
"author": "jackyzha0 <j.zhao2k19@gmail.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -15,10 +15,30 @@ interface Item {
|
|||||||
type SearchType = "basic" | "tags"
|
type SearchType = "basic" | "tags"
|
||||||
let searchType: SearchType = "basic"
|
let searchType: SearchType = "basic"
|
||||||
let currentSearchTerm: string = ""
|
let currentSearchTerm: string = ""
|
||||||
let index: FlexSearch.Document<Item> | undefined = undefined
|
|
||||||
const p = new DOMParser()
|
|
||||||
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
|
||||||
|
let index = new FlexSearch.Document<Item>({
|
||||||
|
charset: "latin:extra",
|
||||||
|
encode: encoder,
|
||||||
|
document: {
|
||||||
|
id: "id",
|
||||||
|
index: [
|
||||||
|
{
|
||||||
|
field: "title",
|
||||||
|
tokenize: "forward",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "content",
|
||||||
|
tokenize: "forward",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "tags",
|
||||||
|
tokenize: "forward",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const p = new DOMParser()
|
||||||
const fetchContentCache: Map<FullSlug, Element[]> = new Map()
|
const fetchContentCache: Map<FullSlug, Element[]> = new Map()
|
||||||
const contextWindowWords = 30
|
const contextWindowWords = 30
|
||||||
const numSearchResults = 8
|
const numSearchResults = 8
|
||||||
@@ -81,10 +101,10 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
|
|||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightHTML(searchTerm: string, innerHTML: string) {
|
function highlightHTML(searchTerm: string, el: HTMLElement) {
|
||||||
const p = new DOMParser()
|
const p = new DOMParser()
|
||||||
const tokenizedTerms = tokenizeTerm(searchTerm)
|
const tokenizedTerms = tokenizeTerm(searchTerm)
|
||||||
const html = p.parseFromString(innerHTML, "text/html")
|
const html = p.parseFromString(el.innerHTML, "text/html")
|
||||||
|
|
||||||
const createHighlightSpan = (text: string) => {
|
const createHighlightSpan = (text: string) => {
|
||||||
const span = document.createElement("span")
|
const span = document.createElement("span")
|
||||||
@@ -168,7 +188,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
removeAllChildren(preview)
|
removeAllChildren(preview)
|
||||||
}
|
}
|
||||||
if (searchLayout) {
|
if (searchLayout) {
|
||||||
searchLayout.style.visibility = "hidden"
|
searchLayout.classList.remove("display-results")
|
||||||
}
|
}
|
||||||
|
|
||||||
searchType = "basic" // reset search type after closing
|
searchType = "basic" // reset search type after closing
|
||||||
@@ -204,6 +224,7 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
|
|
||||||
if (currentHover) {
|
if (currentHover) {
|
||||||
currentHover.classList.remove("focus")
|
currentHover.classList.remove("focus")
|
||||||
|
currentHover.blur()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If search is active, then we will render the first result and display accordingly
|
// If search is active, then we will render the first result and display accordingly
|
||||||
@@ -230,9 +251,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
: (document.activeElement as HTMLInputElement | null)
|
: (document.activeElement as HTMLInputElement | null)
|
||||||
const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null
|
const prevResult = currentResult?.previousElementSibling as HTMLInputElement | null
|
||||||
currentResult?.classList.remove("focus")
|
currentResult?.classList.remove("focus")
|
||||||
await displayPreview(prevResult)
|
|
||||||
prevResult?.focus()
|
prevResult?.focus()
|
||||||
currentHover = prevResult
|
currentHover = prevResult
|
||||||
|
await displayPreview(prevResult)
|
||||||
}
|
}
|
||||||
} else if (e.key === "ArrowDown" || e.key === "Tab") {
|
} else if (e.key === "ArrowDown" || e.key === "Tab") {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -244,9 +265,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
: (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null)
|
: (document.getElementsByClassName("result-card")[0] as HTMLInputElement | null)
|
||||||
const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null
|
const secondResult = firstResult?.nextElementSibling as HTMLInputElement | null
|
||||||
firstResult?.classList.remove("focus")
|
firstResult?.classList.remove("focus")
|
||||||
await displayPreview(secondResult)
|
|
||||||
secondResult?.focus()
|
secondResult?.focus()
|
||||||
currentHover = secondResult
|
currentHover = secondResult
|
||||||
|
await displayPreview(secondResult)
|
||||||
} else {
|
} else {
|
||||||
// If an element in results-container already has focus, focus next one
|
// If an element in results-container already has focus, focus next one
|
||||||
const active = currentHover
|
const active = currentHover
|
||||||
@@ -254,9 +275,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
: (document.activeElement as HTMLInputElement | null)
|
: (document.activeElement as HTMLInputElement | null)
|
||||||
active?.classList.remove("focus")
|
active?.classList.remove("focus")
|
||||||
const nextResult = active?.nextElementSibling as HTMLInputElement | null
|
const nextResult = active?.nextElementSibling as HTMLInputElement | null
|
||||||
await displayPreview(nextResult)
|
|
||||||
nextResult?.focus()
|
nextResult?.focus()
|
||||||
currentHover = nextResult
|
currentHover = nextResult
|
||||||
|
await displayPreview(nextResult)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,9 +326,9 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
currentHover?.classList.remove("focus")
|
currentHover?.classList.remove("focus")
|
||||||
currentHover?.blur()
|
currentHover?.blur()
|
||||||
const target = ev.target as HTMLInputElement
|
const target = ev.target as HTMLInputElement
|
||||||
await displayPreview(target)
|
|
||||||
currentHover = target
|
currentHover = target
|
||||||
currentHover.classList.add("focus")
|
currentHover.classList.add("focus")
|
||||||
|
await displayPreview(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onMouseLeave(ev: MouseEvent) {
|
async function onMouseLeave(ev: MouseEvent) {
|
||||||
@@ -385,12 +406,11 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
async function displayPreview(el: HTMLElement | null) {
|
async function displayPreview(el: HTMLElement | null) {
|
||||||
if (!searchLayout || !enablePreview || !el || !preview) return
|
if (!searchLayout || !enablePreview || !el || !preview) return
|
||||||
const slug = el.id as FullSlug
|
const slug = el.id as FullSlug
|
||||||
el.classList.add("focus")
|
const innerDiv = await fetchContent(slug).then((contents) =>
|
||||||
|
contents.flatMap((el) => [...highlightHTML(currentSearchTerm, el as HTMLElement).children]),
|
||||||
|
)
|
||||||
previewInner = document.createElement("div")
|
previewInner = document.createElement("div")
|
||||||
previewInner.classList.add("preview-inner")
|
previewInner.classList.add("preview-inner")
|
||||||
const innerDiv = await fetchContent(slug).then((contents) =>
|
|
||||||
contents.map((el) => highlightHTML(currentSearchTerm, el.innerHTML)),
|
|
||||||
)
|
|
||||||
previewInner.append(...innerDiv)
|
previewInner.append(...innerDiv)
|
||||||
preview.replaceChildren(previewInner)
|
preview.replaceChildren(previewInner)
|
||||||
|
|
||||||
@@ -398,13 +418,13 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
const highlights = [...preview.querySelectorAll(".highlight")].sort(
|
const highlights = [...preview.querySelectorAll(".highlight")].sort(
|
||||||
(a, b) => b.innerHTML.length - a.innerHTML.length,
|
(a, b) => b.innerHTML.length - a.innerHTML.length,
|
||||||
)
|
)
|
||||||
highlights[0]?.scrollIntoView()
|
highlights[0]?.scrollIntoView({ block: "start" })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onType(e: HTMLElementEventMap["input"]) {
|
async function onType(e: HTMLElementEventMap["input"]) {
|
||||||
if (!searchLayout || !index) return
|
if (!searchLayout || !index) return
|
||||||
currentSearchTerm = (e.target as HTMLInputElement).value
|
currentSearchTerm = (e.target as HTMLInputElement).value
|
||||||
searchLayout.style.visibility = currentSearchTerm === "" ? "hidden" : "visible"
|
searchLayout.classList.toggle("display-results", currentSearchTerm !== "")
|
||||||
searchType = currentSearchTerm.startsWith("#") ? "tags" : "basic"
|
searchType = currentSearchTerm.startsWith("#") ? "tags" : "basic"
|
||||||
|
|
||||||
let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[]
|
let searchResults: FlexSearch.SimpleDocumentSearchResultSetUnit[]
|
||||||
@@ -444,8 +464,8 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
searchBar?.addEventListener("input", onType)
|
searchBar?.addEventListener("input", onType)
|
||||||
window.addCleanup(() => searchBar?.removeEventListener("input", onType))
|
window.addCleanup(() => searchBar?.removeEventListener("input", onType))
|
||||||
|
|
||||||
index ??= await fillDocument(data)
|
|
||||||
registerEscapeHandler(container, hideSearch)
|
registerEscapeHandler(container, hideSearch)
|
||||||
|
await fillDocument(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -454,37 +474,19 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
|||||||
* @param data data to fill index with
|
* @param data data to fill index with
|
||||||
*/
|
*/
|
||||||
async function fillDocument(data: { [key: FullSlug]: ContentDetails }) {
|
async function fillDocument(data: { [key: FullSlug]: ContentDetails }) {
|
||||||
const index = new FlexSearch.Document<Item>({
|
|
||||||
charset: "latin:extra",
|
|
||||||
encode: encoder,
|
|
||||||
document: {
|
|
||||||
id: "id",
|
|
||||||
index: [
|
|
||||||
{
|
|
||||||
field: "title",
|
|
||||||
tokenize: "forward",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "content",
|
|
||||||
tokenize: "forward",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: "tags",
|
|
||||||
tokenize: "forward",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
let id = 0
|
let id = 0
|
||||||
|
const promises: Array<Promise<unknown>> = []
|
||||||
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
||||||
await index.addAsync(id++, {
|
promises.push(
|
||||||
|
index.addAsync(id++, {
|
||||||
id,
|
id,
|
||||||
slug: slug as FullSlug,
|
slug: slug as FullSlug,
|
||||||
title: fileData.title,
|
title: fileData.title,
|
||||||
content: fileData.content,
|
content: fileData.content,
|
||||||
tags: fileData.tags,
|
tags: fileData.tags,
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return index
|
return await Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@use "../../styles/variables.scss" as *;
|
||||||
|
|
||||||
button#explorer {
|
button#explorer {
|
||||||
all: unset;
|
all: unset;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -85,7 +87,7 @@ svg {
|
|||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
font-family: var(--headerFont);
|
font-family: var(--headerFont);
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 600;
|
font-weight: $boldWeight;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@@ -110,7 +112,7 @@ svg {
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
font-weight: 600;
|
font-weight: $boldWeight;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -83,11 +83,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > #search-layout {
|
& > #search-layout {
|
||||||
display: flex;
|
display: none;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
visibility: hidden;
|
|
||||||
border: 1px solid var(--lightgray);
|
border: 1px solid var(--lightgray);
|
||||||
|
|
||||||
|
&.display-results {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
@media all and (min-width: $tabletBreakpoint) {
|
@media all and (min-width: $tabletBreakpoint) {
|
||||||
&[data-preview] {
|
&[data-preview] {
|
||||||
& .result-card > p.preview {
|
& .result-card > p.preview {
|
||||||
@@ -128,34 +131,27 @@
|
|||||||
& .highlight {
|
& .highlight {
|
||||||
background: color-mix(in srgb, var(--tertiary) 60%, transparent);
|
background: color-mix(in srgb, var(--tertiary) 60%, transparent);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
scroll-margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > #preview-container {
|
& > #preview-container {
|
||||||
display: block;
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
& .preview-inner {
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 1em;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-y: auto;
|
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
color: var(--dark);
|
color: var(--dark);
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
font-weight: 400;
|
font-weight: $normalWeight;
|
||||||
background: var(--light);
|
background: var(--light);
|
||||||
border-top-right-radius: 5px;
|
border-top-right-radius: 5px;
|
||||||
border-bottom-right-radius: 5px;
|
border-bottom-right-radius: 5px;
|
||||||
box-shadow:
|
overflow-y: auto;
|
||||||
0 14px 50px rgba(27, 33, 48, 0.12),
|
padding: 1rem;
|
||||||
0 10px 30px rgba(27, 33, 48, 0.16);
|
|
||||||
}
|
|
||||||
|
|
||||||
a.internal {
|
& .preview-inner {
|
||||||
background-color: none;
|
margin: 0 auto;
|
||||||
|
width: min($pageWidth, 100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +198,7 @@
|
|||||||
padding: 0.2rem 0.4rem;
|
padding: 0.2rem 0.4rem;
|
||||||
margin: 0 0.1rem;
|
margin: 0 0.1rem;
|
||||||
line-height: 1.4rem;
|
line-height: 1.4rem;
|
||||||
font-weight: bold;
|
font-weight: $boldWeight;
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
|
|
||||||
&.match-tag {
|
&.match-tag {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ ul,
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
font-weight: 600;
|
font-weight: $boldWeight;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
color: var(--secondary);
|
color: var(--secondary);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@use "./variables.scss" as *;
|
||||||
@use "sass:color";
|
@use "sass:color";
|
||||||
|
|
||||||
.callout {
|
.callout {
|
||||||
@@ -156,6 +157,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.callout-title-inner {
|
.callout-title-inner {
|
||||||
font-weight: 700;
|
font-weight: $boldWeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,5 @@ $tabletBreakpoint: 1000px;
|
|||||||
$sidePanelWidth: 380px;
|
$sidePanelWidth: 380px;
|
||||||
$topSpacing: 6rem;
|
$topSpacing: 6rem;
|
||||||
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth;
|
$fullPageWidth: $pageWidth + 2 * $sidePanelWidth;
|
||||||
|
$boldWeight: 700;
|
||||||
|
$normalWeight: 400;
|
||||||
|
|||||||
Reference in New Issue
Block a user