363dce6744
This should be a *tiny* bit more efficient, since it avoids parsing substrings that we don't care about. *Please note:* I cannot find an ESLint rule to enforce this automatically.
792 lines
25 KiB
JavaScript
792 lines
25 KiB
JavaScript
/* Copyright 2017 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.
|
|
*/
|
|
|
|
/** @typedef {import("./event_utils").EventBus} EventBus */
|
|
/** @typedef {import("./interfaces").IPDFLinkService} IPDFLinkService */
|
|
|
|
import { isValidRotation, parseQueryString } from "./ui_utils.js";
|
|
import { waitOnEventOrTimeout } from "./event_utils.js";
|
|
|
|
// Heuristic value used when force-resetting `this._blockHashChange`.
|
|
const HASH_CHANGE_TIMEOUT = 1000; // milliseconds
|
|
// Heuristic value used when adding the current position to the browser history.
|
|
const POSITION_UPDATED_THRESHOLD = 50;
|
|
// Heuristic value used when adding a temporary position to the browser history.
|
|
const UPDATE_VIEWAREA_TIMEOUT = 1000; // milliseconds
|
|
|
|
/**
|
|
* @typedef {Object} PDFHistoryOptions
|
|
* @property {IPDFLinkService} linkService - The navigation/linking service.
|
|
* @property {EventBus} eventBus - The application event bus.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} InitializeParameters
|
|
* @property {string} fingerprint - The PDF document's unique fingerprint.
|
|
* @property {boolean} [resetHistory] - Reset the browsing history.
|
|
* @property {boolean} [updateUrl] - Attempt to update the document URL, with
|
|
* the current hash, when pushing/replacing browser history entries.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} PushParameters
|
|
* @property {string} [namedDest] - The named destination. If absent, a
|
|
* stringified version of `explicitDest` is used.
|
|
* @property {Array} explicitDest - The explicit destination array.
|
|
* @property {number} pageNumber - The page to which the destination points.
|
|
*/
|
|
|
|
function getCurrentHash() {
|
|
return document.location.hash;
|
|
}
|
|
|
|
class PDFHistory {
|
|
/**
|
|
* @param {PDFHistoryOptions} options
|
|
*/
|
|
constructor({ linkService, eventBus }) {
|
|
this.linkService = linkService;
|
|
this.eventBus = eventBus;
|
|
|
|
this._initialized = false;
|
|
this._fingerprint = "";
|
|
this.reset();
|
|
|
|
this._boundEvents = null;
|
|
// Ensure that we don't miss a "pagesinit" event,
|
|
// by registering the listener immediately.
|
|
this.eventBus._on("pagesinit", () => {
|
|
this._isPagesLoaded = false;
|
|
|
|
this.eventBus._on(
|
|
"pagesloaded",
|
|
evt => {
|
|
this._isPagesLoaded = !!evt.pagesCount;
|
|
},
|
|
{ once: true }
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize the history for the PDF document, using either the current
|
|
* browser history entry or the document hash, whichever is present.
|
|
* @param {InitializeParameters} params
|
|
*/
|
|
initialize({ fingerprint, resetHistory = false, updateUrl = false }) {
|
|
if (!fingerprint || typeof fingerprint !== "string") {
|
|
console.error(
|
|
'PDFHistory.initialize: The "fingerprint" must be a non-empty string.'
|
|
);
|
|
return;
|
|
}
|
|
// Ensure that any old state is always reset upon initialization.
|
|
if (this._initialized) {
|
|
this.reset();
|
|
}
|
|
const reInitialized =
|
|
this._fingerprint !== "" && this._fingerprint !== fingerprint;
|
|
this._fingerprint = fingerprint;
|
|
this._updateUrl = updateUrl === true;
|
|
|
|
this._initialized = true;
|
|
this._bindEvents();
|
|
const state = window.history.state;
|
|
|
|
this._popStateInProgress = false;
|
|
this._blockHashChange = 0;
|
|
this._currentHash = getCurrentHash();
|
|
this._numPositionUpdates = 0;
|
|
|
|
this._uid = this._maxUid = 0;
|
|
this._destination = null;
|
|
this._position = null;
|
|
|
|
if (!this._isValidState(state, /* checkReload = */ true) || resetHistory) {
|
|
const { hash, page, rotation } = this._parseCurrentHash(
|
|
/* checkNameddest = */ true
|
|
);
|
|
|
|
if (!hash || reInitialized || resetHistory) {
|
|
// Ensure that the browser history is reset on PDF document load.
|
|
this._pushOrReplaceState(null, /* forceReplace = */ true);
|
|
return;
|
|
}
|
|
// Ensure that the browser history is initialized correctly when
|
|
// the document hash is present on PDF document load.
|
|
this._pushOrReplaceState(
|
|
{ hash, page, rotation },
|
|
/* forceReplace = */ true
|
|
);
|
|
return;
|
|
}
|
|
|
|
// The browser history contains a valid entry, ensure that the history is
|
|
// initialized correctly on PDF document load.
|
|
const destination = state.destination;
|
|
this._updateInternalState(
|
|
destination,
|
|
state.uid,
|
|
/* removeTemporary = */ true
|
|
);
|
|
|
|
if (destination.rotation !== undefined) {
|
|
this._initialRotation = destination.rotation;
|
|
}
|
|
if (destination.dest) {
|
|
this._initialBookmark = JSON.stringify(destination.dest);
|
|
|
|
// If the history is updated, e.g. through the user changing the hash,
|
|
// before the initial destination has become visible, then we do *not*
|
|
// want to potentially add `this._position` to the browser history.
|
|
this._destination.page = null;
|
|
} else if (destination.hash) {
|
|
this._initialBookmark = destination.hash;
|
|
} else if (destination.page) {
|
|
// Fallback case; shouldn't be necessary, but better safe than sorry.
|
|
this._initialBookmark = `page=${destination.page}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset the current `PDFHistory` instance, and consequently prevent any
|
|
* further updates and/or navigation of the browser history.
|
|
*/
|
|
reset() {
|
|
if (this._initialized) {
|
|
this._pageHide(); // Simulate a 'pagehide' event when resetting.
|
|
|
|
this._initialized = false;
|
|
this._unbindEvents();
|
|
}
|
|
if (this._updateViewareaTimeout) {
|
|
clearTimeout(this._updateViewareaTimeout);
|
|
this._updateViewareaTimeout = null;
|
|
}
|
|
this._initialBookmark = null;
|
|
this._initialRotation = null;
|
|
}
|
|
|
|
/**
|
|
* Push an internal destination to the browser history.
|
|
* @param {PushParameters}
|
|
*/
|
|
push({ namedDest = null, explicitDest, pageNumber }) {
|
|
if (!this._initialized) {
|
|
return;
|
|
}
|
|
if (namedDest && typeof namedDest !== "string") {
|
|
console.error(
|
|
"PDFHistory.push: " +
|
|
`"${namedDest}" is not a valid namedDest parameter.`
|
|
);
|
|
return;
|
|
} else if (!Array.isArray(explicitDest)) {
|
|
console.error(
|
|
"PDFHistory.push: " +
|
|
`"${explicitDest}" is not a valid explicitDest parameter.`
|
|
);
|
|
return;
|
|
} else if (!this._isValidPage(pageNumber)) {
|
|
// Allow an unset `pageNumber` if and only if the history is still empty;
|
|
// please refer to the `this._destination.page = null;` comment above.
|
|
if (pageNumber !== null || this._destination) {
|
|
console.error(
|
|
"PDFHistory.push: " +
|
|
`"${pageNumber}" is not a valid pageNumber parameter.`
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const hash = namedDest || JSON.stringify(explicitDest);
|
|
if (!hash) {
|
|
// The hash *should* never be undefined, but if that were to occur,
|
|
// avoid any possible issues by not updating the browser history.
|
|
return;
|
|
}
|
|
|
|
let forceReplace = false;
|
|
if (
|
|
this._destination &&
|
|
(isDestHashesEqual(this._destination.hash, hash) ||
|
|
isDestArraysEqual(this._destination.dest, explicitDest))
|
|
) {
|
|
// When the new destination is identical to `this._destination`, and
|
|
// its `page` is undefined, replace the current browser history entry.
|
|
// NOTE: This can only occur if `this._destination` was set either:
|
|
// - through the document hash being specified on load.
|
|
// - through the user changing the hash of the document.
|
|
if (this._destination.page) {
|
|
return;
|
|
}
|
|
forceReplace = true;
|
|
}
|
|
if (this._popStateInProgress && !forceReplace) {
|
|
return;
|
|
}
|
|
|
|
this._pushOrReplaceState(
|
|
{
|
|
dest: explicitDest,
|
|
hash,
|
|
page: pageNumber,
|
|
rotation: this.linkService.rotation,
|
|
},
|
|
forceReplace
|
|
);
|
|
|
|
if (!this._popStateInProgress) {
|
|
// Prevent the browser history from updating while the new destination is
|
|
// being scrolled into view, to avoid potentially inconsistent state.
|
|
this._popStateInProgress = true;
|
|
// We defer the resetting of `this._popStateInProgress`, to account for
|
|
// e.g. zooming occurring when the new destination is being navigated to.
|
|
Promise.resolve().then(() => {
|
|
this._popStateInProgress = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Push a page to the browser history; generally the `push` method should be
|
|
* used instead.
|
|
* @param {number} pageNumber
|
|
*/
|
|
pushPage(pageNumber) {
|
|
if (!this._initialized) {
|
|
return;
|
|
}
|
|
if (!this._isValidPage(pageNumber)) {
|
|
console.error(
|
|
`PDFHistory.pushPage: "${pageNumber}" is not a valid page number.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (this._destination?.page === pageNumber) {
|
|
// When the new page is identical to the one in `this._destination`, we
|
|
// don't want to add a potential duplicate entry in the browser history.
|
|
return;
|
|
}
|
|
if (this._popStateInProgress) {
|
|
return;
|
|
}
|
|
|
|
this._pushOrReplaceState({
|
|
// Simulate an internal destination, for `this._tryPushCurrentPosition`:
|
|
dest: null,
|
|
hash: `page=${pageNumber}`,
|
|
page: pageNumber,
|
|
rotation: this.linkService.rotation,
|
|
});
|
|
|
|
if (!this._popStateInProgress) {
|
|
// Prevent the browser history from updating while the new page is
|
|
// being scrolled into view, to avoid potentially inconsistent state.
|
|
this._popStateInProgress = true;
|
|
// We defer the resetting of `this._popStateInProgress`, to account for
|
|
// e.g. zooming occurring when the new page is being navigated to.
|
|
Promise.resolve().then(() => {
|
|
this._popStateInProgress = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Push the current position to the browser history.
|
|
*/
|
|
pushCurrentPosition() {
|
|
if (!this._initialized || this._popStateInProgress) {
|
|
return;
|
|
}
|
|
this._tryPushCurrentPosition();
|
|
}
|
|
|
|
/**
|
|
* Go back one step in the browser history.
|
|
* NOTE: Avoids navigating away from the document, useful for "named actions".
|
|
*/
|
|
back() {
|
|
if (!this._initialized || this._popStateInProgress) {
|
|
return;
|
|
}
|
|
const state = window.history.state;
|
|
if (this._isValidState(state) && state.uid > 0) {
|
|
window.history.back();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Go forward one step in the browser history.
|
|
* NOTE: Avoids navigating away from the document, useful for "named actions".
|
|
*/
|
|
forward() {
|
|
if (!this._initialized || this._popStateInProgress) {
|
|
return;
|
|
}
|
|
const state = window.history.state;
|
|
if (this._isValidState(state) && state.uid < this._maxUid) {
|
|
window.history.forward();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @type {boolean} Indicating if the user is currently moving through the
|
|
* browser history, useful e.g. for skipping the next 'hashchange' event.
|
|
*/
|
|
get popStateInProgress() {
|
|
return (
|
|
this._initialized &&
|
|
(this._popStateInProgress || this._blockHashChange > 0)
|
|
);
|
|
}
|
|
|
|
get initialBookmark() {
|
|
return this._initialized ? this._initialBookmark : null;
|
|
}
|
|
|
|
get initialRotation() {
|
|
return this._initialized ? this._initialRotation : null;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_pushOrReplaceState(destination, forceReplace = false) {
|
|
const shouldReplace = forceReplace || !this._destination;
|
|
const newState = {
|
|
fingerprint: this._fingerprint,
|
|
uid: shouldReplace ? this._uid : this._uid + 1,
|
|
destination,
|
|
};
|
|
|
|
if (
|
|
typeof PDFJSDev !== "undefined" &&
|
|
PDFJSDev.test("CHROME") &&
|
|
window.history.state?.chromecomState
|
|
) {
|
|
// history.state.chromecomState is managed by chromecom.js.
|
|
newState.chromecomState = window.history.state.chromecomState;
|
|
}
|
|
this._updateInternalState(destination, newState.uid);
|
|
|
|
let newUrl;
|
|
if (this._updateUrl && destination?.hash) {
|
|
const baseUrl = document.location.href.split("#", 1)[0];
|
|
// Prevent errors in Firefox.
|
|
if (!baseUrl.startsWith("file://")) {
|
|
newUrl = `${baseUrl}#${destination.hash}`;
|
|
}
|
|
}
|
|
if (shouldReplace) {
|
|
window.history.replaceState(newState, "", newUrl);
|
|
} else {
|
|
window.history.pushState(newState, "", newUrl);
|
|
}
|
|
|
|
if (
|
|
typeof PDFJSDev !== "undefined" &&
|
|
PDFJSDev.test("CHROME") &&
|
|
top === window
|
|
) {
|
|
// eslint-disable-next-line no-undef
|
|
chrome.runtime.sendMessage("showPageAction");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_tryPushCurrentPosition(temporary = false) {
|
|
if (!this._position) {
|
|
return;
|
|
}
|
|
let position = this._position;
|
|
if (temporary) {
|
|
position = Object.assign(Object.create(null), this._position);
|
|
position.temporary = true;
|
|
}
|
|
|
|
if (!this._destination) {
|
|
this._pushOrReplaceState(position);
|
|
return;
|
|
}
|
|
if (this._destination.temporary) {
|
|
// Always replace a previous *temporary* position.
|
|
this._pushOrReplaceState(position, /* forceReplace = */ true);
|
|
return;
|
|
}
|
|
if (this._destination.hash === position.hash) {
|
|
return; // The current document position has not changed.
|
|
}
|
|
if (
|
|
!this._destination.page &&
|
|
(POSITION_UPDATED_THRESHOLD <= 0 ||
|
|
this._numPositionUpdates <= POSITION_UPDATED_THRESHOLD)
|
|
) {
|
|
// `this._destination` was set through the user changing the hash of
|
|
// the document. Do not add `this._position` to the browser history,
|
|
// to avoid "flooding" it with lots of (nearly) identical entries,
|
|
// since we cannot ensure that the document position has changed.
|
|
return;
|
|
}
|
|
|
|
let forceReplace = false;
|
|
if (
|
|
this._destination.page >= position.first &&
|
|
this._destination.page <= position.page
|
|
) {
|
|
// When the `page` of `this._destination` is still visible, do not
|
|
// update the browsing history when `this._destination` either:
|
|
// - contains an internal destination, since in this case we
|
|
// cannot ensure that the document position has actually changed.
|
|
// - was set through the user changing the hash of the document.
|
|
if (this._destination.dest !== undefined || !this._destination.first) {
|
|
return;
|
|
}
|
|
// To avoid "flooding" the browser history, replace the current entry.
|
|
forceReplace = true;
|
|
}
|
|
this._pushOrReplaceState(position, forceReplace);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_isValidPage(val) {
|
|
return (
|
|
Number.isInteger(val) && val > 0 && val <= this.linkService.pagesCount
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_isValidState(state, checkReload = false) {
|
|
if (!state) {
|
|
return false;
|
|
}
|
|
if (state.fingerprint !== this._fingerprint) {
|
|
if (checkReload) {
|
|
// Potentially accept the history entry, even if the fingerprints don't
|
|
// match, when the viewer was reloaded (see issue 6847).
|
|
if (
|
|
typeof state.fingerprint !== "string" ||
|
|
state.fingerprint.length !== this._fingerprint.length
|
|
) {
|
|
return false;
|
|
}
|
|
const [perfEntry] = performance.getEntriesByType("navigation");
|
|
if (perfEntry?.type !== "reload") {
|
|
return false;
|
|
}
|
|
} else {
|
|
// This should only occur in viewers with support for opening more than
|
|
// one PDF document, e.g. the GENERIC viewer.
|
|
return false;
|
|
}
|
|
}
|
|
if (!Number.isInteger(state.uid) || state.uid < 0) {
|
|
return false;
|
|
}
|
|
if (state.destination === null || typeof state.destination !== "object") {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_updateInternalState(destination, uid, removeTemporary = false) {
|
|
if (this._updateViewareaTimeout) {
|
|
// When updating `this._destination`, make sure that we always wait for
|
|
// the next 'updateviewarea' event before (potentially) attempting to
|
|
// push the current position to the browser history.
|
|
clearTimeout(this._updateViewareaTimeout);
|
|
this._updateViewareaTimeout = null;
|
|
}
|
|
if (removeTemporary && destination?.temporary) {
|
|
// When the `destination` comes from the browser history,
|
|
// we no longer treat it as a *temporary* position.
|
|
delete destination.temporary;
|
|
}
|
|
this._destination = destination;
|
|
this._uid = uid;
|
|
this._maxUid = Math.max(this._maxUid, uid);
|
|
// This should always be reset when `this._destination` is updated.
|
|
this._numPositionUpdates = 0;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_parseCurrentHash(checkNameddest = false) {
|
|
const hash = unescape(getCurrentHash()).substring(1);
|
|
const params = parseQueryString(hash);
|
|
|
|
const nameddest = params.get("nameddest") || "";
|
|
let page = params.get("page") | 0;
|
|
|
|
if (!this._isValidPage(page) || (checkNameddest && nameddest.length > 0)) {
|
|
page = null;
|
|
}
|
|
return { hash, page, rotation: this.linkService.rotation };
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_updateViewarea({ location }) {
|
|
if (this._updateViewareaTimeout) {
|
|
clearTimeout(this._updateViewareaTimeout);
|
|
this._updateViewareaTimeout = null;
|
|
}
|
|
|
|
this._position = {
|
|
hash: location.pdfOpenParams.substring(1),
|
|
page: this.linkService.page,
|
|
first: location.pageNumber,
|
|
rotation: location.rotation,
|
|
};
|
|
|
|
if (this._popStateInProgress) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
POSITION_UPDATED_THRESHOLD > 0 &&
|
|
this._isPagesLoaded &&
|
|
this._destination &&
|
|
!this._destination.page
|
|
) {
|
|
// If the current destination was set through the user changing the hash
|
|
// of the document, we will usually not try to push the current position
|
|
// to the browser history; see `this._tryPushCurrentPosition()`.
|
|
//
|
|
// To prevent `this._tryPushCurrentPosition()` from effectively being
|
|
// reduced to a no-op in this case, we will assume that the position
|
|
// *did* in fact change if the 'updateviewarea' event was dispatched
|
|
// more than `POSITION_UPDATED_THRESHOLD` times.
|
|
this._numPositionUpdates++;
|
|
}
|
|
|
|
if (UPDATE_VIEWAREA_TIMEOUT > 0) {
|
|
// When closing the browser, a 'pagehide' event will be dispatched which
|
|
// *should* allow us to push the current position to the browser history.
|
|
// In practice, it seems that the event is arriving too late in order for
|
|
// the session history to be successfully updated.
|
|
// (For additional details, please refer to the discussion in
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1153393.)
|
|
//
|
|
// To workaround this we attempt to *temporarily* add the current position
|
|
// to the browser history only when the viewer is *idle*,
|
|
// i.e. when scrolling and/or zooming does not occur.
|
|
//
|
|
// PLEASE NOTE: It's absolutely imperative that the browser history is
|
|
// *not* updated too often, since that would render the viewer more or
|
|
// less unusable. Hence the use of a timeout to delay the update until
|
|
// the viewer has been idle for `UPDATE_VIEWAREA_TIMEOUT` milliseconds.
|
|
this._updateViewareaTimeout = setTimeout(() => {
|
|
if (!this._popStateInProgress) {
|
|
this._tryPushCurrentPosition(/* temporary = */ true);
|
|
}
|
|
this._updateViewareaTimeout = null;
|
|
}, UPDATE_VIEWAREA_TIMEOUT);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_popState({ state }) {
|
|
const newHash = getCurrentHash(),
|
|
hashChanged = this._currentHash !== newHash;
|
|
this._currentHash = newHash;
|
|
|
|
if (
|
|
(typeof PDFJSDev !== "undefined" &&
|
|
PDFJSDev.test("CHROME") &&
|
|
state?.chromecomState &&
|
|
!this._isValidState(state)) ||
|
|
!state
|
|
) {
|
|
// This case corresponds to the user changing the hash of the document.
|
|
this._uid++;
|
|
|
|
const { hash, page, rotation } = this._parseCurrentHash();
|
|
this._pushOrReplaceState(
|
|
{ hash, page, rotation },
|
|
/* forceReplace = */ true
|
|
);
|
|
return;
|
|
}
|
|
if (!this._isValidState(state)) {
|
|
// This should only occur in viewers with support for opening more than
|
|
// one PDF document, e.g. the GENERIC viewer.
|
|
return;
|
|
}
|
|
|
|
// Prevent the browser history from updating until the new destination,
|
|
// as stored in the browser history, has been scrolled into view.
|
|
this._popStateInProgress = true;
|
|
|
|
if (hashChanged) {
|
|
// When the hash changed, implying that the 'popstate' event will be
|
|
// followed by a 'hashchange' event, then we do *not* want to update the
|
|
// browser history when handling the 'hashchange' event (in web/app.js)
|
|
// since that would *overwrite* the new destination navigated to below.
|
|
//
|
|
// To avoid accidentally disabling all future user-initiated hash changes,
|
|
// if there's e.g. another 'hashchange' listener that stops the event
|
|
// propagation, we make sure to always force-reset `this._blockHashChange`
|
|
// after `HASH_CHANGE_TIMEOUT` milliseconds have passed.
|
|
this._blockHashChange++;
|
|
waitOnEventOrTimeout({
|
|
target: window,
|
|
name: "hashchange",
|
|
delay: HASH_CHANGE_TIMEOUT,
|
|
}).then(() => {
|
|
this._blockHashChange--;
|
|
});
|
|
}
|
|
|
|
// Navigate to the new destination.
|
|
const destination = state.destination;
|
|
this._updateInternalState(
|
|
destination,
|
|
state.uid,
|
|
/* removeTemporary = */ true
|
|
);
|
|
|
|
if (isValidRotation(destination.rotation)) {
|
|
this.linkService.rotation = destination.rotation;
|
|
}
|
|
if (destination.dest) {
|
|
this.linkService.goToDestination(destination.dest);
|
|
} else if (destination.hash) {
|
|
this.linkService.setHash(destination.hash);
|
|
} else if (destination.page) {
|
|
// Fallback case; shouldn't be necessary, but better safe than sorry.
|
|
this.linkService.page = destination.page;
|
|
}
|
|
|
|
// Since `PDFLinkService.goToDestination` is asynchronous, we thus defer the
|
|
// resetting of `this._popStateInProgress` slightly.
|
|
Promise.resolve().then(() => {
|
|
this._popStateInProgress = false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_pageHide() {
|
|
// Attempt to push the `this._position` into the browser history when
|
|
// navigating away from the document. This is *only* done if the history
|
|
// is empty/temporary, since otherwise an existing browser history entry
|
|
// will end up being overwritten (given that new entries cannot be pushed
|
|
// into the browser history when the 'unload' event has already fired).
|
|
if (!this._destination || this._destination.temporary) {
|
|
this._tryPushCurrentPosition();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_bindEvents() {
|
|
if (this._boundEvents) {
|
|
return; // The event listeners were already added.
|
|
}
|
|
this._boundEvents = {
|
|
updateViewarea: this._updateViewarea.bind(this),
|
|
popState: this._popState.bind(this),
|
|
pageHide: this._pageHide.bind(this),
|
|
};
|
|
|
|
this.eventBus._on("updateviewarea", this._boundEvents.updateViewarea);
|
|
window.addEventListener("popstate", this._boundEvents.popState);
|
|
window.addEventListener("pagehide", this._boundEvents.pageHide);
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_unbindEvents() {
|
|
if (!this._boundEvents) {
|
|
return; // The event listeners were already removed.
|
|
}
|
|
this.eventBus._off("updateviewarea", this._boundEvents.updateViewarea);
|
|
window.removeEventListener("popstate", this._boundEvents.popState);
|
|
window.removeEventListener("pagehide", this._boundEvents.pageHide);
|
|
|
|
this._boundEvents = null;
|
|
}
|
|
}
|
|
|
|
function isDestHashesEqual(destHash, pushHash) {
|
|
if (typeof destHash !== "string" || typeof pushHash !== "string") {
|
|
return false;
|
|
}
|
|
if (destHash === pushHash) {
|
|
return true;
|
|
}
|
|
const nameddest = parseQueryString(destHash).get("nameddest");
|
|
if (nameddest === pushHash) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isDestArraysEqual(firstDest, secondDest) {
|
|
function isEntryEqual(first, second) {
|
|
if (typeof first !== typeof second) {
|
|
return false;
|
|
}
|
|
if (Array.isArray(first) || Array.isArray(second)) {
|
|
return false;
|
|
}
|
|
if (first !== null && typeof first === "object" && second !== null) {
|
|
if (Object.keys(first).length !== Object.keys(second).length) {
|
|
return false;
|
|
}
|
|
for (const key in first) {
|
|
if (!isEntryEqual(first[key], second[key])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return first === second || (Number.isNaN(first) && Number.isNaN(second));
|
|
}
|
|
|
|
if (!(Array.isArray(firstDest) && Array.isArray(secondDest))) {
|
|
return false;
|
|
}
|
|
if (firstDest.length !== secondDest.length) {
|
|
return false;
|
|
}
|
|
for (let i = 0, ii = firstDest.length; i < ii; i++) {
|
|
if (!isEntryEqual(firstDest[i], secondDest[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export { isDestArraysEqual, isDestHashesEqual, PDFHistory };
|