/* 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.
 */

// eslint-disable-next-line max-len
/** @typedef {import("../src/display/api.js").PDFDocumentProxy} PDFDocumentProxy */
// eslint-disable-next-line max-len
/** @typedef {import("../src/display/api.js").PDFDocumentLoadingTask} PDFDocumentLoadingTask */

import {
  animationStarted,
  apiPageLayoutToViewerModes,
  apiPageModeToSidebarView,
  AutoPrintRegExp,
  CursorTool,
  DEFAULT_SCALE_VALUE,
  docStyle,
  getActiveOrFocusedElement,
  isValidRotation,
  isValidScrollMode,
  isValidSpreadMode,
  normalizeWheelEventDirection,
  parseQueryString,
  ProgressBar,
  ScrollMode,
  SidebarView,
  SpreadMode,
  TextLayerMode,
} from "./ui_utils.js";
import {
  AnnotationEditorType,
  build,
  FeatureTest,
  getDocument,
  getFilenameFromUrl,
  getPdfFilenameFromUrl,
  GlobalWorkerOptions,
  InvalidPDFException,
  isDataScheme,
  isPdfFile,
  OutputScale,
  PDFWorker,
  ResponseException,
  shadow,
  stopEvent,
  TouchManager,
  updateUrlHash,
  version,
} from "pdfjs-lib";
import { AppOptions, OptionKind } from "./app_options.js";
import { EventBus, FirefoxEventBus } from "./event_utils.js";
import { ExternalServices, initCom, MLManager } from "web-external_services";
import {
  ImageAltTextSettings,
  NewAltTextManager,
} from "web-new_alt_text_manager";
import { LinkTarget, PDFLinkService } from "./pdf_link_service.js";
import { AltTextManager } from "web-alt_text_manager";
import { AnnotationEditorParams } from "web-annotation_editor_params";
import { CaretBrowsingMode } from "./caret_browsing.js";
import { CommentManager } from "./comment_manager.js";
import { DownloadManager } from "web-download_manager";
import { EditorUndoBar } from "./editor_undo_bar.js";
import { OverlayManager } from "./overlay_manager.js";
import { PasswordPrompt } from "./password_prompt.js";
import { PDFAttachmentViewer } from "web-pdf_attachment_viewer";
import { PDFCursorTools } from "web-pdf_cursor_tools";
import { PDFDocumentProperties } from "web-pdf_document_properties";
import { PDFFindBar } from "web-pdf_find_bar";
import { PDFFindController } from "./pdf_find_controller.js";
import { PDFHistory } from "./pdf_history.js";
import { PDFLayerViewer } from "web-pdf_layer_viewer";
import { PDFOutlineViewer } from "web-pdf_outline_viewer";
import { PDFPresentationMode } from "web-pdf_presentation_mode";
import { PDFPrintServiceFactory } from "web-print_service";
import { PDFRenderingQueue } from "./pdf_rendering_queue.js";
import { PDFScriptingManager } from "./pdf_scripting_manager.js";
import { PdfTextExtractor } from "./pdf_text_extractor.js";
import { PDFThumbnailViewer } from "web-pdf_thumbnail_viewer";
import { PDFViewer } from "./pdf_viewer.js";
import { Preferences } from "web-preferences";
import { RenderingStates } from "./renderable_view.js";
import { SecondaryToolbar } from "web-secondary_toolbar";
import { SignatureManager } from "web-signature_manager";
import { Toolbar } from "web-toolbar";
import { ViewHistory } from "./view_history.js";
import { ViewsManager } from "web-views_manager";

const FORCE_PAGES_LOADED_TIMEOUT = 10000; // ms

const ViewOnLoad = {
  UNKNOWN: -1,
  PREVIOUS: 0, // Default value.
  INITIAL: 1,
};

const PDFViewerApplication = {
  initialBookmark: document.location.hash.substring(1),
  _initializedCapability: {
    ...Promise.withResolvers(),
    settled: false,
  },
  appConfig: null,
  /** @type {PDFDocumentProxy} */
  pdfDocument: null,
  /** @type {PDFDocumentLoadingTask} */
  pdfLoadingTask: null,
  printService: null,
  /** @type {PDFViewer} */
  pdfViewer: null,
  /** @type {PDFThumbnailViewer} */
  pdfThumbnailViewer: null,
  /** @type {PDFRenderingQueue} */
  pdfRenderingQueue: null,
  /** @type {PDFPresentationMode} */
  pdfPresentationMode: null,
  /** @type {PDFDocumentProperties} */
  pdfDocumentProperties: null,
  /** @type {PDFLinkService} */
  pdfLinkService: null,
  /** @type {PdfTextExtractor|null} */
  pdfTextExtractor: null,
  /** @type {PDFHistory} */
  pdfHistory: null,
  /** @type {ViewsManager} */
  viewsManager: null,
  /** @type {PDFOutlineViewer} */
  pdfOutlineViewer: null,
  /** @type {PDFAttachmentViewer} */
  pdfAttachmentViewer: null,
  /** @type {PDFLayerViewer} */
  pdfLayerViewer: null,
  /** @type {PDFCursorTools} */
  pdfCursorTools: null,
  /** @type {PDFScriptingManager} */
  pdfScriptingManager: null,
  /** @type {ViewHistory} */
  store: null,
  /** @type {DownloadManager} */
  downloadManager: null,
  /** @type {OverlayManager} */
  overlayManager: null,
  /** @type {Preferences} */
  preferences: new Preferences(),
  /** @type {Toolbar} */
  toolbar: null,
  /** @type {SecondaryToolbar} */
  secondaryToolbar: null,
  /** @type {EventBus} */
  eventBus: null,
  /** @type {L10n} */
  l10n: null,
  /** @type {AnnotationEditorParams} */
  annotationEditorParams: null,
  /** @type {ImageAltTextSettings} */
  imageAltTextSettings: null,
  isInitialViewSet: false,
  isViewerEmbedded: window.parent !== window,
  url: "",
  baseUrl: "",
  mlManager: null,
  _downloadUrl: "",
  _eventBusAbortController: null,
  _windowAbortController: null,
  _globalAbortController: new AbortController(),
  documentInfo: null,
  metadata: null,
  _contentDispositionFilename: null,
  _contentLength: null,
  _saveInProgress: false,
  _wheelUnusedTicks: 0,
  _wheelUnusedFactor: 1,
  _touchManager: null,
  _touchUnusedTicks: 0,
  _touchUnusedFactor: 1,
  _PDFBug: null,
  _hasAnnotationEditors: false,
  _title: document.title,
  _printAnnotationStoragePromise: null,
  _isCtrlKeyDown: false,
  _caretBrowsing: null,
  _isScrolling: false,
  editorUndoBar: null,
  _printPermissionPromise: null,

  // Called once when the document is loaded.
  async initialize(appConfig) {
    this.appConfig = appConfig;

    // Ensure that `Preferences`, and indirectly `AppOptions`, have initialized
    // before creating e.g. the various viewer components.
    try {
      await this.preferences.initializedPromise;
    } catch (ex) {
      console.error("initialize:", ex);
    }
    if (AppOptions.get("pdfBugEnabled")) {
      await this._parseHashParams();
    }

    let mode;
    switch (AppOptions.get("viewerCssTheme")) {
      case 1:
        mode = "light";
        break;
      case 2:
        mode = "dark";
        break;
    }
    if (mode) {
      docStyle.setProperty("color-scheme", mode);
    }

    if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
      if (AppOptions.get("enableFakeMLManager")) {
        this.mlManager =
          MLManager.getFakeMLManager?.({
            enableGuessAltText: AppOptions.get("enableGuessAltText"),
            enableAltTextModelDownload: AppOptions.get(
              "enableAltTextModelDownload"
            ),
          }) || null;
      }
    } else if (PDFJSDev.test("MOZCENTRAL")) {
      if (AppOptions.get("enableAltText")) {
        // We want to load the image-to-text AI engine as soon as possible.
        this.mlManager = new MLManager({
          enableGuessAltText: AppOptions.get("enableGuessAltText"),
          enableAltTextModelDownload: AppOptions.get(
            "enableAltTextModelDownload"
          ),
          altTextLearnMoreUrl: AppOptions.get("altTextLearnMoreUrl"),
        });
      }
    }

    // Ensure that the `L10n`-instance has been initialized before creating
    // e.g. the various viewer components.
    this.l10n = await this.externalServices.createL10n();
    document.getElementsByTagName("html")[0].dir = this.l10n.getDirection();
    // Connect Fluent, when necessary, and translate what we already have.
    if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
      this.l10n.translate(appConfig.appContainer || document.documentElement);
    }

    if (
      this.isViewerEmbedded &&
      AppOptions.get("externalLinkTarget") === LinkTarget.NONE
    ) {
      // Prevent external links from "replacing" the viewer,
      // when it's embedded in e.g. an <iframe> or an <object>.
      AppOptions.set("externalLinkTarget", LinkTarget.TOP);
    }
    await this._initializeViewerComponents();

    this.pdfTextExtractor = new PdfTextExtractor(this.externalServices);

    // Bind the various event handlers *after* the viewer has been
    // initialized, to prevent errors if an event arrives too soon.
    this.bindEvents();
    this.bindWindowEvents();

    this._initializedCapability.settled = true;
    this._initializedCapability.resolve();
  },

  /**
   * Potentially parse special debugging flags in the hash section of the URL.
   * @private
   */
  async _parseHashParams() {
    const hash = document.location.hash.substring(1);
    if (!hash) {
      return;
    }
    const { mainContainer, viewerContainer } = this.appConfig,
      params = parseQueryString(hash);

    const loadPDFBug = async () => {
      if (this._PDFBug) {
        return;
      }
      const { PDFBug } =
        typeof PDFJSDev === "undefined"
          ? await import(AppOptions.get("debuggerSrc")) // eslint-disable-line no-unsanitized/method
          : await __raw_import__(AppOptions.get("debuggerSrc"));

      this._PDFBug = PDFBug;
    };

    // Parameters that need to be handled manually.
    if (params.get("disableworker") === "true") {
      try {
        GlobalWorkerOptions.workerSrc ||= AppOptions.get("workerSrc");

        typeof PDFJSDev === "undefined" // eslint-disable-line no-unused-expressions
          ? await import("pdfjs/pdf.worker.js")
          : await __raw_import__(PDFWorker.workerSrc);

        // Ensure that the "fake" worker won't be ignored.
        AppOptions.set("workerPort", null);
      } catch (ex) {
        console.error("_parseHashParams:", ex);
      }
    }
    if (params.has("textlayer")) {
      switch (params.get("textlayer")) {
        case "off":
          AppOptions.set("textLayerMode", TextLayerMode.DISABLE);
          break;
        case "visible":
        case "shadow":
        case "hover":
          viewerContainer.classList.add(`textLayer-${params.get("textlayer")}`);
          try {
            await loadPDFBug();
            this._PDFBug.loadCSS();
          } catch (ex) {
            console.error("_parseHashParams:", ex);
          }
          break;
      }
    }
    if (params.has("pdfbug")) {
      const enabled = params.get("pdfbug").split(",");
      try {
        await loadPDFBug();
        this._PDFBug.init(mainContainer, enabled);
      } catch (ex) {
        console.error("_parseHashParams:", ex);
      }

      const debugOpts = { pdfBug: true, fontExtraProperties: true };
      if (globalThis.StepperManager?.enabled) {
        debugOpts.minDurationToUpdateCanvas = 0;
      }
      AppOptions.setAll(debugOpts);
    }
    // It is not possible to change locale for the (various) extension builds.
    if (
      (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
      params.has("locale")
    ) {
      AppOptions.set("localeProperties", { lang: params.get("locale") });
    }

    // Parameters that can be handled automatically.
    const opts = {
      disableAutoFetch: x => x === "true",
      disableFontFace: x => x === "true",
      disableHistory: x => x === "true",
      disableRange: x => x === "true",
      disableStream: x => x === "true",
      verbosity: x => x | 0,
    };

    // Set some specific preferences for tests.
    if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
      Object.assign(opts, {
        capCanvasAreaFactor: x => parseInt(x),
        docBaseUrl: x => x,
        enableAltText: x => x === "true",
        enableAutoLinking: x => x === "true",
        enableComment: x => x === "true",
        enableFakeMLManager: x => x === "true",
        enableGuessAltText: x => x === "true",
        enablePermissions: x => x === "true",
        enableSplitMerge: x => x === "true",
        enableUpdatedAddImage: x => x === "true",
        highlightEditorColors: x => x,
        maxCanvasPixels: x => parseInt(x),
        spreadModeOnLoad: x => parseInt(x),
        supportsCaretBrowsingMode: x => x === "true",
        viewerCssTheme: x => parseInt(x),
        forcePageColors: x => x === "true",
        pageColorsBackground: x => x,
        pageColorsForeground: x => x,
        sidebarViewOnLoad: x => parseInt(x),
      });
    }

    for (const name in opts) {
      const check = opts[name],
        key = name.toLowerCase();

      if (params.has(key)) {
        AppOptions.set(name, check(params.get(key)));
      }
    }
  },

  /**
   * @private
   */
  async _initializeViewerComponents() {
    const { appConfig, externalServices, l10n, mlManager } = this;
    const abortSignal = this._globalAbortController.signal;

    const eventBus =
      typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")
        ? new FirefoxEventBus(
            AppOptions.get("allowedGlobalEvents"),
            externalServices,
            AppOptions.get("isInAutomation")
          )
        : new EventBus();
    this.eventBus = AppOptions.eventBus = eventBus;

    mlManager?.setEventBus(eventBus, abortSignal);

    const overlayManager = (this.overlayManager = new OverlayManager());

    const renderingQueue = (this.pdfRenderingQueue = new PDFRenderingQueue());
    renderingQueue.onIdle = this._cleanup.bind(this);

    const linkService = (this.pdfLinkService = new PDFLinkService({
      eventBus,
      externalLinkTarget: AppOptions.get("externalLinkTarget"),
      externalLinkRel: AppOptions.get("externalLinkRel"),
      ignoreDestinationZoom: AppOptions.get("ignoreDestinationZoom"),
    }));

    const downloadManager = (this.downloadManager = new DownloadManager());

    const findController = (this.findController = new PDFFindController({
      linkService,
      eventBus,
      updateMatchesCountOnProgress:
        typeof PDFJSDev === "undefined"
          ? !window.isGECKOVIEW
          : !PDFJSDev.test("GECKOVIEW"),
    }));

    const pdfScriptingManager = (this.pdfScriptingManager =
      new PDFScriptingManager({
        eventBus,
        externalServices,
        docProperties: this._scriptingDocProperties.bind(this),
      }));

    const container = appConfig.mainContainer,
      viewer = appConfig.viewerContainer;
    const annotationEditorMode = AppOptions.get("annotationEditorMode");
    const hasForcedColors =
      AppOptions.get("forcePageColors") ||
      window.matchMedia("(forced-colors: active)").matches;
    const pageColors = hasForcedColors
      ? {
          background: AppOptions.get("pageColorsBackground"),
          foreground: AppOptions.get("pageColorsForeground"),
        }
      : null;

    let altTextManager;
    if (AppOptions.get("enableUpdatedAddImage")) {
      altTextManager = appConfig.newAltTextDialog
        ? new NewAltTextManager(
            appConfig.newAltTextDialog,
            overlayManager,
            eventBus
          )
        : null;
    } else {
      altTextManager = appConfig.altTextDialog
        ? new AltTextManager(
            appConfig.altTextDialog,
            container,
            overlayManager,
            eventBus
          )
        : null;
    }

    if (appConfig.editorUndoBar) {
      this.editorUndoBar = new EditorUndoBar(appConfig.editorUndoBar, eventBus);
    }

    const signatureManager =
      AppOptions.get("enableSignatureEditor") && appConfig.addSignatureDialog
        ? new SignatureManager(
            appConfig.addSignatureDialog,
            appConfig.editSignatureDialog,
            appConfig.annotationEditorParams?.editorSignatureAddSignature ||
              null,
            overlayManager,
            l10n,
            externalServices.createSignatureStorage(eventBus, abortSignal),
            eventBus
          )
        : null;

    const ltr = appConfig.viewerContainer
      ? getComputedStyle(appConfig.viewerContainer).direction === "ltr"
      : true;
    const commentManager =
      AppOptions.get("enableComment") && appConfig.editCommentDialog
        ? new CommentManager(
            appConfig.editCommentDialog,
            {
              learnMoreUrl: AppOptions.get("commentLearnMoreUrl"),
              sidebar:
                appConfig.annotationEditorParams?.editorCommentsSidebar || null,
              sidebarResizer:
                appConfig.annotationEditorParams
                  ?.editorCommentsSidebarResizer || null,
              commentsList:
                appConfig.annotationEditorParams?.editorCommentsSidebarList ||
                null,
              commentCount:
                appConfig.annotationEditorParams?.editorCommentsSidebarCount ||
                null,
              sidebarTitle:
                appConfig.annotationEditorParams?.editorCommentsSidebarTitle ||
                null,
              closeButton:
                appConfig.annotationEditorParams
                  ?.editorCommentsSidebarCloseButton || null,
              commentToolbarButton:
                appConfig.toolbar?.editorCommentButton || null,
            },
            eventBus,
            linkService,
            overlayManager,
            ltr,
            hasForcedColors
          )
        : null;

    const enableHWA = AppOptions.get("enableHWA"),
      maxCanvasPixels = AppOptions.get("maxCanvasPixels"),
      maxCanvasDim = AppOptions.get("maxCanvasDim"),
      capCanvasAreaFactor = AppOptions.get("capCanvasAreaFactor");
    const pdfViewer = (this.pdfViewer = new PDFViewer({
      container,
      viewer,
      viewerAlert: appConfig.viewerAlert,
      eventBus,
      renderingQueue,
      linkService,
      downloadManager,
      altTextManager,
      commentManager,
      signatureManager,
      editorUndoBar: this.editorUndoBar,
      findController,
      scriptingManager:
        AppOptions.get("enableScripting") && pdfScriptingManager,
      l10n,
      textLayerMode: AppOptions.get("textLayerMode"),
      annotationMode: AppOptions.get("annotationMode"),
      annotationEditorMode,
      annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"),
      enableHighlightFloatingButton: AppOptions.get(
        "enableHighlightFloatingButton"
      ),
      enableUpdatedAddImage: AppOptions.get("enableUpdatedAddImage"),
      enableNewAltTextWhenAddingImage: AppOptions.get(
        "enableNewAltTextWhenAddingImage"
      ),
      imageResourcesPath: AppOptions.get("imageResourcesPath"),
      enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
      maxCanvasPixels,
      maxCanvasDim,
      capCanvasAreaFactor,
      enableDetailCanvas: AppOptions.get("enableDetailCanvas"),
      enablePermissions: AppOptions.get("enablePermissions"),
      enableOptimizedPartialRendering: AppOptions.get(
        "enableOptimizedPartialRendering"
      ),
      pageColors,
      mlManager,
      abortSignal,
      enableHWA,
      supportsPinchToZoom: this.supportsPinchToZoom,
      enableAutoLinking: AppOptions.get("enableAutoLinking"),
      minDurationToUpdateCanvas: AppOptions.get("minDurationToUpdateCanvas"),
    }));

    renderingQueue.setViewer(pdfViewer);
    linkService.setViewer(pdfViewer);
    pdfScriptingManager.setViewer(pdfViewer);

    if (appConfig.viewsManager?.thumbnailsView) {
      this.pdfThumbnailViewer = new PDFThumbnailViewer({
        container: appConfig.viewsManager.thumbnailsView,
        eventBus,
        renderingQueue,
        linkService,
        maxCanvasPixels,
        maxCanvasDim,
        pageColors,
        abortSignal,
        enableHWA,
        enableSplitMerge: AppOptions.get("enableSplitMerge"),
        manageMenu: appConfig.viewsManager.manageMenu,
        addFileButton: appConfig.viewsManager.viewsManagerAddFileButton,
      });
      renderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
    }

    // The browsing history is only enabled when the viewer is standalone,
    // i.e. not when it is embedded in a web page.
    if (!this.isViewerEmbedded && !AppOptions.get("disableHistory")) {
      this.pdfHistory = new PDFHistory({
        linkService,
        eventBus,
      });
      linkService.setHistory(this.pdfHistory);
    }

    if (!this.supportsIntegratedFind && appConfig.findBar) {
      this.findBar = new PDFFindBar(
        appConfig.findBar,
        appConfig.principalContainer,
        eventBus
      );
    }

    if (appConfig.annotationEditorParams) {
      if (annotationEditorMode !== AnnotationEditorType.DISABLE) {
        const editorSignatureButton = appConfig.toolbar?.editorSignatureButton;
        if (editorSignatureButton && AppOptions.get("enableSignatureEditor")) {
          editorSignatureButton.parentElement.hidden = false;
        }
        const editorCommentButton = appConfig.toolbar?.editorCommentButton;
        if (editorCommentButton && AppOptions.get("enableComment")) {
          editorCommentButton.parentElement.hidden = false;
        }
        this.annotationEditorParams = new AnnotationEditorParams(
          appConfig.annotationEditorParams,
          eventBus
        );
      } else {
        for (const id of ["editorModeButtons", "editorModeSeparator"]) {
          document.getElementById(id)?.classList.add("hidden");
        }
      }
    }

    if (mlManager && appConfig.secondaryToolbar?.imageAltTextSettingsButton) {
      this.imageAltTextSettings = new ImageAltTextSettings(
        appConfig.altTextSettingsDialog,
        overlayManager,
        eventBus,
        mlManager
      );
    }

    if (appConfig.documentProperties) {
      this.pdfDocumentProperties = new PDFDocumentProperties(
        appConfig.documentProperties,
        overlayManager,
        eventBus,
        l10n,
        /* fileNameLookup = */ () => this._docFilename,
        /* titleLookup = */ () => this._docTitle
      );
    }

    // NOTE: The cursor-tools are unlikely to be helpful/useful in GeckoView,
    // in particular the `HandTool` which basically simulates touch scrolling.
    if (appConfig.secondaryToolbar?.cursorHandToolButton) {
      this.pdfCursorTools = new PDFCursorTools({
        container,
        eventBus,
        cursorToolOnLoad: AppOptions.get("cursorToolOnLoad"),
      });
    }

    if (appConfig.toolbar) {
      if (
        typeof PDFJSDev === "undefined"
          ? window.isGECKOVIEW
          : PDFJSDev.test("GECKOVIEW")
      ) {
        const nimbusData = JSON.parse(
          AppOptions.get("nimbusDataStr") || "null"
        );
        this.toolbar = new Toolbar(appConfig.toolbar, eventBus, nimbusData);
      } else {
        this.toolbar = new Toolbar(
          appConfig.toolbar,
          eventBus,
          AppOptions.get("toolbarDensity")
        );
      }
    }

    if (appConfig.secondaryToolbar) {
      if (AppOptions.get("enableAltText")) {
        appConfig.secondaryToolbar.imageAltTextSettingsButton?.classList.remove(
          "hidden"
        );
        appConfig.secondaryToolbar.imageAltTextSettingsSeparator?.classList.remove(
          "hidden"
        );
      }

      this.secondaryToolbar = new SecondaryToolbar(
        appConfig.secondaryToolbar,
        eventBus
      );
    }

    if (
      this.supportsFullscreen &&
      appConfig.secondaryToolbar?.presentationModeButton
    ) {
      this.pdfPresentationMode = new PDFPresentationMode({
        container,
        pdfViewer,
        eventBus,
      });
    }

    if (appConfig.passwordOverlay) {
      this.passwordPrompt = new PasswordPrompt(
        appConfig.passwordOverlay,
        overlayManager,
        this.isViewerEmbedded
      );
    }

    if (appConfig.viewsManager?.outlinesView) {
      this.pdfOutlineViewer = new PDFOutlineViewer({
        container: appConfig.viewsManager.outlinesView,
        eventBus,
        l10n,
        linkService,
        downloadManager,
      });
    }

    if (appConfig.viewsManager?.attachmentsView) {
      this.pdfAttachmentViewer = new PDFAttachmentViewer({
        container: appConfig.viewsManager.attachmentsView,
        eventBus,
        l10n,
        downloadManager,
      });
    }

    if (appConfig.viewsManager?.layersView) {
      this.pdfLayerViewer = new PDFLayerViewer({
        container: appConfig.viewsManager.layersView,
        eventBus,
        l10n,
      });
    }

    if (appConfig.viewsManager) {
      this.viewsManager = new ViewsManager({
        elements: appConfig.viewsManager,
        eventBus,
        l10n,
      });
      this.viewsManager.onToggled = this.forceRendering.bind(this);
      this.viewsManager.onUpdateThumbnails = () => {
        // Use the rendered pages to set the corresponding thumbnail images.
        for (const pageView of pdfViewer.getCachedPageViews()) {
          if (pageView.renderingState === RenderingStates.FINISHED) {
            this.pdfThumbnailViewer
              .getThumbnail(pageView.id - 1)
              ?.setImage(pageView);
          }
        }
        this.pdfThumbnailViewer.scrollThumbnailIntoView(
          pdfViewer.currentPageNumber
        );
      };
    }
  },

  async run(config) {
    await this.initialize(config);

    const { appConfig, eventBus } = this;
    let file;
    if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
      const queryString = document.location.search.substring(1);
      const params = parseQueryString(queryString);
      file = params.get("file") ?? AppOptions.get("defaultUrl");
      try {
        file = new URL(file).href;
      } catch {
        file = encodeURIComponent(file).replaceAll("%2F", "/");
      }
      validateFileURL(file);
    } else if (PDFJSDev.test("MOZCENTRAL")) {
      file = window.location.href;
    } else if (PDFJSDev.test("CHROME")) {
      file = AppOptions.get("defaultUrl");
    }

    if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
      const fileInput = (this._openFileInput = document.createElement("input"));
      fileInput.id = "fileInput";
      fileInput.hidden = true;
      fileInput.type = "file";
      fileInput.value = null;
      document.body.append(fileInput);

      fileInput.addEventListener("change", function (evt) {
        const { files } = evt.target;
        if (!files || files.length === 0) {
          return;
        }
        eventBus.dispatch("fileinputchange", {
          source: this,
          fileInput: evt.target,
        });
      });

      // Enable dragging-and-dropping a new PDF file onto the viewerContainer.
      appConfig.mainContainer.addEventListener("dragover", function (evt) {
        for (const item of evt.dataTransfer.items) {
          if (item.type === "application/pdf") {
            evt.dataTransfer.dropEffect =
              evt.dataTransfer.effectAllowed === "copy" ? "copy" : "move";
            stopEvent(evt);
            return;
          }
        }
      });
      appConfig.mainContainer.addEventListener("drop", function (evt) {
        if (evt.dataTransfer.files?.[0].type !== "application/pdf") {
          return;
        }
        stopEvent(evt);
        eventBus.dispatch("fileinputchange", {
          source: this,
          fileInput: evt.dataTransfer,
        });
      });
    }

    if (!AppOptions.get("supportsDocumentFonts")) {
      AppOptions.set("disableFontFace", true);
      this.l10n.get("pdfjs-web-fonts-disabled").then(msg => {
        console.warn(msg);
      });
    }

    const togglePrintingButtons = visible => {
      appConfig.toolbar?.print?.classList.toggle("hidden", !visible);
      appConfig.secondaryToolbar?.printButton.classList.toggle(
        "hidden",
        !visible
      );
    };
    if (!this.supportsPrinting) {
      togglePrintingButtons(false);
    } else {
      eventBus.on("printingallowed", ({ isAllowed }) =>
        togglePrintingButtons(isAllowed)
      );
    }

    if (!this.supportsFullscreen) {
      appConfig.secondaryToolbar?.presentationModeButton.classList.add(
        "hidden"
      );
    }

    if (this.supportsIntegratedFind) {
      appConfig.findBar?.toggleButton?.classList.add("hidden");
    }

    if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
      if (file) {
        this.open({ url: file });
      } else {
        this._hideViewBookmark();
      }
    } else if (PDFJSDev.test("MOZCENTRAL || CHROME")) {
      this.setTitleUsingUrl(file, /* downloadUrl = */ file);

      this.externalServices.initPassiveLoading();
    } else {
      throw new Error("Not implemented: run");
    }
  },

  get externalServices() {
    return shadow(this, "externalServices", new ExternalServices());
  },

  get initialized() {
    return this._initializedCapability.settled;
  },

  get initializedPromise() {
    return this._initializedCapability.promise;
  },

  updateZoom(steps, scaleFactor, origin) {
    if (this.pdfViewer.isInPresentationMode) {
      return;
    }
    this.pdfViewer.updateScale({
      drawingDelay: AppOptions.get("defaultZoomDelay"),
      steps,
      scaleFactor,
      origin,
    });
  },

  zoomIn() {
    this.updateZoom(1);
  },

  zoomOut() {
    this.updateZoom(-1);
  },

  zoomReset() {
    if (this.pdfViewer.isInPresentationMode) {
      return;
    }
    this.pdfViewer.currentScaleValue = DEFAULT_SCALE_VALUE;
  },

  touchPinchCallback(origin, prevDistance, distance) {
    if (this.supportsPinchToZoom) {
      const newScaleFactor = this._accumulateFactor(
        this.pdfViewer.currentScale,
        distance / prevDistance,
        "_touchUnusedFactor"
      );
      this.updateZoom(null, newScaleFactor, origin);
    } else {
      const PIXELS_PER_LINE_SCALE = 30;
      const ticks = this._accumulateTicks(
        (distance - prevDistance) / PIXELS_PER_LINE_SCALE,
        "_touchUnusedTicks"
      );
      this.updateZoom(ticks, null, origin);
    }
  },

  touchPinchEndCallback() {
    this._touchUnusedTicks = 0;
    this._touchUnusedFactor = 1;
  },

  get pagesCount() {
    return this.pdfDocument ? this.pdfDocument.numPages : 0;
  },

  get page() {
    return this.pdfViewer.currentPageNumber;
  },

  set page(val) {
    this.pdfViewer.currentPageNumber = val;
  },

  get supportsPrinting() {
    return shadow(
      this,
      "supportsPrinting",
      AppOptions.get("supportsPrinting") &&
        PDFPrintServiceFactory.supportsPrinting
    );
  },

  get supportsFullscreen() {
    return shadow(this, "supportsFullscreen", document.fullscreenEnabled);
  },

  get supportsPinchToZoom() {
    return shadow(
      this,
      "supportsPinchToZoom",
      AppOptions.get("supportsPinchToZoom")
    );
  },

  get supportsIntegratedFind() {
    return shadow(
      this,
      "supportsIntegratedFind",
      AppOptions.get("supportsIntegratedFind")
    );
  },

  get loadingBar() {
    const barElement = document.getElementById("loadingBar");
    const bar = barElement ? new ProgressBar(barElement) : null;
    return shadow(this, "loadingBar", bar);
  },

  get supportsMouseWheelZoomCtrlKey() {
    return shadow(
      this,
      "supportsMouseWheelZoomCtrlKey",
      AppOptions.get("supportsMouseWheelZoomCtrlKey")
    );
  },

  get supportsMouseWheelZoomMetaKey() {
    return shadow(
      this,
      "supportsMouseWheelZoomMetaKey",
      AppOptions.get("supportsMouseWheelZoomMetaKey")
    );
  },

  get supportsCaretBrowsingMode() {
    return AppOptions.get("supportsCaretBrowsingMode");
  },

  moveCaret(isUp, select) {
    this._caretBrowsing ||= new CaretBrowsingMode(
      this._globalAbortController.signal,
      this.appConfig.mainContainer,
      this.appConfig.viewerContainer,
      this.appConfig.toolbar?.container
    );
    this._caretBrowsing.moveCaret(isUp, select);
  },

  setTitleUsingUrl(url = "", downloadUrl = null) {
    this.url = url;
    this.baseUrl =
      typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")
        ? updateUrlHash(url, "", /* allowRel = */ true)
        : updateUrlHash(url, "");
    if (downloadUrl) {
      this._downloadUrl =
        // eslint-disable-next-line no-nested-ternary
        downloadUrl === url
          ? this.baseUrl
          : typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")
            ? updateUrlHash(downloadUrl, "", /* allowRel = */ true)
            : updateUrlHash(downloadUrl, "");
    }
    if (isDataScheme(url)) {
      this._hideViewBookmark();
    } else if (
      typeof PDFJSDev !== "undefined" &&
      PDFJSDev.test("MOZCENTRAL || CHROME")
    ) {
      AppOptions.set("docBaseUrl", this.baseUrl);
    }

    let title = getPdfFilenameFromUrl(url, "");
    if (!title) {
      try {
        title = decodeURIComponent(getFilenameFromUrl(url));
      } catch {
        // decodeURIComponent may throw URIError.
      }
    }
    this.setTitle(title || url); // Always fallback to the raw URL.
  },

  setTitle(title = this._title) {
    this._title = title;

    if (this.isViewerEmbedded) {
      // Embedded PDF viewers should not be changing their parent page's title.
      return;
    }
    const editorIndicator =
      this._hasAnnotationEditors && !this.pdfRenderingQueue.printing;
    document.title = `${editorIndicator ? "* " : ""}${title}`;
  },

  get _docFilename() {
    // Use `this.url` instead of `this.baseUrl` to perform filename detection
    // based on the reference fragment as ultimate fallback if needed.
    return this._contentDispositionFilename || getPdfFilenameFromUrl(this.url);
  },

  get _docTitle() {
    const { documentInfo, metadata } = this;

    const title = metadata?.get("dc:title");
    if (title) {
      // Ghostscript can produce invalid 'dc:title' Metadata entries:
      //  - The title may be "Untitled" (fixes bug 1031612).
      //  - The title may contain incorrectly encoded characters, which thus
      //    looks broken, hence we ignore the Metadata entry when it contains
      //    characters from the Specials Unicode block (fixes bug 1605526).
      if (title !== "Untitled" && !/[\uFFF0-\uFFFF]/g.test(title)) {
        return title;
      }
    }
    return documentInfo.Title;
  },

  /**
   * @private
   */
  _hideViewBookmark() {
    const { secondaryToolbar } = this.appConfig;
    // URL does not reflect proper document location - hiding some buttons.
    secondaryToolbar?.viewBookmarkButton.classList.add("hidden");

    // Avoid displaying multiple consecutive separators in the secondaryToolbar.
    if (secondaryToolbar?.presentationModeButton.classList.contains("hidden")) {
      document.getElementById("viewBookmarkSeparator")?.classList.add("hidden");
    }
  },

  /**
   * Closes opened PDF document.
   * @returns {Promise} - Returns the promise, which is resolved when all
   *                      destruction is completed.
   */
  async close() {
    this._unblockDocumentLoadEvent();
    this._hideViewBookmark();

    if (!this.pdfLoadingTask) {
      return;
    }
    if (
      (typeof PDFJSDev === "undefined" ||
        PDFJSDev.test("GENERIC && !TESTING")) &&
      this.pdfDocument?.annotationStorage.size > 0 &&
      this._annotationStorageModified
    ) {
      try {
        // Trigger saving, to prevent data loss in forms; see issue 12257.
        await this.save();
      } catch {
        // Ignoring errors, to ensure that document closing won't break.
      }
    }
    const promises = [];

    promises.push(this.pdfLoadingTask.destroy());
    this.pdfLoadingTask = null;

    if (this.pdfDocument) {
      this.pdfDocument = null;

      this.pdfThumbnailViewer?.setDocument(null);
      this.pdfViewer.setDocument(null);
      this.pdfLinkService.setDocument(null);
      this.pdfDocumentProperties?.setDocument(null);
      this.pdfTextExtractor?.setViewer(null);
    }
    this.pdfLinkService.externalLinkEnabled = true;
    this.store = null;
    this.isInitialViewSet = false;
    this.url = "";
    this.baseUrl = "";
    this._downloadUrl = "";
    this.documentInfo = null;
    this.metadata = null;
    this._contentDispositionFilename = null;
    this._contentLength = null;
    this._saveInProgress = false;
    this._hasAnnotationEditors = false;

    promises.push(
      this.pdfScriptingManager.destroyPromise,
      this.passwordPrompt.close()
    );

    this.setTitle();
    this.viewsManager?.reset();
    this.pdfOutlineViewer?.reset();
    this.pdfAttachmentViewer?.reset();
    this.pdfLayerViewer?.reset();

    this.pdfHistory?.reset();
    this.findBar?.reset();
    this.toolbar?.reset();
    this.secondaryToolbar?.reset();
    this._PDFBug?.cleanup();

    await Promise.all(promises);
  },

  /**
   * Opens a new PDF document.
   * @param {Object} args - Accepts any/all of the properties from
   *   {@link DocumentInitParameters}, and also a `originalUrl` string.
   * @returns {Promise} - Promise that is resolved when the document is opened.
   */
  async open(args) {
    if (this.pdfLoadingTask) {
      // We need to destroy already opened document.
      await this.close();
    }
    // Set the necessary global worker parameters, using the available options.
    const workerParams = AppOptions.getAll(OptionKind.WORKER);
    Object.assign(GlobalWorkerOptions, workerParams);

    if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
      if (args.data && isPdfFile(args.filename)) {
        this._contentDispositionFilename = args.filename;
      }
    } else if (args.url) {
      // The Firefox built-in viewer always calls `setTitleUsingUrl`, before
      // `initPassiveLoading`, and it never provides an `originalUrl` here.
      this.setTitleUsingUrl(
        args.originalUrl || args.url,
        /* downloadUrl = */ args.url
      );
    }

    // Set the necessary API parameters, using all the available options.
    const apiParams = AppOptions.getAll(OptionKind.API);
    const loadingTask = getDocument({
      ...apiParams,
      ...args,
    });
    this.pdfLoadingTask = loadingTask;

    loadingTask.onPassword = (updateCallback, reason) => {
      if (this.isViewerEmbedded) {
        // The load event can't be triggered until the password is entered, so
        // if the viewer is in an iframe and its visibility depends on the
        // onload callback then the viewer never shows (bug 1801341).
        this._unblockDocumentLoadEvent();
      }

      this.pdfLinkService.externalLinkEnabled = false;
      this.passwordPrompt.setUpdateCallback(updateCallback, reason);
      this.passwordPrompt.open();
    };

    loadingTask.onProgress = evt => this.progress(evt.percent);

    return loadingTask.promise.then(
      pdfDocument => {
        this.load(pdfDocument);
      },
      reason => {
        if (loadingTask !== this.pdfLoadingTask) {
          return undefined; // Ignore errors for previously opened PDF files.
        }

        let key = "pdfjs-loading-error";
        if (reason instanceof InvalidPDFException) {
          key = "pdfjs-invalid-file-error";
        } else if (reason instanceof ResponseException) {
          key = reason.missing
            ? "pdfjs-missing-file-error"
            : "pdfjs-unexpected-response-error";
        }
        return this._documentError(key, { message: reason.message }).then(
          () => {
            throw reason;
          }
        );
      }
    );
  },

  async download() {
    let data;
    try {
      data = await (this.pdfDocument
        ? this.pdfDocument.getData()
        : this.pdfLoadingTask.getData());
    } catch {
      // When the PDF document isn't ready, simply download using the URL.
    }
    this.downloadManager.download(data, this._downloadUrl, this._docFilename);
  },

  async save() {
    if (this._saveInProgress) {
      return;
    }
    this._saveInProgress = true;
    await this.pdfScriptingManager.dispatchWillSave();

    try {
      const data = await this.pdfDocument.saveDocument();
      this.downloadManager.download(data, this._downloadUrl, this._docFilename);
    } catch (reason) {
      // When the PDF document isn't ready, fallback to a "regular" download.
      console.error(`Error when saving the document:`, reason);
      await this.download();
    } finally {
      await this.pdfScriptingManager.dispatchDidSave();
      this._saveInProgress = false;
    }

    const editorStats = this.pdfDocument?.annotationStorage.editorStats;
    if (editorStats) {
      this.externalServices.reportTelemetry({
        type: "editing",
        data: {
          type: "save",
          stats: editorStats,
        },
      });
    }
  },

  async downloadOrSave() {
    // In the Firefox case, this method MUST always trigger a download.
    // When the user is closing a modified and unsaved document, we display a
    // prompt asking for saving or not. In case they save, we must wait for
    // saving to complete before closing the tab.
    // So in case this function does not trigger a download, we must trigger a
    // a message and change PdfjsChild.sys.mjs to take it into account.
    const { classList } = this.appConfig.appContainer;
    classList.add("wait");
    await (this.pdfDocument?.annotationStorage.size > 0
      ? this.save()
      : this.download());
    classList.remove("wait");
  },

  /**
   * Report the error; used for errors affecting loading and/or parsing of
   * the entire PDF document.
   */
  async _documentError(key, moreInfo = null) {
    this._unblockDocumentLoadEvent();

    const message = await this._otherError(
      key || "pdfjs-loading-error",
      moreInfo
    );

    this.eventBus.dispatch("documenterror", {
      source: this,
      message,
      reason: moreInfo?.message ?? null,
    });
  },

  /**
   * Report the error; used for errors affecting e.g. only a single page.
   * @param {string} key - The localization key for the error.
   * @param {Object} [moreInfo] - Further information about the error that is
   *                              more technical. Should have a 'message' and
   *                              optionally a 'stack' property.
   * @returns {string} A (localized) error message that is human readable.
   */
  async _otherError(key, moreInfo = null) {
    const message = await this.l10n.get(key);

    const moreInfoText = [`PDF.js v${version || "?"} (build: ${build || "?"})`];
    if (moreInfo) {
      moreInfoText.push(`Message: ${moreInfo.message}`);

      if (moreInfo.stack) {
        moreInfoText.push(`Stack: ${moreInfo.stack}`);
      } else {
        if (moreInfo.filename) {
          moreInfoText.push(`File: ${moreInfo.filename}`);
        }
        if (moreInfo.lineNumber) {
          moreInfoText.push(`Line: ${moreInfo.lineNumber}`);
        }
      }
    }

    console.error(`${message}\n\n${moreInfoText.join("\n")}`);
    return message;
  },

  progress(percent) {
    // When we transition from full request to range requests, it's possible
    // that we discard some of the loaded data. This can cause the loading
    // bar to move backwards. So prevent this by only updating the bar if it
    // increases.
    if (!this.loadingBar || percent <= this.loadingBar.percent) {
      return;
    }
    this.loadingBar.percent = percent;

    // When disableAutoFetch is enabled, it's not uncommon for the entire file
    // to never be fetched (depends on e.g. the file structure). In this case
    // the loading bar will not be completely filled, nor will it be hidden.
    // To prevent displaying a partially filled loading bar permanently, we
    // hide it when no data has been loaded during a certain amount of time.
    if (
      this.pdfDocument?.loadingParams.disableAutoFetch ??
      AppOptions.get("disableAutoFetch")
    ) {
      this.loadingBar.setDisableAutoFetch();
    }
  },

  load(pdfDocument) {
    this.pdfDocument = pdfDocument;

    this._printPermissionPromise = new Promise(resolve => {
      this.eventBus.on(
        "printingallowed",
        ({ isAllowed }) => {
          if (
            typeof PDFJSDev !== "undefined" &&
            PDFJSDev.test("MOZCENTRAL") &&
            !isAllowed
          ) {
            window.print = () => {
              console.warn("Printing is not allowed.");
            };
          }
          resolve(isAllowed);
        },
        { once: true }
      );
    });

    pdfDocument.getDownloadInfo().then(({ length }) => {
      this._contentLength = length; // Ensure that the correct length is used.
      this.loadingBar?.hide();

      firstPagePromise.then(() => {
        this.eventBus.dispatch("documentloaded", { source: this });
      });
    });

    // Since the `setInitialView` call below depends on this being resolved,
    // fetch it early to avoid delaying initial rendering of the PDF document.
    const pageLayoutPromise = pdfDocument.getPageLayout().catch(() => {
      /* Avoid breaking initial rendering; ignoring errors. */
    });
    const pageModePromise = pdfDocument.getPageMode().catch(() => {
      /* Avoid breaking initial rendering; ignoring errors. */
    });
    const openActionPromise = pdfDocument.getOpenAction().catch(() => {
      /* Avoid breaking initial rendering; ignoring errors. */
    });

    this.toolbar?.setPagesCount(pdfDocument.numPages, false);
    this.secondaryToolbar?.setPagesCount(pdfDocument.numPages);

    if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("CHROME")) {
      const baseUrl = updateUrlHash(location, "");
      // Ignore "data:"-URLs for performance reasons, even though it may cause
      // internal links to not work perfectly in all cases (see bug 1803050).
      this.pdfLinkService.setDocument(
        pdfDocument,
        isDataScheme(baseUrl) ? null : baseUrl
      );
    } else {
      this.pdfLinkService.setDocument(pdfDocument);
    }
    this.pdfDocumentProperties?.setDocument(pdfDocument);

    const pdfViewer = this.pdfViewer;
    pdfViewer.setDocument(pdfDocument);
    this.pdfTextExtractor.setViewer(pdfViewer);
    const { firstPagePromise, onePageRendered, pagesPromise } = pdfViewer;

    this.pdfThumbnailViewer?.setDocument(pdfDocument);

    const storedPromise = (this.store = new ViewHistory(
      pdfDocument.fingerprints[0]
    ))
      .getMultiple({
        page: null,
        zoom: DEFAULT_SCALE_VALUE,
        scrollLeft: "0",
        scrollTop: "0",
        rotation: null,
        sidebarView: SidebarView.UNKNOWN,
        scrollMode: ScrollMode.UNKNOWN,
        spreadMode: SpreadMode.UNKNOWN,
      })
      .catch(() => {
        /* Unable to read from storage; ignoring errors. */
      });

    firstPagePromise.then(pdfPage => {
      this.loadingBar?.setWidth(this.appConfig.viewerContainer);
      this._initializeAnnotationStorageCallbacks(pdfDocument);

      Promise.all([
        animationStarted,
        storedPromise,
        pageLayoutPromise,
        pageModePromise,
        openActionPromise,
      ])
        .then(async ([timeStamp, stored, pageLayout, pageMode, openAction]) => {
          const viewOnLoad = AppOptions.get("viewOnLoad");

          this._initializePdfHistory({
            fingerprint: pdfDocument.fingerprints[0],
            viewOnLoad,
            initialDest: openAction?.dest,
          });
          const initialBookmark = this.initialBookmark;

          // Initialize the default values, from user preferences.
          const zoom = AppOptions.get("defaultZoomValue");
          let hash = zoom ? `zoom=${zoom}` : null;

          let rotation = null;
          let sidebarView = AppOptions.get("sidebarViewOnLoad");
          let scrollMode = AppOptions.get("scrollModeOnLoad");
          let spreadMode = AppOptions.get("spreadModeOnLoad");

          if (stored?.page && viewOnLoad !== ViewOnLoad.INITIAL) {
            hash =
              `page=${stored.page}&zoom=${zoom || stored.zoom},` +
              `${stored.scrollLeft},${stored.scrollTop}`;

            rotation = parseInt(stored.rotation, 10);
            // Always let user preference take precedence over the view history.
            if (sidebarView === SidebarView.UNKNOWN) {
              sidebarView = stored.sidebarView | 0;
            }
            if (scrollMode === ScrollMode.UNKNOWN) {
              scrollMode = stored.scrollMode | 0;
            }
            if (spreadMode === SpreadMode.UNKNOWN) {
              spreadMode = stored.spreadMode | 0;
            }
          }
          // Always let the user preference/view history take precedence.
          if (pageMode && sidebarView === SidebarView.UNKNOWN) {
            sidebarView = apiPageModeToSidebarView(pageMode);
          }
          if (
            pageLayout &&
            scrollMode === ScrollMode.UNKNOWN &&
            spreadMode === SpreadMode.UNKNOWN
          ) {
            const modes = apiPageLayoutToViewerModes(pageLayout);
            // TODO: Try to improve page-switching when using the mouse-wheel
            // and/or arrow-keys before allowing the document to control this.
            // scrollMode = modes.scrollMode;
            spreadMode = modes.spreadMode;
          }

          this.setInitialView(hash, {
            rotation,
            sidebarView,
            scrollMode,
            spreadMode,
          });
          this.eventBus.dispatch("documentinit", { source: this });

          // For documents with different page sizes, once all pages are
          // resolved, ensure that the correct location becomes visible on load.
          // (To reduce the risk, in very large and/or slow loading documents,
          //  that the location changes *after* the user has started interacting
          //  with the viewer, wait for either `pagesPromise` or a timeout.)
          await Promise.race([
            pagesPromise,
            new Promise(resolve => {
              setTimeout(resolve, FORCE_PAGES_LOADED_TIMEOUT);
            }),
          ]);
          if (!initialBookmark && !hash) {
            return;
          }
          if (pdfViewer.hasEqualPageSizes) {
            return;
          }
          this.initialBookmark = initialBookmark;

          // eslint-disable-next-line no-self-assign
          pdfViewer.currentScaleValue = pdfViewer.currentScaleValue;
          // Re-apply the initial document location.
          this.setInitialView(hash);
        })
        .catch(() => {
          // Ensure that the document is always completely initialized,
          // even if there are any errors thrown above.
          this.setInitialView();
        })
        .then(function () {
          // At this point, rendering of the initial page(s) should always have
          // started (and may even have completed).
          // To prevent any future issues, e.g. the document being completely
          // blank on load, always trigger rendering here.
          pdfViewer.update();
        });
    });

    pagesPromise.then(
      () => {
        this._unblockDocumentLoadEvent();

        this._initializeAutoPrint(pdfDocument, openActionPromise);
      },
      reason => {
        this._documentError("pdfjs-loading-error", { message: reason.message });
      }
    );

    onePageRendered.then(data => {
      this.externalServices.reportTelemetry({
        type: "pageInfo",
        timestamp: data.timestamp,
      });

      if (this.pdfOutlineViewer) {
        pdfDocument.getOutline().then(outline => {
          if (pdfDocument !== this.pdfDocument) {
            return; // The document was closed while the outline resolved.
          }
          this.pdfOutlineViewer.render({ outline, pdfDocument });
        });
      }
      if (this.pdfAttachmentViewer) {
        pdfDocument.getAttachments().then(attachments => {
          if (pdfDocument !== this.pdfDocument) {
            return; // The document was closed while the attachments resolved.
          }
          this.pdfAttachmentViewer.render({ attachments });
        });
      }
      if (this.pdfLayerViewer) {
        // Ensure that the layers accurately reflects the current state in the
        // viewer itself, rather than the default state provided by the API.
        pdfViewer.optionalContentConfigPromise.then(optionalContentConfig => {
          if (pdfDocument !== this.pdfDocument) {
            return; // The document was closed while the layers resolved.
          }
          this.pdfLayerViewer.render({ optionalContentConfig, pdfDocument });
        });
      }
    });

    this._initializePageLabels(pdfDocument);
    this._initializeMetadata(pdfDocument);
  },

  /**
   * @private
   */
  async _scriptingDocProperties(pdfDocument) {
    if (!this.documentInfo) {
      // It should be *extremely* rare for metadata to not have been resolved
      // when this code runs, but ensure that we handle that case here.
      await new Promise(resolve => {
        this.eventBus._on("metadataloaded", resolve, { once: true });
      });
      if (pdfDocument !== this.pdfDocument) {
        return null; // The document was closed while the metadata resolved.
      }
    }
    if (!this._contentLength) {
      // Always waiting for the entire PDF document to be loaded will, most
      // likely, delay sandbox-creation too much in the general case for all
      // PDF documents which are not provided as binary data to the API.
      // Hence we'll simply have to trust that the `contentLength` (as provided
      // by the server), when it exists, is accurate enough here.
      await new Promise(resolve => {
        this.eventBus._on("documentloaded", resolve, { once: true });
      });
      if (pdfDocument !== this.pdfDocument) {
        return null; // The document was closed while the downloadInfo resolved.
      }
    }

    return {
      ...this.documentInfo,
      baseURL: this.baseUrl,
      filesize: this._contentLength,
      filename: this._docFilename,
      metadata: this.metadata?.getRaw(),
      authors: this.metadata?.get("dc:creator"),
      numPages: this.pagesCount,
      URL: this.url,
    };
  },

  /**
   * @private
   */
  async _initializeAutoPrint(pdfDocument, openActionPromise) {
    const [openAction, jsActions] = await Promise.all([
      openActionPromise,
      this.pdfViewer.enableScripting ? null : pdfDocument.getJSActions(),
    ]);

    if (pdfDocument !== this.pdfDocument) {
      return; // The document was closed while the auto print data resolved.
    }
    let triggerAutoPrint = openAction?.action === "Print";

    if (jsActions) {
      console.warn("Warning: JavaScript support is not enabled");

      // Hack to support auto printing.
      for (const name in jsActions) {
        if (triggerAutoPrint) {
          break;
        }
        switch (name) {
          case "WillClose":
          case "WillSave":
          case "DidSave":
          case "WillPrint":
          case "DidPrint":
            continue;
        }
        triggerAutoPrint = jsActions[name].some(js => AutoPrintRegExp.test(js));
      }
    }

    if (triggerAutoPrint) {
      this.triggerPrinting();
    }
  },

  /**
   * @private
   */
  async _initializeMetadata(pdfDocument) {
    const {
      info,
      metadata,
      contentDispositionFilename,
      contentLength,
      hasStructTree,
    } = await pdfDocument.getMetadata();

    if (pdfDocument !== this.pdfDocument) {
      return; // The document was closed while the metadata resolved.
    }
    this.externalServices.reportTelemetry({
      type: "taggedPDF",
      data: hasStructTree,
    });
    this.documentInfo = info;
    this.metadata = metadata;
    this._contentDispositionFilename ??= contentDispositionFilename;
    this._contentLength ??= contentLength; // See `getDownloadInfo`-call above.

    // Provides some basic debug information
    console.log(
      `PDF ${pdfDocument.fingerprints[0]} [${info.PDFFormatVersion} ` +
        `${(metadata?.get("pdf:producer") || info.Producer || "-").trim()} / ` +
        `${(metadata?.get("xmp:creatortool") || info.Creator || "-").trim()}` +
        `] (PDF.js: ${version || "?"} [${build || "?"}])`
    );
    const pdfTitle = this._docTitle;

    if (pdfTitle) {
      this.setTitle(
        `${pdfTitle} - ${this._contentDispositionFilename || this._title}`
      );
    } else if (this._contentDispositionFilename) {
      this.setTitle(this._contentDispositionFilename);
    }

    if (
      info.IsXFAPresent &&
      !info.IsAcroFormPresent &&
      !pdfDocument.isPureXfa
    ) {
      if (pdfDocument.loadingParams.enableXfa) {
        console.warn("Warning: XFA Foreground documents are not supported");
      } else {
        console.warn("Warning: XFA support is not enabled");
      }
    } else if (
      (info.IsAcroFormPresent || info.IsXFAPresent) &&
      !this.pdfViewer.renderForms
    ) {
      console.warn("Warning: Interactive form support is not enabled");
    }

    if (info.IsSignaturesPresent) {
      console.warn("Warning: Digital signatures validation is not supported");
    }

    this.eventBus.dispatch("metadataloaded", { source: this });
  },

  /**
   * @private
   */
  async _initializePageLabels(pdfDocument) {
    if (
      typeof PDFJSDev === "undefined"
        ? window.isGECKOVIEW
        : PDFJSDev.test("GECKOVIEW")
    ) {
      return;
    }
    const labels = await pdfDocument.getPageLabels();

    if (pdfDocument !== this.pdfDocument) {
      return; // The document was closed while the page labels resolved.
    }
    if (!labels || AppOptions.get("disablePageLabels")) {
      return;
    }
    const numLabels = labels.length;
    // Ignore page labels that correspond to standard page numbering,
    // or page labels that are all empty.
    let standardLabels = 0,
      emptyLabels = 0;
    for (let i = 0; i < numLabels; i++) {
      const label = labels[i];
      if (label === (i + 1).toString()) {
        standardLabels++;
      } else if (label === "") {
        emptyLabels++;
      } else {
        break;
      }
    }
    if (standardLabels >= numLabels || emptyLabels >= numLabels) {
      return;
    }
    const { pdfViewer, pdfThumbnailViewer, toolbar } = this;

    pdfViewer.setPageLabels(labels);
    pdfThumbnailViewer?.setPageLabels(labels);

    // Changing toolbar page display to use labels and we need to set
    // the label of the current page.
    toolbar?.setPagesCount(numLabels, true);
    toolbar?.setPageNumber(
      pdfViewer.currentPageNumber,
      pdfViewer.currentPageLabel
    );
  },

  /**
   * @private
   */
  _initializePdfHistory({ fingerprint, viewOnLoad, initialDest = null }) {
    if (!this.pdfHistory) {
      return;
    }
    this.pdfHistory.initialize({
      fingerprint,
      resetHistory: viewOnLoad === ViewOnLoad.INITIAL,
      updateUrl: AppOptions.get("historyUpdateUrl"),
    });

    if (this.pdfHistory.initialBookmark) {
      this.initialBookmark = this.pdfHistory.initialBookmark;

      this.initialRotation = this.pdfHistory.initialRotation;
    }

    // Always let the browser history/document hash take precedence.
    if (
      initialDest &&
      !this.initialBookmark &&
      viewOnLoad === ViewOnLoad.UNKNOWN
    ) {
      this.initialBookmark = JSON.stringify(initialDest);
      // TODO: Re-factor the `PDFHistory` initialization to remove this hack
      // that's currently necessary to prevent weird initial history state.
      this.pdfHistory.push({ explicitDest: initialDest, pageNumber: null });
    }
  },

  /**
   * @private
   */
  _initializeAnnotationStorageCallbacks(pdfDocument) {
    if (pdfDocument !== this.pdfDocument) {
      return;
    }
    const { annotationStorage } = pdfDocument;

    annotationStorage.onSetModified = () => {
      window.addEventListener("beforeunload", beforeUnload);

      if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
        this._annotationStorageModified = true;
      }
    };
    annotationStorage.onResetModified = () => {
      window.removeEventListener("beforeunload", beforeUnload);

      if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
        delete this._annotationStorageModified;
      }
    };
    annotationStorage.onAnnotationEditor = typeStr => {
      this._hasAnnotationEditors = !!typeStr;
      this.setTitle();
    };
  },

  setInitialView(
    storedHash,
    { rotation, sidebarView, scrollMode, spreadMode } = {}
  ) {
    const setRotation = angle => {
      if (isValidRotation(angle)) {
        this.pdfViewer.pagesRotation = angle;
      }
    };
    const setViewerModes = (scroll, spread) => {
      if (isValidScrollMode(scroll)) {
        this.pdfViewer.scrollMode = scroll;
      }
      if (isValidSpreadMode(spread)) {
        this.pdfViewer.spreadMode = spread;
      }
    };
    this.isInitialViewSet = true;
    this.viewsManager?.setInitialView(sidebarView);

    setViewerModes(scrollMode, spreadMode);

    if (this.initialBookmark) {
      setRotation(this.initialRotation);
      delete this.initialRotation;

      this.pdfLinkService.setHash(this.initialBookmark);
      this.initialBookmark = null;
    } else if (storedHash) {
      setRotation(rotation);

      this.pdfLinkService.setHash(storedHash);
    }

    // Ensure that the correct page number is displayed in the UI,
    // even if the active page didn't change during document load.
    this.toolbar?.setPageNumber(
      this.pdfViewer.currentPageNumber,
      this.pdfViewer.currentPageLabel
    );
    this.secondaryToolbar?.setPageNumber(this.pdfViewer.currentPageNumber);

    if (!this.pdfViewer.currentScaleValue) {
      // Scale was not initialized: invalid bookmark or scale was not specified.
      // Setting the default one.
      this.pdfViewer.currentScaleValue = DEFAULT_SCALE_VALUE;
    }
  },

  /**
   * @private
   */
  _cleanup() {
    if (!this.pdfDocument) {
      return; // run cleanup when document is loaded
    }
    this.pdfViewer.cleanup();
    this.pdfThumbnailViewer?.cleanup();

    this.pdfDocument.cleanup(
      /* keepLoadedFonts = */ AppOptions.get("fontExtraProperties")
    );
  },

  forceRendering() {
    this.pdfRenderingQueue.printing = !!this.printService;
    this.pdfRenderingQueue.isThumbnailViewEnabled =
      this.viewsManager?.visibleView === SidebarView.THUMBS;
    this.pdfRenderingQueue.renderHighestPriority();
  },

  beforePrint() {
    this._printAnnotationStoragePromise = this.pdfScriptingManager
      .dispatchWillPrint()
      .catch(() => {
        /* Avoid breaking printing; ignoring errors. */
      })
      .then(() => this.pdfDocument?.annotationStorage.print);

    if (this.printService) {
      // There is no way to suppress beforePrint/afterPrint events,
      // but PDFPrintService may generate double events -- this will ignore
      // the second event that will be coming from native window.print().
      return;
    }

    if (!this.supportsPrinting || !this.pdfViewer.printingAllowed) {
      this._otherError("pdfjs-printing-not-supported");
      return;
    }

    // The beforePrint is a sync method and we need to know layout before
    // returning from this method. Ensure that we can get sizes of the pages.
    if (!this.pdfViewer.pageViewsReady) {
      this.l10n.get("pdfjs-printing-not-ready").then(msg => {
        // eslint-disable-next-line no-alert
        window.alert(msg);
      });
      return;
    }

    this.printService = PDFPrintServiceFactory.createPrintService({
      pdfDocument: this.pdfDocument,
      pagesOverview: this.pdfViewer.getPagesOverview(),
      printContainer: this.appConfig.printContainer,
      printResolution: AppOptions.get("printResolution"),
      printAnnotationStoragePromise: this._printAnnotationStoragePromise,
    });
    this.forceRendering();
    // Disable the editor-indicator during printing (fixes bug 1790552).
    this.setTitle();

    this.printService.layout();

    if (this._hasAnnotationEditors) {
      this.externalServices.reportTelemetry({
        type: "editing",
        data: {
          type: "print",
          stats: this.pdfDocument?.annotationStorage.editorStats,
        },
      });
    }
  },

  afterPrint() {
    if (this._printAnnotationStoragePromise) {
      this._printAnnotationStoragePromise.then(() => {
        this.pdfScriptingManager.dispatchDidPrint();
      });
      this._printAnnotationStoragePromise = null;
    }

    if (this.printService) {
      this.printService.destroy();
      this.printService = null;

      this.pdfDocument?.annotationStorage.resetModified();
    }
    this.forceRendering();
    // Re-enable the editor-indicator after printing (fixes bug 1790552).
    this.setTitle();
  },

  rotatePages(delta) {
    this.pdfViewer.pagesRotation += delta;
    // Note that the thumbnail viewer is updated, and rendering is triggered,
    // in the 'rotationchanging' event handler.
  },

  requestPresentationMode() {
    this.pdfPresentationMode?.request();
  },

  async triggerPrinting() {
    if (this.supportsPrinting && (await this._printPermissionPromise)) {
      window.print();
    }
  },

  bindEvents() {
    if (this._eventBusAbortController) {
      return;
    }
    const ac = (this._eventBusAbortController = new AbortController());
    const opts = { signal: ac.signal };

    const {
      eventBus,
      externalServices,
      pdfDocumentProperties,
      pdfViewer,
      preferences,
    } = this;

    eventBus._on("resize", onResize.bind(this), opts);
    eventBus._on("hashchange", onHashchange.bind(this), opts);
    eventBus._on("beforeprint", this.beforePrint.bind(this), opts);
    eventBus._on("afterprint", this.afterPrint.bind(this), opts);
    eventBus._on("pagerender", onPageRender.bind(this), opts);
    eventBus._on("pagerendered", onPageRendered.bind(this), opts);
    eventBus._on("updateviewarea", onUpdateViewarea.bind(this), opts);
    eventBus._on("pagechanging", onPageChanging.bind(this), opts);
    eventBus._on("scalechanging", onScaleChanging.bind(this), opts);
    eventBus._on("rotationchanging", onRotationChanging.bind(this), opts);
    eventBus._on("sidebarviewchanged", onSidebarViewChanged.bind(this), opts);
    eventBus._on("pagemode", onPageMode.bind(this), opts);
    eventBus._on("namedaction", onNamedAction.bind(this), opts);
    eventBus._on(
      "presentationmodechanged",
      evt => (pdfViewer.presentationModeState = evt.state),
      opts
    );
    eventBus._on(
      "presentationmode",
      this.requestPresentationMode.bind(this),
      opts
    );
    eventBus._on(
      "switchannotationeditormode",
      evt => (pdfViewer.annotationEditorMode = evt),
      opts
    );
    eventBus._on("print", this.triggerPrinting.bind(this), opts);
    eventBus._on("download", this.downloadOrSave.bind(this), opts);
    eventBus._on("firstpage", () => (this.page = 1), opts);
    eventBus._on("lastpage", () => (this.page = this.pagesCount), opts);
    eventBus._on("nextpage", () => pdfViewer.nextPage(), opts);
    eventBus._on("previouspage", () => pdfViewer.previousPage(), opts);
    eventBus._on("zoomin", this.zoomIn.bind(this), opts);
    eventBus._on("zoomout", this.zoomOut.bind(this), opts);
    eventBus._on("zoomreset", this.zoomReset.bind(this), opts);
    eventBus._on("pagenumberchanged", onPageNumberChanged.bind(this), opts);
    eventBus._on(
      "scalechanged",
      evt => (pdfViewer.currentScaleValue = evt.value),
      opts
    );
    eventBus._on("rotatecw", this.rotatePages.bind(this, 90), opts);
    eventBus._on("rotateccw", this.rotatePages.bind(this, -90), opts);
    eventBus._on(
      "optionalcontentconfig",
      evt => (pdfViewer.optionalContentConfigPromise = evt.promise),
      opts
    );
    eventBus._on(
      "switchscrollmode",
      evt => (pdfViewer.scrollMode = evt.mode),
      opts
    );
    eventBus._on(
      "scrollmodechanged",
      onViewerModesChanged.bind(this, "scrollMode"),
      opts
    );
    eventBus._on(
      "switchspreadmode",
      evt => (pdfViewer.spreadMode = evt.mode),
      opts
    );
    eventBus._on(
      "spreadmodechanged",
      onViewerModesChanged.bind(this, "spreadMode"),
      opts
    );
    eventBus._on(
      "imagealttextsettings",
      onImageAltTextSettings.bind(this),
      opts
    );
    eventBus._on(
      "documentproperties",
      () => pdfDocumentProperties?.open(),
      opts
    );
    eventBus._on("findfromurlhash", onFindFromUrlHash.bind(this), opts);
    eventBus._on(
      "updatefindmatchescount",
      onUpdateFindMatchesCount.bind(this),
      opts
    );
    eventBus._on(
      "updatefindcontrolstate",
      onUpdateFindControlState.bind(this),
      opts
    );

    if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
      eventBus._on("fileinputchange", onFileInputChange.bind(this), opts);
      eventBus._on("openfile", onOpenFile.bind(this), opts);
    }
    if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
      eventBus._on(
        "annotationeditorstateschanged",
        evt => externalServices.updateEditorStates(evt),
        opts
      );
      eventBus._on(
        "reporttelemetry",
        evt => externalServices.reportTelemetry(evt.details),
        opts
      );
    }
    if (
      typeof PDFJSDev === "undefined" ||
      PDFJSDev.test("TESTING || MOZCENTRAL")
    ) {
      eventBus._on(
        "setpreference",
        evt => preferences.set(evt.name, evt.value),
        opts
      );
    }
    eventBus._on("pagesedited", this.onPagesEdited.bind(this), opts);
    eventBus._on(
      "savepageseditedpdf",
      this.onSavePagesEditedPDF.bind(this),
      opts
    );
  },

  bindWindowEvents() {
    if (this._windowAbortController) {
      return;
    }
    this._windowAbortController = new AbortController();

    const {
      eventBus,
      appConfig: { mainContainer },
      pdfViewer,
      _windowAbortController: { signal },
    } = this;

    this._touchManager = new TouchManager({
      container: window,
      isPinchingDisabled: () => pdfViewer.isInPresentationMode,
      isPinchingStopped: () => this.overlayManager?.active,
      onPinching: this.touchPinchCallback.bind(this),
      onPinchEnd: this.touchPinchEndCallback.bind(this),
      signal,
    });

    function addWindowResolutionChange(evt = null) {
      if (evt) {
        pdfViewer.refresh();
      }
      const mediaQueryList = window.matchMedia(
        `(resolution: ${OutputScale.pixelRatio}dppx)`
      );
      mediaQueryList.addEventListener("change", addWindowResolutionChange, {
        once: true,
        signal,
      });
    }
    addWindowResolutionChange();

    window.addEventListener("wheel", onWheel.bind(this), {
      passive: false,
      signal,
    });
    window.addEventListener("click", onClick.bind(this), { signal });
    window.addEventListener("keydown", onKeyDown.bind(this), { signal });
    window.addEventListener("keyup", onKeyUp.bind(this), { signal });
    window.addEventListener(
      "resize",
      () => eventBus.dispatch("resize", { source: window }),
      { signal }
    );
    window.addEventListener(
      "hashchange",
      () => {
        eventBus.dispatch("hashchange", {
          source: window,
          hash: document.location.hash.substring(1),
        });
      },
      { signal }
    );
    window.addEventListener(
      "beforeprint",
      () => eventBus.dispatch("beforeprint", { source: window }),
      { signal }
    );
    window.addEventListener(
      "afterprint",
      () => eventBus.dispatch("afterprint", { source: window }),
      { signal }
    );
    window.addEventListener(
      "updatefromsandbox",
      evt => {
        eventBus.dispatch("updatefromsandbox", {
          source: window,
          detail: evt.detail,
        });
      },
      { signal }
    );

    if (
      (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) &&
      !("onscrollend" in document.documentElement)
    ) {
      return;
    }
    if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
      // Using the values lastScrollTop and lastScrollLeft is a workaround to
      // https://bugzilla.mozilla.org/show_bug.cgi?id=1881974.
      // TODO: remove them once the bug is fixed.
      ({ scrollTop: this._lastScrollTop, scrollLeft: this._lastScrollLeft } =
        mainContainer);
    }

    let scrollendTimeoutID, scrollAbortController;
    const scrollend = () => {
      if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
        ({ scrollTop: this._lastScrollTop, scrollLeft: this._lastScrollLeft } =
          mainContainer);
      }
      clearTimeout(scrollendTimeoutID);
      if (this._isScrolling) {
        scrollAbortController.abort();
        scrollAbortController = null;
        this._isScrolling = false;
      }
    };
    const scroll = () => {
      if (this._isCtrlKeyDown) {
        return;
      }
      if (
        (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) &&
        this._lastScrollTop === mainContainer.scrollTop &&
        this._lastScrollLeft === mainContainer.scrollLeft
      ) {
        return;
      }

      if (!this._isScrolling) {
        scrollAbortController = new AbortController();
        const abortSignal = AbortSignal.any([
          scrollAbortController.signal,
          signal,
        ]);

        mainContainer.addEventListener("scrollend", scrollend, {
          signal: abortSignal,
        });
        mainContainer.addEventListener("blur", scrollend, {
          signal: abortSignal,
        });
        this._isScrolling = true;
      }
      clearTimeout(scrollendTimeoutID);
      // Why 100 ? Because of:
      // https://developer.chrome.com/blog/scrollend-a-new-javascript-event
      // Maybe we could find a better value... ideally the `scrollend` event
      // should be correctly fired.
      scrollendTimeoutID = setTimeout(scrollend, 100);
    };
    mainContainer.addEventListener("scroll", scroll, {
      passive: true,
      signal,
    });
  },

  unbindEvents() {
    this._eventBusAbortController?.abort();
    this._eventBusAbortController = null;
  },

  unbindWindowEvents() {
    this._windowAbortController?.abort();
    this._windowAbortController = null;
    this._touchManager = null;
  },

  /**
   * @ignore
   */
  async testingClose() {
    this.unbindEvents();
    this.unbindWindowEvents();

    this._globalAbortController?.abort();
    this._globalAbortController = null;

    this.findBar?.close();

    await Promise.all([this.l10n?.destroy(), this.close()]);
  },

  onPagesEdited(data) {
    this.pdfViewer.onPagesEdited(data);
  },

  async onSavePagesEditedPDF({ data: extractParams }) {
    if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
      return;
    }
    if (!this.pdfDocument) {
      return;
    }
    const modifiedPdfBytes = await this.pdfDocument.extractPages(extractParams);
    if (!modifiedPdfBytes) {
      console.error(
        "Something wrong happened when saving the edited PDF.\nPlease file a bug."
      );
      return;
    }
    this.downloadManager.download(
      modifiedPdfBytes,
      this._downloadUrl,
      this._docFilename
    );
  },

  _accumulateTicks(ticks, prop) {
    // If the direction changed, reset the accumulated ticks.
    if ((this[prop] > 0 && ticks < 0) || (this[prop] < 0 && ticks > 0)) {
      this[prop] = 0;
    }
    this[prop] += ticks;
    const wholeTicks = Math.trunc(this[prop]);
    this[prop] -= wholeTicks;
    return wholeTicks;
  },

  _accumulateFactor(previousScale, factor, prop) {
    if (factor === 1) {
      return 1;
    }
    // If the direction changed, reset the accumulated factor.
    if ((this[prop] > 1 && factor < 1) || (this[prop] < 1 && factor > 1)) {
      this[prop] = 1;
    }

    const newFactor =
      Math.floor(previousScale * factor * this[prop] * 100) /
      (100 * previousScale);
    this[prop] = factor / newFactor;

    return newFactor;
  },

  /**
   * Should be called *after* all pages have loaded, or if an error occurred,
   * to unblock the "load" event; see https://bugzilla.mozilla.org/show_bug.cgi?id=1618553
   * @private
   */
  _unblockDocumentLoadEvent() {
    document.blockUnblockOnload?.(false);

    // Ensure that this method is only ever run once.
    this._unblockDocumentLoadEvent = () => {};
  },

  /**
   * Used together with the integration-tests, to enable awaiting full
   * initialization of the scripting/sandbox.
   */
  get scriptingReady() {
    return this.pdfScriptingManager.ready;
  },
};

initCom(PDFViewerApplication);
PDFPrintServiceFactory.initGlobals(PDFViewerApplication);

if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
  const HOSTED_VIEWER_ORIGINS = new Set([
    "null",
    "http://mozilla.github.io",
    "https://mozilla.github.io",
  ]);
  // eslint-disable-next-line no-var
  var validateFileURL = function (file) {
    if (!file) {
      return;
    }
    const viewerOrigin = URL.parse(window.location)?.origin || "null";
    if (HOSTED_VIEWER_ORIGINS.has(viewerOrigin)) {
      // Hosted or local viewer, allow for any file locations
      return;
    }
    const fileOrigin = URL.parse(file, window.location)?.origin;
    if (fileOrigin === viewerOrigin) {
      return;
    }
    const ex = new Error("file origin does not match viewer's");

    PDFViewerApplication._documentError("pdfjs-loading-error", {
      message: ex.message,
    });
    // Removing of the following line will not guarantee that the viewer will
    // start accepting URLs from foreign origin -- CORS headers on the remote
    // server must be properly configured.
    throw ex;
  };

  // eslint-disable-next-line no-var
  var onFileInputChange = function (evt) {
    if (this.pdfViewer?.isInPresentationMode) {
      return; // Opening a new PDF file isn't supported in Presentation Mode.
    }
    const file = evt.fileInput.files[0];

    this.open({
      url: URL.createObjectURL(file),
      originalUrl: encodeURIComponent(file.name),
    });
  };

  // eslint-disable-next-line no-var
  var onOpenFile = function (evt) {
    this._openFileInput?.click();
  };
}

function onPageRender({ pageNumber }) {
  // If the page is (the most) visible when it starts rendering,
  // ensure that the page number input loading indicator is displayed.
  if (pageNumber === this.page) {
    this.toolbar?.updateLoadingIndicatorState(true);
  }
}

function onPageRendered({ pageNumber, isDetailView, error }) {
  // If the page is still visible when it has finished rendering,
  // ensure that the page number input loading indicator is hidden.
  if (pageNumber === this.page) {
    this.toolbar?.updateLoadingIndicatorState(false);
  }

  // Use the rendered page to set the corresponding thumbnail image.
  if (!isDetailView && this.viewsManager?.visibleView === SidebarView.THUMBS) {
    const pageView = this.pdfViewer.getPageView(/* index = */ pageNumber - 1);
    const thumbnailView = this.pdfThumbnailViewer?.getThumbnail(
      /* index = */ pageNumber - 1
    );
    if (pageView) {
      thumbnailView?.setImage(pageView);
    }
  }

  if (error) {
    this._otherError("pdfjs-rendering-error", error);
  }
}

function onPageMode({ mode }) {
  // Handle the 'pagemode' hash parameter, see also `PDFLinkService_setHash`.
  let view;
  switch (mode) {
    case "thumbs":
      view = SidebarView.THUMBS;
      break;
    case "bookmarks":
    case "outline": // non-standard
      view = SidebarView.OUTLINE;
      break;
    case "attachments": // non-standard
      view = SidebarView.ATTACHMENTS;
      break;
    case "layers": // non-standard
      view = SidebarView.LAYERS;
      break;
    case "none":
      view = SidebarView.NONE;
      break;
    default:
      console.error('Invalid "pagemode" hash parameter: ' + mode);
      return;
  }
  this.viewsManager?.switchView(view, /* forceOpen = */ true);
}

