Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
ndm / opt / ndm / pdf_viewer_resources.pak
Size: Mime:
*ãäåÓ
æçúŽè®éëê&ë_HìñXíjlîŸwïý…ðž‡ñïŒòƑó>–ôWŸõð öš÷”¢øA¦ù—©ú£¬ûï­üq³ýضþ@ºÿBÀ…ÆùÊÒ‰ÞeëJððô#	Ý

.ûb>"/* Copyright 2015 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. */

body {
  background-color: rgb(82, 86, 89);
  font-family: 'Roboto', 'Noto', sans-serif;
  margin: 0;
}

viewer-page-indicator {
  visibility: hidden;
  z-index: 2;
}

viewer-pdf-toolbar {
  position: fixed;
  width: 100%;
  z-index: 4;
}

#plugin {
  height: 100%;
  position: fixed;
  width: 100%;
  z-index: 1;
}

#sizer {
  position: absolute;
  z-index: 0;
}

@media(max-height: 250px) {
  viewer-pdf-toolbar {
    display: none;
  }
}

@media(max-height: 200px) {
  viewer-zoom-toolbar {
    display: none;
  }
}

@media(max-width: 300px) {
  viewer-zoom-toolbar {
    display: none;
  }
}
<!doctype html>
<html>
<head>
  <base href="chrome://pdf-viewer/">
  <meta charset="utf-8">
  <link rel="import" href="elements/viewer-error-screen/viewer-error-screen.html">
  <link rel="import" href="elements/viewer-page-indicator/viewer-page-indicator.html">
  <link rel="import" href="elements/viewer-page-selector/viewer-page-selector.html">
  <link rel="import" href="elements/viewer-password-screen/viewer-password-screen.html">
  <link rel="import" href="elements/viewer-pdf-toolbar/viewer-pdf-toolbar.html">
  <link rel="import" href="elements/viewer-zoom-toolbar/viewer-zoom-toolbar.html">

  <link rel="stylesheet" href="chrome://resources/css/text_defaults.css">
  <link rel="stylesheet" href="chrome://resources/css/roboto.css">
  <link rel="stylesheet" href="index.css">
</head>
<body>

<viewer-pdf-toolbar id="toolbar" hidden></viewer-pdf-toolbar>

<div id="sizer"></div>
<viewer-password-screen id="password-screen"></viewer-password-screen>

<viewer-zoom-toolbar id="zoom-toolbar"></viewer-zoom-toolbar>

<viewer-page-indicator id="page-indicator"></viewer-page-indicator>

<viewer-error-screen id="error-screen"></viewer-error-screen>

</body>
<script src="toolbar_manager.js"></script>
<script src="viewport.js"></script>
<script src="open_pdf_params_parser.js"></script>
<script src="navigator.js"></script>
<script src="viewport_scroller.js"></script>
<script src="gesture_detector.js"></script>
<script src="zoom_manager.js"></script>
<script src="chrome://resources/js/promise_resolver.js"></script>
<script src="chrome://resources/js/cr.js"></script>
<script src="chrome://resources/js/util.js"></script>
<script src="browser_api.js"></script>
<script src="pdf.js"></script>
<script src="main.js"></script>
</html>
// Copyright 2014 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';

/**
 * Global PDFViewer object, accessible for testing.
 * @type Object
 */
var viewer;

(function() {
  /**
  * Stores any pending messages received which should be passed to the
  * PDFViewer when it is created.
  * @type Array
  */
  var pendingMessages = [];

  /**
  * Handles events that are received prior to the PDFViewer being created.
  * @param {Object} message A message event received.
  */
  function handleScriptingMessage(message) {
    pendingMessages.push(message);
  }

  /**
  * Initialize the global PDFViewer and pass any outstanding messages to it.
  * @param {Object} browserApi An object providing an API to the browser.
  */
  function initViewer(browserApi) {
    // PDFViewer will handle any messages after it is created.
    window.removeEventListener('message', handleScriptingMessage, false);
    viewer = new PDFViewer(browserApi);
    while (pendingMessages.length > 0)
      viewer.handleScriptingMessage(pendingMessages.shift());
  }

  /**
  * Entrypoint for starting the PDF viewer. This function obtains the browser
  * API for the PDF and constructs a PDFViewer object with it.
  */
  cr.sendWithPromise('initialize').then(function(opts) {
    // Set up an event listener to catch scripting messages which are sent prior
    // to the PDFViewer being created.
    window.addEventListener('message', handleScriptingMessage, false);

    createBrowserApi(opts).then(initViewer);
  });
})()
// 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';

/**
 * An enum containing a value specifying whether the PDF is currently loading,
 * has finished loading or failed to load.
 */
var LoadState = {
  LOADING: 'loading',
  SUCCESS: 'success',
  FAILED: 'failed'
};

/**
 * @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;
}

/**
 * Return the filename component of a URL, percent decoded if possible.
 * @param {string} url The URL to get the filename from.
 * @return {string} The filename component.
 */
function getFilenameFromURL(url) {
  // Ignore the query and fragment.
  var mainUrl = url.split(/#|\?/)[0];
  var components = mainUrl.split(/\/|\\/);
  var filename = components[components.length - 1];
  try {
    return decodeURIComponent(filename);
  } catch (e) {
    if (e instanceof URIError)
      return filename;
    throw e;
  }
}

/**
 * Whether keydown events should currently be ignored. Events are ignored when
 * an editable element has focus, to allow for proper editing controls.
 * @param {HTMLElement} activeElement The currently selected DOM node.
 * @return {boolean} True if keydown events should be ignored.
 */
function shouldIgnoreKeyEvents(activeElement) {
  while (activeElement.shadowRoot != null &&
         activeElement.shadowRoot.activeElement != null) {
    activeElement = activeElement.shadowRoot.activeElement;
  }

  return (activeElement.isContentEditable ||
          activeElement.tagName == 'INPUT' ||
          activeElement.tagName == 'TEXTAREA');
}

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

/**
 * The height of the toolbar along the top of the page. The document will be
 * shifted down by this much in the viewport.
 */
PDFViewer.MATERIAL_TOOLBAR_HEIGHT = 56;

/**
 * Minimum height for the material toolbar to show (px). Should match the media
 * query in index-material.css. If the window is smaller than this at load,
 * leave no space for the toolbar.
 */
PDFViewer.TOOLBAR_WINDOW_MIN_HEIGHT = 250;

/**
 * The light-gray background color used for print preview.
 */
PDFViewer.LIGHT_BACKGROUND_COLOR = '0xFFCCCCCC';

/**
 * The dark-gray background color used for the regular viewer.
 */
PDFViewer.DARK_BACKGROUND_COLOR = '0xFF525659';

/**
 * Creates a new PDFViewer. There should only be one of these objects per
 * document.
 * @constructor
 * @param {!BrowserApi} browserApi An object providing an API to the browser.
 */
function PDFViewer(browserApi) {
  this.browserApi_ = browserApi;
  this.originalUrl_ = this.browserApi_.getStreamInfo().originalUrl;
  this.loadState_ = LoadState.LOADING;
  this.parentWindow_ = null;
  this.parentOrigin_ = null;
  this.isFormFieldFocused_ = false;

  this.delayedScriptingMessages_ = [];

  this.isPrintPreview_ = location.origin === 'chrome://print';

  // Parse open pdf parameters.
  this.paramsParser_ =
      new OpenPDFParamsParser(this.getNamedDestination_.bind(this));
  var toolbarEnabled =
      this.paramsParser_.getUiUrlParams(this.originalUrl_).toolbar &&
      !this.isPrintPreview_;

  // 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');
  if (this.isPrintPreview_)
    this.pageIndicator_ = $('page-indicator');
  this.passwordScreen_ = $('password-screen');
  this.passwordScreen_.addEventListener('password-submitted',
                                        this.onPasswordSubmitted_.bind(this));
  this.errorScreen_ = $('error-screen');
  // Can only reload if we are in a normal tab.
  this.errorScreen_.reloadFn = function() {
    chrome.send('reload');
  };

  // Create the viewport.
  var shortWindow = window.innerHeight < PDFViewer.TOOLBAR_WINDOW_MIN_HEIGHT;
  var topToolbarHeight =
      (toolbarEnabled) ? PDFViewer.MATERIAL_TOOLBAR_HEIGHT : 0;
  this.viewport_ = new Viewport(window,
                                this.sizer_,
                                this.viewportChanged_.bind(this),
                                this.beforeZoom_.bind(this),
                                this.afterZoom_.bind(this),
                                getScrollbarWidth(),
                                this.browserApi_.getDefaultZoom(),
                                topToolbarHeight);

  // 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('embed');
  // 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.plugin_.setAttribute('src', this.originalUrl_);
  this.plugin_.setAttribute('stream-url',
                            this.browserApi_.getStreamInfo().streamUrl);
  var headers = '';
  for (var header in this.browserApi_.getStreamInfo().responseHeaders) {
    headers += header + ': ' +
        this.browserApi_.getStreamInfo().responseHeaders[header] + '\n';
  }
  this.plugin_.setAttribute('headers', headers);

  var backgroundColor = PDFViewer.DARK_BACKGROUND_COLOR;
  this.plugin_.setAttribute('background-color', backgroundColor);
  this.plugin_.setAttribute('top-toolbar-height', topToolbarHeight);

  if (this.browserApi_.getStreamInfo().embedded) {
    this.plugin_.setAttribute('top-level-url',
                              this.browserApi_.getStreamInfo().tabUrl);
  } else {
    this.plugin_.setAttribute('full-frame', '');
  }
  document.body.appendChild(this.plugin_);

  // Setup the button event listeners.
  this.zoomToolbar_ = $('zoom-toolbar');
  this.zoomToolbar_.addEventListener('fit-to-width',
      this.viewport_.fitToWidth.bind(this.viewport_));
  this.zoomToolbar_.addEventListener('fit-to-page',
      this.fitToPage_.bind(this));
  this.zoomToolbar_.addEventListener('zoom-in',
      this.viewport_.zoomIn.bind(this.viewport_));
  this.zoomToolbar_.addEventListener('zoom-out',
      this.viewport_.zoomOut.bind(this.viewport_));

  this.gestureDetector_ = new GestureDetector(this.plugin_);
  this.gestureDetector_.addEventListener(
      'pinchstart', this.viewport_.pinchZoomStart.bind(this.viewport_));
  this.sentPinchEvent_ = false;
  this.gestureDetector_.addEventListener(
      'pinchupdate', this.onPinchUpdate_.bind(this));
  this.gestureDetector_.addEventListener(
      'pinchend', this.onPinchEnd_.bind(this));

  if (toolbarEnabled) {
    this.toolbar_ = $('toolbar');
    this.toolbar_.hidden = false;
    this.toolbar_.addEventListener('save', this.save_.bind(this));
    this.toolbar_.addEventListener('print', this.print_.bind(this));
    this.toolbar_.addEventListener('rotate-right',
        this.rotateClockwise_.bind(this));
    // Must attach to mouseup on the plugin element, since it eats mousedown
    // and click events.
    this.plugin_.addEventListener('mouseup',
        this.toolbar_.hideDropdowns.bind(this.toolbar_));

    this.toolbar_.docTitle = getFilenameFromURL(this.originalUrl_);
  }

  document.body.addEventListener('change-page', function(e) {
    this.viewport_.goToPage(e.detail.page);
  }.bind(this));

  document.body.addEventListener('navigate', function(e) {
    var disposition =
        e.detail.newtab ? Navigator.WindowOpenDisposition.NEW_BACKGROUND_TAB :
                          Navigator.WindowOpenDisposition.CURRENT_TAB;
    this.navigator_.navigate(e.detail.uri, disposition);
  }.bind(this));

  this.toolbarManager_ =
      new ToolbarManager(window, this.toolbar_, this.zoomToolbar_);

  // Set up the ZoomManager.
  this.zoomManager_ = new ZoomManager(
      this.viewport_, this.browserApi_.setZoom.bind(this.browserApi_),
      this.browserApi_.getInitialZoom());
  this.browserApi_.addZoomEventListener(
      this.zoomManager_.onBrowserZoomChange.bind(this.zoomManager_));

  // Setup the keyboard event listener.
  document.addEventListener('keydown', this.handleKeyEvent_.bind(this));
  document.addEventListener('mousemove', this.handleMouseEvent_.bind(this));
  document.addEventListener('mouseout', this.handleMouseEvent_.bind(this));

  var tabId = this.browserApi_.getStreamInfo().tabId;
  this.navigator_ = new Navigator(
      this.originalUrl_, this.viewport_, this.paramsParser_,
      new NavigatorDelegate(tabId));
  this.viewportScroller_ =
      new ViewportScroller(this.viewport_, this.plugin_, window);

  // Request translated strings.
  cr.sendWithPromise('getStrings').then(this.handleStrings_.bind(this));
}

PDFViewer.prototype = {
  /**
   * Sets the callback which will be run when the PDF document has finished
   * loading. If the document is already loaded, it will be run immediately.
   * @param {Function} callback the callback to be called.
   */
  setLoadCallback: function(callback) {
    this.loadCallback_ = callback;
    if (this.loadState_ != LoadState.LOADING && this.loadCallback_)
      this.loadCallback_(this.loadState_ == LoadState.SUCCESS);
  },

  /**
   * @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.fromScriptingAPI;

    if (shouldIgnoreKeyEvents(document.activeElement) || e.defaultPrevented)
      return;

    this.toolbarManager_.hideToolbarsAfterTimeout(e);

    var pageUpHandler = function() {
      // 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;
      }
    }.bind(this);
    var pageDownHandler = function() {
      // 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;
      }
    }.bind(this);

    switch (e.keyCode) {
      case 9:  // Tab key.
        this.toolbarManager_.showToolbarsForKeyboardNavigation();
        return;
      case 27:  // Escape key.
        if (!this.isPrintPreview_) {
          this.toolbarManager_.hideSingleToolbarLayer();
          return;
        }
        break;  // Ensure escape falls through to the print-preview handler.
      case 32:  // Space key.
        if (e.shiftKey)
          pageUpHandler();
        else
          pageDownHandler();
        return;
      case 33:  // Page up key.
        pageUpHandler();
        return;
      case 34:  // Page down key.
        pageDownHandler();
        return;
      case 37:  // Left arrow key.
        if (!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
          // Go to the previous page if there are no horizontal scrollbars and
          // no form field is focused.
          if (!(this.viewport_.documentHasScrollbars().horizontal ||
                this.isFormFieldFocused_)) {
            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.
        if (!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
          // Go to the next page if there are no horizontal scrollbars and no
          // form field is focused.
          if (!(this.viewport_.documentHasScrollbars().horizontal ||
                this.isFormFieldFocused_)) {
            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 65:  // 'a' key.
        if (e.ctrlKey || e.metaKey) {
          this.plugin_.postMessage({
            type: 'selectAll'
          });
          // Since we do selection ourselves.
          e.preventDefault();
        }
        return;
      case 71: // 'g' key.
        if (this.toolbar_ && (e.ctrlKey || e.metaKey) && e.altKey) {
          this.toolbarManager_.showToolbars();
          this.toolbar_.selectPageNumber();
        }
        return;
      case 219:  // Left bracket key.
        if (e.ctrlKey)
          this.rotateCounterClockwise_();
        return;
      case 220:  // Backslash key.
        if (e.ctrlKey)
          this.zoomToolbar_.fitToggleFromHotKey();
        return;
      case 221:  // Right bracket key.
        if (e.ctrlKey)
          this.rotateClockwise_();
        return;
    }

    // Give print preview a chance to handle the key event.
    if (!fromScriptingAPI && this.isPrintPreview_) {
      this.sendScriptingMessage_({
        type: 'sendKeyEvent',
        keyEvent: SerializeKeyEvent(e)
      });
    } else {
      // Show toolbars as a fallback.
      if (!(e.shiftKey || e.ctrlKey || e.altKey))
        this.toolbarManager_.showToolbars();
    }
  },

  handleMouseEvent_: function(e) {
    if (e.type == 'mousemove')
      this.toolbarManager_.handleMouseMove(e);
    else if (e.type == 'mouseout')
      this.toolbarManager_.hideToolbarsForMouseOut();
  },

  /**
   * @private
   * Rotate the plugin clockwise.
   */
  rotateClockwise_: function() {
    this.plugin_.postMessage({
      type: 'rotateClockwise'
    });
  },

  /**
   * @private
   * Rotate the plugin counter-clockwise.
   */
  rotateCounterClockwise_: function() {
    this.plugin_.postMessage({
      type: 'rotateCounterclockwise'
    });
  },

  /**
   * @private
   * Set zoom to "fit to page".
   */
  fitToPage_: function() {
    this.viewport_.fitToPage();
    this.toolbarManager_.forceHideTopToolbar();
  },

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

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

  /**
   * Fetches the page number corresponding to the given named destination from
   * the plugin.
   * @param {string} name The namedDestination to fetch page number from plugin.
   */
  getNamedDestination_: function(name) {
    this.plugin_.postMessage({
      type: 'getNamedDestination',
      namedDestination: name
    });
  },

  /**
   * @private
   * Sends a 'documentLoaded' message to the PDFScriptingAPI if the document has
   * finished loading.
   */
  sendDocumentLoadedMessage_: function() {
    if (this.loadState_ == LoadState.LOADING)
      return;
    if (this.loadCallback_)
      this.loadCallback_(this.loadState_ == LoadState.SUCCESS);
    this.sendScriptingMessage_({
      type: 'documentLoaded',
      load_state: this.loadState_
    });
  },

  /**
   * @private
   * Handle open pdf parameters. This function updates the viewport as per
   * the parameters mentioned in the url while opening pdf. The order is
   * important as later actions can override the effects of previous actions.
   * @param {Object} viewportPosition The initial position of the viewport to be
   *     displayed.
   */
  handleURLParams_: function(viewportPosition) {
    if (viewportPosition.page != undefined)
      this.viewport_.goToPage(viewportPosition.page);
    if (viewportPosition.position) {
      // Make sure we don't cancel effect of page parameter.
      this.viewport_.position = {
        x: this.viewport_.position.x + viewportPosition.position.x,
        y: this.viewport_.position.y + viewportPosition.position.y
      };
    }
    if (viewportPosition.zoom)
      this.viewport_.setZoom(viewportPosition.zoom);
  },

  /**
   * @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) {
    if (this.toolbar_)
      this.toolbar_.loadProgress = progress;

    if (progress == -1) {
      // Document load failed.
      this.errorScreen_.show();
      this.sizer_.style.display = 'none';
      if (this.passwordScreen_.active) {
        this.passwordScreen_.deny();
        this.passwordScreen_.active = false;
      }
      this.loadState_ = LoadState.FAILED;
      this.sendDocumentLoadedMessage_();
    } else if (progress == 100) {
      // Document load complete.
      if (this.lastViewportPosition_)
        this.viewport_.position = this.lastViewportPosition_;
      this.paramsParser_.getViewportFromUrlParams(
          this.originalUrl_,
          this.handleURLParams_.bind(this));
      this.loadState_ = LoadState.SUCCESS;
      this.sendDocumentLoadedMessage_();
      while (this.delayedScriptingMessages_.length > 0)
        this.handleScriptingMessage(this.delayedScriptingMessages_.shift());

      this.toolbarManager_.hideToolbarsAfterTimeout();
    }
  },

  /**
   * @private
   * Load a dictionary of translated strings into the UI. Used as a callback for
   * chrome.resourcesPrivate.
   * @param {Object} strings Dictionary of translated strings
   */
  handleStrings_: function(strings) {
    document.documentElement.dir = strings.textdirection;
    document.documentElement.lang = strings.language;

    $('toolbar').strings = strings;
    $('zoom-toolbar').strings = strings;
    $('password-screen').strings = strings;
    $('error-screen').strings = strings;
  },

  /**
   * @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_);
        // If we received the document dimensions, the password was good so we
        // can dismiss the password screen.
        if (this.passwordScreen_.active)
          this.passwordScreen_.accept();

        if (this.pageIndicator_)
          this.pageIndicator_.initialFadeIn();

        if (this.toolbar_) {
          this.toolbar_.docLength =
              this.documentDimensions_.pageDimensions.length;
        }
        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;
        window.location.href = href;
        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 'getSelectedTextReply':
        this.sendScriptingMessage_(message.data);
        break;
      case 'goToPage':
        this.viewport_.goToPage(message.data.page);
        break;
      case 'loadProgress':
        this.updateProgress_(message.data.progress);
        break;
      case 'navigate':
        // If in print preview, always open a new tab.
        if (this.isPrintPreview_) {
          this.navigator_.navigate(
              message.data.url,
              Navigator.WindowOpenDisposition.NEW_BACKGROUND_TAB);
        } else {
          this.navigator_.navigate(message.data.url, message.data.disposition);
        }
        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 'cancelStreamUrl':
        chrome.mimeHandlerPrivate.abortStream();
        break;
      case 'metadata':
        if (message.data.title) {
          document.title = message.data.title;
        } else {
          document.title = getFilenameFromURL(this.originalUrl_);
        }
        this.bookmarks_ = message.data.bookmarks;
        if (this.toolbar_) {
          this.toolbar_.docTitle = document.title;
          this.toolbar_.bookmarks = this.bookmarks;
        }
        break;
      case 'setIsSelecting':
        this.viewportScroller_.setEnableScrolling(message.data.isSelecting);
        break;
      case 'getNamedDestinationReply':
        this.paramsParser_.onNamedDestinationReceived(message.data.pageNumber);
        break;
      case 'formFocusChange':
        this.isFormFieldFocused_ = message.data.focused;
        break;
    }
  },

  /**
   * @private
   * A callback that's called before the zoom changes. Notify the plugin to stop
   * reacting to scroll events while zoom is taking place to avoid flickering.
   */
  beforeZoom_: function() {
    this.plugin_.postMessage({
      type: 'stopScrolling'
    });

    if (this.viewport_.pinchPhase == Viewport.PinchPhase.PINCH_START) {
      var position = this.viewport_.position;
      var zoom = this.viewport_.zoom;
      var pinchPhase = this.viewport_.pinchPhase;
      this.plugin_.postMessage({
        type: 'viewport',
        zoom: zoom,
        xOffset: position.x,
        yOffset: position.y,
        pinchPhase: pinchPhase
      });
    }
  },

  /**
   * @private
   * A callback that's called after the zoom changes. Notify the plugin of the
   * zoom change and to continue reacting to scroll events.
   */
  afterZoom_: function() {
    var position = this.viewport_.position;
    var zoom = this.viewport_.zoom;
    var pinchVector = this.viewport_.pinchPanVector || {x: 0, y: 0};
    var pinchCenter = this.viewport_.pinchCenter || {x: 0, y: 0};
    var pinchPhase = this.viewport_.pinchPhase;

    this.plugin_.postMessage({
      type: 'viewport',
      zoom: zoom,
      xOffset: position.x,
      yOffset: position.y,
      pinchPhase: pinchPhase,
      pinchX: pinchCenter.x,
      pinchY: pinchCenter.y,
      pinchVectorX: pinchVector.x,
      pinchVectorY: pinchVector.y
    });
    this.zoomManager_.onPdfZoomChange();
  },

  /**
   * @private
   * A callback that's called when an update to a pinch zoom is detected.
   * @param {!Object} e the pinch event.
   */
  onPinchUpdate_: function(e) {
    // Throttle number of pinch events to one per frame.
    if (!this.sentPinchEvent_) {
      this.sentPinchEvent_ = true;
      window.requestAnimationFrame(function() {
        this.sentPinchEvent_ = false;
        this.viewport_.pinchZoom(e);
      }.bind(this));
    }
  },

  /**
   * @private
   * A callback that's called when the end of a pinch zoom is detected.
   * @param {!Object} e the pinch event.
   */
  onPinchEnd_: function(e) {
    // Using rAF for pinch end prevents pinch updates scheduled by rAF getting
    // sent after the pinch end.
    window.requestAnimationFrame(function() {
      this.viewport_.pinchZoomEnd(e);
    }.bind(this));
  },

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

    // Offset the toolbar position so that it doesn't move if scrollbars appear.
    var hasScrollbars = this.viewport_.documentHasScrollbars();
    var scrollbarWidth = this.viewport_.scrollbarWidth;
    var verticalScrollbarWidth = hasScrollbars.vertical ? scrollbarWidth : 0;
    var horizontalScrollbarWidth =
        hasScrollbars.horizontal ? scrollbarWidth : 0;

    // Shift the zoom toolbar to the left by half a scrollbar width. This
    // gives a compromise: if there is no scrollbar visible then the toolbar
    // will be half a scrollbar width further left than the spec but if there
    // is a scrollbar visible it will be half a scrollbar width further right
    // than the spec. In RTL layout, the zoom toolbar is on the left side, but
    // the scrollbar is still on the right, so this is not necessary.
    if (!isRTL()) {
      this.zoomToolbar_.style.right = -verticalScrollbarWidth +
          (scrollbarWidth / 2) + 'px';
    }
    // Having a horizontal scrollbar is much rarer so we don't offset the
    // toolbar from the bottom any more than what the spec says. This means
    // that when there is a scrollbar visible, it will be a full scrollbar
    // width closer to the bottom of the screen than usual, but this is ok.
    this.zoomToolbar_.style.bottom = -horizontalScrollbarWidth + 'px';

    // Update the page indicator.
    var visiblePage = this.viewport_.getMostVisiblePage();

    if (this.toolbar_)
      this.toolbar_.pageNo = visiblePage + 1;

    // TODO(raymes): Give pageIndicator_ the same API as toolbar_.
    if (this.pageIndicator_) {
      this.pageIndicator_.index = visiblePage;
      if (this.documentDimensions_.pageDimensions.length > 1 &&
          hasScrollbars.vertical) {
        this.pageIndicator_.style.visibility = 'visible';
      } else {
        this.pageIndicator_.style.visibility = 'hidden';
      }
    }

    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
    });
  },

  /**
   * 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) {
    if (this.parentWindow_ != message.source) {
      this.parentWindow_ = message.source;
      this.parentOrigin_ = message.origin;
      // Ensure that we notify the embedder if the document is loaded.
      if (this.loadState_ != LoadState.LOADING)
        this.sendDocumentLoadedMessage_();
    }

    if (this.handlePrintPreviewScriptingMessage_(message))
      return;

    // Delay scripting messages from users of the scripting API until the
    // document is loaded. This simplifies use of the APIs.
    if (this.loadState_ != LoadState.SUCCESS) {
      this.delayedScriptingMessages_.push(message);
      return;
    }

    switch (message.data.type.toString()) {
      case 'getSelectedText':
      case 'print':
      case 'selectAll':
        this.plugin_.postMessage(message.data);
        break;
    }
  },

  /**
   * @private
   * Handle scripting messages specific to print preview.
   * @param {MessageObject} message the message to handle.
   * @return {boolean} true if the message was handled, false otherwise.
   */
  handlePrintPreviewScriptingMessage_: function(message) {
    if (!this.isPrintPreview_)
      return false;

    switch (message.data.type.toString()) {
      case 'loadPreviewPage':
        this.plugin_.postMessage(message.data);
        return true;
      case 'resetPrintPreviewMode':
        this.loadState_ = LoadState.LOADING;
        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)
        });
        return true;
      case 'sendKeyEvent':
        this.handleKeyEvent_(DeserializeKeyEvent(message.data.keyEvent));
        return true;
    }

    return false;
  },

  /**
   * @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) {
    if (this.parentWindow_ && this.parentOrigin_) {
      var targetOrigin;
      // Only send data back to the embedder if it is from the same origin,
      // unless we're sending it to ourselves (which could happen in the case
      // of tests). We also allow documentLoaded messages through as this won't
      // leak important information.
      if (this.parentOrigin_ == window.location.origin)
        targetOrigin = this.parentOrigin_;
      else if (message.type == 'documentLoaded')
        targetOrigin = '*';
      else
        targetOrigin = this.originalUrl_;
      this.parentWindow_.postMessage(message, targetOrigin);
    }
  },

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

  /**
   * Each bookmark is an Object containing a:
   * - title
   * - page (optional)
   * - array of children (themselves bookmarks)
   * @type {Array} the top-level bookmarks of the PDF.
   */
  get bookmarks() {
    return this.bookmarks_;
  }
};
// Copyright 2015 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';

/** Idle time in ms before the UI is hidden. */
var HIDE_TIMEOUT = 2000;
/** Time in ms after force hide before toolbar is shown again. */
var FORCE_HIDE_TIMEOUT = 1000;
/**
 * Velocity required in a mousemove to reveal the UI (pixels/ms). This is
 * intended to be high enough that a fast flick of the mouse is required to
 * reach it.
 */
var SHOW_VELOCITY = 10;
/** Distance from the top of the screen required to reveal the toolbars. */
var TOP_TOOLBAR_REVEAL_DISTANCE = 100;
/** Distance from the bottom-right of the screen required to reveal toolbars. */
var SIDE_TOOLBAR_REVEAL_DISTANCE_RIGHT = 150;
var SIDE_TOOLBAR_REVEAL_DISTANCE_BOTTOM = 250;



/**
 * @param {MouseEvent} e Event to test.
 * @return {boolean} True if the mouse is close to the top of the screen.
 */
function isMouseNearTopToolbar(e) {
  return e.y < TOP_TOOLBAR_REVEAL_DISTANCE;
}

/**
 * @param {MouseEvent} e Event to test.
 * @param {Window} window Window to test against.
 * @return {boolean} True if the mouse is close to the bottom-right of the
 * screen.
 */
function isMouseNearSideToolbar(e, window) {
  var atSide = e.x > window.innerWidth - SIDE_TOOLBAR_REVEAL_DISTANCE_RIGHT;
  if (isRTL())
    atSide = e.x < SIDE_TOOLBAR_REVEAL_DISTANCE_RIGHT;
  var atBottom = e.y > window.innerHeight - SIDE_TOOLBAR_REVEAL_DISTANCE_BOTTOM;
  return atSide && atBottom;
}

/**
 * Constructs a Toolbar Manager, responsible for co-ordinating between multiple
 * toolbar elements.
 * @constructor
 * @param {Object} window The window containing the UI.
 * @param {Object} toolbar The top toolbar element.
 * @param {Object} zoomToolbar The zoom toolbar element.
 */
function ToolbarManager(window, toolbar, zoomToolbar) {
  this.window_ = window;
  this.toolbar_ = toolbar;
  this.zoomToolbar_ = zoomToolbar;

  this.toolbarTimeout_ = null;
  this.isMouseNearTopToolbar_ = false;
  this.isMouseNearSideToolbar_ = false;

  this.sideToolbarAllowedOnly_ = false;
  this.sideToolbarAllowedOnlyTimer_ = null;

  this.keyboardNavigationActive = false;

  this.lastMovementTimestamp = null;

  this.window_.addEventListener('resize', this.resizeDropdowns_.bind(this));
  this.resizeDropdowns_();
}

