From 351713ef8b60e46bb7bb7b916e63728d69670010 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Fri, 26 Dec 2025 07:20:43 -0500 Subject: [PATCH] Show definition popups on mouseover --- script.js | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 46 ++++++++++++++++++ 2 files changed, 185 insertions(+) diff --git a/script.js b/script.js index b472a25..228ad4e 100644 --- a/script.js +++ b/script.js @@ -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(); + } +}); diff --git a/style.css b/style.css index b51f3e5..8431945 100644 --- a/style.css +++ b/style.css @@ -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,