mirror of
https://github.com/zen-browser/pdf.js.git
synced 2025-07-10 02:05:37 +02:00
Add a new Page scrolling mode (issue 2638, 8952, 10907)
This implements a new Page scrolling mode, essentially bringing (and extending) the functionality from `PDFSinglePageViewer` into the regular `PDFViewer`-class. Compared to `PDFSinglePageViewer`, which as its name suggests will only display one page at a time, in the `PDFViewer`-implementation this new Page scrolling mode also support spreadModes properly (somewhat similar to e.g. Adobe Reader). Given the size and scope of these changes, I've tried to focus on implementing the basic functionality. Hence there's room for further clean-up and/or improvements, including e.g. simplifying the CSS/JS related to PresentationMode and implementing easier page-switching with the mouse-wheel/arrow-keys.
This commit is contained in:
parent
3945965605
commit
511458fbbc
14 changed files with 296 additions and 244 deletions
|
@ -445,14 +445,6 @@ class BaseViewer {
|
|||
return this.pdfDocument ? this._pagesCapability.promise : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
get _viewerElement() {
|
||||
// In most viewers, e.g. `PDFViewer`, this should return `this.viewer`.
|
||||
throw new Error("Not implemented: _viewerElement");
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
@ -538,6 +530,10 @@ class BaseViewer {
|
|||
this._firstPageCapability.resolve(firstPdfPage);
|
||||
this._optionalContentConfigPromise = optionalContentConfigPromise;
|
||||
|
||||
const viewerElement =
|
||||
this._scrollMode === ScrollMode.PAGE
|
||||
? this._scrollModePageState.shadowViewer
|
||||
: this.viewer;
|
||||
const scale = this.currentScale;
|
||||
const viewport = firstPdfPage.getViewport({
|
||||
scale: scale * PixelsPerInch.PDF_TO_CSS_UNITS,
|
||||
|
@ -552,7 +548,7 @@ class BaseViewer {
|
|||
|
||||
for (let pageNum = 1; pageNum <= pagesCount; ++pageNum) {
|
||||
const pageView = new PDFPageView({
|
||||
container: this._viewerElement,
|
||||
container: viewerElement,
|
||||
eventBus: this.eventBus,
|
||||
id: pageNum,
|
||||
scale,
|
||||
|
@ -582,7 +578,12 @@ class BaseViewer {
|
|||
firstPageView.setPdfPage(firstPdfPage);
|
||||
this.linkService.cachePageRef(1, firstPdfPage.ref);
|
||||
}
|
||||
if (this._spreadMode !== SpreadMode.NONE) {
|
||||
|
||||
if (this._scrollMode === ScrollMode.PAGE) {
|
||||
// Since the pages are placed in a `DocumentFragment`, ensure that
|
||||
// the current page becomes visible upon loading of the document.
|
||||
this._ensurePageViewVisible();
|
||||
} else if (this._spreadMode !== SpreadMode.NONE) {
|
||||
this._updateSpreadMode();
|
||||
}
|
||||
|
||||
|
@ -684,8 +685,16 @@ class BaseViewer {
|
|||
this._onePageRenderedCapability = createPromiseCapability();
|
||||
this._pagesCapability = createPromiseCapability();
|
||||
this._scrollMode = ScrollMode.VERTICAL;
|
||||
this._previousScrollMode = ScrollMode.UNKNOWN;
|
||||
this._spreadMode = SpreadMode.NONE;
|
||||
|
||||
this._scrollModePageState = {
|
||||
shadowViewer: document.createDocumentFragment(),
|
||||
previousPageNumber: 1,
|
||||
scrollDown: true,
|
||||
pages: [],
|
||||
};
|
||||
|
||||
if (this._onBeforeDraw) {
|
||||
this.eventBus._off("pagerender", this._onBeforeDraw);
|
||||
this._onBeforeDraw = null;
|
||||
|
@ -700,6 +709,71 @@ class BaseViewer {
|
|||
this._updateScrollMode();
|
||||
}
|
||||
|
||||
_ensurePageViewVisible() {
|
||||
if (this._scrollMode !== ScrollMode.PAGE) {
|
||||
throw new Error("_ensurePageViewVisible: Invalid scrollMode value.");
|
||||
}
|
||||
const pageNumber = this._currentPageNumber,
|
||||
state = this._scrollModePageState,
|
||||
viewer = this.viewer;
|
||||
|
||||
// Remove the currently active pages, if any, from the viewer.
|
||||
if (viewer.hasChildNodes()) {
|
||||
// Temporarily remove all the pages from the DOM.
|
||||
viewer.textContent = "";
|
||||
|
||||
for (const pageView of this._pages) {
|
||||
state.shadowViewer.appendChild(pageView.div);
|
||||
}
|
||||
}
|
||||
state.pages.length = 0;
|
||||
|
||||
if (this._spreadMode === SpreadMode.NONE) {
|
||||
// Finally, append the new page to the viewer.
|
||||
const pageView = this._pages[pageNumber - 1];
|
||||
viewer.appendChild(pageView.div);
|
||||
|
||||
state.pages.push(pageView);
|
||||
} else {
|
||||
const pageIndexSet = new Set(),
|
||||
parity = this._spreadMode - 1;
|
||||
|
||||
// Determine the pageIndices in the new spread.
|
||||
if (pageNumber % 2 !== parity) {
|
||||
// Left-hand side page.
|
||||
pageIndexSet.add(pageNumber - 1);
|
||||
pageIndexSet.add(pageNumber);
|
||||
} else {
|
||||
// Right-hand side page.
|
||||
pageIndexSet.add(pageNumber - 2);
|
||||
pageIndexSet.add(pageNumber - 1);
|
||||
}
|
||||
|
||||
// Finally, append the new pages to the viewer and apply the spreadMode.
|
||||
let spread = null;
|
||||
for (let i = 0, ii = this._pages.length; i < ii; ++i) {
|
||||
if (!pageIndexSet.has(i)) {
|
||||
continue;
|
||||
}
|
||||
if (spread === null) {
|
||||
spread = document.createElement("div");
|
||||
spread.className = "spread";
|
||||
viewer.appendChild(spread);
|
||||
} else if (i % 2 === parity) {
|
||||
spread = spread.cloneNode(false);
|
||||
viewer.appendChild(spread);
|
||||
}
|
||||
const pageView = this._pages[i];
|
||||
spread.appendChild(pageView.div);
|
||||
|
||||
state.pages.push(pageView);
|
||||
}
|
||||
}
|
||||
|
||||
state.scrollDown = pageNumber >= state.previousPageNumber;
|
||||
state.previousPageNumber = pageNumber;
|
||||
}
|
||||
|
||||
_scrollUpdate() {
|
||||
if (this.pagesCount === 0) {
|
||||
return;
|
||||
|
@ -708,6 +782,29 @@ class BaseViewer {
|
|||
}
|
||||
|
||||
_scrollIntoView({ pageDiv, pageSpot = null, pageNumber = null }) {
|
||||
if (this._scrollMode === ScrollMode.PAGE) {
|
||||
if (pageNumber) {
|
||||
// Ensure that `this._currentPageNumber` is correct.
|
||||
this._setCurrentPageNumber(pageNumber);
|
||||
}
|
||||
this._ensurePageViewVisible();
|
||||
// Ensure that rendering always occurs, to avoid showing a blank page,
|
||||
// even if the current position doesn't change when the page is scrolled.
|
||||
this.update();
|
||||
}
|
||||
|
||||
if (!pageSpot && !this.isInPresentationMode) {
|
||||
const left = pageDiv.offsetLeft + pageDiv.clientLeft;
|
||||
const right = left + pageDiv.clientWidth;
|
||||
const { scrollLeft, clientWidth } = this.container;
|
||||
if (
|
||||
this._scrollMode === ScrollMode.HORIZONTAL ||
|
||||
left < scrollLeft ||
|
||||
right > scrollLeft + clientWidth
|
||||
) {
|
||||
pageSpot = { left: 0, top: 0 };
|
||||
}
|
||||
}
|
||||
scrollIntoView(pageDiv, pageSpot);
|
||||
}
|
||||
|
||||
|
@ -772,8 +869,7 @@ class BaseViewer {
|
|||
get _pageWidthScaleFactor() {
|
||||
if (
|
||||
this._spreadMode !== SpreadMode.NONE &&
|
||||
this._scrollMode !== ScrollMode.HORIZONTAL &&
|
||||
!this.isInPresentationMode
|
||||
this._scrollMode !== ScrollMode.HORIZONTAL
|
||||
) {
|
||||
return 2;
|
||||
}
|
||||
|
@ -794,7 +890,7 @@ class BaseViewer {
|
|||
let hPadding = noPadding ? 0 : SCROLLBAR_PADDING;
|
||||
let vPadding = noPadding ? 0 : VERTICAL_PADDING;
|
||||
|
||||
if (!noPadding && this._isScrollModeHorizontal) {
|
||||
if (!noPadding && this._scrollMode === ScrollMode.HORIZONTAL) {
|
||||
[hPadding, vPadding] = [vPadding, hPadding]; // Swap the padding values.
|
||||
}
|
||||
const pageWidthScale =
|
||||
|
@ -1047,10 +1143,6 @@ class BaseViewer {
|
|||
};
|
||||
}
|
||||
|
||||
_updateHelper(visiblePages) {
|
||||
throw new Error("Not implemented: _updateHelper");
|
||||
}
|
||||
|
||||
update() {
|
||||
const visible = this._getVisiblePages();
|
||||
const visiblePages = visible.views,
|
||||
|
@ -1064,7 +1156,28 @@ class BaseViewer {
|
|||
|
||||
this.renderingQueue.renderHighestPriority(visible);
|
||||
|
||||
this._updateHelper(visiblePages); // Run any class-specific update code.
|
||||
if (!this.isInPresentationMode) {
|
||||
const isSimpleLayout =
|
||||
this._spreadMode === SpreadMode.NONE &&
|
||||
(this._scrollMode === ScrollMode.PAGE ||
|
||||
this._scrollMode === ScrollMode.VERTICAL);
|
||||
let currentId = this._currentPageNumber;
|
||||
let stillFullyVisible = false;
|
||||
|
||||
for (const page of visiblePages) {
|
||||
if (page.percent < 100) {
|
||||
break;
|
||||
}
|
||||
if (page.id === currentId && isSimpleLayout) {
|
||||
stillFullyVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!stillFullyVisible) {
|
||||
currentId = visiblePages[0].id;
|
||||
}
|
||||
this._setCurrentPageNumber(currentId);
|
||||
}
|
||||
|
||||
this._updateLocation(visible.first);
|
||||
this.eventBus.dispatch("updateviewarea", {
|
||||
|
@ -1081,14 +1194,6 @@ class BaseViewer {
|
|||
this.container.focus();
|
||||
}
|
||||
|
||||
get _isScrollModeHorizontal() {
|
||||
// Used to ensure that pre-rendering of the next/previous page works
|
||||
// correctly, since Scroll/Spread modes are ignored in Presentation Mode.
|
||||
return this.isInPresentationMode
|
||||
? false
|
||||
: this._scrollMode === ScrollMode.HORIZONTAL;
|
||||
}
|
||||
|
||||
get _isContainerRtl() {
|
||||
return getComputedStyle(this.container).direction === "rtl";
|
||||
}
|
||||
|
@ -1115,9 +1220,8 @@ class BaseViewer {
|
|||
|
||||
/**
|
||||
* Helper method for `this._getVisiblePages`. Should only ever be used when
|
||||
* the viewer can only display a single page at a time, for example in:
|
||||
* - `PDFSinglePageViewer`.
|
||||
* - `PDFViewer` with Presentation Mode active.
|
||||
* the viewer can only display a single page at a time, for example:
|
||||
* - When PresentationMode is active.
|
||||
*/
|
||||
_getCurrentVisiblePage() {
|
||||
if (!this.pagesCount) {
|
||||
|
@ -1138,12 +1242,24 @@ class BaseViewer {
|
|||
}
|
||||
|
||||
_getVisiblePages() {
|
||||
if (this.isInPresentationMode) {
|
||||
// The algorithm in `getVisibleElements` doesn't work in all browsers and
|
||||
// configurations (e.g. Chrome) when PresentationMode is active.
|
||||
return this._getCurrentVisiblePage();
|
||||
}
|
||||
const views =
|
||||
this._scrollMode === ScrollMode.PAGE
|
||||
? this._scrollModePageState.pages
|
||||
: this._pages,
|
||||
horizontal = this._scrollMode === ScrollMode.HORIZONTAL,
|
||||
rtl = horizontal && this._isContainerRtl;
|
||||
|
||||
return getVisibleElements({
|
||||
scrollEl: this.container,
|
||||
views: this._pages,
|
||||
views,
|
||||
sortByVisibility: true,
|
||||
horizontal: this._isScrollModeHorizontal,
|
||||
rtl: this._isScrollModeHorizontal && this._isContainerRtl,
|
||||
horizontal,
|
||||
rtl,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1245,15 +1361,25 @@ class BaseViewer {
|
|||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
get _scrollAhead() {
|
||||
switch (this._scrollMode) {
|
||||
case ScrollMode.PAGE:
|
||||
return this._scrollModePageState.scrollDown;
|
||||
case ScrollMode.HORIZONTAL:
|
||||
return this.scroll.right;
|
||||
}
|
||||
return this.scroll.down;
|
||||
}
|
||||
|
||||
forceRendering(currentlyVisiblePages) {
|
||||
const visiblePages = currentlyVisiblePages || this._getVisiblePages();
|
||||
const scrollAhead = this._isScrollModeHorizontal
|
||||
? this.scroll.right
|
||||
: this.scroll.down;
|
||||
const scrollAhead = this._scrollAhead;
|
||||
const preRenderExtra =
|
||||
this._scrollMode === ScrollMode.VERTICAL &&
|
||||
this._spreadMode !== SpreadMode.NONE &&
|
||||
!this.isInPresentationMode;
|
||||
this._scrollMode !== ScrollMode.HORIZONTAL;
|
||||
|
||||
const pageView = this.renderingQueue.getHighestPriority(
|
||||
visiblePages,
|
||||
|
@ -1492,6 +1618,8 @@ class BaseViewer {
|
|||
if (!isValidScrollMode(mode)) {
|
||||
throw new Error(`Invalid scroll mode: ${mode}`);
|
||||
}
|
||||
this._previousScrollMode = this._scrollMode;
|
||||
|
||||
this._scrollMode = mode;
|
||||
this.eventBus.dispatch("scrollmodechanged", { source: this, mode });
|
||||
|
||||
|
@ -1511,6 +1639,14 @@ class BaseViewer {
|
|||
if (!this.pdfDocument || !pageNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrollMode === ScrollMode.PAGE) {
|
||||
this._ensurePageViewVisible();
|
||||
} else if (this._previousScrollMode === ScrollMode.PAGE) {
|
||||
// Ensure that the current spreadMode is still applied correctly when
|
||||
// the *previous* scrollMode was `ScrollMode.PAGE`.
|
||||
this._updateSpreadMode();
|
||||
}
|
||||
// Non-numeric scale values can be sensitive to the scroll orientation.
|
||||
// Call this before re-scrolling to the current page, to ensure that any
|
||||
// changes in scale don't move the current page.
|
||||
|
@ -1552,32 +1688,40 @@ class BaseViewer {
|
|||
}
|
||||
const viewer = this.viewer,
|
||||
pages = this._pages;
|
||||
// Temporarily remove all the pages from the DOM.
|
||||
viewer.textContent = "";
|
||||
|
||||
if (this._spreadMode === SpreadMode.NONE) {
|
||||
for (let i = 0, iMax = pages.length; i < iMax; ++i) {
|
||||
viewer.appendChild(pages[i].div);
|
||||
}
|
||||
if (this._scrollMode === ScrollMode.PAGE) {
|
||||
this._ensurePageViewVisible();
|
||||
} else {
|
||||
const parity = this._spreadMode - 1;
|
||||
let spread = null;
|
||||
for (let i = 0, iMax = pages.length; i < iMax; ++i) {
|
||||
if (spread === null) {
|
||||
spread = document.createElement("div");
|
||||
spread.className = "spread";
|
||||
viewer.appendChild(spread);
|
||||
} else if (i % 2 === parity) {
|
||||
spread = spread.cloneNode(false);
|
||||
viewer.appendChild(spread);
|
||||
// Temporarily remove all the pages from the DOM.
|
||||
viewer.textContent = "";
|
||||
|
||||
if (this._spreadMode === SpreadMode.NONE) {
|
||||
for (let i = 0, ii = pages.length; i < ii; ++i) {
|
||||
viewer.appendChild(pages[i].div);
|
||||
}
|
||||
} else {
|
||||
const parity = this._spreadMode - 1;
|
||||
let spread = null;
|
||||
for (let i = 0, ii = pages.length; i < ii; ++i) {
|
||||
if (spread === null) {
|
||||
spread = document.createElement("div");
|
||||
spread.className = "spread";
|
||||
viewer.appendChild(spread);
|
||||
} else if (i % 2 === parity) {
|
||||
spread = spread.cloneNode(false);
|
||||
viewer.appendChild(spread);
|
||||
}
|
||||
spread.appendChild(pages[i].div);
|
||||
}
|
||||
spread.appendChild(pages[i].div);
|
||||
}
|
||||
}
|
||||
|
||||
if (!pageNumber) {
|
||||
return;
|
||||
}
|
||||
// Non-numeric scale values can be sensitive to the scroll orientation.
|
||||
// Call this before re-scrolling to the current page, to ensure that any
|
||||
// changes in scale don't move the current page.
|
||||
if (this._currentScaleValue && isNaN(this._currentScaleValue)) {
|
||||
this._setScale(this._currentScaleValue, true);
|
||||
}
|
||||
|
@ -1589,9 +1733,6 @@ class BaseViewer {
|
|||
* @private
|
||||
*/
|
||||
_getPageAdvance(currentPageNumber, previous = false) {
|
||||
if (this.isInPresentationMode) {
|
||||
return 1;
|
||||
}
|
||||
switch (this._scrollMode) {
|
||||
case ScrollMode.WRAPPED: {
|
||||
const { views } = this._getVisiblePages(),
|
||||
|
@ -1655,6 +1796,7 @@ class BaseViewer {
|
|||
case ScrollMode.HORIZONTAL: {
|
||||
break;
|
||||
}
|
||||
case ScrollMode.PAGE:
|
||||
case ScrollMode.VERTICAL: {
|
||||
if (this._spreadMode === SpreadMode.NONE) {
|
||||
break; // Normal vertical scrolling.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue