Lesen in Seiten auch im Browser und auf dem Desktop, mit Fußnoten unten auf der Seite und Doppelseite
63-pages.js (DioraPages): die Seitenansicht des E-Reader-Modus der Android-Apps, jetzt für alle – Seitenumbrüche vor angeschnittenen Zeilen, ein Kapitel auf einmal, Fußnoten am Seitenfuß wie im gedruckten Buch, Blättern per Tippen/Klick am Rand, Wischen, Pfeil-/Bild-/Leertaste und Mausrad. Neu: Doppelseite (zwei Seiten nebeneinander ab 900 px Breite). Einstellung „Scrollen | Seiten“ (+ „Fußnoten unten“, „Doppelseite“) im Schrift-&-Layout-Panel, je Gerät. Fußnoten: Erkennung und Auflösung aus diora-android native-ui.js übernommen (40-epub.js, 78-footnotes.js) – die Notiz wird im richtigen Kapitel gesucht (IDs wiederholen sich je Datei), auch das Popover zeigt jetzt genau die Notiz. Vorbereitete Bücher werden einmal neu aufbereitet. Behoben gegenüber der App-Fassung: ein Verweis in der ersten Zeile einer Seite (hochgestellt) zählte zur Vorseite, und ein Verweis, der samt Notiz nicht mehr passte, blieb ohne Notiz stehen – jetzt wandert er mit ihr auf die nächste Seite. Schnelles Blättern löst „Weit gescrollt“ nicht mehr aus. sw CACHE v157 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
308eed150a
commit
6bb774adb9
8 changed files with 1258 additions and 41 deletions
|
|
@ -1866,6 +1866,46 @@ html.nui-tts-on body.reader-immersive .reader-overlay { bottom: var(--bar-h); }
|
|||
.footnote-popover.reader-theme-sepia { background:#ede0c4; color:#3b2a1a; border-color:#c8b89a; }
|
||||
.footnote-popover.reader-theme-bright { background:#f0f0f0; color:#111; border-color:#bbb; }
|
||||
.footnote-popover-close { position:absolute; top:4px; right:6px; background:none; border:none; color:inherit; opacity:0.5; cursor:pointer; font-size:14px; padding:2px 4px; line-height:1; }
|
||||
.footnote-popover > :first-child { margin-top:0; }
|
||||
.footnote-popover > :last-child { margin-bottom:0; }
|
||||
|
||||
/* ---- Reading in pages (63-pages.js) ----
|
||||
Only the current chapter is laid out; the others stay in the DOM (block
|
||||
indices, search, notes, anchors). No free scrolling: the pager moves. */
|
||||
#reader-content.nui-window > [data-epub-src]:not(.nui-ch-current) { display:none; }
|
||||
/* Room below the chapter, so its last page can show just the remaining text
|
||||
(then the page's colour) instead of scrolling back over lines already read. */
|
||||
#reader-content.nui-window::after { content:""; display:block; height:100vh; }
|
||||
#reader-content.nui-window { overflow-y:hidden; -ms-overflow-style:none; scrollbar-width:none; }
|
||||
#reader-content.nui-window::-webkit-scrollbar { display:none; }
|
||||
/* Masks: the line cut at the page's bottom and whatever sits in the top gap. */
|
||||
.nui-page-mask { position:fixed; z-index:250; pointer-events:none; display:none; }
|
||||
/* Footnotes at the foot of the page, like a printed book. */
|
||||
.nui-page-notes { position:fixed; z-index:255; overflow:hidden; padding:0 0 4px; box-sizing:border-box;
|
||||
line-height:1.4; font-weight:400; cursor:pointer; }
|
||||
.nui-pn-rule { width:30%; border-top:1px solid currentColor; margin:2px 0 6px; }
|
||||
.nui-pn-item { margin:0 0 4px; padding-left:1.6em; text-indent:-1.6em; }
|
||||
.nui-pn-item p, .nui-pn-item div, .nui-pn-item aside, .nui-pn-item li { display:inline; margin:0; padding:0; text-indent:0; }
|
||||
.nui-pn-item p + p::before { content:" "; }
|
||||
.nui-pn-label { display:inline-block; min-width:1.6em; text-indent:0; font-weight:700; }
|
||||
.nui-pn-item a { color:inherit; text-decoration:none; }
|
||||
.nui-pn-item img { display:none; }
|
||||
/* Cut off: say so, a tap shows the whole note. */
|
||||
.nui-page-notes.nui-pn-cut::after { content:"… mehr"; position:absolute; right:0; bottom:2px; padding-left:8px;
|
||||
background:inherit; font-weight:700; }
|
||||
/* Two pages side by side (a spread, 63-pages.js): the right one is a copy of
|
||||
the chapter, scrolled to the page after the left one. */
|
||||
#reader-spread-page { display:none; }
|
||||
#reader-overlay.reader-spread #reader-content,
|
||||
#reader-overlay.reader-spread #reader-spread-page { flex:1 1 0; min-width:0; }
|
||||
#reader-overlay.reader-spread #reader-spread-page { display:block; overflow:hidden; box-shadow:inset 1px 0 0 var(--border);
|
||||
user-select:none; -webkit-user-select:none; cursor:default; }
|
||||
#reader-spread-page::after { content:""; display:block; height:100vh; }
|
||||
/* The browser's line under the page (the apps have their own footer). */
|
||||
#reader-page-footer { display:flex; justify-content:space-between; gap:12px; flex-shrink:0; padding:4px 16px 6px;
|
||||
font-size:12px; color:var(--muted,#888); font-variant-numeric:tabular-nums; }
|
||||
.reader-theme-sepia #reader-page-footer { background:#f5e6c8; color:#7a6650; }
|
||||
.reader-theme-bright #reader-page-footer { background:#fff; color:#777; }
|
||||
|
||||
/* Page numbers of the printed edition (epub:type="pagebreak") */
|
||||
.reader-content .epub-pagenum { display:inline-block; font-size:0.62em; line-height:1.3; font-weight:400; font-style:normal; letter-spacing:0; text-transform:none; vertical-align:0.35em; padding:0 0.35em; margin-right:0.4em; border:1px solid currentColor; border-radius:3px; opacity:0.45; user-select:none; }
|
||||
|
|
|
|||
|
|
@ -228,14 +228,41 @@ function _injectImageBlobs(html, chapterDir, imageMap) {
|
|||
// Calibre/zeno.org conversions) use neither and just wrap a plain link to
|
||||
// another chapter file's fragment in a <sup> — in either direction
|
||||
// (<sup><a>…</a></sup> or <a><sup>…</sup></a>). Recognize all of these
|
||||
// rather than only the single hardcoded class name.
|
||||
// rather than only the single hardcoded class name. Also role="doc-noteref",
|
||||
// more note classes, and a bare note number as the link's text ("3", "[12]", "*").
|
||||
function _looksLikeFootnoteLink(el, href) {
|
||||
if (!href.includes('#')) return false;
|
||||
const cls = (el.getAttribute('class') || '').toLowerCase();
|
||||
if (/\bnote|\bfootnote|\bfn\b/.test(cls)) return true;
|
||||
if (/\b(fnref|noteref|footnote-?ref|endnote-?ref)\b/.test(cls)) return true;
|
||||
const epubType = (el.getAttribute('epub:type') || '').toLowerCase();
|
||||
if (epubType.includes('noteref')) return true;
|
||||
return !!(el.closest('sup') || el.querySelector('sup'));
|
||||
if ((el.getAttribute('role') || '').toLowerCase() === 'doc-noteref') return true;
|
||||
if (el.closest('sup') || el.querySelector('sup')) return true;
|
||||
return /^\s*[\[(]?\s*(\d{1,4}|[ivxlc]{1,6}|[a-z]|[*†‡§¶]+)\s*[\])]?\s*$/i.test(el.textContent || '');
|
||||
}
|
||||
|
||||
// The number at the start of a note that links back to the text ("3 Endnote…")
|
||||
// is a way back, not a reference to another note.
|
||||
const _NOTE_BOX_SELECTOR = '.footnote, .endnote, aside, [role="doc-footnote"], [role="doc-endnote"], ' +
|
||||
'[epub\\:type~="footnote"], [epub\\:type~="endnote"], [epub\\:type~="rearnote"]';
|
||||
function _isNoteBacklink(el) {
|
||||
const role = (el.getAttribute('role') || '').toLowerCase();
|
||||
const type = (el.getAttribute('epub:type') || '').toLowerCase();
|
||||
if (role === 'doc-backlink' || /backlink/.test(type)) return true;
|
||||
// Inside a note's box. Not the link itself: a reference can carry the class
|
||||
// "footnote" too (<a class="footnote" href="#ftn.fn1">, Suhrkamp/DocBook).
|
||||
if (el.parentElement && el.parentElement.closest(_NOTE_BOX_SELECTOR)) return true;
|
||||
const block = el.closest('p, li, div, dd');
|
||||
if (!block) return false;
|
||||
// A note: the block is a link target itself, or holds an empty anchor that is
|
||||
// (zeno.org / e-artnow: <p><a id="F258"></a><a href="…#N258">1</a> Note…</p>).
|
||||
const anchored = block.id || el.id || Array.prototype.some.call(block.querySelectorAll('[id]'),
|
||||
x => x !== el && !(x.textContent || '').trim());
|
||||
if (!anchored) return false;
|
||||
const text = (block.textContent || '').trim();
|
||||
const firstLink = Array.prototype.find.call(block.querySelectorAll('a'), a => (a.textContent || '').trim());
|
||||
return firstLink === el && text.startsWith((el.textContent || '').trim());
|
||||
}
|
||||
|
||||
// Chapters are XHTML, but parsed as HTML (forgiving with broken books). HTML
|
||||
|
|
@ -272,15 +299,18 @@ function sanitizeEpubHtml(html) {
|
|||
if (href.startsWith('http://') || href.startsWith('https://')) {
|
||||
el.setAttribute('target', '_blank');
|
||||
el.setAttribute('rel', 'noopener noreferrer');
|
||||
} else if (_looksLikeFootnoteLink(el, href)) {
|
||||
// The whole book is one concatenated DOM, so a fragment id resolves
|
||||
// via getElementById regardless of which chapter file it originally
|
||||
// pointed into — strip any "../Text/other.html" path prefix and keep
|
||||
// just the id after the last '#'.
|
||||
} else if (!(href.includes('#') && _isNoteBacklink(el)) && _looksLikeFootnoteLink(el, href)) {
|
||||
// data-footnote-ref (the id) marks the link as a reference; the href
|
||||
// as written (data-footnote-href) finds the note in the chapter file it
|
||||
// points into (78-footnotes.js) — ids like "fn1" repeat across files.
|
||||
el.dataset.footnoteRef = href.slice(href.indexOf('#') + 1);
|
||||
el.dataset.footnoteHref = href;
|
||||
el.removeAttribute('href');
|
||||
el.style.cursor = 'pointer';
|
||||
} else {
|
||||
// A link within the book (a cross-reference, a note's way back): kept
|
||||
// for the apps, which follow it (diora-android native-ui.js).
|
||||
if (href && !/^[a-z]+:/i.test(href)) el.dataset.internalHref = href;
|
||||
el.removeAttribute('href');
|
||||
el.style.cursor = 'default';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,9 +41,9 @@ let _parseCodeSig = null;
|
|||
|
||||
function _parsePreparationSig() {
|
||||
if (_parseCodeSig) return _parseCodeSig;
|
||||
// window.__dioraPrepareSig: what an app layer adds to the preparation (the
|
||||
// footnote detection in diora-android's native-ui.js) — a change there too.
|
||||
const src = [parseEpub, sanitizeEpubHtml, _injectImageBlobs, _resolveImageBlob, _parseEpubToc]
|
||||
// window.__dioraPrepareSig: what an app layer adds to the preparation — a
|
||||
// change there too.
|
||||
const src = [parseEpub, sanitizeEpubHtml, _looksLikeFootnoteLink, _isNoteBacklink, _injectImageBlobs, _resolveImageBlob, _parseEpubToc]
|
||||
.map(f => String(f)).join('\n') + (window.__dioraPrepareSig || '');
|
||||
let h = 5381;
|
||||
for (let i = 0; i < src.length; i++) h = ((h << 5) + h + src.charCodeAt(i)) | 0;
|
||||
|
|
|
|||
1018
static/js/app/63-pages.js
Normal file
1018
static/js/app/63-pages.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -113,6 +113,21 @@ function _readerWeight() {
|
|||
return Math.round(w / step) * step;
|
||||
}
|
||||
|
||||
// "Scrollen | Seiten" and, in pages, whether the notes stand at the page's foot
|
||||
// and whether two pages lie side by side.
|
||||
// The apps' e-reader mode always reads in pages: no choice there.
|
||||
function _readerModeButtons() {
|
||||
if (!window.DioraPages || DioraPages.isForced()) return '';
|
||||
const pages = DioraPages.isOn();
|
||||
return `
|
||||
<button class="btn btn-sm ${pages ? '' : 'active'}" data-rs-mode="scroll">Scrollen</button>
|
||||
<button class="btn btn-sm ${pages ? 'active' : ''}" data-rs-mode="pages">Seiten</button>
|
||||
<button class="btn btn-sm ${DioraPages.pageNotes() ? 'active' : ''}" id="rs-page-notes" style="${pages ? '' : 'display:none'}"
|
||||
title="Die Fußnoten stehen unten auf der Seite, auf der auf sie verwiesen wird">Fußnoten unten</button>
|
||||
<button class="btn btn-sm ${DioraPages.spread() ? 'active' : ''}" id="rs-page-spread" style="${pages ? '' : 'display:none'}"
|
||||
title="Zwei Seiten nebeneinander wie ein aufgeschlagenes Buch — ab einer Breite von 900 Pixeln">Doppelseite</button>`;
|
||||
}
|
||||
|
||||
function toggleSettingsPanel() {
|
||||
const overlay = $('reader-overlay');
|
||||
const contentEl = $('reader-content');
|
||||
|
|
@ -148,6 +163,7 @@ function toggleSettingsPanel() {
|
|||
<button class="btn btn-sm ${readerSettings.theme === 'dark' ? 'active' : ''}" data-rs-theme="dark">Dunkel</button>
|
||||
<button class="btn btn-sm ${readerSettings.theme === 'sepia' ? 'active' : ''}" data-rs-theme="sepia">Sepia</button>
|
||||
<button class="btn btn-sm ${readerSettings.theme === 'bright' ? 'active' : ''}" data-rs-theme="bright">Hell</button>
|
||||
${_readerModeButtons()}
|
||||
`;
|
||||
} else {
|
||||
panel.innerHTML = `
|
||||
|
|
@ -267,6 +283,27 @@ function toggleSettingsPanel() {
|
|||
saveReaderSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// Scrolling or pages (63-pages.js): per device, for every book.
|
||||
const syncMode = () => {
|
||||
const pages = DioraPages.isOn();
|
||||
panel.querySelectorAll('[data-rs-mode]').forEach(b => b.classList.toggle('active', (b.dataset.rsMode === 'pages') === pages));
|
||||
const notes = panel.querySelector('#rs-page-notes');
|
||||
if (notes) {
|
||||
notes.style.display = pages ? '' : 'none';
|
||||
notes.classList.toggle('active', DioraPages.pageNotes());
|
||||
}
|
||||
const spread = panel.querySelector('#rs-page-spread');
|
||||
if (spread) {
|
||||
spread.style.display = pages ? '' : 'none';
|
||||
spread.classList.toggle('active', DioraPages.spread());
|
||||
}
|
||||
};
|
||||
panel.querySelectorAll('[data-rs-mode]').forEach(btn => {
|
||||
btn.addEventListener('click', () => { DioraPages.setOn(btn.dataset.rsMode === 'pages'); syncMode(); });
|
||||
});
|
||||
panel.querySelector('#rs-page-notes')?.addEventListener('click', () => { DioraPages.togglePageNotes(); syncMode(); });
|
||||
panel.querySelector('#rs-page-spread')?.addEventListener('click', () => { DioraPages.setSpread(!DioraPages.spread()); syncMode(); });
|
||||
} else {
|
||||
const zoomRange = panel.querySelector('#rs-zoom');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,41 +1,131 @@
|
|||
'use strict';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Footnote popover
|
||||
// Footnotes: finding a reference's note, and the popover that shows it
|
||||
// ---------------------------------------------------------------------------
|
||||
// The whole book is one concatenated DOM, and ids like "fn1" repeat per
|
||||
// chapter file: a note is looked up in the chapter its link points into (the
|
||||
// link's original href, kept in data-footnote-href by sanitizeEpubHtml), and
|
||||
// only exactly the note is shown — not the parent around an id that sits on
|
||||
// the note's own <p>/<aside>/<li>, which can be a whole chapter. The same
|
||||
// lookup serves the popover here, the notes at the foot of a page
|
||||
// (63-pages.js) and the apps' note sheet (diora-android native-ui.js).
|
||||
|
||||
const _NOTE_INLINE = /^(A|SPAN|SUP|SUB|B|I|EM|STRONG|SMALL|CODE)$/;
|
||||
|
||||
function noteHrefOf(link) {
|
||||
return link.dataset.footnoteHref || ('#' + (link.dataset.footnoteRef || ''));
|
||||
}
|
||||
|
||||
function _noteChapterFor(fromEl, href) {
|
||||
const chapter = fromEl.closest('[data-epub-src]');
|
||||
const file = href.split('#')[0];
|
||||
if (!file || !chapter) return chapter;
|
||||
const src = chapter.dataset.epubSrc || '';
|
||||
const dir = src.includes('/') ? src.slice(0, src.lastIndexOf('/') + 1) : '';
|
||||
const target = resolveEpubPath(dir, file);
|
||||
return document.querySelector(`#reader-content > [data-epub-src="${CSS.escape(target)}"]`) || chapter;
|
||||
}
|
||||
|
||||
// The element `href` (as written in the book, relative to fromEl's chapter) points at.
|
||||
function resolveNoteTarget(fromEl, href) {
|
||||
const id = decodeURIComponent(href.slice(href.indexOf('#') + 1));
|
||||
const scope = _noteChapterFor(fromEl, href);
|
||||
const content = document.getElementById('reader-content');
|
||||
let el = id && scope ? scope.querySelector(`[id="${CSS.escape(id)}"]`) : null;
|
||||
if (!el && id && content) el = content.querySelector(`[id="${CSS.escape(id)}"]`);
|
||||
if (!el && !id) el = scope; // link to a whole chapter file
|
||||
return el;
|
||||
}
|
||||
|
||||
// Converters (zeno.org / e-artnow, some Calibre output) park an empty
|
||||
// <a id="F537"></a> right *before* the note's paragraph instead of inside it —
|
||||
// or at the start of it (then the paragraph is the note, not what follows).
|
||||
function _noteAfterEmptyAnchor(target) {
|
||||
if ((target.textContent || '').trim() || target.querySelector('img')) return null;
|
||||
const own = target.closest('p, li, dd, dt, td, blockquote');
|
||||
const ownText = own && !own.hasAttribute('data-epub-src') ? (own.textContent || '').trim() : '';
|
||||
if (ownText && !/^[\[(]?\d{1,4}[\])]?\.?$/.test(ownText)) return own;
|
||||
if (ownText) {
|
||||
// Only the number: the note (a poem, a long quote) runs on in the next
|
||||
// paragraphs, up to the next note's anchor.
|
||||
const box = document.createElement('div');
|
||||
box.appendChild(own.cloneNode(true));
|
||||
let len = 0;
|
||||
for (let next = own.nextElementSibling; next && len < 6000; next = next.nextElementSibling) {
|
||||
if (next.matches('h1, h2, h3, h4, h5, h6') || next.querySelector('[id]')) break;
|
||||
if (!(next.textContent || '').trim()) continue;
|
||||
box.appendChild(next.cloneNode(true));
|
||||
len += next.textContent.length;
|
||||
}
|
||||
return box;
|
||||
}
|
||||
for (let el = target; el && !el.hasAttribute('data-epub-src'); el = el.parentElement) {
|
||||
let next = el.nextElementSibling;
|
||||
while (next && !(next.textContent || '').trim()) next = next.nextElementSibling;
|
||||
if (next) return next.matches('h1, h2, h3, h4, h5, h6') ? null : next;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _noteBox(target) {
|
||||
const following = _noteAfterEmptyAnchor(target);
|
||||
if (following) return following;
|
||||
let box = target.closest(_NOTE_BOX_SELECTOR); // 40-epub.js
|
||||
if (box && (box.hasAttribute('data-epub-src') || box.id === 'reader-content')) box = null;
|
||||
if (!box) box = _NOTE_INLINE.test(target.tagName) ? (target.closest('p, li, dd, div, td, blockquote') || target) : target;
|
||||
// Never a whole chapter or section: fall back to the paragraph around the target.
|
||||
if (box.hasAttribute('data-epub-src') || (box.textContent || '').length > 6000) {
|
||||
box = target.closest('p, li, dd') || target;
|
||||
}
|
||||
return box;
|
||||
}
|
||||
|
||||
// A copy of the note at `target`, ready to show outside the book.
|
||||
function noteContentFor(target, label) {
|
||||
const clone = _noteBox(target).cloneNode(true);
|
||||
clone.querySelectorAll('[role="doc-backlink"], [epub\\:type~="backlink"]').forEach(e => e.remove());
|
||||
// A page of the printed edition that starts inside the note.
|
||||
clone.querySelectorAll('.epub-pagenum, [role="doc-pagebreak"], [epub\\:type~="pagebreak"]').forEach(e => e.remove());
|
||||
// Links back to the text ("↩", or the note's own number at its start).
|
||||
clone.querySelectorAll('a').forEach(a => {
|
||||
const t = (a.textContent || '').trim();
|
||||
if (/^[↩↑⤴^]+$/.test(t)) a.remove();
|
||||
});
|
||||
// The note's own number at its very start ("3 Endnote…", "[4] …"): whatever
|
||||
// shows the note labels it already.
|
||||
const first = Array.prototype.find.call(clone.querySelectorAll('a, sup, span, b, strong'),
|
||||
e => (e.textContent || '').trim());
|
||||
const firstText = first ? (first.textContent || '').trim() : '';
|
||||
const bare = t => t.replace(/[\[\]().\s]/g, '');
|
||||
if (first && firstText && (clone.textContent || '').trim().startsWith(firstText) &&
|
||||
(bare(firstText) === bare(label || '') || /^[\[(]?(\d{1,4}|[*†‡§]+)[\])]?\.?$/.test(firstText))) {
|
||||
first.remove();
|
||||
// …and the space that followed it.
|
||||
const walker = document.createTreeWalker(clone, NodeFilter.SHOW_TEXT, null, false);
|
||||
const t = walker.nextNode();
|
||||
if (t) t.nodeValue = t.nodeValue.replace(/^\s+/, '');
|
||||
}
|
||||
// No duplicate ids in the page (getElementById must keep finding the book's).
|
||||
clone.removeAttribute('id');
|
||||
clone.querySelectorAll('[id]').forEach(e => e.removeAttribute('id'));
|
||||
clone.querySelectorAll('mark.tts-current, mark.reader-search-match').forEach(m => m.replaceWith(...m.childNodes));
|
||||
return clone;
|
||||
}
|
||||
|
||||
// The note of a reference link, or null.
|
||||
function noteForLink(link) {
|
||||
const target = resolveNoteTarget(link, noteHrefOf(link));
|
||||
return target ? noteContentFor(target, (link.textContent || '').trim()) : null;
|
||||
}
|
||||
|
||||
let currentFootnotePopover = null;
|
||||
let _footnotePopoverTimer = null;
|
||||
|
||||
function showFootnotePopover(link) {
|
||||
dismissFootnotePopover();
|
||||
const targetEl = document.getElementById(link.dataset.footnoteRef);
|
||||
if (!targetEl) return;
|
||||
// The note's paragraph, also when the id sits on an inline element in it
|
||||
// (<p><span><a id="F400" …>143</a></span> Note…</p>).
|
||||
const inline = /^(A|SPAN|SUP|SUB|B|I|EM|STRONG|SMALL)$/.test(targetEl.tagName);
|
||||
const container = targetEl.closest('.footnote') ||
|
||||
(inline && targetEl.closest('p, li, dd, blockquote')) || targetEl.parentElement;
|
||||
if (!container) return;
|
||||
|
||||
const clone = container.cloneNode(true);
|
||||
clone.querySelector('sup')?.remove();
|
||||
clone.querySelectorAll('.epub-pagenum').forEach(e => e.remove());
|
||||
// The note's own number at its start, a link back to the text ("1 Note…").
|
||||
const first = Array.from(clone.querySelectorAll('a')).find(a => a.textContent.trim());
|
||||
const num = first ? first.textContent.trim() : '';
|
||||
if (/^[\[(]?\d{1,4}[\])]?\.?$/.test(num) && clone.textContent.trim().startsWith(num)) first.remove();
|
||||
// Only the number: the note (a poem, a long quote) runs on in the next
|
||||
// paragraphs, up to the next note's anchor.
|
||||
if (!clone.textContent.trim()) {
|
||||
let len = 0;
|
||||
for (let next = container.nextElementSibling; next && len < 6000; next = next.nextElementSibling) {
|
||||
if (/^H[1-6]$/.test(next.tagName) || next.querySelector('[id]')) break;
|
||||
if (!next.textContent.trim()) continue;
|
||||
clone.appendChild(next.cloneNode(true));
|
||||
len += next.textContent.length;
|
||||
}
|
||||
}
|
||||
const note = noteForLink(link);
|
||||
if (!note) return;
|
||||
|
||||
const popover = document.createElement('div');
|
||||
popover.className = 'footnote-popover';
|
||||
|
|
@ -43,7 +133,7 @@ function showFootnotePopover(link) {
|
|||
['reader-theme-sepia', 'reader-theme-bright'].forEach(cls => {
|
||||
if (_overlay?.classList.contains(cls)) popover.classList.add(cls);
|
||||
});
|
||||
popover.innerHTML = clone.innerHTML;
|
||||
popover.appendChild(note);
|
||||
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'footnote-popover-close';
|
||||
|
|
@ -66,7 +156,7 @@ function showFootnotePopover(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';
|
||||
if (pr.bottom > window.innerHeight - 8) popover.style.top = Math.max(8, rect.top - pr.height - 8) + 'px';
|
||||
}
|
||||
|
||||
function dismissFootnotePopover() {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* diora service worker — caches the app shell for offline use.
|
||||
*/
|
||||
|
||||
const CACHE = 'diora-v156';
|
||||
const CACHE = 'diora-v157';
|
||||
const SHELL = [
|
||||
'/static/css/app.css',
|
||||
'/static/js/app/01-core.js',
|
||||
|
|
@ -31,6 +31,7 @@ const SHELL = [
|
|||
'/static/js/app/58-pdf.js',
|
||||
'/static/js/app/60-tts.js',
|
||||
'/static/js/app/62-reader.js',
|
||||
'/static/js/app/63-pages.js',
|
||||
'/static/js/app/64-reader-settings.js',
|
||||
'/static/js/app/66-pdf-paginated.js',
|
||||
'/static/js/app/68-bookmarks.js',
|
||||
|
|
|
|||
|
|
@ -386,6 +386,7 @@
|
|||
<script src="/static/js/app/58-pdf.js"></script>
|
||||
<script src="/static/js/app/60-tts.js"></script>
|
||||
<script src="/static/js/app/62-reader.js"></script>
|
||||
<script src="/static/js/app/63-pages.js"></script>
|
||||
<script src="/static/js/app/64-reader-settings.js"></script>
|
||||
<script src="/static/js/app/66-pdf-paginated.js"></script>
|
||||
<script src="/static/js/app/68-bookmarks.js"></script>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue