From 76be137283a497c88b7da445cd9f4b8533a04f35 Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Mon, 29 Jan 2024 08:56:20 +0000
Subject: [PATCH] fix: attempt to merge cached folder state between builds (closes #691)

---
 quartz/components/Explorer.tsx               |    3 
 quartz/components/scripts/explorer.inline.ts |  156 +++++++++++++++++++++-------------------------------
 quartz/components/scripts/toc.inline.ts      |    6 +
 3 files changed, 68 insertions(+), 97 deletions(-)

diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx
index e3ed9b1..fdfff23 100644
--- a/quartz/components/Explorer.tsx
+++ b/quartz/components/Explorer.tsx
@@ -69,9 +69,8 @@
     }
 
     // Get all folders of tree. Initialize with collapsed state
-    const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
-
     // Stringify to pass json tree as data attribute ([data-tree])
+    const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed")
     jsonTree = JSON.stringify(folders)
   }
 
diff --git a/quartz/components/scripts/explorer.inline.ts b/quartz/components/scripts/explorer.inline.ts
index 8e79d20..12546bb 100644
--- a/quartz/components/scripts/explorer.inline.ts
+++ b/quartz/components/scripts/explorer.inline.ts
@@ -1,132 +1,106 @@
 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")
     }
   }
 })
 
 function toggleExplorer(this: HTMLElement) {
-  // Toggle collapsed state of entire explorer
   this.classList.toggle("collapsed")
-  const content = this.nextElementSibling as HTMLElement
+  const content = this.nextElementSibling as MaybeHTMLElement
+  if (!content) return
+
   content.classList.toggle("collapsed")
   content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
 }
 
 function toggleFolder(evt: MouseEvent) {
   evt.stopPropagation()
+  const target = evt.target as MaybeHTMLElement
+  if (!target) return
 
-  // Element that was clicked
-  const target = evt.target as HTMLElement
-
-  // Check if target was svg icon or button
   const isSvg = target.nodeName === "svg"
+  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
 
-  // corresponding <ul> element relative to clicked button/folder
-  let childFolderContainer: HTMLElement
-
-  // <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
-
-  // Collapse folder container
+  childFolderContainer.classList.toggle("open")
   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")
+  if (!explorer) return
+
+  if (explorer.dataset.behavior === "collapse") {
+    for (const item of document.getElementsByClassName(
+      "folder-button",
+    ) as HTMLCollectionOf<HTMLElement>) {
+      item.removeEventListener("click", toggleFolder)
+      item.addEventListener("click", toggleFolder)
+    }
+  }
+
+  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.removeEventListener("click", toggleFolder)
+    item.addEventListener("click", toggleFolder)
+  }
 
   // Get folder state from local storage
   const storageTree = localStorage.getItem("fileTree")
-
-  // Convert to bool
   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 })
+  }
 
-  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)
-          item.addEventListener("click", toggleFolder)
-        },
-      )
+  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)
     }
-
-    // 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)
-        }
-      }
-    })
-  } 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)
-  }
 }
 
 window.addEventListener("resize", setupExplorer)
 document.addEventListener("nav", () => {
   setupExplorer()
-
   observer.disconnect()
 
   // select pseudo element at end of list
@@ -142,11 +116,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")
 }
 
 /**
diff --git a/quartz/components/scripts/toc.inline.ts b/quartz/components/scripts/toc.inline.ts
index f3da52c..2e1e52b 100644
--- a/quartz/components/scripts/toc.inline.ts
+++ b/quartz/components/scripts/toc.inline.ts
@@ -16,7 +16,8 @@
 
 function toggleToc(this: HTMLElement) {
   this.classList.toggle("collapsed")
-  const content = this.nextElementSibling as HTMLElement
+  const content = this.nextElementSibling as HTMLElement | undefined
+  if (!content) return
   content.classList.toggle("collapsed")
   content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
 }
@@ -25,7 +26,8 @@
   const toc = document.getElementById("toc")
   if (toc) {
     const collapsed = toc.classList.contains("collapsed")
-    const content = toc.nextElementSibling as HTMLElement
+    const content = toc.nextElementSibling as HTMLElement | undefined
+    if (!content) return
     content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
     toc.removeEventListener("click", toggleToc)
     toc.addEventListener("click", toggleToc)

--
Gitblit v1.10.0