Some checks failed
		
		
	
	Types tests / Test (lts/*) (push) Has been cancelled
				
			Lint / Lint (lts/*) (push) Has been cancelled
				
			CodeQL / Analyze (javascript) (push) Has been cancelled
				
			CI / Test (20) (push) Has been cancelled
				
			CI / Test (22) (push) Has been cancelled
				
			CI / Test (24) (push) Has been cancelled
				
			
		
			
				
	
	
		
			515 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			515 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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.
 | 
						|
 */
 | 
						|
 | 
						|
/** @typedef {import("./event_utils.js").EventBus} EventBus */
 | 
						|
/** @typedef {import("./interfaces.js").IL10n} IL10n */
 | 
						|
 | 
						|
import {
 | 
						|
  docStyle,
 | 
						|
  PresentationModeState,
 | 
						|
  SidebarView,
 | 
						|
  toggleCheckedBtn,
 | 
						|
  toggleExpandedBtn,
 | 
						|
} from "./ui_utils.js";
 | 
						|
 | 
						|
const SIDEBAR_WIDTH_VAR = "--sidebar-width";
 | 
						|
const SIDEBAR_MIN_WIDTH = 200; // pixels
 | 
						|
const SIDEBAR_RESIZING_CLASS = "sidebarResizing";
 | 
						|
const UI_NOTIFICATION_CLASS = "pdfSidebarNotification";
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef {Object} PDFSidebarOptions
 | 
						|
 * @property {PDFSidebarElements} elements - The DOM elements.
 | 
						|
 * @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 {HTMLDivElement} resizer - The DOM element that can be dragged in
 | 
						|
 *   order to adjust the width of 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 {HTMLButtonElement} currentOutlineItemButton - The button used to
 | 
						|
 *   find the current outline item.
 | 
						|
 */
 | 
						|
 | 
						|
class PDFSidebar {
 | 
						|
  #isRTL = false;
 | 
						|
 | 
						|
  #mouseAC = null;
 | 
						|
 | 
						|
  #outerContainerWidth = null;
 | 
						|
 | 
						|
  #width = null;
 | 
						|
 | 
						|
  /**
 | 
						|
   * @param {PDFSidebarOptions} options
 | 
						|
   */
 | 
						|
  constructor({ elements, 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.onUpdateThumbnails = null;
 | 
						|
 | 
						|
    this.outerContainer = elements.outerContainer;
 | 
						|
    this.sidebarContainer = elements.sidebarContainer;
 | 
						|
    this.toggleButton = elements.toggleButton;
 | 
						|
    this.resizer = elements.resizer;
 | 
						|
 | 
						|
    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._currentOutlineItemButton = elements.currentOutlineItemButton;
 | 
						|
 | 
						|
    this.eventBus = eventBus;
 | 
						|
 | 
						|
    this.#isRTL = l10n.getDirection() === "rtl";
 | 
						|
    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 forceRendering = 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) {
 | 
						|
          forceRendering = 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;
 | 
						|
 | 
						|
    // Update the CSS classes (and aria attributes), for all buttons and views.
 | 
						|
    toggleCheckedBtn(
 | 
						|
      this.thumbnailButton,
 | 
						|
      view === SidebarView.THUMBS,
 | 
						|
      this.thumbnailView
 | 
						|
    );
 | 
						|
    toggleCheckedBtn(
 | 
						|
      this.outlineButton,
 | 
						|
      view === SidebarView.OUTLINE,
 | 
						|
      this.outlineView
 | 
						|
    );
 | 
						|
    toggleCheckedBtn(
 | 
						|
      this.attachmentsButton,
 | 
						|
      view === SidebarView.ATTACHMENTS,
 | 
						|
      this.attachmentsView
 | 
						|
    );
 | 
						|
    toggleCheckedBtn(
 | 
						|
      this.layersButton,
 | 
						|
      view === SidebarView.LAYERS,
 | 
						|
      this.layersView
 | 
						|
    );
 | 
						|
 | 
						|
    if (forceOpen && !this.isOpen) {
 | 
						|
      this.open();
 | 
						|
      return; // Opening will trigger rendering and dispatch the event.
 | 
						|
    }
 | 
						|
    if (forceRendering) {
 | 
						|
      this.onUpdateThumbnails();
 | 
						|
      this.onToggled();
 | 
						|
    }
 | 
						|
    if (isViewChanged) {
 | 
						|
      this.#dispatchEvent();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  open() {
 | 
						|
    if (this.isOpen) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this.isOpen = true;
 | 
						|
    toggleExpandedBtn(this.toggleButton, true);
 | 
						|
 | 
						|
    this.outerContainer.classList.add("sidebarMoving", "sidebarOpen");
 | 
						|
 | 
						|
    if (this.active === SidebarView.THUMBS) {
 | 
						|
      this.onUpdateThumbnails();
 | 
						|
    }
 | 
						|
    this.onToggled();
 | 
						|
    this.#dispatchEvent();
 | 
						|
 | 
						|
    this.#hideUINotification();
 | 
						|
  }
 | 
						|
 | 
						|
  close(evt = null) {
 | 
						|
    if (!this.isOpen) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this.isOpen = false;
 | 
						|
    toggleExpandedBtn(this.toggleButton, false);
 | 
						|
 | 
						|
    this.outerContainer.classList.add("sidebarMoving");
 | 
						|
    this.outerContainer.classList.remove("sidebarOpen");
 | 
						|
 | 
						|
    this.onToggled();
 | 
						|
    this.#dispatchEvent();
 | 
						|
 | 
						|
    if (evt?.detail > 0) {
 | 
						|
      // Remove focus from the toggleButton if it's clicked (see issue 17361).
 | 
						|
      this.toggleButton.blur();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  toggle(evt = null) {
 | 
						|
    if (this.isOpen) {
 | 
						|
      this.close(evt);
 | 
						|
    } else {
 | 
						|
      this.open();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  #dispatchEvent() {
 | 
						|
    if (this.isInitialViewSet) {
 | 
						|
      this.isInitialEventDispatched ||= true;
 | 
						|
    }
 | 
						|
 | 
						|
    this.eventBus.dispatch("sidebarviewchanged", {
 | 
						|
      source: this,
 | 
						|
      view: this.visibleView,
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  #showUINotification() {
 | 
						|
    this.toggleButton.setAttribute(
 | 
						|
      "data-l10n-id",
 | 
						|
      "pdfjs-toggle-sidebar-notification-button"
 | 
						|
    );
 | 
						|
 | 
						|
    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",
 | 
						|
        "pdfjs-toggle-sidebar-button"
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  #addEventListeners() {
 | 
						|
    const { eventBus, outerContainer } = this;
 | 
						|
 | 
						|
    this.sidebarContainer.addEventListener("transitionend", evt => {
 | 
						|
      if (evt.target === this.sidebarContainer) {
 | 
						|
        outerContainer.classList.remove("sidebarMoving");
 | 
						|
        // Ensure that rendering is triggered after opening/closing the sidebar.
 | 
						|
        eventBus.dispatch("resize", { source: this });
 | 
						|
      }
 | 
						|
    });
 | 
						|
 | 
						|
    this.toggleButton.addEventListener("click", evt => {
 | 
						|
      this.toggle(evt);
 | 
						|
    });
 | 
						|
 | 
						|
    // Buttons for switching views.
 | 
						|
    this.thumbnailButton.addEventListener("click", () => {
 | 
						|
      this.switchView(SidebarView.THUMBS);
 | 
						|
    });
 | 
						|
 | 
						|
    this.outlineButton.addEventListener("click", () => {
 | 
						|
      this.switchView(SidebarView.OUTLINE);
 | 
						|
    });
 | 
						|
    this.outlineButton.addEventListener("dblclick", () => {
 | 
						|
      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", () => {
 | 
						|
      eventBus.dispatch("resetlayers", { source: this });
 | 
						|
    });
 | 
						|
 | 
						|
    // Buttons for view-specific options.
 | 
						|
    this._currentOutlineItemButton.addEventListener("click", () => {
 | 
						|
      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);
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    eventBus._on("outlineloaded", evt => {
 | 
						|
      onTreeLoaded(evt.outlineCount, this.outlineButton, SidebarView.OUTLINE);
 | 
						|
 | 
						|
      evt.currentOutlineItemPromise.then(enabled => {
 | 
						|
        if (!this.isInitialViewSet) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
        this._currentOutlineItemButton.disabled = !enabled;
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    eventBus._on("attachmentsloaded", evt => {
 | 
						|
      onTreeLoaded(
 | 
						|
        evt.attachmentsCount,
 | 
						|
        this.attachmentsButton,
 | 
						|
        SidebarView.ATTACHMENTS
 | 
						|
      );
 | 
						|
    });
 | 
						|
 | 
						|
    eventBus._on("layersloaded", evt => {
 | 
						|
      onTreeLoaded(evt.layersCount, this.layersButton, SidebarView.LAYERS);
 | 
						|
    });
 | 
						|
 | 
						|
    // Update the thumbnailViewer, if visible, when exiting presentation mode.
 | 
						|
    eventBus._on("presentationmodechanged", evt => {
 | 
						|
      if (
 | 
						|
        evt.state === PresentationModeState.NORMAL &&
 | 
						|
        this.visibleView === SidebarView.THUMBS
 | 
						|
      ) {
 | 
						|
        this.onUpdateThumbnails();
 | 
						|
      }
 | 
						|
    });
 | 
						|
 | 
						|
    // Handle resizing of the sidebar.
 | 
						|
    this.resizer.addEventListener("mousedown", evt => {
 | 
						|
      if (evt.button !== 0) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      // Disable the `transition-duration` rules when sidebar resizing begins,
 | 
						|
      // in order to improve responsiveness and to avoid visual glitches.
 | 
						|
      outerContainer.classList.add(SIDEBAR_RESIZING_CLASS);
 | 
						|
 | 
						|
      this.#mouseAC = new AbortController();
 | 
						|
      const opts = { signal: this.#mouseAC.signal };
 | 
						|
 | 
						|
      window.addEventListener("mousemove", this.#mouseMove.bind(this), opts);
 | 
						|
      window.addEventListener("mouseup", this.#mouseUp.bind(this), opts);
 | 
						|
      window.addEventListener("blur", this.#mouseUp.bind(this), opts);
 | 
						|
    });
 | 
						|
 | 
						|
    eventBus._on("resize", evt => {
 | 
						|
      // When the *entire* viewer is resized, such that it becomes narrower,
 | 
						|
      // ensure that the sidebar doesn't end up being too wide.
 | 
						|
      if (evt.source !== window) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      // Always reset the cached width when the viewer is resized.
 | 
						|
      this.#outerContainerWidth = null;
 | 
						|
 | 
						|
      if (!this.#width) {
 | 
						|
        // The sidebar hasn't been resized, hence no need to adjust its width.
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      // NOTE: If the sidebar is closed, we don't need to worry about
 | 
						|
      //       visual glitches nor ensure that rendering is triggered.
 | 
						|
      if (!this.isOpen) {
 | 
						|
        this.#updateWidth(this.#width);
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      outerContainer.classList.add(SIDEBAR_RESIZING_CLASS);
 | 
						|
      const updated = this.#updateWidth(this.#width);
 | 
						|
 | 
						|
      Promise.resolve().then(() => {
 | 
						|
        outerContainer.classList.remove(SIDEBAR_RESIZING_CLASS);
 | 
						|
        // Trigger rendering if the sidebar width changed, to avoid
 | 
						|
        // depending on the order in which 'resize' events are handled.
 | 
						|
        if (updated) {
 | 
						|
          eventBus.dispatch("resize", { source: this });
 | 
						|
        }
 | 
						|
      });
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * @type {number}
 | 
						|
   */
 | 
						|
  get outerContainerWidth() {
 | 
						|
    return (this.#outerContainerWidth ||= this.outerContainer.clientWidth);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * returns {boolean} Indicating if the sidebar width was updated.
 | 
						|
   */
 | 
						|
  #updateWidth(width = 0) {
 | 
						|
    // Prevent the sidebar from becoming too narrow, or from occupying more
 | 
						|
    // than half of the available viewer width.
 | 
						|
    const maxWidth = Math.floor(this.outerContainerWidth / 2);
 | 
						|
    if (width > maxWidth) {
 | 
						|
      width = maxWidth;
 | 
						|
    }
 | 
						|
    if (width < SIDEBAR_MIN_WIDTH) {
 | 
						|
      width = SIDEBAR_MIN_WIDTH;
 | 
						|
    }
 | 
						|
    // Only update the UI when the sidebar width did in fact change.
 | 
						|
    if (width === this.#width) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    this.#width = width;
 | 
						|
 | 
						|
    docStyle.setProperty(SIDEBAR_WIDTH_VAR, `${width}px`);
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  #mouseMove(evt) {
 | 
						|
    let width = evt.clientX;
 | 
						|
    // For sidebar resizing to work correctly in RTL mode, invert the width.
 | 
						|
    if (this.#isRTL) {
 | 
						|
      width = this.outerContainerWidth - width;
 | 
						|
    }
 | 
						|
    this.#updateWidth(width);
 | 
						|
  }
 | 
						|
 | 
						|
  #mouseUp(evt) {
 | 
						|
    // Re-enable the `transition-duration` rules when sidebar resizing ends...
 | 
						|
    this.outerContainer.classList.remove(SIDEBAR_RESIZING_CLASS);
 | 
						|
    // ... and ensure that rendering will always be triggered.
 | 
						|
    this.eventBus.dispatch("resize", { source: this });
 | 
						|
 | 
						|
    this.#mouseAC?.abort();
 | 
						|
    this.#mouseAC = null;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export { PDFSidebar };
 |