[add] highlight position node on start & on resize (easier to find your place)
This commit is contained in:
parent
5d9c0804bd
commit
d8ee1f0747
@ -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,25 +283,36 @@ class EBookReader {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
isScaling = true;
|
isScaling = true;
|
||||||
clearTimeout(debounceID);
|
clearTimeout(timeoutID);
|
||||||
|
|
||||||
if (!lastLocation) {
|
if (!cfiLocation)
|
||||||
lastLocation = await this.currentLocation();
|
({ cfi: cfiLocation } = await getCFIFromXPath(bookState.progress));
|
||||||
} 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;
|
||||||
|
|
||||||
|
// Update Font Size
|
||||||
|
renderDoc.documentElement.style.setProperty(
|
||||||
|
"--editor-font-size",
|
||||||
|
(readerSettings.fontSize || 1) + "em"
|
||||||
|
);
|
||||||
|
|
||||||
|
timeoutID = setTimeout(() => {
|
||||||
|
// Display Position
|
||||||
|
this.display(cfiLocation);
|
||||||
|
|
||||||
|
// Reset Variables
|
||||||
|
isScaling = false;
|
||||||
|
cfiLocation = undefined;
|
||||||
|
|
||||||
|
// Highlight Location
|
||||||
|
highlightPositionMarker();
|
||||||
|
|
||||||
|
// Save Settings (Font Size)
|
||||||
saveSettings();
|
saveSettings();
|
||||||
|
}, 250);
|
||||||
// Update Font Size
|
|
||||||
renderDoc.documentElement.style.setProperty(
|
|
||||||
"--editor-font-size",
|
|
||||||
(readerSettings.fontSize || 1) + "em"
|
|
||||||
);
|
|
||||||
|
|
||||||
debounceID = setTimeout(debounceGesture, 200);
|
|
||||||
}
|
|
||||||
}.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
|
||||||
**/
|
**/
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user