From c803941cddbd6a0589637b5daacb381bdf56c7c8 Mon Sep 17 00:00:00 2001 From: neerajadhav Date: Mon, 7 Jul 2025 18:29:27 +0530 Subject: [PATCH 01/10] feat(navigation): improve hash anchor scrolling with header-aware positioning and visual feedback - Add scrollToElementWithBuffer and highlightElement utility functions to util.ts - Implement header-aware scroll positioning to prevent content hiding behind fixed headers - Add temporary highlight effect for scrolled-to elements to improve user experience - Refactor SPA and popover scripts to use shared utility functions (DRY principle) - Support configurable buffer space and highlight duration for different use cases Fixes content being obscured by page header when navigating to hash anchors. Enhances UX with visual feedback showing which heading was targeted. --- quartz/components/scripts/popover.inline.ts | 7 +- quartz/components/scripts/spa.inline.ts | 13 ++-- quartz/components/scripts/util.ts | 80 +++++++++++++++++++++ 3 files changed, 93 insertions(+), 7 deletions(-) diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index 989af7ee8..3348e0249 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -1,6 +1,7 @@ import { computePosition, flip, inline, shift } from "@floating-ui/dom" +import { fetchCanonical, scrollInContainerToElement } from "./util" + import { normalizeRelativeURLs } from "../../util/path" -import { fetchCanonical } from "./util" const p = new DOMParser() let activeAnchor: HTMLAnchorElement | null = null @@ -33,8 +34,8 @@ async function mouseEnterHandler( const targetAnchor = `#popover-internal-${hash.slice(1)}` const heading = popoverInner.querySelector(targetAnchor) as HTMLElement | null if (heading) { - // leave ~12px of buffer when scrolling to a heading - popoverInner.scroll({ top: heading.offsetTop - 12, behavior: "instant" }) + // Use utility function to scroll with buffer and highlight + scrollInContainerToElement(popoverInner, heading, 20, true, "instant") } } } diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts index 22fcd72b4..1459c0f01 100644 --- a/quartz/components/scripts/spa.inline.ts +++ b/quartz/components/scripts/spa.inline.ts @@ -1,6 +1,7 @@ -import micromorph from "micromorph" import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path" -import { fetchCanonical } from "./util" +import { fetchCanonical, scrollToElementWithBuffer } from "./util" + +import micromorph from "micromorph" // adapted from `micromorph` // https://github.com/natemoo-re/micromorph @@ -108,7 +109,9 @@ async function _navigate(url: URL, isBack: boolean = false) { if (!isBack) { if (url.hash) { const el = document.getElementById(decodeURIComponent(url.hash.substring(1))) - el?.scrollIntoView() + if (el) { + scrollToElementWithBuffer(el) + } } else { window.scrollTo({ top: 0 }) } @@ -155,7 +158,9 @@ function createRouter() { if (isSamePage(url) && url.hash) { const el = document.getElementById(decodeURIComponent(url.hash.substring(1))) - el?.scrollIntoView() + if (el) { + scrollToElementWithBuffer(el) + } history.pushState({}, "", url) return } diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts index f71790104..b76469020 100644 --- a/quartz/components/scripts/util.ts +++ b/quartz/components/scripts/util.ts @@ -44,3 +44,83 @@ export async function fetchCanonical(url: URL): Promise { const [_, redirect] = text.match(canonicalRegex) ?? [] return redirect ? fetch(`${new URL(redirect, url)}`) : res } + +/** + * Highlights an element with a temporary background color effect + * @param el - The element to highlight + * @param duration - How long to show the highlight in milliseconds (default: 2000) + * @param color - The highlight color (default: uses CSS variable --highlight) + */ +export function highlightElement(el: HTMLElement, duration: number = 2000, color: string = 'var(--highlight, #ffeb3b40)') { + // Store original styles + const originalBackground = el.style.backgroundColor + const originalTransition = el.style.transition + + // Apply highlight styles + el.style.transition = 'background-color 0.3s ease' + el.style.backgroundColor = color + + // Remove highlight after specified duration + setTimeout(() => { + el.style.backgroundColor = originalBackground + // Remove transition after background fades back + setTimeout(() => { + el.style.transition = originalTransition + }, 300) + }, duration) +} + +/** + * Scrolls to an element with buffer space from the page header + * @param el - The element to scroll to + * @param buffer - Additional buffer space in pixels (default: 20) + * @param highlight - Whether to highlight the element after scrolling (default: true) + */ +export function scrollToElementWithBuffer(el: HTMLElement, buffer: number = 20, highlight: boolean = true) { + // Get the height of the header to calculate buffer + const header = document.querySelector('.page-header') as HTMLElement + const headerHeight = header ? header.offsetHeight : 0 + const totalOffset = headerHeight + buffer + + // Calculate the target position + const elementTop = el.offsetTop + const targetPosition = elementTop - totalOffset + + // Scroll to the calculated position + window.scrollTo({ + top: Math.max(0, targetPosition), + behavior: 'smooth' + }) + + // Add highlight effect if requested + if (highlight) { + highlightElement(el) + } +} + +/** + * Scrolls within a container element to a target element with buffer + * @param container - The container element to scroll within + * @param target - The target element to scroll to + * @param buffer - Buffer space in pixels (default: 20) + * @param highlight - Whether to highlight the element after scrolling (default: true) + * @param behavior - Scroll behavior (default: 'instant') + */ +export function scrollInContainerToElement( + container: HTMLElement, + target: HTMLElement, + buffer: number = 20, + highlight: boolean = true, + behavior: ScrollBehavior = 'instant' +) { + const targetPosition = target.offsetTop - buffer + container.scroll({ + top: Math.max(0, targetPosition), + behavior + }) + + // Add highlight effect if requested + if (highlight) { + highlightElement(target) + } +} From c6967979899f45d4bf797d542fe6212967bba6eb Mon Sep 17 00:00:00 2001 From: neerajadhav Date: Mon, 7 Jul 2025 19:02:49 +0530 Subject: [PATCH 02/10] feat: enhance popover and SPA navigation with smart scrolling and highlighting - Add buffer-aware scrolling to hash anchors in both SPA and popover contexts - Implement temporary highlight feedback for targeted headings with auto-fade - Refactor scroll and highlight logic into reusable utility functions - Improve popover heading matching with stricter fallback logic to avoid false positives - Ensure highlights clear properly on popover enter/leave events - Fix scroll behavior for cached popovers to always navigate to correct heading - Add robust error handling and TypeScript compatibility for isolated file checks Breaking changes: None Fixes: Scroll positioning issues, highlight persistence bugs, imprecise heading matching --- quartz/components/scripts/popover.inline.ts | 78 +++++++++++++---- quartz/components/scripts/util.ts | 92 ++++++++++++++++++--- 2 files changed, 140 insertions(+), 30 deletions(-) diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index 3348e0249..f41faa0f8 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -1,5 +1,5 @@ +import { clearAllHighlights, fetchCanonical, scrollInContainerToElement } from "./util" import { computePosition, flip, inline, shift } from "@floating-ui/dom" -import { fetchCanonical, scrollInContainerToElement } from "./util" import { normalizeRelativeURLs } from "../../util/path" @@ -15,6 +15,9 @@ async function mouseEnterHandler( return } + // Clear any existing highlights immediately when entering a new link + clearAllHighlights() + async function setPosition(popoverElement: HTMLElement) { const { x, y } = await computePosition(link, popoverElement, { strategy: "fixed", @@ -25,17 +28,47 @@ async function mouseEnterHandler( }) } - function showPopover(popoverElement: HTMLElement) { + function showPopover(popoverElement: HTMLElement, targetHash: string = "") { clearActivePopover() popoverElement.classList.add("active-popover") setPosition(popoverElement as HTMLElement) - if (hash !== "") { - const targetAnchor = `#popover-internal-${hash.slice(1)}` - const heading = popoverInner.querySelector(targetAnchor) as HTMLElement | null - if (heading) { - // Use utility function to scroll with buffer and highlight - scrollInContainerToElement(popoverInner, heading, 20, true, "instant") + // Always scroll to hash anchor if present, regardless of popover state + if (targetHash !== "") { + const popoverInner = popoverElement.querySelector(".popover-inner") as HTMLElement | null + if (popoverInner) { + const hashWithoutPound = targetHash.slice(1) + const targetAnchor = `popover-internal-${hashWithoutPound}` + + // Try to find the element by ID first + let heading = popoverInner.querySelector(`#${targetAnchor}`) as HTMLElement | null + + // If not found by ID, try to find by text content (fallback for headings) + if (!heading) { + const headings = popoverInner.querySelectorAll('h1, h2, h3, h4, h5, h6') + for (const h of headings) { + const headingElement = h as HTMLElement + const headingText = headingElement.textContent?.trim().toLowerCase() || '' + const targetText = hashWithoutPound.toLowerCase().replace(/-/g, ' ') + + // More strict matching: avoid matching very short headings unless they're exact matches + const isExactMatch = headingText === targetText + const isSubstringMatch = headingText.length >= 3 && ( + headingText.includes(targetText) || + (targetText.length >= 3 && targetText.includes(headingText)) + ) + + if (isExactMatch || isSubstringMatch) { + heading = headingElement + break + } + } + } + + if (heading) { + // Use utility function to scroll with buffer and highlight + scrollInContainerToElement(popoverInner, heading, 20, true, "instant") + } } } } @@ -49,7 +82,7 @@ async function mouseEnterHandler( // dont refetch if there's already a popover if (!!document.getElementById(popoverId)) { - showPopover(prevPopoverElement as HTMLElement) + showPopover(prevPopoverElement as HTMLElement, hash) return } @@ -97,7 +130,7 @@ async function mouseEnterHandler( const targetID = `popover-internal-${el.id}` el.id = targetID }) - const elts = [...html.getElementsByClassName("popover-hint")] + const elts = Array.from(html.getElementsByClassName("popover-hint")) if (elts.length === 0) return elts.forEach((elt) => popoverInner.appendChild(elt)) @@ -112,23 +145,34 @@ async function mouseEnterHandler( return } - showPopover(popoverElement) + showPopover(popoverElement, hash) } function clearActivePopover() { activeAnchor = null const allPopoverElements = document.querySelectorAll(".popover") allPopoverElements.forEach((popoverElement) => popoverElement.classList.remove("active-popover")) + // Clear any remaining highlights when closing popovers + clearAllHighlights() +} + +function clearActivePopoverAndHighlights() { + clearActivePopover() + // Also clear highlights immediately when mouse leaves + clearAllHighlights() } document.addEventListener("nav", () => { - const links = [...document.querySelectorAll("a.internal")] as HTMLAnchorElement[] + const links = Array.from(document.querySelectorAll("a.internal")) as HTMLAnchorElement[] for (const link of links) { link.addEventListener("mouseenter", mouseEnterHandler) - link.addEventListener("mouseleave", clearActivePopover) - window.addCleanup(() => { - link.removeEventListener("mouseenter", mouseEnterHandler) - link.removeEventListener("mouseleave", clearActivePopover) - }) + link.addEventListener("mouseleave", clearActivePopoverAndHighlights) + // Use type assertion to avoid TypeScript error when checking individual files + if (typeof (window as any).addCleanup === 'function') { + (window as any).addCleanup(() => { + link.removeEventListener("mouseenter", mouseEnterHandler) + link.removeEventListener("mouseleave", clearActivePopoverAndHighlights) + }) + } } }) diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts index b76469020..d9aba422b 100644 --- a/quartz/components/scripts/util.ts +++ b/quartz/components/scripts/util.ts @@ -14,9 +14,14 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb: } outsideContainer?.addEventListener("click", click) - window.addCleanup(() => outsideContainer?.removeEventListener("click", click)) + // Use type assertion to avoid TypeScript error when checking individual files + if (typeof (window as any).addCleanup === 'function') { + (window as any).addCleanup(() => outsideContainer?.removeEventListener("click", click)) + } document.addEventListener("keydown", esc) - window.addCleanup(() => document.removeEventListener("keydown", esc)) + if (typeof (window as any).addCleanup === 'function') { + (window as any).addCleanup(() => document.removeEventListener("keydown", esc)) + } } export function removeAllChildren(node: HTMLElement) { @@ -45,6 +50,39 @@ export async function fetchCanonical(url: URL): Promise { return redirect ? fetch(`${new URL(redirect, url)}`) : res } +// Keep track of active highlights to clean them up +let activeHighlights: Set<{ + element: HTMLElement, + originalBackground: string, + originalTransition: string, + timeoutId: number +}> = new Set() + +/** + * Clears all active highlights immediately + */ +export function clearAllHighlights() { + // Clear tracked highlights + activeHighlights.forEach(({ element, originalBackground, originalTransition, timeoutId }) => { + clearTimeout(timeoutId) + element.style.backgroundColor = originalBackground + element.style.transition = originalTransition + }) + activeHighlights.clear() + + // Also clear any highlights that might not be tracked (backup cleanup) + // This catches highlights in popovers or other edge cases + const allHighlightedElements = document.querySelectorAll('[style*="background-color"]') + allHighlightedElements.forEach((el) => { + const element = el as HTMLElement + if (element.style.backgroundColor.includes('var(--highlight') || + element.style.backgroundColor.includes('#ffeb3b')) { + element.style.backgroundColor = '' + element.style.transition = '' + } + }) +} + /** * Highlights an element with a temporary background color effect * @param el - The element to highlight @@ -52,6 +90,14 @@ export async function fetchCanonical(url: URL): Promise { * @param color - The highlight color (default: uses CSS variable --highlight) */ export function highlightElement(el: HTMLElement, duration: number = 2000, color: string = 'var(--highlight, #ffeb3b40)') { + // Clear any existing highlight on this element + activeHighlights.forEach((highlight) => { + if (highlight.element === el) { + clearTimeout(highlight.timeoutId) + activeHighlights.delete(highlight) + } + }) + // Store original styles const originalBackground = el.style.backgroundColor const originalTransition = el.style.transition @@ -60,14 +106,28 @@ export function highlightElement(el: HTMLElement, duration: number = 2000, color el.style.transition = 'background-color 0.3s ease' el.style.backgroundColor = color - // Remove highlight after specified duration - setTimeout(() => { + // Set up cleanup + const timeoutId = window.setTimeout(() => { el.style.backgroundColor = originalBackground // Remove transition after background fades back setTimeout(() => { el.style.transition = originalTransition + // Remove from active highlights + activeHighlights.forEach((highlight) => { + if (highlight.element === el) { + activeHighlights.delete(highlight) + } + }) }, 300) }, duration) + + // Track this highlight + activeHighlights.add({ + element: el, + originalBackground, + originalTransition, + timeoutId + }) } /** @@ -113,14 +173,20 @@ export function scrollInContainerToElement( highlight: boolean = true, behavior: ScrollBehavior = 'instant' ) { - const targetPosition = target.offsetTop - buffer - container.scroll({ - top: Math.max(0, targetPosition), - behavior + // Use requestAnimationFrame to ensure content is rendered before scrolling + requestAnimationFrame(() => { + const targetPosition = target.offsetTop - buffer + container.scroll({ + top: Math.max(0, targetPosition), + behavior + }) + + // Add highlight effect if requested + if (highlight) { + // Small delay to ensure scroll completes before highlighting + setTimeout(() => { + highlightElement(target) + }, 50) + } }) - - // Add highlight effect if requested - if (highlight) { - highlightElement(target) - } } From 1f6cbcbb71d998b58f6cb4a500cd9d7596fcba78 Mon Sep 17 00:00:00 2001 From: neerajadhav Date: Mon, 7 Jul 2025 19:19:44 +0530 Subject: [PATCH 03/10] style: fix code formatting with Prettier - Format popover.inline.ts and util.ts according to project style guidelines - Ensure consistent code style across navigation enhancement files --- quartz/components/scripts/popover.inline.ts | 28 ++++---- quartz/components/scripts/util.ts | 76 ++++++++++++--------- 2 files changed, 57 insertions(+), 47 deletions(-) diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index f41faa0f8..2efd22138 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -39,32 +39,32 @@ async function mouseEnterHandler( if (popoverInner) { const hashWithoutPound = targetHash.slice(1) const targetAnchor = `popover-internal-${hashWithoutPound}` - + // Try to find the element by ID first let heading = popoverInner.querySelector(`#${targetAnchor}`) as HTMLElement | null - + // If not found by ID, try to find by text content (fallback for headings) if (!heading) { - const headings = popoverInner.querySelectorAll('h1, h2, h3, h4, h5, h6') + const headings = popoverInner.querySelectorAll("h1, h2, h3, h4, h5, h6") for (const h of headings) { const headingElement = h as HTMLElement - const headingText = headingElement.textContent?.trim().toLowerCase() || '' - const targetText = hashWithoutPound.toLowerCase().replace(/-/g, ' ') - + const headingText = headingElement.textContent?.trim().toLowerCase() || "" + const targetText = hashWithoutPound.toLowerCase().replace(/-/g, " ") + // More strict matching: avoid matching very short headings unless they're exact matches const isExactMatch = headingText === targetText - const isSubstringMatch = headingText.length >= 3 && ( - headingText.includes(targetText) || - (targetText.length >= 3 && targetText.includes(headingText)) - ) - + const isSubstringMatch = + headingText.length >= 3 && + (headingText.includes(targetText) || + (targetText.length >= 3 && targetText.includes(headingText))) + if (isExactMatch || isSubstringMatch) { heading = headingElement break } } } - + if (heading) { // Use utility function to scroll with buffer and highlight scrollInContainerToElement(popoverInner, heading, 20, true, "instant") @@ -168,8 +168,8 @@ document.addEventListener("nav", () => { link.addEventListener("mouseenter", mouseEnterHandler) link.addEventListener("mouseleave", clearActivePopoverAndHighlights) // Use type assertion to avoid TypeScript error when checking individual files - if (typeof (window as any).addCleanup === 'function') { - (window as any).addCleanup(() => { + if (typeof (window as any).addCleanup === "function") { + ;(window as any).addCleanup(() => { link.removeEventListener("mouseenter", mouseEnterHandler) link.removeEventListener("mouseleave", clearActivePopoverAndHighlights) }) diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts index d9aba422b..11598cea0 100644 --- a/quartz/components/scripts/util.ts +++ b/quartz/components/scripts/util.ts @@ -15,12 +15,12 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb: outsideContainer?.addEventListener("click", click) // Use type assertion to avoid TypeScript error when checking individual files - if (typeof (window as any).addCleanup === 'function') { - (window as any).addCleanup(() => outsideContainer?.removeEventListener("click", click)) + if (typeof (window as any).addCleanup === "function") { + ;(window as any).addCleanup(() => outsideContainer?.removeEventListener("click", click)) } document.addEventListener("keydown", esc) - if (typeof (window as any).addCleanup === 'function') { - (window as any).addCleanup(() => document.removeEventListener("keydown", esc)) + if (typeof (window as any).addCleanup === "function") { + ;(window as any).addCleanup(() => document.removeEventListener("keydown", esc)) } } @@ -52,9 +52,9 @@ export async function fetchCanonical(url: URL): Promise { // Keep track of active highlights to clean them up let activeHighlights: Set<{ - element: HTMLElement, - originalBackground: string, - originalTransition: string, + element: HTMLElement + originalBackground: string + originalTransition: string timeoutId: number }> = new Set() @@ -69,16 +69,18 @@ export function clearAllHighlights() { element.style.transition = originalTransition }) activeHighlights.clear() - + // Also clear any highlights that might not be tracked (backup cleanup) // This catches highlights in popovers or other edge cases const allHighlightedElements = document.querySelectorAll('[style*="background-color"]') allHighlightedElements.forEach((el) => { const element = el as HTMLElement - if (element.style.backgroundColor.includes('var(--highlight') || - element.style.backgroundColor.includes('#ffeb3b')) { - element.style.backgroundColor = '' - element.style.transition = '' + if ( + element.style.backgroundColor.includes("var(--highlight") || + element.style.backgroundColor.includes("#ffeb3b") + ) { + element.style.backgroundColor = "" + element.style.transition = "" } }) } @@ -89,7 +91,11 @@ export function clearAllHighlights() { * @param duration - How long to show the highlight in milliseconds (default: 2000) * @param color - The highlight color (default: uses CSS variable --highlight) */ -export function highlightElement(el: HTMLElement, duration: number = 2000, color: string = 'var(--highlight, #ffeb3b40)') { +export function highlightElement( + el: HTMLElement, + duration: number = 2000, + color: string = "var(--highlight, #ffeb3b40)", +) { // Clear any existing highlight on this element activeHighlights.forEach((highlight) => { if (highlight.element === el) { @@ -97,15 +103,15 @@ export function highlightElement(el: HTMLElement, duration: number = 2000, color activeHighlights.delete(highlight) } }) - + // Store original styles const originalBackground = el.style.backgroundColor const originalTransition = el.style.transition - + // Apply highlight styles - el.style.transition = 'background-color 0.3s ease' + el.style.transition = "background-color 0.3s ease" el.style.backgroundColor = color - + // Set up cleanup const timeoutId = window.setTimeout(() => { el.style.backgroundColor = originalBackground @@ -120,13 +126,13 @@ export function highlightElement(el: HTMLElement, duration: number = 2000, color }) }, 300) }, duration) - + // Track this highlight activeHighlights.add({ element: el, originalBackground, originalTransition, - timeoutId + timeoutId, }) } @@ -136,22 +142,26 @@ export function highlightElement(el: HTMLElement, duration: number = 2000, color * @param buffer - Additional buffer space in pixels (default: 20) * @param highlight - Whether to highlight the element after scrolling (default: true) */ -export function scrollToElementWithBuffer(el: HTMLElement, buffer: number = 20, highlight: boolean = true) { +export function scrollToElementWithBuffer( + el: HTMLElement, + buffer: number = 20, + highlight: boolean = true, +) { // Get the height of the header to calculate buffer - const header = document.querySelector('.page-header') as HTMLElement + const header = document.querySelector(".page-header") as HTMLElement const headerHeight = header ? header.offsetHeight : 0 const totalOffset = headerHeight + buffer - + // Calculate the target position const elementTop = el.offsetTop const targetPosition = elementTop - totalOffset - + // Scroll to the calculated position window.scrollTo({ top: Math.max(0, targetPosition), - behavior: 'smooth' + behavior: "smooth", }) - + // Add highlight effect if requested if (highlight) { highlightElement(el) @@ -167,20 +177,20 @@ export function scrollToElementWithBuffer(el: HTMLElement, buffer: number = 20, * @param behavior - Scroll behavior (default: 'instant') */ export function scrollInContainerToElement( - container: HTMLElement, - target: HTMLElement, - buffer: number = 20, + container: HTMLElement, + target: HTMLElement, + buffer: number = 20, highlight: boolean = true, - behavior: ScrollBehavior = 'instant' + behavior: ScrollBehavior = "instant", ) { // Use requestAnimationFrame to ensure content is rendered before scrolling requestAnimationFrame(() => { const targetPosition = target.offsetTop - buffer - container.scroll({ - top: Math.max(0, targetPosition), - behavior + container.scroll({ + top: Math.max(0, targetPosition), + behavior, }) - + // Add highlight effect if requested if (highlight) { // Small delay to ensure scroll completes before highlighting From 4d303635458554eef43a2bceb8cc839201f3638d Mon Sep 17 00:00:00 2001 From: neerajadhav Date: Tue, 8 Jul 2025 17:33:31 +0530 Subject: [PATCH 04/10] refactor: remove unnecessary type assertions for window.addCleanup --- quartz/components/scripts/util.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts index 11598cea0..974a5d727 100644 --- a/quartz/components/scripts/util.ts +++ b/quartz/components/scripts/util.ts @@ -14,13 +14,12 @@ export function registerEscapeHandler(outsideContainer: HTMLElement | null, cb: } outsideContainer?.addEventListener("click", click) - // Use type assertion to avoid TypeScript error when checking individual files - if (typeof (window as any).addCleanup === "function") { - ;(window as any).addCleanup(() => outsideContainer?.removeEventListener("click", click)) + if (typeof window.addCleanup === "function") { + window.addCleanup(() => outsideContainer?.removeEventListener("click", click)) } document.addEventListener("keydown", esc) - if (typeof (window as any).addCleanup === "function") { - ;(window as any).addCleanup(() => document.removeEventListener("keydown", esc)) + if (typeof window.addCleanup === "function") { + window.addCleanup(() => document.removeEventListener("keydown", esc)) } } From b3ab6fdd5935d0221a8bd868674d32f99a02f19f Mon Sep 17 00:00:00 2001 From: neerajadhav Date: Tue, 8 Jul 2025 17:36:18 +0530 Subject: [PATCH 05/10] fix: remove unnecessary type assertions for window.addCleanup in popover --- quartz/components/scripts/popover.inline.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index 2efd22138..91b02d015 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -167,9 +167,8 @@ document.addEventListener("nav", () => { for (const link of links) { link.addEventListener("mouseenter", mouseEnterHandler) link.addEventListener("mouseleave", clearActivePopoverAndHighlights) - // Use type assertion to avoid TypeScript error when checking individual files - if (typeof (window as any).addCleanup === "function") { - ;(window as any).addCleanup(() => { + if (typeof window.addCleanup === "function") { + window.addCleanup(() => { link.removeEventListener("mouseenter", mouseEnterHandler) link.removeEventListener("mouseleave", clearActivePopoverAndHighlights) }) From 30677df3d3dfc2a949fa30c223cfcb05139416a6 Mon Sep 17 00:00:00 2001 From: neerajadhav Date: Tue, 8 Jul 2025 18:00:35 +0530 Subject: [PATCH 06/10] refactor: simplify highlightElement cleanup logic --- quartz/components/scripts/util.ts | 41 +++++++++++++------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts index 974a5d727..24ce989d6 100644 --- a/quartz/components/scripts/util.ts +++ b/quartz/components/scripts/util.ts @@ -96,12 +96,11 @@ export function highlightElement( color: string = "var(--highlight, #ffeb3b40)", ) { // Clear any existing highlight on this element - activeHighlights.forEach((highlight) => { - if (highlight.element === el) { - clearTimeout(highlight.timeoutId) - activeHighlights.delete(highlight) - } - }) + const existingHighlight = Array.from(activeHighlights).find(h => h.element === el) + if (existingHighlight) { + clearTimeout(existingHighlight.timeoutId) + activeHighlights.delete(existingHighlight) + } // Store original styles const originalBackground = el.style.backgroundColor @@ -112,27 +111,21 @@ export function highlightElement( el.style.backgroundColor = color // Set up cleanup - const timeoutId = window.setTimeout(() => { - el.style.backgroundColor = originalBackground - // Remove transition after background fades back - setTimeout(() => { - el.style.transition = originalTransition - // Remove from active highlights - activeHighlights.forEach((highlight) => { - if (highlight.element === el) { - activeHighlights.delete(highlight) - } - }) - }, 300) - }, duration) - - // Track this highlight - activeHighlights.add({ + const highlight = { element: el, originalBackground, originalTransition, - timeoutId, - }) + timeoutId: 0, + } + + highlight.timeoutId = window.setTimeout(() => { + el.style.backgroundColor = originalBackground + el.style.transition = originalTransition + activeHighlights.delete(highlight) + }, duration) + + // Track this highlight + activeHighlights.add(highlight) } /** From d6219339ef5ae71388ff6c70892f898d2e956b67 Mon Sep 17 00:00:00 2001 From: neerajadhav Date: Tue, 8 Jul 2025 18:16:12 +0530 Subject: [PATCH 07/10] refactor: remove backup highlight cleanup from clearAllHighlights --- quartz/components/scripts/util.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts index 24ce989d6..4d4c80001 100644 --- a/quartz/components/scripts/util.ts +++ b/quartz/components/scripts/util.ts @@ -68,20 +68,6 @@ export function clearAllHighlights() { element.style.transition = originalTransition }) activeHighlights.clear() - - // Also clear any highlights that might not be tracked (backup cleanup) - // This catches highlights in popovers or other edge cases - const allHighlightedElements = document.querySelectorAll('[style*="background-color"]') - allHighlightedElements.forEach((el) => { - const element = el as HTMLElement - if ( - element.style.backgroundColor.includes("var(--highlight") || - element.style.backgroundColor.includes("#ffeb3b") - ) { - element.style.backgroundColor = "" - element.style.transition = "" - } - }) } /** From 4db33faff7d6977f27ff00910c6ccba554f51dd4 Mon Sep 17 00:00:00 2001 From: neerajadhav Date: Tue, 8 Jul 2025 18:21:14 +0530 Subject: [PATCH 08/10] refactor: remove requestAnimationFrame and highlight delay from scrollInContainerToElement --- quartz/components/scripts/popover.inline.ts | 2 +- quartz/components/scripts/util.ts | 25 +++++++++------------ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts index 91b02d015..7f1a6aeb0 100644 --- a/quartz/components/scripts/popover.inline.ts +++ b/quartz/components/scripts/popover.inline.ts @@ -130,7 +130,7 @@ async function mouseEnterHandler( const targetID = `popover-internal-${el.id}` el.id = targetID }) - const elts = Array.from(html.getElementsByClassName("popover-hint")) + const elts = [...html.getElementsByClassName("popover-hint")] if (elts.length === 0) return elts.forEach((elt) => popoverInner.appendChild(elt)) diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts index 4d4c80001..66fe60c4e 100644 --- a/quartz/components/scripts/util.ts +++ b/quartz/components/scripts/util.ts @@ -161,20 +161,15 @@ export function scrollInContainerToElement( highlight: boolean = true, behavior: ScrollBehavior = "instant", ) { - // Use requestAnimationFrame to ensure content is rendered before scrolling - requestAnimationFrame(() => { - const targetPosition = target.offsetTop - buffer - container.scroll({ - top: Math.max(0, targetPosition), - behavior, - }) - - // Add highlight effect if requested - if (highlight) { - // Small delay to ensure scroll completes before highlighting - setTimeout(() => { - highlightElement(target) - }, 50) - } + // Scroll immediately, since content should already be rendered in a static site + const targetPosition = target.offsetTop - buffer + container.scroll({ + top: Math.max(0, targetPosition), + behavior, }) + + // Add highlight effect if requested + if (highlight) { + highlightElement(target) + } } From 80109683631208690e254a503cc7516fa13894a2 Mon Sep 17 00:00:00 2001 From: neerajadhav Date: Tue, 8 Jul 2025 18:25:23 +0530 Subject: [PATCH 09/10] fix: code formating issue --- quartz/components/scripts/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/components/scripts/util.ts b/quartz/components/scripts/util.ts index 66fe60c4e..cf7a6a6ec 100644 --- a/quartz/components/scripts/util.ts +++ b/quartz/components/scripts/util.ts @@ -82,7 +82,7 @@ export function highlightElement( color: string = "var(--highlight, #ffeb3b40)", ) { // Clear any existing highlight on this element - const existingHighlight = Array.from(activeHighlights).find(h => h.element === el) + const existingHighlight = Array.from(activeHighlights).find((h) => h.element === el) if (existingHighlight) { clearTimeout(existingHighlight.timeoutId) activeHighlights.delete(existingHighlight) From 1c67a752e3e78375e24de2bbbade521223041bd8 Mon Sep 17 00:00:00 2001 From: neerajadhav Date: Tue, 8 Jul 2025 18:44:02 +0530 Subject: [PATCH 10/10] fix: reformat manually instead of prettier --- quartz/components/scripts/spa.inline.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts index 1459c0f01..997fe38ca 100644 --- a/quartz/components/scripts/spa.inline.ts +++ b/quartz/components/scripts/spa.inline.ts @@ -1,8 +1,7 @@ +import micromorph from "micromorph" import { FullSlug, RelativeURL, getFullSlug, normalizeRelativeURLs } from "../../util/path" import { fetchCanonical, scrollToElementWithBuffer } from "./util" -import micromorph from "micromorph" - // adapted from `micromorph` // https://github.com/natemoo-re/micromorph const NODE_TYPE_ELEMENT = 1