function onNamedAction(evt) {
  // Processing a couple of named actions that might be useful, see also
  // `PDFLinkService.executeNamedAction`.
  switch (evt.action) {
    case "GoToPage":
      this.appConfig.toolbar?.pageNumber.select();
      break;

    case "Find":
      if (!this.supportsIntegratedFind) {
        this.findBar?.toggle();
      }
      break;

    case "Print":
      this.triggerPrinting();
      break;

    case "SaveAs":
      this.downloadOrSave();
      break;
  }
}

function onSidebarViewChanged({ view }) {
  this.pdfRenderingQueue.isThumbnailViewEnabled = view === SidebarView.THUMBS;

  if (this.isInitialViewSet) {
    // Only update the storage when the document has been loaded *and* rendered.
    this.store?.set("sidebarView", view).catch(() => {
      // Unable to write to storage.
    });
  }
}

function onUpdateViewarea({ location }) {
  if (this.isInitialViewSet) {
    // Only update the storage when the document has been loaded *and* rendered.
    this.store
      ?.setMultiple({
        page: location.pageNumber,
        zoom: location.scale,
        scrollLeft: location.left,
        scrollTop: location.top,
        rotation: location.rotation,
      })
      .catch(() => {
        // Unable to write to storage.
      });
  }
  if (this.appConfig.secondaryToolbar) {
    this.appConfig.secondaryToolbar.viewBookmarkButton.href =
      this.pdfLinkService.getAnchorUrl(location.pdfOpenParams);
  }
}

function onViewerModesChanged(name, evt) {
  if (this.isInitialViewSet && !this.pdfViewer.isInPresentationMode) {
    // Only update the storage when the document has been loaded *and* rendered.
    this.store?.set(name, evt.mode).catch(() => {
      // Unable to write to storage.
    });
  }
}

function onResize() {
  const { pdfDocument, pdfViewer, pdfRenderingQueue } = this;

  if (pdfRenderingQueue.printing && window.matchMedia("print").matches) {
    // Work-around issue 15324 by ignoring "resize" events during printing.
    return;
  }

  if (!pdfDocument) {
    return;
  }
  const currentScaleValue = pdfViewer.currentScaleValue;
  if (
    currentScaleValue === "auto" ||
    currentScaleValue === "page-fit" ||
    currentScaleValue === "page-width"
  ) {
    // Note: the scale is constant for 'page-actual'.
    pdfViewer.currentScaleValue = currentScaleValue;
  }
  pdfViewer.update();
}

function onHashchange(evt) {
  const hash = evt.hash;
  if (!hash) {
    return;
  }
  if (!this.isInitialViewSet) {
    this.initialBookmark = hash;
  } else if (!this.pdfHistory?.popStateInProgress) {
    this.pdfLinkService.setHash(hash);
  }
}

function onPageNumberChanged(evt) {
  const { pdfViewer } = this;
  // Note that for `<input type="number">` HTML elements, an empty string will
  // be returned for non-number inputs; hence we simply do nothing in that case.
  if (evt.value !== "") {
    this.pdfLinkService.goToPage(evt.value);
  }

  // Ensure that the page number input displays the correct value, even if the
  // value entered by the user was invalid (e.g. a floating point number).
  if (
    evt.value !== pdfViewer.currentPageNumber.toString() &&
    evt.value !== pdfViewer.currentPageLabel
  ) {
    this.toolbar?.setPageNumber(
      pdfViewer.currentPageNumber,
      pdfViewer.currentPageLabel
    );
  }
}

function onImageAltTextSettings() {
  this.imageAltTextSettings?.open({
    enableGuessAltText: AppOptions.get("enableGuessAltText"),
    enableNewAltTextWhenAddingImage: AppOptions.get(
      "enableNewAltTextWhenAddingImage"
    ),
  });
}

function onFindFromUrlHash(evt) {
  this.eventBus.dispatch("find", {
    source: evt.source,
    type: "",
    query: evt.query,
    caseSensitive: false,
    entireWord: false,
    highlightAll: true,
    findPrevious: false,
    matchDiacritics: true,
  });
}

function onUpdateFindMatchesCount({ matchesCount }) {
  if (this.supportsIntegratedFind) {
    this.externalServices.updateFindMatchesCount(matchesCount);
  } else {
    this.findBar?.updateResultsCount(matchesCount);
  }
}

function onUpdateFindControlState({
  state,
  previous,
  entireWord,
  matchesCount,
  rawQuery,
}) {
  if (this.supportsIntegratedFind) {
    this.externalServices.updateFindControlState({
      result: state,
      findPrevious: previous,
      entireWord,
      matchesCount,
      rawQuery,
    });
  } else {
    this.findBar?.updateUIState(state, previous, matchesCount);
  }
}

function onScaleChanging(evt) {
  this.toolbar?.setPageScale(evt.presetValue, evt.scale);

  this.pdfViewer.update();
}

function onRotationChanging(evt) {
  if (this.pdfThumbnailViewer) {
    this.pdfThumbnailViewer.pagesRotation = evt.pagesRotation;
  }

  this.forceRendering();
  // Ensure that the active page doesn't change during rotation.
  this.pdfViewer.currentPageNumber = evt.pageNumber;
}

function onPageChanging({ pageNumber, pageLabel }) {
  this.toolbar?.setPageNumber(pageNumber, pageLabel);
  this.secondaryToolbar?.setPageNumber(pageNumber);

  if (this.viewsManager?.visibleView === SidebarView.THUMBS) {
    this.pdfThumbnailViewer?.scrollThumbnailIntoView(pageNumber);
  }

  // Show/hide the loading indicator in the page number input element.
  const currentPage = this.pdfViewer.getPageView(/* index = */ pageNumber - 1);
  this.toolbar?.updateLoadingIndicatorState(
    currentPage?.renderingState === RenderingStates.RUNNING
  );
}

function onWheel(evt) {
  const {
    pdfViewer,
    supportsMouseWheelZoomCtrlKey,
    supportsMouseWheelZoomMetaKey,
    supportsPinchToZoom,
  } = this;

  if (pdfViewer.isInPresentationMode) {
    return;
  }

  // Pinch-to-zoom on a trackpad maps to a wheel event with ctrlKey set to true
  // https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent#browser_compatibility
  // Hence if ctrlKey is true but ctrl key hasn't been pressed then we can
  // infer that we have a pinch-to-zoom.
  // But the ctrlKey could have been pressed outside of the browser window,
  // hence we try to do some magic to guess if the scaleFactor is likely coming
  // from a pinch-to-zoom or not.

  // It is important that we query deltaMode before delta{X,Y}, so that
  // Firefox doesn't switch to DOM_DELTA_PIXEL mode for compat with other
  // browsers, see https://bugzilla.mozilla.org/show_bug.cgi?id=1392460.
  const deltaMode = evt.deltaMode;

  // The following formula is a bit strange but it comes from:
  // https://searchfox.org/mozilla-central/rev/d62c4c4d5547064487006a1506287da394b64724/widget/InputData.cpp#618-626
  let scaleFactor = Math.exp(-evt.deltaY / 100);

  const isBuiltInMac =
    typeof PDFJSDev !== "undefined" &&
    PDFJSDev.test("MOZCENTRAL") &&
    FeatureTest.platform.isMac;
  const isPinchToZoom =
    evt.ctrlKey &&
    !this._isCtrlKeyDown &&
    deltaMode === WheelEvent.DOM_DELTA_PIXEL &&
    evt.deltaX === 0 &&
    (Math.abs(scaleFactor - 1) < 0.05 || isBuiltInMac) &&
    evt.deltaZ === 0;
  const origin = [evt.clientX, evt.clientY];

  if (
    isPinchToZoom ||
    (evt.ctrlKey && supportsMouseWheelZoomCtrlKey) ||
    (evt.metaKey && supportsMouseWheelZoomMetaKey)
  ) {
    // Only zoom the pages, not the entire viewer.
    evt.preventDefault();
    // NOTE: this check must be placed *after* preventDefault.
    if (
      this._isScrolling ||
      document.visibilityState === "hidden" ||
      this.overlayManager.active
    ) {
      return;
    }

    if (isPinchToZoom && supportsPinchToZoom) {
      scaleFactor = this._accumulateFactor(
        pdfViewer.currentScale,
        scaleFactor,
        "_wheelUnusedFactor"
      );
      this.updateZoom(null, scaleFactor, origin);
    } else {
      const delta = normalizeWheelEventDirection(evt);

      let ticks = 0;
      if (
        deltaMode === WheelEvent.DOM_DELTA_LINE ||
        deltaMode === WheelEvent.DOM_DELTA_PAGE
      ) {
        // For line-based devices, use one tick per event, because different
        // OSs have different defaults for the number lines. But we generally
        // want one "clicky" roll of the wheel (which produces one event) to
        // adjust the zoom by one step.
        //
        // If we're getting fractional lines (I can't think of a scenario
        // this might actually happen), be safe and use the accumulator.
        ticks =
          Math.abs(delta) >= 1
            ? Math.sign(delta)
            : this._accumulateTicks(delta, "_wheelUnusedTicks");
      } else {
        // pixel-based devices
        const PIXELS_PER_LINE_SCALE = 30;
        ticks = this._accumulateTicks(
          delta / PIXELS_PER_LINE_SCALE,
          "_wheelUnusedTicks"
        );
      }

      this.updateZoom(ticks, null, origin);
    }
  }
}