ToolbarManager.prototype = {

  handleMouseMove: function(e) {
    this.isMouseNearTopToolbar_ = this.toolbar_ && isMouseNearTopToolbar(e);
    this.isMouseNearSideToolbar_ = isMouseNearSideToolbar(e, this.window_);

    this.keyboardNavigationActive = false;
    var touchInteractionActive =
        (e.sourceCapabilities && e.sourceCapabilities.firesTouchEvents);

    // Allow the top toolbar to be shown if the mouse moves away from the side
    // toolbar (as long as the timeout has elapsed).
    if (!this.isMouseNearSideToolbar_ && !this.sideToolbarAllowedOnlyTimer_)
      this.sideToolbarAllowedOnly_ = false;

    // Allow the top toolbar to be shown if the mouse moves to the top edge.
    if (this.isMouseNearTopToolbar_)
      this.sideToolbarAllowedOnly_ = false;

    // Tapping the screen with toolbars open tries to close them.
    if (touchInteractionActive && this.zoomToolbar_.isVisible()) {
      this.hideToolbarsIfAllowed();
      return;
    }

    // Show the toolbars if the mouse is near the top or bottom-right of the
    // screen, if the mouse moved fast, or if the touchscreen was tapped.
    if (this.isMouseNearTopToolbar_ || this.isMouseNearSideToolbar_ ||
        this.isHighVelocityMouseMove_(e) || touchInteractionActive) {
      if (this.sideToolbarAllowedOnly_)
        this.zoomToolbar_.show();
      else
        this.showToolbars();
    }
    this.hideToolbarsAfterTimeout();
  },

  /**
   * Whether a mousemove event is high enough velocity to reveal the toolbars.
   * @param {MouseEvent} e Event to test.
   * @return {boolean} true if the event is a high velocity mousemove, false
   * otherwise.
   * @private
   */
  isHighVelocityMouseMove_: function(e) {
    if (e.type == 'mousemove') {
      if (this.lastMovementTimestamp == null) {
        this.lastMovementTimestamp = this.getCurrentTimestamp_();
      } else {
        var movement =
            Math.sqrt(e.movementX * e.movementX + e.movementY * e.movementY);
        var newTime = this.getCurrentTimestamp_();
        var interval = newTime - this.lastMovementTimestamp;
        this.lastMovementTimestamp = newTime;

        if (interval != 0)
          return movement / interval > SHOW_VELOCITY;
      }
    }
    return false;
  },

  /**
   * Wrapper around Date.now() to make it easily replaceable for testing.
   * @return {int}
   * @private
   */
  getCurrentTimestamp_: function() {
    return Date.now();
  },

  /**
   * Display both UI toolbars.
   */
  showToolbars: function() {
    if (this.toolbar_)
      this.toolbar_.show();
    this.zoomToolbar_.show();
  },

  /**
   * Show toolbars and mark that navigation is being performed with
   * tab/shift-tab. This disables toolbar hiding until the mouse is moved or
   * escape is pressed.
   */
  showToolbarsForKeyboardNavigation: function() {
    this.keyboardNavigationActive = true;
    this.showToolbars();
  },

  /**
   * Hide toolbars after a delay, regardless of the position of the mouse.
   * Intended to be called when the mouse has moved out of the parent window.
   */
  hideToolbarsForMouseOut: function() {
    this.isMouseNearTopToolbar_ = false;
    this.isMouseNearSideToolbar_ = false;
    this.hideToolbarsAfterTimeout();
  },

  /**
   * Check if the toolbars are able to be closed, and close them if they are.
   * Toolbars may be kept open based on mouse/keyboard activity and active
   * elements.
   */
  hideToolbarsIfAllowed: function() {
    if (this.isMouseNearSideToolbar_ || this.isMouseNearTopToolbar_)
      return;

    if (this.toolbar_ && this.toolbar_.shouldKeepOpen())
      return;

    if (this.keyboardNavigationActive)
      return;

    // Remove focus to make any visible tooltips disappear -- otherwise they'll
    // still be visible on screen when the toolbar is off screen.
    if ((this.toolbar_ && document.activeElement == this.toolbar_) ||
        document.activeElement == this.zoomToolbar_) {
      document.activeElement.blur();
    }

    if (this.toolbar_)
      this.toolbar_.hide();
    this.zoomToolbar_.hide();
  },

  /**
   * Hide the toolbar after the HIDE_TIMEOUT has elapsed.
   */
  hideToolbarsAfterTimeout: function() {
    if (this.toolbarTimeout_)
      this.window_.clearTimeout(this.toolbarTimeout_);
    this.toolbarTimeout_ = this.window_.setTimeout(
        this.hideToolbarsIfAllowed.bind(this), HIDE_TIMEOUT);
  },

  /**
   * Hide the 'topmost' layer of toolbars. Hides any dropdowns that are open, or
   * hides the basic toolbars otherwise.
   */
  hideSingleToolbarLayer: function() {
    if (!this.toolbar_ || !this.toolbar_.hideDropdowns()) {
      this.keyboardNavigationActive = false;
      this.hideToolbarsIfAllowed();
    }
  },

  /**
   * Hide the top toolbar and keep it hidden until both:
   * - The mouse is moved away from the right side of the screen
   * - 1 second has passed.
   *
   * The top toolbar can be immediately re-opened by moving the mouse to the top
   * of the screen.
   */
  forceHideTopToolbar: function() {
    if (!this.toolbar_)
      return;
    this.toolbar_.hide();
    this.sideToolbarAllowedOnly_ = true;
    this.sideToolbarAllowedOnlyTimer_ = this.window_.setTimeout(function() {
      this.sideToolbarAllowedOnlyTimer_ = null;
    }.bind(this), FORCE_HIDE_TIMEOUT);
  },

  /**
   * Updates the size of toolbar dropdowns based on the positions of the rest of
   * the UI.
   * @private
   */
  resizeDropdowns_: function() {
    if (!this.toolbar_)
      return;
    var lowerBound = this.window_.innerHeight - this.zoomToolbar_.clientHeight;
    this.toolbar_.setDropdownLowerBound(lowerBound);
  }
};
// Copyright 2014 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.

/**
 * Returns the height of the intersection of two rectangles.
 * @param {Object} rect1 the first rect
 * @param {Object} rect2 the second rect
 * @return {number} the height of the intersection of the rects
 */
function getIntersectionHeight(rect1, rect2) {
  return Math.max(0,
      Math.min(rect1.y + rect1.height, rect2.y + rect2.height) -
      Math.max(rect1.y, rect2.y));
}

/**
 * Makes sure that the scale level doesn't get out of the limits.
 * @param {number} scale The new scale level.
 * @return {number} The scale clamped within the limits.
 */
function clampScale(scale) {
  return Math.min(5, Math.max(0.25, scale));
}

/**
 * Computes vector between two points.
 * @param {!Object} p1 The first point.
 * @param {!Object} p2 The second point.
 * @return {!Object} The vector.
 */
function vectorDelta(p1, p2) {
  return {
    x: p2.x - p1.x,
    y: p2.y - p1.y
  };
}

function frameToPluginCoordinate(coordinateInFrame) {
  var container = $('plugin');
  return {
    x: coordinateInFrame.x - container.getBoundingClientRect().left,
    y: coordinateInFrame.y - container.getBoundingClientRect().top
  };
}

/**
 * Create a new viewport.
 * @constructor
 * @param {Window} window the window
 * @param {Object} sizer is the element which represents the size of the
 *     document in the viewport
 * @param {Function} viewportChangedCallback is run when the viewport changes
 * @param {Function} beforeZoomCallback is run before a change in zoom
 * @param {Function} afterZoomCallback is run after a change in zoom
 * @param {number} scrollbarWidth the width of scrollbars on the page
 * @param {number} defaultZoom The default zoom level.
 * @param {number} topToolbarHeight The number of pixels that should initially
 *     be left blank above the document for the toolbar.
 */
function Viewport(window,
                  sizer,
                  viewportChangedCallback,
                  beforeZoomCallback,
                  afterZoomCallback,
                  scrollbarWidth,
                  defaultZoom,
                  topToolbarHeight) {
  this.window_ = window;
  this.sizer_ = sizer;
  this.viewportChangedCallback_ = viewportChangedCallback;
  this.beforeZoomCallback_ = beforeZoomCallback;
  this.afterZoomCallback_ = afterZoomCallback;
  this.allowedToChangeZoom_ = false;
  this.zoom_ = 1;
  this.documentDimensions_ = null;
  this.pageDimensions_ = [];
  this.scrollbarWidth_ = scrollbarWidth;
  this.fittingType_ = Viewport.FittingType.NONE;
  this.defaultZoom_ = defaultZoom;
  this.topToolbarHeight_ = topToolbarHeight;
  this.prevScale_ = 1;
  this.pinchPhase_ = Viewport.PinchPhase.PINCH_NONE;
  this.pinchPanVector_ = null;
  this.pinchCenter_ = null;
  this.firstPinchCenterInFrame_ = null;

  window.addEventListener('scroll', this.updateViewport_.bind(this));
  window.addEventListener('resize', this.resize_.bind(this));
}

/**
 * Enumeration of page fitting types.
 * @enum {string}
 */
Viewport.FittingType = {
  NONE: 'none',
  FIT_TO_PAGE: 'fit-to-page',
  FIT_TO_WIDTH: 'fit-to-width'
};

/**
 * Enumeration of pinch states.
 * This should match PinchPhase enum in pdf/out_of_process_instance.h
 * @enum {number}
 */
Viewport.PinchPhase = {
  PINCH_NONE: 0,
  PINCH_START: 1,
  PINCH_UPDATE_ZOOM_OUT: 2,
  PINCH_UPDATE_ZOOM_IN: 3,
  PINCH_END: 4
};

/**
 * The increment to scroll a page by in pixels when up/down/left/right arrow
 * keys are pressed. Usually we just let the browser handle scrolling on the
 * window when these keys are pressed but in certain cases we need to simulate
 * these events.
 */
Viewport.SCROLL_INCREMENT = 40;

/**
 * Predefined zoom factors to be used when zooming in/out. These are in
 * ascending order. This should match the lists in
 * components/ui/zoom/page_zoom_constants.h and
 * chrome/browser/resources/settings/appearance_page/appearance_page.js
 */
Viewport.ZOOM_FACTORS = [0.25, 1 / 3, 0.5, 2 / 3, 0.75, 0.8, 0.9,
                         1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5];

/**
 * The minimum and maximum range to be used to clip zoom factor.
 */
Viewport.ZOOM_FACTOR_RANGE = {
  min: Viewport.ZOOM_FACTORS[0],
  max: Viewport.ZOOM_FACTORS[Viewport.ZOOM_FACTORS.length - 1]
};

/**
 * The width of the page shadow around pages in pixels.
 */
Viewport.PAGE_SHADOW = {top: 3, bottom: 7, left: 5, right: 5};

