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