ozone: evdev: Sync caps lock LED state to evdev
[chromium-blink-merge.git] / chrome / browser / resources / pdf / pdf.js
blobc652ca4f3ad0065447aa2a45c19b8c7517bbb633
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.
5 'use strict';
7 /**
8  * @return {number} Width of a scrollbar in pixels
9  */
10 function getScrollbarWidth() {
11   var div = document.createElement('div');
12   div.style.visibility = 'hidden';
13   div.style.overflow = 'scroll';
14   div.style.width = '50px';
15   div.style.height = '50px';
16   div.style.position = 'absolute';
17   document.body.appendChild(div);
18   var result = div.offsetWidth - div.clientWidth;
19   div.parentNode.removeChild(div);
20   return result;
23 /**
24  * Return the filename component of a URL.
25  * @param {string} url The URL to get the filename from.
26  * @return {string} The filename component.
27  */
28 function getFilenameFromURL(url) {
29   var components = url.split(/\/|\\/);
30   return components[components.length - 1];
33 /**
34  * Called when navigation happens in the current tab.
35  * @param {string} url The url to be opened in the current tab.
36  */
37 function onNavigateInCurrentTab(url) {
38   window.location.href = url;
41 /**
42  * Called when navigation happens in the new tab.
43  * @param {string} url The url to be opened in the new tab.
44  */
45 function onNavigateInNewTab(url) {
46   // Prefer the tabs API because it guarantees we can just open a new tab.
47   // window.open doesn't have this guarantee.
48   if (chrome.tabs)
49     chrome.tabs.create({ url: url});
50   else
51     window.open(url);
54 /**
55  * The minimum number of pixels to offset the toolbar by from the bottom and
56  * right side of the screen.
57  */
58 PDFViewer.MIN_TOOLBAR_OFFSET = 15;
60 /**
61  * Creates a new PDFViewer. There should only be one of these objects per
62  * document.
63  * @constructor
64  * @param {Object} streamDetails The stream object which points to the data
65  *     contained in the PDF.
66  */
67 function PDFViewer(streamDetails) {
68   this.streamDetails_ = streamDetails;
69   this.loaded_ = false;
70   this.parentWindow_ = null;
72   this.delayedScriptingMessages_ = [];
74   this.isPrintPreview_ =
75       this.streamDetails_.originalUrl.indexOf('chrome://print') == 0;
76   this.isMaterial_ = location.pathname.substring(1) === 'index-material.html';
78   // The sizer element is placed behind the plugin element to cause scrollbars
79   // to be displayed in the window. It is sized according to the document size
80   // of the pdf and zoom level.
81   this.sizer_ = $('sizer');
82   this.toolbar_ = $('toolbar');
83   this.pageIndicator_ = $('page-indicator');
84   this.progressBar_ = $('progress-bar');
85   this.passwordScreen_ = $('password-screen');
86   this.passwordScreen_.addEventListener('password-submitted',
87                                         this.onPasswordSubmitted_.bind(this));
88   this.errorScreen_ = $('error-screen');
90   // Create the viewport.
91   this.viewport_ = new Viewport(window,
92                                 this.sizer_,
93                                 this.viewportChanged_.bind(this),
94                                 this.beforeZoom_.bind(this),
95                                 this.afterZoom_.bind(this),
96                                 getScrollbarWidth());
98   // Create the plugin object dynamically so we can set its src. The plugin
99   // element is sized to fill the entire window and is set to be fixed
100   // positioning, acting as a viewport. The plugin renders into this viewport
101   // according to the scroll position of the window.
102   this.plugin_ = document.createElement('embed');
103   // NOTE: The plugin's 'id' field must be set to 'plugin' since
104   // chrome/renderer/printing/print_web_view_helper.cc actually references it.
105   this.plugin_.id = 'plugin';
106   this.plugin_.type = 'application/x-google-chrome-pdf';
107   this.plugin_.addEventListener('message', this.handlePluginMessage_.bind(this),
108                                 false);
110   // Handle scripting messages from outside the extension that wish to interact
111   // with it. We also send a message indicating that extension has loaded and
112   // is ready to receive messages.
113   window.addEventListener('message', this.handleScriptingMessage.bind(this),
114                           false);
116   document.title = getFilenameFromURL(this.streamDetails_.originalUrl);
117   this.plugin_.setAttribute('src', this.streamDetails_.originalUrl);
118   this.plugin_.setAttribute('stream-url', this.streamDetails_.streamUrl);
119   var headers = '';
120   for (var header in this.streamDetails_.responseHeaders) {
121     headers += header + ': ' +
122         this.streamDetails_.responseHeaders[header] + '\n';
123   }
124   this.plugin_.setAttribute('headers', headers);
126   if (this.isMaterial_)
127     this.plugin_.setAttribute('is-material', '');
129   if (!this.streamDetails_.embedded)
130     this.plugin_.setAttribute('full-frame', '');
131   document.body.appendChild(this.plugin_);
133   // Setup the button event listeners.
134   if (!this.isMaterial_) {
135     $('fit-to-width-button').addEventListener('click',
136         this.viewport_.fitToWidth.bind(this.viewport_));
137     $('fit-to-page-button').addEventListener('click',
138         this.viewport_.fitToPage.bind(this.viewport_));
139     $('zoom-in-button').addEventListener('click',
140         this.viewport_.zoomIn.bind(this.viewport_));
141     $('zoom-out-button').addEventListener('click',
142         this.viewport_.zoomOut.bind(this.viewport_));
143     $('save-button').addEventListener('click', this.save_.bind(this));
144     $('print-button').addEventListener('click', this.print_.bind(this));
145   }
147   if (this.isMaterial_) {
148     this.bookmarksPane_ = $('bookmarks-pane');
150     this.zoomSelector_ = $('zoom-selector');
151     this.zoomSelector_.zoomMin = Viewport.ZOOM_FACTOR_RANGE.min * 100;
152     this.zoomSelector_.zoomMax = Viewport.ZOOM_FACTOR_RANGE.max * 100;
153     this.zoomSelector_.addEventListener('zoom', function(e) {
154       this.viewport_.setZoom(e.detail.zoom);
155     }.bind(this));
156     this.zoomSelector_.addEventListener('fit-to-width',
157         this.viewport_.fitToWidth.bind(this.viewport_));
158     this.zoomSelector_.addEventListener('fit-to-page',
159         this.viewport_.fitToPage.bind(this.viewport_));
161     this.materialToolbar_ = $('material-toolbar');
162     this.materialToolbar_.docTitle = document.title;
163     this.materialToolbar_.addEventListener('save', this.save_.bind(this));
164     this.materialToolbar_.addEventListener('print', this.print_.bind(this));
165     this.materialToolbar_.addEventListener('rotate-right',
166         this.rotateClockwise_.bind(this));
167     this.materialToolbar_.addEventListener('toggle-bookmarks', function() {
168       this.bookmarksPane_.buttonToggle();
169     }.bind(this));
171     document.body.addEventListener('change-page', function(e) {
172       this.viewport_.goToPage(e.detail.page);
173     }.bind(this));
175     this.uiManager_ = new UiManager(window, this.materialToolbar_,
176                                     [this.bookmarksPane_]);
177   }
179   // Setup the keyboard event listener.
180   document.onkeydown = this.handleKeyEvent_.bind(this);
182   // Set up the zoom API.
183   if (this.shouldManageZoom_()) {
184     chrome.tabs.setZoomSettings(this.streamDetails_.tabId,
185                                 {mode: 'manual', scope: 'per-tab'},
186                                 this.afterZoom_.bind(this));
187     chrome.tabs.onZoomChange.addListener(function(zoomChangeInfo) {
188       if (zoomChangeInfo.tabId != this.streamDetails_.tabId)
189         return;
190       // If the zoom level is close enough to the current zoom level, don't
191       // change it. This avoids us getting into an infinite loop of zoom changes
192       // due to floating point error.
193       var MIN_ZOOM_DELTA = 0.01;
194       var zoomDelta = Math.abs(this.viewport_.zoom -
195                                zoomChangeInfo.newZoomFactor);
196       // We should not change zoom level when we are responsible for initiating
197       // the zoom. onZoomChange() is called before setZoomComplete() callback
198       // when we initiate the zoom.
199       if ((zoomDelta > MIN_ZOOM_DELTA) && !this.setZoomInProgress_)
200         this.viewport_.setZoom(zoomChangeInfo.newZoomFactor);
201     }.bind(this));
202   }
204   // Parse open pdf parameters.
205   this.paramsParser_ =
206       new OpenPDFParamsParser(this.getNamedDestination_.bind(this));
207   this.navigator_ = new Navigator(this.streamDetails_.originalUrl,
208                                   this.viewport_, this.paramsParser_,
209                                   onNavigateInCurrentTab, onNavigateInNewTab);
210   this.viewportScroller_ =
211       new ViewportScroller(this.viewport_, this.plugin_, window);
214 PDFViewer.prototype = {
215   /**
216    * @private
217    * Handle key events. These may come from the user directly or via the
218    * scripting API.
219    * @param {KeyboardEvent} e the event to handle.
220    */
221   handleKeyEvent_: function(e) {
222     var position = this.viewport_.position;
223     // Certain scroll events may be sent from outside of the extension.
224     var fromScriptingAPI = e.fromScriptingAPI;
226     var pageUpHandler = function() {
227       // Go to the previous page if we are fit-to-page.
228       if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
229         this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
230         // Since we do the movement of the page.
231         e.preventDefault();
232       } else if (fromScriptingAPI) {
233         position.y -= this.viewport.size.height;
234         this.viewport.position = position;
235       }
236     }.bind(this);
237     var pageDownHandler = function() {
238       // Go to the next page if we are fit-to-page.
239       if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
240         this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
241         // Since we do the movement of the page.
242         e.preventDefault();
243       } else if (fromScriptingAPI) {
244         position.y += this.viewport.size.height;
245         this.viewport.position = position;
246       }
247     }.bind(this);
249     switch (e.keyCode) {
250       case 32:  // Space key.
251         if (e.shiftKey)
252           pageUpHandler();
253         else
254           pageDownHandler();
255         return;
256       case 33:  // Page up key.
257         pageUpHandler();
258         return;
259       case 34:  // Page down key.
260         pageDownHandler();
261         return;
262       case 37:  // Left arrow key.
263         if (!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
264           // Go to the previous page if there are no horizontal scrollbars.
265           if (!this.viewport_.documentHasScrollbars().horizontal) {
266             this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
267             // Since we do the movement of the page.
268             e.preventDefault();
269           } else if (fromScriptingAPI) {
270             position.x -= Viewport.SCROLL_INCREMENT;
271             this.viewport.position = position;
272           }
273         }
274         return;
275       case 38:  // Up arrow key.
276         if (fromScriptingAPI) {
277           position.y -= Viewport.SCROLL_INCREMENT;
278           this.viewport.position = position;
279         }
280         return;
281       case 39:  // Right arrow key.
282         if (!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
283           // Go to the next page if there are no horizontal scrollbars.
284           if (!this.viewport_.documentHasScrollbars().horizontal) {
285             this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
286             // Since we do the movement of the page.
287             e.preventDefault();
288           } else if (fromScriptingAPI) {
289             position.x += Viewport.SCROLL_INCREMENT;
290             this.viewport.position = position;
291           }
292         }
293         return;
294       case 40:  // Down arrow key.
295         if (fromScriptingAPI) {
296           position.y += Viewport.SCROLL_INCREMENT;
297           this.viewport.position = position;
298         }
299         return;
300       case 65:  // a key.
301         if (e.ctrlKey || e.metaKey) {
302           this.plugin_.postMessage({
303             type: 'selectAll'
304           });
305           // Since we do selection ourselves.
306           e.preventDefault();
307         }
308         return;
309       case 71: // g key.
310         if (this.isMaterial_ && (e.ctrlKey || e.metaKey)) {
311           this.materialToolbar_.selectPageNumber();
312           // To prevent the default "find text" behaviour in Chrome.
313           e.preventDefault();
314         }
315         return;
316       case 80:  // p key.
317         if (e.ctrlKey || e.metaKey) {
318           this.print_();
319           // Since we do the printing of the page.
320           e.preventDefault();
321         }
322         return;
323       case 219:  // left bracket.
324         if (e.ctrlKey)
325           this.rotateCounterClockwise_();
326         return;
327       case 221:  // right bracket.
328         if (e.ctrlKey)
329           this.rotateClockwise_();
330         return;
331     }
333     // Give print preview a chance to handle the key event.
334     if (!fromScriptingAPI && this.isPrintPreview_) {
335       this.sendScriptingMessage_({
336         type: 'sendKeyEvent',
337         keyEvent: SerializeKeyEvent(e)
338       });
339     }
340   },
342   /**
343    * @private
344    * Rotate the plugin clockwise.
345    */
346   rotateClockwise_: function() {
347     this.plugin_.postMessage({
348       type: 'rotateClockwise'
349     });
350   },
352   /**
353    * @private
354    * Rotate the plugin counter-clockwise.
355    */
356   rotateCounterClockwise_: function() {
357     this.plugin_.postMessage({
358       type: 'rotateCounterclockwise'
359     });
360   },
362   /**
363    * @private
364    * Notify the plugin to print.
365    */
366   print_: function() {
367     this.plugin_.postMessage({
368       type: 'print'
369     });
370   },
372   /**
373    * @private
374    * Notify the plugin to save.
375    */
376   save_: function() {
377     this.plugin_.postMessage({
378       type: 'save'
379     });
380   },
382   /**
383    * Fetches the page number corresponding to the given named destination from
384    * the plugin.
385    * @param {string} name The namedDestination to fetch page number from plugin.
386    */
387   getNamedDestination_: function(name) {
388     this.plugin_.postMessage({
389       type: 'getNamedDestination',
390       namedDestination: name
391     });
392   },
394   /**
395    * @private
396    * Handle open pdf parameters. This function updates the viewport as per
397    * the parameters mentioned in the url while opening pdf. The order is
398    * important as later actions can override the effects of previous actions.
399    * @param {Object} viewportPosition The initial position of the viewport to be
400    *     displayed.
401    */
402   handleURLParams_: function(viewportPosition) {
403     if (viewportPosition.page != undefined)
404       this.viewport_.goToPage(viewportPosition.page);
405     if (viewportPosition.position) {
406       // Make sure we don't cancel effect of page parameter.
407       this.viewport_.position = {
408         x: this.viewport_.position.x + viewportPosition.position.x,
409         y: this.viewport_.position.y + viewportPosition.position.y
410       };
411     }
412     if (viewportPosition.zoom)
413       this.viewport_.setZoom(viewportPosition.zoom);
414   },
416   /**
417    * @private
418    * Update the loading progress of the document in response to a progress
419    * message being received from the plugin.
420    * @param {number} progress the progress as a percentage.
421    */
422   updateProgress_: function(progress) {
423     if (this.isMaterial_)
424       this.materialToolbar_.loadProgress = progress;
425     else
426       this.progressBar_.progress = progress;
428     if (progress == -1) {
429       // Document load failed.
430       this.errorScreen_.style.visibility = 'visible';
431       this.sizer_.style.display = 'none';
432       if (!this.isMaterial_)
433         this.toolbar_.style.visibility = 'hidden';
434       if (this.passwordScreen_.active) {
435         this.passwordScreen_.deny();
436         this.passwordScreen_.active = false;
437       }
438     } else if (progress == 100) {
439       // Document load complete.
440       if (this.lastViewportPosition_)
441         this.viewport_.position = this.lastViewportPosition_;
442       this.paramsParser_.getViewportFromUrlParams(
443           this.streamDetails_.originalUrl, this.handleURLParams_.bind(this));
444       this.loaded_ = true;
445       this.sendScriptingMessage_({
446         type: 'documentLoaded'
447       });
448       while (this.delayedScriptingMessages_.length > 0)
449         this.handleScriptingMessage(this.delayedScriptingMessages_.shift());
451       if (this.isMaterial_)
452         this.uiManager_.hideUiAfterTimeout();
453     }
454   },
456   /**
457    * @private
458    * An event handler for handling password-submitted events. These are fired
459    * when an event is entered into the password screen.
460    * @param {Object} event a password-submitted event.
461    */
462   onPasswordSubmitted_: function(event) {
463     this.plugin_.postMessage({
464       type: 'getPasswordComplete',
465       password: event.detail.password
466     });
467   },
469   /**
470    * @private
471    * An event handler for handling message events received from the plugin.
472    * @param {MessageObject} message a message event.
473    */
474   handlePluginMessage_: function(message) {
475     switch (message.data.type.toString()) {
476       case 'documentDimensions':
477         this.documentDimensions_ = message.data;
478         this.viewport_.setDocumentDimensions(this.documentDimensions_);
479         // If we received the document dimensions, the password was good so we
480         // can dismiss the password screen.
481         if (this.passwordScreen_.active)
482           this.passwordScreen_.accept();
484         if (this.isMaterial_) {
485           this.materialToolbar_.docLength =
486               this.documentDimensions_.pageDimensions.length;
487         } else {
488           this.pageIndicator_.initialFadeIn();
489           this.toolbar_.initialFadeIn();
490         }
492         break;
493       case 'email':
494         var href = 'mailto:' + message.data.to + '?cc=' + message.data.cc +
495             '&bcc=' + message.data.bcc + '&subject=' + message.data.subject +
496             '&body=' + message.data.body;
497         window.location.href = href;
498         break;
499       case 'getAccessibilityJSONReply':
500         this.sendScriptingMessage_(message.data);
501         break;
502       case 'getPassword':
503         // If the password screen isn't up, put it up. Otherwise we're
504         // responding to an incorrect password so deny it.
505         if (!this.passwordScreen_.active)
506           this.passwordScreen_.active = true;
507         else
508           this.passwordScreen_.deny();
509         break;
510       case 'getSelectedTextReply':
511         this.sendScriptingMessage_(message.data);
512         break;
513       case 'goToPage':
514         this.viewport_.goToPage(message.data.page);
515         break;
516       case 'loadProgress':
517         this.updateProgress_(message.data.progress);
518         break;
519       case 'navigate':
520         // If in print preview, always open a new tab.
521         if (this.isPrintPreview_)
522           this.navigator_.navigate(message.data.url, true);
523         else
524           this.navigator_.navigate(message.data.url, message.data.newTab);
525         break;
526       case 'setScrollPosition':
527         var position = this.viewport_.position;
528         if (message.data.x !== undefined)
529           position.x = message.data.x;
530         if (message.data.y !== undefined)
531           position.y = message.data.y;
532         this.viewport_.position = position;
533         break;
534       case 'setTranslatedStrings':
535         this.passwordScreen_.text = message.data.getPasswordString;
536         if (!this.isMaterial_) {
537           this.progressBar_.text = message.data.loadingString;
538           if (!this.isPrintPreview_)
539             this.progressBar_.style.visibility = 'visible';
540         }
541         this.errorScreen_.text = message.data.loadFailedString;
542         break;
543       case 'cancelStreamUrl':
544         chrome.mimeHandlerPrivate.abortStream();
545         break;
546       case 'bookmarks':
547         this.bookmarks_ = message.data.bookmarks;
548         if (this.isMaterial_ && this.bookmarks_.length !== 0) {
549           $('bookmarks-container').bookmarks = this.bookmarks;
550           this.materialToolbar_.hasBookmarks = true;
551         }
552         break;
553       case 'setIsSelecting':
554         this.viewportScroller_.setEnableScrolling(message.data.isSelecting);
555         break;
556       case 'getNamedDestinationReply':
557         this.paramsParser_.onNamedDestinationReceived(
558             message.data.pageNumber);
559         break;
560     }
561   },
563   /**
564    * @private
565    * A callback that's called before the zoom changes. Notify the plugin to stop
566    * reacting to scroll events while zoom is taking place to avoid flickering.
567    */
568   beforeZoom_: function() {
569     this.plugin_.postMessage({
570       type: 'stopScrolling'
571     });
572   },
574   /**
575    * @private
576    * A callback that's called after the zoom changes. Notify the plugin of the
577    * zoom change and to continue reacting to scroll events.
578    */
579   afterZoom_: function() {
580     var position = this.viewport_.position;
581     var zoom = this.viewport_.zoom;
582     if (this.isMaterial_)
583       this.zoomSelector_.zoomValue = 100 * zoom;
584     if (this.shouldManageZoom_() && !this.setZoomInProgress_) {
585       this.setZoomInProgress_ = true;
586       chrome.tabs.setZoom(this.streamDetails_.tabId, zoom,
587                           this.setZoomComplete_.bind(this, zoom));
588     }
589     this.plugin_.postMessage({
590       type: 'viewport',
591       zoom: zoom,
592       xOffset: position.x,
593       yOffset: position.y
594     });
595   },
597   /**
598    * @private
599    * A callback that's called after chrome.tabs.setZoom is complete. This will
600    * call chrome.tabs.setZoom again if the zoom level has changed since it was
601    * last called.
602    * @param {number} lastZoom the zoom level that chrome.tabs.setZoom was called
603    *     with.
604    */
605   setZoomComplete_: function(lastZoom) {
606     var zoom = this.viewport_.zoom;
607     if (zoom !== lastZoom) {
608       chrome.tabs.setZoom(this.streamDetails_.tabId, zoom,
609                           this.setZoomComplete_.bind(this, zoom));
610     } else {
611       this.setZoomInProgress_ = false;
612     }
613   },
615   /**
616    * @private
617    * A callback that's called after the viewport changes.
618    */
619   viewportChanged_: function() {
620     if (!this.documentDimensions_)
621       return;
623     // Update the buttons selected.
624     if (!this.isMaterial_) {
625       $('fit-to-page-button').classList.remove('polymer-selected');
626       $('fit-to-width-button').classList.remove('polymer-selected');
627       if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
628         $('fit-to-page-button').classList.add('polymer-selected');
629       } else if (this.viewport_.fittingType ==
630                  Viewport.FittingType.FIT_TO_WIDTH) {
631         $('fit-to-width-button').classList.add('polymer-selected');
632       }
633     }
635     // Offset the toolbar position so that it doesn't move if scrollbars appear.
636     var hasScrollbars = this.viewport_.documentHasScrollbars();
637     var scrollbarWidth = this.viewport_.scrollbarWidth;
638     var verticalScrollbarWidth = hasScrollbars.vertical ? scrollbarWidth : 0;
639     var horizontalScrollbarWidth =
640         hasScrollbars.horizontal ? scrollbarWidth : 0;
641     var toolbarRight = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
642     var toolbarBottom = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
643     toolbarRight -= verticalScrollbarWidth;
644     toolbarBottom -= horizontalScrollbarWidth;
645     if (!this.isMaterial_) {
646       this.toolbar_.style.right = toolbarRight + 'px';
647       this.toolbar_.style.bottom = toolbarBottom + 'px';
648       // Hide the toolbar if it doesn't fit in the viewport.
649       if (this.toolbar_.offsetLeft < 0 || this.toolbar_.offsetTop < 0)
650         this.toolbar_.style.visibility = 'hidden';
651       else
652         this.toolbar_.style.visibility = 'visible';
653     }
655     // Update the page indicator.
656     var visiblePage = this.viewport_.getMostVisiblePage();
657     if (this.isMaterial_) {
658       this.materialToolbar_.pageIndex = visiblePage;
659     } else {
660       this.pageIndicator_.index = visiblePage;
661       if (this.documentDimensions_.pageDimensions.length > 1 &&
662           hasScrollbars.vertical) {
663         this.pageIndicator_.style.visibility = 'visible';
664       } else {
665         this.pageIndicator_.style.visibility = 'hidden';
666       }
667     }
669     var visiblePageDimensions = this.viewport_.getPageScreenRect(visiblePage);
670     var size = this.viewport_.size;
671     this.sendScriptingMessage_({
672       type: 'viewport',
673       pageX: visiblePageDimensions.x,
674       pageY: visiblePageDimensions.y,
675       pageWidth: visiblePageDimensions.width,
676       viewportWidth: size.width,
677       viewportHeight: size.height
678     });
679   },
681   /**
682    * Handle a scripting message from outside the extension (typically sent by
683    * PDFScriptingAPI in a page containing the extension) to interact with the
684    * plugin.
685    * @param {MessageObject} message the message to handle.
686    */
687   handleScriptingMessage: function(message) {
688     if (this.parentWindow_ != message.source) {
689       this.parentWindow_ = message.source;
690       // Ensure that we notify the embedder if the document is loaded.
691       if (this.loaded_) {
692         this.sendScriptingMessage_({
693           type: 'documentLoaded'
694         });
695       }
696     }
698     if (this.handlePrintPreviewScriptingMessage_(message))
699       return;
701     // Delay scripting messages from users of the scripting API until the
702     // document is loaded. This simplifies use of the APIs.
703     if (!this.loaded_) {
704       this.delayedScriptingMessages_.push(message);
705       return;
706     }
708     switch (message.data.type.toString()) {
709       case 'getAccessibilityJSON':
710       case 'getSelectedText':
711       case 'print':
712       case 'selectAll':
713         this.plugin_.postMessage(message.data);
714         break;
715     }
716   },
718   /**
719    * @private
720    * Handle scripting messages specific to print preview.
721    * @param {MessageObject} message the message to handle.
722    * @return {boolean} true if the message was handled, false otherwise.
723    */
724   handlePrintPreviewScriptingMessage_: function(message) {
725     if (!this.isPrintPreview_)
726       return false;
728     switch (message.data.type.toString()) {
729       case 'loadPreviewPage':
730         this.plugin_.postMessage(message.data);
731         return true;
732       case 'resetPrintPreviewMode':
733         this.loaded_ = false;
734         if (!this.inPrintPreviewMode_) {
735           this.inPrintPreviewMode_ = true;
736           this.viewport_.fitToPage();
737         }
739         // Stash the scroll location so that it can be restored when the new
740         // document is loaded.
741         this.lastViewportPosition_ = this.viewport_.position;
743         // TODO(raymes): Disable these properly in the plugin.
744         var printButton = $('print-button');
745         if (printButton)
746           printButton.parentNode.removeChild(printButton);
747         var saveButton = $('save-button');
748         if (saveButton)
749           saveButton.parentNode.removeChild(saveButton);
751         if (!this.isMaterial_)
752           this.pageIndicator_.pageLabels = message.data.pageNumbers;
754         this.plugin_.postMessage({
755           type: 'resetPrintPreviewMode',
756           url: message.data.url,
757           grayscale: message.data.grayscale,
758           // If the PDF isn't modifiable we send 0 as the page count so that no
759           // blank placeholder pages get appended to the PDF.
760           pageCount: (message.data.modifiable ?
761                       message.data.pageNumbers.length : 0)
762         });
763         return true;
764       case 'sendKeyEvent':
765         this.handleKeyEvent_(DeserializeKeyEvent(message.data.keyEvent));
766         return true;
767     }
769     return false;
770   },
772   /**
773    * @private
774    * Send a scripting message outside the extension (typically to
775    * PDFScriptingAPI in a page containing the extension).
776    * @param {Object} message the message to send.
777    */
778   sendScriptingMessage_: function(message) {
779     if (this.parentWindow_)
780       this.parentWindow_.postMessage(message, '*');
781   },
783   /**
784    * @private
785    * Return whether this PDFViewer should manage zoom for its containing page.
786    * @return {boolean} Whether this PDFViewer should manage zoom for its
787    *     containing page.
788    */
789   shouldManageZoom_: function() {
790     return !!(chrome.tabs && !this.streamDetails_.embedded &&
791               this.streamDetails_.tabId != -1);
792   },
794   /**
795    * @type {Viewport} the viewport of the PDF viewer.
796    */
797   get viewport() {
798     return this.viewport_;
799   },
801   /**
802    * Each bookmark is an Object containing a:
803    * - title
804    * - page (optional)
805    * - array of children (themselves bookmarks)
806    * @type {Array} the top-level bookmarks of the PDF.
807    */
808   get bookmarks() {
809     return this.bookmarks_;
810   }