UNPKG

starlight-digital-garden

Version:
124 lines (107 loc) 3.73 kB
import { computePosition, autoPlacement, offset } from "@floating-ui/dom"; const tooltip = document.querySelector("#linkpreview") as HTMLElement; const noPreviewClass = "no-preview"; const elements = document.querySelectorAll( ".sl-markdown-content a" ) as NodeListOf<HTMLAnchorElement>; // response may arrive after cursor left the link let currentHref: string; // it is anoying that preview shows up before user ends mouse movement // if cursor stays long enough above the link - consider it as intentional let showPreviewTimer: NodeJS.Timeout | undefined; // if cursor moves out for a short period of time and comes back we should not hide preview // if cursor moves out from link to preview window we should we should not hide preview let hidePreviewTimer: NodeJS.Timeout | undefined; function hideLinkPreview() { clearTimeout(showPreviewTimer); if (hidePreviewTimer !== undefined) return; hidePreviewTimer = setTimeout(() => { currentHref = ""; tooltip.style.display = ""; hidePreviewTimer = undefined; }, 200); } function clearTimers() { clearTimeout(showPreviewTimer); clearTimeout(hidePreviewTimer); hidePreviewTimer = undefined; } async function showLinkPreview(e: MouseEvent | FocusEvent) { const start = `${window.location.protocol}//${window.location.host}`; const target = e.target as HTMLElement; const link = target?.closest("a"); const hrefRaw = (link?.href || "") as string | SVGAnimatedString; let href = ""; let local = false; let hash = ""; let svg = false; if (typeof hrefRaw === "string") { href = hrefRaw; hash = new URL(href).hash; local = href.startsWith(start); } else { href = hrefRaw.baseVal; hash = new URL(href, window.location.origin).hash; local = href.startsWith("/"); svg = true; } const hrefWithoutAnchor = href.replace(hash, ""); const locationWithoutAnchor = window.location.href.replace( window.location.hash, "" ); currentHref = href; if (hrefWithoutAnchor === locationWithoutAnchor || !local) { hideLinkPreview(); return; } // maybe use https://developer.mozilla.org/en-US/docs/Web/API/Element/matches ? const noPreview = link?.classList.contains(noPreviewClass) || !!target.closest(`.${noPreviewClass}`); if (noPreview) { hideLinkPreview(); return; } clearTimers(); const text = await fetch(href).then((x) => x.text()); if (currentHref !== href) return; showPreviewTimer = setTimeout(() => { if (currentHref !== href) return; const doc = new DOMParser().parseFromString(text, "text/html"); const content = (doc.querySelector(".sl-markdown-content") as HTMLElement) ?.outerHTML; tooltip.innerHTML = svg ? `${doc.querySelector("h1")?.outerHTML}${content}` : content; tooltip.style.display = "block"; let offsetTop = 0; if (hash !== "") { const heading = tooltip.querySelector(hash) as HTMLElement | null; if (heading) offsetTop = heading.offsetTop; } tooltip.scroll({ top: offsetTop, behavior: "instant" }); computePosition(target, tooltip, { middleware: [offset(10), autoPlacement()], }).then(({ x, y }) => { Object.assign(tooltip.style, { left: `${x}px`, top: `${y}px`, }); }); }, 400); } // TODO: astro:page-load tooltip.addEventListener("mouseenter", clearTimers); tooltip.addEventListener("mouseleave", hideLinkPreview); const events = [ ["mouseenter", showLinkPreview], ["mouseleave", hideLinkPreview], ["focus", showLinkPreview], ["blur", hideLinkPreview], ] as const; Array.from(elements).forEach((element) => { events.forEach(([event, listener]) => { element.addEventListener(event, listener); }); });