| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 | /* Copyright 2016 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. */import {  PresentationModeState,  RenderingStates,  SidebarView,} from "./ui_utils.js";const UI_NOTIFICATION_CLASS = "pdfSidebarNotification";/** * @typedef {Object} PDFSidebarOptions * @property {PDFSidebarElements} elements - The DOM elements. * @property {PDFViewer} pdfViewer - The document viewer. * @property {PDFThumbnailViewer} pdfThumbnailViewer - The thumbnail viewer. * @property {EventBus} eventBus - The application event bus. * @property {IL10n} l10n - The localization service. *//** * @typedef {Object} PDFSidebarElements * @property {HTMLDivElement} outerContainer - The outer container *   (encasing both the viewer and sidebar elements). * @property {HTMLDivElement} sidebarContainer - The sidebar container *   (in which the views are placed). * @property {HTMLButtonElement} toggleButton - The button used for *   opening/closing the sidebar. * @property {HTMLButtonElement} thumbnailButton - The button used to show *   the thumbnail view. * @property {HTMLButtonElement} outlineButton - The button used to show *   the outline view. * @property {HTMLButtonElement} attachmentsButton - The button used to show *   the attachments view. * @property {HTMLButtonElement} layersButton - The button used to show *   the layers view. * @property {HTMLDivElement} thumbnailView - The container in which *   the thumbnails are placed. * @property {HTMLDivElement} outlineView - The container in which *   the outline is placed. * @property {HTMLDivElement} attachmentsView - The container in which *   the attachments are placed. * @property {HTMLDivElement} layersView - The container in which *   the layers are placed. * @property {HTMLDivElement} outlineOptionsContainer - The container in which *   the outline view-specific option button(s) are placed. * @property {HTMLButtonElement} currentOutlineItemButton - The button used to *   find the current outline item. */class PDFSidebar {  /**   * @param {PDFSidebarOptions} options   */  constructor({ elements, pdfViewer, pdfThumbnailViewer, eventBus, l10n }) {    this.isOpen = false;    this.active = SidebarView.THUMBS;    this.isInitialViewSet = false;    this.isInitialEventDispatched = false;    /**     * Callback used when the sidebar has been opened/closed, to ensure that     * the viewers (PDFViewer/PDFThumbnailViewer) are updated correctly.     */    this.onToggled = null;    this.pdfViewer = pdfViewer;    this.pdfThumbnailViewer = pdfThumbnailViewer;    this.outerContainer = elements.outerContainer;    this.sidebarContainer = elements.sidebarContainer;    this.toggleButton = elements.toggleButton;    this.thumbnailButton = elements.thumbnailButton;    this.outlineButton = elements.outlineButton;    this.attachmentsButton = elements.attachmentsButton;    this.layersButton = elements.layersButton;    this.thumbnailView = elements.thumbnailView;    this.outlineView = elements.outlineView;    this.attachmentsView = elements.attachmentsView;    this.layersView = elements.layersView;    this._outlineOptionsContainer = elements.outlineOptionsContainer;    this._currentOutlineItemButton = elements.currentOutlineItemButton;    this.eventBus = eventBus;    this.l10n = l10n;    this.#addEventListeners();  }  reset() {    this.isInitialViewSet = false;    this.isInitialEventDispatched = false;    this.#hideUINotification(/* reset = */ true);    this.switchView(SidebarView.THUMBS);    this.outlineButton.disabled = false;    this.attachmentsButton.disabled = false;    this.layersButton.disabled = false;    this._currentOutlineItemButton.disabled = true;  }  /**   * @type {number} One of the values in {SidebarView}.   */  get visibleView() {    return this.isOpen ? this.active : SidebarView.NONE;  }  /**   * @param {number} view - The sidebar view that should become visible,   *                        must be one of the values in {SidebarView}.   */  setInitialView(view = SidebarView.NONE) {    if (this.isInitialViewSet) {      return;    }    this.isInitialViewSet = true;    // If the user has already manually opened the sidebar, immediately closing    // it would be bad UX; also ignore the "unknown" sidebar view value.    if (view === SidebarView.NONE || view === SidebarView.UNKNOWN) {      this.#dispatchEvent();      return;    }    this.switchView(view, /* forceOpen = */ true);    // Prevent dispatching two back-to-back "sidebarviewchanged" events,    // since `this.switchView` dispatched the event if the view changed.    if (!this.isInitialEventDispatched) {      this.#dispatchEvent();    }  }  /**   * @param {number} view - The sidebar view that should be switched to,   *                        must be one of the values in {SidebarView}.   * @param {boolean} [forceOpen] - Ensure that the sidebar is open.   *                                The default value is `false`.   */  switchView(view, forceOpen = false) {    const isViewChanged = view !== this.active;    let shouldForceRendering = false;    switch (view) {      case SidebarView.NONE:        if (this.isOpen) {          this.close();        }        return; // Closing will trigger rendering and dispatch the event.      case SidebarView.THUMBS:        if (this.isOpen && isViewChanged) {          shouldForceRendering = true;        }        break;      case SidebarView.OUTLINE:        if (this.outlineButton.disabled) {          return;        }        break;      case SidebarView.ATTACHMENTS:        if (this.attachmentsButton.disabled) {          return;        }        break;      case SidebarView.LAYERS:        if (this.layersButton.disabled) {          return;        }        break;      default:        console.error(`PDFSidebar.switchView: "${view}" is not a valid view.`);        return;    }    // Update the active view *after* it has been validated above,    // in order to prevent setting it to an invalid state.    this.active = view;    const isThumbs = view === SidebarView.THUMBS,      isOutline = view === SidebarView.OUTLINE,      isAttachments = view === SidebarView.ATTACHMENTS,      isLayers = view === SidebarView.LAYERS;    // Update the CSS classes (and aria attributes), for all buttons...    this.thumbnailButton.classList.toggle("toggled", isThumbs);    this.outlineButton.classList.toggle("toggled", isOutline);    this.attachmentsButton.classList.toggle("toggled", isAttachments);    this.layersButton.classList.toggle("toggled", isLayers);    this.thumbnailButton.setAttribute("aria-checked", isThumbs);    this.outlineButton.setAttribute("aria-checked", isOutline);    this.attachmentsButton.setAttribute("aria-checked", isAttachments);    this.layersButton.setAttribute("aria-checked", isLayers);    // ... and for all views.    this.thumbnailView.classList.toggle("hidden", !isThumbs);    this.outlineView.classList.toggle("hidden", !isOutline);    this.attachmentsView.classList.toggle("hidden", !isAttachments);    this.layersView.classList.toggle("hidden", !isLayers);    // Finally, update view-specific CSS classes.    this._outlineOptionsContainer.classList.toggle("hidden", !isOutline);    if (forceOpen && !this.isOpen) {      this.open();      return; // Opening will trigger rendering and dispatch the event.    }    if (shouldForceRendering) {      this.#updateThumbnailViewer();      this.#forceRendering();    }    if (isViewChanged) {      this.#dispatchEvent();    }  }  open() {    if (this.isOpen) {      return;    }    this.isOpen = true;    this.toggleButton.classList.add("toggled");    this.toggleButton.setAttribute("aria-expanded", "true");    this.outerContainer.classList.add("sidebarMoving", "sidebarOpen");    if (this.active === SidebarView.THUMBS) {      this.#updateThumbnailViewer();    }    this.#forceRendering();    this.#dispatchEvent();    this.#hideUINotification();  }  close() {    if (!this.isOpen) {      return;    }    this.isOpen = false;    this.toggleButton.classList.remove("toggled");    this.toggleButton.setAttribute("aria-expanded", "false");    this.outerContainer.classList.add("sidebarMoving");    this.outerContainer.classList.remove("sidebarOpen");    this.#forceRendering();    this.#dispatchEvent();  }  toggle() {    if (this.isOpen) {      this.close();    } else {      this.open();    }  }  #dispatchEvent() {    if (this.isInitialViewSet && !this.isInitialEventDispatched) {      this.isInitialEventDispatched = true;    }    this.eventBus.dispatch("sidebarviewchanged", {      source: this,      view: this.visibleView,    });  }  #forceRendering() {    if (this.onToggled) {      this.onToggled();    } else {      // Fallback      this.pdfViewer.forceRendering();      this.pdfThumbnailViewer.forceRendering();    }  }  #updateThumbnailViewer() {    const { pdfViewer, pdfThumbnailViewer } = this;    // Use the rendered pages to set the corresponding thumbnail images.    const pagesCount = pdfViewer.pagesCount;    for (let pageIndex = 0; pageIndex < pagesCount; pageIndex++) {      const pageView = pdfViewer.getPageView(pageIndex);      if (pageView?.renderingState === RenderingStates.FINISHED) {        const thumbnailView = pdfThumbnailViewer.getThumbnail(pageIndex);        thumbnailView.setImage(pageView);      }    }    pdfThumbnailViewer.scrollThumbnailIntoView(pdfViewer.currentPageNumber);  }  #showUINotification() {    this.toggleButton.setAttribute(      "data-l10n-id",      "toggle_sidebar_notification2"    );    this.l10n.translate(this.toggleButton);    if (!this.isOpen) {      // Only show the notification on the `toggleButton` if the sidebar is      // currently closed, to avoid unnecessarily bothering the user.      this.toggleButton.classList.add(UI_NOTIFICATION_CLASS);    }  }  #hideUINotification(reset = false) {    if (this.isOpen || reset) {      // Only hide the notification on the `toggleButton` if the sidebar is      // currently open, or when the current PDF document is being closed.      this.toggleButton.classList.remove(UI_NOTIFICATION_CLASS);    }    if (reset) {      this.toggleButton.setAttribute("data-l10n-id", "toggle_sidebar");      this.l10n.translate(this.toggleButton);    }  }  #addEventListeners() {    this.sidebarContainer.addEventListener("transitionend", evt => {      if (evt.target === this.sidebarContainer) {        this.outerContainer.classList.remove("sidebarMoving");      }    });    this.toggleButton.addEventListener("click", () => {      this.toggle();    });    // Buttons for switching views.    this.thumbnailButton.addEventListener("click", () => {      this.switchView(SidebarView.THUMBS);    });    this.outlineButton.addEventListener("click", () => {      this.switchView(SidebarView.OUTLINE);    });    this.outlineButton.addEventListener("dblclick", () => {      this.eventBus.dispatch("toggleoutlinetree", { source: this });    });    this.attachmentsButton.addEventListener("click", () => {      this.switchView(SidebarView.ATTACHMENTS);    });    this.layersButton.addEventListener("click", () => {      this.switchView(SidebarView.LAYERS);    });    this.layersButton.addEventListener("dblclick", () => {      this.eventBus.dispatch("resetlayers", { source: this });    });    // Buttons for view-specific options.    this._currentOutlineItemButton.addEventListener("click", () => {      this.eventBus.dispatch("currentoutlineitem", { source: this });    });    // Disable/enable views.    const onTreeLoaded = (count, button, view) => {      button.disabled = !count;      if (count) {        this.#showUINotification();      } else if (this.active === view) {        // If the `view` was opened by the user during document load,        // switch away from it if it turns out to be empty.        this.switchView(SidebarView.THUMBS);      }    };    this.eventBus._on("outlineloaded", evt => {      onTreeLoaded(evt.outlineCount, this.outlineButton, SidebarView.OUTLINE);      evt.currentOutlineItemPromise.then(enabled => {        if (!this.isInitialViewSet) {          return;        }        this._currentOutlineItemButton.disabled = !enabled;      });    });    this.eventBus._on("attachmentsloaded", evt => {      onTreeLoaded(        evt.attachmentsCount,        this.attachmentsButton,        SidebarView.ATTACHMENTS      );    });    this.eventBus._on("layersloaded", evt => {      onTreeLoaded(evt.layersCount, this.layersButton, SidebarView.LAYERS);    });    // Update the thumbnailViewer, if visible, when exiting presentation mode.    this.eventBus._on("presentationmodechanged", evt => {      if (        evt.state === PresentationModeState.NORMAL &&        this.visibleView === SidebarView.THUMBS      ) {        this.#updateThumbnailViewer();      }    });  }}export { PDFSidebar };
 |