Jacky Zhao
2024-02-02 c00089bd5728188ce554303b5b18754467c97c85
chore: add window.addCleanup() for cleaning up handlers
12 files modified
96 ■■■■ changed files
docs/advanced/creating components.md 5 ●●●●● patch | view | raw | blame | history
globals.d.ts 1 ●●●● patch | view | raw | blame | history
quartz/components/scripts/callout.inline.ts 24 ●●●● patch | view | raw | blame | history
quartz/components/scripts/darkmode.inline.ts 2 ●●● patch | view | raw | blame | history
quartz/components/scripts/explorer.inline.ts 6 ●●●● patch | view | raw | blame | history
quartz/components/scripts/graph.inline.ts 2 ●●● patch | view | raw | blame | history
quartz/components/scripts/popover.inline.ts 2 ●●● patch | view | raw | blame | history
quartz/components/scripts/search.inline.ts 27 ●●●● patch | view | raw | blame | history
quartz/components/scripts/spa.inline.ts 7 ●●●●● patch | view | raw | blame | history
quartz/components/scripts/toc.inline.ts 2 ●●● patch | view | raw | blame | history
quartz/components/scripts/util.ts 4 ●●●● patch | view | raw | blame | history
quartz/plugins/emitters/componentResources.ts 14 ●●●●● patch | view | raw | blame | history
docs/advanced/creating components.md
@@ -156,12 +156,13 @@
  // do page specific logic here
  // e.g. attach event listeners
  const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
  toggleSwitch.removeEventListener("change", switchTheme)
  toggleSwitch.addEventListener("change", switchTheme)
  window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
})
```
It is best practice to also unmount any existing event handlers to prevent memory leaks.
It is best practice to track any event handlers via `window.addCleanup` to prevent memory leaks.
This will get called on page navigation.
#### Importing Code
globals.d.ts
@@ -8,5 +8,6 @@
  }
  interface Window {
    spaNavigate(url: URL, isBack: boolean = false)
    addCleanup(fn: (...args: any[]) => void)
  }
}
quartz/components/scripts/callout.inline.ts
@@ -1,21 +1,21 @@
function toggleCallout(this: HTMLElement) {
  const outerBlock = this.parentElement!
  outerBlock.classList.toggle(`is-collapsed`)
  const collapsed = outerBlock.classList.contains(`is-collapsed`)
  outerBlock.classList.toggle("is-collapsed")
  const collapsed = outerBlock.classList.contains("is-collapsed")
  const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
  outerBlock.style.maxHeight = height + `px`
  outerBlock.style.maxHeight = height + "px"
  // walk and adjust height of all parents
  let current = outerBlock
  let parent = outerBlock.parentElement
  while (parent) {
    if (!parent.classList.contains(`callout`)) {
    if (!parent.classList.contains("callout")) {
      return
    }
    const collapsed = parent.classList.contains(`is-collapsed`)
    const collapsed = parent.classList.contains("is-collapsed")
    const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
    parent.style.maxHeight = height + `px`
    parent.style.maxHeight = height + "px"
    current = parent
    parent = parent.parentElement
@@ -30,15 +30,15 @@
    const title = div.firstElementChild
    if (title) {
      title.removeEventListener(`click`, toggleCallout)
      title.addEventListener(`click`, toggleCallout)
      title.addEventListener("click", toggleCallout)
      window.addCleanup(() => title.removeEventListener("click", toggleCallout))
      const collapsed = div.classList.contains(`is-collapsed`)
      const collapsed = div.classList.contains("is-collapsed")
      const height = collapsed ? title.scrollHeight : div.scrollHeight
      div.style.maxHeight = height + `px`
      div.style.maxHeight = height + "px"
    }
  }
}
document.addEventListener(`nav`, setupCallout)
window.addEventListener(`resize`, setupCallout)
document.addEventListener("nav", setupCallout)
window.addEventListener("resize", setupCallout)
quartz/components/scripts/darkmode.inline.ts
@@ -19,8 +19,8 @@
  // Darkmode toggle
  const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
  toggleSwitch.removeEventListener("change", switchTheme)
  toggleSwitch.addEventListener("change", switchTheme)
  window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
  if (currentTheme === "dark") {
    toggleSwitch.checked = true
  }
quartz/components/scripts/explorer.inline.ts
@@ -57,20 +57,20 @@
    for (const item of document.getElementsByClassName(
      "folder-button",
    ) as HTMLCollectionOf<HTMLElement>) {
      item.removeEventListener("click", toggleFolder)
      item.addEventListener("click", toggleFolder)
      window.addCleanup(() => item.removeEventListener("click", toggleFolder))
    }
  }
  explorer.removeEventListener("click", toggleExplorer)
  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.removeEventListener("click", toggleFolder)
    item.addEventListener("click", toggleFolder)
    window.addCleanup(() => item.removeEventListener("click", toggleFolder))
  }
  // Get folder state from local storage
quartz/components/scripts/graph.inline.ts
@@ -325,6 +325,6 @@
  await renderGraph("graph-container", slug)
  const containerIcon = document.getElementById("global-graph-icon")
  containerIcon?.removeEventListener("click", renderGlobalGraph)
  containerIcon?.addEventListener("click", renderGlobalGraph)
  window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
})
quartz/components/scripts/popover.inline.ts
@@ -76,7 +76,7 @@
document.addEventListener("nav", () => {
  const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
  for (const link of links) {
    link.removeEventListener("mouseenter", mouseEnterHandler)
    link.addEventListener("mouseenter", mouseEnterHandler)
    window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler))
  }
})
quartz/components/scripts/search.inline.ts
@@ -13,14 +13,13 @@
// Can be expanded with things like "term" in the future
type SearchType = "basic" | "tags"
// Current searchType
let searchType: SearchType = "basic"
// Current search term // TODO: exact match
let currentSearchTerm: string = ""
// index for search
let index: FlexSearch.Document<Item> | undefined = undefined
const p = new DOMParser()
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
const fetchContentCache: Map<FullSlug, Element[]> = new Map()
const contextWindowWords = 30
const numSearchResults = 8
const numTagResults = 5
@@ -79,7 +78,6 @@
}
function highlightHTML(searchTerm: string, el: HTMLElement) {
  // try to highlight longest tokens first
  const p = new DOMParser()
  const tokenizedTerms = tokenizeTerm(searchTerm)
  const html = p.parseFromString(el.innerHTML, "text/html")
@@ -117,12 +115,6 @@
  return html.body
}
const p = new DOMParser()
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
let prevShortcutHandler: ((e: HTMLElementEventMap["keydown"]) => void) | undefined = undefined
const fetchContentCache: Map<FullSlug, Element[]> = new Map()
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
  const currentSlug = e.detail.url
@@ -496,16 +488,12 @@
    await displayResults(finalResults)
  }
  if (prevShortcutHandler) {
    document.removeEventListener("keydown", prevShortcutHandler)
  }
  document.addEventListener("keydown", shortcutHandler)
  prevShortcutHandler = shortcutHandler
  searchIcon?.removeEventListener("click", () => showSearch("basic"))
  window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
  searchIcon?.addEventListener("click", () => showSearch("basic"))
  searchBar?.removeEventListener("input", onType)
  window.addCleanup(() => searchIcon?.removeEventListener("click", () => showSearch("basic")))
  searchBar?.addEventListener("input", onType)
  window.addCleanup(() => searchBar?.removeEventListener("input", onType))
  // setup index if it hasn't been already
  if (!index) {
@@ -546,13 +534,12 @@
async function fillDocument(index: FlexSearch.Document<Item, false>, data: any) {
  let id = 0
  for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
    await index.addAsync(id, {
    await index.addAsync(id++, {
      id,
      slug: slug as FullSlug,
      title: fileData.title,
      content: fileData.content,
      tags: fileData.tags,
    })
    id++
  }
}
quartz/components/scripts/spa.inline.ts
@@ -39,6 +39,9 @@
  document.dispatchEvent(event)
}
const cleanupFns: Set<(...args: any[]) => void> = new Set()
window.addCleanup = (fn) => cleanupFns.add(fn)
let p: DOMParser
async function navigate(url: URL, isBack: boolean = false) {
  p = p || new DOMParser()
@@ -57,6 +60,10 @@
  if (!contents) return
  // cleanup old
  cleanupFns.forEach((fn) => fn())
  cleanupFns.clear()
  const html = p.parseFromString(contents, "text/html")
  normalizeRelativeURLs(html, url)
quartz/components/scripts/toc.inline.ts
@@ -29,8 +29,8 @@
    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)
    window.addCleanup(() => toc.removeEventListener("click", toggleToc))
  }
}
quartz/components/scripts/util.ts
@@ -12,10 +12,10 @@
    cb()
  }
  outsideContainer?.removeEventListener("click", click)
  outsideContainer?.addEventListener("click", click)
  document.removeEventListener("keydown", esc)
  window.addCleanup(() => outsideContainer?.removeEventListener("click", click))
  document.addEventListener("keydown", esc)
  window.addCleanup(() => document.removeEventListener("keydown", esc))
}
export function removeAllChildren(node: HTMLElement) {
quartz/plugins/emitters/componentResources.ts
@@ -131,9 +131,11 @@
    componentResources.afterDOMLoaded.push(spaRouterScript)
  } else {
    componentResources.afterDOMLoaded.push(`
        window.spaNavigate = (url, _) => window.location.assign(url)
        const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
        document.dispatchEvent(event)`)
      window.spaNavigate = (url, _) => window.location.assign(url)
      window.addCleanup = () => {}
      const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
      document.dispatchEvent(event)
    `)
  }
  let wsUrl = `ws://localhost:${ctx.argv.wsPort}`
@@ -147,9 +149,9 @@
      loadTime: "afterDOMReady",
      contentType: "inline",
      script: `
          const socket = new WebSocket('${wsUrl}')
          socket.addEventListener('message', () => document.location.reload())
        `,
        const socket = new WebSocket('${wsUrl}')
        socket.addEventListener('message', () => document.location.reload())
      `,
    })
  }
}