Skip to content

Commit 89bd419

Browse files
Fix: add Second Table of Contents component
Fixes Table of Contents fold button not working consistently due to instantiating the TOC twice in `quartz.layout` Still to fix: Explorer
1 parent 5c5dc94 commit 89bd419

File tree

7 files changed

+200
-3
lines changed

7 files changed

+200
-3
lines changed

quartz.layout.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const defaultContentPageLayout: PageLayout = {
4848
Component.ArticleTitle(),
4949
Component.ContentMeta(),
5050
Component.TagList(),
51-
Component.MobileOnly(Component.TableOfContents()),
51+
Component.MobileOnly(Component.TableOfContents2()),
5252
],
5353
left: [
5454
Component.PageTitle(),
@@ -73,7 +73,7 @@ export const defaultContentPageLayout: PageLayout = {
7373

7474
// components for pages that display lists of pages (e.g. tags or folders)
7575
export const defaultListPageLayout: PageLayout = {
76-
beforeBody: [Component.Breadcrumbs(), Component.ArticleTitle(), Component.ContentMeta(), Component.MobileOnly(Component.TableOfContents())],
76+
beforeBody: [Component.Breadcrumbs(), Component.ArticleTitle(), Component.ContentMeta(), Component.MobileOnly(Component.TableOfContents2())],
7777
left: [
7878
Component.PageTitle(),
7979
Component.MobileOnly(Component.Spacer()),
+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
2+
import legacyStyle from "./styles/legacyToc.scss"
3+
import modernStyle from "./styles/toc.scss"
4+
import { classNames } from "../util/lang"
5+
6+
// @ts-ignore
7+
import script from "./scripts/toc.inline"
8+
import { i18n } from "../i18n"
9+
10+
interface Options {
11+
layout: "modern" | "legacy"
12+
}
13+
14+
const defaultOptions: Options = {
15+
layout: "modern",
16+
}
17+
18+
const TableOfContents: QuartzComponent = ({
19+
fileData,
20+
displayClass,
21+
cfg,
22+
}: QuartzComponentProps) => {
23+
if (!fileData.toc) {
24+
return null
25+
}
26+
27+
return (
28+
<div class={classNames(displayClass, "toc")}>
29+
<button
30+
type="button"
31+
id="toc2"
32+
class={fileData.collapseToc ? "collapsed" : ""}
33+
aria-controls="toc-content"
34+
aria-expanded={!fileData.collapseToc}
35+
>
36+
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
37+
<svg
38+
xmlns="http://www.w3.org/2000/svg"
39+
width="24"
40+
height="24"
41+
viewBox="0 0 24 24"
42+
fill="none"
43+
stroke="currentColor"
44+
stroke-width="2"
45+
stroke-linecap="round"
46+
stroke-linejoin="round"
47+
class="fold"
48+
>
49+
<polyline points="6 9 12 15 18 9"></polyline>
50+
</svg>
51+
</button>
52+
<div id="toc-content" class={fileData.collapseToc ? "collapsed" : ""}>
53+
<ul class="overflow">
54+
{fileData.toc.map((tocEntry) => (
55+
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
56+
<a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
57+
{tocEntry.text}
58+
</a>
59+
</li>
60+
))}
61+
</ul>
62+
</div>
63+
</div>
64+
)
65+
}
66+
TableOfContents.css = modernStyle
67+
TableOfContents.afterDOMLoaded = script
68+
69+
const LegacyTableOfContents: QuartzComponent = ({ fileData, cfg }: QuartzComponentProps) => {
70+
if (!fileData.toc) {
71+
return null
72+
}
73+
return (
74+
<details id="toc" open={!fileData.collapseToc}>
75+
<summary>
76+
<h3>{i18n(cfg.locale).components.tableOfContents.title}</h3>
77+
</summary>
78+
<ul>
79+
{fileData.toc.map((tocEntry) => (
80+
<li key={tocEntry.slug} class={`depth-${tocEntry.depth}`}>
81+
<a href={`#${tocEntry.slug}`} data-for={tocEntry.slug}>
82+
{tocEntry.text}
83+
</a>
84+
</li>
85+
))}
86+
</ul>
87+
</details>
88+
)
89+
}
90+
LegacyTableOfContents.css = legacyStyle
91+
92+
export default ((opts?: Partial<Options>) => {
93+
const layout = opts?.layout ?? defaultOptions.layout
94+
return layout === "modern" ? TableOfContents : LegacyTableOfContents
95+
}) satisfies QuartzComponentConstructor

quartz/components/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import Breadcrumbs from "./Breadcrumbs"
2222
import Comments from "./Comments"
2323

2424
import LinksHeader from "./LinksHeader"
25+
import TableOfContents2 from "./TableOfContents2"
2526

2627
export {
2728
ArticleTitle,
@@ -47,4 +48,6 @@ export {
4748
Breadcrumbs,
4849
LinksHeader,
4950
Comments,
51+
52+
TableOfContents2,
5053
}

quartz/components/scripts/toc.inline.ts

+25
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,28 @@ document.addEventListener("nav", () => {
4747
const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")
4848
headers.forEach((header) => observer.observe(header))
4949
})
50+
51+
// MMW: toc2 for mobile
52+
53+
function setupToc2() {
54+
const toc = document.getElementById("toc2")
55+
if (toc) {
56+
const collapsed = toc.classList.contains("collapsed")
57+
const content = toc.nextElementSibling as HTMLElement | undefined
58+
if (!content) return
59+
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
60+
toc.addEventListener("click", toggleToc)
61+
window.addCleanup(() => toc.removeEventListener("click", toggleToc))
62+
}
63+
}
64+
65+
window.addEventListener("resize", setupToc2)
66+
document.addEventListener("nav", () => {
67+
setupToc2()
68+
69+
// update toc entry highlighting
70+
observer.disconnect()
71+
const headers = document.querySelectorAll("h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]")
72+
headers.forEach((header) => observer.observe(header))
73+
})
74+

quartz/components/styles/toc.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
button#toc {
1+
button#toc, button#toc2 {
22
background-color: transparent;
33
border: none;
44
text-align: left;

quartz/plugins/transformers/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ export { ObsidianFlavoredMarkdown } from "./ofm"
99
export { OxHugoFlavouredMarkdown } from "./oxhugofm"
1010
export { SyntaxHighlighting } from "./syntax"
1111
export { TableOfContents } from "./toc"
12+
export { TableOfContents2 } from "./toc2"
1213
export { HardLineBreaks } from "./linebreaks"

quartz/plugins/transformers/toc2.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { QuartzTransformerPlugin } from "../types"
2+
import { Root } from "mdast"
3+
import { visit } from "unist-util-visit"
4+
import { toString } from "mdast-util-to-string"
5+
import Slugger from "github-slugger"
6+
7+
export interface Options {
8+
maxDepth: 1 | 2 | 3 | 4 | 5 | 6
9+
minEntries: number
10+
showByDefault: boolean
11+
collapseByDefault: boolean
12+
}
13+
14+
const defaultOptions: Options = {
15+
maxDepth: 3,
16+
minEntries: 1,
17+
showByDefault: true,
18+
collapseByDefault: false,
19+
}
20+
21+
interface TocEntry {
22+
depth: number
23+
text: string
24+
slug: string // this is just the anchor (#some-slug), not the canonical slug
25+
}
26+
27+
const slugAnchor = new Slugger()
28+
export const TableOfContents2: QuartzTransformerPlugin<Partial<Options>> = (userOpts) => {
29+
const opts = { ...defaultOptions, ...userOpts }
30+
return {
31+
name: "TableOfContents",
32+
markdownPlugins() {
33+
return [
34+
() => {
35+
return async (tree: Root, file) => {
36+
const display = file.data.frontmatter?.enableToc ?? opts.showByDefault
37+
if (display) {
38+
slugAnchor.reset()
39+
const toc: TocEntry[] = []
40+
let highestDepth: number = opts.maxDepth
41+
visit(tree, "heading", (node) => {
42+
if (node.depth <= opts.maxDepth) {
43+
const text = toString(node)
44+
highestDepth = Math.min(highestDepth, node.depth)
45+
toc.push({
46+
depth: node.depth,
47+
text,
48+
slug: slugAnchor.slug(text),
49+
})
50+
}
51+
})
52+
53+
if (toc.length > 0 && toc.length > opts.minEntries) {
54+
file.data.toc = toc.map((entry) => ({
55+
...entry,
56+
depth: entry.depth - highestDepth,
57+
}))
58+
file.data.collapseToc = opts.collapseByDefault
59+
}
60+
}
61+
}
62+
},
63+
]
64+
},
65+
}
66+
}
67+
68+
declare module "vfile" {
69+
interface DataMap {
70+
toc: TocEntry[]
71+
collapseToc: boolean
72+
}
73+
}

0 commit comments

Comments
 (0)