mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-19 10:54:06 -06:00
Add a new "render" event that is triggered post-encryption
This commit is contained in:
parent
54d739214a
commit
43cfbaae22
@ -45,8 +45,12 @@ This question is best answered by tracing what happens when a user (you!) runs `
|
||||
|
||||
1. The browser opens a Quartz page and loads the HTML. The `<head>` also links to page styles (emitted to `public/index.css`) and page-critical JS (emitted to `public/prescript.js`)
|
||||
2. Then, once the body is loaded, the browser loads the non-critical JS (emitted to `public/postscript.js`)
|
||||
3. Once the page is done loading, the page will then dispatch a custom synthetic browser event `"nav"`. This is used so client-side scripts declared by components can 'setup' anything that requires access to the page DOM.
|
||||
1. If the [[SPA Routing|enableSPA option]] is enabled in the [[configuration]], this `"nav"` event is also fired on any client-navigation to allow for components to unregister and reregister any event handlers and state.
|
||||
2. If it's not, we wire up the `"nav"` event to just be fired a single time after page load to allow for consistency across how state is setup across both SPA and non-SPA contexts.
|
||||
3. Once the page is done loading, the page will dispatch two custom synthetic browser events:
|
||||
1. **`"nav"`** event: Fired when the user navigates to a new page. This is used for navigation-specific logic like updating URL-dependent state, analytics tracking, etc.
|
||||
- Contains `e.detail.url` with the current page URL
|
||||
- Fired on initial page load and on client-side navigation (if [[SPA Routing|enableSPA option]] is enabled)
|
||||
2. **`"render"`** event: Fired when content needs to be processed or re-rendered. This is used for DOM manipulation, setting up event listeners, and other content-specific logic.
|
||||
- Contains `e.detail.htmlElement` with the DOM element that was updated
|
||||
- Fired on initial page load (with `document.body`) and whenever content is dynamically updated (e.g., in popovers, search results, after decryption)
|
||||
|
||||
The architecture and design of the plugin system was intentionally left pretty vague here as this is described in much more depth in the guide on [[making plugins|making your own plugin]].
|
||||
|
||||
@ -149,15 +149,46 @@ As the names suggest, the `.beforeDOMLoaded` scripts are executed _before_ the p
|
||||
|
||||
The `.afterDOMLoaded` script executes once the page has been completely loaded. This is a good place to setup anything that should last for the duration of a site visit (e.g. getting something saved from local storage).
|
||||
|
||||
If you need to create an `afterDOMLoaded` script that depends on _page specific_ elements that may change when navigating to a new page, you can listen for the `"nav"` event that gets fired whenever a page loads (which may happen on navigation if [[SPA Routing]] is enabled).
|
||||
If you need to create an `afterDOMLoaded` script that depends on _page specific_ elements that may change when navigating to a new page, you have two options:
|
||||
|
||||
**For navigation-specific logic**, listen for the `"nav"` event that gets fired whenever the user navigates to a new page:
|
||||
|
||||
```ts
|
||||
document.addEventListener("nav", () => {
|
||||
// do page specific logic here
|
||||
// e.g. attach event listeners
|
||||
const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
|
||||
toggleSwitch.addEventListener("change", switchTheme)
|
||||
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
|
||||
document.addEventListener("nav", (e) => {
|
||||
// runs only on page navigation
|
||||
// e.detail.url contains the new page URL
|
||||
const currentUrl = e.detail.url
|
||||
console.log(`Navigated to: ${currentUrl}`)
|
||||
})
|
||||
```
|
||||
|
||||
**For rendering/re-rendering content**, use the `"render"` event which is fired when content needs to be processed or updated:
|
||||
|
||||
```ts
|
||||
document.addEventListener("render", (e) => {
|
||||
// runs when content is rendered or re-rendered
|
||||
// e.detail.htmlElement contains the DOM element that was updated
|
||||
const container = e.detail.htmlElement
|
||||
|
||||
// attach event listeners to elements within this container
|
||||
const toggleSwitch = container.querySelector("#switch") as HTMLInputElement
|
||||
if (toggleSwitch) {
|
||||
toggleSwitch.addEventListener("change", switchTheme)
|
||||
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
You can also use the utility function from `"./util"` to simplify render event handling:
|
||||
|
||||
```ts
|
||||
import { addRenderListener } from "./util"
|
||||
|
||||
addRenderListener((container) => {
|
||||
// your rendering logic here
|
||||
// container is the DOM element that was updated
|
||||
const elements = container.querySelectorAll(".my-component")
|
||||
elements.forEach(setupElement)
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
171
docs/advanced/event system.md
Normal file
171
docs/advanced/event system.md
Normal file
@ -0,0 +1,171 @@
|
||||
---
|
||||
title: Event System
|
||||
---
|
||||
|
||||
Quartz uses a custom event system to coordinate between navigation and content rendering. Understanding these events is crucial for creating interactive components that work correctly with [[SPA Routing]].
|
||||
|
||||
## Event Types
|
||||
|
||||
### Navigation Event (`nav`)
|
||||
|
||||
The `nav` event is fired when the user navigates to a new page. This should be used for logic that needs to run once per page navigation.
|
||||
|
||||
```ts
|
||||
document.addEventListener("nav", (e: CustomEventMap["nav"]) => {
|
||||
// Access the current page URL
|
||||
const currentUrl = e.detail.url
|
||||
console.log(`User navigated to: ${currentUrl}`)
|
||||
|
||||
// Good for:
|
||||
// - Analytics tracking
|
||||
// - URL-dependent state updates
|
||||
// - Setting up page-level event handlers
|
||||
// - Theme/mode initialization
|
||||
})
|
||||
```
|
||||
|
||||
**When it fires:**
|
||||
- On initial page load
|
||||
- On client-side navigation (if SPA routing is enabled)
|
||||
- Does NOT fire on content re-renders
|
||||
|
||||
### Render Event (`render`)
|
||||
|
||||
The `render` event is fired when content needs to be processed or updated. This should be used for DOM manipulation and content-specific logic.
|
||||
|
||||
```ts
|
||||
document.addEventListener("render", (e: CustomEventMap["render"]) => {
|
||||
// Access the container that was updated
|
||||
const container = e.detail.htmlElement
|
||||
|
||||
// Process elements within this container
|
||||
const codeBlocks = container.querySelectorAll("pre code")
|
||||
codeBlocks.forEach(addSyntaxHighlighting)
|
||||
|
||||
// Good for:
|
||||
// - Setting up event listeners on new content
|
||||
// - Processing dynamic content (syntax highlighting, math rendering, etc.)
|
||||
// - Initializing interactive components
|
||||
})
|
||||
```
|
||||
|
||||
**When it fires:**
|
||||
- On initial page load (with `document.body` as the container)
|
||||
- When popover content is loaded
|
||||
- When search results are displayed
|
||||
- After content is decrypted
|
||||
- Whenever `dispatchRenderEvent()` is called
|
||||
|
||||
## Utility Functions
|
||||
|
||||
Quartz provides utility functions in `quartz/components/scripts/util.ts` to make working with these events easier:
|
||||
|
||||
### `addRenderListener(fn)`
|
||||
|
||||
A convenience function for listening to render events:
|
||||
|
||||
```ts
|
||||
import { addRenderListener } from "./util"
|
||||
|
||||
addRenderListener((container: HTMLElement) => {
|
||||
// Your rendering logic here
|
||||
// container is the DOM element that was updated
|
||||
const myElements = container.querySelectorAll(".my-component")
|
||||
myElements.forEach(setupMyComponent)
|
||||
})
|
||||
```
|
||||
|
||||
This is equivalent to manually adding a render event listener but with cleaner syntax.
|
||||
|
||||
### `dispatchRenderEvent(htmlElement)`
|
||||
|
||||
Triggers a render event for a specific DOM element:
|
||||
|
||||
```ts
|
||||
import { dispatchRenderEvent } from "./util"
|
||||
|
||||
// After dynamically creating or updating content
|
||||
const myContainer = document.getElementById("dynamic-content")
|
||||
// ... update the container content ...
|
||||
dispatchRenderEvent(myContainer)
|
||||
```
|
||||
|
||||
This will cause all render event listeners to process the specified container.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to use `nav` vs `render`
|
||||
|
||||
- **Use `nav` for:** Page-level setup, URL tracking, global state management
|
||||
- **Use `render` for:** Content processing, element-specific event handlers, DOM manipulation
|
||||
|
||||
### Memory Management
|
||||
|
||||
Always clean up event handlers to prevent memory leaks:
|
||||
|
||||
```ts
|
||||
addRenderListener((container) => {
|
||||
const buttons = container.querySelectorAll(".my-button")
|
||||
|
||||
const handleClick = (e) => { /* ... */ }
|
||||
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener("click", handleClick)
|
||||
// Clean up when navigating away
|
||||
window.addCleanup(() => {
|
||||
button.removeEventListener("click", handleClick)
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
The `window.addCleanup()` function ensures handlers are removed when navigating to a new page.
|
||||
|
||||
### Scoped Processing
|
||||
|
||||
Always scope your render logic to the provided container:
|
||||
|
||||
```ts
|
||||
// ✅ Good - only processes elements within the updated container
|
||||
addRenderListener((container) => {
|
||||
const elements = container.querySelectorAll(".my-element")
|
||||
elements.forEach(process)
|
||||
})
|
||||
|
||||
// ❌ Bad - processes all elements on the page
|
||||
addRenderListener((container) => {
|
||||
const elements = document.querySelectorAll(".my-element")
|
||||
elements.forEach(process)
|
||||
})
|
||||
```
|
||||
|
||||
This ensures your logic only runs on newly updated content and avoids duplicate processing.
|
||||
|
||||
## Migration from Old System
|
||||
|
||||
If you have existing code that used the old `rerender` flag pattern:
|
||||
|
||||
```ts
|
||||
// Old pattern ❌
|
||||
document.addEventListener("nav", (e) => {
|
||||
if (e.detail.rerender) return // Skip rerender events
|
||||
// ... setup logic
|
||||
})
|
||||
```
|
||||
|
||||
You should split this into separate event handlers:
|
||||
|
||||
```ts
|
||||
// New pattern ✅
|
||||
document.addEventListener("nav", (e) => {
|
||||
// Navigation-only logic
|
||||
updateURL(e.detail.url)
|
||||
})
|
||||
|
||||
addRenderListener((container) => {
|
||||
// Content rendering logic
|
||||
setupComponents(container)
|
||||
})
|
||||
```
|
||||
|
||||
This provides cleaner separation of concerns and better performance.
|
||||
3
index.d.ts
vendored
3
index.d.ts
vendored
@ -6,7 +6,8 @@ declare module "*.scss" {
|
||||
// dom custom event
|
||||
interface CustomEventMap {
|
||||
prenav: CustomEvent<{}>
|
||||
nav: CustomEvent<{ url: FullSlug; rerender?: boolean }>
|
||||
nav: CustomEvent<{ url: FullSlug }>
|
||||
render: CustomEvent<{ htmlElement: HTMLElement }>
|
||||
themechange: CustomEvent<{ theme: "light" | "dark" }>
|
||||
readermodechange: CustomEvent<{ mode: "on" | "off" }>
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { addRenderListener } from "./util"
|
||||
|
||||
function toggleCallout(this: HTMLElement) {
|
||||
const outerBlock = this.parentElement!
|
||||
outerBlock.classList.toggle("is-collapsed")
|
||||
@ -7,8 +9,8 @@ function toggleCallout(this: HTMLElement) {
|
||||
content.style.gridTemplateRows = collapsed ? "0fr" : "1fr"
|
||||
}
|
||||
|
||||
function setupCallout() {
|
||||
const collapsible = document.getElementsByClassName(
|
||||
function setupCallout(container: HTMLElement) {
|
||||
const collapsible = container.getElementsByClassName(
|
||||
`callout is-collapsible`,
|
||||
) as HTMLCollectionOf<HTMLElement>
|
||||
for (const div of collapsible) {
|
||||
@ -24,4 +26,4 @@ function setupCallout() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("nav", setupCallout)
|
||||
addRenderListener(setupCallout)
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { getFullSlug } from "../../util/path"
|
||||
import { addRenderListener } from "./util"
|
||||
|
||||
const checkboxId = (index: number) => `${getFullSlug(window)}-checkbox-${index}`
|
||||
|
||||
document.addEventListener("nav", () => {
|
||||
const checkboxes = document.querySelectorAll(
|
||||
addRenderListener((container: HTMLElement) => {
|
||||
const checkboxes = container.querySelectorAll(
|
||||
"input.checkbox-toggle",
|
||||
) as NodeListOf<HTMLInputElement>
|
||||
checkboxes.forEach((el, index) => {
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { addRenderListener } from "./util"
|
||||
|
||||
const svgCopy =
|
||||
'<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true"><path fill-rule="evenodd" d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path><path fill-rule="evenodd" d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"></path></svg>'
|
||||
const svgCheck =
|
||||
'<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true"><path fill-rule="evenodd" fill="rgb(63, 185, 80)" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path></svg>'
|
||||
|
||||
document.addEventListener("nav", () => {
|
||||
const els = document.getElementsByTagName("pre")
|
||||
addRenderListener((container: HTMLElement) => {
|
||||
const els = container.getElementsByTagName("pre")
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
const codeBlock = els[i].getElementsByTagName("code")[0]
|
||||
if (codeBlock) {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { addRenderListener } from "./util"
|
||||
|
||||
const changeTheme = (e: CustomEventMap["themechange"]) => {
|
||||
const theme = e.detail.theme
|
||||
const iframe = document.querySelector("iframe.giscus-frame") as HTMLIFrameElement
|
||||
@ -59,8 +61,8 @@ type GiscusElement = Omit<HTMLElement, "dataset"> & {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("nav", () => {
|
||||
const giscusContainer = document.querySelector(".giscus") as GiscusElement
|
||||
addRenderListener((container: HTMLElement) => {
|
||||
const giscusContainer = container.querySelector(".giscus") as GiscusElement
|
||||
if (!giscusContainer) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
EncryptionResult,
|
||||
} from "../../util/encryption"
|
||||
import { FullSlug, getFullSlug } from "../../util/path"
|
||||
import { addRenderListener, dispatchRenderEvent } from "./util"
|
||||
|
||||
const showLoading = (container: Element, show: boolean) => {
|
||||
const loadingDiv = container.querySelector(".decrypt-loading") as HTMLElement
|
||||
@ -78,6 +79,9 @@ const decryptWithPassword = async (
|
||||
contentWrapper.className = "decrypted-content-wrapper"
|
||||
contentWrapper.innerHTML = decryptedContent
|
||||
container.parentNode!.replaceChild(contentWrapper, container)
|
||||
// set data-decrypted of the original container to true
|
||||
containerElement.dataset.decrypted = "true"
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@ -121,13 +125,6 @@ const decryptWithPassword = async (
|
||||
}
|
||||
}
|
||||
|
||||
const notifyNav = (url: FullSlug) => {
|
||||
const event: CustomEventMap["nav"] = new CustomEvent("nav", {
|
||||
detail: { url, rerender: true },
|
||||
})
|
||||
document.dispatchEvent(event)
|
||||
}
|
||||
|
||||
function updateTitle(container: HTMLElement | null) {
|
||||
console.log(container)
|
||||
if (container) {
|
||||
@ -138,32 +135,22 @@ function updateTitle(container: HTMLElement | null) {
|
||||
}
|
||||
}
|
||||
|
||||
const tryAutoDecrypt = async (container: HTMLElement): Promise<boolean> => {
|
||||
const filePath = getFullSlug(window)
|
||||
const tryAutoDecrypt = async (parent: HTMLElement, container: HTMLElement): Promise<boolean> => {
|
||||
const fullSlug = container.dataset.slug as FullSlug
|
||||
const config = JSON.parse(container.dataset.config!) as EncryptionConfig
|
||||
const hash = JSON.parse(container.dataset.hash!) as Hash
|
||||
const parent =
|
||||
(container.closest(".popover") as HTMLElement) ||
|
||||
(container.closest(".preview-inner") as HTMLElement) ||
|
||||
(container.closest(".center") as HTMLElement) ||
|
||||
null
|
||||
|
||||
const password = await searchForValidPassword(fullSlug, hash, config)
|
||||
|
||||
if (password && (await decryptWithPassword(container, password, false))) {
|
||||
notifyNav(filePath)
|
||||
dispatchRenderEvent(parent)
|
||||
updateTitle(parent)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const manualDecrypt = async (container: HTMLElement) => {
|
||||
const parent =
|
||||
(container.closest(".preview-inner") as HTMLElement) ||
|
||||
(container.closest(".center") as HTMLElement) ||
|
||||
null
|
||||
const manualDecrypt = async (parent: HTMLElement, container: HTMLElement) => {
|
||||
const passwordInput = container.querySelector(".decrypt-password") as HTMLInputElement
|
||||
const password = passwordInput.value
|
||||
|
||||
@ -173,29 +160,28 @@ const manualDecrypt = async (container: HTMLElement) => {
|
||||
}
|
||||
|
||||
if (await decryptWithPassword(container, password, true)) {
|
||||
const filePath = getFullSlug(window)
|
||||
notifyNav(filePath)
|
||||
dispatchRenderEvent(parent)
|
||||
updateTitle(parent)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("nav", async () => {
|
||||
// Try auto-decryption for all encrypted content
|
||||
const encryptedElements = document.querySelectorAll(
|
||||
".encrypted-content",
|
||||
addRenderListener(async (element) => {
|
||||
// Try auto-decryption for all encrypted content with data-decrypted="false"
|
||||
const encryptedElements = element.querySelectorAll(
|
||||
".encrypted-content[data-decrypted='false']",
|
||||
) as NodeListOf<HTMLElement>
|
||||
|
||||
for (const encryptedContainer of encryptedElements) {
|
||||
await tryAutoDecrypt(encryptedContainer)
|
||||
await tryAutoDecrypt(element, encryptedContainer)
|
||||
}
|
||||
|
||||
// Manual decryption handlers
|
||||
const buttons = document.querySelectorAll(".decrypt-button")
|
||||
const buttons = element.querySelectorAll(".decrypt-button")
|
||||
|
||||
buttons.forEach((button) => {
|
||||
const handleClick = async function (this: HTMLElement) {
|
||||
const encryptedContainer = this.closest(".encrypted-content")!
|
||||
await manualDecrypt(encryptedContainer as HTMLElement)
|
||||
await manualDecrypt(element, encryptedContainer as HTMLElement)
|
||||
}
|
||||
|
||||
button.addEventListener("click", handleClick)
|
||||
@ -206,12 +192,12 @@ document.addEventListener("nav", async () => {
|
||||
})
|
||||
|
||||
// Enter key handler
|
||||
document.querySelectorAll(".decrypt-password").forEach((input) => {
|
||||
element.querySelectorAll(".decrypt-password").forEach((input) => {
|
||||
const handleKeypress = async function (this: HTMLInputElement, e: Event) {
|
||||
const keyEvent = e as KeyboardEvent
|
||||
if (keyEvent.key === "Enter") {
|
||||
const encryptedContainer = this.closest(".encrypted-content")!
|
||||
await manualDecrypt(encryptedContainer as HTMLElement)
|
||||
await manualDecrypt(element, encryptedContainer as HTMLElement)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { registerEscapeHandler, removeAllChildren } from "./util"
|
||||
import { registerEscapeHandler, removeAllChildren, addRenderListener } from "./util"
|
||||
|
||||
interface Position {
|
||||
x: number
|
||||
@ -144,8 +144,8 @@ const cssVars = [
|
||||
|
||||
let mermaidImport = undefined
|
||||
|
||||
document.addEventListener("nav", async () => {
|
||||
const nodes = document.querySelectorAll(
|
||||
addRenderListener(async (container: HTMLElement) => {
|
||||
const nodes = container.querySelectorAll(
|
||||
"code.mermaid:not([data-processed])",
|
||||
) as NodeListOf<HTMLElement>
|
||||
if (nodes.length === 0) return
|
||||
|
||||
@ -1,17 +1,10 @@
|
||||
import { computePosition, flip, inline, shift } from "@floating-ui/dom"
|
||||
import { FullSlug, normalizeRelativeURLs } from "../../util/path"
|
||||
import { fetchCanonical } from "./util"
|
||||
import { fetchCanonical, dispatchRenderEvent, addRenderListener } from "./util"
|
||||
|
||||
const p = new DOMParser()
|
||||
let activeAnchor: HTMLAnchorElement | null = null
|
||||
|
||||
function notifyNav(url: FullSlug) {
|
||||
const event: CustomEventMap["nav"] = new CustomEvent("nav", {
|
||||
detail: { url, rerender: true },
|
||||
})
|
||||
document.dispatchEvent(event)
|
||||
}
|
||||
|
||||
async function mouseEnterHandler(
|
||||
this: HTMLAnchorElement,
|
||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||
@ -45,7 +38,7 @@ async function mouseEnterHandler(
|
||||
}
|
||||
}
|
||||
|
||||
notifyNav(link.getAttribute("href") as FullSlug)
|
||||
dispatchRenderEvent(popoverInner)
|
||||
}
|
||||
|
||||
const targetUrl = new URL(link.href)
|
||||
@ -129,8 +122,9 @@ function clearActivePopover() {
|
||||
allPopoverElements.forEach((popoverElement) => popoverElement.classList.remove("active-popover"))
|
||||
}
|
||||
|
||||
document.addEventListener("nav", () => {
|
||||
const links = [...document.querySelectorAll("a.internal")] as HTMLAnchorElement[]
|
||||
addRenderListener((element) => {
|
||||
const links = [...element.querySelectorAll("a.internal")] as HTMLAnchorElement[]
|
||||
|
||||
for (const link of links) {
|
||||
link.addEventListener("mouseenter", mouseEnterHandler)
|
||||
link.addEventListener("mouseleave", clearActivePopover)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import FlexSearch from "flexsearch"
|
||||
import { registerEscapeHandler, removeAllChildren } from "./util"
|
||||
import { registerEscapeHandler, removeAllChildren, dispatchRenderEvent } from "./util"
|
||||
import { FullSlug, normalizeRelativeURLs, resolveRelative } from "../../util/path"
|
||||
import { contentDecryptedEventListener, decryptContent } from "../../util/encryption"
|
||||
|
||||
@ -143,13 +143,6 @@ function highlightHTML(searchTerm: string, el: HTMLElement) {
|
||||
return html.body
|
||||
}
|
||||
|
||||
function notifyNav(url: FullSlug) {
|
||||
const event: CustomEventMap["nav"] = new CustomEvent("nav", {
|
||||
detail: { url, rerender: true },
|
||||
})
|
||||
document.dispatchEvent(event)
|
||||
}
|
||||
|
||||
async function setupSearch(searchElement: Element, currentSlug: FullSlug, data: ContentIndex) {
|
||||
const container = searchElement.querySelector(".search-container") as HTMLElement
|
||||
if (!container) return
|
||||
@ -274,10 +267,8 @@ async function setupSearch(searchElement: Element, currentSlug: FullSlug, data:
|
||||
const slug = idDataMap[id]
|
||||
let title = data[slug].title
|
||||
|
||||
if (data[slug].decrypted === false) {
|
||||
title = "🔒 " + title
|
||||
} else if (data[slug].decrypted === true) {
|
||||
title = "🔓 " + title
|
||||
if (data[slug].encryptionResult) {
|
||||
title = (data[slug].decrypted ? "🔓 " : "🔒 ") + data[slug].title
|
||||
}
|
||||
|
||||
return {
|
||||
@ -406,7 +397,7 @@ async function setupSearch(searchElement: Element, currentSlug: FullSlug, data:
|
||||
highlights[0]?.scrollIntoView({ block: "start" })
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
notifyNav(slug)
|
||||
dispatchRenderEvent(previewInner)
|
||||
}
|
||||
|
||||
async function onType(e: HTMLElementEventMap["input"]) {
|
||||
@ -544,9 +535,6 @@ async function fillDocument(data: ContentIndex) {
|
||||
}
|
||||
|
||||
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||
// Ignore rerender events
|
||||
if (e.detail.rerender) return
|
||||
|
||||
const currentSlug = e.detail.url
|
||||
const data = await fetchData
|
||||
const searchElement = document.getElementsByClassName("search")
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import micromorph from "micromorph"
|
||||
import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path"
|
||||
import { fetchCanonical } from "./util"
|
||||
import { fetchCanonical, dispatchRenderEvent } from "./util"
|
||||
|
||||
// adapted from `micromorph`
|
||||
// https://github.com/natemoo-re/micromorph
|
||||
@ -38,6 +38,9 @@ const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined
|
||||
function notifyNav(url: FullSlug) {
|
||||
const event: CustomEventMap["nav"] = new CustomEvent("nav", { detail: { url } })
|
||||
document.dispatchEvent(event)
|
||||
|
||||
// Also trigger render event for the whole document body
|
||||
dispatchRenderEvent(document.body)
|
||||
}
|
||||
|
||||
const cleanupFns: Set<(...args: any[]) => void> = new Set()
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { addRenderListener } from "./util"
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const slug = entry.target.id
|
||||
@ -34,11 +36,11 @@ function setupToc() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("nav", () => {
|
||||
addRenderListener((container: HTMLElement) => {
|
||||
setupToc()
|
||||
|
||||
// update toc entry highlighting
|
||||
observer.disconnect()
|
||||
const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")
|
||||
const headers = container.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")
|
||||
headers.forEach((header) => observer.observe(header))
|
||||
})
|
||||
|
||||
@ -44,3 +44,16 @@ export async function fetchCanonical(url: URL): Promise<Response> {
|
||||
const [_, redirect] = text.match(canonicalRegex) ?? []
|
||||
return redirect ? fetch(`${new URL(redirect, url)}`) : res
|
||||
}
|
||||
|
||||
export function addRenderListener(renderFn: (container: HTMLElement) => void) {
|
||||
document.addEventListener("render", (e: CustomEventMap["render"]) => {
|
||||
renderFn(e.detail.htmlElement)
|
||||
})
|
||||
}
|
||||
|
||||
export function dispatchRenderEvent(htmlElement: HTMLElement) {
|
||||
const event: CustomEventMap["render"] = new CustomEvent("render", {
|
||||
detail: { htmlElement },
|
||||
})
|
||||
document.dispatchEvent(event)
|
||||
}
|
||||
|
||||
@ -6,10 +6,11 @@
|
||||
justify-content: center;
|
||||
|
||||
.encryption-notice {
|
||||
background: color-mix(in srgb, var(--lightgray) 60%, var(--light));
|
||||
/* background: color-mix(in srgb, var(--lightgray) 60%, var(--light));
|
||||
*/
|
||||
border: 1px solid var(--lightgray);
|
||||
padding: 2rem 1.5rem 2rem;
|
||||
border-radius: 5px;
|
||||
padding: 2rem 1.5rem;
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
@ -29,10 +30,12 @@
|
||||
color: var(--darkgray);
|
||||
line-height: 1.5;
|
||||
font-size: 0.95rem;
|
||||
margin: 0 0 1rem 0;
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.decrypt-form {
|
||||
margin: 1rem 0 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
@ -88,16 +91,6 @@
|
||||
font-size: 16px; // Prevent zoom on iOS
|
||||
}
|
||||
}
|
||||
|
||||
.encrypted-message-footnote {
|
||||
color: var(--gray);
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.8;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.decrypt-loading {
|
||||
@ -144,12 +137,18 @@
|
||||
}
|
||||
|
||||
// Hide the decrypt form when encrypted content appears in popover and search
|
||||
.popover .encrypted-content .encryption-notice .decrypt-form {
|
||||
display: none;
|
||||
}
|
||||
.search-space, .popover {
|
||||
.encrypted-content .encryption-notice .decrypt-form {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-space .encrypted-content .encryption-notice .decrypt-form {
|
||||
display: none;
|
||||
.encryption-notice {
|
||||
/*
|
||||
border: 0;
|
||||
padding: 0;
|
||||
*/
|
||||
margin: 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.decrypted-content-wrapper {
|
||||
|
||||
@ -211,6 +211,8 @@ function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentReso
|
||||
window.addCleanup = () => {}
|
||||
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
|
||||
document.dispatchEvent(event)
|
||||
const renderEvent = new CustomEvent("render", { detail: { htmlElement: document.body } })
|
||||
document.dispatchEvent(renderEvent)
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,6 +117,7 @@ export const Encrypt: QuartzTransformerPlugin<Partial<Options>> = (userOpts) =>
|
||||
`data-encrypted='${JSON.stringify(encryptionResult)}'`,
|
||||
`data-hash='${JSON.stringify(file.data.hash)}'`,
|
||||
`data-slug='${file.data.slug}'`,
|
||||
`data-decrypted='false'`,
|
||||
].join(" ")
|
||||
|
||||
// Create a new tree with encrypted content placeholder
|
||||
|
||||
Loading…
Reference in New Issue
Block a user