Viewport.prototype = {
  /**
   * Returns the zoomed and rounded document dimensions for the given zoom.
   * Rounding is necessary when interacting with the renderer which tends to
   * operate in integral values (for example for determining if scrollbars
   * should be shown).
   * @param {number} zoom The zoom to use to compute the scaled dimensions.
   * @return {Object} A dictionary with scaled 'width'/'height' of the document.
   * @private
   */
  getZoomedDocumentDimensions_: function(zoom) {
    if (!this.documentDimensions_)
      return null;
    return {
      width: Math.round(this.documentDimensions_.width * zoom),
      height: Math.round(this.documentDimensions_.height * zoom)
    };
  },

  /**
   * @private
   * Returns true if the document needs scrollbars at the given zoom level.
   * @param {number} zoom compute whether scrollbars are needed at this zoom
   * @return {Object} with 'horizontal' and 'vertical' keys which map to bool
   *     values indicating if the horizontal and vertical scrollbars are needed
   *     respectively.
   */
  documentNeedsScrollbars_: function(zoom) {
    var zoomedDimensions = this.getZoomedDocumentDimensions_(zoom);
    if (!zoomedDimensions) {
      return {
        horizontal: false,
        vertical: false
      };
    }

    // If scrollbars are required for one direction, expand the document in the
    // other direction to take the width of the scrollbars into account when
    // deciding whether the other direction needs scrollbars.
    if (zoomedDimensions.width > this.window_.innerWidth)
      zoomedDimensions.height += this.scrollbarWidth_;
    else if (zoomedDimensions.height > this.window_.innerHeight)
      zoomedDimensions.width += this.scrollbarWidth_;
    return {
      horizontal: zoomedDimensions.width > this.window_.innerWidth,
      vertical: zoomedDimensions.height + this.topToolbarHeight_ >
          this.window_.innerHeight
    };
  },

  /**
   * Returns true if the document needs scrollbars at the current zoom level.
   * @return {Object} with 'x' and 'y' keys which map to bool values
   *     indicating if the horizontal and vertical scrollbars are needed
   *     respectively.
   */
  documentHasScrollbars: function() {
    return this.documentNeedsScrollbars_(this.zoom_);
  },

  /**
   * @private
   * Helper function called when the zoomed document size changes.
   */
  contentSizeChanged_: function() {
    var zoomedDimensions = this.getZoomedDocumentDimensions_(this.zoom_);
    if (zoomedDimensions) {
      this.sizer_.style.width = zoomedDimensions.width + 'px';
      this.sizer_.style.height = zoomedDimensions.height +
          this.topToolbarHeight_ + 'px';
    }
  },

  /**
   * @private
   * Called when the viewport should be updated.
   */
  updateViewport_: function() {
    this.viewportChangedCallback_();
  },

  /**
   * @private
   * Called when the viewport size changes.
   */
  resize_: function() {
    if (this.fittingType_ == Viewport.FittingType.FIT_TO_PAGE)
      this.fitToPageInternal_(false);
    else if (this.fittingType_ == Viewport.FittingType.FIT_TO_WIDTH)
      this.fitToWidth();
    else
      this.updateViewport_();
  },

  /**
   * @type {Object} the scroll position of the viewport.
   */
  get position() {
    return {
      x: this.window_.pageXOffset,
      y: this.window_.pageYOffset - this.topToolbarHeight_
    };
  },

  /**
   * Scroll the viewport to the specified position.
   * @type {Object} position the position to scroll to.
   */
  set position(position) {
    this.window_.scrollTo(position.x, position.y + this.topToolbarHeight_);
  },

  /**
   * @type {Object} the size of the viewport excluding scrollbars.
   */
  get size() {
    var needsScrollbars = this.documentNeedsScrollbars_(this.zoom_);
    var scrollbarWidth = needsScrollbars.vertical ? this.scrollbarWidth_ : 0;
    var scrollbarHeight = needsScrollbars.horizontal ? this.scrollbarWidth_ : 0;
    return {
      width: this.window_.innerWidth - scrollbarWidth,
      height: this.window_.innerHeight - scrollbarHeight
    };
  },

  /**
   * @type {number} the zoom level of the viewport.
   */
  get zoom() {
    return this.zoom_;
  },

  /**
   * @type {Viewport.PinchPhase} The phase of the current pinch gesture for
   *    the viewport.
   */
  get pinchPhase() {
    return this.pinchPhase_;
  },

  /**
   * @type {Object} The panning caused by the current pinch gesture (as
   *    the deltas of the x and y coordinates).
   */
  get pinchPanVector() {
    return this.pinchPanVector_;
  },

  /**
   * @type {Object} The coordinates of the center of the current pinch gesture.
   */
  get pinchCenter() {
    return this.pinchCenter_;
  },

  /**
   * @private
   * Used to wrap a function that might perform zooming on the viewport. This is
   * required so that we can notify the plugin that zooming is in progress
   * so that while zooming is taking place it can stop reacting to scroll events
   * from the viewport. This is to avoid flickering.
   */
  mightZoom_: function(f) {
    this.beforeZoomCallback_();
    this.allowedToChangeZoom_ = true;
    f();
    this.allowedToChangeZoom_ = false;
    this.afterZoomCallback_();
  },

  /**
   * @private
   * Sets the zoom of the viewport.
   * @param {number} newZoom the zoom level to zoom to.
   */
  setZoomInternal_: function(newZoom) {
    if (!this.allowedToChangeZoom_) {
      throw 'Called Viewport.setZoomInternal_ without calling ' +
            'Viewport.mightZoom_.';
    }
    // Record the scroll position (relative to the top-left of the window).
    var currentScrollPos = {
      x: this.position.x / this.zoom_,
      y: this.position.y / this.zoom_
    };
    this.zoom_ = newZoom;
    this.contentSizeChanged_();
    // Scroll to the scaled scroll position.
    this.position = {
      x: currentScrollPos.x * newZoom,
      y: currentScrollPos.y * newZoom
    };
  },

  /**
   * @private
   * Sets the zoom of the viewport.
   * Same as setZoomInternal_ but for pinch zoom we have some more operations.
   * @param {number} scaleDelta The zoom delta.
   * @param {!Object} center The pinch center in content coordinates.
   */
  setPinchZoomInternal_: function(scaleDelta, center) {
    assert(this.allowedToChangeZoom_,
        'Called Viewport.setPinchZoomInternal_ without calling ' +
        'Viewport.mightZoom_.');
    this.zoom_ = clampScale(this.zoom_ * scaleDelta);

    var newCenterInContent = this.frameToContent(center);
    var delta = {
      x: (newCenterInContent.x - this.oldCenterInContent.x),
      y: (newCenterInContent.y - this.oldCenterInContent.y)
    };

    // Record the scroll position (relative to the pinch center).
    var currentScrollPos = {
      x: this.position.x - delta.x * this.zoom_,
      y: this.position.y - delta.y * this.zoom_
    };

    this.contentSizeChanged_();
    // Scroll to the scaled scroll position.
    this.position = {
      x: currentScrollPos.x,
      y: currentScrollPos.y
    };
  },

  /**
   *  @private
   *  Converts a point from frame to content coordinates.
   *  @param {!Object} framePoint The frame coordinates.
   *  @return {!Object} The content coordinates.
   */
  frameToContent: function(framePoint) {
    // TODO(mcnee) Add a helper Point class to avoid duplicating operations
    // on plain {x,y} objects.
    return {
      x: (framePoint.x + this.position.x) / this.zoom_,
      y: (framePoint.y + this.position.y) / this.zoom_
    };
  },

  /**
   * Sets the zoom to the given zoom level.
   * @param {number} newZoom the zoom level to zoom to.
   */
  setZoom: function(newZoom) {
    this.fittingType_ = Viewport.FittingType.NONE;
    newZoom = Math.max(Viewport.ZOOM_FACTOR_RANGE.min,
                       Math.min(newZoom, Viewport.ZOOM_FACTOR_RANGE.max));
    this.mightZoom_(function() {
      this.setZoomInternal_(newZoom);
      this.updateViewport_();
    }.bind(this));
  },

  /**
   * @type {number} the width of scrollbars in the viewport in pixels.
   */
  get scrollbarWidth() {
    return this.scrollbarWidth_;
  },

  /**
   * @type {Viewport.FittingType} the fitting type the viewport is currently in.
   */
  get fittingType() {
    return this.fittingType_;
  },

  /**
   * @private
   * @param {integer} y the y-coordinate to get the page at.
   * @return {integer} the index of a page overlapping the given y-coordinate.
   */
  getPageAtY_: function(y) {
    var min = 0;
    var max = this.pageDimensions_.length - 1;
    while (max >= min) {
      var page = Math.floor(min + ((max - min) / 2));
      // There might be a gap between the pages, in which case use the bottom
      // of the previous page as the top for finding the page.
      var top = 0;
      if (page > 0) {
        top = this.pageDimensions_[page - 1].y +
            this.pageDimensions_[page - 1].height;
      }
      var bottom = this.pageDimensions_[page].y +
          this.pageDimensions_[page].height;

      if (top <= y && bottom > y)
        return page;
      else if (top > y)
        max = page - 1;
      else
        min = page + 1;
    }
    return 0;
  },

  /**
   * Returns the page with the greatest proportion of its height in the current
   * viewport.
   * @return {int} the index of the most visible page.
   */
  getMostVisiblePage: function() {
    var firstVisiblePage = this.getPageAtY_(this.position.y / this.zoom_);
    if (firstVisiblePage == this.pageDimensions_.length - 1)
      return firstVisiblePage;

    var viewportRect = {
      x: this.position.x / this.zoom_,
      y: this.position.y / this.zoom_,
      width: this.size.width / this.zoom_,
      height: this.size.height / this.zoom_
    };
    var firstVisiblePageVisibility = getIntersectionHeight(
        this.pageDimensions_[firstVisiblePage], viewportRect) /
        this.pageDimensions_[firstVisiblePage].height;
    var nextPageVisibility = getIntersectionHeight(
        this.pageDimensions_[firstVisiblePage + 1], viewportRect) /
        this.pageDimensions_[firstVisiblePage + 1].height;
    if (nextPageVisibility > firstVisiblePageVisibility)
      return firstVisiblePage + 1;
    return firstVisiblePage;
  },

  /**
   * @private
   * Compute the zoom level for fit-to-page or fit-to-width. |pageDimensions| is
   * the dimensions for a given page and if |widthOnly| is true, it indicates
   * that fit-to-page zoom should be computed rather than fit-to-page.
   * @param {Object} pageDimensions the dimensions of a given page
   * @param {boolean} widthOnly a bool indicating whether fit-to-page or
   *     fit-to-width should be computed.
   * @return {number} the zoom to use
   */
  computeFittingZoom_: function(pageDimensions, widthOnly) {
    // First compute the zoom without scrollbars.
    var zoomWidth = this.window_.innerWidth / pageDimensions.width;
    var zoom;
    var zoomHeight;
    if (widthOnly) {
      zoom = zoomWidth;
    } else {
      zoomHeight = this.window_.innerHeight / pageDimensions.height;
      zoom = Math.min(zoomWidth, zoomHeight);
    }
    // Check if there needs to be any scrollbars.
    var needsScrollbars = this.documentNeedsScrollbars_(zoom);

    // If the document fits, just return the zoom.
    if (!needsScrollbars.horizontal && !needsScrollbars.vertical)
      return zoom;

    var zoomedDimensions = this.getZoomedDocumentDimensions_(zoom);

    // Check if adding a scrollbar will result in needing the other scrollbar.
    var scrollbarWidth = this.scrollbarWidth_;
    if (needsScrollbars.horizontal &&
        zoomedDimensions.height > this.window_.innerHeight - scrollbarWidth) {
      needsScrollbars.vertical = true;
    }
    if (needsScrollbars.vertical &&
        zoomedDimensions.width > this.window_.innerWidth - scrollbarWidth) {
      needsScrollbars.horizontal = true;
    }

    // Compute available window space.
    var windowWithScrollbars = {
      width: this.window_.innerWidth,
      height: this.window_.innerHeight
    };
    if (needsScrollbars.horizontal)
      windowWithScrollbars.height -= scrollbarWidth;
    if (needsScrollbars.vertical)
      windowWithScrollbars.width -= scrollbarWidth;

    // Recompute the zoom.
    zoomWidth = windowWithScrollbars.width / pageDimensions.width;
    if (widthOnly) {
      zoom = zoomWidth;
    } else {
      zoomHeight = windowWithScrollbars.height / pageDimensions.height;
      zoom = Math.min(zoomWidth, zoomHeight);
    }
    return zoom;
  },

  /**
   * Zoom the viewport so that the page-width consumes the entire viewport.
   */
  fitToWidth: function() {
    this.mightZoom_(function() {
      this.fittingType_ = Viewport.FittingType.FIT_TO_WIDTH;
      if (!this.documentDimensions_)
        return;
      // When computing fit-to-width, the maximum width of a page in the
      // document is used, which is equal to the size of the document width.
      this.setZoomInternal_(this.computeFittingZoom_(this.documentDimensions_,
                                                     true));
      var page = this.getMostVisiblePage();
      this.updateViewport_();
    }.bind(this));
  },

  /**
   * @private
   * Zoom the viewport so that a page consumes the entire viewport.
   * @param {boolean} scrollToTopOfPage Set to true if the viewport should be
   *     scrolled to the top of the current page. Set to false if the viewport
   *     should remain at the current scroll position.
   */
  fitToPageInternal_: function(scrollToTopOfPage) {
    this.mightZoom_(function() {
      this.fittingType_ = Viewport.FittingType.FIT_TO_PAGE;
      if (!this.documentDimensions_)
        return;
      var page = this.getMostVisiblePage();
      // Fit to the current page's height and the widest page's width.
      var dimensions = {
        width: this.documentDimensions_.width,
        height: this.pageDimensions_[page].height,
      };
      this.setZoomInternal_(this.computeFittingZoom_(dimensions, false));
      if (scrollToTopOfPage) {
        this.position = {
          x: 0,
          y: this.pageDimensions_[page].y * this.zoom_
        };
      }
      this.updateViewport_();
    }.bind(this));
  },

  /**
   * Zoom the viewport so that a page consumes the entire viewport. Also scrolls
   * the viewport to the top of the current page.
   */
  fitToPage: function() {
    this.fitToPageInternal_(true);
  },

  /**
   * Zoom out to the next predefined zoom level.
   */
  zoomOut: function() {
    this.mightZoom_(function() {
      this.fittingType_ = Viewport.FittingType.NONE;
      var nextZoom = Viewport.ZOOM_FACTORS[0];
      for (var i = 0; i < Viewport.ZOOM_FACTORS.length; i++) {
        if (Viewport.ZOOM_FACTORS[i] < this.zoom_)
          nextZoom = Viewport.ZOOM_FACTORS[i];
      }
      this.setZoomInternal_(nextZoom);
      this.updateViewport_();
    }.bind(this));
  },

  /**
   * Zoom in to the next predefined zoom level.
   */
  zoomIn: function() {
    this.mightZoom_(function() {
      this.fittingType_ = Viewport.FittingType.NONE;
      var nextZoom = Viewport.ZOOM_FACTORS[Viewport.ZOOM_FACTORS.length - 1];
      for (var i = Viewport.ZOOM_FACTORS.length - 1; i >= 0; i--) {
        if (Viewport.ZOOM_FACTORS[i] > this.zoom_)
          nextZoom = Viewport.ZOOM_FACTORS[i];
      }
      this.setZoomInternal_(nextZoom);
      this.updateViewport_();
    }.bind(this));
  },

  /**
   * Pinch zoom event handler.
   * @param {!Object} e The pinch event.
   */
  pinchZoom: function(e) {
    this.mightZoom_(function() {
      this.pinchPhase_ = e.direction == 'out' ?
                         Viewport.PinchPhase.PINCH_UPDATE_ZOOM_OUT :
                         Viewport.PinchPhase.PINCH_UPDATE_ZOOM_IN;

      var scaleDelta = e.startScaleRatio / this.prevScale_;
      this.pinchPanVector_ =
          vectorDelta(e.center, this.firstPinchCenterInFrame_);

      var needsScrollbars = this.documentNeedsScrollbars_(
          clampScale(this.zoom_ * scaleDelta));

      this.pinchCenter_ = e.center;

      // If there's no horizontal scrolling, keep the content centered so the
      // user can't zoom in on the non-content area.
      // TODO(mcnee) Investigate other ways of scaling when we don't have
      // horizontal scrolling. We want to keep the document centered,
      // but this causes a potentially awkward transition when we start
      // using the gesture center.
      if (!needsScrollbars.horizontal) {
        this.pinchCenter_ = {
          x: this.window_.innerWidth / 2,
          y: this.window_.innerHeight / 2
        };
      } else if (this.keepContentCentered_) {
        this.oldCenterInContent =
            this.frameToContent(frameToPluginCoordinate(e.center));
        this.keepContentCentered_ = false;
      }

      this.setPinchZoomInternal_(
          scaleDelta, frameToPluginCoordinate(e.center));
      this.updateViewport_();
      this.prevScale_ = e.startScaleRatio;
    }.bind(this));
  },

  pinchZoomStart: function(e) {
    this.pinchPhase_ = Viewport.PinchPhase.PINCH_START;
    this.prevScale_ = 1;
    this.oldCenterInContent =
        this.frameToContent(frameToPluginCoordinate(e.center));

    var needsScrollbars = this.documentNeedsScrollbars_(this.zoom_);
    this.keepContentCentered_ = !needsScrollbars.horizontal;
    // We keep track of begining of the pinch.
    // By doing so we will be able to compute the pan distance.
    this.firstPinchCenterInFrame_ = e.center;
  },

  pinchZoomEnd: function(e) {
    this.mightZoom_(function() {
      this.pinchPhase_ = Viewport.PinchPhase.PINCH_END;
      var scaleDelta = e.startScaleRatio / this.prevScale_;
      this.pinchCenter_ = e.center;

      this.setPinchZoomInternal_(
          scaleDelta, frameToPluginCoordinate(e.center));
      this.updateViewport_();
    }.bind(this));

    this.pinchPhase_ = Viewport.PinchPhase.PINCH_NONE;
    this.pinchPanVector_ = null;
    this.pinchCenter_ = null;
    this.firstPinchCenterInFrame_ = null;
  },

  /**
   * Go to the given page index.
   * @param {number} page the index of the page to go to. zero-based.
   */
  goToPage: function(page) {
    this.mightZoom_(function() {
      if (this.pageDimensions_.length === 0)
        return;
      if (page < 0)
        page = 0;
      if (page >= this.pageDimensions_.length)
        page = this.pageDimensions_.length - 1;
      var dimensions = this.pageDimensions_[page];
      var toolbarOffset = 0;
      // Unless we're in fit to page mode, scroll above the page by
      // |this.topToolbarHeight_| so that the toolbar isn't covering it
      // initially.
      if (this.fittingType_ != Viewport.FittingType.FIT_TO_PAGE)
        toolbarOffset = this.topToolbarHeight_;
      this.position = {
        x: dimensions.x * this.zoom_,
        y: dimensions.y * this.zoom_ - toolbarOffset
      };
      this.updateViewport_();
    }.bind(this));
  },

  /**
   * Set the dimensions of the document.
   * @param {Object} documentDimensions the dimensions of the document
   */
  setDocumentDimensions: function(documentDimensions) {
    this.mightZoom_(function() {
      var initialDimensions = !this.documentDimensions_;
      this.documentDimensions_ = documentDimensions;
      this.pageDimensions_ = this.documentDimensions_.pageDimensions;
      if (initialDimensions) {
        this.setZoomInternal_(
            Math.min(this.defaultZoom_,
                     this.computeFittingZoom_(this.documentDimensions_, true)));
        this.position = {
          x: 0,
          y: -this.topToolbarHeight_
        };
      }
      this.contentSizeChanged_();
      this.resize_();
    }.bind(this));
  },

  /**
   * Get the coordinates of the page contents (excluding the page shadow)
   * relative to the screen.
   * @param {number} page the index of the page to get the rect for.
   * @return {Object} a rect representing the page in screen coordinates.
   */
  getPageScreenRect: function(page) {
    if (!this.documentDimensions_) {
      return {
        x: 0,
        y: 0,
        width: 0,
        height: 0
      };
    }
    if (page >= this.pageDimensions_.length)
      page = this.pageDimensions_.length - 1;

    var pageDimensions = this.pageDimensions_[page];

    // Compute the page dimensions minus the shadows.
    var insetDimensions = {
      x: pageDimensions.x + Viewport.PAGE_SHADOW.left,
      y: pageDimensions.y + Viewport.PAGE_SHADOW.top,
      width: pageDimensions.width - Viewport.PAGE_SHADOW.left -
          Viewport.PAGE_SHADOW.right,
      height: pageDimensions.height - Viewport.PAGE_SHADOW.top -
          Viewport.PAGE_SHADOW.bottom
    };

    // Compute the x-coordinate of the page within the document.
    // TODO(raymes): This should really be set when the PDF plugin passes the
    // page coordinates, but it isn't yet.
    var x = (this.documentDimensions_.width - pageDimensions.width) / 2 +
        Viewport.PAGE_SHADOW.left;
    // Compute the space on the left of the document if the document fits
    // completely in the screen.
    var spaceOnLeft = (this.size.width -
        this.documentDimensions_.width * this.zoom_) / 2;
    spaceOnLeft = Math.max(spaceOnLeft, 0);

    return {
      x: x * this.zoom_ + spaceOnLeft - this.window_.pageXOffset,
      y: insetDimensions.y * this.zoom_ - this.window_.pageYOffset,
      width: insetDimensions.width * this.zoom_,
      height: insetDimensions.height * this.zoom_
    };
  }
};
// Copyright 2014 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';

/**
 * Creates a new OpenPDFParamsParser. This parses the open pdf parameters
 * passed in the url to set initial viewport settings for opening the pdf.
 * @param {Object} getNamedDestinationsFunction The function called to fetch
 *     the page number for a named destination.
 */
function OpenPDFParamsParser(getNamedDestinationsFunction) {
  this.outstandingRequests_ = [];
  this.getNamedDestinationsFunction_ = getNamedDestinationsFunction;
}

OpenPDFParamsParser.prototype = {
  /**
   * @private
   * Parse zoom parameter of open PDF parameters. If this
   * parameter is passed while opening PDF then PDF should be opened
   * at the specified zoom level.
   * @param {number} zoom value.
   * @param {Object} viewportPosition to store zoom and position value.
   */
  parseZoomParam_: function(paramValue, viewportPosition) {
    var paramValueSplit = paramValue.split(',');
    if ((paramValueSplit.length != 1) && (paramValueSplit.length != 3))
      return;

    // User scale of 100 means zoom value of 100% i.e. zoom factor of 1.0.
    var zoomFactor = parseFloat(paramValueSplit[0]) / 100;
    if (isNaN(zoomFactor))
      return;

    // Handle #zoom=scale.
    if (paramValueSplit.length == 1) {
      viewportPosition['zoom'] = zoomFactor;
      return;
    }

    // Handle #zoom=scale,left,top.
    var position = {x: parseFloat(paramValueSplit[1]),
                    y: parseFloat(paramValueSplit[2])};
    viewportPosition['position'] = position;
    viewportPosition['zoom'] = zoomFactor;
  },

  /**
   * Parse the parameters encoded in the fragment of a URL into a dictionary.
   * @private
   * @param {string} url to parse
   * @return {Object} Key-value pairs of URL parameters
   */
  parseUrlParams_: function(url) {
    var params = {};

    var paramIndex = url.search('#');
    if (paramIndex == -1)
      return params;

    var paramTokens = url.substring(paramIndex + 1).split('&');
    if ((paramTokens.length == 1) && (paramTokens[0].search('=') == -1)) {
      // Handle the case of http://foo.com/bar#NAMEDDEST. This is not
      // explicitly mentioned except by example in the Adobe
      // "PDF Open Parameters" document.
      params['nameddest'] = paramTokens[0];
      return params;
    }

    for (var i = 0; i < paramTokens.length; ++i) {
      var keyValueSplit = paramTokens[i].split('=');
      if (keyValueSplit.length != 2)
        continue;
      params[keyValueSplit[0]] = keyValueSplit[1];
    }

    return params;
  },

  /**
   * Parse PDF url parameters used for controlling the state of UI. These need
   * to be available when the UI is being initialized, rather than when the PDF
   * is finished loading.
   * @param {string} url that needs to be parsed.
   * @return {Object} parsed url parameters.
   */
  getUiUrlParams: function(url) {
    var params = this.parseUrlParams_(url);
    var uiParams = {toolbar: true};

    if ('toolbar' in params && params['toolbar'] == 0)
      uiParams.toolbar = false;

    return uiParams;
  },

  /**
   * Parse PDF url parameters. These parameters are mentioned in the url
   * and specify actions to be performed when opening pdf files.
   * See http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/
   * pdfs/pdf_open_parameters.pdf for details.
   * @param {string} url that needs to be parsed.
   * @param {Function} callback function to be called with viewport info.
   */
  getViewportFromUrlParams: function(url, callback) {
    var viewportPosition = {};
    viewportPosition['url'] = url;

    var paramsDictionary = this.parseUrlParams_(url);

    if ('page' in paramsDictionary) {
      // |pageNumber| is 1-based, but goToPage() take a zero-based page number.
      var pageNumber = parseInt(paramsDictionary['page']);
      if (!isNaN(pageNumber) && pageNumber > 0)
        viewportPosition['page'] = pageNumber - 1;
    }

    if ('zoom' in paramsDictionary)
      this.parseZoomParam_(paramsDictionary['zoom'], viewportPosition);

    if (viewportPosition.page === undefined &&
        'nameddest' in paramsDictionary) {
      this.outstandingRequests_.push({
        callback: callback,
        viewportPosition: viewportPosition
      });
      this.getNamedDestinationsFunction_(paramsDictionary['nameddest']);
    } else {
      callback(viewportPosition);
    }
  },

  /**
   * This is called when a named destination is received and the page number
   * corresponding to the request for which a named destination is passed.
   * @param {number} pageNumber The page corresponding to the named destination
   *    requested.
   */
  onNamedDestinationReceived: function(pageNumber) {
    var outstandingRequest = this.outstandingRequests_.shift();
    if (pageNumber != -1)
      outstandingRequest.viewportPosition.page = pageNumber;
    outstandingRequest.callback(outstandingRequest.viewportPosition);
  },
};
// Copyright 2015 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';

/**
 * Creates a new NavigatorDelegate for calling browser-specific functions to
 * do the actual navigating.
 * @param {boolean} isInTab Indicates if the PDF viewer is displayed in a tab.
 */
function NavigatorDelegate(isInTab) {
  this.isInTab_ = isInTab;
}

/**
 * Creates a new Navigator for navigating to links inside or outside the PDF.
 * @param {string} originalUrl The original page URL.
 * @param {Object} viewport The viewport info of the page.
 * @param {Object} paramsParser The object for URL parsing.
 * @param {Object} navigatorDelegate The object with callback functions that
 *    get called when navigation happens in the current tab, a new tab,
 *    and a new window.
 */
function Navigator(originalUrl, viewport, paramsParser, navigatorDelegate) {
  this.originalUrl_ = originalUrl;
  this.viewport_ = viewport;
  this.paramsParser_ = paramsParser;
  this.navigatorDelegate_ = navigatorDelegate;
}

NavigatorDelegate.prototype = {
  /**
   * @public
   * Called when navigation should happen in the current tab.
   * @param {string} url The url to be opened in the current tab.
   */
  navigateInCurrentTab: function(url) {
    // When the PDFviewer is inside a browser tab, prefer the tabs API because
    // it can navigate from one file:// URL to another.
    if (chrome.tabs && this.isInTab_)
      chrome.tabs.update({url: url});
    else
      window.location.href = url;
  },

  /**
   * @public
   * Called when navigation should happen in the new tab.
   * @param {string} url The url to be opened in the new tab.
   * @param {boolean} active Indicates if the new tab should be the active tab.
   */
  navigateInNewTab: function(url, active) {
    // Prefer the tabs API because it guarantees we can just open a new tab.
    // window.open doesn't have this guarantee.
    if (chrome.tabs)
      chrome.tabs.create({url: url, active: active});
    else
      window.open(url);
  },

  /**
   * @public
   * Called when navigation should happen in the new window.
   * @param {string} url The url to be opened in the new window.
   */
  navigateInNewWindow: function(url) {
    // Prefer the windows API because it guarantees we can just open a new
    // window. window.open with '_blank' argument doesn't have this guarantee.
    if (chrome.windows)
      chrome.windows.create({url: url});
    else
      window.open(url, '_blank');
  }
};

/**
 * Represents options when navigating to a new url. C++ counterpart of
 * the enum is in ui/base/window_open_disposition.h. This enum represents
 * the only values that are passed from Plugin.
 * @enum {number}
 */
Navigator.WindowOpenDisposition = {
  CURRENT_TAB: 1,
  NEW_FOREGROUND_TAB: 3,
  NEW_BACKGROUND_TAB: 4,
  NEW_WINDOW: 6,
  SAVE_TO_DISK: 7
};

Navigator.prototype = {
  /**
   * @private
   * Function to navigate to the given URL. This might involve navigating
   * within the PDF page or opening a new url (in the same tab or a new tab).
   * @param {string} url The URL to navigate to.
   * @param {number} disposition The window open disposition when
   *    navigating to the new URL.
   */
  navigate: function(url, disposition) {
    if (url.length == 0)
      return;

    // If |urlFragment| starts with '#', then it's for the same URL with a
    // different URL fragment.
    if (url.charAt(0) == '#') {
      // if '#' is already present in |originalUrl| then remove old fragment
      // and add new url fragment.
      var hashIndex = this.originalUrl_.search('#');
      if (hashIndex != -1)
        url = this.originalUrl_.substring(0, hashIndex) + url;
      else
        url = this.originalUrl_ + url;
    }

    // If there's no scheme, then take a guess at the scheme.
    if (url.indexOf('://') == -1 && url.indexOf('mailto:') == -1)
      url = this.guessUrlWithoutScheme_(url);

    if (!this.isValidUrl_(url))
      return;

    switch (disposition) {
      case Navigator.WindowOpenDisposition.CURRENT_TAB:
        this.paramsParser_.getViewportFromUrlParams(
            url, this.onViewportReceived_.bind(this));
        break;
      case Navigator.WindowOpenDisposition.NEW_BACKGROUND_TAB:
        this.navigatorDelegate_.navigateInNewTab(url, false);
        break;
      case Navigator.WindowOpenDisposition.NEW_FOREGROUND_TAB:
        this.navigatorDelegate_.navigateInNewTab(url, true);
        break;
      case Navigator.WindowOpenDisposition.NEW_WINDOW:
        this.navigatorDelegate_.navigateInNewWindow(url);
        break;
      case Navigator.WindowOpenDisposition.SAVE_TO_DISK:
        // TODO(jaepark): Alt + left clicking a link in PDF should
        // download the link.
        this.paramsParser_.getViewportFromUrlParams(
            url, this.onViewportReceived_.bind(this));
        break;
      default:
        break;
    }
  },

  /**
   * @private
   * Called when the viewport position is received.
   * @param {Object} viewportPosition Dictionary containing the viewport
   *    position.
   */
  onViewportReceived_: function(viewportPosition) {
    var originalUrl = this.originalUrl_;
    var hashIndex = originalUrl.search('#');
    if (hashIndex != -1)
      originalUrl = originalUrl.substring(0, hashIndex);

    var newUrl = viewportPosition.url;
    hashIndex = newUrl.search('#');
    if (hashIndex != -1)
      newUrl = newUrl.substring(0, hashIndex);

    var pageNumber = viewportPosition.page;
    if (pageNumber != undefined && originalUrl == newUrl)
      this.viewport_.goToPage(pageNumber);
    else
      this.navigatorDelegate_.navigateInCurrentTab(viewportPosition.url);
  },

  /**
   * @private
   * Checks if the URL starts with a scheme and is not just a scheme.
   * @param {string} The input URL
   * @return {boolean} Whether the url is valid.
   */
  isValidUrl_: function(url) {
    // Make sure |url| starts with a valid scheme.
    if (!url.startsWith('http://') &&
        !url.startsWith('https://') &&
        !url.startsWith('ftp://') &&
        !url.startsWith('file://') &&
        !url.startsWith('mailto:')) {
      return false;
    }

    // Navigations to file:-URLs are only allowed from file:-URLs.
    if (url.startsWith('file:') && !this.originalUrl_.startsWith('file:'))
      return false;


    // Make sure |url| is not only a scheme.
    if (url == 'http://' ||
        url == 'https://' ||
        url == 'ftp://' ||
        url == 'file://' ||
        url == 'mailto:') {
      return false;
    }

    return true;
  },

  /**
   * @private
   * Attempt to figure out what a URL is when there is no scheme.
   * @param {string} The input URL
   * @return {string} The URL with a scheme or the original URL if it is not
   *     possible to determine the scheme.
   */
  guessUrlWithoutScheme_: function(url) {
    // If the original URL is mailto:, that does not make sense to start with,
    // and neither does adding |url| to it.
    // If the original URL is not a valid URL, this cannot make a valid URL.
    // In both cases, just bail out.
    if (this.originalUrl_.startsWith('mailto:') ||
        !this.isValidUrl_(this.originalUrl_)) {
      return url;
    }

    // Check for absolute paths.
    if (url.startsWith('/')) {
      var schemeEndIndex = this.originalUrl_.indexOf('://');
      var firstSlash = this.originalUrl_.indexOf('/', schemeEndIndex + 3);
      // e.g. http://www.foo.com/bar -> http://www.foo.com
      var domain = firstSlash != -1 ?
          this.originalUrl_.substr(0, firstSlash) : this.originalUrl_;
      return domain + url;
    }

    // Check for obvious relative paths.
    var isRelative = false;
    if (url.startsWith('.') || url.startsWith('\\'))
      isRelative = true;

    // In Adobe Acrobat Reader XI, it looks as though links with less than
    // 2 dot separators in the domain are considered relative links, and
    // those with 2 of more are considered http URLs. e.g.
    //
    // www.foo.com/bar -> http
    // foo.com/bar -> relative link
    if (!isRelative) {
      var domainSeparatorIndex = url.indexOf('/');
      var domainName = domainSeparatorIndex == -1 ?
          url : url.substr(0, domainSeparatorIndex);
      var domainDotCount = (domainName.match(/\./g) || []).length;
      if (domainDotCount < 2)
        isRelative = true;
    }

    if (isRelative) {
      var slashIndex = this.originalUrl_.lastIndexOf('/');
      var path = slashIndex != -1 ?
          this.originalUrl_.substr(0, slashIndex) : this.originalUrl_;
      return path + '/' + url;
    }

    return 'http://' + url;
  }
};
// Copyright 2015 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';

/**
 * @private
 * The period of time in milliseconds to wait between updating the viewport
 * position by the scroll velocity.
 */
ViewportScroller.DRAG_TIMER_INTERVAL_MS_ = 100;

/**
 * @private
 * The maximum drag scroll distance per DRAG_TIMER_INTERVAL in pixels.
 */
ViewportScroller.MAX_DRAG_SCROLL_DISTANCE_ = 100;

/**
 * Creates a new ViewportScroller.
 * A ViewportScroller scrolls the page in response to drag selection with the
 * mouse.
 * @param {Object} viewport The viewport info of the page.
 * @param {Object} plugin The PDF plugin element.
 * @param {Object} window The window containing the viewer.
 */
function ViewportScroller(viewport, plugin, window) {
  this.viewport_ = viewport;
  this.plugin_ = plugin;
  this.window_ = window;
  this.mousemoveCallback_ = null;
  this.timerId_ = null;
  this.scrollVelocity_ = null;
  this.lastFrameTime_ = 0;
}

ViewportScroller.prototype = {
  /**
   * @private
   * Start scrolling the page by |scrollVelocity_| every
   * |DRAG_TIMER_INTERVAL_MS_|.
   */
  startDragScrollTimer_: function() {
    if (this.timerId_ === null) {
      this.timerId_ =
          this.window_.setInterval(this.dragScrollPage_.bind(this),
                                   ViewportScroller.DRAG_TIMER_INTERVAL_MS_);
      this.lastFrameTime_ = Date.now();
    }
  },

  /**
   * @private
   * Stops the drag scroll timer if it is active.
   */
  stopDragScrollTimer_: function() {
    if (this.timerId_ !== null) {
      this.window_.clearInterval(this.timerId_);
      this.timerId_ = null;
      this.lastFrameTime_ = 0;
    }
  },

  /**
   * @private
   * Scrolls the viewport by the current scroll velocity.
   */
  dragScrollPage_: function() {
    var position = this.viewport_.position;
    var currentFrameTime = Date.now();
    var timeAdjustment = (currentFrameTime - this.lastFrameTime_) /
                         ViewportScroller.DRAG_TIMER_INTERVAL_MS_;
    position.y += (this.scrollVelocity_.y * timeAdjustment);
    position.x += (this.scrollVelocity_.x * timeAdjustment);
    this.viewport_.position = position;
    this.lastFrameTime_ = currentFrameTime;
  },

  /**
   * @private
   * Calculate the velocity to scroll while dragging using the distance of the
   * cursor outside the viewport.
   * @param {Object} event The mousemove event.
   * @return {Object} Object with x and y direction scroll velocity.
   */
  calculateVelocity_: function(event) {
    var x = Math.min(Math.max(-event.offsetX,
                              event.offsetX - this.plugin_.offsetWidth, 0),
                     ViewportScroller.MAX_DRAG_SCROLL_DISTANCE_) *
            Math.sign(event.offsetX);
    var y = Math.min(Math.max(-event.offsetY,
                              event.offsetY - this.plugin_.offsetHeight, 0),
                     ViewportScroller.MAX_DRAG_SCROLL_DISTANCE_) *
            Math.sign(event.offsetY);
    return {
      x: x,
      y: y
    };
  },

  /**
   * @private
   * Handles mousemove events. It updates the scroll velocity and starts and
   * stops timer based on scroll velocity.
   * @param {Object} event The mousemove event.
   */
  onMousemove_: function(event) {
    this.scrollVelocity_ = this.calculateVelocity_(event);
    if (!this.scrollVelocity_.x && !this.scrollVelocity_.y)
      this.stopDragScrollTimer_();
    else if (!this.timerId_)
      this.startDragScrollTimer_();
  },

  /**
   * Sets whether to scroll the viewport when the mouse is outside the
   * viewport.
   * @param {boolean} isSelecting Represents selection status.
   */
  setEnableScrolling: function(isSelecting) {
    if (isSelecting) {
      if (!this.mousemoveCallback_)
        this.mousemoveCallback_ = this.onMousemove_.bind(this);
      this.plugin_.addEventListener('mousemove', this.mousemoveCallback_,
                                    false);
    } else {
      this.stopDragScrollTimer_();
      if (this.mousemoveCallback_) {
        this.plugin_.removeEventListener('mousemove', this.mousemoveCallback_,
                                         false);
      }
    }
  }
};
// Copyright 2016 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';

/**
 * A class that listens for touch events and produces events when these
 * touches form gestures (e.g. pinching).
 */
class GestureDetector {
  /**
   * Constructs a GestureDetector.
   * @param {!Element} element The element to monitor for touch gestures.
   */
  constructor(element) {
    this.element_ = element;

    this.element_.addEventListener(
        'touchstart', this.onTouchStart_.bind(this), { passive: false });
    this.element_.addEventListener(
        'touchmove', this.onTouch_.bind(this), { passive: true });
    this.element_.addEventListener(
        'touchend', this.onTouch_.bind(this), { passive: true });
    this.element_.addEventListener(
        'touchcancel', this.onTouch_.bind(this), { passive: true });

    this.pinchStartEvent_ = null;
    this.lastEvent_ = null;

    this.listeners_ = new Map([
      ['pinchstart', []],
      ['pinchupdate', []],
      ['pinchend', []]
    ]);
  }

  /**
   * Add a |listener| to be notified of |type| events.
   * @param {string} type The event type to be notified for.
   * @param {Function} listener The callback.
   */
  addEventListener(type, listener) {
    if (this.listeners_.has(type)) {
      this.listeners_.get(type).push(listener);
    }
  }

  /**
   * Call the relevant listeners with the given |pinchEvent|.
   * @private
   * @param {!Object} pinchEvent The event to notify the listeners of.
   */
  notify_(pinchEvent) {
    let listeners = this.listeners_.get(pinchEvent.type);

    for (let l of listeners)
      l(pinchEvent);
  }

  /**
   * The callback for touchstart events on the element.
   * @private
   * @param {!TouchEvent} event Touch event on the element.
   */
  onTouchStart_(event) {
    // We must preventDefault if there is a two finger touch. By doing so
    // native pinch-zoom does not interfere with our way of handling the event.
    if (event.touches.length == 2) {
      event.preventDefault();
      this.pinchStartEvent_ = event;
      this.lastEvent_ = event;
      this.notify_({
        type: 'pinchstart',
        center: GestureDetector.center_(event)
      });
    }
  }

  /**
   * The callback for touch move, end, and cancel events on the element.
   * @private
   * @param {!TouchEvent} event Touch event on the element.
   */
  onTouch_(event) {
    if (!this.pinchStartEvent_)
      return;

    // Check if the pinch ends with the current event.
    if (event.touches.length < 2 ||
        this.lastEvent_.touches.length !== event.touches.length) {
      let startScaleRatio = GestureDetector.pinchScaleRatio_(
          this.lastEvent_, this.pinchStartEvent_);
      let center = GestureDetector.center_(this.lastEvent_);
      let endEvent = {
        type: 'pinchend',
        startScaleRatio: startScaleRatio,
        center: center
      };
      this.pinchStartEvent_ = null;
      this.lastEvent_ = null;
      this.notify_(endEvent);
      return;
    }

    let scaleRatio = GestureDetector.pinchScaleRatio_(event, this.lastEvent_);
    let startScaleRatio = GestureDetector.pinchScaleRatio_(
        event, this.pinchStartEvent_);
    let center = GestureDetector.center_(event);
    this.notify_({
      type: 'pinchupdate',
      scaleRatio: scaleRatio,
      direction: scaleRatio > 1.0 ? 'in' : 'out',
      startScaleRatio: startScaleRatio,
      center: center
    });

    this.lastEvent_ = event;
  }

  /**
   * Computes the change in scale between this touch event
   * and a previous one.
   * @private
   * @param {!TouchEvent} event Latest touch event on the element.
   * @param {!TouchEvent} prevEvent A previous touch event on the element.
   * @return {?number} The ratio of the scale of this event and the
   *     scale of the previous one.
   */
  static pinchScaleRatio_(event, prevEvent) {
    let distance1 = GestureDetector.distance_(prevEvent);
    let distance2 = GestureDetector.distance_(event);
    return distance1 === 0 ? null : distance2 / distance1;
  }

  /**
   * Computes the distance between fingers.
   * @private
   * @param {!TouchEvent} event Touch event with at least 2 touch points.
   * @return {number} Distance between touch[0] and touch[1].
   */
  static distance_(event) {
    let touch1 = event.touches[0];
    let touch2 = event.touches[1];
    let dx = touch1.clientX - touch2.clientX;
    let dy = touch1.clientY - touch2.clientY;
    return Math.sqrt(dx * dx + dy * dy);
  }

  /**
   * Computes the midpoint between fingers.
   * @private
   * @param {!TouchEvent} event Touch event with at least 2 touch points.
   * @return {!Object} Midpoint between touch[0] and touch[1].
   */
  static center_(event) {
    let touch1 = event.touches[0];
    let touch2 = event.touches[1];
    return {
      x: (touch1.clientX + touch2.clientX) / 2,
      y: (touch1.clientY + touch2.clientY) / 2
    };
  }
};
// Copyright 2015 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';

/**
 * A class that manages updating the browser with zoom changes.
 */
class ZoomManager {
  /**
   * Constructs a ZoomManager
   * @param {!Viewport} viewport A Viewport for which to manage zoom.
   * @param {Function} setBrowserZoomFunction A function that sets the browser
   *     zoom to the provided value.
   * @param {number} initialZoom The initial browser zoom level.
   */
  constructor(viewport, setBrowserZoomFunction, initialZoom) {
    this.viewport_ = viewport;
    this.setBrowserZoomFunction_ = setBrowserZoomFunction;
    this.browserZoom_ = initialZoom;
    this.changingBrowserZoom_ = null;
  }

  /**
   * Invoked when a browser-initiated zoom-level change occurs.
   * @param {number} newZoom the zoom level to zoom to.
   */
  onBrowserZoomChange(newZoom) {
    // If we are changing the browser zoom level, ignore any browser zoom level
    // change events. Either, the change occurred before our update and will be
    // overwritten, or the change being reported is the change we are making,
    // which we have already handled.
    if (this.changingBrowserZoom_)
      return;

    if (this.floatingPointEquals(this.browserZoom_, newZoom))
      return;

    this.browserZoom_ = newZoom;
    this.viewport_.setZoom(newZoom);
  }

  /**
   * Invoked when an extension-initiated zoom-level change occurs.
   */
  onPdfZoomChange() {
    // If we are already changing the browser zoom level in response to a
    // previous extension-initiated zoom-level change, ignore this zoom change.
    // Once the browser zoom level is changed, we check whether the extension's
    // zoom level matches the most recently sent zoom level.
    if (this.changingBrowserZoom_)
      return;

    let zoom = this.viewport_.zoom;
    if (this.floatingPointEquals(this.browserZoom_, zoom))
      return;

    this.changingBrowserZoom_ = this.setBrowserZoomFunction_(zoom).then(
        function() {
      this.browserZoom_ = zoom;
      this.changingBrowserZoom_ = null;

      // The extension's zoom level may have changed while the browser zoom
      // change was in progress. We call back into onPdfZoomChange to ensure the
      // browser zoom is up to date.
      this.onPdfZoomChange();
    }.bind(this));
  }

  /**
   * Returns whether two numbers are approximately equal.
   * @param {number} a The first number.
   * @param {number} b The second number.
   */
  floatingPointEquals(a, b) {
    let MIN_ZOOM_DELTA = 0.01;
    // If the zoom level is close enough to the current zoom level, don't
    // change it. This avoids us getting into an infinite loop of zoom changes
    // due to floating point error.
    return Math.abs(a - b) <= MIN_ZOOM_DELTA;
  }
};
// Copyright 2015 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';

/**
 * Returns a promise that will resolve to the default zoom factor.
 * @return {Promise<number>} A promise that will resolve to the default zoom
 *     factor.
 */
function lookupDefaultZoom() {
  return cr.sendWithPromise('getDefaultZoom');
}

/**
 * Returns a promise that will resolve to the initial zoom factor
 * upon starting the plugin. This may differ from the default zoom
 * if, for example, the page is zoomed before the plugin is run.
 * @return {Promise<number>} A promise that will resolve to the initial zoom
 *     factor.
 */
function lookupInitialZoom() {
  return cr.sendWithPromise('getInitialZoom');
}

/**
 * A class providing an interface to the browser.
 */
class BrowserApi {
  /**
   * @constructor
   * @param {!Object} streamInfo The stream object which points to the data
   *     contained in the PDF.
   * @param {number} defaultZoom The default browser zoom.
   * @param {number} initialZoom The initial browser zoom
   *     upon starting the plugin.
   * @param {boolean} manageZoom Whether to manage zoom.
   */
  constructor(streamInfo, defaultZoom, initialZoom, manageZoom) {
    this.streamInfo_ = streamInfo;
    this.defaultZoom_ = defaultZoom;
    this.initialZoom_ = initialZoom;
    this.manageZoom_ = manageZoom;
  }

  /**
   * Returns a promise to a BrowserApi.
   * @param {!Object} streamInfo The stream object pointing to the data
   *     contained in the PDF.
   * @param {boolean} manageZoom Whether to manage zoom.
   */
  static create(streamInfo, manageZoom) {
    return Promise.all([
      lookupDefaultZoom(),
      lookupInitialZoom()
    ]).then(function(zoomFactors) {
      return new BrowserApi(
        streamInfo, zoomFactors[0], zoomFactors[1], manageZoom);
    });
  }

  /**
   * Returns the stream info pointing to the data contained in the PDF.
   * @return {Object} The stream info object.
   */
  getStreamInfo() {
    return this.streamInfo_;
  }

  /**
   * Sets the browser zoom.
   * @param {number} zoom The zoom factor to send to the browser.
   * @return {Promise} A promise that will be resolved when the browser zoom
   *     has been updated.
   */
  setZoom(zoom) {
    if (!this.manageZoom_)
      return Promise.resolve();
    return cr.sendWithPromise('setZoom', zoom);
  }

  /**
   * Returns the default browser zoom factor.
   * @return {number} The default browser zoom factor.
   */
  getDefaultZoom() {
    return this.defaultZoom_;
  }

  /**
   * Returns the initial browser zoom factor.
   * @return {number} The initial browser zoom factor.
   */
  getInitialZoom() {
    return this.initialZoom_;
  }

  /**
   * Adds an event listener to be notified when the browser zoom changes.
   * @param {function} listener The listener to be called with the new zoom
   *     factor.
   */
  addZoomEventListener(listener) {
    if (!this.manageZoom_)
      return;

    cr.addWebUIListener('onZoomLevelChanged', function(newZoomFactor) {
      listener(newZoomFactor);
    });
  }
};

/**
 * Creates a BrowserApi instance for an extension not running as a mime handler.
 * @return {Promise<BrowserApi>} A promise to a BrowserApi instance constructed
 *     from the URL.
 */
function createBrowserApi(opts) {
  let streamInfo = {
    streamUrl: opts.streamURL,
    originalUrl: opts.originalURL,
    responseHeaders: opts.responseHeaders,
    embedded: window.parent != window,
    tabId: -1,
  };
  return new Promise(function(resolve, reject) {
    resolve(BrowserApi.create(streamInfo, true));
  });
}
/* Copyright 2015 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. */

:root {
  --iron-icon-height: 20px;
  --iron-icon-width: 20px;
  --paper-icon-button: {
    height: 32px;
    padding: 6px;
    width: 32px;
  };
  --paper-icon-button-ink-color: rgb(189, 189, 189);
  --viewer-icon-ink-color: rgb(189, 189, 189);
}
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-iconset-svg/iron-iconset-svg.html">

<iron-iconset-svg size="24" name="pdf">
  <svg>
    <defs>
      <!--
      These icons are copied from Polymer's iron-icons and kept in sorted order.
      See http://goo.gl/Y1OdAq for instructions on adding additional icons.
      -->
      <g id="add"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"></path></g>
      <g id="bookmark"><path d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2z"></path></g>
      <g id="bookmark-border"><path d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2zm0 15l-5-2.18L7 18V5h10v13z"></path></g>
      <g id="fullscreen-exit"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"></path></g>
      <g id="remove"><path d="M19 13H5v-2h14v2z"></path></g>
      <g id="rotate-right"><path d="M15.55 5.55L11 1v3.07C7.06 4.56 4 7.92 4 12s3.05 7.44 7 7.93v-2.02c-2.84-.48-5-2.94-5-5.91s2.16-5.43 5-5.91V10l4.55-4.45zM19.93 11c-.17-1.39-.72-2.73-1.62-3.89l-1.42 1.42c.54.75.88 1.6 1.02 2.47h2.02zM13 17.9v2.02c1.39-.17 2.74-.71 3.9-1.61l-1.44-1.44c-.75.54-1.59.89-2.46 1.03zm3.89-2.42l1.42 1.41c.9-1.16 1.45-2.5 1.62-3.89h-2.02c-.14.87-.48 1.72-1.02 2.48z"></path></g>
    </defs>
  </svg>
</iron-iconset-svg>
/* Copyright 2015 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. */

#item {
  @apply(--layout-center);
  @apply(--layout-horizontal);
  color: rgb(80, 80, 80);
  cursor: pointer;
  font-size: 77.8%;
  height: 30px;
  position: relative;
}

#item:hover {
  background-color: rgb(237, 237, 237);
  color: rgb(20, 20, 20);
}

paper-ripple {
  /* Allowing the ripple to capture pointer events prevents a focus rectangle
   * for showing up for clicks, while still allowing it with tab-navigation.
   * This undoes a paper-ripple bugfix aimed at non-Chrome browsers.
   * TODO(tsergeant): Improve focus in viewer-bookmark so this can be removed
   * (https://crbug.com/5448190). */
  pointer-events: auto;
}

#title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#expand {
  --iron-icon-height: 16px;
  --iron-icon-width: 16px;
  --paper-icon-button-ink-color: var(--paper-grey-900);
  height: 28px;
  min-width: 28px;
  padding: 6px;
  transition: transform 150ms;
  width: 28px;
}

:host-context([dir=rtl]) #expand {
  transform: rotate(180deg);
}

:host([children-shown]) #expand {
  transform: rotate(90deg);
}
<link rel="import" href="chrome://resources/cr_elements/icons.html">
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-ripple/paper-ripple.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-styles/color.html">

<dom-module id="viewer-bookmark" attributes="bookmark">
  <link rel="import" type="css" href="viewer-bookmark.css">
  <template>
    <div id="item" on-click="onClick">
      <paper-ripple></paper-ripple>
      <paper-icon-button id="expand" icon="cr:chevron-right"
          on-click="toggleChildren">
      </paper-icon-button>
      <span id="title" tabindex="0">{{bookmark.title}}</span>
    </div>
    <!-- dom-if will stamp the complex bookmark tree lazily as individual nodes
      are opened. -->
    <template is="dom-if" if="{{childrenShown}}" id="sub-bookmarks">
      <template is="dom-repeat" items="{{bookmark.children}}">
        <viewer-bookmark bookmark="{{item}}" depth="{{childDepth}}">
        </viewer-bookmark>
      </template>
    </template>
  </template>
</dom-module>
<script src="viewer-bookmark.js"></script>
// Copyright 2015 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.

(function() {
  /** Amount that each level of bookmarks is indented by (px). */
  var BOOKMARK_INDENT = 20;

  Polymer({
    is: 'viewer-bookmark',

    properties: {
      /**
       * A bookmark object, each containing a:
       * - title
       * - page (optional)
       * - children (an array of bookmarks)
       */
      bookmark: {
        type: Object,
        observer: 'bookmarkChanged_'
      },

      depth: {
        type: Number,
        observer: 'depthChanged'
      },

      childDepth: Number,

      childrenShown: {
        type: Boolean,
        reflectToAttribute: true,
        value: false
      },

      keyEventTarget: {
        type: Object,
        value: function() {
          return this.$.item;
        }
      }
    },

    behaviors: [
      Polymer.IronA11yKeysBehavior
    ],

    keyBindings: {
      'enter': 'onEnter_',
      'space': 'onSpace_'
    },

    bookmarkChanged_: function() {
      this.$.expand.style.visibility =
          this.bookmark.children.length > 0 ? 'visible' : 'hidden';
    },

    depthChanged: function() {
      this.childDepth = this.depth + 1;
      this.$.item.style.webkitPaddingStart =
          (this.depth * BOOKMARK_INDENT) + 'px';
    },

    onClick: function() {
      if (this.bookmark.hasOwnProperty('page'))
        this.fire('change-page', {page: this.bookmark.page});
      else if (this.bookmark.hasOwnProperty('uri'))
        this.fire('navigate', {uri: this.bookmark.uri, newtab: true});
    },

    onEnter_: function(e) {
      // Don't allow events which have propagated up from the expand button to
      // trigger a click.
      if (e.detail.keyboardEvent.target != this.$.expand)
        this.onClick();
    },

    onSpace_: function(e) {
      // paper-icon-button stops propagation of space events, so there's no need
      // to check the event source here.
      this.onClick();
      // Prevent default space scroll behavior.
      e.detail.keyboardEvent.preventDefault();
    },

    toggleChildren: function(e) {
      this.childrenShown = !this.childrenShown;
      e.stopPropagation();  // Prevent the above onClick handler from firing.
    }
  });
})();
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="../viewer-bookmark/viewer-bookmark.html">

<dom-module id="viewer-bookmarks-content">
  <template>
    <template is="dom-repeat" items="{{bookmarks}}">
      <viewer-bookmark bookmark="{{item}}" depth="0"></viewer-bookmark>
    </template>
  </template>
</dom-module>
<script src="viewer-bookmarks-content.js"></script>
// Copyright 2015 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.

Polymer({
  is: 'viewer-bookmarks-content'
});
/* Copyright 2015 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. */

.last-item {
  margin-bottom: 24px;
}
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/animations/fade-in-animation.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-dialog/paper-dialog.html">

<dom-module id="viewer-error-screen">
  <link rel="import" type="css" href="viewer-error-screen.css">
  <template>
    <paper-dialog id="dialog" modal no-cancel-on-esc-key
        entry-animation="fade-in-animation">
      <div id="load-failed-message" class="last-item">
        {{strings.pageLoadFailed}}
      </div>
      <div class="buttons" hidden$="{{!reloadFn}}">
        <paper-button on-click="reload" autofocus>
          {{strings.pageReload}}
        </paper-button>
      </div>
    </paper-dialog>
  </template>
</dom-module>
<script src="viewer-error-screen.js"></script>
// Copyright 2014 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.

Polymer({
  is: 'viewer-error-screen',
  properties: {
    reloadFn: {
      type: Object,
      value: null,
      observer: 'reloadFnChanged_'
    },

    strings: Object,
  },

  reloadFnChanged_: function() {
    // The default margins in paper-dialog don't work well with hiding/showing
    // the .buttons div. We need to manually manage the bottom margin to get
    // around this.
    if (this.reloadFn)
      this.$['load-failed-message'].classList.remove('last-item');
    else
      this.$['load-failed-message'].classList.add('last-item');
  },

  show: function() {
    this.$.dialog.open();
  },

  reload: function() {
    if (this.reloadFn)
      this.reloadFn();
  }
});
/* 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. */

:host {
  -webkit-transition: opacity 400ms ease-in-out;
  pointer-events: none;
  position: fixed;
  right: 0;
}

#text {
  background-color: rgba(0, 0, 0, 0.5);
  border-radius: 5px;
  color: white;
  float: left;
  font-family: sans-serif;
  font-size: 12px;
  font-weight: bold;
  line-height: 48px;
  text-align: center;
  text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
  width: 62px;
}

#triangle-right {
  border-bottom: 6px solid transparent;
  border-left: 8px solid rgba(0, 0, 0, 0.5);
  border-top: 6px solid transparent;
  display: inline;
  float: left;
  height: 0;
  margin-top: 18px;
  width: 0;
}<link rel="import" href="chrome://resources/html/polymer.html">

<dom-module id="viewer-page-indicator">
  <link rel="import" type="css" href="viewer-page-indicator.css">
  <template>
    <div id="text">{{label}}</div>
    <div id="triangle-right"></div>
  </template>
</dom-module>
<script src="viewer-page-indicator.js"></script>
// Copyright 2014 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.

Polymer({
  is: 'viewer-page-indicator',

  properties: {
    label: {
      type: String,
      value: '1'
    },

    index: {
      type: Number,
      observer: 'indexChanged'
    },

    pageLabels: {
      type: Array,
      value: null,
      observer: 'pageLabelsChanged'
    }
  },

  timerId: undefined,

  ready: function() {
    var callback = this.fadeIn.bind(this, 2000);
    window.addEventListener('scroll', function() {
      requestAnimationFrame(callback);
    });
  },

  initialFadeIn: function() {
    this.fadeIn(6000);
  },

  fadeIn: function(displayTime) {
    var percent = window.scrollY /
        (document.body.scrollHeight -
         document.documentElement.clientHeight);
    this.style.top = percent *
        (document.documentElement.clientHeight - this.offsetHeight) + 'px';

    this.style.opacity = 1;
    clearTimeout(this.timerId);

    this.timerId = setTimeout(function() {
      this.style.opacity = 0;
      this.timerId = undefined;
    }.bind(this), displayTime);
  },

  pageLabelsChanged: function() {
    this.indexChanged();
  },

  indexChanged: function() {
    if (this.pageLabels)
      this.label = this.pageLabels[this.index];
    else
      this.label = String(this.index + 1);
  }
});
/* Copyright 2015 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. */

:host {
  color: #fff;
  font-size: 94.4%;
}

:host ::selection {
  background: rgba(255, 255, 255, 0.3);
}

#pageselector {
  --paper-input-container-underline: {
    visibility: hidden;
  };
  --paper-input-container-underline-focus: {
    visibility: hidden;
  };
  display: inline-block;
  padding: 0;
  width: 1ch;
}

#input {
  -webkit-margin-start: -3px;
  color: #fff;
  line-height: 18px;
  padding: 3px;
  text-align: end;
  vertical-align: baseline;
}

#input:focus,
#input:hover {
  background-color: rgba(0, 0, 0, 0.5);
  border-radius: 2px;
}

#slash {
  padding: 0 3px;
}

#pagelength-spacer {
  display: inline-block;
  text-align: start;
}

#slash,
#pagelength {
  font-size: 76.5%;
}
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-input/iron-input.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-input/paper-input-container.html">

<dom-module id="viewer-page-selector">
  <link rel="import" type="css" href="viewer-page-selector.css">
  <template>
    <paper-input-container id="pageselector" no-label-float>
      <input id="input" is="iron-input" value="{{pageNo}}"
          prevent-invalid-input allowed-pattern="\d" on-mouseup="select"
          on-change="pageNoCommitted" aria-label$="{{strings.labelPageNumber}}">
    </paper-input-container>
    <span id="slash"> / </span>
    <span id="pagelength-spacer">
      <span id="pagelength">{{docLength}}</span>
    </span>
  </template>
</dom-module>
<script src="viewer-page-selector.js"></script>
// Copyright 2015 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.

Polymer({
  is: 'viewer-page-selector',

  properties: {
    /**
     * The number of pages the document contains.
     */
    docLength: {
      type: Number,
      value: 1,
      observer: 'docLengthChanged'
    },

    /**
     * The current page being viewed (1-based). A change to pageNo is mirrored
     * immediately to the input field. A change to the input field is not
     * mirrored back until pageNoCommitted() is called and change-page is fired.
     */
    pageNo: {
      type: Number,
      value: 1
    },

    strings: Object,
  },

  pageNoCommitted: function() {
    var page = parseInt(this.$.input.value);

    if (!isNaN(page) && page <= this.docLength && page > 0)
      this.fire('change-page', {page: page - 1});
    else
      this.$.input.value = this.pageNo;
    this.$.input.blur();
  },

  docLengthChanged: function() {
    var numDigits = this.docLength.toString().length;
    this.$.pageselector.style.width = numDigits + 'ch';
    // Set both sides of the slash to the same width, so that the layout is
    // exactly centered.
    this.$['pagelength-spacer'].style.width = numDigits + 'ch';
  },

  select: function() {
    this.$.input.select();
  },

  /**
   * @return {boolean} True if the selector input field is currently focused.
   */
  isActive: function() {
    return this.shadowRoot.activeElement == this.$.input;
  }
});
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-flex-layout/iron-flex-layout-classes.html">
<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/animations/fade-in-animation.html">
<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/animations/fade-out-animation.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-button/paper-button.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-dialog/paper-dialog.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-input/paper-input.html">

<dom-module id="viewer-password-screen">
  <template>
    <style include="iron-flex iron-flex-alignment"></style>
    <paper-dialog id="dialog" modal no-cancel-on-esc-key
        entry-animation="fade-in-animation" exit-animation="fade-out-animation">
      <div id="message">{{strings.passwordPrompt}}</div>
      <div class="horizontal layout start">
        <paper-input-container id="password-container" class="flex"
            no-label-float invalid="[[invalid]]">
          <input is="iron-input" id="password" type="password" size="20"
              on-keypress="handleKey" autofocus>
          </input>
          <paper-input-error>{{strings.passwordInvalid}}</paper-input-error>
        </paper-input-container>
        <paper-button id="submit" on-click="submit">
          {{strings.passwordSubmit}}
        </paper-button>
      </div>
    </paper-dialog>
  </template>
</dom-module>
<script src="viewer-password-screen.js"></script>
// Copyright 2014 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.

Polymer({
  is: 'viewer-password-screen',

  properties: {
    invalid: Boolean,

    active: {
      type: Boolean,
      value: false,
      observer: 'activeChanged'
    },

    strings: Object,
  },

  ready: function() {
    this.activeChanged();
  },

  accept: function() {
    this.active = false;
  },

  deny: function() {
    this.$.password.disabled = false;
    this.$.submit.disabled = false;
    this.invalid = true;
    this.$.password.focus();
    this.$.password.select();
  },

  handleKey: function(e) {
    if (e.keyCode == 13)
      this.submit();
  },

  submit: function() {
    if (this.$.password.value.length == 0)
      return;
    this.$.password.disabled = true;
    this.$.submit.disabled = true;
    this.fire('password-submitted', {password: this.$.password.value});
  },

  activeChanged: function() {
    if (this.active) {
      this.$.dialog.open();
      this.$.password.focus();
    } else {
      this.$.dialog.close();
    }
  }
});
/* Copyright 2015 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. */

:host ::selection {
  background: rgba(255, 255, 255, 0.3);
}

/* We introduce a wrapper aligner element to help with laying out the main
 * toolbar content without changing the bottom-aligned progress bar. */
#aligner {
  @apply(--layout-horizontal);
  @apply(--layout-center);
  padding: 0 16px;
  width: 100%;
}

#title {
  @apply(--layout-flex-5);
  font-size: 77.8%;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#pageselector-container {
  @apply(--layout-flex-1);
  text-align: center;
  /* The container resizes according to the width of the toolbar. On small
   * screens with large numbers of pages, overflow page numbers without
   * wrapping. */
  white-space: nowrap;
}

#buttons {
  @apply(--layout-flex-5);
  -webkit-user-select: none;
  text-align: end;
}

paper-icon-button {
  -webkit-margin-end: 12px;
}

viewer-toolbar-dropdown {
  -webkit-margin-end: 4px;
}

paper-progress {
  --paper-progress-active-color: var(--google-blue-300);
  --paper-progress-container-color: transparent;
  --paper-progress-height: 3px;
  transition: opacity 150ms;
  width: 100%;
}

paper-toolbar {
  --paper-toolbar-background: rgb(50, 54, 57);
  --paper-toolbar-height: 48px;
  @apply(--shadow-elevation-2dp);
  color: rgb(241, 241, 241);
  font-size: 1.5em;
}

.invisible {
  visibility: hidden;
}

@media(max-width: 675px) {
  #bookmarks,
  #rotate-left {
    display: none;
  }

  #pageselector-container {
    flex: 2;
  }
}

@media(max-width: 450px) {
  #rotate-right {
    display: none;
  }
}

@media(max-width: 400px) {
  #buttons,
  #pageselector-container {
    display: none;
  }
}
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-flex-layout/iron-flex-layout.html">
<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/animations/slide-up-animation.html">
<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/animations/transform-animation.html">
<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/neon-animation-runner-behavior.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-icon-button/paper-icon-button.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-progress/paper-progress.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-toolbar/paper-toolbar.html">
<link rel="import" href="chrome://resources/cr_elements/icons.html">
<link rel="import" href="../icons.html">
<link rel="import" href="../viewer-bookmarks-content/viewer-bookmarks-content.html">
<link rel="import" href="../viewer-page-selector/viewer-page-selector.html">
<link rel="import" href="../viewer-toolbar-dropdown/viewer-toolbar-dropdown.html">

<dom-module id="viewer-pdf-toolbar">
  <link rel="import" type="css" href="../shared-icon-style.css">
  <link rel="import" type="css" href="viewer-pdf-toolbar.css">
  <template>

    <paper-toolbar>
      <div id="aligner" class="middle">
        <span id="title" title="{{docTitle}}">
          <span>{{docTitle}}</span>
        </span>

        <div id="pageselector-container">
          <viewer-page-selector id="pageselector" class="invisible"
              doc-length="{{docLength}}" page-no="{{pageNo}}"
              strings="{{strings}}">
          </viewer-page-selector>
        </div>

        <div id="buttons" class="invisible">
          <paper-icon-button id="rotate-right" icon="pdf:rotate-right"
              on-click="rotateRight"
              aria-label$="{{strings.tooltipRotateCW}}"
              title$="{{strings.tooltipRotateCW}}">
          </paper-icon-button>

          <paper-icon-button id="download" icon="cr:file-download"
              on-click="download"
              aria-label$="{{strings.tooltipDownload}}"
              title$="{{strings.tooltipDownload}}">
          </paper-icon-button>

          <!--paper-icon-button id="print" icon="cr:print"
              on-click="print"
              aria-label$="{{strings.tooltipPrint}}"
              title$="{{strings.tooltipPrint}}">
          </paper-icon-button-->

          <viewer-toolbar-dropdown id="bookmarks"
                                   hidden$="[[!bookmarks.length]]"
                                   open-icon="pdf:bookmark"
                                   closed-icon="pdf:bookmark-border"
                                   header="{{strings.bookmarks}}">
              <viewer-bookmarks-content bookmarks="{{bookmarks}}">
              </viewer-bookmarks-content>
          </viewer-toolbar-dropdown>
        </div>
      </div>
      <div class="bottom fit">
        <paper-progress id="progress" value="{{loadProgress}}"></paper-progress>
      </div>
    </paper-toolbar>
  </template>
</dom-module>
<script src="viewer-pdf-toolbar.js"></script>
// Copyright 2015 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.
(function() {
  Polymer({
    is: 'viewer-pdf-toolbar',

    behaviors: [
      Polymer.NeonAnimationRunnerBehavior
    ],

    properties: {
      /**
       * The current loading progress of the PDF document (0 - 100).
       */
      loadProgress: {
        type: Number,
        observer: 'loadProgressChanged'
      },

      /**
       * The title of the PDF document.
       */
      docTitle: String,

      /**
       * The number of the page being viewed (1-based).
       */
      pageNo: Number,

      /**
       * Tree of PDF bookmarks (or null if the document has no bookmarks).
       */
      bookmarks: {
        type: Object,
        value: null
      },

      /**
       * The number of pages in the PDF document.
       */
      docLength: Number,

      /**
       * Whether the toolbar is opened and visible.
       */
      opened: {
        type: Boolean,
        value: true
      },

      strings: Object,

      animationConfig: {
        value: function() {
          return {
            'entry': {
              name: 'transform-animation',
              node: this,
              transformFrom: 'translateY(-100%)',
              transformTo: 'translateY(0%)',
              timing: {
                easing: 'cubic-bezier(0, 0, 0.2, 1)',
                duration: 250
              }
            },
            'exit': {
              name: 'slide-up-animation',
              node: this,
              timing: {
                easing: 'cubic-bezier(0.4, 0, 1, 1)',
                duration: 250
              }
            }
          };
        }
      }
    },

    listeners: {
      'neon-animation-finish': '_onAnimationFinished'
    },

    _onAnimationFinished: function() {
      this.style.transform = this.opened ? 'none' : 'translateY(-100%)';
    },

    loadProgressChanged: function() {
      if (this.loadProgress >= 100) {
        this.$.pageselector.classList.toggle('invisible', false);
        this.$.buttons.classList.toggle('invisible', false);
        this.$.progress.style.opacity = 0;
      }
    },

    hide: function() {
      if (this.opened)
        this.toggleVisibility();
    },

    show: function() {
      if (!this.opened) {
        this.toggleVisibility();
      }
    },

    toggleVisibility: function() {
      this.opened = !this.opened;
      this.cancelAnimation();
      this.playAnimation(this.opened ? 'entry' : 'exit');
    },

    selectPageNumber: function() {
      this.$.pageselector.select();
    },

    shouldKeepOpen: function() {
      return this.$.bookmarks.dropdownOpen || this.loadProgress < 100 ||
          this.$.pageselector.isActive();
    },

    hideDropdowns: function() {
      if (this.$.bookmarks.dropdownOpen) {
        this.$.bookmarks.toggleDropdown();
        return true;
      }
      return false;
    },

    setDropdownLowerBound: function(lowerBound) {
      this.$.bookmarks.lowerBound = lowerBound;
    },

    rotateRight: function() {
      this.fire('rotate-right');
    },

    download: function() {
      this.fire('save');
    },

    print: function() {
      this.fire('print');
    }
  });
})();
/* Copyright 2015 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. */

:host {
  text-align: start;
}

#container {
  position: absolute;
  /* Controls the position of the dropdown relative to the right of the screen.
   * Default is aligned with the right of the toolbar buttons.
   * TODO(tsergeant): Change the layout of the dropdown so this is not required.
   */
  right: var(--viewer-toolbar-dropdown-right-distance, 36px);
}

:host-context([dir=rtl]) #container {
  left: var(--viewer-toolbar-dropdown-right-distance, 36px);
  right: auto;
}

paper-material {
  background-color: rgb(256, 256, 256);
  border-radius: 4px;
  overflow-y: hidden;
  padding-bottom: 2px;
  width: 260px;
}

#scroll-container {
  max-height: 300px;
  overflow-y: auto;
  padding: 6px 0 4px 0;
}

#icon {
  cursor: pointer;
  display: inline-block;
}

:host([dropdown-open]) #icon {
  background-color: rgb(25, 27, 29);
  border-radius: 4px;
}

#arrow {
  -webkit-margin-start: -12px;
  -webkit-padding-end: 4px;
}

h1 {
  border-bottom: 1px solid rgb(219, 219, 219);
  color: rgb(33, 33, 33);
  font-size: 77.8%;
  font-weight: 500;
  margin: 0;
  padding: 14px 28px;
}
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/polymer/v1_0/neon-animation/web-animations.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-material/paper-material.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-icon-button/paper-icon-button.html">
<link rel="import" href="chrome://resources/cr_elements/icons.html">

<dom-module id="viewer-toolbar-dropdown">
  <link rel="import" type="css" href="../shared-icon-style.css">
  <link rel="import" type="css" href="viewer-toolbar-dropdown.css">
  <template>
    <div on-click="toggleDropdown" id="icon">
      <paper-icon-button id="main-icon" icon="[[dropdownIcon]]"
          aria-label$="{{header}}" title$="{{header}}">
      </paper-icon-button>
      <iron-icon icon="cr:arrow-drop-down" id="arrow"></iron-icon>
    </div>

    <div id="container">
      <paper-material id="dropdown" style="display: none">
        <h1>{{header}}</h1>
        <div id="scroll-container">
          <content></content>
        </div>
      </paper-material>
    </div>
  </template>
</dom-module>

<script src="viewer-toolbar-dropdown.js"></script>
// Copyright 2015 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.

(function() {
  /**
   * Size of additional padding in the inner scrollable section of the dropdown.
   */
  var DROPDOWN_INNER_PADDING = 12;

  /** Size of vertical padding on the outer #dropdown element. */
  var DROPDOWN_OUTER_PADDING = 2;

  /** Minimum height of toolbar dropdowns (px). */
  var MIN_DROPDOWN_HEIGHT = 200;

  Polymer({
    is: 'viewer-toolbar-dropdown',

    properties: {
      /** String to be displayed at the top of the dropdown. */
      header: String,

      /** Icon to display when the dropdown is closed. */
      closedIcon: String,

      /** Icon to display when the dropdown is open. */
      openIcon: String,

      /** True if the dropdown is currently open. */
      dropdownOpen: {
        type: Boolean,
        reflectToAttribute: true,
        value: false
      },

      /** Toolbar icon currently being displayed. */
      dropdownIcon: {
        type: String,
        computed: 'computeIcon_(dropdownOpen, closedIcon, openIcon)'
      },

      /** Lowest vertical point that the dropdown should occupy (px). */
      lowerBound: {
        type: Number,
        observer: 'lowerBoundChanged_'
      },

      /**
       * True if the max-height CSS property for the dropdown scroll container
       * is valid. If false, the height will be updated the next time the
       * dropdown is visible.
       */
      maxHeightValid_: false,

      /** Current animation being played, or null if there is none. */
      animation_: Object
    },

    computeIcon_: function(dropdownOpen, closedIcon, openIcon) {
      return dropdownOpen ? openIcon : closedIcon;
    },

    lowerBoundChanged_: function() {
      this.maxHeightValid_ = false;
      if (this.dropdownOpen)
        this.updateMaxHeight();
    },

    toggleDropdown: function() {
      this.dropdownOpen = !this.dropdownOpen;
      if (this.dropdownOpen) {
        this.$.dropdown.style.display = 'block';
        if (!this.maxHeightValid_)
          this.updateMaxHeight();
      }
      this.cancelAnimation_();
      this.playAnimation_(this.dropdownOpen);
    },

    updateMaxHeight: function() {
      var scrollContainer = this.$['scroll-container'];
      var height = this.lowerBound -
          scrollContainer.getBoundingClientRect().top -
          DROPDOWN_INNER_PADDING;
      height = Math.max(height, MIN_DROPDOWN_HEIGHT);
      scrollContainer.style.maxHeight = height + 'px';
      this.maxHeightValid_ = true;
    },

    cancelAnimation_: function() {
      if (this._animation)
        this._animation.cancel();
    },

    /**
     * Start an animation on the dropdown.
     * @param {boolean} isEntry True to play entry animation, false to play
     * exit.
     * @private
     */
    playAnimation_: function(isEntry) {
      this.animation_ = isEntry ? this.animateEntry_() : this.animateExit_();
      this.animation_.onfinish = function() {
        this.animation_ = null;
        if (!this.dropdownOpen)
          this.$.dropdown.style.display = 'none';
      }.bind(this);
    },

    animateEntry_: function() {
      var maxHeight = this.$.dropdown.getBoundingClientRect().height -
          DROPDOWN_OUTER_PADDING;

      if (maxHeight < 0)
        maxHeight = 0;

      var fade = new KeyframeEffect(this.$.dropdown, [
            {opacity: 0},
            {opacity: 1}
          ], {duration: 150, easing: 'cubic-bezier(0, 0, 0.2, 1)'});
      var slide = new KeyframeEffect(this.$.dropdown, [
            {height: '20px', transform: 'translateY(-10px)'},
            {height: maxHeight + 'px', transform: 'translateY(0)'}
          ], {duration: 250, easing: 'cubic-bezier(0, 0, 0.2, 1)'});

      return document.timeline.play(new GroupEffect([fade, slide]));
    },

    animateExit_: function() {
      return this.$.dropdown.animate([
            {transform: 'translateY(0)', opacity: 1},
            {transform: 'translateY(-5px)', opacity: 0}
          ], {duration: 100, easing: 'cubic-bezier(0.4, 0, 1, 1)'});
    }
  });

})();
/* Copyright 2015 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. */

#wrapper {
  transition: transform 250ms;
  transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
}

:host([closed]) #wrapper {
  /* 132px roughly flips the location of the button across the right edge of the
   * page. */
  transform: translateX(132px);
  transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
}

:host-context([dir=rtl]):host([closed]) #wrapper {
  transform: translateX(-132px);
}

paper-fab {
  --paper-fab-keyboard-focus-background: var(--viewer-icon-ink-color);
  --paper-fab-mini: {
    height: 36px;
    padding: 8px;
    width: 36px;
  };
  @apply(--shadow-elevation-4dp);
  background-color: rgb(242, 242, 242);
  color: rgb(96, 96, 96);
  overflow: visible;
}
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/polymer/v1_0/paper-fab/paper-fab.html">

<dom-module id="viewer-zoom-button">
  <link rel="import" type="css" href="../shared-icon-style.css">
  <link rel="import" type="css" href="viewer-zoom-button.css">
  <template>
    <div id="wrapper">
      <paper-fab id="button" mini icon="[[visibleIcon_]]" on-click="fireClick"
          aria-label$="[[visibleTooltip_]]" title="[[visibleTooltip_]]">
      </paper-fab>
    </div>
  </template>
</dom-module>
<script src="viewer-zoom-button.js"></script>
// Copyright 2015 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.

Polymer({
  is: 'viewer-zoom-button',

  properties: {
    /**
     * Icons to be displayed on the FAB. Multiple icons should be separated with
     * spaces, and will be cycled through every time the FAB is clicked.
     */
    icons: String,

    /**
     * Array version of the list of icons. Polymer does not allow array
     * properties to be set from HTML, so we must use a string property and
     * perform the conversion manually.
     * @private
     */
    icons_: {
      type: Array,
      value: [''],
      computed: 'computeIconsArray_(icons)'
    },

    tooltips: Array,

    closed: {
      type: Boolean,
      reflectToAttribute: true,
      value: false
    },

    delay: {
      type: Number,
      observer: 'delayChanged_'
    },

    /**
     * Index of the icon currently being displayed.
     */
    activeIndex: {
      type: Number,
      value: 0
    },

    /**
     * Icon currently being displayed on the FAB.
     * @private
     */
    visibleIcon_: {
      type: String,
      computed: 'computeVisibleIcon_(icons_, activeIndex)'
    },

    visibleTooltip_: {
      type: String,
      computed: 'computeVisibleTooltip_(tooltips, activeIndex)'
    }
  },

  computeIconsArray_: function(icons) {
    return icons.split(' ');
  },

  computeVisibleIcon_: function(icons, activeIndex) {
    return icons[activeIndex];
  },

  computeVisibleTooltip_: function(tooltips, activeIndex) {
    return tooltips[activeIndex];
  },

  delayChanged_: function() {
    this.$.wrapper.style.transitionDelay = this.delay + 'ms';
  },

  show: function() {
    this.closed = false;
  },

  hide: function() {
    this.closed = true;
  },

  fireClick: function() {
    // We cannot attach an on-click to the entire viewer-zoom-button, as this
    // will include clicks on the margins. Instead, proxy clicks on the FAB
    // through.
    this.fire('fabclick');

    this.activeIndex = (this.activeIndex + 1) % this.icons_.length;
  }
});
/* Copyright 2015 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. */

:host {
  -webkit-user-select: none;
  bottom: 0;
  padding: 48px 0;
  position: fixed;
  right: 0;
  z-index: 3;
}

:host-context([dir=rtl]) {
  left: 0;
  right: auto;
}

#zoom-buttons {
  position: relative;
  right: 48px;
}

:host-context([dir=rtl]) #zoom-buttons {
  left: 48px;
  right: auto;
}

viewer-zoom-button {
  display: block;
}

/* A small gap between the zoom in/zoom out buttons. */
#zoom-out-button {
  margin-top: 10px;
}

/* A larger gap between the fit button and bottom two buttons. */
#zoom-in-button {
  margin-top: 24px;
}
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/cr_elements/icons.html">
<link rel="import" href="../icons.html">
<link rel="import" href="viewer-zoom-button.html">

<dom-module id="viewer-zoom-toolbar">
  <link rel="import" type="css" href="viewer-zoom-toolbar.css">
  <template>

    <div id="zoom-buttons">
      <viewer-zoom-button id="fit-button" on-fabclick="fitToggle" delay="100"
          icons="pdf:fullscreen-exit cr:fullscreen">
      </viewer-zoom-button>
      <viewer-zoom-button id="zoom-in-button" icons="pdf:add"
          on-fabclick="zoomIn" delay="50"></viewer-zoom-button>
      <viewer-zoom-button id="zoom-out-button" icons="pdf:remove"
          on-fabclick="zoomOut" delay="0"></viewer-zoom-button>
    </div>
  </template>
</dom-module>
<script src="viewer-zoom-toolbar.js"></script>
// Copyright 2015 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.

(function() {

  var FIT_TO_PAGE = 0;
  var FIT_TO_WIDTH = 1;

  Polymer({
    is: 'viewer-zoom-toolbar',

    properties: {
      strings: {
        type: Object,
        observer: 'updateTooltips_'
      },

      visible_: {
        type: Boolean,
        value: true
      }
    },

    isVisible: function() {
      return this.visible_;
    },

    /**
     * @private
     * Change button tooltips to match any changes to localized strings.
     */
    updateTooltips_: function() {
      this.$['fit-button'].tooltips = [
          this.strings.tooltipFitToPage,
          this.strings.tooltipFitToWidth
      ];
      this.$['zoom-in-button'].tooltips = [this.strings.tooltipZoomIn];
      this.$['zoom-out-button'].tooltips = [this.strings.tooltipZoomOut];
    },

    /**
     * Handle clicks of the fit-button.
     */
    fitToggle: function() {
      if (this.$['fit-button'].activeIndex == FIT_TO_WIDTH)
        this.fire('fit-to-width');
      else
        this.fire('fit-to-page');
    },

    /**
     * Handle the keyboard shortcut equivalent of fit-button clicks.
     */
    fitToggleFromHotKey: function() {
      this.fitToggle();

      // Toggle the button state since there was no mouse click.
      var button = this.$['fit-button'];
      if (button.activeIndex == FIT_TO_WIDTH)
        button.activeIndex = FIT_TO_PAGE;
      else
        button.activeIndex = FIT_TO_WIDTH;
    },

    /**
     * Handle clicks of the zoom-in-button.
     */
    zoomIn: function() {
      this.fire('zoom-in');
    },

    /**
     * Handle clicks of the zoom-out-button.
     */
    zoomOut: function() {
      this.fire('zoom-out');
    },

    show: function() {
      if (!this.visible_) {
        this.visible_ = true;
        this.$['fit-button'].show();
        this.$['zoom-in-button'].show();
        this.$['zoom-out-button'].show();
      }
    },

    hide: function() {
      if (this.visible_) {
        this.visible_ = false;
        this.$['fit-button'].hide();
        this.$['zoom-in-button'].hide();
        this.$['zoom-out-button'].hide();
      }
    },
  });

})();