[add] highlight position node on start & on resize (easier to find your place)

This commit is contained in:
Evan Reichard 2023-10-15 23:23:58 -04:00
parent 5d9c0804bd
commit d8ee1f0747
3 changed files with 138 additions and 65 deletions

View File

@ -7,6 +7,7 @@ class EBookReader {
pages: 0, pages: 0,
percentage: 0, percentage: 0,
progress: "", progress: "",
progressElement: null,
readActivity: [], readActivity: [],
words: 0, words: 0,
}; };
@ -15,9 +16,11 @@ class EBookReader {
// Set Variables // Set Variables
Object.assign(this.bookState, bookState); Object.assign(this.bookState, bookState);
// Load Settings
this.loadSettings();
// Load EPUB // Load EPUB
this.book = ePub(file, { openAs: "epub" }); this.book = ePub(file, { openAs: "epub" });
window.book = this.book;
// Render // Render
this.rendition = this.book.renderTo("viewer", { this.rendition = this.book.renderTo("viewer", {
@ -30,9 +33,6 @@ class EBookReader {
// Setup Reader // Setup Reader
this.book.ready.then(this.setupReader.bind(this)); this.book.ready.then(this.setupReader.bind(this));
// Load Settings
this.loadSettings();
// Initialize // Initialize
this.initDevice(); this.initDevice();
this.initWakeLock(); this.initWakeLock();
@ -46,9 +46,8 @@ class EBookReader {
**/ **/
async setupReader() { async setupReader() {
// Load Progress // Load Progress
let currentCFI = await this.fromProgress(this.bookState.progress); let { cfi } = await this.getCFIFromXPath(this.bookState.progress);
if (!currentCFI) this.bookState.currentWord = 0; if (!cfi) this.bookState.currentWord = 0;
await this.rendition.display(currentCFI);
let getStats = function () { let getStats = function () {
// Start Timer // Start Timer
@ -64,7 +63,12 @@ class EBookReader {
// Register Content Hook // Register Content Hook
this.rendition.hooks.content.register(getStats); this.rendition.hooks.content.register(getStats);
getStats(); await this.rendition.display(cfi);
// Highlight Element - DOM Has Element
let { element } = await this.getCFIFromXPath(this.bookState.progress);
this.bookState.progressElement = element;
this.highlightPositionMarker();
} }
initDevice() { initDevice() {
@ -144,19 +148,19 @@ class EBookReader {
/** /**
* Set theme & meta theme color * Set theme & meta theme color
**/ **/
setTheme(themeName) { setTheme(newTheme) {
// Update Settings // Update Settings
this.readerSettings.theme = themeName; this.readerSettings.theme = newTheme;
this.saveSettings(); this.saveSettings();
// Set Reader Theme // Set Reader Theme
this.rendition.themes.select(themeName); this.rendition.themes.select(newTheme);
// Get Reader Theme // Get Reader Theme
let themeColorEl = document.querySelector("[name='theme-color']"); let themeColorEl = document.querySelector("[name='theme-color']");
let themeStyleSheet = document.querySelector("#themes").sheet; let themeStyleSheet = document.querySelector("#themes").sheet;
let themeStyleRule = Array.from(themeStyleSheet.cssRules).find( let themeStyleRule = Array.from(themeStyleSheet.cssRules).find(
(item) => item.selectorText == "." + themeName (item) => item.selectorText == "." + newTheme
); );
// Match Reader Theme // Match Reader Theme
@ -164,6 +168,41 @@ class EBookReader {
let backgroundColor = themeStyleRule.style.backgroundColor; let backgroundColor = themeStyleRule.style.backgroundColor;
themeColorEl.setAttribute("content", backgroundColor); themeColorEl.setAttribute("content", backgroundColor);
document.body.style.backgroundColor = backgroundColor; document.body.style.backgroundColor = backgroundColor;
// Update Position Highlight Theme
this.rendition.getContents().forEach((item) => {
item.document.querySelectorAll(".highlight").forEach((el) => {
Object.assign(el.style, {
background: backgroundColor,
});
});
});
}
highlightPositionMarker() {
if (!this.bookState.progressElement) return;
// Remove Existing
this.rendition.getContents().forEach((item) => {
item.document.querySelectorAll(".highlight").forEach((el) => {
el.removeAttribute("style");
el.classList.remove("highlight");
});
});
// Compute Style
let backgroundColor = getComputedStyle(
this.bookState.progressElement.ownerDocument.body
).backgroundColor;
// Set Style
Object.assign(this.bookState.progressElement.style, {
background: backgroundColor,
filter: "invert(0.2)",
});
// Update Class
this.bookState.progressElement.classList.add("highlight");
} }
/** /**
@ -196,12 +235,17 @@ class EBookReader {
let topBar = document.querySelector("#top-bar"); let topBar = document.querySelector("#top-bar");
let bottomBar = document.querySelector("#bottom-bar"); let bottomBar = document.querySelector("#bottom-bar");
// Local Functions
let getCFIFromXPath = this.getCFIFromXPath.bind(this);
let highlightPositionMarker = this.highlightPositionMarker.bind(this);
let nextPage = this.nextPage.bind(this); let nextPage = this.nextPage.bind(this);
let prevPage = this.prevPage.bind(this); let prevPage = this.prevPage.bind(this);
let saveSettings = this.saveSettings.bind(this); let saveSettings = this.saveSettings.bind(this);
// Font Scaling // Local Vars
let readerSettings = this.readerSettings; let readerSettings = this.readerSettings;
let bookState = this.bookState;
this.rendition.hooks.render.register(function (doc, data) { this.rendition.hooks.render.register(function (doc, data) {
let renderDoc = doc.document; let renderDoc = doc.document;
@ -227,15 +271,10 @@ class EBookReader {
// ------------------------------------------------ // // ------------------------------------------------ //
// ---------------- Resize Helpers ---------------- // // ---------------- Resize Helpers ---------------- //
// ------------------------------------------------ // // ------------------------------------------------ //
let isScaling = false;
let lastScale = 1; let lastScale = 1;
let lastLocation = undefined; let isScaling = false;
let debounceID = undefined; let timeoutID = undefined;
let debounceGesture = () => { let cfiLocation = undefined;
this.display(lastLocation.start.cfi);
lastLocation = undefined;
isScaling = false;
};
// Gesture Listener // Gesture Listener
renderDoc.addEventListener( renderDoc.addEventListener(
@ -244,16 +283,15 @@ class EBookReader {
e.preventDefault(); e.preventDefault();
isScaling = true; isScaling = true;
clearTimeout(debounceID); clearTimeout(timeoutID);
if (!cfiLocation)
({ cfi: cfiLocation } = await getCFIFromXPath(bookState.progress));
if (!lastLocation) {
lastLocation = await this.currentLocation();
} else {
// Damped Scale // Damped Scale
readerSettings.fontSize = readerSettings.fontSize =
(readerSettings.fontSize || 1) + (e.scale - lastScale) / 5; (readerSettings.fontSize || 1) + (e.scale - lastScale) / 5;
lastScale = e.scale; lastScale = e.scale;
saveSettings();
// Update Font Size // Update Font Size
renderDoc.documentElement.style.setProperty( renderDoc.documentElement.style.setProperty(
@ -261,8 +299,20 @@ class EBookReader {
(readerSettings.fontSize || 1) + "em" (readerSettings.fontSize || 1) + "em"
); );
debounceID = setTimeout(debounceGesture, 200); timeoutID = setTimeout(() => {
} // Display Position
this.display(cfiLocation);
// Reset Variables
isScaling = false;
cfiLocation = undefined;
// Highlight Location
highlightPositionMarker();
// Save Settings (Font Size)
saveSettings();
}, 250);
}.bind(this), }.bind(this),
true true
); );
@ -486,7 +536,11 @@ class EBookReader {
this.updateBookStats(stats); this.updateBookStats(stats);
// Update & Flush Progress // Update & Flush Progress
this.bookState.progress = await this.toProgress(); let currentCFI = await this.rendition.currentLocation();
let { element, xpath } = await this.getXPathFromCFI(currentCFI.start.cfi);
this.bookState.progress = xpath;
this.bookState.progressElement = element;
this.flushProgress(); this.flushProgress();
} }
@ -512,7 +566,10 @@ class EBookReader {
this.updateBookStats(stats); this.updateBookStats(stats);
// Update & Flush Progress // Update & Flush Progress
this.bookState.progress = await this.toProgress(); let currentCFI = await this.rendition.currentLocation();
let { element, xpath } = await this.getXPathFromCFI(currentCFI.start.cfi);
this.bookState.progress = xpath;
this.bookState.progressElement = element;
this.flushProgress(); this.flushProgress();
} }
@ -690,17 +747,21 @@ class EBookReader {
/** /**
* Get XPath from current location * Get XPath from current location
**/ **/
async toProgress() { async getXPathFromCFI(cfi) {
// Get DocFragment (current book spline index) // Get DocFragment (current book spline index)
let currentPos = await this.rendition.currentLocation(); let startCFI = cfi.replace("epubcfi(", "");
let docFragmentIndex = currentPos.start.index + 1; let docFragmentIndex =
this.book.spine.spineItems.find((item) =>
startCFI.startsWith(item.cfiBase)
).index + 1;
// Base Progress // Base Progress
let newPos = "/body/DocFragment[" + docFragmentIndex + "]/body"; let newPos = "/body/DocFragment[" + docFragmentIndex + "]/body";
// Get first visible node // Get first visible node
let contents = this.rendition.getContents()[0]; let contents = this.rendition.getContents()[0];
let node = contents.range(currentPos.start.cfi).startContainer; let node = contents.range(cfi).startContainer;
let element = null;
// Walk upwards and build progress until body // Walk upwards and build progress until body
let childPos = ""; let childPos = "";
@ -709,6 +770,8 @@ class EBookReader {
switch (node.nodeType) { switch (node.nodeType) {
case Node.ELEMENT_NODE: case Node.ELEMENT_NODE:
// Store First Element Node
if (!element) element = node;
let relativeIndex = let relativeIndex =
Array.from(node.parentNode.children) Array.from(node.parentNode.children)
.filter((item) => item.nodeName == node.nodeName) .filter((item) => item.nodeName == node.nodeName)
@ -742,44 +805,51 @@ class EBookReader {
node = node.parentNode; node = node.parentNode;
} }
let xpath = newPos + childPos;
// Return derived progress // Return derived progress
return newPos + childPos; return { xpath, element };
} }
/** /**
* Get CFI from XPath * Get CFI from current location
**/ **/
async fromProgress(progress) { async getCFIFromXPath(xpath) {
// Progress Reference - Example: /body/DocFragment[15]/body/div[10]/text().184 // XPath Reference - Example: /body/DocFragment[15]/body/div[10]/text().184
// //
// - /body/DocFragment[15] = 15th item in book spline // - /body/DocFragment[15] = 15th item in book spline
// - [...]/body/div[10] = 10th child div under body (direct descendents only) // - [...]/body/div[10] = 10th child div under body (direct descendents only)
// - [...]/text().184 = text node of parent, character offset @ 184 chars? // - [...]/text().184 = text node of parent, character offset @ 184 chars?
// No Progress // No XPath
if (!progress || progress == "") return; if (!xpath || xpath == "") return;
// Match Document Fragment Index // Match Document Fragment Index
let fragMatch = progress.match(/^\/body\/DocFragment\[(\d+)\]/); let fragMatch = xpath.match(/^\/body\/DocFragment\[(\d+)\]/);
if (!fragMatch) { if (!fragMatch) {
console.warn("No Progress Match"); console.warn("No XPath Match");
return; return;
} }
// Match Item Index // Match Item Index
let indexMatch = progress.match(/\.(\d+)$/); let indexMatch = xpath.match(/\.(\d+)$/);
let itemIndex = indexMatch ? parseInt(indexMatch[1]) : 0; let itemIndex = indexMatch ? parseInt(indexMatch[1]) : 0;
// Get Spine Item // Get Spine Item
let spinePosition = parseInt(fragMatch[1]) - 1; let spinePosition = parseInt(fragMatch[1]) - 1;
let docItem = this.book.spine.get(spinePosition); let sectionItem = this.book.spine.get(spinePosition);
await sectionItem.load(this.book.load.bind(this.book));
// Required for docItem.document Access // Document Rendered > Document Not Rendered
await docItem.load(this.book.load.bind(this.book)); let docItem =
this.rendition
.getContents()
.find((item) => item.sectionIndex == spinePosition)?.document ||
sectionItem.document;
// Derive XPath & Namespace // Derive XPath & Namespace
let namespaceURI = docItem.document.documentElement.namespaceURI; let namespaceURI = docItem.documentElement.namespaceURI;
let remainingXPath = progress let remainingXPath = xpath
// Replace with new base // Replace with new base
.replace(fragMatch[0], "/html") .replace(fragMatch[0], "/html")
// Replace `.0` Ending Indexes // Replace `.0` Ending Indexes
@ -791,9 +861,9 @@ class EBookReader {
if (namespaceURI) remainingXPath = remainingXPath.replaceAll("/", "/ns:"); if (namespaceURI) remainingXPath = remainingXPath.replaceAll("/", "/ns:");
// Perform XPath // Perform XPath
let docSearch = docItem.document.evaluate( let docSearch = docItem.evaluate(
remainingXPath, remainingXPath,
docItem.document, docItem,
function (prefix) { function (prefix) {
if (prefix === "ns") { if (prefix === "ns") {
return namespaceURI; return namespaceURI;
@ -804,11 +874,14 @@ class EBookReader {
); );
// Get Element & CFI // Get Element & CFI
let matchedItem = docSearch.iterateNext(); let element = docSearch.iterateNext();
let matchedCFI = docItem.cfiFromElement(matchedItem); let cfi = sectionItem.cfiFromElement(element);
return matchedCFI;
return { cfi, element };
} }
// getElementFromXPath(xpath)
/** /**
* Get visible word count - used for reading stats * Get visible word count - used for reading stats
**/ **/

View File

@ -22,7 +22,7 @@
} }
html { html {
height: 100vh; height: calc(100% + env(safe-area-inset-bottom));
padding: env(safe-area-inset-top) env(safe-area-inset-right) padding: env(safe-area-inset-top) env(safe-area-inset-right)
0 env(safe-area-inset-left); 0 env(safe-area-inset-left);
} }

View File

@ -2,7 +2,7 @@
<a href="../">Documents</a> <a href="../">Documents</a>
{{end}} {{define "content"}} {{end}} {{define "content"}}
<div id="viewer" class="w-full h-screen"></div> <div id="viewer" class="w-full h-full"></div>
<script src="../../assets/reader/platform.js"></script> <script src="../../assets/reader/platform.js"></script>
<script src="../../assets/reader/jszip.min.js"></script> <script src="../../assets/reader/jszip.min.js"></script>