Show definition popups on mouseover

This commit is contained in:
Jared Miller 2025-12-26 07:20:43 -05:00
parent d3b047cab2
commit 180832b84c
No known key found for this signature in database
2 changed files with 185 additions and 0 deletions

139
script.js
View file

@ -351,3 +351,142 @@ window.addEventListener("hashchange", () => {
tocList?.querySelector(`a[href="#${id}"]`)?.scrollIntoView({ block: "nearest" });
}
});
// Entry popup preview on hover
const popup = document.createElement("div");
popup.className = "entry-popup";
popup.hidden = true;
document.body.appendChild(popup);
let hoverTimer = null;
let closeTimer = null;
const HOVER_DELAY = 500;
const CLOSE_DELAY = 500;
const getEntryContent = (id) => {
const titleEl = document.getElementById(id);
if (!titleEl) return null;
const allTitles = Array.from(document.querySelectorAll(".entry-title"));
const titleIndex = allTitles.indexOf(titleEl);
const nextTitle = allTitles[titleIndex + 1] || null;
const range = document.createRange();
range.setStartBefore(titleEl);
if (nextTitle) {
range.setEndBefore(nextTitle);
} else {
range.setEndAfter(contentEl.lastChild);
}
const content = range.cloneContents();
const clonedTitle = content.querySelector(".entry-title");
if (clonedTitle) {
clonedTitle.removeAttribute("id");
}
return content;
};
const positionPopup = (linkRect) => {
const pad = 8;
const popupRect = popup.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
let top = linkRect.top - popupRect.height - pad;
let left = linkRect.left;
if (top < pad) {
top = linkRect.bottom + pad;
}
if (left + popupRect.width > vw - pad) {
left = vw - popupRect.width - pad;
}
if (left < pad) {
left = pad;
}
popup.style.top = `${top}px`;
popup.style.left = `${left}px`;
};
const showPopup = (link) => {
const href = link.getAttribute("href");
if (!href?.startsWith("#entry-")) return;
const id = href.slice(1);
const content = getEntryContent(id);
if (!content) return;
popup.innerHTML = "";
popup.appendChild(content);
popup.hidden = false;
popup.style.top = "0";
popup.style.left = "0";
popup.classList.remove("visible");
requestAnimationFrame(() => {
popup.scrollTop = 0;
const linkRect = link.getBoundingClientRect();
positionPopup(linkRect);
popup.classList.add("visible");
});
};
const hidePopup = () => {
popup.classList.remove("visible");
setTimeout(() => {
if (!popup.classList.contains("visible")) {
popup.hidden = true;
}
}, 200);
};
const clearTimers = () => {
clearTimeout(hoverTimer);
clearTimeout(closeTimer);
hoverTimer = null;
closeTimer = null;
};
const isEntryLink = (el) =>
el?.tagName === "A" &&
el.classList.contains("entry-link") &&
el.getAttribute("href")?.startsWith("#entry-");
contentEl?.addEventListener("mouseenter", (e) => {
if (isMobile()) return;
if (!isEntryLink(e.target)) return;
clearTimers();
hoverTimer = setTimeout(() => showPopup(e.target), HOVER_DELAY);
}, true);
contentEl?.addEventListener("mouseleave", (e) => {
if (isMobile()) return;
if (!isEntryLink(e.target)) return;
clearTimers();
closeTimer = setTimeout(hidePopup, CLOSE_DELAY);
}, true);
popup.addEventListener("mouseenter", () => {
clearTimers();
});
popup.addEventListener("mouseleave", () => {
clearTimers();
closeTimer = setTimeout(hidePopup, CLOSE_DELAY);
});
popup.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
clearTimers();
hidePopup();
}
});

View file

@ -267,6 +267,52 @@ body.toc-collapsed .toc-toggle::before {
grid-template-columns: 0 minmax(0, 1fr);
}
/* Entry popup preview */
.entry-popup {
position: fixed;
z-index: 300;
max-width: min(420px, 90vw);
max-height: min(320px, 60vh);
overflow-y: auto;
background: var(--paper);
border-radius: 12px;
padding: 16px 20px;
box-shadow:
0 8px 32px var(--shadow),
0 2px 8px rgba(45, 34, 26, 0.1);
border: 1px solid rgba(70, 50, 36, 0.15);
font-size: 0.95rem;
line-height: 1.55;
opacity: 0;
transform: translateY(4px);
transition:
opacity 0.2s ease,
transform 0.2s ease;
pointer-events: none;
}
.entry-popup.visible {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.entry-popup .entry-title {
animation: none;
}
.entry-popup p {
margin: 0 0 0.75em;
}
.entry-popup p:last-child {
margin-bottom: 0;
}
.entry-popup .entry-link {
pointer-events: auto;
}
@media (max-width: 700px) {
.page,
.hero,