Skip to content

Commit c00089b

Browse files
committed
chore: add window.addCleanup() for cleaning up handlers
1 parent 8a6ebd1 commit c00089b

12 files changed

+47
-49
lines changed

docs/advanced/creating components.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,13 @@ document.addEventListener("nav", () => {
156156
// do page specific logic here
157157
// e.g. attach event listeners
158158
const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
159-
toggleSwitch.removeEventListener("change", switchTheme)
160159
toggleSwitch.addEventListener("change", switchTheme)
160+
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
161161
})
162162
```
163163

164-
It is best practice to also unmount any existing event handlers to prevent memory leaks.
164+
It is best practice to track any event handlers via `window.addCleanup` to prevent memory leaks.
165+
This will get called on page navigation.
165166

166167
#### Importing Code
167168

globals.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ export declare global {
88
}
99
interface Window {
1010
spaNavigate(url: URL, isBack: boolean = false)
11+
addCleanup(fn: (...args: any[]) => void)
1112
}
1213
}
+12-12
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
function toggleCallout(this: HTMLElement) {
22
const outerBlock = this.parentElement!
3-
outerBlock.classList.toggle(`is-collapsed`)
4-
const collapsed = outerBlock.classList.contains(`is-collapsed`)
3+
outerBlock.classList.toggle("is-collapsed")
4+
const collapsed = outerBlock.classList.contains("is-collapsed")
55
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
6-
outerBlock.style.maxHeight = height + `px`
6+
outerBlock.style.maxHeight = height + "px"
77

88
// walk and adjust height of all parents
99
let current = outerBlock
1010
let parent = outerBlock.parentElement
1111
while (parent) {
12-
if (!parent.classList.contains(`callout`)) {
12+
if (!parent.classList.contains("callout")) {
1313
return
1414
}
1515

16-
const collapsed = parent.classList.contains(`is-collapsed`)
16+
const collapsed = parent.classList.contains("is-collapsed")
1717
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
18-
parent.style.maxHeight = height + `px`
18+
parent.style.maxHeight = height + "px"
1919

2020
current = parent
2121
parent = parent.parentElement
@@ -30,15 +30,15 @@ function setupCallout() {
3030
const title = div.firstElementChild
3131

3232
if (title) {
33-
title.removeEventListener(`click`, toggleCallout)
34-
title.addEventListener(`click`, toggleCallout)
33+
title.addEventListener("click", toggleCallout)
34+
window.addCleanup(() => title.removeEventListener("click", toggleCallout))
3535

36-
const collapsed = div.classList.contains(`is-collapsed`)
36+
const collapsed = div.classList.contains("is-collapsed")
3737
const height = collapsed ? title.scrollHeight : div.scrollHeight
38-
div.style.maxHeight = height + `px`
38+
div.style.maxHeight = height + "px"
3939
}
4040
}
4141
}
4242

43-
document.addEventListener(`nav`, setupCallout)
44-
window.addEventListener(`resize`, setupCallout)
43+
document.addEventListener("nav", setupCallout)
44+
window.addEventListener("resize", setupCallout)

quartz/components/scripts/darkmode.inline.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ document.addEventListener("nav", () => {
1919

2020
// Darkmode toggle
2121
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
22-
toggleSwitch.removeEventListener("change", switchTheme)
2322
toggleSwitch.addEventListener("change", switchTheme)
23+
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
2424
if (currentTheme === "dark") {
2525
toggleSwitch.checked = true
2626
}

quartz/components/scripts/explorer.inline.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,20 @@ function setupExplorer() {
5757
for (const item of document.getElementsByClassName(
5858
"folder-button",
5959
) as HTMLCollectionOf<HTMLElement>) {
60-
item.removeEventListener("click", toggleFolder)
6160
item.addEventListener("click", toggleFolder)
61+
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
6262
}
6363
}
6464

65-
explorer.removeEventListener("click", toggleExplorer)
6665
explorer.addEventListener("click", toggleExplorer)
66+
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
6767

6868
// Set up click handlers for each folder (click handler on folder "icon")
6969
for (const item of document.getElementsByClassName(
7070
"folder-icon",
7171
) as HTMLCollectionOf<HTMLElement>) {
72-
item.removeEventListener("click", toggleFolder)
7372
item.addEventListener("click", toggleFolder)
73+
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
7474
}
7575

7676
// Get folder state from local storage

quartz/components/scripts/graph.inline.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,6 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
325325
await renderGraph("graph-container", slug)
326326

327327
const containerIcon = document.getElementById("global-graph-icon")
328-
containerIcon?.removeEventListener("click", renderGlobalGraph)
329328
containerIcon?.addEventListener("click", renderGlobalGraph)
329+
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
330330
})

quartz/components/scripts/popover.inline.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ async function mouseEnterHandler(
7676
document.addEventListener("nav", () => {
7777
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
7878
for (const link of links) {
79-
link.removeEventListener("mouseenter", mouseEnterHandler)
8079
link.addEventListener("mouseenter", mouseEnterHandler)
80+
window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler))
8181
}
8282
})

quartz/components/scripts/search.inline.ts

+7-20
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@ interface Item {
1313

1414
// Can be expanded with things like "term" in the future
1515
type SearchType = "basic" | "tags"
16-
17-
// Current searchType
1816
let searchType: SearchType = "basic"
19-
// Current search term // TODO: exact match
2017
let currentSearchTerm: string = ""
21-
// index for search
2218
let index: FlexSearch.Document<Item> | undefined = undefined
19+
const p = new DOMParser()
20+
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
2321

22+
const fetchContentCache: Map<FullSlug, Element[]> = new Map()
2423
const contextWindowWords = 30
2524
const numSearchResults = 8
2625
const numTagResults = 5
@@ -79,7 +78,6 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
7978
}
8079

8180
function highlightHTML(searchTerm: string, el: HTMLElement) {
82-
// try to highlight longest tokens first
8381
const p = new DOMParser()
8482
const tokenizedTerms = tokenizeTerm(searchTerm)
8583
const html = p.parseFromString(el.innerHTML, "text/html")
@@ -117,12 +115,6 @@ function highlightHTML(searchTerm: string, el: HTMLElement) {
117115
return html.body
118116
}
119117

120-
const p = new DOMParser()
121-
const encoder = (str: string) => str.toLowerCase().split(/([^a-z]|[^\x00-\x7F])/)
122-
let prevShortcutHandler: ((e: HTMLElementEventMap["keydown"]) => void) | undefined = undefined
123-
124-
const fetchContentCache: Map<FullSlug, Element[]> = new Map()
125-
126118
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
127119
const currentSlug = e.detail.url
128120

@@ -496,16 +488,12 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
496488
await displayResults(finalResults)
497489
}
498490

499-
if (prevShortcutHandler) {
500-
document.removeEventListener("keydown", prevShortcutHandler)
501-
}
502-
503491
document.addEventListener("keydown", shortcutHandler)
504-
prevShortcutHandler = shortcutHandler
505-
searchIcon?.removeEventListener("click", () => showSearch("basic"))
492+
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
506493
searchIcon?.addEventListener("click", () => showSearch("basic"))
507-
searchBar?.removeEventListener("input", onType)
494+
window.addCleanup(() => searchIcon?.removeEventListener("click", () => showSearch("basic")))
508495
searchBar?.addEventListener("input", onType)
496+
window.addCleanup(() => searchBar?.removeEventListener("input", onType))
509497

510498
// setup index if it hasn't been already
511499
if (!index) {
@@ -546,13 +534,12 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
546534
async function fillDocument(index: FlexSearch.Document<Item, false>, data: any) {
547535
let id = 0
548536
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
549-
await index.addAsync(id, {
537+
await index.addAsync(id++, {
550538
id,
551539
slug: slug as FullSlug,
552540
title: fileData.title,
553541
content: fileData.content,
554542
tags: fileData.tags,
555543
})
556-
id++
557544
}
558545
}

quartz/components/scripts/spa.inline.ts

+7
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ function notifyNav(url: FullSlug) {
3939
document.dispatchEvent(event)
4040
}
4141

42+
const cleanupFns: Set<(...args: any[]) => void> = new Set()
43+
window.addCleanup = (fn) => cleanupFns.add(fn)
44+
4245
let p: DOMParser
4346
async function navigate(url: URL, isBack: boolean = false) {
4447
p = p || new DOMParser()
@@ -57,6 +60,10 @@ async function navigate(url: URL, isBack: boolean = false) {
5760

5861
if (!contents) return
5962

63+
// cleanup old
64+
cleanupFns.forEach((fn) => fn())
65+
cleanupFns.clear()
66+
6067
const html = p.parseFromString(contents, "text/html")
6168
normalizeRelativeURLs(html, url)
6269

quartz/components/scripts/toc.inline.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ function setupToc() {
2929
const content = toc.nextElementSibling as HTMLElement | undefined
3030
if (!content) return
3131
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
32-
toc.removeEventListener("click", toggleToc)
3332
toc.addEventListener("click", toggleToc)
33+
window.addCleanup(() => toc.removeEventListener("click", toggleToc))
3434
}
3535
}
3636

quartz/components/scripts/util.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb:
1212
cb()
1313
}
1414

15-
outsideContainer?.removeEventListener("click", click)
1615
outsideContainer?.addEventListener("click", click)
17-
document.removeEventListener("keydown", esc)
16+
window.addCleanup(() => outsideContainer?.removeEventListener("click", click))
1817
document.addEventListener("keydown", esc)
18+
window.addCleanup(() => document.removeEventListener("keydown", esc))
1919
}
2020

2121
export function removeAllChildren(node: HTMLElement) {

quartz/plugins/emitters/componentResources.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,11 @@ function addGlobalPageResources(
131131
componentResources.afterDOMLoaded.push(spaRouterScript)
132132
} else {
133133
componentResources.afterDOMLoaded.push(`
134-
window.spaNavigate = (url, _) => window.location.assign(url)
135-
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
136-
document.dispatchEvent(event)`)
134+
window.spaNavigate = (url, _) => window.location.assign(url)
135+
window.addCleanup = () => {}
136+
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
137+
document.dispatchEvent(event)
138+
`)
137139
}
138140

139141
let wsUrl = `ws://localhost:${ctx.argv.wsPort}`
@@ -147,9 +149,9 @@ function addGlobalPageResources(
147149
loadTime: "afterDOMReady",
148150
contentType: "inline",
149151
script: `
150-
const socket = new WebSocket('${wsUrl}')
151-
socket.addEventListener('message', () => document.location.reload())
152-
`,
152+
const socket = new WebSocket('${wsUrl}')
153+
socket.addEventListener('message', () => document.location.reload())
154+
`,
153155
})
154156
}
155157
}

0 commit comments

Comments
 (0)