• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5'use strict';
6
7<include src="../../../../ui/webui/resources/js/util.js">
8<include src="pdf_scripting_api.js">
9<include src="viewport.js">
10
11/**
12 * @return {number} Width of a scrollbar in pixels
13 */
14function getScrollbarWidth() {
15  var div = document.createElement('div');
16  div.style.visibility = 'hidden';
17  div.style.overflow = 'scroll';
18  div.style.width = '50px';
19  div.style.height = '50px';
20  div.style.position = 'absolute';
21  document.body.appendChild(div);
22  var result = div.offsetWidth - div.clientWidth;
23  div.parentNode.removeChild(div);
24  return result;
25}
26
27/**
28 * The minimum number of pixels to offset the toolbar by from the bottom and
29 * right side of the screen.
30 */
31PDFViewer.MIN_TOOLBAR_OFFSET = 15;
32
33/**
34 * Creates a new PDFViewer. There should only be one of these objects per
35 * document.
36 */
37function PDFViewer() {
38  this.loaded = false;
39
40  // The sizer element is placed behind the plugin element to cause scrollbars
41  // to be displayed in the window. It is sized according to the document size
42  // of the pdf and zoom level.
43  this.sizer_ = $('sizer');
44  this.toolbar_ = $('toolbar');
45  this.pageIndicator_ = $('page-indicator');
46  this.progressBar_ = $('progress-bar');
47  this.passwordScreen_ = $('password-screen');
48  this.passwordScreen_.addEventListener('password-submitted',
49                                        this.onPasswordSubmitted_.bind(this));
50  this.errorScreen_ = $('error-screen');
51
52  // Create the viewport.
53  this.viewport_ = new Viewport(window,
54                                this.sizer_,
55                                this.viewportChangedCallback_.bind(this),
56                                getScrollbarWidth());
57
58  // Create the plugin object dynamically so we can set its src. The plugin
59  // element is sized to fill the entire window and is set to be fixed
60  // positioning, acting as a viewport. The plugin renders into this viewport
61  // according to the scroll position of the window.
62  this.plugin_ = document.createElement('object');
63  // NOTE: The plugin's 'id' field must be set to 'plugin' since
64  // chrome/renderer/printing/print_web_view_helper.cc actually references it.
65  this.plugin_.id = 'plugin';
66  this.plugin_.type = 'application/x-google-chrome-pdf';
67  this.plugin_.addEventListener('message', this.handlePluginMessage_.bind(this),
68                                false);
69
70  // Handle scripting messages from outside the extension that wish to interact
71  // with it. We also send a message indicating that extension has loaded and
72  // is ready to receive messages.
73  window.addEventListener('message', this.handleScriptingMessage_.bind(this),
74                          false);
75  this.sendScriptingMessage_({type: 'readyToReceive'});
76
77  // If the viewer is started from a MIME type request, there will be a
78  // background page and stream details object with the details of the request.
79  // Otherwise, we take the query string of the URL to indicate the URL of the
80  // PDF to load. This is used for print preview in particular.
81  if (chrome.extension.getBackgroundPage &&
82      chrome.extension.getBackgroundPage()) {
83    this.streamDetails =
84        chrome.extension.getBackgroundPage().popStreamDetails();
85  }
86
87  if (!this.streamDetails) {
88    // The URL of this page will be of the form
89    // "chrome-extension://<extension id>?<pdf url>". We pull out the <pdf url>
90    // part here.
91    var url = window.location.search.substring(1);
92    this.streamDetails = {
93      streamUrl: url,
94      originalUrl: url,
95      responseHeaders: ''
96    };
97  }
98
99  this.plugin_.setAttribute('src', this.streamDetails.originalUrl);
100  this.plugin_.setAttribute('stream-url', this.streamDetails.streamUrl);
101  var headers = '';
102  for (var header in this.streamDetails.responseHeaders) {
103    headers += header + ': ' +
104        this.streamDetails.responseHeaders[header] + '\n';
105  }
106  this.plugin_.setAttribute('headers', headers);
107
108  if (window.top == window)
109    this.plugin_.setAttribute('full-frame', '');
110  document.body.appendChild(this.plugin_);
111
112  // Setup the button event listeners.
113  $('fit-to-width-button').addEventListener('click',
114      this.viewport_.fitToWidth.bind(this.viewport_));
115  $('fit-to-page-button').addEventListener('click',
116      this.viewport_.fitToPage.bind(this.viewport_));
117  $('zoom-in-button').addEventListener('click',
118      this.viewport_.zoomIn.bind(this.viewport_));
119  $('zoom-out-button').addEventListener('click',
120      this.viewport_.zoomOut.bind(this.viewport_));
121  $('save-button-link').href = this.streamDetails.originalUrl;
122  $('print-button').addEventListener('click', this.print_.bind(this));
123
124  // Setup the keyboard event listener.
125  document.onkeydown = this.handleKeyEvent_.bind(this);
126}
127
128PDFViewer.prototype = {
129  /**
130   * @private
131   * Handle key events. These may come from the user directly or via the
132   * scripting API.
133   * @param {KeyboardEvent} e the event to handle.
134   */
135  handleKeyEvent_: function(e) {
136    var position = this.viewport_.position;
137    // Certain scroll events may be sent from outside of the extension.
138    var fromScriptingAPI = e.type == 'scriptingKeypress';
139
140    switch (e.keyCode) {
141      case 33:  // Page up key.
142        // Go to the previous page if we are fit-to-page.
143        if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
144          this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
145          // Since we do the movement of the page.
146          e.preventDefault();
147        } else if (fromScriptingAPI) {
148          position.y -= this.viewport.size.height;
149          this.viewport.position = position;
150        }
151        return;
152      case 34:  // Page down key.
153        // Go to the next page if we are fit-to-page.
154        if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
155          this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
156          // Since we do the movement of the page.
157          e.preventDefault();
158        } else if (fromScriptingAPI) {
159          position.y += this.viewport.size.height;
160          this.viewport.position = position;
161        }
162        return;
163      case 37:  // Left arrow key.
164        // Go to the previous page if there are no horizontal scrollbars.
165        if (!this.viewport_.documentHasScrollbars().x) {
166          this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
167          // Since we do the movement of the page.
168          e.preventDefault();
169        } else if (fromScriptingAPI) {
170          position.x -= Viewport.SCROLL_INCREMENT;
171          this.viewport.position = position;
172        }
173        return;
174      case 38:  // Up arrow key.
175        if (fromScriptingAPI) {
176          position.y -= Viewport.SCROLL_INCREMENT;
177          this.viewport.position = position;
178        }
179        return;
180      case 39:  // Right arrow key.
181        // Go to the next page if there are no horizontal scrollbars.
182        if (!this.viewport_.documentHasScrollbars().x) {
183          this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
184          // Since we do the movement of the page.
185          e.preventDefault();
186        } else if (fromScriptingAPI) {
187          position.x += Viewport.SCROLL_INCREMENT;
188          this.viewport.position = position;
189        }
190        return;
191      case 40:  // Down arrow key.
192        if (fromScriptingAPI) {
193          position.y += Viewport.SCROLL_INCREMENT;
194          this.viewport.position = position;
195        }
196        return;
197      case 187:  // +/= key.
198      case 107:  // Numpad + key.
199        if (e.ctrlKey || e.metaKey) {
200          this.viewport_.zoomIn();
201          // Since we do the zooming of the page.
202          e.preventDefault();
203        }
204        return;
205      case 189:  // -/_ key.
206      case 109:  // Numpad - key.
207        if (e.ctrlKey || e.metaKey) {
208          this.viewport_.zoomOut();
209          // Since we do the zooming of the page.
210          e.preventDefault();
211        }
212        return;
213      case 83:  // s key.
214        if (e.ctrlKey || e.metaKey) {
215          // Simulate a click on the button so that the <a download ...>
216          // attribute is used.
217          $('save-button-link').click();
218          // Since we do the saving of the page.
219          e.preventDefault();
220        }
221        return;
222      case 80:  // p key.
223        if (e.ctrlKey || e.metaKey) {
224          this.print_();
225          // Since we do the printing of the page.
226          e.preventDefault();
227        }
228        return;
229    }
230  },
231
232  /**
233   * @private
234   * Notify the plugin to print.
235   */
236  print_: function() {
237    this.plugin_.postMessage({
238      type: 'print',
239    });
240  },
241
242  /**
243   * @private
244   * Update the loading progress of the document in response to a progress
245   * message being received from the plugin.
246   * @param {number} progress the progress as a percentage.
247   */
248  updateProgress_: function(progress) {
249    this.progressBar_.progress = progress;
250    if (progress == -1) {
251      // Document load failed.
252      this.errorScreen_.style.visibility = 'visible';
253      this.sizer_.style.display = 'none';
254      this.toolbar_.style.visibility = 'hidden';
255      if (this.passwordScreen_.active) {
256        this.passwordScreen_.deny();
257        this.passwordScreen_.active = false;
258      }
259    } else if (progress == 100) {
260      // Document load complete.
261      this.loaded = true;
262      var loadEvent = new Event('pdfload');
263      window.dispatchEvent(loadEvent);
264      this.sendScriptingMessage_({
265        type: 'documentLoaded'
266      });
267      if (this.lastViewportPosition_)
268        this.viewport_.position = this.lastViewportPosition_;
269    }
270  },
271
272  /**
273   * @private
274   * An event handler for handling password-submitted events. These are fired
275   * when an event is entered into the password screen.
276   * @param {Object} event a password-submitted event.
277   */
278  onPasswordSubmitted_: function(event) {
279    this.plugin_.postMessage({
280      type: 'getPasswordComplete',
281      password: event.detail.password
282    });
283  },
284
285  /**
286   * @private
287   * An event handler for handling message events received from the plugin.
288   * @param {MessageObject} message a message event.
289   */
290  handlePluginMessage_: function(message) {
291    switch (message.data.type.toString()) {
292      case 'documentDimensions':
293        this.documentDimensions_ = message.data;
294        this.viewport_.setDocumentDimensions(this.documentDimensions_);
295        this.toolbar_.style.visibility = 'visible';
296        // If we received the document dimensions, the password was good so we
297        // can dismiss the password screen.
298        if (this.passwordScreen_.active)
299          this.passwordScreen_.accept();
300
301        this.pageIndicator_.initialFadeIn();
302        this.toolbar_.initialFadeIn();
303        break;
304      case 'email':
305        var href = 'mailto:' + message.data.to + '?cc=' + message.data.cc +
306            '&bcc=' + message.data.bcc + '&subject=' + message.data.subject +
307            '&body=' + message.data.body;
308        var w = window.open(href, '_blank', 'width=1,height=1');
309        if (w)
310          w.close();
311        break;
312      case 'getAccessibilityJSONReply':
313        this.sendScriptingMessage_(message.data);
314        break;
315      case 'getPassword':
316        // If the password screen isn't up, put it up. Otherwise we're
317        // responding to an incorrect password so deny it.
318        if (!this.passwordScreen_.active)
319          this.passwordScreen_.active = true;
320        else
321          this.passwordScreen_.deny();
322        break;
323      case 'goToPage':
324        this.viewport_.goToPage(message.data.page);
325        break;
326      case 'loadProgress':
327        this.updateProgress_(message.data.progress);
328        break;
329      case 'navigate':
330        if (message.data.newTab)
331          window.open(message.data.url);
332        else
333          window.location.href = message.data.url;
334        break;
335      case 'setScrollPosition':
336        var position = this.viewport_.position;
337        if (message.data.x != undefined)
338          position.x = message.data.x;
339        if (message.data.y != undefined)
340          position.y = message.data.y;
341        this.viewport_.position = position;
342        break;
343      case 'setTranslatedStrings':
344        this.passwordScreen_.text = message.data.getPasswordString;
345        this.progressBar_.text = message.data.loadingString;
346        this.errorScreen_.text = message.data.loadFailedString;
347        break;
348      case 'cancelStreamUrl':
349        chrome.streamsPrivate.abort(this.streamDetails.streamUrl);
350        break;
351    }
352  },
353
354  /**
355   * @private
356   * A callback that's called when the viewport changes.
357   */
358  viewportChangedCallback_: function() {
359    if (!this.documentDimensions_)
360      return;
361
362    // Update the buttons selected.
363    $('fit-to-page-button').classList.remove('polymer-selected');
364    $('fit-to-width-button').classList.remove('polymer-selected');
365    if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
366      $('fit-to-page-button').classList.add('polymer-selected');
367    } else if (this.viewport_.fittingType ==
368               Viewport.FittingType.FIT_TO_WIDTH) {
369      $('fit-to-width-button').classList.add('polymer-selected');
370    }
371
372    var hasScrollbars = this.viewport_.documentHasScrollbars();
373    var scrollbarWidth = this.viewport_.scrollbarWidth;
374    // Offset the toolbar position so that it doesn't move if scrollbars appear.
375    var toolbarRight = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
376    var toolbarBottom = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
377    if (hasScrollbars.vertical)
378      toolbarRight -= scrollbarWidth;
379    if (hasScrollbars.horizontal)
380      toolbarBottom -= scrollbarWidth;
381    this.toolbar_.style.right = toolbarRight + 'px';
382    this.toolbar_.style.bottom = toolbarBottom + 'px';
383
384    // Update the page indicator.
385    var visiblePage = this.viewport_.getMostVisiblePage();
386    this.pageIndicator_.index = visiblePage;
387    if (this.documentDimensions_.pageDimensions.length > 1 &&
388        hasScrollbars.vertical) {
389      this.pageIndicator_.style.visibility = 'visible';
390    } else {
391      this.pageIndicator_.style.visibility = 'hidden';
392    }
393
394    var position = this.viewport_.position;
395    var zoom = this.viewport_.zoom;
396    // Notify the plugin of the viewport change.
397    this.plugin_.postMessage({
398      type: 'viewport',
399      zoom: zoom,
400      xOffset: position.x,
401      yOffset: position.y
402    });
403
404    var visiblePageDimensions = this.viewport_.getPageScreenRect(visiblePage);
405    var size = this.viewport_.size;
406    this.sendScriptingMessage_({
407      type: 'viewport',
408      pageX: visiblePageDimensions.x,
409      pageY: visiblePageDimensions.y,
410      pageWidth: visiblePageDimensions.width,
411      viewportWidth: size.width,
412      viewportHeight: size.height,
413    });
414  },
415
416  /**
417   * @private
418   * Handle a scripting message from outside the extension (typically sent by
419   * PDFScriptingAPI in a page containing the extension) to interact with the
420   * plugin.
421   * @param {MessageObject} message the message to handle.
422   */
423  handleScriptingMessage_: function(message) {
424    switch (message.data.type.toString()) {
425      case 'getAccessibilityJSON':
426      case 'loadPreviewPage':
427        this.plugin_.postMessage(message.data);
428        break;
429      case 'resetPrintPreviewMode':
430        if (!this.inPrintPreviewMode_) {
431          this.inPrintPreviewMode_ = true;
432          this.viewport_.fitToPage();
433        }
434
435        // Stash the scroll location so that it can be restored when the new
436        // document is loaded.
437        this.lastViewportPosition_ = this.viewport_.position;
438
439        // TODO(raymes): Disable these properly in the plugin.
440        var printButton = $('print-button');
441        if (printButton)
442          printButton.parentNode.removeChild(printButton);
443        var saveButton = $('save-button');
444        if (saveButton)
445          saveButton.parentNode.removeChild(saveButton);
446
447        this.pageIndicator_.pageLabels = message.data.pageNumbers;
448
449        this.plugin_.postMessage({
450          type: 'resetPrintPreviewMode',
451          url: message.data.url,
452          grayscale: message.data.grayscale,
453          // If the PDF isn't modifiable we send 0 as the page count so that no
454          // blank placeholder pages get appended to the PDF.
455          pageCount: (message.data.modifiable ?
456                      message.data.pageNumbers.length : 0)
457        });
458        break;
459      case 'sendKeyEvent':
460        var e = document.createEvent('Event');
461        e.initEvent('scriptingKeypress');
462        e.keyCode = message.data.keyCode;
463        this.handleKeyEvent_(e);
464        break;
465    }
466
467  },
468
469  /**
470   * @private
471   * Send a scripting message outside the extension (typically to
472   * PDFScriptingAPI in a page containing the extension).
473   * @param {Object} message the message to send.
474   */
475  sendScriptingMessage_: function(message) {
476    window.parent.postMessage(message, '*');
477  },
478
479  /**
480   * @type {Viewport} the viewport of the PDF viewer.
481   */
482  get viewport() {
483    return this.viewport_;
484  }
485};
486
487var viewer = new PDFViewer();
488