Emile Bangma
2025-02-03 91189dfd2f4cb32e205117b327e0ae7a0c2dd716
quartz/components/scripts/explorer.inline.ts
@@ -1,16 +1,18 @@
import { FolderState } from "../ExplorerNode"
// Current state of folders
let explorerState: FolderState[]
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 explorer = document.getElementById("explorer-ul")
  const explorerUl = document.getElementById("explorer-ul")
  if (!explorerUl) return
  for (const entry of entries) {
    if (entry.isIntersecting) {
      explorer?.classList.add("no-background")
      explorerUl.classList.add("no-background")
    } else {
      explorer?.classList.remove("no-background")
      explorerUl.classList.remove("no-background")
    }
  }
})
@@ -18,113 +20,163 @@
function toggleExplorer(this: HTMLElement) {
  // Toggle collapsed state of entire explorer
  this.classList.toggle("collapsed")
  const content = this.nextElementSibling as HTMLElement
  // Toggle collapsed aria state of entire explorer
  this.setAttribute(
    "aria-expanded",
    this.getAttribute("aria-expanded") === "true" ? "false" : "true",
  )
  const content = (
    this.nextElementSibling?.nextElementSibling
      ? this.nextElementSibling.nextElementSibling
      : this.nextElementSibling
  ) as MaybeHTMLElement
  if (!content) return
  content.classList.toggle("collapsed")
  content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
  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 HTMLElement
  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
  let childFolderContainer: HTMLElement
  const childFolderContainer = (
    isSvg
      ? target.parentElement?.nextSibling
      : target.parentElement?.parentElement?.nextElementSibling
  ) as MaybeHTMLElement
  const currentFolderParent = (
    isSvg ? target.nextElementSibling : target.parentElement
  ) as MaybeHTMLElement
  if (!(childFolderContainer && currentFolderParent)) return
  // <li> element of folder (stores folder-path dataset)
  let currentFolderParent: HTMLElement
  // Get correct relative container and toggle collapsed class
  if (isSvg) {
    childFolderContainer = target.parentElement?.nextSibling as HTMLElement
    currentFolderParent = target.nextElementSibling as HTMLElement
    childFolderContainer.classList.toggle("open")
  } else {
    childFolderContainer = target.parentElement?.parentElement?.nextElementSibling as HTMLElement
    currentFolderParent = target.parentElement as HTMLElement
    childFolderContainer.classList.toggle("open")
  }
  if (!childFolderContainer) return
  childFolderContainer.classList.toggle("open")
  // Collapse folder container
  const isCollapsed = childFolderContainer.classList.contains("open")
  setFolderState(childFolderContainer, !isCollapsed)
  // Save folder state to localStorage
  const clickFolderPath = currentFolderParent.dataset.folderpath as string
  const fullFolderPath = clickFolderPath
  toggleCollapsedByPath(explorerState, fullFolderPath)
  const stringifiedFileTree = JSON.stringify(explorerState)
  const fullFolderPath = currentFolderParent.dataset.folderpath as string
  toggleCollapsedByPath(currentExplorerState, fullFolderPath)
  const stringifiedFileTree = JSON.stringify(currentExplorerState)
  localStorage.setItem("fileTree", stringifiedFileTree)
}
function setupExplorer() {
  // Set click handler for collapsing entire explorer
  const explorer = document.getElementById("explorer")
  const allExplorers = document.querySelectorAll(".explorer > button") as NodeListOf<HTMLElement>
  // Get folder state from local storage
  const storageTree = localStorage.getItem("fileTree")
  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"
    // Convert to bool
    const useSavedFolderState = explorer?.dataset.savestate === "true"
  if (explorer) {
    // Get config
    const collapseBehavior = explorer.dataset.behavior
    if (explorer) {
      // Get config
      const collapseBehavior = explorer.dataset.behavior
    // Add click handlers for all folders (click handler on folder "label")
    if (collapseBehavior === "collapse") {
      Array.prototype.forEach.call(
        document.getElementsByClassName("folder-button"),
        function (item) {
          item.removeEventListener("click", toggleFolder)
      // 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
    explorer.removeEventListener("click", toggleExplorer)
    explorer.addEventListener("click", toggleExplorer)
  }
  // Set up click handlers for each folder (click handler on folder "icon")
  Array.prototype.forEach.call(document.getElementsByClassName("folder-icon"), function (item) {
    item.removeEventListener("click", toggleFolder)
    item.addEventListener("click", toggleFolder)
  })
  if (storageTree && useSavedFolderState) {
    // Get state from localStorage and set folder state
    explorerState = JSON.parse(storageTree)
    explorerState.map((folderUl) => {
      // grab <li> element for matching folder path
      const folderLi = document.querySelector(`[data-folderpath='${folderUl.path}']`) as HTMLElement
      // Get corresponding content <ul> tag and set state
      if (folderLi) {
        const folderUL = folderLi.parentElement?.nextElementSibling
        if (folderUL) {
          setFolderState(folderUL as HTMLElement, folderUl.collapsed)
        }
      }
      // 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-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)
      }
    })
  } else if (explorer?.dataset.tree) {
    // If tree is not in localStorage or config is disabled, use tree passed from Explorer as dataset
    explorerState = JSON.parse(explorer.dataset.tree)
  }
}
function toggleExplorerFolders() {
  const currentFile = (document.querySelector("body")?.getAttribute("data-slug") ?? "").replace(
    /\/index$/g,
    "",
  )
  const allFolders = document.querySelectorAll(".folder-outer")
  allFolders.forEach((element) => {
    const folderUl = Array.from(element.children).find((child) =>
      child.matches("ul[data-folderul]"),
    )
    if (folderUl) {
      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()
@@ -134,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()
})
/**
@@ -142,11 +200,7 @@
 * @param collapsed if folder should be set to collapsed or not
 */
function setFolderState(folderElement: HTMLElement, collapsed: boolean) {
  if (collapsed) {
    folderElement?.classList.remove("open")
  } else {
    folderElement?.classList.add("open")
  }
  return collapsed ? folderElement.classList.remove("open") : folderElement.classList.add("open")
}
/**