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.
8 * @return {number} Width of a scrollbar in pixels
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);
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.
28 function getFilenameFromURL(url) {
29 var components = url.split(/\/|\\/);
30 return components[components.length - 1];
34 * Called when navigation happens in the current tab.
35 * @param {string} url The url to be opened in the current tab.
37 function onNavigateInCurrentTab(url) {
38 // Prefer the tabs API because it can navigate from one file:// URL to
41 chrome.tabs.update({url: url});
43 window.location.href = url;
47 * Called when navigation happens in the new tab.
48 * @param {string} url The url to be opened in the new tab.
50 function onNavigateInNewTab(url) {
51 // Prefer the tabs API because it guarantees we can just open a new tab.
52 // window.open doesn't have this guarantee.
54 chrome.tabs.create({url: url});
60 * Whether keydown events should currently be ignored. Events are ignored when
61 * an editable element has focus, to allow for proper editing controls.
62 * @param {HTMLElement} activeElement The currently selected DOM node.
63 * @return {boolean} True if keydown events should be ignored.
65 function shouldIgnoreKeyEvents(activeElement) {
66 while (activeElement.shadowRoot != null &&
67 activeElement.shadowRoot.activeElement != null) {
68 activeElement = activeElement.shadowRoot.activeElement;
71 return (activeElement.isContentEditable ||
72 activeElement.tagName == 'INPUT' ||
73 activeElement.tagName == 'TEXTAREA');
77 * The minimum number of pixels to offset the toolbar by from the bottom and
78 * right side of the screen.
80 PDFViewer.MIN_TOOLBAR_OFFSET = 15;
83 * The height of the toolbar along the top of the page. The document will be
84 * shifted down by this much in the viewport.
86 PDFViewer.MATERIAL_TOOLBAR_HEIGHT = 64;
89 * Creates a new PDFViewer. There should only be one of these objects per
92 * @param {!BrowserApi} browserApi An object providing an API to the browser.
94 function PDFViewer(browserApi) {
95 this.browserApi_ = browserApi;
96 this.loadState_ = LoadState.LOADING;
97 this.parentWindow_ = null;
99 this.delayedScriptingMessages_ = [];
101 this.isPrintPreview_ = this.browserApi_.getStreamInfo().originalUrl.indexOf(
102 'chrome://print') == 0;
103 this.isMaterial_ = location.pathname.substring(1) === 'index-material.html';
105 // The sizer element is placed behind the plugin element to cause scrollbars
106 // to be displayed in the window. It is sized according to the document size
107 // of the pdf and zoom level.
108 this.sizer_ = $('sizer');
109 this.toolbar_ = $('toolbar');
110 this.pageIndicator_ = $('page-indicator');
111 this.progressBar_ = $('progress-bar');
112 this.passwordScreen_ = $('password-screen');
113 this.passwordScreen_.addEventListener('password-submitted',
114 this.onPasswordSubmitted_.bind(this));
115 this.errorScreen_ = $('error-screen');
117 // Create the viewport.
118 var topToolbarHeight =
119 this.isMaterial_ ? PDFViewer.MATERIAL_TOOLBAR_HEIGHT : 0;
120 this.viewport_ = new Viewport(window,
122 this.viewportChanged_.bind(this),
123 this.beforeZoom_.bind(this),
124 this.afterZoom_.bind(this),
126 this.browserApi_.getDefaultZoom(),
129 // Create the plugin object dynamically so we can set its src. The plugin
130 // element is sized to fill the entire window and is set to be fixed
131 // positioning, acting as a viewport. The plugin renders into this viewport
132 // according to the scroll position of the window.
133 this.plugin_ = document.createElement('embed');
134 // NOTE: The plugin's 'id' field must be set to 'plugin' since
135 // chrome/renderer/printing/print_web_view_helper.cc actually references it.
136 this.plugin_.id = 'plugin';
137 this.plugin_.type = 'application/x-google-chrome-pdf';
138 this.plugin_.addEventListener('message', this.handlePluginMessage_.bind(this),
141 // Handle scripting messages from outside the extension that wish to interact
142 // with it. We also send a message indicating that extension has loaded and
143 // is ready to receive messages.
144 window.addEventListener('message', this.handleScriptingMessage.bind(this),
147 document.title = decodeURIComponent(
148 getFilenameFromURL(this.browserApi_.getStreamInfo().originalUrl));
149 this.plugin_.setAttribute('src',
150 this.browserApi_.getStreamInfo().originalUrl);
151 this.plugin_.setAttribute('stream-url',
152 this.browserApi_.getStreamInfo().streamUrl);
154 for (var header in this.browserApi_.getStreamInfo().responseHeaders) {
155 headers += header + ': ' +
156 this.browserApi_.getStreamInfo().responseHeaders[header] + '\n';
158 this.plugin_.setAttribute('headers', headers);
160 if (this.isMaterial_) {
161 this.plugin_.setAttribute('is-material', '');
162 this.plugin_.setAttribute('top-toolbar-height',
163 PDFViewer.MATERIAL_TOOLBAR_HEIGHT);
166 if (!this.browserApi_.getStreamInfo().embedded)
167 this.plugin_.setAttribute('full-frame', '');
168 document.body.appendChild(this.plugin_);
170 // Setup the button event listeners.
171 if (!this.isMaterial_) {
172 $('fit-to-width-button').addEventListener('click',
173 this.viewport_.fitToWidth.bind(this.viewport_));
174 $('fit-to-page-button').addEventListener('click',
175 this.viewport_.fitToPage.bind(this.viewport_));
176 $('zoom-in-button').addEventListener('click',
177 this.viewport_.zoomIn.bind(this.viewport_));
178 $('zoom-out-button').addEventListener('click',
179 this.viewport_.zoomOut.bind(this.viewport_));
180 $('save-button').addEventListener('click', this.save_.bind(this));
181 $('print-button').addEventListener('click', this.print_.bind(this));
184 if (this.isMaterial_) {
185 this.zoomToolbar_ = $('zoom-toolbar');
186 this.zoomToolbar_.addEventListener('fit-to-width',
187 this.viewport_.fitToWidth.bind(this.viewport_));
188 this.zoomToolbar_.addEventListener('fit-to-page',
189 this.fitToPage_.bind(this));
190 this.zoomToolbar_.addEventListener('zoom-in',
191 this.viewport_.zoomIn.bind(this.viewport_));
192 this.zoomToolbar_.addEventListener('zoom-out',
193 this.viewport_.zoomOut.bind(this.viewport_));
195 this.materialToolbar_ = $('material-toolbar');
196 this.materialToolbar_.docTitle = document.title;
197 this.materialToolbar_.addEventListener('save', this.save_.bind(this));
198 this.materialToolbar_.addEventListener('print', this.print_.bind(this));
199 this.materialToolbar_.addEventListener('rotate-right',
200 this.rotateClockwise_.bind(this));
201 this.materialToolbar_.addEventListener('rotate-left',
202 this.rotateCounterClockwise_.bind(this));
204 document.body.addEventListener('change-page', function(e) {
205 this.viewport_.goToPage(e.detail.page);
208 this.toolbarManager_ =
209 new ToolbarManager(window, this.materialToolbar_, this.zoomToolbar_);
211 // Must attach to mouseup on the plugin element, since it eats mousedown and
213 this.plugin_.addEventListener(
215 this.materialToolbar_.hideDropdowns.bind(this.materialToolbar_));
218 // Set up the ZoomManager.
219 this.zoomManager_ = new ZoomManager(
220 this.viewport_, this.browserApi_.setZoom.bind(this.browserApi_),
221 this.browserApi_.getDefaultZoom());
222 this.browserApi_.addZoomEventListener(
223 this.zoomManager_.onBrowserZoomChange.bind(this.zoomManager_));
225 // Setup the keyboard event listener.
226 document.addEventListener('keydown', this.handleKeyEvent_.bind(this));
227 document.addEventListener('mousemove', this.handleMouseEvent_.bind(this));
229 // Parse open pdf parameters.
231 new OpenPDFParamsParser(this.getNamedDestination_.bind(this));
232 this.navigator_ = new Navigator(this.browserApi_.getStreamInfo().originalUrl,
233 this.viewport_, this.paramsParser_,
234 onNavigateInCurrentTab, onNavigateInNewTab);
235 this.viewportScroller_ =
236 new ViewportScroller(this.viewport_, this.plugin_, window);
239 PDFViewer.prototype = {
242 * Handle key events. These may come from the user directly or via the
244 * @param {KeyboardEvent} e the event to handle.
246 handleKeyEvent_: function(e) {
247 var position = this.viewport_.position;
248 // Certain scroll events may be sent from outside of the extension.
249 var fromScriptingAPI = e.fromScriptingAPI;
251 if (shouldIgnoreKeyEvents(document.activeElement) || e.defaultPrevented)
254 if (this.isMaterial_)
255 this.toolbarManager_.hideToolbarsAfterTimeout(e);
257 var pageUpHandler = function() {
258 // Go to the previous page if we are fit-to-page.
259 if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
260 this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
261 // Since we do the movement of the page.
263 } else if (fromScriptingAPI) {
264 position.y -= this.viewport.size.height;
265 this.viewport.position = position;
268 var pageDownHandler = function() {
269 // Go to the next page if we are fit-to-page.
270 if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
271 this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
272 // Since we do the movement of the page.
274 } else if (fromScriptingAPI) {
275 position.y += this.viewport.size.height;
276 this.viewport.position = position;
281 case 27: // Escape key.
282 if (this.isMaterial_)
283 this.toolbarManager_.hideSingleToolbarLayer();
285 case 32: // Space key.
291 case 33: // Page up key.
294 case 34: // Page down key.
297 case 37: // Left arrow key.
298 if (!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
299 // Go to the previous page if there are no horizontal scrollbars.
300 if (!this.viewport_.documentHasScrollbars().horizontal) {
301 this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
302 // Since we do the movement of the page.
304 } else if (fromScriptingAPI) {
305 position.x -= Viewport.SCROLL_INCREMENT;
306 this.viewport.position = position;
310 case 38: // Up arrow key.
311 if (fromScriptingAPI) {
312 position.y -= Viewport.SCROLL_INCREMENT;
313 this.viewport.position = position;
316 case 39: // Right arrow key.
317 if (!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
318 // Go to the next page if there are no horizontal scrollbars.
319 if (!this.viewport_.documentHasScrollbars().horizontal) {
320 this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
321 // Since we do the movement of the page.
323 } else if (fromScriptingAPI) {
324 position.x += Viewport.SCROLL_INCREMENT;
325 this.viewport.position = position;
329 case 40: // Down arrow key.
330 if (fromScriptingAPI) {
331 position.y += Viewport.SCROLL_INCREMENT;
332 this.viewport.position = position;
336 if (e.ctrlKey || e.metaKey) {
337 this.plugin_.postMessage({
340 // Since we do selection ourselves.
345 if (this.isMaterial_ && (e.ctrlKey || e.metaKey)) {
346 this.toolbarManager_.showToolbars();
347 this.materialToolbar_.selectPageNumber();
348 // To prevent the default "find text" behaviour in Chrome.
352 case 219: // left bracket.
354 this.rotateCounterClockwise_();
356 case 221: // right bracket.
358 this.rotateClockwise_();
362 // Give print preview a chance to handle the key event.
363 if (!fromScriptingAPI && this.isPrintPreview_) {
364 this.sendScriptingMessage_({
365 type: 'sendKeyEvent',
366 keyEvent: SerializeKeyEvent(e)
368 } else if (this.isMaterial_) {
369 // Show toolbars as a fallback.
370 if (!(e.shiftKey || e.ctrlKey || e.altKey))
371 this.toolbarManager_.showToolbars();
375 handleMouseEvent_: function(e) {
376 if (this.isMaterial_)
377 this.toolbarManager_.showToolbarsForMouseMove(e);
382 * Rotate the plugin clockwise.
384 rotateClockwise_: function() {
385 this.plugin_.postMessage({
386 type: 'rotateClockwise'
392 * Rotate the plugin counter-clockwise.
394 rotateCounterClockwise_: function() {
395 this.plugin_.postMessage({
396 type: 'rotateCounterclockwise'
400 fitToPage_: function() {
401 this.viewport_.fitToPage();
402 this.toolbarManager_.forceHideTopToolbar();
407 * Notify the plugin to print.
410 this.plugin_.postMessage({
417 * Notify the plugin to save.
420 this.plugin_.postMessage({
426 * Fetches the page number corresponding to the given named destination from
428 * @param {string} name The namedDestination to fetch page number from plugin.
430 getNamedDestination_: function(name) {
431 this.plugin_.postMessage({
432 type: 'getNamedDestination',
433 namedDestination: name
439 * Sends a 'documentLoaded' message to the PDFScriptingAPI if the document has
442 sendDocumentLoadedMessage_: function() {
443 if (this.loadState_ == LoadState.LOADING)
445 this.sendScriptingMessage_({
446 type: 'documentLoaded',
447 load_state: this.loadState_
453 * Handle open pdf parameters. This function updates the viewport as per
454 * the parameters mentioned in the url while opening pdf. The order is
455 * important as later actions can override the effects of previous actions.
456 * @param {Object} viewportPosition The initial position of the viewport to be
459 handleURLParams_: function(viewportPosition) {
460 if (viewportPosition.page != undefined)
461 this.viewport_.goToPage(viewportPosition.page);
462 if (viewportPosition.position) {
463 // Make sure we don't cancel effect of page parameter.
464 this.viewport_.position = {
465 x: this.viewport_.position.x + viewportPosition.position.x,
466 y: this.viewport_.position.y + viewportPosition.position.y
469 if (viewportPosition.zoom)
470 this.viewport_.setZoom(viewportPosition.zoom);
475 * Update the loading progress of the document in response to a progress
476 * message being received from the plugin.
477 * @param {number} progress the progress as a percentage.
479 updateProgress_: function(progress) {
480 if (this.isMaterial_)
481 this.materialToolbar_.loadProgress = progress;
483 this.progressBar_.progress = progress;
485 if (progress == -1) {
486 // Document load failed.
487 this.errorScreen_.style.visibility = 'visible';
488 this.sizer_.style.display = 'none';
489 if (!this.isMaterial_)
490 this.toolbar_.style.visibility = 'hidden';
491 if (this.passwordScreen_.active) {
492 this.passwordScreen_.deny();
493 this.passwordScreen_.active = false;
495 this.loadState_ = LoadState.FAILED;
496 this.sendDocumentLoadedMessage_();
497 } else if (progress == 100) {
498 // Document load complete.
499 if (this.lastViewportPosition_)
500 this.viewport_.position = this.lastViewportPosition_;
501 this.paramsParser_.getViewportFromUrlParams(
502 this.browserApi_.getStreamInfo().originalUrl,
503 this.handleURLParams_.bind(this));
504 this.loadState_ = LoadState.SUCCESS;
505 this.sendDocumentLoadedMessage_();
506 while (this.delayedScriptingMessages_.length > 0)
507 this.handleScriptingMessage(this.delayedScriptingMessages_.shift());
509 if (this.isMaterial_)
510 this.toolbarManager_.hideToolbarsAfterTimeout();
516 * An event handler for handling password-submitted events. These are fired
517 * when an event is entered into the password screen.
518 * @param {Object} event a password-submitted event.
520 onPasswordSubmitted_: function(event) {
521 this.plugin_.postMessage({
522 type: 'getPasswordComplete',
523 password: event.detail.password
529 * An event handler for handling message events received from the plugin.
530 * @param {MessageObject} message a message event.
532 handlePluginMessage_: function(message) {
533 switch (message.data.type.toString()) {
534 case 'documentDimensions':
535 this.documentDimensions_ = message.data;
536 this.viewport_.setDocumentDimensions(this.documentDimensions_);
537 // If we received the document dimensions, the password was good so we
538 // can dismiss the password screen.
539 if (this.passwordScreen_.active)
540 this.passwordScreen_.accept();
542 if (this.isMaterial_) {
543 this.materialToolbar_.docLength =
544 this.documentDimensions_.pageDimensions.length;
546 this.pageIndicator_.initialFadeIn();
547 this.toolbar_.initialFadeIn();
551 var href = 'mailto:' + message.data.to + '?cc=' + message.data.cc +
552 '&bcc=' + message.data.bcc + '&subject=' + message.data.subject +
553 '&body=' + message.data.body;
554 window.location.href = href;
556 case 'getAccessibilityJSONReply':
557 this.sendScriptingMessage_(message.data);
560 // If the password screen isn't up, put it up. Otherwise we're
561 // responding to an incorrect password so deny it.
562 if (!this.passwordScreen_.active)
563 this.passwordScreen_.active = true;
565 this.passwordScreen_.deny();
567 case 'getSelectedTextReply':
568 this.sendScriptingMessage_(message.data);
571 this.viewport_.goToPage(message.data.page);
574 this.updateProgress_(message.data.progress);
577 // If in print preview, always open a new tab.
578 if (this.isPrintPreview_)
579 this.navigator_.navigate(message.data.url, true);
581 this.navigator_.navigate(message.data.url, message.data.newTab);
583 case 'setScrollPosition':
584 var position = this.viewport_.position;
585 if (message.data.x !== undefined)
586 position.x = message.data.x;
587 if (message.data.y !== undefined)
588 position.y = message.data.y;
589 this.viewport_.position = position;
591 case 'setTranslatedStrings':
592 this.passwordScreen_.text = message.data.getPasswordString;
593 if (!this.isMaterial_) {
594 this.progressBar_.text = message.data.loadingString;
595 if (!this.isPrintPreview_)
596 this.progressBar_.style.visibility = 'visible';
598 this.errorScreen_.text = message.data.loadFailedString;
600 case 'cancelStreamUrl':
601 chrome.mimeHandlerPrivate.abortStream();
604 this.bookmarks_ = message.data.bookmarks;
605 if (this.isMaterial_ && this.bookmarks_.length !== 0)
606 this.materialToolbar_.bookmarks = this.bookmarks;
608 case 'setIsSelecting':
609 this.viewportScroller_.setEnableScrolling(message.data.isSelecting);
611 case 'getNamedDestinationReply':
612 this.paramsParser_.onNamedDestinationReceived(
613 message.data.pageNumber);
620 * A callback that's called before the zoom changes. Notify the plugin to stop
621 * reacting to scroll events while zoom is taking place to avoid flickering.
623 beforeZoom_: function() {
624 this.plugin_.postMessage({
625 type: 'stopScrolling'
631 * A callback that's called after the zoom changes. Notify the plugin of the
632 * zoom change and to continue reacting to scroll events.
634 afterZoom_: function() {
635 var position = this.viewport_.position;
636 var zoom = this.viewport_.zoom;
637 if (this.isMaterial_)
638 this.zoomToolbar_.zoomValue = 100 * zoom;
639 this.plugin_.postMessage({
645 this.zoomManager_.onPdfZoomChange();
650 * A callback that's called after the viewport changes.
652 viewportChanged_: function() {
653 if (!this.documentDimensions_)
656 // Update the buttons selected.
657 if (!this.isMaterial_) {
658 $('fit-to-page-button').classList.remove('polymer-selected');
659 $('fit-to-width-button').classList.remove('polymer-selected');
660 if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
661 $('fit-to-page-button').classList.add('polymer-selected');
662 } else if (this.viewport_.fittingType ==
663 Viewport.FittingType.FIT_TO_WIDTH) {
664 $('fit-to-width-button').classList.add('polymer-selected');
668 // Offset the toolbar position so that it doesn't move if scrollbars appear.
669 var hasScrollbars = this.viewport_.documentHasScrollbars();
670 var scrollbarWidth = this.viewport_.scrollbarWidth;
671 var verticalScrollbarWidth = hasScrollbars.vertical ? scrollbarWidth : 0;
672 var horizontalScrollbarWidth =
673 hasScrollbars.horizontal ? scrollbarWidth : 0;
674 var toolbarRight = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
675 var toolbarBottom = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
676 toolbarRight -= verticalScrollbarWidth;
677 toolbarBottom -= horizontalScrollbarWidth;
678 if (!this.isMaterial_) {
679 this.toolbar_.style.right = toolbarRight + 'px';
680 this.toolbar_.style.bottom = toolbarBottom + 'px';
681 // Hide the toolbar if it doesn't fit in the viewport.
682 if (this.toolbar_.offsetLeft < 0 || this.toolbar_.offsetTop < 0)
683 this.toolbar_.style.visibility = 'hidden';
685 this.toolbar_.style.visibility = 'visible';
688 // Update the page indicator.
689 var visiblePage = this.viewport_.getMostVisiblePage();
690 if (this.isMaterial_) {
691 this.materialToolbar_.pageNo = visiblePage + 1;
693 this.pageIndicator_.index = visiblePage;
694 if (this.documentDimensions_.pageDimensions.length > 1 &&
695 hasScrollbars.vertical) {
696 this.pageIndicator_.style.visibility = 'visible';
698 this.pageIndicator_.style.visibility = 'hidden';
702 var visiblePageDimensions = this.viewport_.getPageScreenRect(visiblePage);
703 var size = this.viewport_.size;
704 this.sendScriptingMessage_({
706 pageX: visiblePageDimensions.x,
707 pageY: visiblePageDimensions.y,
708 pageWidth: visiblePageDimensions.width,
709 viewportWidth: size.width,
710 viewportHeight: size.height
715 * Handle a scripting message from outside the extension (typically sent by
716 * PDFScriptingAPI in a page containing the extension) to interact with the
718 * @param {MessageObject} message the message to handle.
720 handleScriptingMessage: function(message) {
721 if (this.parentWindow_ != message.source) {
722 this.parentWindow_ = message.source;
723 // Ensure that we notify the embedder if the document is loaded.
724 if (this.loadState_ != LoadState.LOADING)
725 this.sendDocumentLoadedMessage_();
728 if (this.handlePrintPreviewScriptingMessage_(message))
731 // Delay scripting messages from users of the scripting API until the
732 // document is loaded. This simplifies use of the APIs.
733 if (this.loadState_ != LoadState.SUCCESS) {
734 this.delayedScriptingMessages_.push(message);
738 switch (message.data.type.toString()) {
739 case 'getAccessibilityJSON':
740 case 'getSelectedText':
743 this.plugin_.postMessage(message.data);
750 * Handle scripting messages specific to print preview.
751 * @param {MessageObject} message the message to handle.
752 * @return {boolean} true if the message was handled, false otherwise.
754 handlePrintPreviewScriptingMessage_: function(message) {
755 if (!this.isPrintPreview_)
758 switch (message.data.type.toString()) {
759 case 'loadPreviewPage':
760 this.plugin_.postMessage(message.data);
762 case 'resetPrintPreviewMode':
763 this.loadState_ = LoadState.LOADING;
764 if (!this.inPrintPreviewMode_) {
765 this.inPrintPreviewMode_ = true;
766 this.viewport_.fitToPage();
769 // Stash the scroll location so that it can be restored when the new
770 // document is loaded.
771 this.lastViewportPosition_ = this.viewport_.position;
773 // TODO(raymes): Disable these properly in the plugin.
774 var printButton = $('print-button');
776 printButton.parentNode.removeChild(printButton);
777 var saveButton = $('save-button');
779 saveButton.parentNode.removeChild(saveButton);
781 if (!this.isMaterial_)
782 this.pageIndicator_.pageLabels = message.data.pageNumbers;
784 this.plugin_.postMessage({
785 type: 'resetPrintPreviewMode',
786 url: message.data.url,
787 grayscale: message.data.grayscale,
788 // If the PDF isn't modifiable we send 0 as the page count so that no
789 // blank placeholder pages get appended to the PDF.
790 pageCount: (message.data.modifiable ?
791 message.data.pageNumbers.length : 0)
795 this.handleKeyEvent_(DeserializeKeyEvent(message.data.keyEvent));
804 * Send a scripting message outside the extension (typically to
805 * PDFScriptingAPI in a page containing the extension).
806 * @param {Object} message the message to send.
808 sendScriptingMessage_: function(message) {
809 if (this.parentWindow_)
810 this.parentWindow_.postMessage(message, '*');
815 * @type {Viewport} the viewport of the PDF viewer.
818 return this.viewport_;
822 * Each bookmark is an Object containing a:
825 * - array of children (themselves bookmarks)
826 * @type {Array} the top-level bookmarks of the PDF.
829 return this.bookmarks_;