Javascript  |  488行  |  16.81 KB

// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

'use strict';

<include src="../../../../ui/webui/resources/js/util.js">
<include src="pdf_scripting_api.js">
<include src="viewport.js">

/**
 * @return {number} Width of a scrollbar in pixels
 */
function getScrollbarWidth() {
  var div = document.createElement('div');
  div.style.visibility = 'hidden';
  div.style.overflow = 'scroll';
  div.style.width = '50px';
  div.style.height = '50px';
  div.style.position = 'absolute';
  document.body.appendChild(div);
  var result = div.offsetWidth - div.clientWidth;
  div.parentNode.removeChild(div);
  return result;
}

/**
 * The minimum number of pixels to offset the toolbar by from the bottom and
 * right side of the screen.
 */
PDFViewer.MIN_TOOLBAR_OFFSET = 15;

/**
 * Creates a new PDFViewer. There should only be one of these objects per
 * document.
 */
function PDFViewer() {
  this.loaded = false;

  // The sizer element is placed behind the plugin element to cause scrollbars
  // to be displayed in the window. It is sized according to the document size
  // of the pdf and zoom level.
  this.sizer_ = $('sizer');
  this.toolbar_ = $('toolbar');
  this.pageIndicator_ = $('page-indicator');
  this.progressBar_ = $('progress-bar');
  this.passwordScreen_ = $('password-screen');
  this.passwordScreen_.addEventListener('password-submitted',
                                        this.onPasswordSubmitted_.bind(this));
  this.errorScreen_ = $('error-screen');

  // Create the viewport.
  this.viewport_ = new Viewport(window,
                                this.sizer_,
                                this.viewportChangedCallback_.bind(this),
                                getScrollbarWidth());

  // Create the plugin object dynamically so we can set its src. The plugin
  // element is sized to fill the entire window and is set to be fixed
  // positioning, acting as a viewport. The plugin renders into this viewport
  // according to the scroll position of the window.
  this.plugin_ = document.createElement('object');
  // NOTE: The plugin's 'id' field must be set to 'plugin' since
  // chrome/renderer/printing/print_web_view_helper.cc actually references it.
  this.plugin_.id = 'plugin';
  this.plugin_.type = 'application/x-google-chrome-pdf';
  this.plugin_.addEventListener('message', this.handlePluginMessage_.bind(this),
                                false);

  // Handle scripting messages from outside the extension that wish to interact
  // with it. We also send a message indicating that extension has loaded and
  // is ready to receive messages.
  window.addEventListener('message', this.handleScriptingMessage_.bind(this),
                          false);
  this.sendScriptingMessage_({type: 'readyToReceive'});

  // If the viewer is started from a MIME type request, there will be a
  // background page and stream details object with the details of the request.
  // Otherwise, we take the query string of the URL to indicate the URL of the
  // PDF to load. This is used for print preview in particular.
  if (chrome.extension.getBackgroundPage &&
      chrome.extension.getBackgroundPage()) {
    this.streamDetails =
        chrome.extension.getBackgroundPage().popStreamDetails();
  }

  if (!this.streamDetails) {
    // The URL of this page will be of the form
    // "chrome-extension://<extension id>?<pdf url>". We pull out the <pdf url>
    // part here.
    var url = window.location.search.substring(1);
    this.streamDetails = {
      streamUrl: url,
      originalUrl: url,
      responseHeaders: ''
    };
  }

  this.plugin_.setAttribute('src', this.streamDetails.originalUrl);
  this.plugin_.setAttribute('stream-url', this.streamDetails.streamUrl);
  var headers = '';
  for (var header in this.streamDetails.responseHeaders) {
    headers += header + ': ' +
        this.streamDetails.responseHeaders[header] + '\n';
  }
  this.plugin_.setAttribute('headers', headers);

  if (window.top == window)
    this.plugin_.setAttribute('full-frame', '');
  document.body.appendChild(this.plugin_);

  // Setup the button event listeners.
  $('fit-to-width-button').addEventListener('click',
      this.viewport_.fitToWidth.bind(this.viewport_));
  $('fit-to-page-button').addEventListener('click',
      this.viewport_.fitToPage.bind(this.viewport_));
  $('zoom-in-button').addEventListener('click',
      this.viewport_.zoomIn.bind(this.viewport_));
  $('zoom-out-button').addEventListener('click',
      this.viewport_.zoomOut.bind(this.viewport_));
  $('save-button-link').href = this.streamDetails.originalUrl;
  $('print-button').addEventListener('click', this.print_.bind(this));

  // Setup the keyboard event listener.
  document.onkeydown = this.handleKeyEvent_.bind(this);
}

PDFViewer.prototype = {
  /**
   * @private
   * Handle key events. These may come from the user directly or via the
   * scripting API.
   * @param {KeyboardEvent} e the event to handle.
   */
  handleKeyEvent_: function(e) {
    var position = this.viewport_.position;
    // Certain scroll events may be sent from outside of the extension.
    var fromScriptingAPI = e.type == 'scriptingKeypress';

    switch (e.keyCode) {
      case 33:  // Page up key.
        // Go to the previous page if we are fit-to-page.
        if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
          this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
          // Since we do the movement of the page.
          e.preventDefault();
        } else if (fromScriptingAPI) {
          position.y -= this.viewport.size.height;
          this.viewport.position = position;
        }
        return;
      case 34:  // Page down key.
        // Go to the next page if we are fit-to-page.
        if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
          this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
          // Since we do the movement of the page.
          e.preventDefault();
        } else if (fromScriptingAPI) {
          position.y += this.viewport.size.height;
          this.viewport.position = position;
        }
        return;
      case 37:  // Left arrow key.
        // Go to the previous page if there are no horizontal scrollbars.
        if (!this.viewport_.documentHasScrollbars().x) {
          this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
          // Since we do the movement of the page.
          e.preventDefault();
        } else if (fromScriptingAPI) {
          position.x -= Viewport.SCROLL_INCREMENT;
          this.viewport.position = position;
        }
        return;
      case 38:  // Up arrow key.
        if (fromScriptingAPI) {
          position.y -= Viewport.SCROLL_INCREMENT;
          this.viewport.position = position;
        }
        return;
      case 39:  // Right arrow key.
        // Go to the next page if there are no horizontal scrollbars.
        if (!this.viewport_.documentHasScrollbars().x) {
          this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
          // Since we do the movement of the page.
          e.preventDefault();
        } else if (fromScriptingAPI) {
          position.x += Viewport.SCROLL_INCREMENT;
          this.viewport.position = position;
        }
        return;
      case 40:  // Down arrow key.
        if (fromScriptingAPI) {
          position.y += Viewport.SCROLL_INCREMENT;
          this.viewport.position = position;
        }
        return;
      case 187:  // +/= key.
      case 107:  // Numpad + key.
        if (e.ctrlKey || e.metaKey) {
          this.viewport_.zoomIn();
          // Since we do the zooming of the page.
          e.preventDefault();
        }
        return;
      case 189:  // -/_ key.
      case 109:  // Numpad - key.
        if (e.ctrlKey || e.metaKey) {
          this.viewport_.zoomOut();
          // Since we do the zooming of the page.
          e.preventDefault();
        }
        return;
      case 83:  // s key.
        if (e.ctrlKey || e.metaKey) {
          // Simulate a click on the button so that the <a download ...>
          // attribute is used.
          $('save-button-link').click();
          // Since we do the saving of the page.
          e.preventDefault();
        }
        return;
      case 80:  // p key.
        if (e.ctrlKey || e.metaKey) {
          this.print_();
          // Since we do the printing of the page.
          e.preventDefault();
        }
        return;
    }
  },

  /**
   * @private
   * Notify the plugin to print.
   */
  print_: function() {
    this.plugin_.postMessage({
      type: 'print',
    });
  },

  /**
   * @private
   * Update the loading progress of the document in response to a progress
   * message being received from the plugin.
   * @param {number} progress the progress as a percentage.
   */
  updateProgress_: function(progress) {
    this.progressBar_.progress = progress;
    if (progress == -1) {
      // Document load failed.
      this.errorScreen_.style.visibility = 'visible';
      this.sizer_.style.display = 'none';
      this.toolbar_.style.visibility = 'hidden';
      if (this.passwordScreen_.active) {
        this.passwordScreen_.deny();
        this.passwordScreen_.active = false;
      }
    } else if (progress == 100) {
      // Document load complete.
      this.loaded = true;
      var loadEvent = new Event('pdfload');
      window.dispatchEvent(loadEvent);
      this.sendScriptingMessage_({
        type: 'documentLoaded'
      });
      if (this.lastViewportPosition_)
        this.viewport_.position = this.lastViewportPosition_;
    }
  },

  /**
   * @private
   * An event handler for handling password-submitted events. These are fired
   * when an event is entered into the password screen.
   * @param {Object} event a password-submitted event.
   */
  onPasswordSubmitted_: function(event) {
    this.plugin_.postMessage({
      type: 'getPasswordComplete',
      password: event.detail.password
    });
  },

  /**
   * @private
   * An event handler for handling message events received from the plugin.
   * @param {MessageObject} message a message event.
   */
  handlePluginMessage_: function(message) {
    switch (message.data.type.toString()) {
      case 'documentDimensions':
        this.documentDimensions_ = message.data;
        this.viewport_.setDocumentDimensions(this.documentDimensions_);
        this.toolbar_.style.visibility = 'visible';
        // If we received the document dimensions, the password was good so we
        // can dismiss the password screen.
        if (this.passwordScreen_.active)
          this.passwordScreen_.accept();

        this.pageIndicator_.initialFadeIn();
        this.toolbar_.initialFadeIn();
        break;
      case 'email':
        var href = 'mailto:' + message.data.to + '?cc=' + message.data.cc +
            '&bcc=' + message.data.bcc + '&subject=' + message.data.subject +
            '&body=' + message.data.body;
        var w = window.open(href, '_blank', 'width=1,height=1');
        if (w)
          w.close();
        break;
      case 'getAccessibilityJSONReply':
        this.sendScriptingMessage_(message.data);
        break;
      case 'getPassword':
        // If the password screen isn't up, put it up. Otherwise we're
        // responding to an incorrect password so deny it.
        if (!this.passwordScreen_.active)
          this.passwordScreen_.active = true;
        else
          this.passwordScreen_.deny();
        break;
      case 'goToPage':
        this.viewport_.goToPage(message.data.page);
        break;
      case 'loadProgress':
        this.updateProgress_(message.data.progress);
        break;
      case 'navigate':
        if (message.data.newTab)
          window.open(message.data.url);
        else
          window.location.href = message.data.url;
        break;
      case 'setScrollPosition':
        var position = this.viewport_.position;
        if (message.data.x != undefined)
          position.x = message.data.x;
        if (message.data.y != undefined)
          position.y = message.data.y;
        this.viewport_.position = position;
        break;
      case 'setTranslatedStrings':
        this.passwordScreen_.text = message.data.getPasswordString;
        this.progressBar_.text = message.data.loadingString;
        this.errorScreen_.text = message.data.loadFailedString;
        break;
      case 'cancelStreamUrl':
        chrome.streamsPrivate.abort(this.streamDetails.streamUrl);
        break;
    }
  },

  /**
   * @private
   * A callback that's called when the viewport changes.
   */
  viewportChangedCallback_: function() {
    if (!this.documentDimensions_)
      return;

    // Update the buttons selected.
    $('fit-to-page-button').classList.remove('polymer-selected');
    $('fit-to-width-button').classList.remove('polymer-selected');
    if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
      $('fit-to-page-button').classList.add('polymer-selected');
    } else if (this.viewport_.fittingType ==
               Viewport.FittingType.FIT_TO_WIDTH) {
      $('fit-to-width-button').classList.add('polymer-selected');
    }

    var hasScrollbars = this.viewport_.documentHasScrollbars();
    var scrollbarWidth = this.viewport_.scrollbarWidth;
    // Offset the toolbar position so that it doesn't move if scrollbars appear.
    var toolbarRight = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
    var toolbarBottom = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
    if (hasScrollbars.vertical)
      toolbarRight -= scrollbarWidth;
    if (hasScrollbars.horizontal)
      toolbarBottom -= scrollbarWidth;
    this.toolbar_.style.right = toolbarRight + 'px';
    this.toolbar_.style.bottom = toolbarBottom + 'px';

    // Update the page indicator.
    var visiblePage = this.viewport_.getMostVisiblePage();
    this.pageIndicator_.index = visiblePage;
    if (this.documentDimensions_.pageDimensions.length > 1 &&
        hasScrollbars.vertical) {
      this.pageIndicator_.style.visibility = 'visible';
    } else {
      this.pageIndicator_.style.visibility = 'hidden';
    }

    var position = this.viewport_.position;
    var zoom = this.viewport_.zoom;
    // Notify the plugin of the viewport change.
    this.plugin_.postMessage({
      type: 'viewport',
      zoom: zoom,
      xOffset: position.x,
      yOffset: position.y
    });

    var visiblePageDimensions = this.viewport_.getPageScreenRect(visiblePage);
    var size = this.viewport_.size;
    this.sendScriptingMessage_({
      type: 'viewport',
      pageX: visiblePageDimensions.x,
      pageY: visiblePageDimensions.y,
      pageWidth: visiblePageDimensions.width,
      viewportWidth: size.width,
      viewportHeight: size.height,
    });
  },

  /**
   * @private
   * Handle a scripting message from outside the extension (typically sent by
   * PDFScriptingAPI in a page containing the extension) to interact with the
   * plugin.
   * @param {MessageObject} message the message to handle.
   */
  handleScriptingMessage_: function(message) {
    switch (message.data.type.toString()) {
      case 'getAccessibilityJSON':
      case 'loadPreviewPage':
        this.plugin_.postMessage(message.data);
        break;
      case 'resetPrintPreviewMode':
        if (!this.inPrintPreviewMode_) {
          this.inPrintPreviewMode_ = true;
          this.viewport_.fitToPage();
        }

        // Stash the scroll location so that it can be restored when the new
        // document is loaded.
        this.lastViewportPosition_ = this.viewport_.position;

        // TODO(raymes): Disable these properly in the plugin.
        var printButton = $('print-button');
        if (printButton)
          printButton.parentNode.removeChild(printButton);
        var saveButton = $('save-button');
        if (saveButton)
          saveButton.parentNode.removeChild(saveButton);

        this.pageIndicator_.pageLabels = message.data.pageNumbers;

        this.plugin_.postMessage({
          type: 'resetPrintPreviewMode',
          url: message.data.url,
          grayscale: message.data.grayscale,
          // If the PDF isn't modifiable we send 0 as the page count so that no
          // blank placeholder pages get appended to the PDF.
          pageCount: (message.data.modifiable ?
                      message.data.pageNumbers.length : 0)
        });
        break;
      case 'sendKeyEvent':
        var e = document.createEvent('Event');
        e.initEvent('scriptingKeypress');
        e.keyCode = message.data.keyCode;
        this.handleKeyEvent_(e);
        break;
    }

  },

  /**
   * @private
   * Send a scripting message outside the extension (typically to
   * PDFScriptingAPI in a page containing the extension).
   * @param {Object} message the message to send.
   */
  sendScriptingMessage_: function(message) {
    window.parent.postMessage(message, '*');
  },

  /**
   * @type {Viewport} the viewport of the PDF viewer.
   */
  get viewport() {
    return this.viewport_;
  }
};

var viewer = new PDFViewer();