57 lines
1.6 KiB
TypeScript
57 lines
1.6 KiB
TypeScript
/**
|
|
* useHeadingFlash — watches location.hash and applies a fading highlight
|
|
* animation to the target heading element on every hash change.
|
|
*
|
|
* Handles page refresh / initial load by retrying up to ~500ms until the
|
|
* target element exists in the DOM.
|
|
*/
|
|
|
|
import { createEffect, onCleanup } from "solid-js";
|
|
import { useLocation } from "@solidjs/router";
|
|
|
|
const FLASH_CLASS = "heading-flash";
|
|
const MAX_RETRIES = 10;
|
|
const RETRY_INTERVAL = 50;
|
|
|
|
function flashElement(el: HTMLElement) {
|
|
// Re-trigger the animation: remove and re-add the class
|
|
el.classList.remove(FLASH_CLASS);
|
|
void el.offsetWidth; // force reflow
|
|
el.classList.add(FLASH_CLASS);
|
|
}
|
|
|
|
export function useHeadingFlash() {
|
|
const location = useLocation();
|
|
|
|
createEffect(() => {
|
|
const hash = location.hash;
|
|
if (!hash) return;
|
|
|
|
const id = decodeURIComponent(hash.startsWith("#") ? hash.slice(1) : hash);
|
|
if (!id) return;
|
|
|
|
let retries = 0;
|
|
let timer: ReturnType<typeof setTimeout>;
|
|
|
|
const tryFlash = () => {
|
|
const el = document.getElementById(id);
|
|
if (el) {
|
|
flashElement(el);
|
|
el.addEventListener("animationend", function onEnd() {
|
|
el.classList.remove(FLASH_CLASS);
|
|
el.removeEventListener("animationend", onEnd);
|
|
}, { once: true });
|
|
} else if (retries < MAX_RETRIES) {
|
|
retries++;
|
|
timer = setTimeout(tryFlash, RETRY_INTERVAL);
|
|
}
|
|
};
|
|
|
|
// Delay slightly so Solid has a chance to flush DOM updates from
|
|
// a concurrent SPA navigation before we look for the element.
|
|
timer = setTimeout(tryFlash, 0);
|
|
|
|
onCleanup(() => clearTimeout(timer));
|
|
});
|
|
}
|