zoom_service.js

/* eslint-disable prettier/prettier */
import { MIN_X_OFFSET, MIN_Y_OFFSET, TW_DATA_TAG } from "./util.js";
import { ViewerType } from "./base_viewer.js";

/**
 * Zoom service object
 */
class ZoomService {
  constructor(app, initialZoom, isZoomFitToPage) {
    this._app = app;
    this._app.viewer.isZoomFitToPage = isZoomFitToPage || false;
    this._initialZoomValue = initialZoom || 0;
    this.zoomDropDownOpened = false;
    this._bindEvents();

    /**
     * Available zoom levels for selected document and viewer dimensions.
     */
    this.zoomLevels = [];
  }

  // #region private functions
  _bindEvents() {
    const me = this;
    this._app.elements.tw2018elem_zoomDropDownValue.addEventListener(
      "keydown",
      function (e) {
        switch (e.which || e.keyCode) {
          case 13: {
            let zoomValue = 0;
            try {
              zoomValue = parseFloat(
                this.app.elements.tw2018elem_zoomDropDownValue.value
                  .replace("%", "")
                  .trim()
              );
            } catch (err) {}

            if (
              isNaN(zoomValue) ||
              zoomValue < 10 ||
              zoomValue > this._zoomService.getMaxZoomValue()
            ) {
              me._app.elements.tw2018elem_zoomDropDownValue.value =
                this.app.viewer.validZoomValue;
              return;
            }

            me._app.viewer.isZoomFitToPage = false;
            me.setZoomValue(zoomValue);
            me.hideZoomDropDown();
          }
        }
      }
    );

    this._app.elements.tw2018elem_zoomValuesDropDownClose.addEventListener(
      "click",
      function () {
        me.hideZoomDropDown();
      }
    );

    this._app.elements.tw2018elem_zoomDropDownArrow.addEventListener(
      "click",
      function (event) {
        if (this.disabled) {
          event.preventDefault();
          return;
        }

        if (me.zoomDropDownOpened) {
          me.hideZoomDropDown();
        } else {
          me.showZoomDropDown();
        }
        event.preventDefault();
      }
    );

    me._app.elements.tw2018elem_zoominBtn.addEventListener(
      "click",
      function (event) {
        if (!event.defaultPrevented) {
          me.zoomIn();
        }
      }
    );

    me._app.elements.tw2018elem_zoomoutBtn.addEventListener(
      "click",
      function (event) {
        if (!event.defaultPrevented) {
          me.zoomOut();
        }
      }
    );
  }

  hideZoomDropDown() {
    this._app.elements.tw2018elem_zoomValuesDropDown.classList.remove(
      "tw2018css_show"
    );
    this._app.elements.tw2018elem_zoomDropDownArrow.setAttribute(
      "aria-expanded",
      "false"
    );
    this.zoomDropDownOpened = false;
  }

  showZoomDropDown() {
    const rectToolbar =
      this._app.elements.tw2018elem_toolbar.getBoundingClientRect();
    const rect =
      this._app.elements.tw2018elem_zoomDropDownValue.getBoundingClientRect();
    // const y = rectToolbar.height;
    // const y = rect.bottom - 1;
    const y = rect.top - rectToolbar.top + rect.height + 8;
    const x = rect.left - rectToolbar.left - 1;
    // this._app.elements.tw2018elem_zoomValues.style.top = y + "px";
    // this._app.elements.tw2018elem_zoomValues.style.left = x + "px";
    this._app.elements.tw2018elem_zoomValues.classList.add("tw2018css_show");
    this._app.elements.tw2018elem_zoomValuesDropDown.style.top = y + "px";
    this._app.elements.tw2018elem_zoomValuesDropDown.style.left = x + "px";
    this._app.elements.tw2018elem_zoomValuesDropDown.classList.add(
      "tw2018css_show"
    );
    this._app.elements.tw2018elem_zoomDropDownArrow.setAttribute(
      "aria-expanded",
      "true"
    );
    this.zoomDropDownOpened = true;
  }

  _updateToolbarZoomValue() {
    this._app.elements.tw2018elem_zoominBtn.disabled =
      this._app.viewer.currentZoomLevel === this.zoomLevels.length - 1;
    this._app.elements.tw2018elem_zoomoutBtn.disabled =
      this._app.viewer.currentZoomLevel === 0;

    this._selectToolbarZoomDropDownValue();
  }

  _getToolbarZoomLi(scale, text) {
    const li = document.createElement("li");
    li.setAttribute("scale", scale);
    const btn = document.createElement("button");
    btn.innerText = text;
    li.appendChild(btn);
    // eslint-disable-next-line no-unsanitized/property
    if (scale === this._app.viewer.scale) {
      li.classList.add("tw2018css_selected");
      li.setAttribute(TW_DATA_TAG, "");
      li.setAttribute("aria-selected", "true");
    }

    const me = this;
    me._app.elements.tw2018elem_zoomValues.appendChild(li);
    btn.addEventListener("click", function () {
      me._app.elements.tw2018elem_zoomDropDownValue.value = this.innerHTML;
      const scaleVal = this.parentElement.getAttribute("scale");
      me._app.viewer.scale =
        scaleVal === "fit" ? me._app.viewer.fitToPage : parseFloat(scaleVal);
      me._app.viewer.isZoomFitToPage =
        me._app.viewer.scale === me._app.viewer.fitToPage;
      me._app.viewer.currentZoomLevel = me.zoomLevels.indexOf(
        me._app.viewer.scale
      );
      me.executeZoom();
      me.hideZoomDropDown();
    });
    return li;
  }

  _fillToolbarZoomDropDownValues() {
    const val = Math.round(this._app.viewer.scale * 100) + "%";
    this._app.elements.tw2018elem_zoomDropDownValue.value = val;
    this._app.viewer.validZoomValue = val;
    this._app.elements.tw2018elem_zoomValues.innerHTML = "";

    for (let i = 0; i < this.zoomLevels.length; i++) {
      this._app.elements.tw2018elem_zoomValues.appendChild(
        this._getToolbarZoomLi(
          this.zoomLevels[i],
          Math.round(this.zoomLevels[i] * 100) + "%"
        )
      );
    }

    const horizontalSeparator = document.createElement("hr");
    horizontalSeparator.setAttribute(TW_DATA_TAG, "");
    horizontalSeparator.classList.add("tw2018css_horizontalSeparator");
    this._app.elements.tw2018elem_zoomValues.appendChild(horizontalSeparator);

    this._app.elements.tw2018elem_zoomValues.appendChild(
      this._getToolbarZoomLi(1, this._app.texts.zoom_actual_size)
    );
    const fitSize = this._getToolbarZoomLi(
      "fit",
      this._app.texts.zoom_fit_to_page
    );
    if (this._app.viewer.isZoomFitToPage) {
      fitSize.classList.add("tw2018css_selected");
      fitSize.setAttribute(TW_DATA_TAG, "");
    }
    this._app.elements.tw2018elem_zoomValues.appendChild(fitSize);
  }

  // In this method it isn't important what will be zoom level, we are just
  // looking for the page with the biggest size
  _getNumberOfPageWithBiggestSize() {
    let numberOfBiggestPage = 0;
    let minZoomFactor = 999999;
    for (let i = 0; i < this._app.pdfDocument.pages.length; i++) {
      const page = this._app.pdfDocument.pages[i];
      const zoomFactorWidth = this._app.visibleAreaWidth / page.originalWidth;
      const zoomFactorHeight =
        this._app.visibleAreaHeight / page.originalHeight;

      const pageZoom =
        zoomFactorWidth < zoomFactorHeight ? zoomFactorWidth : zoomFactorHeight;

      if (pageZoom <= minZoomFactor) {
        minZoomFactor = pageZoom;
        numberOfBiggestPage = i + 1;
      }
    }

    return numberOfBiggestPage;
  }

  _getDisplayRatio() {
    return 96 / 72;
  }

  _getOriginalDisplayTotalHeightAndWidthWithoutScrollBars() {
    const pageSizesInfo = {
      maxWidth: 0,
      totalHeight: 0,
    };

    for (let i = 0; i < this._app.pdfDocument.pages.length; i++) {
      const page = this._app.pdfDocument.pages[i];
      pageSizesInfo.totalHeight += MIN_Y_OFFSET;
      pageSizesInfo.totalHeight += page.originalHeight;

      if (page.originalWidth > pageSizesInfo.maxWidth) {
        pageSizesInfo.maxWidth = page.originalWidth;
      }
    }
    pageSizesInfo.totalHeight += MIN_Y_OFFSET;
    return pageSizesInfo;
  }

  _selectToolbarZoomDropDownValue() {
    const val = Math.round(this._app.viewer.scale * 100) + "%";
    this._app.elements.tw2018elem_zoomDropDownValue.value = val;
    this._app.viewer.validZoomValue = val;
    for (
      let i = 0;
      i < this._app.elements.tw2018elem_zoomValues.childElementCount;
      i++
    ) {
      const elem = this._app.elements.tw2018elem_zoomValues.children[i];
      const scaleAttr = elem.getAttribute("scale");
      if (
        (scaleAttr === "fit" && this._app.viewer.isZoomFitToPage) ||
        scaleAttr === this._app.viewer.scale
      ) {
        elem.classList.add("tw2018css_selected");
      } else {
        elem.classList.remove("tw2018css_selected");
      }
    }
  }

  _checkIfVerticalScrollIsRequired(totalHeight, isHorizontalBarVisible) {
    const horizontalScrollBarHeight = isHorizontalBarVisible ? 20 : 0;
    return (
      totalHeight + horizontalScrollBarHeight > this._app.visibleAreaHeight
    );
  }

  _getFitToPageZoomLevel(totalHeight = -1) {
    const numberOfPageWithBiggestSize = this._getNumberOfPageWithBiggestSize();
    const biggestPage =
      this._app.pdfDocument.pages[numberOfPageWithBiggestSize - 1];

    const isVerticalScrollRequired = this._checkIfVerticalScrollIsRequired(
      totalHeight,
      false
    ); // horizontalScrollRequired is false because we are searching FitToPage zoom so there will not be horizontalScroll
    const verticalScrollWidth = isVerticalScrollRequired ? 20 : 0;

    const controlHeight = this._app.visibleAreaHeight - MIN_Y_OFFSET * 2;
    const controlWidth =
      this._app.visibleAreaWidth - verticalScrollWidth - MIN_X_OFFSET * 2;

    const zoomFactorWidth = controlWidth / biggestPage.originalWidth;
    const zoomFactorHeight = controlHeight / biggestPage.originalHeight;

    return Math.min(zoomFactorWidth, zoomFactorHeight);
  }

  _setMaxZoomLevel() {
    const numberOfPageWithBiggestSize = this._getNumberOfPageWithBiggestSize();
    const maxPageWidth =
      this._app.pdfDocument.pages[numberOfPageWithBiggestSize - 1]
        .originalWidth;
    const maxPageHeight =
      this._app.pdfDocument.pages[numberOfPageWithBiggestSize - 1]
        .originalHeight;

    const maxBitmapSizeInBytes = 100 * 1024 * 1024;

    const bytesPerPixel = 4; // 32 bit
    const displayRatio = this._getDisplayRatio();
    this._app.viewer.maxZoomLevel = Math.sqrt(
      maxBitmapSizeInBytes /
        bytesPerPixel /
        (maxPageWidth * displayRatio * maxPageHeight * displayRatio)
    );
  }
  // #endregion

  // #region internal functions
  useInitialZoomValueIfSet() {
    if (!this._initialZoomValue) {
      return false;
    }

    this.calculateZoom();
    this.setZoomValue(this._initialZoomValue);
    this._initialZoomValue = 0;
    return true;
  }

  mouseWheelHandler(e) {
    // cross-browser wheel data
    const delta = Math.max(-1, Math.min(1, e.wheelDelta || -e.detail));

    if (
      delta > 0 &&
      this._app.viewer.currentZoomLevel < this.zoomLevels.length - 1
    ) {
      // const oldValue = this.zoomLevels[this.viewer.currentZoomLevel];
      this._app.viewer.currentZoomLevel++;
      this.executeZoom();
    }

    if (delta < 0 && this._app.viewer.currentZoomLevel > 0) {
      // const oldValue = this.zoomLevels[this.viewer.currentZoomLevel];
      this._app.viewer.currentZoomLevel--;
      this.executeZoom();
    }
  }

  executeZoom(reloadDocument = true) {
    this._app.viewer.tempPageNumber = this._app.viewer.currentPageNumber;
    this._app.viewer.scale = this.zoomLevels[this._app.viewer.currentZoomLevel];
    this._app.viewer.isZoomFitToPage =
      this._app.viewer.scale === this._app.viewer.fitToPage;
    this._updateToolbarZoomValue();
    if (reloadDocument) {
      this._app.renderPdfDocInViewer();
    }
  }

  setZoomLevels(fitToPage, customZoom, maxZoomLevel) {
    this.zoomLevels = [];
    const customZoom1 = Math.min(fitToPage, customZoom);
    const customZoom2 = Math.max(fitToPage, customZoom);

    let customZoom1Added = false;
    let customZoom2Added = false;
    if (customZoom1 === customZoom2) {
      customZoom1Added = true;
    }

    maxZoomLevel = (parseInt((maxZoomLevel * 100) / 50) * 50) / 100; // round max value to nearest 50

    const defaultZoomLevels = [0.1, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 4, 8, 16];
    for (let i = 0; i < defaultZoomLevels.length; i++) {
      const zoomLevel = defaultZoomLevels[i];
      if (maxZoomLevel < zoomLevel) {
        break;
      }

      if (!customZoom1Added && customZoom1 < zoomLevel) {
        customZoom1Added = true;
        if (customZoom1 > 0 && !this.zoomLevels.includes(customZoom1)) {
          this.zoomLevels.push(customZoom1);
        }
      }

      if (!customZoom2Added && customZoom2 < zoomLevel) {
        customZoom2Added = true;
        if (customZoom2 > 0 && !this.zoomLevels.includes(customZoom2)) {
          this.zoomLevels.push(customZoom2);
        }
      }

      if (!this.zoomLevels.includes(zoomLevel)) {
        this.zoomLevels.push(zoomLevel);
      }
    }

    if (
      !customZoom1Added &&
      customZoom1 < maxZoomLevel &&
      customZoom1 > 0 &&
      !this.zoomLevels.includes(customZoom1)
    ) {
      this.zoomLevels.push(customZoom1);
    }

    if (
      !customZoom2Added &&
      customZoom2 < maxZoomLevel &&
      customZoom2 > 0 &&
      !this.zoomLevels.includes(customZoom2)
    ) {
      this.zoomLevels.push(customZoom2);
    }

    if (!this.zoomLevels.includes(maxZoomLevel)) {
      this.zoomLevels.push(maxZoomLevel);
    }
  }

  fitToPageIfNeeded() {
    this._app.viewer.fitToPage = this._getFitToPageZoomLevel();
    if (this._app.viewer.isZoomFitToPage) {
      this._app.viewer.scale = this._app.viewer.fitToPage;
      this.setZoomLevels(
        this._app.viewer.fitToPage,
        this._app.viewer.scale,
        this._app.viewer.maxZoomLevel
      );
      this._fillToolbarZoomDropDownValues();
      this._app.viewer.currentZoomLevel = this.zoomLevels.indexOf(
        this._app.viewer.scale
      );
      this.executeZoom();
    }
  }

  calculateZoom() {
    const heightArea =
      this._app.viewer.type === ViewerType.MULTI_VIEW
        ? this._getOriginalDisplayTotalHeightAndWidthWithoutScrollBars()
        : this._getNumberOfPageWithBiggestSize();

    this._app.viewer.fitToPage = this._getFitToPageZoomLevel(
      heightArea.totalHeight
    );

    this.setZoomLevels(
      this._app.viewer.fitToPage,
      this._app.viewer.scale,
      this._app.viewer.maxZoomLevel
    );

    if (this._app.viewer.isZoomFitToPage) {
      this._app.viewer.scale = this._app.viewer.fitToPage;
      this._app.viewer.currentZoomLevel = this.zoomLevels.indexOf(
        this._app.viewer.fitToPage
      );
    }

    this._setMaxZoomLevel();
    this.setZoomLevels(
      this._app.viewer.fitToPage,
      this._app.viewer.scale,
      this._app.viewer.maxZoomLevel
    );
    this._fillToolbarZoomDropDownValues();
  }
  // #endregion

  // #region public functions
  /** Magnify displayed content size. */
  zoomIn() {
    if (this._app.viewer.currentZoomLevel === this.zoomLevels.length - 1) {
      return;
    }
    // const oldValue = this.zoomLevels[this.viewer.currentZoomLevel];
    this._app.viewer.currentZoomLevel++;
    this.executeZoom();
  }

  //* Reduce displayed content size. */
  zoomOut() {
    if (this._app.viewer.currentZoomLevel === 0) {
      return;
    }
    // const oldValue = this.zoomLevels[this.viewer.currentZoomLevel];
    this._app.viewer.currentZoomLevel--;
    this.executeZoom();
  }

  /** Sets and applies *zoomValue*.
   * @param {number} zoomValue - Zoom value to set.
   */
  setZoomValue(zoomValue) {
    this._app.viewer.scale = zoomValue / 100;
    if (this._app.viewer.isZoomFitToPage) {
      this._app.viewer.scale = this._app.viewer.fitToPage;
    }
    this._app.viewer.currentZoomLevel = this.zoomLevels.indexOf(
      this._app.viewer.scale
    );
    if (this._app.viewer.currentZoomLevel === -1) {
      this.setZoomLevels(
        this._app.viewer.fitToPage,
        this._app.viewer.scale,
        this._app.viewer.maxZoomLevel
      );
      this._app.viewer.currentZoomLevel = this.zoomLevels.indexOf(
        this._app.viewer.scale
      );
      this._fillToolbarZoomDropDownValues();
    }
    this.executeZoom();
  }

  /** Returns max zoom value for loaded document and
   * current viewer dimension size. */
  getMaxZoomValue() {
    return this.zoomLevels[this.zoomLevels.length - 1] * 100;
  }
  // #endregion
}

export { ZoomService };