From 91189dfd2f4cb32e205117b327e0ae7a0c2dd716 Mon Sep 17 00:00:00 2001
From: Emile Bangma <github@emilebangma.com>
Date: Mon, 03 Feb 2025 14:25:42 +0000
Subject: [PATCH] feat(explorer): collapsible mobile explorer (#1471)
---
quartz/components/scripts/explorer.inline.ts | 155 +++++++++++++++++++++++++++++++++++++++------------
1 files changed, 118 insertions(+), 37 deletions(-)
diff --git a/quartz/components/scripts/explorer.inline.ts b/quartz/components/scripts/explorer.inline.ts
index 33d328a..9c6c050 100644
--- a/quartz/components/scripts/explorer.inline.ts
+++ b/quartz/components/scripts/explorer.inline.ts
@@ -1,7 +1,9 @@
import { FolderState } from "../ExplorerNode"
+// Current state of folders
type MaybeHTMLElement = HTMLElement | undefined
let currentExplorerState: FolderState[]
+
const observer = new IntersectionObserver((entries) => {
// If last element is observed, remove gradient of "overflow" class so element is visible
const explorerUl = document.getElementById("explorer-ul")
@@ -16,23 +18,43 @@
})
function toggleExplorer(this: HTMLElement) {
+ // Toggle collapsed state of entire explorer
this.classList.toggle("collapsed")
+
+ // Toggle collapsed aria state of entire explorer
this.setAttribute(
"aria-expanded",
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
)
- const content = this.nextElementSibling as MaybeHTMLElement
- if (!content) return
+ const content = (
+ this.nextElementSibling?.nextElementSibling
+ ? this.nextElementSibling.nextElementSibling
+ : this.nextElementSibling
+ ) as MaybeHTMLElement
+ if (!content) return
content.classList.toggle("collapsed")
+ content.classList.toggle("explorer-viewmode")
+
+ // Prevent scroll under
+ if (document.querySelector("#mobile-explorer")) {
+ // Disable scrolling on the page when the explorer is opened on mobile
+ const bodySelector = document.querySelector("#quartz-body")
+ if (bodySelector) bodySelector.classList.toggle("lock-scroll")
+ }
}
function toggleFolder(evt: MouseEvent) {
evt.stopPropagation()
+
+ // Element that was clicked
const target = evt.target as MaybeHTMLElement
if (!target) return
+ // Check if target was svg icon or button
const isSvg = target.nodeName === "svg"
+
+ // corresponding <ul> element relative to clicked button/folder
const childFolderContainer = (
isSvg
? target.parentElement?.nextSibling
@@ -42,10 +64,14 @@
isSvg ? target.nextElementSibling : target.parentElement
) as MaybeHTMLElement
if (!(childFolderContainer && currentFolderParent)) return
-
+ // <li> element of folder (stores folder-path dataset)
childFolderContainer.classList.toggle("open")
+
+ // Collapse folder container
const isCollapsed = childFolderContainer.classList.contains("open")
setFolderState(childFolderContainer, !isCollapsed)
+
+ // Save folder state to localStorage
const fullFolderPath = currentFolderParent.dataset.folderpath as string
toggleCollapsedByPath(currentExplorerState, fullFolderPath)
const stringifiedFileTree = JSON.stringify(currentExplorerState)
@@ -53,57 +79,106 @@
}
function setupExplorer() {
- const explorer = document.getElementById("explorer")
- if (!explorer) return
+ // Set click handler for collapsing entire explorer
+ const allExplorers = document.querySelectorAll(".explorer > button") as NodeListOf<HTMLElement>
- if (explorer.dataset.behavior === "collapse") {
+ for (const explorer of allExplorers) {
+ // Get folder state from local storage
+ const storageTree = localStorage.getItem("fileTree")
+
+ // Convert to bool
+ const useSavedFolderState = explorer?.dataset.savestate === "true"
+
+ if (explorer) {
+ // Get config
+ const collapseBehavior = explorer.dataset.behavior
+
+ // Add click handlers for all folders (click handler on folder "label")
+ if (collapseBehavior === "collapse") {
+ for (const item of document.getElementsByClassName(
+ "folder-button",
+ ) as HTMLCollectionOf<HTMLElement>) {
+ window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
+ item.addEventListener("click", toggleFolder)
+ }
+ }
+
+ // Add click handler to main explorer
+ window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
+ explorer.addEventListener("click", toggleExplorer)
+ }
+
+ // Set up click handlers for each folder (click handler on folder "icon")
for (const item of document.getElementsByClassName(
- "folder-button",
+ "folder-icon",
) as HTMLCollectionOf<HTMLElement>) {
item.addEventListener("click", toggleFolder)
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
}
+
+ // Get folder state from local storage
+ const oldExplorerState: FolderState[] =
+ storageTree && useSavedFolderState ? JSON.parse(storageTree) : []
+ const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed]))
+ const newExplorerState: FolderState[] = explorer.dataset.tree
+ ? JSON.parse(explorer.dataset.tree)
+ : []
+ currentExplorerState = []
+
+ for (const { path, collapsed } of newExplorerState) {
+ currentExplorerState.push({
+ path,
+ collapsed: oldIndex.get(path) ?? collapsed,
+ })
+ }
+
+ currentExplorerState.map((folderState) => {
+ const folderLi = document.querySelector(
+ `[data-folderpath='${folderState.path.replace("'", "-")}']`,
+ ) as MaybeHTMLElement
+ const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
+ if (folderUl) {
+ setFolderState(folderUl, folderState.collapsed)
+ }
+ })
}
+}
- explorer.addEventListener("click", toggleExplorer)
- window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
+function toggleExplorerFolders() {
+ const currentFile = (document.querySelector("body")?.getAttribute("data-slug") ?? "").replace(
+ /\/index$/g,
+ "",
+ )
+ const allFolders = document.querySelectorAll(".folder-outer")
- // Set up click handlers for each folder (click handler on folder "icon")
- for (const item of document.getElementsByClassName(
- "folder-icon",
- ) as HTMLCollectionOf<HTMLElement>) {
- item.addEventListener("click", toggleFolder)
- window.addCleanup(() => item.removeEventListener("click", toggleFolder))
- }
-
- // Get folder state from local storage
- const storageTree = localStorage.getItem("fileTree")
- const useSavedFolderState = explorer?.dataset.savestate === "true"
- const oldExplorerState: FolderState[] =
- storageTree && useSavedFolderState ? JSON.parse(storageTree) : []
- const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed]))
- const newExplorerState: FolderState[] = explorer.dataset.tree
- ? JSON.parse(explorer.dataset.tree)
- : []
- currentExplorerState = []
- for (const { path, collapsed } of newExplorerState) {
- currentExplorerState.push({ path, collapsed: oldIndex.get(path) ?? collapsed })
- }
-
- currentExplorerState.map((folderState) => {
- const folderLi = document.querySelector(
- `[data-folderpath='${folderState.path}']`,
- ) as MaybeHTMLElement
- const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
+ allFolders.forEach((element) => {
+ const folderUl = Array.from(element.children).find((child) =>
+ child.matches("ul[data-folderul]"),
+ )
if (folderUl) {
- setFolderState(folderUl, folderState.collapsed)
+ if (currentFile.includes(folderUl.getAttribute("data-folderul") ?? "")) {
+ if (!element.classList.contains("open")) {
+ element.classList.add("open")
+ }
+ }
}
})
}
window.addEventListener("resize", setupExplorer)
+
document.addEventListener("nav", () => {
+ const explorer = document.querySelector("#mobile-explorer")
+ if (explorer) {
+ explorer.classList.add("collapsed")
+ const content = explorer.nextElementSibling?.nextElementSibling as HTMLElement
+ if (content) {
+ content.classList.add("collapsed")
+ content.classList.toggle("explorer-viewmode")
+ }
+ }
setupExplorer()
+
observer.disconnect()
// select pseudo element at end of list
@@ -111,6 +186,12 @@
if (lastItem) {
observer.observe(lastItem)
}
+
+ // Hide explorer on mobile until it is requested
+ const hiddenUntilDoneLoading = document.querySelector("#mobile-explorer")
+ hiddenUntilDoneLoading?.classList.remove("hide-until-loaded")
+
+ toggleExplorerFolders()
})
/**
--
Gitblit v1.10.0