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