Wikipedia Previews Minimal Vanilla JS

Hover over this link --> JavaScript <-- to see the Wikipedia Preview for the word JavaScript

The HTML...

<span data-wikip="JavaScript">JavaScript</span>

The Code >>

<style>
[data-wikip] {
cursor: help;
text-decoration: underline dotted;
}

.wikip-tooltip {
position: fixed;
left: 0;
top: 0;
z-index: 10000;
max-width: min(320px, calc(100vw - 16px));
min-width: 180px;
padding: 10px 12px;
border: 1px solid #d7dbe0;
border-radius: 8px;
background: #ffffff;
color: #1f2328;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.16);
font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
pointer-events: none;
opacity: 0;
visibility: hidden;
transform: translateY(4px);
transition: opacity 0.14s ease, transform 0.14s ease, visibility 0s linear 0.14s;
}

.wikip-tooltip.is-visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
pointer-events: auto;
transition: opacity 0.14s ease, transform 0.14s ease, visibility 0s linear 0s;
}

.wikip-tooltip__arrow {
position: absolute;
width: 10px;
height: 10px;
background: #ffffff;
border-left: 1px solid #d7dbe0;
border-top: 1px solid #d7dbe0;
transform: rotate(45deg);
}

.wikip-tooltip[data-placement="top"] .wikip-tooltip__arrow {
bottom: -6px;
border-left: 0;
border-top: 0;
border-right: 1px solid #d7dbe0;
border-bottom: 1px solid #d7dbe0;
}

.wikip-tooltip[data-placement="bottom"] .wikip-tooltip__arrow {
top: -6px;
}

.wikip-tooltip__content p {
margin: 0;
}

.wikip-tooltip__content a {
color: #0969da;
text-decoration: underline;
}

.wikip-tooltip__content a:hover,
.wikip-tooltip__content a:focus {
color: #0550ae;
}

[data-wikip]:focus {
outline: 2px solid #0969da;
outline-offset: 2px;
border-radius: 4px;
}

@media (prefers-reduced-motion: reduce) {
.wikip-tooltip {
transition: none;
}
}
</style>

