chore: add window.addCleanup() for cleaning up handlers

This commit is contained in:
Jacky Zhao 2024-02-01 20:07:14 -08:00
parent 8a6ebd1939
commit c00089bd57
12 changed files with 47 additions and 49 deletions

View file

@ -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
View file

@ -8,5 +8,6 @@ export declare global {
}
interface Window {
spaNavigate(url: URL, isBack: boolean = false)
addCleanup(fn: (...args: any[]) => void)
}
}

View file

@ -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)

View file

@ -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
}

View file

@ -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

View file

@ -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))
})

View file

@ -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))
}
})

View file

@ -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++
}
}

View file

@ -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)

View file

@ -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))
}
}

View file

@ -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) {

View file

@ -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())
`,
})
}
}