function closeSecondaryToolbar({ target }) {
  if (!this.secondaryToolbar?.isOpen) {
    return;
  }
  const { toolbar, secondaryToolbar } = this.appConfig;
  if (
    this.pdfViewer.containsElement(target) ||
    (toolbar?.container.contains(target) &&
      !secondaryToolbar?.toolbar.contains(target) &&
      // TODO: change the `contains` for an equality check when the bug:
      //  https://bugzilla.mozilla.org/show_bug.cgi?id=1921984
      // is fixed.
      !secondaryToolbar?.toggleButton.contains(target))
  ) {
    this.secondaryToolbar.close();
  }
}

function closeEditorUndoBar(evt) {
  if (!this.editorUndoBar?.isOpen) {
    return;
  }
  if (this.appConfig.secondaryToolbar?.toolbar.contains(evt.target)) {
    this.editorUndoBar.hide();
  }
}

function onClick(evt) {
  closeSecondaryToolbar.call(this, evt);
  closeEditorUndoBar.call(this, evt);
}

function onKeyUp(evt) {
  // evt.ctrlKey is false hence we use evt.key.
  if (evt.key === "Control") {
    this._isCtrlKeyDown = false;
  }
}

function onKeyDown(evt) {
  this._isCtrlKeyDown = evt.key === "Control";

  if (
    this.editorUndoBar?.isOpen &&
    evt.keyCode !== 9 &&
    evt.keyCode !== 16 &&
    !(
      (evt.keyCode === 13 || evt.keyCode === 32) &&
      getActiveOrFocusedElement() === this.appConfig.editorUndoBar.undoButton
    )
  ) {
    // Hide undo bar on keypress except for Shift, Tab, Shift+Tab.
    // Also avoid hiding if the undo button is triggered.
    this.editorUndoBar.hide();
  }

  if (this.overlayManager.active) {
    return;
  }
  const { eventBus, pdfViewer } = this;
  const isViewerInPresentationMode = pdfViewer.isInPresentationMode;

  let handled = false,
    ensureViewerFocused = false;
  const cmd =
    (evt.ctrlKey ? 1 : 0) |
    (evt.altKey ? 2 : 0) |
    (evt.shiftKey ? 4 : 0) |
    (evt.metaKey ? 8 : 0);

  // First, handle the key bindings that are independent whether an input
  // control is selected or not.
  if (cmd === 1 || cmd === 8 || cmd === 5 || cmd === 12) {
    // either CTRL or META key with optional SHIFT.
    switch (evt.keyCode) {
      case 70: // f
        if (!this.supportsIntegratedFind && !evt.shiftKey) {
          this.findBar?.open();
          handled = true;
        }
        break;
      case 71: // g
        if (!this.supportsIntegratedFind) {
          const { state } = this.findController;
          if (state) {
            const newState = {
              source: window,
              type: "again",
              findPrevious: cmd === 5 || cmd === 12,
            };
            eventBus.dispatch("find", { ...state, ...newState });
          }
          handled = true;
        }
        break;
      case 61: // FF/Mac '='
      case 107: // FF '+' and '='
      case 187: // Chrome '+'
      case 171: // FF with German keyboard
        this.zoomIn();
        handled = true;
        break;
      case 173: // FF/Mac '-'
      case 109: // FF '-'
      case 189: // Chrome '-'
        this.zoomOut();
        handled = true;
        break;
      case 48: // '0'
      case 96: // '0' on Numpad of Swedish keyboard
        if (!isViewerInPresentationMode) {
          // keeping it unhandled (to restore page zoom to 100%)
          setTimeout(() => {
            // ... and resetting the scale after browser adjusts its scale
            this.zoomReset();
          });
          handled = false;
        }
        break;

      case 38: // up arrow
        if (isViewerInPresentationMode || this.page > 1) {
          this.page = 1;
          handled = true;
          ensureViewerFocused = true;
        }
        break;
      case 40: // down arrow
        if (isViewerInPresentationMode || this.page < this.pagesCount) {
          this.page = this.pagesCount;
          handled = true;
          ensureViewerFocused = true;
        }
        break;
    }
  }

  if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC || CHROME")) {
    // CTRL or META without shift
    if (cmd === 1 || cmd === 8) {
      switch (evt.keyCode) {
        case 83: // s
          eventBus.dispatch("download", { source: window });
          handled = true;
          break;

        case 79: // o
          if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
            eventBus.dispatch("openfile", { source: window });
            handled = true;
          }
          break;
      }
    }
  }

  // CTRL+ALT or Option+Command
  if (cmd === 3 || cmd === 10) {
    switch (evt.keyCode) {
      case 80: // p
        this.requestPresentationMode();
        handled = true;
        this.externalServices.reportTelemetry({
          type: "buttons",
          data: { id: "presentationModeKeyboard" },
        });
        break;
      case 71: // g
        // focuses input#pageNumber field
        if (this.appConfig.toolbar) {
          this.appConfig.toolbar.pageNumber.select();
          handled = true;
        }
        break;
    }
  }

  if (handled) {
    if (ensureViewerFocused && !isViewerInPresentationMode) {
      pdfViewer.focus();
    }
    evt.preventDefault();
    return;
  }

  // Some shortcuts should not get handled if a control/input element
  // is selected.
  const curElement = getActiveOrFocusedElement();
  const curElementTagName = curElement?.tagName.toUpperCase();
  if (
    curElementTagName === "INPUT" ||
    curElementTagName === "TEXTAREA" ||
    curElementTagName === "SELECT" ||
    (curElementTagName === "BUTTON" && evt.keyCode === /* Space = */ 32) ||
    curElement?.isContentEditable
  ) {
    // Make sure that the secondary toolbar is closed when Escape is pressed.
    if (evt.keyCode !== /* Esc = */ 27) {
      return;
    }
  }

  // No control key pressed at all.
  if (cmd === 0) {
    let turnPage = 0,
      turnOnlyIfPageFit = false;
    switch (evt.keyCode) {
      case 38: // up arrow
        if (this.supportsCaretBrowsingMode) {
          this.moveCaret(/* isUp = */ true, /* select = */ false);
          handled = true;
          break;
        }
      /* falls through */
      case 33: // pg up
        // vertical scrolling using arrow/pg keys
        if (pdfViewer.isVerticalScrollbarEnabled) {
          turnOnlyIfPageFit = true;
        }
        turnPage = -1;
        break;
      case 8: // backspace
        if (!isViewerInPresentationMode) {
          turnOnlyIfPageFit = true;
        }
        turnPage = -1;
        break;
      case 37: // left arrow
        if (this.supportsCaretBrowsingMode) {
          return;
        }
        // horizontal scrolling using arrow keys
        if (pdfViewer.isHorizontalScrollbarEnabled) {
          turnOnlyIfPageFit = true;
        }
      /* falls through */
      case 75: // 'k'
      case 80: // 'p'
        turnPage = -1;
        break;
      case 27: // esc key
        if (this.secondaryToolbar?.isOpen) {
          this.secondaryToolbar.close();
          handled = true;
        }
        if (!this.supportsIntegratedFind && this.findBar?.opened) {
          this.findBar.close();
          handled = true;
        }
        break;
      case 40: // down arrow
        if (this.supportsCaretBrowsingMode) {
          this.moveCaret(/* isUp = */ false, /* select = */ false);
          handled = true;
          break;
        }
      /* falls through */
      case 34: // pg down
        // vertical scrolling using arrow/pg keys
        if (pdfViewer.isVerticalScrollbarEnabled) {
          turnOnlyIfPageFit = true;
        }
        turnPage = 1;
        break;
      case 32: // spacebar
        if (!isViewerInPresentationMode) {
          turnOnlyIfPageFit = true;
        }
        turnPage = 1;
        break;
      case 39: // right arrow
        if (this.supportsCaretBrowsingMode) {
          return;
        }
        // horizontal scrolling using arrow keys
        if (pdfViewer.isHorizontalScrollbarEnabled) {
          turnOnlyIfPageFit = true;
        }
      /* falls through */
      case 74: // 'j'
      case 78: // 'n'
        turnPage = 1;
        break;

      case 36: // home
        if (isViewerInPresentationMode || this.page > 1) {
          this.page = 1;
          handled = true;
          ensureViewerFocused = true;
        }
        break;
      case 35: // end
        if (isViewerInPresentationMode || this.page < this.pagesCount) {
          this.page = this.pagesCount;
          handled = true;
          ensureViewerFocused = true;
        }
        break;

      case 83: // 's'
        this.pdfCursorTools?.switchTool(CursorTool.SELECT);
        break;
      case 72: // 'h'
        this.pdfCursorTools?.switchTool(CursorTool.HAND);
        break;

      case 82: // 'r'
        this.rotatePages(90);
        break;

      case 115: // F4
        this.viewsManager?.toggle();
        break;
    }

    if (
      turnPage !== 0 &&
      (!turnOnlyIfPageFit || pdfViewer.currentScaleValue === "page-fit")
    ) {
      if (turnPage > 0) {
        pdfViewer.nextPage();
      } else {
        pdfViewer.previousPage();
      }
      handled = true;
    }
  }

  // shift-key
  if (cmd === 4) {
    switch (evt.keyCode) {
      case 32: // spacebar
        if (
          !isViewerInPresentationMode &&
          pdfViewer.currentScaleValue !== "page-fit"
        ) {
          break;
        }
        pdfViewer.previousPage();

        handled = true;
        break;

      case 38: // up arrow
        this.moveCaret(/* isUp = */ true, /* select = */ true);
        handled = true;
        break;
      case 40: // down arrow
        this.moveCaret(/* isUp = */ false, /* select = */ true);
        handled = true;
        break;
      case 82: // 'r'
        this.rotatePages(-90);
        break;
    }
  }

  if (!handled && !isViewerInPresentationMode) {
    // 33=Page Up  34=Page Down  35=End    36=Home
    // 37=Left     38=Up         39=Right  40=Down
    // 32=Spacebar
    if (
      (evt.keyCode >= 33 && evt.keyCode <= 40) ||
      (evt.keyCode === 32 && curElementTagName !== "BUTTON")
    ) {
      ensureViewerFocused = true;
    }
  }

  if (ensureViewerFocused && !pdfViewer.containsElement(curElement)) {
    // The page container is not focused, but a page navigation key has been
    // pressed. Change the focus to the viewer container to make sure that
    // navigation by keyboard works as expected.
    pdfViewer.focus();
  }

  if (handled) {
    evt.preventDefault();
  }
}

function beforeUnload(evt) {
  evt.preventDefault();
  evt.returnValue = "";
  return false;
}

export { PDFViewerApplication };
