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) + } +}