`
// and not the body as the user sees it relative to the viewport.
// So you have to traverse the offsets till you get to the root.
function getOffset(element: HTMLElement) {
let top = element.offsetTop
let left = element.offsetLeft
let offsetParent = element.offsetParent as HTMLElement | null
while (offsetParent) {
left += offsetParent.offsetLeft
top += offsetParent.offsetTop
offsetParent = offsetParent.offsetParent as HTMLElement | null
}
return [top, left]
}
function getBoundingOffset(element: HTMLElement) {
const { top, left } = element.getBoundingClientRect()
return [top, left]
}
function popoverShow(target: HTMLLinkElement) {
if (popoverStartTimer) {
window.clearTimeout(popoverStartTimer)
}
// The mouse has been moved over a link. If this is the "first time",
// we want to delay showing the popover because it could be that the
// *intention* of the user was not to hover over, but they might have
// just moved the mouse over the link by "accident", or in a hurry
// on their way to something else.
// However, if they hover over the link because the popover is already
// open, which happens when you hover over the popover and back again
// to the link, then we don't want any delay.
if (target === currentlyOpen) {
popoverWrap(target)
} else {
popoverStartTimer = window.setTimeout(() => {
popoverWrap(target)
currentlyOpen = target
}, DELAY_SHOW)
}
}
function popoverHide() {
// Important to use `window.setTimeout` instead of `setTimeout` so
// that TypeScript knows which kind of timeout we're talking about.
// If you use plain `setTimeout` TypeScript might think it's a
// Node eventloop kinda timer.
if (popoverStartTimer) {
window.clearTimeout(popoverStartTimer)
}
popoverCloseTimer = window.setTimeout(() => {
const popover = getOrCreatePopoverGlobal()
popover.style.display = 'none'
// Reset because we're closing the popover, so we have to start from afresh.
currentlyOpen = null
}, DELAY_HIDE)
}
export function LinkPreviewPopover() {
useEffect(() => {
function showPopover(event: MouseEvent) {
// If the current window is too narrow, the popover is not useful.
// Since this is tested on every event callback here in the handler,
// if the window width has changed since the mount, the number
// will change accordingly.
if (window.innerWidth < 767) {
return
}
const target = event.currentTarget as HTMLLinkElement
popoverShow(target)
}
function hidePopover() {
popoverHide()
}
const links = Array.from(
document.querySelectorAll
(
'#article-contents a[href], #article-intro a[href]',
),
).filter((link) => {
// This filters out links that are not internal or in-page
// and the ones that are in-page anchor links next to the headings.
// Remember that `link.href` is always absolute because it comes
// from the DOM. So to test the pathname, we have to parse it
// and extract the pathname from the whole URL object.
const { pathname } = new URL(link.href)
return (
link.href.startsWith(window.location.origin) &&
!link.classList.contains('heading-link') &&
!pathname.startsWith('/public/') &&
!pathname.startsWith('/assets/') &&
// This skips those ToolPicker links with `data-tool="vscode"`
// attribute, for example.
!link.dataset.tool &&
!link.dataset.platform
)
})
// Ideally, we'd have an event listener for the entire container and
// the filter, at "runtime", within by filtering for the target
// elements we're interested in. However, this is not possible
// because then when you hover over the text in
// a tag like Link the target
// element is that of the `STRONG` tag.
// The reason it would be better to have a single event listener and
// filter is because it would work even if the DOM changes by
// adding new `` elements.
for (const link of links) {
link.addEventListener('mouseover', showPopover)
link.addEventListener('mouseout', hidePopover)
}
return () => {
for (const link of links) {
link.removeEventListener('mouseover', showPopover)
link.removeEventListener('mouseout', hidePopover)
}
}
}) // Note that this runs on every single mount
return null
}