chore: add window.addCleanup() for cleaning up handlers
This commit is contained in:
		@@ -156,12 +156,13 @@ document.addEventListener("nav", () => {
 | 
			
		||||
  // 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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								globals.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								globals.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -8,5 +8,6 @@ export declare global {
 | 
			
		||||
  }
 | 
			
		||||
  interface Window {
 | 
			
		||||
    spaNavigate(url: URL, isBack: boolean = false)
 | 
			
		||||
    addCleanup(fn: (...args: any[]) => void)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 @@ function setupCallout() {
 | 
			
		||||
    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)
 | 
			
		||||
 
 | 
			
		||||
@@ -19,8 +19,8 @@ document.addEventListener("nav", () => {
 | 
			
		||||
 | 
			
		||||
  // 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
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -57,20 +57,20 @@ function setupExplorer() {
 | 
			
		||||
    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
 | 
			
		||||
 
 | 
			
		||||
@@ -325,6 +325,6 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
 | 
			
		||||
  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))
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -76,7 +76,7 @@ async function mouseEnterHandler(
 | 
			
		||||
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))
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -13,14 +13,13 @@ interface Item {
 | 
			
		||||
 | 
			
		||||
// 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 highlight(searchTerm: string, text: string, trim?: boolean) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 @@ function highlightHTML(searchTerm: string, el: HTMLElement) {
 | 
			
		||||
  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 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
 | 
			
		||||
    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 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
 | 
			
		||||
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++
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,9 @@ function notifyNav(url: FullSlug) {
 | 
			
		||||
  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 @@ async function navigate(url: URL, isBack: boolean = false) {
 | 
			
		||||
 | 
			
		||||
  if (!contents) return
 | 
			
		||||
 | 
			
		||||
  // cleanup old
 | 
			
		||||
  cleanupFns.forEach((fn) => fn())
 | 
			
		||||
  cleanupFns.clear()
 | 
			
		||||
 | 
			
		||||
  const html = p.parseFromString(contents, "text/html")
 | 
			
		||||
  normalizeRelativeURLs(html, url)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -29,8 +29,8 @@ function setupToc() {
 | 
			
		||||
    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))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,10 +12,10 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb:
 | 
			
		||||
    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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -131,9 +131,11 @@ function addGlobalPageResources(
 | 
			
		||||
    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 @@ function addGlobalPageResources(
 | 
			
		||||
      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())
 | 
			
		||||
      `,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user