Add new certificateProvider extension API.
[chromium-blink-merge.git] / chrome / browser / resources / pdf / pdf.js
blob819693194e4cd56a28d4dda6842ea2a05989d4e2
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  * Creates a new PDFViewer. There should only be one of these objects per
99  * document.
100  * @constructor
101  * @param {!BrowserApi} browserApi An object providing an API to the browser.
102  */
103 function PDFViewer(browserApi) {
104   this.browserApi_ = browserApi;
105   this.loadState_ = LoadState.LOADING;
106   this.parentWindow_ = null;
107   this.parentOrigin_ = null;
109   this.delayedScriptingMessages_ = [];
111   this.isPrintPreview_ = this.browserApi_.getStreamInfo().originalUrl.indexOf(
112                              'chrome://print') == 0;
113   this.isMaterial_ = location.pathname.substring(1) === 'index-material.html';
115   // The sizer element is placed behind the plugin element to cause scrollbars
116   // to be displayed in the window. It is sized according to the document size
117   // of the pdf and zoom level.
118   this.sizer_ = $('sizer');
119   this.toolbar_ = $('toolbar');
120   this.pageIndicator_ = $('page-indicator');
121   this.progressBar_ = $('progress-bar');
122   this.passwordScreen_ = $('password-screen');
123   this.passwordScreen_.addEventListener('password-submitted',
124                                         this.onPasswordSubmitted_.bind(this));
125   this.errorScreen_ = $('error-screen');
126   if (chrome.tabs)
127     this.errorScreen_.reloadFn = chrome.tabs.reload;
129   // Create the viewport.
130   var topToolbarHeight =
131       this.isMaterial_ ? PDFViewer.MATERIAL_TOOLBAR_HEIGHT : 0;
132   this.viewport_ = new Viewport(window,
133                                 this.sizer_,
134                                 this.viewportChanged_.bind(this),
135                                 this.beforeZoom_.bind(this),
136                                 this.afterZoom_.bind(this),
137                                 getScrollbarWidth(),
138                                 this.browserApi_.getDefaultZoom(),
139                                 topToolbarHeight);
141   // Create the plugin object dynamically so we can set its src. The plugin
142   // element is sized to fill the entire window and is set to be fixed
143   // positioning, acting as a viewport. The plugin renders into this viewport
144   // according to the scroll position of the window.
145   this.plugin_ = document.createElement('embed');
146   // NOTE: The plugin's 'id' field must be set to 'plugin' since
147   // chrome/renderer/printing/print_web_view_helper.cc actually references it.
148   this.plugin_.id = 'plugin';
149   this.plugin_.type = 'application/x-google-chrome-pdf';
150   this.plugin_.addEventListener('message', this.handlePluginMessage_.bind(this),
151                                 false);
153   // Handle scripting messages from outside the extension that wish to interact
154   // with it. We also send a message indicating that extension has loaded and
155   // is ready to receive messages.
156   window.addEventListener('message', this.handleScriptingMessage.bind(this),
157                           false);
159   this.plugin_.setAttribute('src',
160                             this.browserApi_.getStreamInfo().originalUrl);
161   this.plugin_.setAttribute('stream-url',
162                             this.browserApi_.getStreamInfo().streamUrl);
163   var headers = '';
164   for (var header in this.browserApi_.getStreamInfo().responseHeaders) {
165     headers += header + ': ' +
166         this.browserApi_.getStreamInfo().responseHeaders[header] + '\n';
167   }
168   this.plugin_.setAttribute('headers', headers);
170   if (this.isMaterial_) {
171     this.plugin_.setAttribute('is-material', '');
172     this.plugin_.setAttribute('top-toolbar-height',
173                               PDFViewer.MATERIAL_TOOLBAR_HEIGHT);
174   }
176   if (!this.browserApi_.getStreamInfo().embedded)
177     this.plugin_.setAttribute('full-frame', '');
178   document.body.appendChild(this.plugin_);
180   // Setup the button event listeners.
181   if (!this.isMaterial_) {
182     $('fit-to-width-button').addEventListener('click',
183         this.viewport_.fitToWidth.bind(this.viewport_));
184     $('fit-to-page-button').addEventListener('click',
185         this.viewport_.fitToPage.bind(this.viewport_));
186     $('zoom-in-button').addEventListener('click',
187         this.viewport_.zoomIn.bind(this.viewport_));
188     $('zoom-out-button').addEventListener('click',
189         this.viewport_.zoomOut.bind(this.viewport_));
190     $('save-button').addEventListener('click', this.save_.bind(this));
191     $('print-button').addEventListener('click', this.print_.bind(this));
192   }
194   if (this.isMaterial_) {
195     this.zoomToolbar_ = $('zoom-toolbar');
196     this.zoomToolbar_.addEventListener('fit-to-width',
197         this.viewport_.fitToWidth.bind(this.viewport_));
198     this.zoomToolbar_.addEventListener('fit-to-page',
199         this.fitToPage_.bind(this));
200     this.zoomToolbar_.addEventListener('zoom-in',
201         this.viewport_.zoomIn.bind(this.viewport_));
202     this.zoomToolbar_.addEventListener('zoom-out',
203         this.viewport_.zoomOut.bind(this.viewport_));
205     this.materialToolbar_ = $('material-toolbar');
206     this.materialToolbar_.addEventListener('save', this.save_.bind(this));
207     this.materialToolbar_.addEventListener('print', this.print_.bind(this));
208     this.materialToolbar_.addEventListener('rotate-right',
209         this.rotateClockwise_.bind(this));
210     this.materialToolbar_.addEventListener('rotate-left',
211         this.rotateCounterClockwise_.bind(this));
213     document.body.addEventListener('change-page', function(e) {
214       this.viewport_.goToPage(e.detail.page);
215     }.bind(this));
217     this.toolbarManager_ =
218         new ToolbarManager(window, this.materialToolbar_, this.zoomToolbar_);
220     // Must attach to mouseup on the plugin element, since it eats mousedown and
221     // click events.
222     this.plugin_.addEventListener(
223         'mouseup',
224         this.materialToolbar_.hideDropdowns.bind(this.materialToolbar_));
225   }
227   // Set up the ZoomManager.
228   this.zoomManager_ = new ZoomManager(
229       this.viewport_, this.browserApi_.setZoom.bind(this.browserApi_),
230       this.browserApi_.getDefaultZoom());
231   this.browserApi_.addZoomEventListener(
232       this.zoomManager_.onBrowserZoomChange.bind(this.zoomManager_));
234   // Setup the keyboard event listener.
235   document.addEventListener('keydown', this.handleKeyEvent_.bind(this));
236   document.addEventListener('mousemove', this.handleMouseEvent_.bind(this));
238   // Parse open pdf parameters.
239   this.paramsParser_ =
240       new OpenPDFParamsParser(this.getNamedDestination_.bind(this));
241   this.navigator_ = new Navigator(this.browserApi_.getStreamInfo().originalUrl,
242                                   this.viewport_, this.paramsParser_,
243                                   onNavigateInCurrentTab, onNavigateInNewTab);
244   this.viewportScroller_ =
245       new ViewportScroller(this.viewport_, this.plugin_, window);
248 PDFViewer.prototype = {
249   /**
250    * @private
251    * Handle key events. These may come from the user directly or via the
252    * scripting API.
253    * @param {KeyboardEvent} e the event to handle.
254    */
255   handleKeyEvent_: function(e) {
256     var position = this.viewport_.position;
257     // Certain scroll events may be sent from outside of the extension.
258     var fromScriptingAPI = e.fromScriptingAPI;
260     if (shouldIgnoreKeyEvents(document.activeElement) || e.defaultPrevented)
261       return;
263     if (this.isMaterial_)
264       this.toolbarManager_.hideToolbarsAfterTimeout(e);
266     var pageUpHandler = function() {
267       // Go to the previous page if we are fit-to-page.
268       if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
269         this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
270         // Since we do the movement of the page.
271         e.preventDefault();
272       } else if (fromScriptingAPI) {
273         position.y -= this.viewport.size.height;
274         this.viewport.position = position;
275       }
276     }.bind(this);
277     var pageDownHandler = function() {
278       // Go to the next page if we are fit-to-page.
279       if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
280         this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
281         // Since we do the movement of the page.
282         e.preventDefault();
283       } else if (fromScriptingAPI) {
284         position.y += this.viewport.size.height;
285         this.viewport.position = position;
286       }
287     }.bind(this);
289     switch (e.keyCode) {
290       case 27:  // Escape key.
291         if (this.isMaterial_ && !this.isPrintPreview) {
292           this.toolbarManager_.hideSingleToolbarLayer();
293           return;
294         }
295         break;  // Ensure escape falls through to the print-preview handler.
296       case 32:  // Space key.
297         if (e.shiftKey)
298           pageUpHandler();
299         else
300           pageDownHandler();
301         return;
302       case 33:  // Page up key.
303         pageUpHandler();
304         return;
305       case 34:  // Page down key.
306         pageDownHandler();
307         return;
308       case 37:  // Left arrow key.
309         if (!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
310           // Go to the previous page if there are no horizontal scrollbars.
311           if (!this.viewport_.documentHasScrollbars().horizontal) {
312             this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
313             // Since we do the movement of the page.
314             e.preventDefault();
315           } else if (fromScriptingAPI) {
316             position.x -= Viewport.SCROLL_INCREMENT;
317             this.viewport.position = position;
318           }
319         }
320         return;
321       case 38:  // Up arrow key.
322         if (fromScriptingAPI) {
323           position.y -= Viewport.SCROLL_INCREMENT;
324           this.viewport.position = position;
325         }
326         return;
327       case 39:  // Right arrow key.
328         if (!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
329           // Go to the next page if there are no horizontal scrollbars.
330           if (!this.viewport_.documentHasScrollbars().horizontal) {
331             this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
332             // Since we do the movement of the page.
333             e.preventDefault();
334           } else if (fromScriptingAPI) {
335             position.x += Viewport.SCROLL_INCREMENT;
336             this.viewport.position = position;
337           }
338         }
339         return;
340       case 40:  // Down arrow key.
341         if (fromScriptingAPI) {
342           position.y += Viewport.SCROLL_INCREMENT;
343           this.viewport.position = position;
344         }
345         return;
346       case 65:  // a key.
347         if (e.ctrlKey || e.metaKey) {
348           this.plugin_.postMessage({
349             type: 'selectAll'
350           });
351           // Since we do selection ourselves.
352           e.preventDefault();
353         }
354         return;
355       case 71: // g key.
356         if (this.isMaterial_ && (e.ctrlKey || e.metaKey)) {
357           this.toolbarManager_.showToolbars();
358           this.materialToolbar_.selectPageNumber();
359           // To prevent the default "find text" behaviour in Chrome.
360           e.preventDefault();
361         }
362         return;
363       case 219:  // left bracket.
364         if (e.ctrlKey)
365           this.rotateCounterClockwise_();
366         return;
367       case 221:  // right bracket.
368         if (e.ctrlKey)
369           this.rotateClockwise_();
370         return;
371     }
373     // Give print preview a chance to handle the key event.
374     if (!fromScriptingAPI && this.isPrintPreview_) {
375       this.sendScriptingMessage_({
376         type: 'sendKeyEvent',
377         keyEvent: SerializeKeyEvent(e)
378       });
379     } else if (this.isMaterial_) {
380       // Show toolbars as a fallback.
381       if (!(e.shiftKey || e.ctrlKey || e.altKey))
382         this.toolbarManager_.showToolbars();
383     }
384   },
386   handleMouseEvent_: function(e) {
387     if (this.isMaterial_)
388       this.toolbarManager_.showToolbarsForMouseMove(e);
389   },
391   /**
392    * @private
393    * Rotate the plugin clockwise.
394    */
395   rotateClockwise_: function() {
396     this.plugin_.postMessage({
397       type: 'rotateClockwise'
398     });
399   },
401   /**
402    * @private
403    * Rotate the plugin counter-clockwise.
404    */
405   rotateCounterClockwise_: function() {
406     this.plugin_.postMessage({
407       type: 'rotateCounterclockwise'
408     });
409   },
411   fitToPage_: function() {
412     this.viewport_.fitToPage();
413     this.toolbarManager_.forceHideTopToolbar();
414   },
416   /**
417    * @private
418    * Notify the plugin to print.
419    */
420   print_: function() {
421     this.plugin_.postMessage({
422       type: 'print'
423     });
424   },
426   /**
427    * @private
428    * Notify the plugin to save.
429    */
430   save_: function() {
431     this.plugin_.postMessage({
432       type: 'save'
433     });
434   },
436   /**
437    * Fetches the page number corresponding to the given named destination from
438    * the plugin.
439    * @param {string} name The namedDestination to fetch page number from plugin.
440    */
441   getNamedDestination_: function(name) {
442     this.plugin_.postMessage({
443       type: 'getNamedDestination',
444       namedDestination: name
445     });
446   },
448   /**
449    * @private
450    * Sends a 'documentLoaded' message to the PDFScriptingAPI if the document has
451    * finished loading.
452    */
453   sendDocumentLoadedMessage_: function() {
454     if (this.loadState_ == LoadState.LOADING)
455       return;
456     this.sendScriptingMessage_({
457       type: 'documentLoaded',
458       load_state: this.loadState_
459     });
460   },
462   /**
463    * @private
464    * Handle open pdf parameters. This function updates the viewport as per
465    * the parameters mentioned in the url while opening pdf. The order is
466    * important as later actions can override the effects of previous actions.
467    * @param {Object} viewportPosition The initial position of the viewport to be
468    *     displayed.
469    */
470   handleURLParams_: function(viewportPosition) {
471     if (viewportPosition.page != undefined)
472       this.viewport_.goToPage(viewportPosition.page);
473     if (viewportPosition.position) {
474       // Make sure we don't cancel effect of page parameter.
475       this.viewport_.position = {
476         x: this.viewport_.position.x + viewportPosition.position.x,
477         y: this.viewport_.position.y + viewportPosition.position.y
478       };
479     }
480     if (viewportPosition.zoom)
481       this.viewport_.setZoom(viewportPosition.zoom);
482   },
484   /**
485    * @private
486    * Update the loading progress of the document in response to a progress
487    * message being received from the plugin.
488    * @param {number} progress the progress as a percentage.
489    */
490   updateProgress_: function(progress) {
491     if (this.isMaterial_)
492       this.materialToolbar_.loadProgress = progress;
493     else
494       this.progressBar_.progress = progress;
496     if (progress == -1) {
497       // Document load failed.
498       this.errorScreen_.show();
499       this.sizer_.style.display = 'none';
500       if (!this.isMaterial_)
501         this.toolbar_.style.visibility = 'hidden';
502       if (this.passwordScreen_.active) {
503         this.passwordScreen_.deny();
504         this.passwordScreen_.active = false;
505       }
506       this.loadState_ = LoadState.FAILED;
507       this.sendDocumentLoadedMessage_();
508     } else if (progress == 100) {
509       // Document load complete.
510       if (this.lastViewportPosition_)
511         this.viewport_.position = this.lastViewportPosition_;
512       this.paramsParser_.getViewportFromUrlParams(
513           this.browserApi_.getStreamInfo().originalUrl,
514           this.handleURLParams_.bind(this));
515       this.loadState_ = LoadState.SUCCESS;
516       this.sendDocumentLoadedMessage_();
517       while (this.delayedScriptingMessages_.length > 0)
518         this.handleScriptingMessage(this.delayedScriptingMessages_.shift());
520       if (this.isMaterial_)
521         this.toolbarManager_.hideToolbarsAfterTimeout();
522     }
523   },
525   /**
526    * @private
527    * An event handler for handling password-submitted events. These are fired
528    * when an event is entered into the password screen.
529    * @param {Object} event a password-submitted event.
530    */
531   onPasswordSubmitted_: function(event) {
532     this.plugin_.postMessage({
533       type: 'getPasswordComplete',
534       password: event.detail.password
535     });
536   },
538   /**
539    * @private
540    * An event handler for handling message events received from the plugin.
541    * @param {MessageObject} message a message event.
542    */
543   handlePluginMessage_: function(message) {
544     switch (message.data.type.toString()) {
545       case 'documentDimensions':
546         this.documentDimensions_ = message.data;
547         this.viewport_.setDocumentDimensions(this.documentDimensions_);
548         // If we received the document dimensions, the password was good so we
549         // can dismiss the password screen.
550         if (this.passwordScreen_.active)
551           this.passwordScreen_.accept();
553         if (this.isMaterial_) {
554           this.materialToolbar_.docLength =
555               this.documentDimensions_.pageDimensions.length;
556           this.toolbarManager_.enableToolbars();
557         } else {
558           this.pageIndicator_.initialFadeIn();
559           this.toolbar_.initialFadeIn();
560         }
561         break;
562       case 'email':
563         var href = 'mailto:' + message.data.to + '?cc=' + message.data.cc +
564             '&bcc=' + message.data.bcc + '&subject=' + message.data.subject +
565             '&body=' + message.data.body;
566         window.location.href = href;
567         break;
568       case 'getAccessibilityJSONReply':
569         this.sendScriptingMessage_(message.data);
570         break;
571       case 'getPassword':
572         // If the password screen isn't up, put it up. Otherwise we're
573         // responding to an incorrect password so deny it.
574         if (!this.passwordScreen_.active)
575           this.passwordScreen_.active = true;
576         else
577           this.passwordScreen_.deny();
578         break;
579       case 'getSelectedTextReply':
580         this.sendScriptingMessage_(message.data);
581         break;
582       case 'goToPage':
583         this.viewport_.goToPage(message.data.page);
584         break;
585       case 'loadProgress':
586         this.updateProgress_(message.data.progress);
587         break;
588       case 'navigate':
589         // If in print preview, always open a new tab.
590         if (this.isPrintPreview_)
591           this.navigator_.navigate(message.data.url, true);
592         else
593           this.navigator_.navigate(message.data.url, message.data.newTab);
594         break;
595       case 'setScrollPosition':
596         var position = this.viewport_.position;
597         if (message.data.x !== undefined)
598           position.x = message.data.x;
599         if (message.data.y !== undefined)
600           position.y = message.data.y;
601         this.viewport_.position = position;
602         break;
603       case 'setTranslatedStrings':
604         this.passwordScreen_.text = message.data.getPasswordString;
605         if (!this.isMaterial_) {
606           this.progressBar_.text = message.data.loadingString;
607           if (!this.isPrintPreview_)
608             this.progressBar_.style.visibility = 'visible';
609         }
610         this.errorScreen_.text = message.data.loadFailedString;
611         break;
612       case 'cancelStreamUrl':
613         chrome.mimeHandlerPrivate.abortStream();
614         break;
615       case 'metadata':
616         if (message.data.title) {
617           document.title = message.data.title;
618         } else {
619           document.title =
620               getFilenameFromURL(this.browserApi_.getStreamInfo().originalUrl);
621         }
622         this.bookmarks_ = message.data.bookmarks;
623         if (this.isMaterial_) {
624           this.materialToolbar_.docTitle = document.title;
625           this.materialToolbar_.bookmarks = this.bookmarks;
626         }
627         break;
628       case 'setIsSelecting':
629         this.viewportScroller_.setEnableScrolling(message.data.isSelecting);
630         break;
631       case 'getNamedDestinationReply':
632         this.paramsParser_.onNamedDestinationReceived(
633             message.data.pageNumber);
634         break;
635     }
636   },
638   /**
639    * @private
640    * A callback that's called before the zoom changes. Notify the plugin to stop
641    * reacting to scroll events while zoom is taking place to avoid flickering.
642    */
643   beforeZoom_: function() {
644     this.plugin_.postMessage({
645       type: 'stopScrolling'
646     });
647   },
649   /**
650    * @private
651    * A callback that's called after the zoom changes. Notify the plugin of the
652    * zoom change and to continue reacting to scroll events.
653    */
654   afterZoom_: function() {
655     var position = this.viewport_.position;
656     var zoom = this.viewport_.zoom;
657     this.plugin_.postMessage({
658       type: 'viewport',
659       zoom: zoom,
660       xOffset: position.x,
661       yOffset: position.y
662     });
663     this.zoomManager_.onPdfZoomChange();
664   },
666   /**
667    * @private
668    * A callback that's called after the viewport changes.
669    */
670   viewportChanged_: function() {
671     if (!this.documentDimensions_)
672       return;
674     // Update the buttons selected.
675     if (!this.isMaterial_) {
676       $('fit-to-page-button').classList.remove('polymer-selected');
677       $('fit-to-width-button').classList.remove('polymer-selected');
678       if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
679         $('fit-to-page-button').classList.add('polymer-selected');
680       } else if (this.viewport_.fittingType ==
681                  Viewport.FittingType.FIT_TO_WIDTH) {
682         $('fit-to-width-button').classList.add('polymer-selected');
683       }
684     }
686     // Offset the toolbar position so that it doesn't move if scrollbars appear.
687     var hasScrollbars = this.viewport_.documentHasScrollbars();
688     var scrollbarWidth = this.viewport_.scrollbarWidth;
689     var verticalScrollbarWidth = hasScrollbars.vertical ? scrollbarWidth : 0;
690     var horizontalScrollbarWidth =
691         hasScrollbars.horizontal ? scrollbarWidth : 0;
692     var toolbarRight = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
693     var toolbarBottom = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
694     toolbarRight -= verticalScrollbarWidth;
695     toolbarBottom -= horizontalScrollbarWidth;
696     if (!this.isMaterial_) {
697       this.toolbar_.style.right = toolbarRight + 'px';
698       this.toolbar_.style.bottom = toolbarBottom + 'px';
699       // Hide the toolbar if it doesn't fit in the viewport.
700       if (this.toolbar_.offsetLeft < 0 || this.toolbar_.offsetTop < 0)
701         this.toolbar_.style.visibility = 'hidden';
702       else
703         this.toolbar_.style.visibility = 'visible';
704     }
706     // Update the page indicator.
707     var visiblePage = this.viewport_.getMostVisiblePage();
708     if (this.isMaterial_) {
709       this.materialToolbar_.pageNo = visiblePage + 1;
710     } else {
711       this.pageIndicator_.index = visiblePage;
712       if (this.documentDimensions_.pageDimensions.length > 1 &&
713           hasScrollbars.vertical) {
714         this.pageIndicator_.style.visibility = 'visible';
715       } else {
716         this.pageIndicator_.style.visibility = 'hidden';
717       }
718     }
720     var visiblePageDimensions = this.viewport_.getPageScreenRect(visiblePage);
721     var size = this.viewport_.size;
722     this.sendScriptingMessage_({
723       type: 'viewport',
724       pageX: visiblePageDimensions.x,
725       pageY: visiblePageDimensions.y,
726       pageWidth: visiblePageDimensions.width,
727       viewportWidth: size.width,
728       viewportHeight: size.height
729     });
730   },
732   /**
733    * Handle a scripting message from outside the extension (typically sent by
734    * PDFScriptingAPI in a page containing the extension) to interact with the
735    * plugin.
736    * @param {MessageObject} message the message to handle.
737    */
738   handleScriptingMessage: function(message) {
739     if (this.parentWindow_ != message.source) {
740       this.parentWindow_ = message.source;
741       this.parentOrigin_ = message.origin;
742       // Ensure that we notify the embedder if the document is loaded.
743       if (this.loadState_ != LoadState.LOADING)
744         this.sendDocumentLoadedMessage_();
745     }
747     if (this.handlePrintPreviewScriptingMessage_(message))
748       return;
750     // Delay scripting messages from users of the scripting API until the
751     // document is loaded. This simplifies use of the APIs.
752     if (this.loadState_ != LoadState.SUCCESS) {
753       this.delayedScriptingMessages_.push(message);
754       return;
755     }
757     switch (message.data.type.toString()) {
758       case 'getAccessibilityJSON':
759       case 'getSelectedText':
760       case 'print':
761       case 'selectAll':
762         this.plugin_.postMessage(message.data);
763         break;
764     }
765   },
767   /**
768    * @private
769    * Handle scripting messages specific to print preview.
770    * @param {MessageObject} message the message to handle.
771    * @return {boolean} true if the message was handled, false otherwise.
772    */
773   handlePrintPreviewScriptingMessage_: function(message) {
774     if (!this.isPrintPreview_)
775       return false;
777     switch (message.data.type.toString()) {
778       case 'loadPreviewPage':
779         this.plugin_.postMessage(message.data);
780         return true;
781       case 'resetPrintPreviewMode':
782         this.loadState_ = LoadState.LOADING;
783         if (!this.inPrintPreviewMode_) {
784           this.inPrintPreviewMode_ = true;
785           this.viewport_.fitToPage();
786         }
788         // Stash the scroll location so that it can be restored when the new
789         // document is loaded.
790         this.lastViewportPosition_ = this.viewport_.position;
792         // TODO(raymes): Disable these properly in the plugin.
793         var printButton = $('print-button');
794         if (printButton)
795           printButton.parentNode.removeChild(printButton);
796         var saveButton = $('save-button');
797         if (saveButton)
798           saveButton.parentNode.removeChild(saveButton);
800         if (!this.isMaterial_)
801           this.pageIndicator_.pageLabels = message.data.pageNumbers;
803         this.plugin_.postMessage({
804           type: 'resetPrintPreviewMode',
805           url: message.data.url,
806           grayscale: message.data.grayscale,
807           // If the PDF isn't modifiable we send 0 as the page count so that no
808           // blank placeholder pages get appended to the PDF.
809           pageCount: (message.data.modifiable ?
810                       message.data.pageNumbers.length : 0)
811         });
812         return true;
813       case 'sendKeyEvent':
814         this.handleKeyEvent_(DeserializeKeyEvent(message.data.keyEvent));
815         return true;
816     }
818     return false;
819   },
821   /**
822    * @private
823    * Send a scripting message outside the extension (typically to
824    * PDFScriptingAPI in a page containing the extension).
825    * @param {Object} message the message to send.
826    */
827   sendScriptingMessage_: function(message) {
828     if (this.parentWindow_ && this.parentOrigin_) {
829       var targetOrigin;
830       // Only send data back to the embedder if it is from the same origin,
831       // unless we're sending it to ourselves (which could happen in the case
832       // of tests). We also allow documentLoaded messages through as this won't
833       // leak important information.
834       if (this.parentOrigin_ == window.location.origin)
835         targetOrigin = this.parentOrigin_;
836       else if (message.type == 'documentLoaded')
837         targetOrigin = '*';
838       else
839         targetOrigin = this.browserApi_.getStreamInfo().originalUrl;
840       this.parentWindow_.postMessage(message, targetOrigin);
841     }
842   },
844   /**
845    * @type {Viewport} the viewport of the PDF viewer.
846    */
847   get viewport() {
848     return this.viewport_;
849   },
851   /**
852    * Each bookmark is an Object containing a:
853    * - title
854    * - page (optional)
855    * - array of children (themselves bookmarks)
856    * @type {Array} the top-level bookmarks of the PDF.
857    */
858   get bookmarks() {
859     return this.bookmarks_;
860   }