From 6d5491fdcbccfad7af6c6dcc63ce2f67abd3850c Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Sat, 17 Jun 2023 19:07:40 +0000
Subject: [PATCH] collapsible toc
---
quartz/plugins/types.ts | 2
quartz/plugins/transformers/latex.ts | 28 +++--
quartz/components/Body.tsx | 4
quartz/components/scripts/clipboard.inline.ts | 50 +++++----
index.d.ts | 2
quartz/components/Content.tsx | 2
quartz/components/styles/legacyToc.scss | 2
quartz/components/scripts/spa.inline.ts | 4
quartz/components/scripts/toc.inline.ts | 35 +++++++
quartz/plugins/index.ts | 13 +
quartz/plugins/transformers/ofm.ts | 17 ++-
quartz/components/TableOfContents.tsx | 80 +++++++--------
quartz/components/styles/toc.scss | 49 ++++++---
quartz/plugins/emitters/contentPage.tsx | 2
14 files changed, 176 insertions(+), 114 deletions(-)
diff --git a/index.d.ts b/index.d.ts
index ec4d32a..26ca700 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -5,7 +5,7 @@
// dom custom event
interface CustomEventMap {
- "spa_nav": CustomEvent<{ url: string }>;
+ "nav": CustomEvent<{ url: string }>;
}
declare global {
diff --git a/quartz/components/Body.tsx b/quartz/components/Body.tsx
index f10cf3a..6b1f234 100644
--- a/quartz/components/Body.tsx
+++ b/quartz/components/Body.tsx
@@ -4,9 +4,9 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
function Body({ children }: QuartzComponentProps) {
- return <article>
+ return <div id="quartz-body">
{children}
- </article>
+ </div>
}
Body.afterDOMLoaded = clipboardScript
diff --git a/quartz/components/Content.tsx b/quartz/components/Content.tsx
index 71d0f35..cc5d66a 100644
--- a/quartz/components/Content.tsx
+++ b/quartz/components/Content.tsx
@@ -5,7 +5,7 @@
function Content({ tree }: QuartzComponentProps) {
// @ts-ignore (preact makes it angry)
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
- return content
+ return <article>{content}</article>
}
export default (() => Content) satisfies QuartzComponentConstructor
diff --git a/quartz/components/TableOfContents.tsx b/quartz/components/TableOfContents.tsx
index afb8388..99e73e9 100644
--- a/quartz/components/TableOfContents.tsx
+++ b/quartz/components/TableOfContents.tsx
@@ -2,6 +2,9 @@
import legacyStyle from "./styles/legacyToc.scss"
import modernStyle from "./styles/toc.scss"
+// @ts-ignore
+import script from "./scripts/toc.inline"
+
interface Options {
layout: 'modern' | 'legacy'
}
@@ -10,56 +13,49 @@
layout: 'modern'
}
-export default ((opts?: Partial<Options>) => {
- const layout = opts?.layout ?? defaultOptions.layout
- function TableOfContents({ fileData }: QuartzComponentProps) {
- if (!fileData.toc) {
- return null
- }
+function TableOfContents({ fileData }: QuartzComponentProps) {
+ if (!fileData.toc) {
+ return null
+ }
- return <details class="toc" open>
- <summary><h3>Table of Contents</h3></summary>
+ return <>
+ <button type="button" id="toc">
+ <h3>Table of Contents</h3>
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="fold">
+ <polyline points="6 9 12 15 18 9"></polyline>
+ </svg>
+ </button>
+ <div id="toc-content">
<ul>
{fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
<a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>{tocEntry.text}</a>
</li>)}
</ul>
- </details>
- }
-
- TableOfContents.css = layout === "modern" ? modernStyle : legacyStyle
-
- if (layout === "modern") {
- TableOfContents.afterDOMLoaded = `
-const bufferPx = 150
-const observer = new IntersectionObserver(entries => {
- for (const entry of entries) {
- const slug = entry.target.id
- const tocEntryElement = document.querySelector(\`a[data-for="$\{slug\}"]\`)
- const windowHeight = entry.rootBounds?.height
- if (windowHeight && tocEntryElement) {
- if (entry.boundingClientRect.y < windowHeight) {
- tocEntryElement.classList.add("in-view")
- } else {
- tocEntryElement.classList.remove("in-view")
- }
- }
- }
-})
-
-function init() {
- const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")
- headers.forEach(header => observer.observe(header))
+ </div>
+ </>
}
+TableOfContents.css = modernStyle
+TableOfContents.afterDOMLoaded = script
-init()
-
-document.addEventListener("spa_nav", (e) => {
- observer.disconnect()
- init()
-})
-`
+function LegacyTableOfContents({ fileData }: QuartzComponentProps) {
+ if (!fileData.toc) {
+ return null
}
- return TableOfContents
+ return <details id="toc" open>
+ <summary>
+ <h3>Table of Contents</h3>
+ </summary>
+ <ul>
+ {fileData.toc.map(tocEntry => <li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
+ <a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>{tocEntry.text}</a>
+ </li>)}
+ </ul>
+ </details>
+}
+LegacyTableOfContents.css = legacyStyle
+
+export default ((opts?: Partial<Options>) => {
+ const layout = opts?.layout ?? defaultOptions.layout
+ return layout === "modern" ? TableOfContents : LegacyTableOfContents
}) satisfies QuartzComponentConstructor
diff --git a/quartz/components/scripts/clipboard.inline.ts b/quartz/components/scripts/clipboard.inline.ts
index 8d0758a..76d1b58 100644
--- a/quartz/components/scripts/clipboard.inline.ts
+++ b/quartz/components/scripts/clipboard.inline.ts
@@ -3,27 +3,29 @@
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>'
-const els = document.getElementsByTagName("pre")
-for (let i = 0; i < els.length; i++) {
- const codeBlock = els[i].getElementsByTagName("code")[0]
- const source = codeBlock.innerText.replace(/\n\n/g, "\n")
- const button = document.createElement("button")
- button.className = "clipboard-button"
- button.type = "button"
- button.innerHTML = svgCopy
- button.ariaLabel = "Copy source"
- button.addEventListener("click", () => {
- navigator.clipboard.writeText(source).then(
- () => {
- button.blur()
- button.innerHTML = svgCheck
- setTimeout(() => {
- button.innerHTML = svgCopy
- button.style.borderColor = ""
- }, 2000)
- },
- (error) => console.error(error),
- )
- })
- els[i].prepend(button)
-}
+document.addEventListener("nav", () => {
+ const els = document.getElementsByTagName("pre")
+ for (let i = 0; i < els.length; i++) {
+ const codeBlock = els[i].getElementsByTagName("code")[0]
+ const source = codeBlock.innerText.replace(/\n\n/g, "\n")
+ const button = document.createElement("button")
+ button.className = "clipboard-button"
+ button.type = "button"
+ button.innerHTML = svgCopy
+ button.ariaLabel = "Copy source"
+ button.addEventListener("click", () => {
+ navigator.clipboard.writeText(source).then(
+ () => {
+ button.blur()
+ button.innerHTML = svgCheck
+ setTimeout(() => {
+ button.innerHTML = svgCopy
+ button.style.borderColor = ""
+ }, 2000)
+ },
+ (error) => console.error(error),
+ )
+ })
+ els[i].prepend(button)
+ }
+})
diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts
index da34700..a129dc4 100644
--- a/quartz/components/scripts/spa.inline.ts
+++ b/quartz/components/scripts/spa.inline.ts
@@ -30,7 +30,7 @@
}
function notifyNav(slug: string) {
- const event = new CustomEvent("spa_nav", { detail: { slug } })
+ const event = new CustomEvent("nav", { detail: { slug } })
document.dispatchEvent(event)
}
@@ -96,6 +96,7 @@
return
})
}
+
return new class Router {
go(pathname: string) {
const url = new URL(pathname, window.location.toString())
@@ -113,6 +114,7 @@
}
createRouter()
+notifyNav(document.body.dataset.slug!)
if (!customElements.get('route-announcer')) {
const attrs = {
diff --git a/quartz/components/scripts/toc.inline.ts b/quartz/components/scripts/toc.inline.ts
new file mode 100644
index 0000000..405a21f
--- /dev/null
+++ b/quartz/components/scripts/toc.inline.ts
@@ -0,0 +1,35 @@
+const bufferPx = 150
+const observer = new IntersectionObserver(entries => {
+ for (const entry of entries) {
+ const slug = entry.target.id
+ const tocEntryElement = document.querySelector(`a[data-for="${slug}"]`)
+ const windowHeight = entry.rootBounds?.height
+ if (windowHeight && tocEntryElement) {
+ if (entry.boundingClientRect.y < windowHeight) {
+ tocEntryElement.classList.add("in-view")
+ } else {
+ tocEntryElement.classList.remove("in-view")
+ }
+ }
+ }
+})
+
+function toggleCollapsible(this: HTMLElement) {
+ this.classList.toggle("collapsed")
+ const content = this.nextElementSibling as HTMLElement
+ content.classList.toggle("collapsed")
+ content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
+}
+
+document.addEventListener("nav", () => {
+ const toc = document.getElementById("toc")!
+ const content = toc.nextElementSibling as HTMLElement
+ content.style.maxHeight = content.scrollHeight + "px"
+ toc.removeEventListener("click", toggleCollapsible)
+ toc.addEventListener("click", toggleCollapsible)
+
+ // update toc entry highlighting
+ observer.disconnect()
+ const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")
+ headers.forEach(header => observer.observe(header))
+})
diff --git a/quartz/components/styles/legacyToc.scss b/quartz/components/styles/legacyToc.scss
index 33b9cca..8889dcc 100644
--- a/quartz/components/styles/legacyToc.scss
+++ b/quartz/components/styles/legacyToc.scss
@@ -1,4 +1,4 @@
-details.toc {
+details#toc {
& summary {
cursor: pointer;
diff --git a/quartz/components/styles/toc.scss b/quartz/components/styles/toc.scss
index 3003f40..1f1a27b 100644
--- a/quartz/components/styles/toc.scss
+++ b/quartz/components/styles/toc.scss
@@ -1,22 +1,36 @@
-details.toc {
- & summary {
- cursor: pointer;
+button#toc {
+ background-color: transparent;
+ border: none;
+ text-align: left;
+ cursor: pointer;
+ padding: 0;
+ color: var(--dark);
+ display: flex;
+ align-items: center;
- list-style: none;
- &::marker, &::-webkit-details-marker {
- display: none;
- }
-
- & > * {
- display: inline-block;
- margin: 0;
- }
-
- & > h3 {
- font-size: 1rem;
- }
+ & h3 {
+ font-size: 1rem;
+ display: inline-block;
+ margin: 0;
}
-
+
+ & .fold {
+ margin-left: 0.5rem;
+ transition: transform 0.3s ease;
+ opacity: 0.8;
+ }
+
+ &.collapsed .fold {
+ transform: rotateZ(-90deg)
+ }
+}
+
+#toc-content {
+ list-style: none;
+ overflow: hidden;
+ max-height: none;
+ transition: max-height 0.3s ease;
+
& ul {
list-style: none;
margin: 0.5rem 0;
@@ -37,3 +51,4 @@
}
}
}
+
diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx
index b6ded54..7afab9d 100644
--- a/quartz/plugins/emitters/contentPage.tsx
+++ b/quartz/plugins/emitters/contentPage.tsx
@@ -28,7 +28,7 @@
return {
name: "ContentPage",
getQuartzComponents() {
- return [opts.head, Header, ...opts.header, ...opts.body]
+ return [opts.head, Header, Body, ...opts.header, ...opts.body, ...opts.left, ...opts.right, ...opts.footer]
},
async emit(_contentDir, cfg, content, resources, emit): Promise<string[]> {
const fps: string[] = []
diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts
index 32f8bc7..04de0d4 100644
--- a/quartz/plugins/index.ts
+++ b/quartz/plugins/index.ts
@@ -33,10 +33,6 @@
afterDOMLoaded: []
}
- if (cfg.enableSPA) {
- componentResources.afterDOMLoaded.push(spaRouterScript)
- }
-
for (const component of allComponents) {
const { css, beforeDOMLoaded, afterDOMLoaded } = component
if (css) {
@@ -50,6 +46,15 @@
}
}
+ if (cfg.enableSPA) {
+ componentResources.afterDOMLoaded.push(spaRouterScript)
+ } else {
+ componentResources.afterDOMLoaded.push(`
+ const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } })
+ document.dispatchEvent(event)`
+ )
+ }
+
emit({
slug: "index",
ext: ".css",
diff --git a/quartz/plugins/transformers/latex.ts b/quartz/plugins/transformers/latex.ts
index 3140ab4..73bd07d 100644
--- a/quartz/plugins/transformers/latex.ts
+++ b/quartz/plugins/transformers/latex.ts
@@ -14,18 +14,20 @@
}]
]
},
- externalResources: {
- css: [
- // base css
- "https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css",
- ],
- js: [
- {
- // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md
- src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js",
- loadTime: "afterDOMReady",
- contentType: 'external'
- }
- ]
+ externalResources() {
+ return {
+ css: [
+ // base css
+ "https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css",
+ ],
+ js: [
+ {
+ // fix copy behaviour: https://github.com/KaTeX/KaTeX/blob/main/contrib/copy-tex/README.md
+ src: "https://cdn.jsdelivr.net/npm/katex@0.16.7/dist/contrib/copy-tex.min.js",
+ loadTime: "afterDOMReady",
+ contentType: 'external'
+ }
+ ]
+ }
}
})
diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts
index 1b4e07a..aa83953 100644
--- a/quartz/plugins/transformers/ofm.ts
+++ b/quartz/plugins/transformers/ofm.ts
@@ -6,6 +6,7 @@
import rehypeRaw from "rehype-raw"
import { visit } from "unist-util-visit"
import path from "path"
+import { JSResource } from "../../resources"
export interface Options {
highlight: boolean
@@ -235,6 +236,7 @@
node.children.splice(0, 1, ...blockquoteContent)
// add properties to base blockquote
+ // TODO: add the js to actually support collapsing callout
node.data = {
hProperties: {
...(node.data?.hProperties ?? {}),
@@ -270,16 +272,19 @@
htmlPlugins() {
return [rehypeRaw]
},
- externalResources: {
- js: [{
+ externalResources() {
+ const mermaidScript: JSResource = {
script: `
-import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';
-mermaid.initialize({ startOnLoad: true });
- `,
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.esm.min.mjs';
+ mermaid.initialize({ startOnLoad: true });
+ `,
loadTime: 'afterDOMReady',
moduleType: 'module',
contentType: 'inline'
- }]
+ }
+ return {
+ js: opts.mermaid ? [mermaidScript] : []
+ }
}
}
}
diff --git a/quartz/plugins/types.ts b/quartz/plugins/types.ts
index c67e41d..444fcff 100644
--- a/quartz/plugins/types.ts
+++ b/quartz/plugins/types.ts
@@ -16,7 +16,7 @@
name: string
markdownPlugins(): PluggableList
htmlPlugins(): PluggableList
- externalResources?: Partial<StaticResources>
+ externalResources?(): Partial<StaticResources>
}
export type QuartzFilterPlugin<Options extends OptionType = undefined> = (opts?: Options) => QuartzFilterPluginInstance
--
Gitblit v1.10.0