Source: pdf_sidebar_resizer.js

/* Copyright 2017 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { clamp, NullL10n } from './ui_utils.js'

const SIDEBAR_WIDTH_VAR = '--sidebar-width'
const SIDEBAR_MIN_WIDTH = 200 // pixels
const SIDEBAR_RESIZING_CLASS = 'sidebarResizing'

/**
 * @typedef {Object} PDFSidebarResizerOptions
 * @property {HTMLDivElement} outerContainer - The outer container
 *   (encasing both the viewer and sidebar elements).
 * @property {HTMLDivElement} resizer - The DOM element that can be dragged in
 *   order to adjust the width of the sidebar.
 */

class PDFSidebarResizer {
  /**
   * @param {PDFSidebarResizerOptions} options
   * @param {EventBus} eventBus - The application event bus.
   * @param {IL10n} l10n - Localization service.
   */
  constructor(options, eventBus, l10n = NullL10n) {
    this.enabled = false
    this.isRTL = false
    this.sidebarOpen = false
    this.doc = document.documentElement
    this._width = null
    this._outerContainerWidth = null
    this._boundEvents = Object.create(null)

    this.outerContainer = options.outerContainer
    this.resizer = options.resizer
    this.eventBus = eventBus
    this.l10n = l10n

    if (
      (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) &&
      (typeof CSS === 'undefined' ||
        typeof CSS.supports !== 'function' ||
        !CSS.supports(SIDEBAR_WIDTH_VAR, `calc(-1 * ${SIDEBAR_MIN_WIDTH}px)`))
    ) {
      console.warn(
        'PDFSidebarResizer: ' +
          'The browser does not support resizing of the sidebar.'
      )
      return
    }
    this.enabled = true
    this.resizer.classList.remove('hidden') // Show the resizer DOM element.

    this.l10n.getDirection().then((dir) => {
      this.isRTL = dir === 'rtl'
    })
    this._addEventListeners()
  }

  /**
   * @type {number}
   */
  get outerContainerWidth() {
    if (!this._outerContainerWidth) {
      this._outerContainerWidth = this.outerContainer.clientWidth
    }
    return this._outerContainerWidth
  }

  /**
   * @private
   * returns {boolean} Indicating if the sidebar width was updated.
   */
  _updateWidth(width = 0) {
    if (!this.enabled) {
      return false
    }
    // Prevent the sidebar from becoming too narrow, or from occupying more
    // than half of the available viewer width.
    const newWidth = clamp(
      width,
      SIDEBAR_MIN_WIDTH,
      Math.floor(this.outerContainerWidth / 2)
    )
    // Only update the UI when the sidebar width did in fact change.
    if (newWidth === this._width) {
      return false
    }
    this._width = newWidth
    this.doc.style.setProperty(SIDEBAR_WIDTH_VAR, `${newWidth}px`)
    return true
  }

  /**
   * @private
   */
  _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)
  }

  /**
   * @private
   */
  _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 })

    const _boundEvents = this._boundEvents
    window.removeEventListener('mousemove', _boundEvents.mouseMove)
    window.removeEventListener('mouseup', _boundEvents.mouseUp)
  }

  /**
   * @private
   */
  _addEventListeners() {
    if (!this.enabled) {
      return
    }
    const _boundEvents = this._boundEvents
    _boundEvents.mouseMove = this._mouseMove.bind(this)
    _boundEvents.mouseUp = this._mouseUp.bind(this)

    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.
      this.outerContainer.classList.add(SIDEBAR_RESIZING_CLASS)

      window.addEventListener('mousemove', _boundEvents.mouseMove)
      window.addEventListener('mouseup', _boundEvents.mouseUp)
    })

    this.eventBus._on('sidebarviewchanged', (evt) => {
      this.sidebarOpen = !!(evt && evt.view)
    })

    this.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 || 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.sidebarOpen) {
        this._updateWidth(this._width)
        return
      }
      this.outerContainer.classList.add(SIDEBAR_RESIZING_CLASS)
      const updated = this._updateWidth(this._width)

      Promise.resolve().then(() => {
        this.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) {
          this.eventBus.dispatch('resize', { source: this })
        }
      })
    })
  }
}

export { PDFSidebarResizer }