mirror of
https://github.com/alrayyes/wiki.git
synced 2024-11-22 03:26:22 +00:00
chore: add window.addCleanup() for cleaning up handlers
This commit is contained in:
parent
8a6ebd1939
commit
c00089bd57
12 changed files with 47 additions and 49 deletions
|
@ -156,12 +156,13 @@ document.addEventListener("nav", () => {
|
||||||
// do page specific logic here
|
// do page specific logic here
|
||||||
// e.g. attach event listeners
|
// e.g. attach event listeners
|
||||||
const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
|
const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
|
||||||
toggleSwitch.removeEventListener("change", switchTheme)
|
|
||||||
toggleSwitch.addEventListener("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
|
#### Importing Code
|
||||||
|
|
||||||
|
|
1
globals.d.ts
vendored
1
globals.d.ts
vendored
|
@ -8,5 +8,6 @@ export declare global {
|
||||||
}
|
}
|
||||||
interface Window {
|
interface Window {
|
||||||
spaNavigate(url: URL, isBack: boolean = false)
|
spaNavigate(url: URL, isBack: boolean = false)
|
||||||
|
addCleanup(fn: (...args: any[]) => void)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
function toggleCallout(this: HTMLElement) {
|
function toggleCallout(this: HTMLElement) {
|
||||||
const outerBlock = this.parentElement!
|
const outerBlock = this.parentElement!
|
||||||
outerBlock.classList.toggle(`is-collapsed`)
|
outerBlock.classList.toggle("is-collapsed")
|
||||||
const collapsed = outerBlock.classList.contains(`is-collapsed`)
|
const collapsed = outerBlock.classList.contains("is-collapsed")
|
||||||
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
|
const height = collapsed ? this.scrollHeight : outerBlock.scrollHeight
|
||||||
outerBlock.style.maxHeight = height + `px`
|
outerBlock.style.maxHeight = height + "px"
|
||||||
|
|
||||||
// walk and adjust height of all parents
|
// walk and adjust height of all parents
|
||||||
let current = outerBlock
|
let current = outerBlock
|
||||||
let parent = outerBlock.parentElement
|
let parent = outerBlock.parentElement
|
||||||
while (parent) {
|
while (parent) {
|
||||||
if (!parent.classList.contains(`callout`)) {
|
if (!parent.classList.contains("callout")) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const collapsed = parent.classList.contains(`is-collapsed`)
|
const collapsed = parent.classList.contains("is-collapsed")
|
||||||
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
|
const height = collapsed ? parent.scrollHeight : parent.scrollHeight + current.scrollHeight
|
||||||
parent.style.maxHeight = height + `px`
|
parent.style.maxHeight = height + "px"
|
||||||
|
|
||||||
current = parent
|
current = parent
|
||||||
parent = parent.parentElement
|
parent = parent.parentElement
|
||||||
|
@ -30,15 +30,15 @@ function setupCallout() {
|
||||||
const title = div.firstElementChild
|
const title = div.firstElementChild
|
||||||
|
|
||||||
if (title) {
|
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
|
const height = collapsed ? title.scrollHeight : div.scrollHeight
|
||||||
div.style.maxHeight = height + `px`
|
div.style.maxHeight = height + "px"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener(`nav`, setupCallout)
|
document.addEventListener("nav", setupCallout)
|
||||||
window.addEventListener(`resize`, setupCallout)
|
window.addEventListener("resize", setupCallout)
|
||||||
|
|
|
@ -19,8 +19,8 @@ document.addEventListener("nav", () => {
|
||||||
|
|
||||||
// Darkmode toggle
|
// Darkmode toggle
|
||||||
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
|
const toggleSwitch = document.querySelector("#darkmode-toggle") as HTMLInputElement
|
||||||
toggleSwitch.removeEventListener("change", switchTheme)
|
|
||||||
toggleSwitch.addEventListener("change", switchTheme)
|
toggleSwitch.addEventListener("change", switchTheme)
|
||||||
|
window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
|
||||||
if (currentTheme === "dark") {
|
if (currentTheme === "dark") {
|
||||||
toggleSwitch.checked = true
|
toggleSwitch.checked = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,20 +57,20 @@ function setupExplorer() {
|
||||||
for (const item of document.getElementsByClassName(
|
for (const item of document.getElementsByClassName(
|
||||||
"folder-button",
|
"folder-button",
|
||||||
) as HTMLCollectionOf<HTMLElement>) {
|
) as HTMLCollectionOf<HTMLElement>) {
|
||||||
item.removeEventListener("click", toggleFolder)
|
|
||||||
item.addEventListener("click", toggleFolder)
|
item.addEventListener("click", toggleFolder)
|
||||||
|
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
explorer.removeEventListener("click", toggleExplorer)
|
|
||||||
explorer.addEventListener("click", toggleExplorer)
|
explorer.addEventListener("click", toggleExplorer)
|
||||||
|
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
|
||||||
|
|
||||||
// Set up click handlers for each folder (click handler on folder "icon")
|
// Set up click handlers for each folder (click handler on folder "icon")
|
||||||
for (const item of document.getElementsByClassName(
|
for (const item of document.getElementsByClassName(
|
||||||
"folder-icon",
|
"folder-icon",
|
||||||
) as HTMLCollectionOf<HTMLElement>) {
|
) as HTMLCollectionOf<HTMLElement>) {
|
||||||
item.removeEventListener("click", toggleFolder)
|
|
||||||
item.addEventListener("click", toggleFolder)
|
item.addEventListener("click", toggleFolder)
|
||||||
|
window.addCleanup(() => item.removeEventListener("click", toggleFolder))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get folder state from local storage
|
// Get folder state from local storage
|
||||||
|
|
|
@ -325,6 +325,6 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
await renderGraph("graph-container", slug)
|
await renderGraph("graph-container", slug)
|
||||||
|
|
||||||
const containerIcon = document.getElementById("global-graph-icon")
|
const containerIcon = document.getElementById("global-graph-icon")
|
||||||
containerIcon?.removeEventListener("click", renderGlobalGraph)
|
|
||||||
containerIcon?.addEventListener("click", renderGlobalGraph)
|
containerIcon?.addEventListener("click", renderGlobalGraph)
|
||||||
|
window.addCleanup(() => containerIcon?.removeEventListener("click", renderGlobalGraph))
|
||||||
})
|
})
|
||||||
|
|
|
@ -76,7 +76,7 @@ async function mouseEnterHandler(
|
||||||
document.addEventListener("nav", () => {
|
document.addEventListener("nav", () => {
|
||||||
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
|
const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
|
||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
link.removeEventListener("mouseenter", mouseEnterHandler)
|
|
||||||
link.addEventListener("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
|
// Can be expanded with things like "term" in the future
|
||||||
type SearchType = "basic" | "tags"
|
type SearchType = "basic" | "tags"
|
||||||
|
|
||||||
// Current searchType
|
|
||||||
let searchType: SearchType = "basic"
|
let searchType: SearchType = "basic"
|
||||||
// Current search term // TODO: exact match
|
|
||||||
let currentSearchTerm: string = ""
|
let currentSearchTerm: string = ""
|
||||||
// index for search
|
|
||||||
let index: FlexSearch.Document<Item> | undefined = undefined
|
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 contextWindowWords = 30
|
||||||
const numSearchResults = 8
|
const numSearchResults = 8
|
||||||
const numTagResults = 5
|
const numTagResults = 5
|
||||||
|
@ -79,7 +78,6 @@ function highlight(searchTerm: string, text: string, trim?: boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightHTML(searchTerm: string, el: HTMLElement) {
|
function highlightHTML(searchTerm: string, el: HTMLElement) {
|
||||||
// try to highlight longest tokens first
|
|
||||||
const p = new DOMParser()
|
const p = new DOMParser()
|
||||||
const tokenizedTerms = tokenizeTerm(searchTerm)
|
const tokenizedTerms = tokenizeTerm(searchTerm)
|
||||||
const html = p.parseFromString(el.innerHTML, "text/html")
|
const html = p.parseFromString(el.innerHTML, "text/html")
|
||||||
|
@ -117,12 +115,6 @@ function highlightHTML(searchTerm: string, el: HTMLElement) {
|
||||||
return html.body
|
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"]) => {
|
document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
const currentSlug = e.detail.url
|
const currentSlug = e.detail.url
|
||||||
|
|
||||||
|
@ -496,16 +488,12 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
await displayResults(finalResults)
|
await displayResults(finalResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevShortcutHandler) {
|
|
||||||
document.removeEventListener("keydown", prevShortcutHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("keydown", shortcutHandler)
|
document.addEventListener("keydown", shortcutHandler)
|
||||||
prevShortcutHandler = shortcutHandler
|
window.addCleanup(() => document.removeEventListener("keydown", shortcutHandler))
|
||||||
searchIcon?.removeEventListener("click", () => showSearch("basic"))
|
|
||||||
searchIcon?.addEventListener("click", () => showSearch("basic"))
|
searchIcon?.addEventListener("click", () => showSearch("basic"))
|
||||||
searchBar?.removeEventListener("input", onType)
|
window.addCleanup(() => searchIcon?.removeEventListener("click", () => showSearch("basic")))
|
||||||
searchBar?.addEventListener("input", onType)
|
searchBar?.addEventListener("input", onType)
|
||||||
|
window.addCleanup(() => searchBar?.removeEventListener("input", onType))
|
||||||
|
|
||||||
// setup index if it hasn't been already
|
// setup index if it hasn't been already
|
||||||
if (!index) {
|
if (!index) {
|
||||||
|
@ -546,13 +534,12 @@ document.addEventListener("nav", async (e: CustomEventMap["nav"]) => {
|
||||||
async function fillDocument(index: FlexSearch.Document<Item, false>, data: any) {
|
async function fillDocument(index: FlexSearch.Document<Item, false>, data: any) {
|
||||||
let id = 0
|
let id = 0
|
||||||
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
|
||||||
await index.addAsync(id, {
|
await index.addAsync(id++, {
|
||||||
id,
|
id,
|
||||||
slug: slug as FullSlug,
|
slug: slug as FullSlug,
|
||||||
title: fileData.title,
|
title: fileData.title,
|
||||||
content: fileData.content,
|
content: fileData.content,
|
||||||
tags: fileData.tags,
|
tags: fileData.tags,
|
||||||
})
|
})
|
||||||
id++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,9 @@ function notifyNav(url: FullSlug) {
|
||||||
document.dispatchEvent(event)
|
document.dispatchEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cleanupFns: Set<(...args: any[]) => void> = new Set()
|
||||||
|
window.addCleanup = (fn) => cleanupFns.add(fn)
|
||||||
|
|
||||||
let p: DOMParser
|
let p: DOMParser
|
||||||
async function navigate(url: URL, isBack: boolean = false) {
|
async function navigate(url: URL, isBack: boolean = false) {
|
||||||
p = p || new DOMParser()
|
p = p || new DOMParser()
|
||||||
|
@ -57,6 +60,10 @@ async function navigate(url: URL, isBack: boolean = false) {
|
||||||
|
|
||||||
if (!contents) return
|
if (!contents) return
|
||||||
|
|
||||||
|
// cleanup old
|
||||||
|
cleanupFns.forEach((fn) => fn())
|
||||||
|
cleanupFns.clear()
|
||||||
|
|
||||||
const html = p.parseFromString(contents, "text/html")
|
const html = p.parseFromString(contents, "text/html")
|
||||||
normalizeRelativeURLs(html, url)
|
normalizeRelativeURLs(html, url)
|
||||||
|
|
||||||
|
|
|
@ -29,8 +29,8 @@ function setupToc() {
|
||||||
const content = toc.nextElementSibling as HTMLElement | undefined
|
const content = toc.nextElementSibling as HTMLElement | undefined
|
||||||
if (!content) return
|
if (!content) return
|
||||||
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
|
content.style.maxHeight = collapsed ? "0px" : content.scrollHeight + "px"
|
||||||
toc.removeEventListener("click", toggleToc)
|
|
||||||
toc.addEventListener("click", toggleToc)
|
toc.addEventListener("click", toggleToc)
|
||||||
|
window.addCleanup(() => toc.removeEventListener("click", toggleToc))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,10 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb:
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
outsideContainer?.removeEventListener("click", click)
|
|
||||||
outsideContainer?.addEventListener("click", click)
|
outsideContainer?.addEventListener("click", click)
|
||||||
document.removeEventListener("keydown", esc)
|
window.addCleanup(() => outsideContainer?.removeEventListener("click", click))
|
||||||
document.addEventListener("keydown", esc)
|
document.addEventListener("keydown", esc)
|
||||||
|
window.addCleanup(() => document.removeEventListener("keydown", esc))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeAllChildren(node: HTMLElement) {
|
export function removeAllChildren(node: HTMLElement) {
|
||||||
|
|
|
@ -131,9 +131,11 @@ function addGlobalPageResources(
|
||||||
componentResources.afterDOMLoaded.push(spaRouterScript)
|
componentResources.afterDOMLoaded.push(spaRouterScript)
|
||||||
} else {
|
} else {
|
||||||
componentResources.afterDOMLoaded.push(`
|
componentResources.afterDOMLoaded.push(`
|
||||||
window.spaNavigate = (url, _) => window.location.assign(url)
|
window.spaNavigate = (url, _) => window.location.assign(url)
|
||||||
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
|
window.addCleanup = () => {}
|
||||||
document.dispatchEvent(event)`)
|
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
|
||||||
|
document.dispatchEvent(event)
|
||||||
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
let wsUrl = `ws://localhost:${ctx.argv.wsPort}`
|
let wsUrl = `ws://localhost:${ctx.argv.wsPort}`
|
||||||
|
@ -147,9 +149,9 @@ function addGlobalPageResources(
|
||||||
loadTime: "afterDOMReady",
|
loadTime: "afterDOMReady",
|
||||||
contentType: "inline",
|
contentType: "inline",
|
||||||
script: `
|
script: `
|
||||||
const socket = new WebSocket('${wsUrl}')
|
const socket = new WebSocket('${wsUrl}')
|
||||||
socket.addEventListener('message', () => document.location.reload())
|
socket.addEventListener('message', () => document.location.reload())
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue