Some checks failed
		
		
	
	Types tests / Test (lts/*) (push) Has been cancelled
				
			Lint / Lint (lts/*) (push) Has been cancelled
				
			CodeQL / Analyze (javascript) (push) Has been cancelled
				
			CI / Test (20) (push) Has been cancelled
				
			CI / Test (22) (push) Has been cancelled
				
			CI / Test (24) (push) Has been cancelled
				
			
		
			
				
	
	
		
			744 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			744 lines
		
	
	
		
			24 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 { updateUrlHash } from "pdfjs-lib";
 | 
						|
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 {
 | 
						|
  #eventAbortController = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {PDFHistoryOptions} options
 | 
						|
   */
 | 
						|
  constructor({ linkService, eventBus }) {
 | 
						|
    this.linkService = linkService;
 | 
						|
    this.eventBus = eventBus;
 | 
						|
 | 
						|
    this._initialized = false;
 | 
						|
    this._fingerprint = "";
 | 
						|
    this.reset();
 | 
						|
 | 
						|
    // 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;
 | 
						|
  }
 | 
						|
 | 
						|
  #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 { href, protocol } = document.location;
 | 
						|
      if (protocol !== "file:") {
 | 
						|
        newUrl = updateUrlHash(href, destination.hash);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (shouldReplace) {
 | 
						|
      window.history.replaceState(newState, "", newUrl);
 | 
						|
    } else {
 | 
						|
      window.history.pushState(newState, "", newUrl);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  #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);
 | 
						|
  }
 | 
						|
 | 
						|
  #isValidPage(val) {
 | 
						|
    return (
 | 
						|
      Number.isInteger(val) && val > 0 && val <= this.linkService.pagesCount
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  #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;
 | 
						|
  }
 | 
						|
 | 
						|
  #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;
 | 
						|
  }
 | 
						|
 | 
						|
  #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 };
 | 
						|
  }
 | 
						|
 | 
						|
  #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);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  #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;
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  #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();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  #bindEvents() {
 | 
						|
    if (this.#eventAbortController) {
 | 
						|
      return; // The event listeners were already added.
 | 
						|
    }
 | 
						|
    this.#eventAbortController = new AbortController();
 | 
						|
    const { signal } = this.#eventAbortController;
 | 
						|
 | 
						|
    this.eventBus._on("updateviewarea", this.#updateViewarea.bind(this), {
 | 
						|
      signal,
 | 
						|
    });
 | 
						|
    window.addEventListener("popstate", this.#popState.bind(this), { signal });
 | 
						|
    window.addEventListener("pagehide", this.#pageHide.bind(this), { signal });
 | 
						|
  }
 | 
						|
 | 
						|
  #unbindEvents() {
 | 
						|
    this.#eventAbortController?.abort();
 | 
						|
    this.#eventAbortController = 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 };
 |