Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(explorer): collapsible mobile explorer #1471

Merged
merged 30 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a190cba
Rewrite mobile explorer
saberzero1 Oct 1, 2024
139573f
Finished mobile explorer
saberzero1 Oct 1, 2024
836f2a8
Fix folder/tag page
saberzero1 Oct 1, 2024
526e50a
Cleanup
saberzero1 Oct 1, 2024
7d404ac
Combine into single explorer tree
saberzero1 Oct 1, 2024
1e8e3e6
Restore mobile button position
saberzero1 Oct 1, 2024
60aae86
Merge branch 'jackyzha0:v4' into explorer
saberzero1 Oct 3, 2024
ad1a7d7
classNames usage
saberzero1 Oct 3, 2024
4e8e251
Clean up redundant code
saberzero1 Oct 3, 2024
0513ad4
Addressed feedback
saberzero1 Oct 3, 2024
5fd0f17
Cleanup
saberzero1 Oct 3, 2024
356647f
Restore position of functions
saberzero1 Oct 3, 2024
835f6a0
Revert Footer
saberzero1 Oct 3, 2024
26e0b43
Addressed feedback
saberzero1 Oct 3, 2024
40ca67c
Remove usePagePath
saberzero1 Oct 3, 2024
ff9be50
Prettier
saberzero1 Oct 3, 2024
7e41e10
Merge branch 'v4' into explorer
saberzero1 Oct 4, 2024
fda7b66
Update quartz/components/Footer.tsx
saberzero1 Oct 4, 2024
7416415
Merge branch 'v4' into explorer
saberzero1 Oct 9, 2024
316a993
Changed mobile explorer from dropdown to slide-in
saberzero1 Oct 25, 2024
979d653
Remove container div
saberzero1 Oct 26, 2024
90e984e
Restore position
saberzero1 Oct 26, 2024
c2e9477
Slide in + no ugly onload animation on mobile
saberzero1 Oct 26, 2024
8bb32c2
Cleanup
saberzero1 Oct 26, 2024
70cf007
Icon spacing
saberzero1 Nov 2, 2024
9b8dec0
Merge branch 'v4' into explorer
saberzero1 Feb 1, 2025
a1c36ec
SASS styling fixes
saberzero1 Feb 1, 2025
849e8ac
Remove unused parameter
saberzero1 Feb 1, 2025
9786ead
Copilot nitpick
saberzero1 Feb 1, 2025
e31527a
Prefer compatibility
saberzero1 Feb 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions quartz.layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const defaultContentPageLayout: PageLayout = {
Component.MobileOnly(Component.Spacer()),
Component.Search(),
Component.Darkmode(),
Component.DesktopOnly(Component.Explorer()),
Component.Explorer(),
],
right: [
Component.Graph(),
Expand All @@ -44,7 +44,7 @@ export const defaultListPageLayout: PageLayout = {
Component.MobileOnly(Component.Spacer()),
Component.Search(),
Component.Darkmode(),
Component.DesktopOnly(Component.Explorer()),
Component.Explorer(),
],
right: [],
}
38 changes: 33 additions & 5 deletions quartz/components/Explorer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import explorerStyle from "./styles/explorer.scss"
import style from "./styles/explorer.scss"

// @ts-ignore
import script from "./scripts/explorer.inline"
Expand Down Expand Up @@ -83,18 +83,46 @@ export default ((userOpts?: Partial<Options>) => {
lastBuildId = ctx.buildId
constructFileTree(allFiles)
}

return (
<div class={classNames(displayClass, "explorer")}>
<button
type="button"
id="explorer"
id="mobile-explorer"
class="collapsed hide-until-loaded"
data-behavior={opts.folderClickBehavior}
data-collapsed={opts.folderDefaultState}
data-savestate={opts.useSavedState}
data-tree={jsonTree}
data-mobile={true}
aria-controls="explorer-content"
aria-expanded={false}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-menu"
>
<line x1="4" x2="20" y1="12" y2="12" />
<line x1="4" x2="20" y1="6" y2="6" />
<line x1="4" x2="20" y1="18" y2="18" />
</svg>
</button>
<button
type="button"
id="desktop-explorer"
class="title-button"
data-behavior={opts.folderClickBehavior}
data-collapsed={opts.folderDefaultState}
data-savestate={opts.useSavedState}
data-tree={jsonTree}
data-mobile={false}
aria-controls="explorer-content"
aria-expanded={opts.folderDefaultState === "open"}
aria-expanded={true}
>
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
<svg
Expand Down Expand Up @@ -122,7 +150,7 @@ export default ((userOpts?: Partial<Options>) => {
)
}

Explorer.css = explorerStyle
Explorer.css = style
Explorer.afterDOMLoaded = script
return Explorer
}) satisfies QuartzComponentConstructor
151 changes: 116 additions & 35 deletions quartz/components/scripts/explorer.inline.ts
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -16,23 +18,43 @@ const observer = new IntersectionObserver((entries) => {
})

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
Expand All @@ -42,75 +64,134 @@ function toggleFolder(evt: MouseEvent) {
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)
localStorage.setItem("fileTree", stringifiedFileTree)
}

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>

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)
}

if (explorer.dataset.behavior === "collapse") {
// 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))
}
}

explorer.addEventListener("click", toggleExplorer)
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
// 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,
})
}

// 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))
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)
}
})
}
}

// 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 })
}
function toggleExplorerFolders() {
const currentFile = (document.querySelector("body")?.getAttribute("data-slug") ?? "").replace(
/\/index$/g,
"",
)
const allFolders = document.querySelectorAll(".folder-outer")

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
const lastItem = document.getElementById("explorer-end")
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()
})

/**
Expand Down
Loading