BookmarkManager: Fix 'new folder text field size changes on clicking it' issue.
[chromium-blink-merge.git] / chrome / browser / resources / pdf / pdf.js
blob536e7eab42f64138a56d2cf24648854b92f7ad6e
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, percent decoded if possible.
25  * @param {string} url The URL to get the filename from.
26  * @return {string} The filename component.
27  */
28 function getFilenameFromURL(url) {
29   // Ignore the query and fragment.
30   var mainUrl = url.split(/#|\?/)[0];
31   var components = mainUrl.split(/\/|\\/);
32   var filename = components[components.length - 1];
33   try {
34     return decodeURIComponent(filename);
35   } catch (e) {
36     if (e instanceof URIError)
37       return filename;
38     throw e;
39   }
42 /**
43  * Called when navigation happens in the current tab.
44  * @param {string} url The url to be opened in the current tab.
45  */
46 function onNavigateInCurrentTab(url) {
47   // Prefer the tabs API because it can navigate from one file:// URL to
48   // another.
49   if (chrome.tabs)
50     chrome.tabs.update({url: url});
51   else
52     window.location.href = url;
55 /**
56  * Called when navigation happens in the new tab.
57  * @param {string} url The url to be opened in the new tab.
58  */
59 function onNavigateInNewTab(url) {
60   // Prefer the tabs API because it guarantees we can just open a new tab.
61   // window.open doesn't have this guarantee.
62   if (chrome.tabs)
63     chrome.tabs.create({url: url});
64   else
65     window.open(url);
68 /**
69  * Whether keydown events should currently be ignored. Events are ignored when
70  * an editable element has focus, to allow for proper editing controls.
71  * @param {HTMLElement} activeElement The currently selected DOM node.
72  * @return {boolean} True if keydown events should be ignored.
73  */
74 function shouldIgnoreKeyEvents(activeElement) {
75   while (activeElement.shadowRoot != null &&
76          activeElement.shadowRoot.activeElement != null) {
77     activeElement = activeElement.shadowRoot.activeElement;
78   }
80   return (activeElement.isContentEditable ||
81           activeElement.tagName == 'INPUT' ||
82           activeElement.tagName == 'TEXTAREA');
85 /**
86  * The minimum number of pixels to offset the toolbar by from the bottom and
87  * right side of the screen.
88  */
89 PDFViewer.MIN_TOOLBAR_OFFSET = 15;
91 /**
92  * The height of the toolbar along the top of the page. The document will be
93  * shifted down by this much in the viewport.
94  */
95 PDFViewer.MATERIAL_TOOLBAR_HEIGHT = 56;
97 /**
98  * The light-gray background color used for print preview.
99  */
100 PDFViewer.LIGHT_BACKGROUND_COLOR = '0xFFCCCCCC';
103  * The dark-gray background color used for the regular viewer.
104  */
105 PDFViewer.DARK_BACKGROUND_COLOR = '0xFF525659';
108  * Creates a new PDFViewer. There should only be one of these objects per
109  * document.
110  * @constructor
111  * @param {!BrowserApi} browserApi An object providing an API to the browser.
112  */
113 function PDFViewer(browserApi) {
114   this.browserApi_ = browserApi;
115   this.loadState_ = LoadState.LOADING;
116   this.parentWindow_ = null;
117   this.parentOrigin_ = null;
119   this.delayedScriptingMessages_ = [];
121   this.isPrintPreview_ = this.browserApi_.getStreamInfo().originalUrl.indexOf(
122                              'chrome://print') == 0;
123   this.isMaterial_ = location.pathname.substring(1) === 'index-material.html';
125   // The sizer element is placed behind the plugin element to cause scrollbars
126   // to be displayed in the window. It is sized according to the document size
127   // of the pdf and zoom level.
128   this.sizer_ = $('sizer');
129   this.toolbar_ = $('toolbar');
130   if (!this.isMaterial_ || this.isPrintPreview_)
131     this.pageIndicator_ = $('page-indicator');
132   this.progressBar_ = $('progress-bar');
133   this.passwordScreen_ = $('password-screen');
134   this.passwordScreen_.addEventListener('password-submitted',
135                                         this.onPasswordSubmitted_.bind(this));
136   this.errorScreen_ = $('error-screen');
137   if (chrome.tabs)
138     this.errorScreen_.reloadFn = chrome.tabs.reload;
140   // Create the viewport.
141   var topToolbarHeight =
142       (this.isMaterial_ && !this.isPrintPreview_) ?
143       PDFViewer.MATERIAL_TOOLBAR_HEIGHT : 0;
144   this.viewport_ = new Viewport(window,
145                                 this.sizer_,
146                                 this.viewportChanged_.bind(this),
147                                 this.beforeZoom_.bind(this),
148                                 this.afterZoom_.bind(this),
149                                 getScrollbarWidth(),
150                                 this.browserApi_.getDefaultZoom(),
151                                 topToolbarHeight);
153   // Create the plugin object dynamically so we can set its src. The plugin
154   // element is sized to fill the entire window and is set to be fixed
155   // positioning, acting as a viewport. The plugin renders into this viewport
156   // according to the scroll position of the window.
157   this.plugin_ = document.createElement('embed');
158   // NOTE: The plugin's 'id' field must be set to 'plugin' since
159   // chrome/renderer/printing/print_web_view_helper.cc actually references it.
160   this.plugin_.id = 'plugin';
161   this.plugin_.type = 'application/x-google-chrome-pdf';
162   this.plugin_.addEventListener('message', this.handlePluginMessage_.bind(this),
163                                 false);
165   // Handle scripting messages from outside the extension that wish to interact
166   // with it. We also send a message indicating that extension has loaded and
167   // is ready to receive messages.
168   window.addEventListener('message', this.handleScriptingMessage.bind(this),
169                           false);
171   this.plugin_.setAttribute('src',
172                             this.browserApi_.getStreamInfo().originalUrl);
173   this.plugin_.setAttribute('stream-url',
174                             this.browserApi_.getStreamInfo().streamUrl);
175   var headers = '';
176   for (var header in this.browserApi_.getStreamInfo().responseHeaders) {
177     headers += header + ': ' +
178         this.browserApi_.getStreamInfo().responseHeaders[header] + '\n';
179   }
180   this.plugin_.setAttribute('headers', headers);
182   var backgroundColor = PDFViewer.DARK_BACKGROUND_COLOR;
183   if (!this.isMaterial_)
184     backgroundColor = PDFViewer.LIGHT_BACKGROUND_COLOR;
185   this.plugin_.setAttribute('background-color', backgroundColor);
186   this.plugin_.setAttribute('top-toolbar-height', topToolbarHeight);
188   if (!this.browserApi_.getStreamInfo().embedded)
189     this.plugin_.setAttribute('full-frame', '');
190   document.body.appendChild(this.plugin_);
192   // Setup the button event listeners.
193   if (!this.isMaterial_) {
194     $('fit-to-width-button').addEventListener('click',
195         this.viewport_.fitToWidth.bind(this.viewport_));
196     $('fit-to-page-button').addEventListener('click',
197         this.viewport_.fitToPage.bind(this.viewport_));
198     $('zoom-in-button').addEventListener('click',
199         this.viewport_.zoomIn.bind(this.viewport_));
200     $('zoom-out-button').addEventListener('click',
201         this.viewport_.zoomOut.bind(this.viewport_));
202     $('save-button').addEventListener('click', this.save_.bind(this));
203     $('print-button').addEventListener('click', this.print_.bind(this));
204   }
206   if (this.isMaterial_) {
207     this.zoomToolbar_ = $('zoom-toolbar');
208     this.zoomToolbar_.addEventListener('fit-to-width',
209         this.viewport_.fitToWidth.bind(this.viewport_));
210     this.zoomToolbar_.addEventListener('fit-to-page',
211         this.fitToPage_.bind(this));
212     this.zoomToolbar_.addEventListener('zoom-in',
213         this.viewport_.zoomIn.bind(this.viewport_));
214     this.zoomToolbar_.addEventListener('zoom-out',
215         this.viewport_.zoomOut.bind(this.viewport_));
217     if (!this.isPrintPreview_) {
218       this.materialToolbar_ = $('material-toolbar');
219       this.materialToolbar_.hidden = false;
220       this.materialToolbar_.addEventListener('save', this.save_.bind(this));
221       this.materialToolbar_.addEventListener('print', this.print_.bind(this));
222       this.materialToolbar_.addEventListener('rotate-right',
223           this.rotateClockwise_.bind(this));
224       this.materialToolbar_.addEventListener('rotate-left',
225           this.rotateCounterClockwise_.bind(this));
226       // Must attach to mouseup on the plugin element, since it eats mousedown
227       // and click events.
228       this.plugin_.addEventListener('mouseup',
229           this.materialToolbar_.hideDropdowns.bind(this.materialToolbar_));
230     }
232     document.body.addEventListener('change-page', function(e) {
233       this.viewport_.goToPage(e.detail.page);
234     }.bind(this));
236     this.toolbarManager_ =
237         new ToolbarManager(window, this.materialToolbar_, this.zoomToolbar_);
238   }
240   // Set up the ZoomManager.
241   this.zoomManager_ = new ZoomManager(
242       this.viewport_, this.browserApi_.setZoom.bind(this.browserApi_),
243       this.browserApi_.getInitialZoom());
244   this.browserApi_.addZoomEventListener(
245       this.zoomManager_.onBrowserZoomChange.bind(this.zoomManager_));
247   // Setup the keyboard event listener.
248   document.addEventListener('keydown', this.handleKeyEvent_.bind(this));
249   document.addEventListener('mousemove', this.handleMouseEvent_.bind(this));
250   document.addEventListener('mouseout', this.handleMouseEvent_.bind(this));
252   // Parse open pdf parameters.
253   this.paramsParser_ =
254       new OpenPDFParamsParser(this.getNamedDestination_.bind(this));
255   this.navigator_ = new Navigator(this.browserApi_.getStreamInfo().originalUrl,
256                                   this.viewport_, this.paramsParser_,
257                                   onNavigateInCurrentTab, onNavigateInNewTab);
258   this.viewportScroller_ =
259       new ViewportScroller(this.viewport_, this.plugin_, window);
261   // Request translated strings.
262   if (!this.isPrintPreview_)
263     chrome.resourcesPrivate.getStrings('pdf', this.handleStrings_.bind(this));
266 PDFViewer.prototype = {
267   /**
268    * @private
269    * Handle key events. These may come from the user directly or via the
270    * scripting API.
271    * @param {KeyboardEvent} e the event to handle.
272    */
273   handleKeyEvent_: function(e) {
274     var position = this.viewport_.position;
275     // Certain scroll events may be sent from outside of the extension.
276     var fromScriptingAPI = e.fromScriptingAPI;
278     if (shouldIgnoreKeyEvents(document.activeElement) || e.defaultPrevented)
279       return;
281     if (this.isMaterial_)
282       this.toolbarManager_.hideToolbarsAfterTimeout(e);
284     var pageUpHandler = function() {
285       // Go to the previous page if we are fit-to-page.
286       if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
287         this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
288         // Since we do the movement of the page.
289         e.preventDefault();
290       } else if (fromScriptingAPI) {
291         position.y -= this.viewport.size.height;
292         this.viewport.position = position;
293       }
294     }.bind(this);
295     var pageDownHandler = function() {
296       // Go to the next page if we are fit-to-page.
297       if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
298         this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
299         // Since we do the movement of the page.
300         e.preventDefault();
301       } else if (fromScriptingAPI) {
302         position.y += this.viewport.size.height;
303         this.viewport.position = position;
304       }
305     }.bind(this);
307     switch (e.keyCode) {
308       case 27:  // Escape key.
309         if (this.isMaterial_ && !this.isPrintPreview) {
310           this.toolbarManager_.hideSingleToolbarLayer();
311           return;
312         }
313         break;  // Ensure escape falls through to the print-preview handler.
314       case 32:  // Space key.
315         if (e.shiftKey)
316           pageUpHandler();
317         else
318           pageDownHandler();
319         return;
320       case 33:  // Page up key.
321         pageUpHandler();
322         return;
323       case 34:  // Page down key.
324         pageDownHandler();
325         return;
326       case 37:  // Left arrow key.
327         if (!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
328           // Go to the previous page if there are no horizontal scrollbars.
329           if (!this.viewport_.documentHasScrollbars().horizontal) {
330             this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
331             // Since we do the movement of the page.
332             e.preventDefault();
333           } else if (fromScriptingAPI) {
334             position.x -= Viewport.SCROLL_INCREMENT;
335             this.viewport.position = position;
336           }
337         }
338         return;
339       case 38:  // Up arrow key.
340         if (fromScriptingAPI) {
341           position.y -= Viewport.SCROLL_INCREMENT;
342           this.viewport.position = position;
343         }
344         return;
345       case 39:  // Right arrow key.
346         if (!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
347           // Go to the next page if there are no horizontal scrollbars.
348           if (!this.viewport_.documentHasScrollbars().horizontal) {
349             this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
350             // Since we do the movement of the page.
351             e.preventDefault();
352           } else if (fromScriptingAPI) {
353             position.x += Viewport.SCROLL_INCREMENT;
354             this.viewport.position = position;
355           }
356         }
357         return;
358       case 40:  // Down arrow key.
359         if (fromScriptingAPI) {
360           position.y += Viewport.SCROLL_INCREMENT;
361           this.viewport.position = position;
362         }
363         return;
364       case 65:  // a key.
365         if (e.ctrlKey || e.metaKey) {
366           this.plugin_.postMessage({
367             type: 'selectAll'
368           });
369           // Since we do selection ourselves.
370           e.preventDefault();
371         }
372         return;
373       case 71: // g key.
374         if (this.isMaterial_ && this.materialToolbar_ &&
375             (e.ctrlKey || e.metaKey)) {
376           this.toolbarManager_.showToolbars();
377           this.materialToolbar_.selectPageNumber();
378           // To prevent the default "find text" behaviour in Chrome.
379           e.preventDefault();
380         }
381         return;
382       case 219:  // left bracket.
383         if (e.ctrlKey)
384           this.rotateCounterClockwise_();
385         return;
386       case 221:  // right bracket.
387         if (e.ctrlKey)
388           this.rotateClockwise_();
389         return;
390     }
392     // Give print preview a chance to handle the key event.
393     if (!fromScriptingAPI && this.isPrintPreview_) {
394       this.sendScriptingMessage_({
395         type: 'sendKeyEvent',
396         keyEvent: SerializeKeyEvent(e)
397       });
398     } else if (this.isMaterial_) {
399       // Show toolbars as a fallback.
400       if (!(e.shiftKey || e.ctrlKey || e.altKey))
401         this.toolbarManager_.showToolbars();
402     }
403   },
405   handleMouseEvent_: function(e) {
406     if (this.isMaterial_) {
407       if (e.type == 'mousemove')
408         this.toolbarManager_.showToolbarsForMouseMove(e);
409       else if (e.type == 'mouseout')
410         this.toolbarManager_.hideToolbarsForMouseOut();
411     }
412   },
414   /**
415    * @private
416    * Rotate the plugin clockwise.
417    */
418   rotateClockwise_: function() {
419     this.plugin_.postMessage({
420       type: 'rotateClockwise'
421     });
422   },
424   /**
425    * @private
426    * Rotate the plugin counter-clockwise.
427    */
428   rotateCounterClockwise_: function() {
429     this.plugin_.postMessage({
430       type: 'rotateCounterclockwise'
431     });
432   },
434   fitToPage_: function() {
435     this.viewport_.fitToPage();
436     this.toolbarManager_.forceHideTopToolbar();
437   },
439   /**
440    * @private
441    * Notify the plugin to print.
442    */
443   print_: function() {
444     this.plugin_.postMessage({
445       type: 'print'
446     });
447   },
449   /**
450    * @private
451    * Notify the plugin to save.
452    */
453   save_: function() {
454     this.plugin_.postMessage({
455       type: 'save'
456     });
457   },
459   /**
460    * Fetches the page number corresponding to the given named destination from
461    * the plugin.
462    * @param {string} name The namedDestination to fetch page number from plugin.
463    */
464   getNamedDestination_: function(name) {
465     this.plugin_.postMessage({
466       type: 'getNamedDestination',
467       namedDestination: name
468     });
469   },
471   /**
472    * @private
473    * Sends a 'documentLoaded' message to the PDFScriptingAPI if the document has
474    * finished loading.
475    */
476   sendDocumentLoadedMessage_: function() {
477     if (this.loadState_ == LoadState.LOADING)
478       return;
479     this.sendScriptingMessage_({
480       type: 'documentLoaded',
481       load_state: this.loadState_
482     });
483   },
485   /**
486    * @private
487    * Handle open pdf parameters. This function updates the viewport as per
488    * the parameters mentioned in the url while opening pdf. The order is
489    * important as later actions can override the effects of previous actions.
490    * @param {Object} viewportPosition The initial position of the viewport to be
491    *     displayed.
492    */
493   handleURLParams_: function(viewportPosition) {
494     if (viewportPosition.page != undefined)
495       this.viewport_.goToPage(viewportPosition.page);
496     if (viewportPosition.position) {
497       // Make sure we don't cancel effect of page parameter.
498       this.viewport_.position = {
499         x: this.viewport_.position.x + viewportPosition.position.x,
500         y: this.viewport_.position.y + viewportPosition.position.y
501       };
502     }
503     if (viewportPosition.zoom)
504       this.viewport_.setZoom(viewportPosition.zoom);
505   },
507   /**
508    * @private
509    * Update the loading progress of the document in response to a progress
510    * message being received from the plugin.
511    * @param {number} progress the progress as a percentage.
512    */
513   updateProgress_: function(progress) {
514     if (this.isMaterial_) {
515       if (this.materialToolbar_)
516         this.materialToolbar_.loadProgress = progress;
517     } else {
518       this.progressBar_.progress = progress;
519     }
521     if (progress == -1) {
522       // Document load failed.
523       this.errorScreen_.show();
524       this.sizer_.style.display = 'none';
525       if (!this.isMaterial_)
526         this.toolbar_.style.visibility = 'hidden';
527       if (this.passwordScreen_.active) {
528         this.passwordScreen_.deny();
529         this.passwordScreen_.active = false;
530       }
531       this.loadState_ = LoadState.FAILED;
532       this.sendDocumentLoadedMessage_();
533     } else if (progress == 100) {
534       // Document load complete.
535       if (this.lastViewportPosition_)
536         this.viewport_.position = this.lastViewportPosition_;
537       this.paramsParser_.getViewportFromUrlParams(
538           this.browserApi_.getStreamInfo().originalUrl,
539           this.handleURLParams_.bind(this));
540       this.loadState_ = LoadState.SUCCESS;
541       this.sendDocumentLoadedMessage_();
542       while (this.delayedScriptingMessages_.length > 0)
543         this.handleScriptingMessage(this.delayedScriptingMessages_.shift());
545       if (this.isMaterial_)
546         this.toolbarManager_.hideToolbarsAfterTimeout();
547     }
548   },
550   /**
551    * @private
552    * Load a dictionary of translated strings into the UI. Used as a callback for
553    * chrome.resourcesPrivate.
554    * @param {Object} strings Dictionary of translated strings
555    */
556   handleStrings_: function(strings) {
557     this.passwordScreen_.text = strings.passwordPrompt;
558     if (!this.isMaterial_) {
559       this.progressBar_.text = strings.pageLoading;
560       if (!this.isPrintPreview_)
561         this.progressBar_.style.visibility = 'visible';
562     }
563     this.errorScreen_.text = strings.pageLoadFailed;
564   },
566   /**
567    * @private
568    * An event handler for handling password-submitted events. These are fired
569    * when an event is entered into the password screen.
570    * @param {Object} event a password-submitted event.
571    */
572   onPasswordSubmitted_: function(event) {
573     this.plugin_.postMessage({
574       type: 'getPasswordComplete',
575       password: event.detail.password
576     });
577   },
579   /**
580    * @private
581    * An event handler for handling message events received from the plugin.
582    * @param {MessageObject} message a message event.
583    */
584   handlePluginMessage_: function(message) {
585     switch (message.data.type.toString()) {
586       case 'documentDimensions':
587         this.documentDimensions_ = message.data;
588         this.viewport_.setDocumentDimensions(this.documentDimensions_);
589         // If we received the document dimensions, the password was good so we
590         // can dismiss the password screen.
591         if (this.passwordScreen_.active)
592           this.passwordScreen_.accept();
594         if (this.pageIndicator_)
595           this.pageIndicator_.initialFadeIn();
597         if (this.isMaterial_) {
598           if (this.materialToolbar_) {
599             this.materialToolbar_.docLength =
600                 this.documentDimensions_.pageDimensions.length;
601           }
602         } else {
603           this.toolbar_.initialFadeIn();
604         }
605         break;
606       case 'email':
607         var href = 'mailto:' + message.data.to + '?cc=' + message.data.cc +
608             '&bcc=' + message.data.bcc + '&subject=' + message.data.subject +
609             '&body=' + message.data.body;
610         window.location.href = href;
611         break;
612       case 'getAccessibilityJSONReply':
613         this.sendScriptingMessage_(message.data);
614         break;
615       case 'getPassword':
616         // If the password screen isn't up, put it up. Otherwise we're
617         // responding to an incorrect password so deny it.
618         if (!this.passwordScreen_.active)
619           this.passwordScreen_.active = true;
620         else
621           this.passwordScreen_.deny();
622         break;
623       case 'getSelectedTextReply':
624         this.sendScriptingMessage_(message.data);
625         break;
626       case 'goToPage':
627         this.viewport_.goToPage(message.data.page);
628         break;
629       case 'loadProgress':
630         this.updateProgress_(message.data.progress);
631         break;
632       case 'navigate':
633         // If in print preview, always open a new tab.
634         if (this.isPrintPreview_)
635           this.navigator_.navigate(message.data.url, true);
636         else
637           this.navigator_.navigate(message.data.url, message.data.newTab);
638         break;
639       case 'setScrollPosition':
640         var position = this.viewport_.position;
641         if (message.data.x !== undefined)
642           position.x = message.data.x;
643         if (message.data.y !== undefined)
644           position.y = message.data.y;
645         this.viewport_.position = position;
646         break;
647       case 'cancelStreamUrl':
648         chrome.mimeHandlerPrivate.abortStream();
649         break;
650       case 'metadata':
651         if (message.data.title) {
652           document.title = message.data.title;
653         } else {
654           document.title =
655               getFilenameFromURL(this.browserApi_.getStreamInfo().originalUrl);
656         }
657         this.bookmarks_ = message.data.bookmarks;
658         if (this.isMaterial_ && this.materialToolbar_) {
659           this.materialToolbar_.docTitle = document.title;
660           this.materialToolbar_.bookmarks = this.bookmarks;
661         }
662         break;
663       case 'setIsSelecting':
664         this.viewportScroller_.setEnableScrolling(message.data.isSelecting);
665         break;
666       case 'getNamedDestinationReply':
667         this.paramsParser_.onNamedDestinationReceived(
668             message.data.pageNumber);
669         break;
670     }
671   },
673   /**
674    * @private
675    * A callback that's called before the zoom changes. Notify the plugin to stop
676    * reacting to scroll events while zoom is taking place to avoid flickering.
677    */
678   beforeZoom_: function() {
679     this.plugin_.postMessage({
680       type: 'stopScrolling'
681     });
682   },
684   /**
685    * @private
686    * A callback that's called after the zoom changes. Notify the plugin of the
687    * zoom change and to continue reacting to scroll events.
688    */
689   afterZoom_: function() {
690     var position = this.viewport_.position;
691     var zoom = this.viewport_.zoom;
692     this.plugin_.postMessage({
693       type: 'viewport',
694       zoom: zoom,
695       xOffset: position.x,
696       yOffset: position.y
697     });
698     this.zoomManager_.onPdfZoomChange();
699   },
701   /**
702    * @private
703    * A callback that's called after the viewport changes.
704    */
705   viewportChanged_: function() {
706     if (!this.documentDimensions_)
707       return;
709     // Update the buttons selected.
710     if (!this.isMaterial_) {
711       $('fit-to-page-button').classList.remove('polymer-selected');
712       $('fit-to-width-button').classList.remove('polymer-selected');
713       if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
714         $('fit-to-page-button').classList.add('polymer-selected');
715       } else if (this.viewport_.fittingType ==
716                  Viewport.FittingType.FIT_TO_WIDTH) {
717         $('fit-to-width-button').classList.add('polymer-selected');
718       }
719     }
721     // Offset the toolbar position so that it doesn't move if scrollbars appear.
722     var hasScrollbars = this.viewport_.documentHasScrollbars();
723     var scrollbarWidth = this.viewport_.scrollbarWidth;
724     var verticalScrollbarWidth = hasScrollbars.vertical ? scrollbarWidth : 0;
725     var horizontalScrollbarWidth =
726         hasScrollbars.horizontal ? scrollbarWidth : 0;
727     if (this.isMaterial_) {
728       // Shift the zoom toolbar to the left by half a scrollbar width. This
729       // gives a compromise: if there is no scrollbar visible then the toolbar
730       // will be half a scrollbar width further left than the spec but if there
731       // is a scrollbar visible it will be half a scrollbar width further right
732       // than the spec.
733       this.zoomToolbar_.style.right = -verticalScrollbarWidth +
734           (scrollbarWidth / 2) + 'px';
735       // Having a horizontal scrollbar is much rarer so we don't offset the
736       // toolbar from the bottom any more than what the spec says. This means
737       // that when there is a scrollbar visible, it will be a full scrollbar
738       // width closer to the bottom of the screen than usual, but this is ok.
739       this.zoomToolbar_.style.bottom = -horizontalScrollbarWidth + 'px';
740     } else {
741       var toolbarRight = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
742       var toolbarBottom =
743           Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
744       toolbarRight -= verticalScrollbarWidth;
745       toolbarBottom -= horizontalScrollbarWidth;
746       this.toolbar_.style.right = toolbarRight + 'px';
747       this.toolbar_.style.bottom = toolbarBottom + 'px';
748       // Hide the toolbar if it doesn't fit in the viewport.
749       if (this.toolbar_.offsetLeft < 0 || this.toolbar_.offsetTop < 0)
750         this.toolbar_.style.visibility = 'hidden';
751       else
752         this.toolbar_.style.visibility = 'visible';
753     }
755     // Update the page indicator.
756     var visiblePage = this.viewport_.getMostVisiblePage();
758     if (this.materialToolbar_)
759       this.materialToolbar_.pageNo = visiblePage + 1;
761     // TODO(raymes): Give pageIndicator_ the same API as materialToolbar_.
762     if (this.pageIndicator_) {
763       this.pageIndicator_.index = visiblePage;
764       if (this.documentDimensions_.pageDimensions.length > 1 &&
765           hasScrollbars.vertical) {
766         this.pageIndicator_.style.visibility = 'visible';
767       } else {
768         this.pageIndicator_.style.visibility = 'hidden';
769       }
770     }
772     var visiblePageDimensions = this.viewport_.getPageScreenRect(visiblePage);
773     var size = this.viewport_.size;
774     this.sendScriptingMessage_({
775       type: 'viewport',
776       pageX: visiblePageDimensions.x,
777       pageY: visiblePageDimensions.y,
778       pageWidth: visiblePageDimensions.width,
779       viewportWidth: size.width,
780       viewportHeight: size.height
781     });
782   },
784   /**
785    * Handle a scripting message from outside the extension (typically sent by
786    * PDFScriptingAPI in a page containing the extension) to interact with the
787    * plugin.
788    * @param {MessageObject} message the message to handle.
789    */
790   handleScriptingMessage: function(message) {
791     if (this.parentWindow_ != message.source) {
792       this.parentWindow_ = message.source;
793       this.parentOrigin_ = message.origin;
794       // Ensure that we notify the embedder if the document is loaded.
795       if (this.loadState_ != LoadState.LOADING)
796         this.sendDocumentLoadedMessage_();
797     }
799     if (this.handlePrintPreviewScriptingMessage_(message))
800       return;
802     // Delay scripting messages from users of the scripting API until the
803     // document is loaded. This simplifies use of the APIs.
804     if (this.loadState_ != LoadState.SUCCESS) {
805       this.delayedScriptingMessages_.push(message);
806       return;
807     }
809     switch (message.data.type.toString()) {
810       case 'getAccessibilityJSON':
811       case 'getSelectedText':
812       case 'print':
813       case 'selectAll':
814         this.plugin_.postMessage(message.data);
815         break;
816     }
817   },
819   /**
820    * @private
821    * Handle scripting messages specific to print preview.
822    * @param {MessageObject} message the message to handle.
823    * @return {boolean} true if the message was handled, false otherwise.
824    */
825   handlePrintPreviewScriptingMessage_: function(message) {
826     if (!this.isPrintPreview_)
827       return false;
829     switch (message.data.type.toString()) {
830       case 'loadPreviewPage':
831         this.plugin_.postMessage(message.data);
832         return true;
833       case 'resetPrintPreviewMode':
834         this.loadState_ = LoadState.LOADING;
835         if (!this.inPrintPreviewMode_) {
836           this.inPrintPreviewMode_ = true;
837           this.viewport_.fitToPage();
838         }
840         // Stash the scroll location so that it can be restored when the new
841         // document is loaded.
842         this.lastViewportPosition_ = this.viewport_.position;
844         // TODO(raymes): Disable these properly in the plugin.
845         var printButton = $('print-button');
846         if (printButton)
847           printButton.parentNode.removeChild(printButton);
848         var saveButton = $('save-button');
849         if (saveButton)
850           saveButton.parentNode.removeChild(saveButton);
852         this.pageIndicator_.pageLabels = message.data.pageNumbers;
854         this.plugin_.postMessage({
855           type: 'resetPrintPreviewMode',
856           url: message.data.url,
857           grayscale: message.data.grayscale,
858           // If the PDF isn't modifiable we send 0 as the page count so that no
859           // blank placeholder pages get appended to the PDF.
860           pageCount: (message.data.modifiable ?
861                       message.data.pageNumbers.length : 0)
862         });
863         return true;
864       case 'sendKeyEvent':
865         this.handleKeyEvent_(DeserializeKeyEvent(message.data.keyEvent));
866         return true;
867     }
869     return false;
870   },
872   /**
873    * @private
874    * Send a scripting message outside the extension (typically to
875    * PDFScriptingAPI in a page containing the extension).
876    * @param {Object} message the message to send.
877    */
878   sendScriptingMessage_: function(message) {
879     if (this.parentWindow_ && this.parentOrigin_) {
880       var targetOrigin;
881       // Only send data back to the embedder if it is from the same origin,
882       // unless we're sending it to ourselves (which could happen in the case
883       // of tests). We also allow documentLoaded messages through as this won't
884       // leak important information.
885       if (this.parentOrigin_ == window.location.origin)
886         targetOrigin = this.parentOrigin_;
887       else if (message.type == 'documentLoaded')
888         targetOrigin = '*';
889       else
890         targetOrigin = this.browserApi_.getStreamInfo().originalUrl;
891       this.parentWindow_.postMessage(message, targetOrigin);
892     }
893   },
895   /**
896    * @type {Viewport} the viewport of the PDF viewer.
897    */
898   get viewport() {
899     return this.viewport_;
900   },
902   /**
903    * Each bookmark is an Object containing a:
904    * - title
905    * - page (optional)
906    * - array of children (themselves bookmarks)
907    * @type {Array} the top-level bookmarks of the PDF.
908    */
909   get bookmarks() {
910     return this.bookmarks_;
911   }