Epub improvements (#160)

* Improved navigation and search result highlighting in the EPUB reader

* refactor: implement JIT chunk restoration for robust navigation

- Fixes search and bookmark navigation failing in same-chapter transitions.
- Introduced JIT (Just-in-Time) HTML restoration in `epub_reader.js` to
  re-populate virtualized chunks before CFI resolution or search scrolling.
- Synchronized search highlighting and user highlights within
  newly-restored chunk segments.
- Improved search navigation accuracy by mapping occurrences to
  relative chunk indices.

* Implemented a custom text selection engine for the paginated EPUB reader to support cross-page selection and improved handle interaction.

* Implemented management of external file behavior and improved library filtering.

* Added support for toolbar customization in the EPUB reader.

* Added support for toolbar customization in the PDF reader.

* fix: rendering during auto-scroll and navigation on long pages

* Added a "Scroll to Top" feature to the auto-scroll controls in both EPUB and PDF readers.
This commit is contained in:
Aryan 2026-04-10 17:30:48 +05:30 committed by GitHub
parent db7e05ce63
commit 6a60aec0ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 3045 additions and 2060 deletions

View file

@ -774,15 +774,16 @@
document.addEventListener("DOMContentLoaded", initializeReaderContent);
}
window.CURRENT_SEARCH_QUERY = "";
window.clearSearchHighlights = function () {
window.CURRENT_SEARCH_QUERY = "";
document.querySelectorAll("mark.search-highlight").forEach(function (el) {
var parent = el.parentNode;
if (parent) {
while (el.firstChild) {
parent.insertBefore(el.firstChild, el);
}
parent.removeChild(el);
parent.normalize();
}
@ -792,10 +793,12 @@
window.highlightAllOccurrences = function (query) {
window.clearSearchHighlights();
window.CURRENT_SEARCH_QUERY = query;
if (!query || query.length < 2) return "JS: Query too short for highlighting.";
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
var nodesToModify = [];
var nodesToModify =[];
while ((node = walker.nextNode())) {
if (node.nodeValue.toLowerCase().includes(query.toLowerCase())) {
@ -811,11 +814,9 @@
tempDiv.innerHTML = textNode.nodeValue.replace(regex, '<mark class="search-highlight">$1</mark>');
var parent = textNode.parentNode;
while (tempDiv.firstChild) {
parent.insertBefore(tempDiv.firstChild, textNode);
}
parent.removeChild(textNode);
}
});
@ -823,17 +824,37 @@
return "JS: Highlighted " + document.querySelectorAll("mark.search-highlight").length + " occurrences.";
};
window.scrollToOccurrence = function (index) {
var highlights = document.querySelectorAll("mark.search-highlight");
window.scrollToChunkOccurrence = function (chunkIndex, relativeIndex) {
console.log("NavDiag: scrollToChunkOccurrence chunk=" + chunkIndex + ", relativeIdx=" + relativeIndex);
var chunkDiv = document.querySelector(`.chunk-container[data-chunk-index='${chunkIndex}']`);
if (highlights && index >= 0 && index < highlights.length) {
var element = highlights[index];
if (chunkDiv) {
let wasEmpty = false;
element.scrollIntoView({ behavior: "auto", block: "center", inline: "nearest" });
return "JS: Scrolled to occurrence " + index;
if (chunkDiv.innerHTML === "" && window.virtualization && window.virtualization.chunksData[chunkIndex]) {
console.log("NavDiag: Chunk was empty, restoring content before scrolling.");
chunkDiv.innerHTML = window.virtualization.chunksData[chunkIndex];
chunkDiv.style.height = "";
wasEmpty = true;
}
var highlights = chunkDiv.querySelectorAll("mark.search-highlight");
if ((wasEmpty || highlights.length === 0) && window.CURRENT_SEARCH_QUERY) {
window.highlightAllOccurrences(window.CURRENT_SEARCH_QUERY);
highlights = chunkDiv.querySelectorAll("mark.search-highlight");
}
if (highlights && highlights.length > 0) {
var targetIdx = (relativeIndex >= 0 && relativeIndex < highlights.length) ? relativeIndex : 0;
highlights[targetIdx].scrollIntoView({ behavior: "auto", block: "center", inline: "nearest" });
return "JS: Scrolled to relative occurrence " + targetIdx + " in chunk " + chunkIndex;
} else {
chunkDiv.scrollIntoView({ behavior: "auto", block: "center" });
return "JS: No highlights in chunk, scrolled to chunk center.";
}
}
return "JS: Occurrence " + index + " not found.";
return "JS: Chunk " + chunkIndex + " not found.";
};
window.removeHighlight = function () {
@ -1429,6 +1450,15 @@
let chunkElement = currentNode.querySelector(`.chunk-container[data-chunk-index="${chunkIndex}"]`);
if (chunkElement) {
if (chunkElement.innerHTML === "" && window.virtualization && window.virtualization.chunksData[chunkIndex]) {
console.log("CFI_DIAGNOSIS: Chunk " + chunkIndex + " was empty, restoring content for CFI resolution.");
chunkElement.innerHTML = window.virtualization.chunksData[chunkIndex];
chunkElement.style.height = "";
if (window.CURRENT_HIGHLIGHTS) {
window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);
}
}
let elementsInChunk = Array.from(chunkElement.childNodes).filter(n => n.nodeType === Node.ELEMENT_NODE);
if (indexInChunk >= 0 && indexInChunk < elementsInChunk.length) {
currentNode = elementsInChunk[indexInChunk];
@ -1649,7 +1679,7 @@
cleanCfi = cfi.substring(cfi.indexOf('@') + 1);
}
console.log("PosSaveDiag: JS scrollToCfi called with cleanCfi=" + cleanCfi);
console.log("NavDiag: JS scrollToCfi called with cleanCfi=" + cleanCfi);
if (!cleanCfi || !cleanCfi.startsWith('/')) {
if (window.CfiBridge && window.CfiBridge.onScrollFinished) {
@ -1719,6 +1749,7 @@
}
if (Math.abs(window.scrollY - targetScrollY) > 1) {
console.log("NavDiag: Scrolling to targetY=" + targetScrollY);
window.scrollTo({ top: targetScrollY, behavior: 'auto' });
}
@ -1947,6 +1978,9 @@
if (window.CURRENT_HIGHLIGHTS) {
window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);
}
if (window.CURRENT_SEARCH_QUERY) {
window.highlightAllOccurrences(window.CURRENT_SEARCH_QUERY);
}
}
} else {
if (div.innerHTML !== "") {
@ -2005,6 +2039,10 @@
if (window.CURRENT_HIGHLIGHTS) {
window.HighlightBridgeHelper.restoreHighlights(window.CURRENT_HIGHLIGHTS);
}
if (window.CURRENT_SEARCH_QUERY) {
window.highlightAllOccurrences(window.CURRENT_SEARCH_QUERY);
}
}
if (window.checkImagesForDiagnosis) {