chore: add window.addCleanup() for cleaning up handlers
| | |
| | | // 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 |
| | | |
| | |
| | | } |
| | | interface Window { |
| | | spaNavigate(url: URL, isBack: boolean = false) |
| | | addCleanup(fn: (...args: any[]) => void) |
| | | } |
| | | } |
| | |
| | | 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 |
| | |
| | | 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) |
| | |
| | | |
| | | // 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 |
| | | } |
| | |
| | | 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 |
| | |
| | | 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)) |
| | | }) |
| | |
| | | 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)) |
| | | } |
| | | }) |
| | |
| | | |
| | | // 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 |
| | |
| | | } |
| | | |
| | | 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") |
| | |
| | | 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 |
| | | |
| | |
| | | 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) { |
| | |
| | | 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++ |
| | | } |
| | | } |
| | |
| | | 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() |
| | |
| | | |
| | | if (!contents) return |
| | | |
| | | // cleanup old |
| | | cleanupFns.forEach((fn) => fn()) |
| | | cleanupFns.clear() |
| | | |
| | | const html = p.parseFromString(contents, "text/html") |
| | | normalizeRelativeURLs(html, url) |
| | | |
| | |
| | | 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)) |
| | | } |
| | | } |
| | | |
| | |
| | | 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) { |
| | |
| | | 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}` |
| | |
| | | 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()) |
| | | `, |
| | | }) |
| | | } |
| | | } |