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

Open
wants to merge 25 commits into
base: v4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
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()),
aarnphm marked this conversation as resolved.
Show resolved Hide resolved
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: [],
}
98 changes: 64 additions & 34 deletions quartz/components/Explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default ((userOpts?: Partial<Options>) => {
let jsonTree: string
let lastBuildId: string = ""

function constructFileTree(allFiles: QuartzPluginData[]) {
function constructFileTree(allFiles: QuartzPluginData[], currentFilePath: string) {
// Construct tree from allFiles
fileTree = new FileNode("")
allFiles.forEach((file) => fileTree.add(file))
Expand Down Expand Up @@ -81,42 +81,72 @@ export default ((userOpts?: Partial<Options>) => {
}: QuartzComponentProps) => {
if (ctx.buildId !== lastBuildId) {
lastBuildId = ctx.buildId
constructFileTree(allFiles)
constructFileTree(allFiles, (fileData.filePath ?? "").replaceAll(" ", "-"))
}

return (
<div class={classNames(displayClass, "explorer")}>
<button
type="button"
id="explorer"
data-behavior={opts.folderClickBehavior}
data-collapsed={opts.folderDefaultState}
data-savestate={opts.useSavedState}
data-tree={jsonTree}
aria-controls="explorer-content"
aria-expanded={opts.folderDefaultState === "open"}
>
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="5 8 14 8"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="fold"
<div class="explorer-container">
<div class={classNames(displayClass, "explorer")}>
<button
type="button"
id="mobile-explorer"
class="collapsed"
data-behavior={opts.folderClickBehavior}
data-collapsed={opts.folderDefaultState}
data-savestate={opts.useSavedState}
data-tree={jsonTree}
data-mobile={true}
saberzero1 marked this conversation as resolved.
Show resolved Hide resolved
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={true}
>
<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>
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="5 8 14 8"
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="explorer-content">
<ul class="overflow" id="explorer-ul">
<ExplorerNode node={fileTree} opts={opts} fileData={fileData} />
<li id="explorer-end" />
</ul>
</div>
</div>
</div>
)
Expand Down
1 change: 1 addition & 0 deletions quartz/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
import style from "./styles/footer.scss"
import { version } from "../../package.json"
import { classNames } from "../util/lang"
saberzero1 marked this conversation as resolved.
Show resolved Hide resolved
import { i18n } from "../i18n"

interface Options {
Expand Down
148 changes: 111 additions & 37 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 one 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,127 @@ 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))

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

// 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.replace("'", "-")}']`,
) as MaybeHTMLElement
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
if (folderUl) {
setFolderState(folderUl, folderState.collapsed)
}
})
}
}

currentExplorerState.map((folderState) => {
const folderLi = document.querySelector(
`[data-folderpath='${folderState.path}']`,
) as MaybeHTMLElement
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
if (folderUl) {
setFolderState(folderUl, folderState.collapsed)
function toggleExplorerFolders() {
const currentFile = (document.querySelector("body")?.getAttribute("data-slug") ?? "").replace(
/\/index$/g,
"",
)
const listToReplace = document.querySelectorAll(".folder-outer:has(> ul[data-folderul]")

listToReplace.forEach((element) => {
if (element.children.length > 0) {
if (currentFile.includes(element.firstElementChild?.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)
}

toggleExplorerFolders()
})

/**
Expand Down
Loading