From 5480269d38ffaff7ffd6576d9a9407430429fb2d Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Sun, 09 Mar 2025 21:58:26 +0000
Subject: [PATCH] perf(explorer): client side explorer (#1810)
---
quartz/components/Explorer.tsx | 152 ++++++++++++++++++++++++++------------------------
1 files changed, 79 insertions(+), 73 deletions(-)
diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx
index ac276a8..9c1fbdc 100644
--- a/quartz/components/Explorer.tsx
+++ b/quartz/components/Explorer.tsx
@@ -3,22 +3,34 @@
// @ts-ignore
import script from "./scripts/explorer.inline"
-import { ExplorerNode, FileNode, Options } from "./ExplorerNode"
-import { QuartzPluginData } from "../plugins/vfile"
import { classNames } from "../util/lang"
import { i18n } from "../i18n"
+import { FileTrieNode } from "../util/fileTrie"
+import OverflowList from "./OverflowList"
-// Options interface defined in `ExplorerNode` to avoid circular dependency
-const defaultOptions = {
- folderClickBehavior: "collapse",
+type OrderEntries = "sort" | "filter" | "map"
+
+export interface Options {
+ title?: string
+ folderDefaultState: "collapsed" | "open"
+ folderClickBehavior: "collapse" | "link"
+ useSavedState: boolean
+ sortFn: (a: FileTrieNode, b: FileTrieNode) => number
+ filterFn: (node: FileTrieNode) => boolean
+ mapFn: (node: FileTrieNode) => void
+ order: OrderEntries[]
+}
+
+const defaultOptions: Options = {
folderDefaultState: "collapsed",
+ folderClickBehavior: "collapse",
useSavedState: true,
mapFn: (node) => {
return node
},
sortFn: (a, b) => {
- // Sort order: folders first, then files. Sort folders and files alphabetically
- if ((!a.file && !b.file) || (a.file && b.file)) {
+ // Sort order: folders first, then files. Sort folders and files alphabeticall
+ if ((!a.isFolder && !b.isFolder) || (a.isFolder && b.isFolder)) {
// numeric: true: Whether numeric collation should be used, such that "1" < "2" < "10"
// sensitivity: "base": Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A
return a.displayName.localeCompare(b.displayName, undefined, {
@@ -27,75 +39,44 @@
})
}
- if (a.file && !b.file) {
+ if (!a.isFolder && b.isFolder) {
return 1
} else {
return -1
}
},
- filterFn: (node) => node.name !== "tags",
+ filterFn: (node) => node.slugSegment !== "tags",
order: ["filter", "map", "sort"],
-} satisfies Options
+}
+
+export type FolderState = {
+ path: string
+ collapsed: boolean
+}
export default ((userOpts?: Partial<Options>) => {
- // Parse config
const opts: Options = { ...defaultOptions, ...userOpts }
- // memoized
- let fileTree: FileNode
- let jsonTree: string
- let lastBuildId: string = ""
-
- function constructFileTree(allFiles: QuartzPluginData[]) {
- // Construct tree from allFiles
- fileTree = new FileNode("")
- allFiles.forEach((file) => fileTree.add(file))
-
- // Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied)
- if (opts.order) {
- // Order is important, use loop with index instead of order.map()
- for (let i = 0; i < opts.order.length; i++) {
- const functionName = opts.order[i]
- if (functionName === "map") {
- fileTree.map(opts.mapFn)
- } else if (functionName === "sort") {
- fileTree.sort(opts.sortFn)
- } else if (functionName === "filter") {
- fileTree.filter(opts.filterFn)
- }
- }
- }
-
- // Get all folders of tree. Initialize with collapsed state
- // Stringify to pass json tree as data attribute ([data-tree])
- const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
- jsonTree = JSON.stringify(folders)
- }
-
- const Explorer: QuartzComponent = ({
- ctx,
- cfg,
- allFiles,
- displayClass,
- fileData,
- }: QuartzComponentProps) => {
- if (ctx.buildId !== lastBuildId) {
- lastBuildId = ctx.buildId
- constructFileTree(allFiles)
- }
+ const Explorer: QuartzComponent = ({ cfg, displayClass }: QuartzComponentProps) => {
return (
- <div class={classNames(displayClass, "explorer")}>
+ <div
+ class={classNames(displayClass, "explorer")}
+ data-behavior={opts.folderClickBehavior}
+ data-collapsed={opts.folderDefaultState}
+ data-savestate={opts.useSavedState}
+ data-data-fns={JSON.stringify({
+ order: opts.order,
+ sortFn: opts.sortFn.toString(),
+ filterFn: opts.filterFn.toString(),
+ mapFn: opts.mapFn.toString(),
+ })}
+ >
<button
type="button"
id="mobile-explorer"
- class="collapsed hide-until-loaded"
- data-behavior={opts.folderClickBehavior}
- data-collapsed={opts.folderDefaultState}
- data-savestate={opts.useSavedState}
- data-tree={jsonTree}
+ class="explorer-toggle hide-until-loaded"
data-mobile={true}
aria-controls="explorer-content"
- aria-expanded={false}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -105,7 +86,7 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
- class="lucide lucide-menu"
+ class="lucide-menu"
>
<line x1="4" x2="20" y1="12" y2="12" />
<line x1="4" x2="20" y1="6" y2="6" />
@@ -115,13 +96,8 @@
<button
type="button"
id="desktop-explorer"
- class="title-button"
- data-behavior={opts.folderClickBehavior}
- data-collapsed={opts.folderDefaultState}
- data-savestate={opts.useSavedState}
- data-tree={jsonTree}
+ class="title-button explorer-toggle"
data-mobile={false}
- aria-controls="explorer-content"
aria-expanded={true}
>
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
@@ -140,17 +116,47 @@
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
- <div id="explorer-content">
- <ul class="overflow" id="explorer-ul">
- <ExplorerNode node={fileTree} opts={opts} fileData={fileData} />
- <li id="explorer-end" />
- </ul>
+ <div id="explorer-content" aria-expanded={false}>
+ <OverflowList id="explorer-ul" />
</div>
+ <template id="template-file">
+ <li>
+ <a href="#"></a>
+ </li>
+ </template>
+ <template id="template-folder">
+ <li>
+ <div class="folder-container">
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="12"
+ height="12"
+ viewBox="5 8 14 8"
+ fill="none"
+ stroke="currentColor"
+ stroke-width="2"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ class="folder-icon"
+ >
+ <polyline points="6 9 12 15 18 9"></polyline>
+ </svg>
+ <div>
+ <button class="folder-button">
+ <span class="folder-title"></span>
+ </button>
+ </div>
+ </div>
+ <div class="folder-outer">
+ <ul class="content"></ul>
+ </div>
+ </li>
+ </template>
</div>
)
}
Explorer.css = style
- Explorer.afterDOMLoaded = script
+ Explorer.afterDOMLoaded = script + OverflowList.afterDOMLoaded("explorer-ul")
return Explorer
}) satisfies QuartzComponentConstructor
--
Gitblit v1.10.0