<script>
(() => {
const GAP = 10;
const EDGE = 8;

let activeWikipTrigger = null;
let wikipTooltip = null;
let wikipTooltipContent = null;
let wikipTooltipArrow = null;
let hideTimer = null;

function ensureWikipTooltip() {
if (wikipTooltip) return;

wikipTooltip = document.createElement("div");
wikipTooltip.className = "wikip-tooltip";
wikipTooltip.setAttribute("role", "tooltip");
wikipTooltip.hidden = true;

wikipTooltipArrow = document.createElement("div");
wikipTooltipArrow.className = "wikip-tooltip__arrow";
wikipTooltipArrow.setAttribute("aria-hidden", "true");

wikipTooltipContent = document.createElement("div");
wikipTooltipContent.className = "wikip-tooltip__content";

wikipTooltip.append(wikipTooltipArrow, wikipTooltipContent);
document.body.appendChild(wikipTooltip);

wikipTooltip.addEventListener("mouseenter", () => {
clearTimeout(hideTimer);
});

wikipTooltip.addEventListener("mouseleave", (event) => {
if (activeWikipTrigger && movedBetween(activeWikipTrigger, wikipTooltip, event)) return;
hideWikipTooltip();
});
}

function movedBetween(fromElement, toElement, event) {
const nextTarget = event.relatedTarget;
return nextTarget instanceof Node &&
(fromElement.contains(nextTarget) || toElement.contains(nextTarget));
}

async function fetchWikipExtract(pageTitle) {
const url =
"https://en.wikipedia.org/w/api.php?" +
new URLSearchParams({
origin: "*",
action: "parse",
page: pageTitle,
prop: "text",
format: "json",
section: "0"
});

const response = await fetch(url);
if (!response.ok) throw new Error(response.status);

const data = await response.json();
const html = data?.parse?.text?.["*"];
if (!html) throw new Error("No content");

const parsedDocument = new DOMParser().parseFromString(html, "text/html");
parsedDocument.querySelectorAll("table, style, script").forEach((element) => element.remove());

for (const paragraph of parsedDocument.querySelectorAll("p")) {
if (!paragraph.textContent.trim()) continue;

const contentNode = paragraph.cloneNode(true);
contentNode.querySelectorAll("sup.reference").forEach((element) => element.remove());

contentNode.querySelectorAll("a").forEach((link) => {
const href = link.getAttribute("href");
link.target = "_blank";
link.rel = "noopener noreferrer";

if (href && href.startsWith("/")) {
link.href = "https://en.wikipedia.org" + href;
}
});

if (contentNode.textContent.trim()) return contentNode;
}

throw new Error("No paragraph");
}

function positionWikipTooltip(triggerElement) {
const triggerRect = triggerElement.getBoundingClientRect();
const tooltipRect = wikipTooltip.getBoundingClientRect();

let placement = "top";
let top = triggerRect.top - tooltipRect.height - GAP;

if (top < EDGE) {
placement = "bottom";
top = triggerRect.bottom + GAP;
}

let left = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
left = Math.max(EDGE, Math.min(left, window.innerWidth - tooltipRect.width - EDGE));

if (placement === "bottom" && top + tooltipRect.height > window.innerHeight - EDGE) {
placement = "top";
top = Math.max(EDGE, triggerRect.top - tooltipRect.height - GAP);
}

wikipTooltip.dataset.placement = placement;
wikipTooltip.style.left = left + "px";
wikipTooltip.style.top = top + "px";

const arrowLeft = triggerRect.left + triggerRect.width / 2 - left - 5;
wikipTooltipArrow.style.left =
Math.max(12, Math.min(arrowLeft, tooltipRect.width - 22)) + "px";
}

async function showWikipTooltip(triggerElement) {
clearTimeout(hideTimer);
ensureWikipTooltip();
activeWikipTrigger = triggerElement;

if (
!triggerElement.hasAttribute("tabindex") &&
!/^(A|BUTTON|INPUT|SELECT|TEXTAREA)$/.test(triggerElement.tagName)
) {
triggerElement.tabIndex = 0;
}

const tooltipId =
triggerElement.dataset.wikipTooltipId ||
`wikip-tooltip-${Math.random().toString(36).slice(2)}`;

triggerElement.dataset.wikipTooltipId = tooltipId;
wikipTooltip.id = tooltipId;
triggerElement.setAttribute("aria-describedby", tooltipId);

wikipTooltipContent.textContent = "Loading…";
wikipTooltip.hidden = false;
wikipTooltip.classList.add("is-visible");
positionWikipTooltip(triggerElement);

try {
const pageTitle = triggerElement.dataset.wikip;

if (
triggerElement._wikipCacheKey !== pageTitle ||
!triggerElement._wikipCacheNode
) {
triggerElement._wikipCacheNode = await fetchWikipExtract(pageTitle);
triggerElement._wikipCacheKey = pageTitle;
}

wikipTooltipContent.replaceChildren(
triggerElement._wikipCacheNode.cloneNode(true)
);
} catch {
wikipTooltipContent.textContent = "Error. Try again.";
}

positionWikipTooltip(triggerElement);
}

function hideWikipTooltip() {
clearTimeout(hideTimer);

hideTimer = setTimeout(() => {
if (!wikipTooltip) return;

wikipTooltip.classList.remove("is-visible");
wikipTooltip.hidden = true;

if (activeWikipTrigger) {
activeWikipTrigger.removeAttribute("aria-describedby");
}

activeWikipTrigger = null;
}, 60);
}

function bindWikipTrigger(triggerElement) {
if (triggerElement.dataset.wikipBound) return;
triggerElement.dataset.wikipBound = "true";

triggerElement.addEventListener("mouseenter", () => showWikipTooltip(triggerElement));
triggerElement.addEventListener("focus", () => showWikipTooltip(triggerElement));

triggerElement.addEventListener("mouseleave", (event) => {
if (wikipTooltip && movedBetween(triggerElement, wikipTooltip, event)) return;
hideWikipTooltip();
});

triggerElement.addEventListener("blur", (event) => {
if (wikipTooltip && movedBetween(triggerElement, wikipTooltip, event)) return;
hideWikipTooltip();
});

triggerElement.addEventListener("keydown", (event) => {
if (event.key === "Escape") hideWikipTooltip();
});
}

function initWikipTooltips() {
document.querySelectorAll("[data-wikip]").forEach(bindWikipTrigger);
}

window.addEventListener(
"scroll",
() => {
if (wikipTooltip && activeWikipTrigger && !wikipTooltip.hidden) {
positionWikipTooltip(activeWikipTrigger);
}
},
true
);

window.addEventListener("resize", () => {
if (wikipTooltip && activeWikipTrigger && !wikipTooltip.hidden) {
positionWikipTooltip(activeWikipTrigger);
}
});

document.addEventListener("keydown", (event) => {
if (event.key === "Escape") hideWikipTooltip();
});

if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initWikipTooltips);
} else {
initWikipTooltips();
}
})();
</script>