Show definition popups on mouseover
This commit is contained in:
parent
d3b047cab2
commit
180832b84c
2 changed files with 185 additions and 0 deletions
139
script.js
139
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();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
46
style.css
46
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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue