Skip to content

Commit

Permalink
EPUB: Correctly determine location with RTL scrolling
Browse files Browse the repository at this point in the history
  • Loading branch information
AbeJellinek committed Dec 11, 2024
1 parent 194917b commit edb1d4d
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 32 deletions.
7 changes: 4 additions & 3 deletions src/dom/common/lib/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,10 @@ export function getStartElement(range: Range | PersistentRange): Element | null
return startContainer as Element | null;
}

export function getBoundingPageRect(range: Range) {
let rect = range.getBoundingClientRect();
let win = range.commonAncestorContainer.ownerDocument?.defaultView;
export function getBoundingPageRect(rangeOrElem: Range | Element) {
let rect = rangeOrElem.getBoundingClientRect();
let win = ('ownerDocument' in rangeOrElem ? rangeOrElem : rangeOrElem.commonAncestorContainer)
.ownerDocument?.defaultView;
rect.x += win?.scrollX ?? 0;
rect.y += win?.scrollY ?? 0;
return rect;
Expand Down
98 changes: 69 additions & 29 deletions src/dom/epub/flow.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { EpubCFI } from "epubjs";
import { debounce } from "../../common/lib/debounce";
import { NavigateOptions } from "../common/dom-view";
import { closestElement, isVertical, iterateWalker } from "../common/lib/nodes";
import { closestElement, isRTL, isVertical, iterateWalker } from "../common/lib/nodes";
import EPUBView, { SpreadMode } from "./epub-view";
import { PersistentRange } from "../common/lib/range";
import { getBoundingPageRect, PersistentRange } from "../common/lib/range";
import { isSafari } from "../../common/lib/utilities";
import { getSelectionRanges } from "../common/lib/selection";
import { rectContains } from "../common/lib/rect";
import { isPageRectVisible, rectContains } from "../common/lib/rect";
import Section from "epubjs/types/section";
import SectionRenderer from "./section-renderer";

Expand Down Expand Up @@ -71,6 +71,10 @@ abstract class AbstractFlow implements Flow {

protected _scale = 1;

protected _isRTL = false;

protected _isVertical = false;

protected _onUpdateViewState: () => void;

protected _onUpdateViewStats: () => void;
Expand All @@ -94,6 +98,8 @@ abstract class AbstractFlow implements Flow {
this._onViewUpdate = options.onViewUpdate;
this._onPushHistoryPoint = options.onPushHistoryPoint;

this._isRTL = isRTL(this._iframeDocument.body);

this._iframeWindow.addEventListener('scroll', this._pushHistoryPoint);

this._intersectionObserver = new IntersectionObserver(() => this.invalidate(), {
Expand Down Expand Up @@ -155,15 +161,16 @@ abstract class AbstractFlow implements Flow {
* Return a range before or at the top of the viewport.
*
* @param renderer
* @param isHorizontal Whether the viewport is laid out horizontally (paginated mode)
* @param textNodesOnly Return only text nodes, for constructing CFIs
*/
protected _getFirstVisibleRange(renderer: SectionRenderer, isHorizontal: boolean, textNodesOnly: boolean): Range | null {
protected _getFirstVisibleRange(renderer: SectionRenderer, textNodesOnly: boolean): Range | null {
if (!renderer.mounted) {
return null;
}
let mainAxisViewportEnd = isHorizontal ? this._iframe.clientWidth : this._iframe.clientHeight;
let crossAxisViewportEnd = isHorizontal ? this._iframe.clientHeight : this._iframe.clientWidth;
let isPaginated = this instanceof PaginatedFlow;
let isScrolledVerticalRTL = !isPaginated && this._isVertical && this._isRTL;
let mainAxisViewportEnd = isPaginated ? this._iframe.clientWidth : this._iframe.clientHeight;
let crossAxisViewportEnd = isPaginated ? this._iframe.clientHeight : this._iframe.clientWidth;
let filter = NodeFilter.SHOW_TEXT | (textNodesOnly ? 0 : NodeFilter.SHOW_ELEMENT);
let iter = this._iframeDocument.createNodeIterator(renderer.container, filter, (node) => {
return node.nodeType == Node.TEXT_NODE && node.nodeValue?.trim().length
Expand All @@ -186,10 +193,14 @@ abstract class AbstractFlow implements Flow {
if (!(rect.width || rect.height)) {
continue;
}
let mainAxisRectStart = isHorizontal ? rect.left : rect.top;
let mainAxisRectEnd = isHorizontal ? rect.right : rect.bottom;
let crossAxisRectStart = isHorizontal ? rect.top : rect.left;
let crossAxisRectEnd = isHorizontal ? rect.bottom : rect.right;
let mainAxisRectStart = isPaginated ? rect.left : rect.top;
let mainAxisRectEnd = isPaginated ? rect.right : rect.bottom;
let crossAxisRectStart = isPaginated ? rect.top : rect.left;
let crossAxisRectEnd = isPaginated ? rect.bottom : rect.right;
if (isScrolledVerticalRTL) {
crossAxisRectStart = this._iframe.clientWidth - crossAxisRectStart;
crossAxisRectEnd = this._iframe.clientHeight - crossAxisRectEnd;
}
// If the range starts past the end of the viewport, we've gone too far -- return our previous best guess
if (mainAxisRectStart > mainAxisViewportEnd || crossAxisRectStart > crossAxisViewportEnd) {
return bestRange;
Expand All @@ -201,7 +212,7 @@ abstract class AbstractFlow implements Flow {
) {
return range;
}
// Otherwise, it's above the start of the viewport -- save it as our best guess in case nothing within
// Otherwise, it's before the start of the viewport -- save it as our best guess in case nothing within
// the viewport is usable, but keep going
else {
bestRange = range;
Expand Down Expand Up @@ -299,6 +310,8 @@ export class ScrolledFlow extends AbstractFlow {
this._iframe.classList.add('flow-mode-scrolled');
this._iframeDocument.body.classList.add('flow-mode-scrolled');

this._isVertical = isVertical(this._iframeDocument.body);

for (let view of this._view.renderers) {
view.mount();
}
Expand Down Expand Up @@ -384,39 +397,72 @@ export class ScrolledFlow extends AbstractFlow {
}

canNavigateToPreviousPage() {
return this._iframeWindow.scrollY >= this._iframe.clientHeight
- this.scrollPadding;
if (this._isVertical) {
return Math.abs(this._iframeWindow.scrollX) >= this._iframe.clientWidth
- this.scrollPadding;
}
else {
return this._iframeWindow.scrollY >= this._iframe.clientHeight
- this.scrollPadding;
}
}

canNavigateToNextPage() {
return this._iframeWindow.scrollY <= this._iframeDocument.documentElement.scrollHeight
- this._iframe.clientHeight * 2
+ this.scrollPadding;
if (this._isVertical) {
return Math.abs(this._iframeWindow.scrollX) <= this._iframeDocument.documentElement.scrollWidth
- this._iframe.clientWidth * 2
+ this.scrollPadding;
}
else {
return this._iframeWindow.scrollY <= this._iframeDocument.documentElement.scrollHeight
- this._iframe.clientHeight * 2
+ this.scrollPadding;
}
}

navigateToPreviousPage() {
if (!this.canNavigateToPreviousPage()) {
return;
}
this._iframeWindow.scrollBy({ top: -this._iframe.clientHeight + this.scrollPadding });
if (this._isVertical) {
this._iframeWindow.scrollBy({ left: this._iframe.clientWidth + this.scrollPadding });
}
else {
this._iframeWindow.scrollBy({ top: -this._iframe.clientHeight + this.scrollPadding });
}
this._onViewUpdate();
}

navigateToNextPage() {
if (!this.canNavigateToNextPage()) {
return;
}
this._iframeWindow.scrollBy({ top: this._iframe.clientHeight - this.scrollPadding });
if (this._isVertical) {
this._iframeWindow.scrollBy({ left: -this._iframe.clientWidth - this.scrollPadding });
}
else {
this._iframeWindow.scrollBy({ top: this._iframe.clientHeight - this.scrollPadding });
}
this._onViewUpdate();
}

navigateToFirstPage(): void {
this._iframeWindow.scrollTo({ top: 0 });
if (this._isVertical) {
this._iframeWindow.scrollTo({ left: 0 });
}
else {
this._iframeWindow.scrollTo({ top: 0 });
}
this._onViewUpdate();
}

navigateToLastPage(): void {
this._iframeWindow.scrollTo({ top: this._iframeDocument.documentElement.scrollHeight });
if (this._isVertical) {
this._iframeWindow.scrollTo({ left: this._iframeDocument.documentElement.scrollWidth });
}
else {
this._iframeWindow.scrollTo({ top: this._iframeDocument.documentElement.scrollHeight });
}
this._onViewUpdate();
}

Expand All @@ -426,22 +472,18 @@ export class ScrolledFlow extends AbstractFlow {
if (!renderer.mounted) {
continue;
}
// Avoid calling getBoundingClientRect() because that would force a layout, which is expensive
let visible = renderer.container.offsetTop < this._iframeWindow.scrollY + this._iframe.clientHeight
&& renderer.container.offsetTop + renderer.container.offsetHeight >= this._iframeWindow.scrollY;
let visible = isPageRectVisible(getBoundingPageRect(renderer.container), this._iframeWindow);
if (!foundStart) {
if (!visible) {
continue;
}
this._cachedStartSection = renderer.section;
let startRange = this._getFirstVisibleRange(
renderer,
false,
false
);
let startCFIRange = this._getFirstVisibleRange(
renderer,
false,
true
);
if (startRange) {
Expand Down Expand Up @@ -857,12 +899,10 @@ export class PaginatedFlow extends AbstractFlow {
this._cachedStartSection = renderer.section;
let startRange = this._getFirstVisibleRange(
renderer,
true,
false
true
);
let startCFIRange = this._getFirstVisibleRange(
renderer,
true,
true
);
if (startRange) {
Expand Down

0 comments on commit edb1d4d

Please sign in to comment.