2013-06-19 01:05:55 +09:00
|
|
|
/* Copyright 2012 Mozilla Foundation
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2017-06-28 19:34:12 +09:00
|
|
|
const DEFAULT_SCALE_VALUE = "auto";
|
|
|
|
const DEFAULT_SCALE = 1.0;
|
2021-09-19 18:54:57 +09:00
|
|
|
const DEFAULT_SCALE_DELTA = 1.1;
|
2017-11-29 22:24:27 +09:00
|
|
|
const MIN_SCALE = 0.1;
|
2017-06-28 19:34:12 +09:00
|
|
|
const MAX_SCALE = 10.0;
|
|
|
|
const UNKNOWN_SCALE = 0;
|
|
|
|
const MAX_AUTO_SCALE = 1.25;
|
|
|
|
const SCROLLBAR_PADDING = 40;
|
|
|
|
const VERTICAL_PADDING = 5;
|
|
|
|
|
2021-12-15 21:54:29 +09:00
|
|
|
const RenderingStates = {
|
|
|
|
INITIAL: 0,
|
|
|
|
RUNNING: 1,
|
|
|
|
PAUSED: 2,
|
|
|
|
FINISHED: 3,
|
|
|
|
};
|
|
|
|
|
2017-08-01 21:11:28 +09:00
|
|
|
const PresentationModeState = {
|
|
|
|
UNKNOWN: 0,
|
|
|
|
NORMAL: 1,
|
|
|
|
CHANGING: 2,
|
|
|
|
FULLSCREEN: 3,
|
|
|
|
};
|
|
|
|
|
Add support for finding/highlighting the outlineItem, corresponding to the currently visible page, in the sidebar (issue 7557, bug 1253820, bug 1499050)
This implementation is inspired by the behaviour in (recent versions of) Adobe Reader, since it leads to reasonably simple and straightforward code as far as I'm concerned.
*Specifically:* We'll only consider *one* destination per page when finding/highlighting the current outline item, which is similar to e.g. Adobe Reader, and we choose the *first* outline item at the *lowest* level of the outline tree.
Given that this functionality requires not only parsing of the `outline`, but looking up *all* of the destinations in the document, this feature can when initialized have a non-trivial performance overhead for larger PDF documents.
In an attempt to reduce the performance impact, the following steps are taken here:
- The "find current outline item"-functionality will only be enabled once *one* page has rendered and *all* the pages have been loaded[1], to prevent it interfering with data regular fetching/parsing early on during document loading and viewer initialization.
- With the exception of a couple of small and simple `eventBus`-listeners, in `PDFOutlineViewer`, this new functionality is initialized *lazily* the first time that the user clicks on the `currentOutlineItem`-button.
- The entire "find current outline item"-functionality is disabled when `disableAutoFetch = true` is set, since it can easily lead to the setting becoming essentially pointless[2] by triggering *a lot* of data fetching from a relatively minor viewer-feature.
- Fetch the destinations *individually*, since that's generally more efficient than using `PDFDocumentProxy.getDestinations` to fetch them all at once. Despite making the overall parsing code *more* asynchronous, and leading to a lot more main/worker-thread message passing, in practice this seems faster for larger documents.
Finally, we'll now always highlight an outline item that the user manually clicked on, since only highlighting when the new "find current outline item"-functionality is used seemed inconsistent.
---
[1] Keep in mind that the `outline` itself already isn't fetched/parsed until at least *one* page has been rendered in the viewer.
[2] And also quite slow, since it can take a fair amount of time to fetch all of the necessary `destinations` data when `disableAutoFetch = true` is set.
2020-12-25 20:57:43 +09:00
|
|
|
const SidebarView = {
|
|
|
|
UNKNOWN: -1,
|
|
|
|
NONE: 0,
|
|
|
|
THUMBS: 1, // Default value.
|
|
|
|
OUTLINE: 2,
|
|
|
|
ATTACHMENTS: 3,
|
|
|
|
LAYERS: 4,
|
|
|
|
};
|
|
|
|
|
2018-02-13 23:01:55 +09:00
|
|
|
const TextLayerMode = {
|
|
|
|
DISABLE: 0,
|
|
|
|
ENABLE: 1,
|
2023-04-22 20:07:07 +09:00
|
|
|
ENABLE_PERMISSIONS: 2,
|
2018-02-13 23:01:55 +09:00
|
|
|
};
|
|
|
|
|
Modify a number of the viewer preferences, whose current default value is `0`, such that they behave as expected with the view history
The intention with preferences such as `sidebarViewOnLoad`/`scrollModeOnLoad`/`spreadModeOnLoad` were always that they should be able to *unconditionally* override their view history counterparts.
Due to the way that these preferences were initially implemented[1], trying to e.g. force the sidebar to remain hidden on load cannot be guaranteed[2]. The reason for this is the use of "enumeration values" containing zero, which in hindsight was an unfortunate choice on my part.
At this point it's also not as simple as just re-numbering the affected structures, since that would wreak havoc on existing (modified) preferences. The only reasonable solution that I was able to come up with was to change the *default* values of the preferences themselves, but not their actual values or the meaning thereof.
As part of the refactoring, the `disablePageMode` preference was combined with the *adjusted* `sidebarViewOnLoad` one, to hopefully reduce confusion by not tracking related state separately.
Additionally, the `showPreviousViewOnLoad` and `disableOpenActionDestination` preferences were combined into a *new* `viewOnLoad` enumeration preference, to further avoid tracking related state separately.
2019-01-27 20:07:38 +09:00
|
|
|
const ScrollMode = {
|
|
|
|
UNKNOWN: -1,
|
|
|
|
VERTICAL: 0, // Default value.
|
|
|
|
HORIZONTAL: 1,
|
|
|
|
WRAPPED: 2,
|
2021-10-07 21:04:41 +09:00
|
|
|
PAGE: 3,
|
Modify a number of the viewer preferences, whose current default value is `0`, such that they behave as expected with the view history
The intention with preferences such as `sidebarViewOnLoad`/`scrollModeOnLoad`/`spreadModeOnLoad` were always that they should be able to *unconditionally* override their view history counterparts.
Due to the way that these preferences were initially implemented[1], trying to e.g. force the sidebar to remain hidden on load cannot be guaranteed[2]. The reason for this is the use of "enumeration values" containing zero, which in hindsight was an unfortunate choice on my part.
At this point it's also not as simple as just re-numbering the affected structures, since that would wreak havoc on existing (modified) preferences. The only reasonable solution that I was able to come up with was to change the *default* values of the preferences themselves, but not their actual values or the meaning thereof.
As part of the refactoring, the `disablePageMode` preference was combined with the *adjusted* `sidebarViewOnLoad` one, to hopefully reduce confusion by not tracking related state separately.
Additionally, the `showPreviousViewOnLoad` and `disableOpenActionDestination` preferences were combined into a *new* `viewOnLoad` enumeration preference, to further avoid tracking related state separately.
2019-01-27 20:07:38 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
const SpreadMode = {
|
|
|
|
UNKNOWN: -1,
|
|
|
|
NONE: 0, // Default value.
|
|
|
|
ODD: 1,
|
|
|
|
EVEN: 2,
|
|
|
|
};
|
|
|
|
|
2023-02-04 21:55:12 +09:00
|
|
|
const CursorTool = {
|
|
|
|
SELECT: 0, // The default value.
|
|
|
|
HAND: 1,
|
|
|
|
ZOOM: 2,
|
|
|
|
};
|
|
|
|
|
2019-12-27 00:13:49 +09:00
|
|
|
// Used by `PDFViewerApplication`, and by the API unit-tests.
|
|
|
|
const AutoPrintRegExp = /\bprint\s*\(/;
|
|
|
|
|
2013-06-19 01:05:55 +09:00
|
|
|
/**
|
2022-02-19 00:38:25 +09:00
|
|
|
* Scale factors for the canvas, necessary with HiDPI displays.
|
2013-06-19 01:05:55 +09:00
|
|
|
*/
|
2022-02-19 00:38:25 +09:00
|
|
|
class OutputScale {
|
|
|
|
constructor() {
|
|
|
|
const pixelRatio = window.devicePixelRatio || 1;
|
2022-02-18 06:28:03 +09:00
|
|
|
|
2022-02-19 00:38:25 +09:00
|
|
|
/**
|
|
|
|
* @type {number} Horizontal scale.
|
|
|
|
*/
|
|
|
|
this.sx = pixelRatio;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {number} Vertical scale.
|
|
|
|
*/
|
|
|
|
this.sy = pixelRatio;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {boolean} Returns `true` when scaling is required, `false` otherwise.
|
|
|
|
*/
|
|
|
|
get scaled() {
|
|
|
|
return this.sx !== 1 || this.sy !== 1;
|
|
|
|
}
|
2013-06-19 01:05:55 +09:00
|
|
|
}
|
|
|
|
|
2013-07-13 03:14:13 +09:00
|
|
|
/**
|
|
|
|
* Scrolls specified element into view of its parent.
|
2023-04-28 00:03:03 +09:00
|
|
|
* @param {HTMLElement} element - The element to be visible.
|
|
|
|
* @param {Object} [spot] - An object with optional top and left properties,
|
2015-10-13 19:47:07 +09:00
|
|
|
* specifying the offset from the top left edge.
|
2023-04-28 00:03:03 +09:00
|
|
|
* @param {number} [spot.left]
|
|
|
|
* @param {number} [spot.top]
|
2021-06-03 18:35:12 +09:00
|
|
|
* @param {boolean} [scrollMatches] - When scrolling search results into view,
|
2022-11-24 20:37:07 +09:00
|
|
|
* ignore elements that either: Contains marked content identifiers,
|
|
|
|
* or have the CSS-rule `overflow: hidden;` set. The default value is `false`.
|
2013-07-13 03:14:13 +09:00
|
|
|
*/
|
2021-06-03 18:35:12 +09:00
|
|
|
function scrollIntoView(element, spot, scrollMatches = false) {
|
2013-07-13 03:14:13 +09:00
|
|
|
// Assuming offsetParent is available (it's not available when viewer is in
|
|
|
|
// hidden iframe or object). We have to scroll: if the offsetParent is not set
|
2016-11-19 03:50:29 +09:00
|
|
|
// producing the error. See also animationStarted.
|
2017-06-28 19:34:12 +09:00
|
|
|
let parent = element.offsetParent;
|
2013-07-13 03:14:13 +09:00
|
|
|
if (!parent) {
|
|
|
|
console.error("offsetParent is not set -- cannot scroll");
|
|
|
|
return;
|
|
|
|
}
|
2017-06-28 19:34:12 +09:00
|
|
|
let offsetY = element.offsetTop + element.clientTop;
|
|
|
|
let offsetX = element.offsetLeft + element.clientLeft;
|
2018-05-15 12:10:32 +09:00
|
|
|
while (
|
|
|
|
(parent.clientHeight === parent.scrollHeight &&
|
|
|
|
parent.clientWidth === parent.scrollWidth) ||
|
2022-11-24 20:37:07 +09:00
|
|
|
(scrollMatches &&
|
|
|
|
(parent.classList.contains("markedContent") ||
|
|
|
|
getComputedStyle(parent).overflow === "hidden"))
|
2017-06-28 19:34:12 +09:00
|
|
|
) {
|
2013-07-13 03:14:13 +09:00
|
|
|
offsetY += parent.offsetTop;
|
2013-12-04 03:53:20 +09:00
|
|
|
offsetX += parent.offsetLeft;
|
2021-06-03 18:11:27 +09:00
|
|
|
|
2013-07-13 03:14:13 +09:00
|
|
|
parent = parent.offsetParent;
|
2013-12-04 03:53:20 +09:00
|
|
|
if (!parent) {
|
2013-07-13 03:14:13 +09:00
|
|
|
return; // no need to scroll
|
2013-12-04 03:53:20 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (spot) {
|
|
|
|
if (spot.top !== undefined) {
|
|
|
|
offsetY += spot.top;
|
|
|
|
}
|
|
|
|
if (spot.left !== undefined) {
|
|
|
|
offsetX += spot.left;
|
|
|
|
parent.scrollLeft = offsetX;
|
|
|
|
}
|
2013-07-13 03:14:13 +09:00
|
|
|
}
|
|
|
|
parent.scrollTop = offsetY;
|
|
|
|
}
|
|
|
|
|
2014-09-13 03:39:23 +09:00
|
|
|
/**
|
|
|
|
* Helper function to start monitoring the scroll event and converting them into
|
|
|
|
* PDF.js friendly one: with scroll debounce and scroll direction.
|
|
|
|
*/
|
|
|
|
function watchScroll(viewAreaElement, callback) {
|
2020-04-14 19:28:14 +09:00
|
|
|
const debounceScroll = function (evt) {
|
2014-09-13 03:39:23 +09:00
|
|
|
if (rAF) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// schedule an invocation of scroll for next animation frame.
|
|
|
|
rAF = window.requestAnimationFrame(function viewAreaElementScrolled() {
|
|
|
|
rAF = null;
|
|
|
|
|
2019-12-23 02:16:32 +09:00
|
|
|
const currentX = viewAreaElement.scrollLeft;
|
|
|
|
const lastX = state.lastX;
|
2018-05-15 12:10:32 +09:00
|
|
|
if (currentX !== lastX) {
|
|
|
|
state.right = currentX > lastX;
|
|
|
|
}
|
|
|
|
state.lastX = currentX;
|
2019-12-23 02:16:32 +09:00
|
|
|
const currentY = viewAreaElement.scrollTop;
|
|
|
|
const lastY = state.lastY;
|
2014-12-27 01:43:13 +09:00
|
|
|
if (currentY !== lastY) {
|
|
|
|
state.down = currentY > lastY;
|
2014-09-13 03:39:23 +09:00
|
|
|
}
|
|
|
|
state.lastY = currentY;
|
|
|
|
callback(state);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2019-12-23 02:16:32 +09:00
|
|
|
const state = {
|
2018-05-15 12:10:32 +09:00
|
|
|
right: true,
|
2014-09-13 03:39:23 +09:00
|
|
|
down: true,
|
2018-05-15 12:10:32 +09:00
|
|
|
lastX: viewAreaElement.scrollLeft,
|
2014-09-13 03:39:23 +09:00
|
|
|
lastY: viewAreaElement.scrollTop,
|
Fix inconsistent spacing and trailing commas in objects in `web/` files, so we can enable the `comma-dangle` and `object-curly-spacing` ESLint rules later on
http://eslint.org/docs/rules/comma-dangle
http://eslint.org/docs/rules/object-curly-spacing
Given that we currently have quite inconsistent object formatting, fixing this in in *one* big patch probably wouldn't be feasible (since I cannot imagine anyone wanting to review that); hence I've opted to try and do this piecewise instead.
*Please note:* This patch was created automatically, using the ESLint `--fix` command line option. In a couple of places this caused lines to become too long, and I've fixed those manually; please refer to the interdiff below for the only hand-edits in this patch.
```diff
diff --git a/web/pdf_thumbnail_view.js b/web/pdf_thumbnail_view.js
index 002dbf29..1de4e530 100644
--- a/web/pdf_thumbnail_view.js
+++ b/web/pdf_thumbnail_view.js
@@ -420,8 +420,8 @@ var PDFThumbnailView = (function PDFThumbnailViewClosure() {
setPageLabel: function PDFThumbnailView_setPageLabel(label) {
this.pageLabel = (typeof label === 'string' ? label : null);
- this.l10n.get('thumb_page_title', { page: this.pageId, }, 'Page {{page}}').
- then((msg) => {
+ this.l10n.get('thumb_page_title', { page: this.pageId, },
+ 'Page {{page}}').then((msg) => {
this.anchor.title = msg;
});
diff --git a/web/secondary_toolbar.js b/web/secondary_toolbar.js
index 160e0410..6495fc5e 100644
--- a/web/secondary_toolbar.js
+++ b/web/secondary_toolbar.js
@@ -65,7 +65,8 @@ class SecondaryToolbar {
{ element: options.printButton, eventName: 'print', close: true, },
{ element: options.downloadButton, eventName: 'download', close: true, },
{ element: options.viewBookmarkButton, eventName: null, close: true, },
- { element: options.firstPageButton, eventName: 'firstpage', close: true, },
+ { element: options.firstPageButton, eventName: 'firstpage',
+ close: true, },
{ element: options.lastPageButton, eventName: 'lastpage', close: true, },
{ element: options.pageRotateCwButton, eventName: 'rotatecw',
close: false, },
@@ -76,7 +77,7 @@ class SecondaryToolbar {
{ element: options.cursorHandToolButton, eventName: 'switchcursortool',
eventDetails: { tool: CursorTool.HAND, }, close: true, },
{ element: options.documentPropertiesButton,
- eventName: 'documentproperties', close: true, }
+ eventName: 'documentproperties', close: true, },
];
this.items = {
firstPage: options.firstPageButton,
```
2017-06-01 19:46:12 +09:00
|
|
|
_eventHandler: debounceScroll,
|
2014-09-13 03:39:23 +09:00
|
|
|
};
|
|
|
|
|
2017-06-28 19:34:12 +09:00
|
|
|
let rAF = null;
|
2014-09-13 03:39:23 +09:00
|
|
|
viewAreaElement.addEventListener("scroll", debounceScroll, true);
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
2015-04-28 00:25:32 +09:00
|
|
|
/**
|
2021-08-01 05:15:30 +09:00
|
|
|
* Helper function to parse query string (e.g. ?param1=value¶m2=...).
|
2023-08-31 19:01:33 +09:00
|
|
|
* @param {string} query
|
2021-08-01 05:15:30 +09:00
|
|
|
* @returns {Map}
|
2015-04-28 00:25:32 +09:00
|
|
|
*/
|
|
|
|
function parseQueryString(query) {
|
2021-08-01 05:15:30 +09:00
|
|
|
const params = new Map();
|
2021-11-14 01:36:57 +09:00
|
|
|
for (const [key, value] of new URLSearchParams(query)) {
|
|
|
|
params.set(key.toLowerCase(), value);
|
2015-04-28 00:25:32 +09:00
|
|
|
}
|
|
|
|
return params;
|
|
|
|
}
|
|
|
|
|
2024-01-21 18:13:12 +09:00
|
|
|
const InvisibleCharsRegExp = /[\x00-\x1F]/g;
|
2022-01-03 22:11:12 +09:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} str
|
|
|
|
* @param {boolean} [replaceInvisible]
|
|
|
|
*/
|
|
|
|
function removeNullCharacters(str, replaceInvisible = false) {
|
2024-01-21 18:13:12 +09:00
|
|
|
if (!InvisibleCharsRegExp.test(str)) {
|
2022-01-03 22:11:12 +09:00
|
|
|
return str;
|
|
|
|
}
|
|
|
|
if (replaceInvisible) {
|
2024-01-21 18:13:12 +09:00
|
|
|
return str.replaceAll(InvisibleCharsRegExp, m => (m === "\x00" ? "" : " "));
|
2022-01-03 22:11:12 +09:00
|
|
|
}
|
2023-03-22 23:31:10 +09:00
|
|
|
return str.replaceAll("\x00", "");
|
2022-01-03 22:11:12 +09:00
|
|
|
}
|
|
|
|
|
2022-08-14 18:24:38 +09:00
|
|
|
/**
|
|
|
|
* Use binary search to find the index of the first item in a given array which
|
|
|
|
* passes a given condition. The items are expected to be sorted in the sense
|
|
|
|
* that if the condition is true for one item in the array, then it is also true
|
|
|
|
* for all following items.
|
|
|
|
*
|
|
|
|
* @returns {number} Index of the first array element to pass the test,
|
|
|
|
* or |items.length| if no such element exists.
|
|
|
|
*/
|
|
|
|
function binarySearchFirstItem(items, condition, start = 0) {
|
|
|
|
let minIndex = start;
|
|
|
|
let maxIndex = items.length - 1;
|
|
|
|
|
|
|
|
if (maxIndex < 0 || !condition(items[maxIndex])) {
|
|
|
|
return items.length;
|
|
|
|
}
|
|
|
|
if (condition(items[minIndex])) {
|
|
|
|
return minIndex;
|
|
|
|
}
|
|
|
|
|
|
|
|
while (minIndex < maxIndex) {
|
|
|
|
const currentIndex = (minIndex + maxIndex) >> 1;
|
|
|
|
const currentItem = items[currentIndex];
|
|
|
|
if (condition(currentItem)) {
|
|
|
|
maxIndex = currentIndex;
|
|
|
|
} else {
|
|
|
|
minIndex = currentIndex + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return minIndex; /* === maxIndex */
|
|
|
|
}
|
|
|
|
|
2015-10-29 07:27:42 +09:00
|
|
|
/**
|
|
|
|
* Approximates float number as a fraction using Farey sequence (max order
|
|
|
|
* of 8).
|
|
|
|
* @param {number} x - Positive float number.
|
|
|
|
* @returns {Array} Estimated fraction: the first array item is a numerator,
|
|
|
|
* the second one is a denominator.
|
|
|
|
*/
|
|
|
|
function approximateFraction(x) {
|
|
|
|
// Fast paths for int numbers or their inversions.
|
|
|
|
if (Math.floor(x) === x) {
|
|
|
|
return [x, 1];
|
|
|
|
}
|
2019-12-23 02:16:32 +09:00
|
|
|
const xinv = 1 / x;
|
|
|
|
const limit = 8;
|
2015-10-29 07:27:42 +09:00
|
|
|
if (xinv > limit) {
|
|
|
|
return [1, limit];
|
2016-12-10 22:28:27 +09:00
|
|
|
} else if (Math.floor(xinv) === xinv) {
|
2015-10-29 07:27:42 +09:00
|
|
|
return [1, xinv];
|
|
|
|
}
|
|
|
|
|
2019-12-23 02:16:32 +09:00
|
|
|
const x_ = x > 1 ? xinv : x;
|
2015-10-29 07:27:42 +09:00
|
|
|
// a/b and c/d are neighbours in Farey sequence.
|
2017-06-28 19:34:12 +09:00
|
|
|
let a = 0,
|
|
|
|
b = 1,
|
|
|
|
c = 1,
|
|
|
|
d = 1;
|
2015-10-29 07:27:42 +09:00
|
|
|
// Limiting search to order 8.
|
|
|
|
while (true) {
|
|
|
|
// Generating next term in sequence (order of q).
|
2019-12-23 02:16:32 +09:00
|
|
|
const p = a + c,
|
|
|
|
q = b + d;
|
2015-10-29 07:27:42 +09:00
|
|
|
if (q > limit) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (x_ <= p / q) {
|
|
|
|
c = p;
|
|
|
|
d = q;
|
|
|
|
} else {
|
|
|
|
a = p;
|
|
|
|
b = q;
|
|
|
|
}
|
|
|
|
}
|
2017-06-28 19:34:12 +09:00
|
|
|
let result;
|
2015-10-29 07:27:42 +09:00
|
|
|
// Select closest of the neighbours to x.
|
|
|
|
if (x_ - a / b < c / d - x_) {
|
2016-12-16 21:05:33 +09:00
|
|
|
result = x_ === x ? [a, b] : [b, a];
|
2015-10-29 07:27:42 +09:00
|
|
|
} else {
|
2016-12-16 21:05:33 +09:00
|
|
|
result = x_ === x ? [c, d] : [d, c];
|
2015-10-29 07:27:42 +09:00
|
|
|
}
|
2016-12-16 21:05:33 +09:00
|
|
|
return result;
|
2015-10-29 07:27:42 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
function roundToDivide(x, div) {
|
2019-12-23 02:16:32 +09:00
|
|
|
const r = x % div;
|
2015-10-29 07:27:42 +09:00
|
|
|
return r === 0 ? x : Math.round(x - r + div);
|
|
|
|
}
|
|
|
|
|
Fix Viewer API definitions and include in CI
The Viewer API definitions do not compile because of missing imports and
anonymous objects are typed as `Object`. These issues were not caught
during CI because the test project was not compiling anything from the
Viewer API.
As an example of the first problem:
```
/**
* @implements MyInterface
*/
export class MyClass {
...
}
```
will generate a broken definition that doesn’t import MyInterface:
```
/**
* @implements MyInterface
*/
export class MyClass implements MyInterface {
...
}
```
This can be fixed by adding a typedef jsdoc to specify the import:
```
/** @typedef {import("./otherFile").MyInterface} MyInterface */
```
See https://github.com/jsdoc/jsdoc/issues/1537 and
https://github.com/microsoft/TypeScript/issues/22160 for more details.
As an example of the second problem:
```
/**
* Gets the size of the specified page, converted from PDF units to inches.
* @param {Object} An Object containing the properties: {Array} `view`,
* {number} `userUnit`, and {number} `rotate`.
*/
function getPageSizeInches({ view, userUnit, rotate }) {
...
}
```
generates the broken definition:
```
function getPageSizeInches({ view, userUnit, rotate }: Object) {
...
}
```
The jsdoc should specify the type of each nested property:
```
/**
* Gets the size of the specified page, converted from PDF units to inches.
* @param {Object} options An object containing the properties: {Array} `view`,
* {number} `userUnit`, and {number} `rotate`.
* @param {number[]} options.view
* @param {number} options.userUnit
* @param {number} options.rotate
*/
```
2021-08-26 07:44:06 +09:00
|
|
|
/**
|
|
|
|
* @typedef {Object} GetPageSizeInchesParameters
|
|
|
|
* @property {number[]} view
|
|
|
|
* @property {number} userUnit
|
|
|
|
* @property {number} rotate
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef {Object} PageSize
|
|
|
|
* @property {number} width - In inches.
|
|
|
|
* @property {number} height - In inches.
|
|
|
|
*/
|
|
|
|
|
2018-03-20 01:27:04 +09:00
|
|
|
/**
|
|
|
|
* Gets the size of the specified page, converted from PDF units to inches.
|
Fix Viewer API definitions and include in CI
The Viewer API definitions do not compile because of missing imports and
anonymous objects are typed as `Object`. These issues were not caught
during CI because the test project was not compiling anything from the
Viewer API.
As an example of the first problem:
```
/**
* @implements MyInterface
*/
export class MyClass {
...
}
```
will generate a broken definition that doesn’t import MyInterface:
```
/**
* @implements MyInterface
*/
export class MyClass implements MyInterface {
...
}
```
This can be fixed by adding a typedef jsdoc to specify the import:
```
/** @typedef {import("./otherFile").MyInterface} MyInterface */
```
See https://github.com/jsdoc/jsdoc/issues/1537 and
https://github.com/microsoft/TypeScript/issues/22160 for more details.
As an example of the second problem:
```
/**
* Gets the size of the specified page, converted from PDF units to inches.
* @param {Object} An Object containing the properties: {Array} `view`,
* {number} `userUnit`, and {number} `rotate`.
*/
function getPageSizeInches({ view, userUnit, rotate }) {
...
}
```
generates the broken definition:
```
function getPageSizeInches({ view, userUnit, rotate }: Object) {
...
}
```
The jsdoc should specify the type of each nested property:
```
/**
* Gets the size of the specified page, converted from PDF units to inches.
* @param {Object} options An object containing the properties: {Array} `view`,
* {number} `userUnit`, and {number} `rotate`.
* @param {number[]} options.view
* @param {number} options.userUnit
* @param {number} options.rotate
*/
```
2021-08-26 07:44:06 +09:00
|
|
|
* @param {GetPageSizeInchesParameters} params
|
|
|
|
* @returns {PageSize}
|
2018-03-20 01:27:04 +09:00
|
|
|
*/
|
|
|
|
function getPageSizeInches({ view, userUnit, rotate }) {
|
|
|
|
const [x1, y1, x2, y2] = view;
|
|
|
|
// We need to take the page rotation into account as well.
|
|
|
|
const changeOrientation = rotate % 180 !== 0;
|
|
|
|
|
|
|
|
const width = ((x2 - x1) / 72) * userUnit;
|
|
|
|
const height = ((y2 - y1) / 72) * userUnit;
|
|
|
|
|
|
|
|
return {
|
|
|
|
width: changeOrientation ? height : width,
|
|
|
|
height: changeOrientation ? width : height,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-05-15 12:10:32 +09:00
|
|
|
/**
|
|
|
|
* Helper function for getVisibleElements.
|
|
|
|
*
|
|
|
|
* @param {number} index - initial guess at the first visible element
|
|
|
|
* @param {Array} views - array of pages, into which `index` is an index
|
|
|
|
* @param {number} top - the top of the scroll pane
|
|
|
|
* @returns {number} less than or equal to `index` that is definitely at or
|
|
|
|
* before the first visible element in `views`, but not by too much. (Usually,
|
|
|
|
* this will be the first element in the first partially visible row in
|
|
|
|
* `views`, although sometimes it goes back one row further.)
|
|
|
|
*/
|
|
|
|
function backtrackBeforeAllVisibleElements(index, views, top) {
|
|
|
|
// binarySearchFirstItem's assumption is that the input is ordered, with only
|
2018-05-15 12:10:32 +09:00
|
|
|
// one index where the conditions flips from false to true: [false ...,
|
|
|
|
// true...]. With vertical scrolling and spreads, it is possible to have
|
|
|
|
// [false ..., true, false, true ...]. With wrapped scrolling we can have a
|
|
|
|
// similar sequence, with many more mixed true and false in the middle.
|
2018-05-15 12:10:32 +09:00
|
|
|
//
|
|
|
|
// So there is no guarantee that the binary search yields the index of the
|
|
|
|
// first visible element. It could have been any of the other visible elements
|
|
|
|
// that were preceded by a hidden element.
|
|
|
|
|
|
|
|
// Of course, if either this element or the previous (hidden) element is also
|
|
|
|
// the first element, there's nothing to worry about.
|
|
|
|
if (index < 2) {
|
|
|
|
return index;
|
|
|
|
}
|
|
|
|
|
|
|
|
// That aside, the possible cases are represented below.
|
|
|
|
//
|
|
|
|
// **** = fully hidden
|
|
|
|
// A*B* = mix of partially visible and/or hidden pages
|
|
|
|
// CDEF = fully visible
|
|
|
|
//
|
|
|
|
// (1) Binary search could have returned A, in which case we can stop.
|
|
|
|
// (2) Binary search could also have returned B, in which case we need to
|
|
|
|
// check the whole row.
|
|
|
|
// (3) Binary search could also have returned C, in which case we need to
|
|
|
|
// check the whole previous row.
|
|
|
|
//
|
|
|
|
// There's one other possibility:
|
|
|
|
//
|
|
|
|
// **** = fully hidden
|
|
|
|
// ABCD = mix of fully and/or partially visible pages
|
|
|
|
//
|
|
|
|
// (4) Binary search could only have returned A.
|
|
|
|
|
|
|
|
// Initially assume that we need to find the beginning of the current row
|
|
|
|
// (case 1, 2, or 4), which means finding a page that is above the current
|
|
|
|
// page's top. If the found page is partially visible, we're definitely not in
|
|
|
|
// case 3, and this assumption is correct.
|
|
|
|
let elt = views[index].div;
|
|
|
|
let pageTop = elt.offsetTop + elt.clientTop;
|
|
|
|
|
|
|
|
if (pageTop >= top) {
|
|
|
|
// The found page is fully visible, so we're actually either in case 3 or 4,
|
|
|
|
// and unfortunately we can't tell the difference between them without
|
|
|
|
// scanning the entire previous row, so we just conservatively assume that
|
|
|
|
// we do need to backtrack to that row. In both cases, the previous page is
|
|
|
|
// in the previous row, so use its top instead.
|
|
|
|
elt = views[index - 1].div;
|
|
|
|
pageTop = elt.offsetTop + elt.clientTop;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now we backtrack to the first page that still has its bottom below
|
|
|
|
// `pageTop`, which is the top of a page in the first visible row (unless
|
|
|
|
// we're in case 4, in which case it's the row before that).
|
|
|
|
// `index` is found by binary search, so the page at `index - 1` is
|
|
|
|
// invisible and we can start looking for potentially visible pages from
|
|
|
|
// `index - 2`. (However, if this loop terminates on its first iteration,
|
|
|
|
// which is the case when pages are stacked vertically, `index` should remain
|
|
|
|
// unchanged, so we use a distinct loop variable.)
|
|
|
|
for (let i = index - 2; i >= 0; --i) {
|
|
|
|
elt = views[i].div;
|
|
|
|
if (elt.offsetTop + elt.clientTop + elt.clientHeight <= pageTop) {
|
|
|
|
// We have reached the previous row, so stop now.
|
|
|
|
// This loop is expected to terminate relatively quickly because the
|
|
|
|
// number of pages per row is expected to be small.
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
index = i;
|
|
|
|
}
|
|
|
|
return index;
|
|
|
|
}
|
|
|
|
|
2020-11-04 20:05:29 +09:00
|
|
|
/**
|
|
|
|
* @typedef {Object} GetVisibleElementsParameters
|
|
|
|
* @property {HTMLElement} scrollEl - A container that can possibly scroll.
|
|
|
|
* @property {Array} views - Objects with a `div` property that contains an
|
|
|
|
* HTMLElement, which should all be descendants of `scrollEl` satisfying the
|
|
|
|
* relevant layout assumptions.
|
|
|
|
* @property {boolean} sortByVisibility - If `true`, the returned elements are
|
|
|
|
* sorted in descending order of the percent of their padding box that is
|
|
|
|
* visible. The default value is `false`.
|
|
|
|
* @property {boolean} horizontal - If `true`, the elements are assumed to be
|
|
|
|
* laid out horizontally instead of vertically. The default value is `false`.
|
|
|
|
* @property {boolean} rtl - If `true`, the `scrollEl` container is assumed to
|
|
|
|
* be in right-to-left mode. The default value is `false`.
|
|
|
|
*/
|
|
|
|
|
2014-09-13 03:39:23 +09:00
|
|
|
/**
|
|
|
|
* Generic helper to find out what elements are visible within a scroll pane.
|
2018-05-15 12:10:32 +09:00
|
|
|
*
|
|
|
|
* Well, pretty generic. There are some assumptions placed on the elements
|
|
|
|
* referenced by `views`:
|
|
|
|
* - If `horizontal`, no left of any earlier element is to the right of the
|
|
|
|
* left of any later element.
|
|
|
|
* - Otherwise, `views` can be split into contiguous rows where, within a row,
|
|
|
|
* no top of any element is below the bottom of any other element, and
|
|
|
|
* between rows, no bottom of any element in an earlier row is below the
|
|
|
|
* top of any element in a later row.
|
|
|
|
*
|
|
|
|
* (Here, top, left, etc. all refer to the padding edge of the element in
|
|
|
|
* question. For pages, that ends up being equivalent to the bounding box of the
|
|
|
|
* rendering canvas. Earlier and later refer to index in `views`, not page
|
|
|
|
* layout.)
|
|
|
|
*
|
2023-08-31 19:01:33 +09:00
|
|
|
* @param {GetVisibleElementsParameters} params
|
2018-05-15 12:10:32 +09:00
|
|
|
* @returns {Object} `{ first, last, views: [{ id, x, y, view, percent }] }`
|
2014-09-13 03:39:23 +09:00
|
|
|
*/
|
2020-11-04 20:05:29 +09:00
|
|
|
function getVisibleElements({
|
2018-05-15 12:10:32 +09:00
|
|
|
scrollEl,
|
|
|
|
views,
|
|
|
|
sortByVisibility = false,
|
2020-10-09 23:20:06 +09:00
|
|
|
horizontal = false,
|
2020-11-04 20:05:29 +09:00
|
|
|
rtl = false,
|
|
|
|
}) {
|
2019-01-11 21:18:36 +09:00
|
|
|
const top = scrollEl.scrollTop,
|
|
|
|
bottom = top + scrollEl.clientHeight;
|
|
|
|
const left = scrollEl.scrollLeft,
|
|
|
|
right = left + scrollEl.clientWidth;
|
2014-09-13 03:39:23 +09:00
|
|
|
|
2018-05-15 12:10:32 +09:00
|
|
|
// Throughout this "generic" function, comments will assume we're working with
|
|
|
|
// PDF document pages, which is the most important and complex case. In this
|
|
|
|
// case, the visible elements we're actually interested is the page canvas,
|
|
|
|
// which is contained in a wrapper which adds no padding/border/margin, which
|
|
|
|
// is itself contained in `view.div` which adds no padding (but does add a
|
|
|
|
// border). So, as specified in this function's doc comment, this function
|
|
|
|
// does all of its work on the padding edge of the provided views, starting at
|
|
|
|
// offsetLeft/Top (which includes margin) and adding clientLeft/Top (which is
|
|
|
|
// the border). Adding clientWidth/Height gets us the bottom-right corner of
|
|
|
|
// the padding edge.
|
|
|
|
function isElementBottomAfterViewTop(view) {
|
2019-01-11 21:18:36 +09:00
|
|
|
const element = view.div;
|
|
|
|
const elementBottom =
|
2014-12-27 01:43:13 +09:00
|
|
|
element.offsetTop + element.clientTop + element.clientHeight;
|
|
|
|
return elementBottom > top;
|
|
|
|
}
|
2020-10-09 23:20:06 +09:00
|
|
|
function isElementNextAfterViewHorizontally(view) {
|
2019-01-11 21:18:36 +09:00
|
|
|
const element = view.div;
|
2020-10-09 23:20:06 +09:00
|
|
|
const elementLeft = element.offsetLeft + element.clientLeft;
|
|
|
|
const elementRight = elementLeft + element.clientWidth;
|
|
|
|
return rtl ? elementLeft < right : elementRight > left;
|
2018-05-15 12:10:32 +09:00
|
|
|
}
|
2014-12-27 01:43:13 +09:00
|
|
|
|
2019-01-11 21:18:36 +09:00
|
|
|
const visible = [],
|
2021-11-01 19:52:45 +09:00
|
|
|
ids = new Set(),
|
2019-01-11 21:18:36 +09:00
|
|
|
numViews = views.length;
|
2020-11-04 20:05:29 +09:00
|
|
|
let firstVisibleElementInd = binarySearchFirstItem(
|
|
|
|
views,
|
|
|
|
horizontal
|
|
|
|
? isElementNextAfterViewHorizontally
|
|
|
|
: isElementBottomAfterViewTop
|
|
|
|
);
|
2018-05-15 12:10:32 +09:00
|
|
|
|
Prevent `TypeError: views[index] is undefined` being throw in `getVisibleElements` when the viewer, or all pages, are hidden
Previously a couple of different attempts at fixing this problem has been rejected, given how *crucial* this code is for the correct function of the viewer, since no one has thus far provided any evidence that the problem actually affects the default viewer[1] nor an example using the viewer components directly (without another library on top).
The fact that none of the prior patches contained even a *simple* unit-test probably contributed to the unwillingness of a reviewer to sign off on the suggested changes.
However, it turns out that it's possible to create a reduced test-case, using the default viewer, that demonstrates the error[2]. Since this utilizes a hidden `<iframe>`, please note that this error will thus affect Firefox as well.
Note that while errors are thrown when the hidden `<iframe>` loads, the default viewer doesn't break completely since rendering does start working once the `<iframe>` becomes visible (although the errors do break the initial Toolbar state).
Before making any changes here, I carefully read through not just the immediately relevant code but also the rendering code in the viewer (given it's dependence on `getVisibleElements`). After concluding that the changes should be safe in general, the default viewer was tested without any issues found. (The above being much easier with significant prior experience of working with the viewer code.)
Finally the patch also adds new unit-tests, one of which explicitly triggers the relevant code-path and will thus fail with the current `master` branch.
This patch also makes `PDFViewerApplication` slightly more robust against errors during document opening, to ensure that viewer/document initialization always completes as expected.
Please keep in mind that even though this patch prevents an error in `getVisibleElements`, it's still not possible to set the initial position/zoom level/sidebar view etc. when the viewer is hidden since rendering and scrolling is completely dependent[3] on being able to actually access the DOM elements.
---
[1] And hence the PDF Viewer that's built-in to Firefox.
[2] Copy the HTML code below and save it as `iframe.html`, and place the file in the `web/` folder. Then start the server, with `gulp server`, and navigate to http://localhost:8888/web/iframe.html
```html
<!DOCTYPE html>
<html>
<head>
<title>Iframe test</title>
<script>
window.onload = function() {
const button = document.getElementById('button1');
const frame = document.getElementById('frame1');
button.addEventListener('click', function(evt) {
frame.hidden = !frame.hidden;
});
};
</script>
</head>
<body>
<button id="button1">Toggle iframe</button>
<br>
<iframe id="frame1" width="800" height="600" src="http://localhost:8888/web/viewer.html" hidden="true"></iframe>
</body>
</html>
```
[3] This is an old, pre-exisiting, issue that's not relevant to this patch as such (and it's already being tracked elsewhere).
2019-01-12 02:25:10 +09:00
|
|
|
// Please note the return value of the `binarySearchFirstItem` function when
|
|
|
|
// no valid element is found (hence the `firstVisibleElementInd` check below).
|
2019-01-27 22:50:16 +09:00
|
|
|
if (
|
|
|
|
firstVisibleElementInd > 0 &&
|
|
|
|
firstVisibleElementInd < numViews &&
|
|
|
|
!horizontal
|
|
|
|
) {
|
2018-05-15 12:10:32 +09:00
|
|
|
// In wrapped scrolling (or vertical scrolling with spreads), with some page
|
|
|
|
// sizes, isElementBottomAfterViewTop doesn't satisfy the binary search
|
|
|
|
// condition: there can be pages with bottoms above the view top between
|
|
|
|
// pages with bottoms below. This function detects and corrects that error;
|
|
|
|
// see it for more comments.
|
2018-05-15 12:10:32 +09:00
|
|
|
firstVisibleElementInd = backtrackBeforeAllVisibleElements(
|
|
|
|
firstVisibleElementInd,
|
|
|
|
views,
|
|
|
|
top
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// lastEdge acts as a cutoff for us to stop looping, because we know all
|
|
|
|
// subsequent pages will be hidden.
|
|
|
|
//
|
2018-05-15 12:10:32 +09:00
|
|
|
// When using wrapped scrolling or vertical scrolling with spreads, we can't
|
|
|
|
// simply stop the first time we reach a page below the bottom of the view;
|
|
|
|
// the tops of subsequent pages on the same row could still be visible. In
|
|
|
|
// horizontal scrolling, we don't have that issue, so we can stop as soon as
|
|
|
|
// we pass `right`, without needing the code below that handles the -1 case.
|
2018-05-15 12:10:32 +09:00
|
|
|
let lastEdge = horizontal ? right : -1;
|
2014-12-27 01:43:13 +09:00
|
|
|
|
2019-01-11 21:18:36 +09:00
|
|
|
for (let i = firstVisibleElementInd; i < numViews; i++) {
|
|
|
|
const view = views[i],
|
|
|
|
element = view.div;
|
|
|
|
const currentWidth = element.offsetLeft + element.clientLeft;
|
|
|
|
const currentHeight = element.offsetTop + element.clientTop;
|
|
|
|
const viewWidth = element.clientWidth,
|
|
|
|
viewHeight = element.clientHeight;
|
|
|
|
const viewRight = currentWidth + viewWidth;
|
|
|
|
const viewBottom = currentHeight + viewHeight;
|
2018-05-15 12:10:32 +09:00
|
|
|
|
|
|
|
if (lastEdge === -1) {
|
|
|
|
// As commented above, this is only needed in non-horizontal cases.
|
|
|
|
// Setting lastEdge to the bottom of the first page that is partially
|
|
|
|
// visible ensures that the next page fully below lastEdge is on the
|
|
|
|
// next row, which has to be fully hidden along with all subsequent rows.
|
|
|
|
if (viewBottom >= bottom) {
|
|
|
|
lastEdge = viewBottom;
|
|
|
|
}
|
|
|
|
} else if ((horizontal ? currentWidth : currentHeight) > lastEdge) {
|
2014-09-13 03:39:23 +09:00
|
|
|
break;
|
|
|
|
}
|
2014-12-27 01:43:13 +09:00
|
|
|
|
2018-05-15 12:10:32 +09:00
|
|
|
if (
|
|
|
|
viewBottom <= top ||
|
|
|
|
currentHeight >= bottom ||
|
|
|
|
viewRight <= left ||
|
|
|
|
currentWidth >= right
|
|
|
|
) {
|
2014-09-13 03:39:23 +09:00
|
|
|
continue;
|
|
|
|
}
|
2018-05-15 12:10:32 +09:00
|
|
|
|
2019-01-11 21:18:36 +09:00
|
|
|
const hiddenHeight =
|
|
|
|
Math.max(0, top - currentHeight) + Math.max(0, viewBottom - bottom);
|
|
|
|
const hiddenWidth =
|
|
|
|
Math.max(0, left - currentWidth) + Math.max(0, viewRight - right);
|
Add previous/next-page functionality that takes scroll/spread-modes into account (issue 11946)
- For wrapped scrolling, we unfortunately need to do a fair bit of parsing of the *current* page layout. Compared to e.g. the spread-modes, where we can easily tell how the pages are laid out, with wrapped scrolling we cannot tell without actually checking. In particular documents with varying page sizes require some care, since we need to check all pages on the "row" of the current page are visible and that there aren't any "holes" present. Otherwise, in the general case, there's a risk that we'd skip over pages if we'd simply always advance to the previous/next "row" in wrapped scrolling.
- For horizontal scrolling, this patch simply maintains the current behaviour of advancing *one* page at a time. The reason for this is to prevent inconsistent behaviour for the next and previous cases, since those cannot be handled identically. For the next-case, it'd obviously be simple to advance to the first not completely visible page. However for the previous-case, we'd only be able to go back *one* page since it's not possible to (easily) determine the page layout of non-visible pages (documents with varying page sizes being a particular issue).
- For vertical scrolling, this patch maintains the current behaviour by default. When spread-modes are being used, we'll now attempt to advance to the next *spread*, rather than just the next page, whenever possible. To prevent skipping over a page, this two-page advance will only apply when both pages of the current spread are visible (to avoid breaking documents with varying page sizes) and when the second page in the current spread is fully visible *horizontally* (to handle larger zoom values).
In order to reduce the performance impact of these changes, note that the previous/next-functionality will only call `getVisibleElements` for the scroll/spread-modes where that's necessary and that "normal" vertical scrolling is thus unaffected by these changes.
To support these changes, the `getVisibleElements` helper function will now also include the `widthPercent` in addition to the existing `percent` property.
The `PDFViewer._updateHelper` method is changed slightly w.r.t. updating the `currentPageNumber` for the non-vertical/spread modes, i.e. won't affect "normal" vertical scrolling, since that helped simplify the overall calculation of the page advance.
Finally, these new `BaseViewer` methods also allow (some) simplification of previous/next-page functionality in various viewer components.
*Please note:* There's one thing that this patch does not attempt to change, namely disabling of the previous/next toolbarButtons respectively the firstPage/lastPage secondaryToolbarButtons. The reason for this is that doing so would add quite a bit of complexity in general, and if for some reason `BaseViewer._getPageAdvance` would get things wrong we could end up incorrectly disabling the buttons. Hence it seemed overall safer to *not* touch this, and accept that the buttons won't be `disabled` despite in some edge-cases no further scrolling being possible.
2021-01-16 02:45:12 +09:00
|
|
|
|
|
|
|
const fractionHeight = (viewHeight - hiddenHeight) / viewHeight,
|
|
|
|
fractionWidth = (viewWidth - hiddenWidth) / viewWidth;
|
|
|
|
const percent = (fractionHeight * fractionWidth * 100) | 0;
|
|
|
|
|
2014-12-27 01:43:13 +09:00
|
|
|
visible.push({
|
|
|
|
id: view.id,
|
|
|
|
x: currentWidth,
|
|
|
|
y: currentHeight,
|
2017-04-28 19:02:42 +09:00
|
|
|
view,
|
2019-01-11 21:18:36 +09:00
|
|
|
percent,
|
Add previous/next-page functionality that takes scroll/spread-modes into account (issue 11946)
- For wrapped scrolling, we unfortunately need to do a fair bit of parsing of the *current* page layout. Compared to e.g. the spread-modes, where we can easily tell how the pages are laid out, with wrapped scrolling we cannot tell without actually checking. In particular documents with varying page sizes require some care, since we need to check all pages on the "row" of the current page are visible and that there aren't any "holes" present. Otherwise, in the general case, there's a risk that we'd skip over pages if we'd simply always advance to the previous/next "row" in wrapped scrolling.
- For horizontal scrolling, this patch simply maintains the current behaviour of advancing *one* page at a time. The reason for this is to prevent inconsistent behaviour for the next and previous cases, since those cannot be handled identically. For the next-case, it'd obviously be simple to advance to the first not completely visible page. However for the previous-case, we'd only be able to go back *one* page since it's not possible to (easily) determine the page layout of non-visible pages (documents with varying page sizes being a particular issue).
- For vertical scrolling, this patch maintains the current behaviour by default. When spread-modes are being used, we'll now attempt to advance to the next *spread*, rather than just the next page, whenever possible. To prevent skipping over a page, this two-page advance will only apply when both pages of the current spread are visible (to avoid breaking documents with varying page sizes) and when the second page in the current spread is fully visible *horizontally* (to handle larger zoom values).
In order to reduce the performance impact of these changes, note that the previous/next-functionality will only call `getVisibleElements` for the scroll/spread-modes where that's necessary and that "normal" vertical scrolling is thus unaffected by these changes.
To support these changes, the `getVisibleElements` helper function will now also include the `widthPercent` in addition to the existing `percent` property.
The `PDFViewer._updateHelper` method is changed slightly w.r.t. updating the `currentPageNumber` for the non-vertical/spread modes, i.e. won't affect "normal" vertical scrolling, since that helped simplify the overall calculation of the page advance.
Finally, these new `BaseViewer` methods also allow (some) simplification of previous/next-page functionality in various viewer components.
*Please note:* There's one thing that this patch does not attempt to change, namely disabling of the previous/next toolbarButtons respectively the firstPage/lastPage secondaryToolbarButtons. The reason for this is that doing so would add quite a bit of complexity in general, and if for some reason `BaseViewer._getPageAdvance` would get things wrong we could end up incorrectly disabling the buttons. Hence it seemed overall safer to *not* touch this, and accept that the buttons won't be `disabled` despite in some edge-cases no further scrolling being possible.
2021-01-16 02:45:12 +09:00
|
|
|
widthPercent: (fractionWidth * 100) | 0,
|
2014-12-27 01:43:13 +09:00
|
|
|
});
|
2021-11-01 19:52:45 +09:00
|
|
|
ids.add(view.id);
|
2014-09-13 03:39:23 +09:00
|
|
|
}
|
|
|
|
|
2019-01-11 21:18:36 +09:00
|
|
|
const first = visible[0],
|
2022-06-09 19:53:39 +09:00
|
|
|
last = visible.at(-1);
|
2014-09-13 03:39:23 +09:00
|
|
|
|
|
|
|
if (sortByVisibility) {
|
2020-04-14 19:28:14 +09:00
|
|
|
visible.sort(function (a, b) {
|
2019-12-23 02:16:32 +09:00
|
|
|
const pc = a.percent - b.percent;
|
2014-09-13 03:39:23 +09:00
|
|
|
if (Math.abs(pc) > 0.001) {
|
|
|
|
return -pc;
|
|
|
|
}
|
|
|
|
return a.id - b.id; // ensure stability
|
|
|
|
});
|
|
|
|
}
|
2021-11-01 19:52:45 +09:00
|
|
|
return { first, last, views: visible, ids };
|
2014-09-13 03:39:23 +09:00
|
|
|
}
|
|
|
|
|
2020-08-13 03:30:45 +09:00
|
|
|
function normalizeWheelEventDirection(evt) {
|
2021-02-10 20:28:49 +09:00
|
|
|
let delta = Math.hypot(evt.deltaX, evt.deltaY);
|
2019-12-23 02:16:32 +09:00
|
|
|
const angle = Math.atan2(evt.deltaY, evt.deltaX);
|
2016-09-28 05:27:42 +09:00
|
|
|
if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) {
|
|
|
|
// All that is left-up oriented has to change the sign.
|
|
|
|
delta = -delta;
|
|
|
|
}
|
2020-08-13 03:30:45 +09:00
|
|
|
return delta;
|
|
|
|
}
|
|
|
|
|
|
|
|
function normalizeWheelEventDelta(evt) {
|
2023-01-25 18:47:03 +09:00
|
|
|
const deltaMode = evt.deltaMode; // Avoid being affected by bug 1392460.
|
2020-08-13 03:30:45 +09:00
|
|
|
let delta = normalizeWheelEventDirection(evt);
|
2016-09-28 05:27:42 +09:00
|
|
|
|
2017-06-28 19:34:12 +09:00
|
|
|
const MOUSE_PIXELS_PER_LINE = 30;
|
|
|
|
const MOUSE_LINES_PER_PAGE = 30;
|
2016-09-28 05:27:42 +09:00
|
|
|
|
|
|
|
// Converts delta to per-page units
|
2023-01-25 18:47:03 +09:00
|
|
|
if (deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
|
2016-09-28 05:27:42 +09:00
|
|
|
delta /= MOUSE_PIXELS_PER_LINE * MOUSE_LINES_PER_PAGE;
|
2023-01-25 18:47:03 +09:00
|
|
|
} else if (deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
2016-09-28 05:27:42 +09:00
|
|
|
delta /= MOUSE_LINES_PER_PAGE;
|
|
|
|
}
|
|
|
|
return delta;
|
|
|
|
}
|
|
|
|
|
2017-08-19 21:23:40 +09:00
|
|
|
function isValidRotation(angle) {
|
|
|
|
return Number.isInteger(angle) && angle % 90 === 0;
|
|
|
|
}
|
|
|
|
|
Modify a number of the viewer preferences, whose current default value is `0`, such that they behave as expected with the view history
The intention with preferences such as `sidebarViewOnLoad`/`scrollModeOnLoad`/`spreadModeOnLoad` were always that they should be able to *unconditionally* override their view history counterparts.
Due to the way that these preferences were initially implemented[1], trying to e.g. force the sidebar to remain hidden on load cannot be guaranteed[2]. The reason for this is the use of "enumeration values" containing zero, which in hindsight was an unfortunate choice on my part.
At this point it's also not as simple as just re-numbering the affected structures, since that would wreak havoc on existing (modified) preferences. The only reasonable solution that I was able to come up with was to change the *default* values of the preferences themselves, but not their actual values or the meaning thereof.
As part of the refactoring, the `disablePageMode` preference was combined with the *adjusted* `sidebarViewOnLoad` one, to hopefully reduce confusion by not tracking related state separately.
Additionally, the `showPreviousViewOnLoad` and `disableOpenActionDestination` preferences were combined into a *new* `viewOnLoad` enumeration preference, to further avoid tracking related state separately.
2019-01-27 20:07:38 +09:00
|
|
|
function isValidScrollMode(mode) {
|
|
|
|
return (
|
|
|
|
Number.isInteger(mode) &&
|
|
|
|
Object.values(ScrollMode).includes(mode) &&
|
|
|
|
mode !== ScrollMode.UNKNOWN
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function isValidSpreadMode(mode) {
|
|
|
|
return (
|
|
|
|
Number.isInteger(mode) &&
|
|
|
|
Object.values(SpreadMode).includes(mode) &&
|
|
|
|
mode !== SpreadMode.UNKNOWN
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-03-20 21:25:13 +09:00
|
|
|
function isPortraitOrientation(size) {
|
|
|
|
return size.width <= size.height;
|
|
|
|
}
|
|
|
|
|
2016-11-19 03:50:29 +09:00
|
|
|
/**
|
|
|
|
* Promise that is resolved when DOM window becomes visible.
|
|
|
|
*/
|
2020-04-14 19:28:14 +09:00
|
|
|
const animationStarted = new Promise(function (resolve) {
|
2018-06-01 19:52:06 +09:00
|
|
|
if (
|
|
|
|
typeof PDFJSDev !== "undefined" &&
|
2021-03-20 21:34:38 +09:00
|
|
|
PDFJSDev.test("LIB") &&
|
2018-06-01 19:52:06 +09:00
|
|
|
typeof window === "undefined"
|
|
|
|
) {
|
|
|
|
// Prevent "ReferenceError: window is not defined" errors when running the
|
2021-03-20 21:34:38 +09:00
|
|
|
// unit-tests in Node.js environments.
|
2018-06-01 19:52:06 +09:00
|
|
|
setTimeout(resolve, 20);
|
|
|
|
return;
|
|
|
|
}
|
2016-11-19 03:50:29 +09:00
|
|
|
window.requestAnimationFrame(resolve);
|
|
|
|
});
|
|
|
|
|
2022-05-23 01:00:57 +09:00
|
|
|
const docStyle =
|
|
|
|
typeof PDFJSDev !== "undefined" &&
|
|
|
|
PDFJSDev.test("LIB") &&
|
|
|
|
typeof document === "undefined"
|
|
|
|
? null
|
|
|
|
: document.documentElement.style;
|
|
|
|
|
2017-06-28 19:34:12 +09:00
|
|
|
function clamp(v, min, max) {
|
|
|
|
return Math.min(Math.max(v, min), max);
|
|
|
|
}
|
|
|
|
|
|
|
|
class ProgressBar {
|
2022-07-01 17:14:57 +09:00
|
|
|
#classList = null;
|
|
|
|
|
2023-01-21 20:29:36 +09:00
|
|
|
#disableAutoFetchTimeout = null;
|
|
|
|
|
2022-07-01 17:14:57 +09:00
|
|
|
#percent = 0;
|
|
|
|
|
2023-01-25 19:09:28 +09:00
|
|
|
#style = null;
|
|
|
|
|
2022-07-01 17:14:57 +09:00
|
|
|
#visible = true;
|
|
|
|
|
2022-12-15 00:40:25 +09:00
|
|
|
constructor(bar) {
|
2022-07-01 17:14:57 +09:00
|
|
|
this.#classList = bar.classList;
|
2023-01-25 19:09:28 +09:00
|
|
|
this.#style = bar.style;
|
2017-06-28 19:34:12 +09:00
|
|
|
}
|
2013-06-19 01:05:55 +09:00
|
|
|
|
2017-06-28 19:34:12 +09:00
|
|
|
get percent() {
|
2022-07-01 17:14:57 +09:00
|
|
|
return this.#percent;
|
2017-06-28 19:34:12 +09:00
|
|
|
}
|
2013-07-19 01:28:59 +09:00
|
|
|
|
2017-06-28 19:34:12 +09:00
|
|
|
set percent(val) {
|
2022-07-01 17:14:57 +09:00
|
|
|
this.#percent = clamp(val, 0, 100);
|
|
|
|
|
|
|
|
if (isNaN(val)) {
|
|
|
|
this.#classList.add("indeterminate");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.#classList.remove("indeterminate");
|
|
|
|
|
2023-01-25 19:09:28 +09:00
|
|
|
this.#style.setProperty("--progressBar-percent", `${this.#percent}%`);
|
2017-06-28 19:34:12 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
setWidth(viewer) {
|
|
|
|
if (!viewer) {
|
|
|
|
return;
|
|
|
|
}
|
2019-12-23 02:16:32 +09:00
|
|
|
const container = viewer.parentNode;
|
|
|
|
const scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
|
2017-06-28 19:34:12 +09:00
|
|
|
if (scrollbarWidth > 0) {
|
2023-01-25 19:09:28 +09:00
|
|
|
this.#style.setProperty(
|
|
|
|
"--progressBar-end-offset",
|
|
|
|
`${scrollbarWidth}px`
|
|
|
|
);
|
2017-06-28 19:34:12 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-21 20:29:36 +09:00
|
|
|
setDisableAutoFetch(delay = /* ms = */ 5000) {
|
|
|
|
if (isNaN(this.#percent)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.#disableAutoFetchTimeout) {
|
|
|
|
clearTimeout(this.#disableAutoFetchTimeout);
|
|
|
|
}
|
|
|
|
this.show();
|
|
|
|
|
|
|
|
this.#disableAutoFetchTimeout = setTimeout(() => {
|
|
|
|
this.#disableAutoFetchTimeout = null;
|
|
|
|
this.hide();
|
|
|
|
}, delay);
|
|
|
|
}
|
|
|
|
|
2017-06-28 19:34:12 +09:00
|
|
|
hide() {
|
2022-07-01 17:14:57 +09:00
|
|
|
if (!this.#visible) {
|
2017-06-28 19:34:12 +09:00
|
|
|
return;
|
|
|
|
}
|
2022-07-01 17:14:57 +09:00
|
|
|
this.#visible = false;
|
|
|
|
this.#classList.add("hidden");
|
2017-06-28 19:34:12 +09:00
|
|
|
}
|
2013-06-19 01:05:55 +09:00
|
|
|
|
2017-06-28 19:34:12 +09:00
|
|
|
show() {
|
2022-07-01 17:14:57 +09:00
|
|
|
if (this.#visible) {
|
2017-06-28 19:34:12 +09:00
|
|
|
return;
|
|
|
|
}
|
2022-07-01 17:14:57 +09:00
|
|
|
this.#visible = true;
|
|
|
|
this.#classList.remove("hidden");
|
2017-06-28 19:34:12 +09:00
|
|
|
}
|
|
|
|
}
|
2016-04-09 02:34:27 +09:00
|
|
|
|
2020-09-23 13:59:19 +09:00
|
|
|
/**
|
|
|
|
* Get the active or focused element in current DOM.
|
|
|
|
*
|
|
|
|
* Recursively search for the truly active or focused element in case there are
|
|
|
|
* shadow DOMs.
|
|
|
|
*
|
|
|
|
* @returns {Element} the truly active or focused element.
|
|
|
|
*/
|
|
|
|
function getActiveOrFocusedElement() {
|
|
|
|
let curRoot = document;
|
|
|
|
let curActiveOrFocused =
|
|
|
|
curRoot.activeElement || curRoot.querySelector(":focus");
|
|
|
|
|
2021-02-06 01:36:28 +09:00
|
|
|
while (curActiveOrFocused?.shadowRoot) {
|
2020-09-23 13:59:19 +09:00
|
|
|
curRoot = curActiveOrFocused.shadowRoot;
|
|
|
|
curActiveOrFocused =
|
|
|
|
curRoot.activeElement || curRoot.querySelector(":focus");
|
|
|
|
}
|
|
|
|
|
|
|
|
return curActiveOrFocused;
|
|
|
|
}
|
|
|
|
|
2021-03-05 08:15:03 +09:00
|
|
|
/**
|
|
|
|
* Converts API PageLayout values to the format used by `BaseViewer`.
|
2023-08-31 19:01:33 +09:00
|
|
|
* @param {string} layout - The API PageLayout value.
|
2021-10-07 21:04:41 +09:00
|
|
|
* @returns {Object}
|
2021-03-05 08:15:03 +09:00
|
|
|
*/
|
2021-10-07 21:04:41 +09:00
|
|
|
function apiPageLayoutToViewerModes(layout) {
|
|
|
|
let scrollMode = ScrollMode.VERTICAL,
|
|
|
|
spreadMode = SpreadMode.NONE;
|
|
|
|
|
2021-03-05 08:15:03 +09:00
|
|
|
switch (layout) {
|
|
|
|
case "SinglePage":
|
2021-10-07 21:04:41 +09:00
|
|
|
scrollMode = ScrollMode.PAGE;
|
|
|
|
break;
|
2021-03-05 08:15:03 +09:00
|
|
|
case "OneColumn":
|
2021-10-07 21:04:41 +09:00
|
|
|
break;
|
2021-03-05 08:15:03 +09:00
|
|
|
case "TwoPageLeft":
|
2021-10-07 21:04:41 +09:00
|
|
|
scrollMode = ScrollMode.PAGE;
|
|
|
|
/* falls through */
|
|
|
|
case "TwoColumnLeft":
|
|
|
|
spreadMode = SpreadMode.ODD;
|
|
|
|
break;
|
2021-03-05 08:15:03 +09:00
|
|
|
case "TwoPageRight":
|
2021-10-07 21:04:41 +09:00
|
|
|
scrollMode = ScrollMode.PAGE;
|
|
|
|
/* falls through */
|
|
|
|
case "TwoColumnRight":
|
|
|
|
spreadMode = SpreadMode.EVEN;
|
|
|
|
break;
|
2021-03-05 08:15:03 +09:00
|
|
|
}
|
2021-10-07 21:04:41 +09:00
|
|
|
return { scrollMode, spreadMode };
|
2021-03-05 08:15:03 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Converts API PageMode values to the format used by `PDFSidebar`.
|
|
|
|
* NOTE: There's also a "FullScreen" parameter which is not possible to support,
|
|
|
|
* since the Fullscreen API used in browsers requires that entering
|
|
|
|
* fullscreen mode only occurs as a result of a user-initiated event.
|
|
|
|
* @param {string} mode - The API PageMode value.
|
|
|
|
* @returns {number} A value from {SidebarView}.
|
|
|
|
*/
|
|
|
|
function apiPageModeToSidebarView(mode) {
|
|
|
|
switch (mode) {
|
|
|
|
case "UseNone":
|
|
|
|
return SidebarView.NONE;
|
|
|
|
case "UseThumbs":
|
|
|
|
return SidebarView.THUMBS;
|
|
|
|
case "UseOutlines":
|
|
|
|
return SidebarView.OUTLINE;
|
|
|
|
case "UseAttachments":
|
|
|
|
return SidebarView.ATTACHMENTS;
|
|
|
|
case "UseOC":
|
|
|
|
return SidebarView.LAYERS;
|
|
|
|
}
|
|
|
|
return SidebarView.NONE; // Default value.
|
|
|
|
}
|
|
|
|
|
2023-04-13 19:36:39 +09:00
|
|
|
function toggleCheckedBtn(button, toggle, view = null) {
|
|
|
|
button.classList.toggle("toggled", toggle);
|
|
|
|
button.setAttribute("aria-checked", toggle);
|
|
|
|
|
|
|
|
view?.classList.toggle("hidden", !toggle);
|
|
|
|
}
|
|
|
|
|
2023-05-11 18:48:51 +09:00
|
|
|
function toggleExpandedBtn(button, toggle, view = null) {
|
|
|
|
button.classList.toggle("toggled", toggle);
|
|
|
|
button.setAttribute("aria-expanded", toggle);
|
|
|
|
|
|
|
|
view?.classList.toggle("hidden", !toggle);
|
|
|
|
}
|
|
|
|
|
2017-03-28 08:07:27 +09:00
|
|
|
export {
|
2021-01-09 23:37:44 +09:00
|
|
|
animationStarted,
|
2021-10-07 21:04:41 +09:00
|
|
|
apiPageLayoutToViewerModes,
|
2021-03-05 08:15:03 +09:00
|
|
|
apiPageModeToSidebarView,
|
2021-01-09 23:37:44 +09:00
|
|
|
approximateFraction,
|
2019-12-27 00:13:49 +09:00
|
|
|
AutoPrintRegExp,
|
2021-01-09 23:37:44 +09:00
|
|
|
backtrackBeforeAllVisibleElements, // only exported for testing
|
2022-08-14 18:24:38 +09:00
|
|
|
binarySearchFirstItem,
|
2023-02-04 21:55:12 +09:00
|
|
|
CursorTool,
|
2017-03-28 08:07:27 +09:00
|
|
|
DEFAULT_SCALE,
|
2021-09-19 18:54:57 +09:00
|
|
|
DEFAULT_SCALE_DELTA,
|
2021-01-09 23:37:44 +09:00
|
|
|
DEFAULT_SCALE_VALUE,
|
2022-05-23 01:00:57 +09:00
|
|
|
docStyle,
|
2021-01-09 23:37:44 +09:00
|
|
|
getActiveOrFocusedElement,
|
|
|
|
getPageSizeInches,
|
|
|
|
getVisibleElements,
|
|
|
|
isPortraitOrientation,
|
2017-08-19 21:23:40 +09:00
|
|
|
isValidRotation,
|
Modify a number of the viewer preferences, whose current default value is `0`, such that they behave as expected with the view history
The intention with preferences such as `sidebarViewOnLoad`/`scrollModeOnLoad`/`spreadModeOnLoad` were always that they should be able to *unconditionally* override their view history counterparts.
Due to the way that these preferences were initially implemented[1], trying to e.g. force the sidebar to remain hidden on load cannot be guaranteed[2]. The reason for this is the use of "enumeration values" containing zero, which in hindsight was an unfortunate choice on my part.
At this point it's also not as simple as just re-numbering the affected structures, since that would wreak havoc on existing (modified) preferences. The only reasonable solution that I was able to come up with was to change the *default* values of the preferences themselves, but not their actual values or the meaning thereof.
As part of the refactoring, the `disablePageMode` preference was combined with the *adjusted* `sidebarViewOnLoad` one, to hopefully reduce confusion by not tracking related state separately.
Additionally, the `showPreviousViewOnLoad` and `disableOpenActionDestination` preferences were combined into a *new* `viewOnLoad` enumeration preference, to further avoid tracking related state separately.
2019-01-27 20:07:38 +09:00
|
|
|
isValidScrollMode,
|
|
|
|
isValidSpreadMode,
|
2021-01-09 23:37:44 +09:00
|
|
|
MAX_AUTO_SCALE,
|
|
|
|
MAX_SCALE,
|
|
|
|
MIN_SCALE,
|
|
|
|
normalizeWheelEventDelta,
|
|
|
|
normalizeWheelEventDirection,
|
2022-02-19 00:38:25 +09:00
|
|
|
OutputScale,
|
2017-03-28 08:07:27 +09:00
|
|
|
parseQueryString,
|
2021-01-09 23:37:44 +09:00
|
|
|
PresentationModeState,
|
|
|
|
ProgressBar,
|
2022-01-03 22:11:12 +09:00
|
|
|
removeNullCharacters,
|
2021-12-15 21:54:29 +09:00
|
|
|
RenderingStates,
|
2017-03-28 08:07:27 +09:00
|
|
|
roundToDivide,
|
2021-01-09 23:37:44 +09:00
|
|
|
SCROLLBAR_PADDING,
|
2017-03-28 08:07:27 +09:00
|
|
|
scrollIntoView,
|
2021-01-09 23:37:44 +09:00
|
|
|
ScrollMode,
|
|
|
|
SidebarView,
|
|
|
|
SpreadMode,
|
|
|
|
TextLayerMode,
|
2023-04-13 19:36:39 +09:00
|
|
|
toggleCheckedBtn,
|
2023-05-11 18:48:51 +09:00
|
|
|
toggleExpandedBtn,
|
2021-01-09 23:37:44 +09:00
|
|
|
UNKNOWN_SCALE,
|
|
|
|
VERTICAL_PADDING,
|
|
|
|
watchScroll,
|
2017-03-28 08:07:27 +09:00
|
|
|
};
|