Books: footnote hover popover for EPUB reader
Hovering over a footnote reference shows the footnote content in a floating popover; moving the mouse away dismisses it. On mobile, a tap toggles the popover with an ✕ close button. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fe91000e7c
commit
159aa6f340
2 changed files with 91 additions and 2 deletions
|
|
@ -1724,6 +1724,10 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h)
|
||||||
.hl-color-btn:hover { border-color:#fff; }
|
.hl-color-btn:hover { border-color:#fff; }
|
||||||
.hl-note-btn { background:none; border:1px solid var(--border); color:var(--fg); padding:2px 6px; border-radius:var(--radius); cursor:pointer; }
|
.hl-note-btn { background:none; border:1px solid var(--border); color:var(--fg); padding:2px 6px; border-radius:var(--radius); cursor:pointer; }
|
||||||
|
|
||||||
|
/* Footnote popover */
|
||||||
|
.footnote-popover { position:fixed; z-index:500; max-width:min(400px,90vw); max-height:220px; overflow-y:auto; background:var(--bg-card,#1a1a1a); border:1px solid var(--border); border-radius:var(--radius); padding:10px 28px 10px 12px; box-shadow:0 4px 20px rgba(0,0,0,.6); font-size:0.88em; line-height:1.55; pointer-events:auto; }
|
||||||
|
.footnote-popover-close { position:absolute; top:4px; right:6px; background:none; border:none; color:var(--fg-muted,#888); cursor:pointer; font-size:14px; padding:2px 4px; line-height:1; }
|
||||||
|
|
||||||
/* Highlight marks */
|
/* Highlight marks */
|
||||||
.reader-no-bold * { font-weight: normal !important; }
|
.reader-no-bold * { font-weight: normal !important; }
|
||||||
.reader-content img { max-width: 100%; height: auto; display: block; }
|
.reader-content img { max-width: 100%; height: auto; display: block; }
|
||||||
|
|
|
||||||
|
|
@ -2619,6 +2619,10 @@ function sanitizeEpubHtml(html) {
|
||||||
if (href.startsWith('http://') || href.startsWith('https://')) {
|
if (href.startsWith('http://') || href.startsWith('https://')) {
|
||||||
el.setAttribute('target', '_blank');
|
el.setAttribute('target', '_blank');
|
||||||
el.setAttribute('rel', 'noopener noreferrer');
|
el.setAttribute('rel', 'noopener noreferrer');
|
||||||
|
} else if (el.classList.contains('footnote') && href.startsWith('#')) {
|
||||||
|
el.dataset.footnoteRef = href.slice(1);
|
||||||
|
el.removeAttribute('href');
|
||||||
|
el.style.cursor = 'pointer';
|
||||||
} else {
|
} else {
|
||||||
el.removeAttribute('href');
|
el.removeAttribute('href');
|
||||||
el.style.cursor = 'default';
|
el.style.cursor = 'default';
|
||||||
|
|
@ -3219,8 +3223,13 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
|
||||||
let _immBarsVisible = true;
|
let _immBarsVisible = true;
|
||||||
|
|
||||||
function _immHandleTap(e) {
|
function _immHandleTap(e) {
|
||||||
// Ignore taps on interactive elements (buttons, links, inputs, settings panel)
|
// Ignore taps on interactive elements (buttons, links, inputs, settings panel, footnote popover)
|
||||||
if (e.target.closest('button, a, input, select, label, #reader-settings-panel, .reader-header')) return;
|
if (e.target.closest('button, a, input, select, label, #reader-settings-panel, .reader-header, .footnote-popover')) return;
|
||||||
|
// If a footnote popover is open, dismiss it instead of toggling bars
|
||||||
|
if (currentFootnotePopover && !currentFootnotePopover.contains(e.target)) {
|
||||||
|
dismissFootnotePopover();
|
||||||
|
return;
|
||||||
|
}
|
||||||
_immBarsVisible = !_immBarsVisible;
|
_immBarsVisible = !_immBarsVisible;
|
||||||
document.body.classList.toggle('reader-immersive', !_immBarsVisible);
|
document.body.classList.toggle('reader-immersive', !_immBarsVisible);
|
||||||
// Close settings panel when bars disappear
|
// Close settings panel when bars disappear
|
||||||
|
|
@ -4726,6 +4735,57 @@ function dismissHighlightPopover() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Footnote popover
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let currentFootnotePopover = null;
|
||||||
|
let _footnotePopoverTimer = null;
|
||||||
|
|
||||||
|
function showFootnotePopover(link) {
|
||||||
|
dismissFootnotePopover();
|
||||||
|
const targetEl = document.getElementById(link.dataset.footnoteRef);
|
||||||
|
if (!targetEl) return;
|
||||||
|
const container = targetEl.closest('.footnote') || targetEl.parentElement;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const clone = container.cloneNode(true);
|
||||||
|
clone.querySelector('sup')?.remove();
|
||||||
|
|
||||||
|
const popover = document.createElement('div');
|
||||||
|
popover.className = 'footnote-popover';
|
||||||
|
popover.innerHTML = clone.innerHTML;
|
||||||
|
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.className = 'footnote-popover-close';
|
||||||
|
closeBtn.textContent = '✕';
|
||||||
|
closeBtn.addEventListener('click', dismissFootnotePopover);
|
||||||
|
popover.appendChild(closeBtn);
|
||||||
|
|
||||||
|
const rect = link.getBoundingClientRect();
|
||||||
|
popover.style.top = (rect.bottom + 8) + 'px';
|
||||||
|
popover.style.left = Math.max(8, rect.left - 8) + 'px';
|
||||||
|
|
||||||
|
popover.addEventListener('mouseenter', () => clearTimeout(_footnotePopoverTimer));
|
||||||
|
popover.addEventListener('mouseleave', () => {
|
||||||
|
_footnotePopoverTimer = setTimeout(dismissFootnotePopover, 250);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(popover);
|
||||||
|
currentFootnotePopover = popover;
|
||||||
|
currentFootnotePopover._refLink = link;
|
||||||
|
|
||||||
|
const pr = popover.getBoundingClientRect();
|
||||||
|
if (pr.right > window.innerWidth - 8) popover.style.left = (window.innerWidth - pr.width - 8) + 'px';
|
||||||
|
if (pr.bottom > window.innerHeight - 8) popover.style.top = (rect.top - pr.height - 8) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissFootnotePopover() {
|
||||||
|
clearTimeout(_footnotePopoverTimer);
|
||||||
|
currentFootnotePopover?.remove();
|
||||||
|
currentFootnotePopover = null;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Radio sidebar (compact player)
|
// Radio sidebar (compact player)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -4852,4 +4912,29 @@ function openRadioSidebar() {
|
||||||
if (IS_AUTHENTICATED) {
|
if (IS_AUTHENTICATED) {
|
||||||
setInterval(refreshAllFeeds, 60 * 60 * 1000);
|
setInterval(refreshAllFeeds, 60 * 60 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Footnote hover (desktop) + tap (mobile) via event delegation
|
||||||
|
const _readerContentEl = $('reader-content');
|
||||||
|
if (_readerContentEl) {
|
||||||
|
_readerContentEl.addEventListener('mouseover', e => {
|
||||||
|
const link = e.target.closest('[data-footnote-ref]');
|
||||||
|
if (!link) return;
|
||||||
|
clearTimeout(_footnotePopoverTimer);
|
||||||
|
_footnotePopoverTimer = setTimeout(() => showFootnotePopover(link), 120);
|
||||||
|
});
|
||||||
|
_readerContentEl.addEventListener('mouseout', e => {
|
||||||
|
if (!e.target.closest('[data-footnote-ref]')) return;
|
||||||
|
_footnotePopoverTimer = setTimeout(dismissFootnotePopover, 250);
|
||||||
|
});
|
||||||
|
_readerContentEl.addEventListener('click', e => {
|
||||||
|
const link = e.target.closest('[data-footnote-ref]');
|
||||||
|
if (!link) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentFootnotePopover?._refLink === link) {
|
||||||
|
dismissFootnotePopover();
|
||||||
|
} else {
|
||||||
|
showFootnotePopover(link);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue