preview_area.js revision 5821806d5e7f356e8fa4b058a389a808ea183019
1// Copyright (c) 2012 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 5cr.define('print_preview', function() { 6 'use strict'; 7 8 /** 9 * Creates a PreviewArea object. It represents the area where the preview 10 * document is displayed. 11 * @param {!print_preview.DestinationStore} destinationStore Used to get the 12 * currently selected destination. 13 * @param {!print_preview.PrintTicketStore} printTicketStore Used to get 14 * information about how the preview should be displayed. 15 * @param {!print_preview.NativeLayer} nativeLayer Needed to communicate with 16 * Chromium's preview generation system. 17 * @constructor 18 * @extends {print_preview.Component} 19 */ 20 function PreviewArea(destinationStore, printTicketStore, nativeLayer) { 21 print_preview.Component.call(this); 22 23 /** 24 * Used to get the currently selected destination. 25 * @type {!print_preview.DestinationStore} 26 * @private 27 */ 28 this.destinationStore_ = destinationStore; 29 30 /** 31 * Used to get information about how the preview should be displayed. 32 * @type {!print_preview.PrintTicketStore} 33 * @private 34 */ 35 this.printTicketStore_ = printTicketStore; 36 37 /** 38 * Used to contruct the preview generator. 39 * @type {!print_preview.NativeLayer} 40 * @private 41 */ 42 this.nativeLayer_ = nativeLayer; 43 44 /** 45 * Used to read generated page previews. 46 * @type {print_preview.PreviewGenerator} 47 * @private 48 */ 49 this.previewGenerator_ = null; 50 51 /** 52 * The embedded pdf plugin object. It's value is null if not yet loaded. 53 * @type {HTMLEmbedElement} 54 * @private 55 */ 56 this.plugin_ = null; 57 58 /** 59 * Custom margins component superimposed on the preview plugin. 60 * @type {!print_preview.MarginControlContainer} 61 * @private 62 */ 63 this.marginControlContainer_ = 64 new print_preview.MarginControlContainer(this.printTicketStore_); 65 this.addChild(this.marginControlContainer_); 66 67 /** 68 * Current zoom level as a percentage. 69 * @type {?number} 70 * @private 71 */ 72 this.zoomLevel_ = null; 73 74 /** 75 * Current page offset which can be used to calculate scroll amount. 76 * @type {print_preview.Coordinate2d} 77 * @private 78 */ 79 this.pageOffset_ = null; 80 81 /** 82 * Whether the plugin has finished reloading. 83 * @type {boolean} 84 * @private 85 */ 86 this.isPluginReloaded_ = false; 87 88 /** 89 * Whether the document preview is ready. 90 * @type {boolean} 91 * @private 92 */ 93 this.isDocumentReady_ = false; 94 95 /** 96 * Timeout object used to display a loading message if the preview is taking 97 * a long time to generate. 98 * @type {?number} 99 * @private 100 */ 101 this.loadingTimeout_ = null; 102 103 /** 104 * Overlay element. 105 * @type {HTMLElement} 106 * @private 107 */ 108 this.overlayEl_ = null; 109 110 /** 111 * The "Open system dialog" button. 112 * @type {HTMLButtonElement} 113 * @private 114 */ 115 this.openSystemDialogButton_ = null; 116 }; 117 118 /** 119 * Event types dispatched by the preview area. 120 * @enum {string} 121 */ 122 PreviewArea.EventType = { 123 // Dispatched when the "Open system dialog" button is clicked. 124 OPEN_SYSTEM_DIALOG_CLICK: 125 'print_preview.PreviewArea.OPEN_SYSTEM_DIALOG_CLICK', 126 127 // Dispatched when the document preview is complete. 128 PREVIEW_GENERATION_DONE: 129 'print_preview.PreviewArea.PREVIEW_GENERATION_DONE', 130 131 // Dispatched when the document preview failed to be generated. 132 PREVIEW_GENERATION_FAIL: 133 'print_preview.PreviewArea.PREVIEW_GENERATION_FAIL', 134 135 // Dispatched when a new document preview is being generated. 136 PREVIEW_GENERATION_IN_PROGRESS: 137 'print_preview.PreviewArea.PREVIEW_GENERATION_IN_PROGRESS' 138 }; 139 140 /** 141 * CSS classes used by the preview area. 142 * @enum {string} 143 * @private 144 */ 145 PreviewArea.Classes_ = { 146 COMPATIBILITY_OBJECT: 'preview-area-compatibility-object', 147 CUSTOM_MESSAGE_TEXT: 'preview-area-custom-message-text', 148 MESSAGE: 'preview-area-message', 149 INVISIBLE: 'invisible', 150 OPEN_SYSTEM_DIALOG_BUTTON: 'preview-area-open-system-dialog-button', 151 OPEN_SYSTEM_DIALOG_BUTTON_THROBBER: 152 'preview-area-open-system-dialog-button-throbber', 153 OVERLAY: 'preview-area-overlay-layer' 154 }; 155 156 /** 157 * Enumeration of IDs shown in the preview area. 158 * @enum {string} 159 * @private 160 */ 161 PreviewArea.MessageId_ = { 162 CUSTOM: 'custom', 163 LOADING: 'loading', 164 PREVIEW_FAILED: 'preview-failed' 165 }; 166 167 /** 168 * Maps message IDs to the CSS class that contains them. 169 * @type {object.<PreviewArea.MessageId_, string>} 170 * @private 171 */ 172 PreviewArea.MessageIdClassMap_ = {}; 173 PreviewArea.MessageIdClassMap_[PreviewArea.MessageId_.CUSTOM] = 174 'preview-area-custom-message'; 175 PreviewArea.MessageIdClassMap_[PreviewArea.MessageId_.LOADING] = 176 'preview-area-loading-message'; 177 PreviewArea.MessageIdClassMap_[PreviewArea.MessageId_.PREVIEW_FAILED] = 178 'preview-area-preview-failed-message'; 179 180 /** 181 * Amount of time in milliseconds to wait after issueing a new preview before 182 * the loading message is shown. 183 * @type {number} 184 * @const 185 * @private 186 */ 187 PreviewArea.LOADING_TIMEOUT_ = 200; 188 189 PreviewArea.prototype = { 190 __proto__: print_preview.Component.prototype, 191 192 /** 193 * Should only be called after calling this.render(). 194 * @return {boolean} Whether the preview area has a compatible plugin to 195 * display the print preview in. 196 */ 197 get hasCompatiblePlugin() { 198 return this.previewGenerator_ != null; 199 }, 200 201 /** 202 * Processes a keyboard event that could possibly be used to change state of 203 * the preview plugin. 204 * @param {MouseEvent} e Mouse event to process. 205 */ 206 handleDirectionalKeyEvent: function(e) { 207 // Make sure the PDF plugin is there. 208 // We only care about: PageUp, PageDown, Left, Up, Right, Down. 209 // If the user is holding a modifier key, ignore. 210 if (!this.plugin_ || 211 !arrayContains([33, 34, 37, 38, 39, 40], e.keyCode) || 212 e.metaKey || e.altKey || e.shiftKey || e.ctrlKey) { 213 return; 214 } 215 216 // Don't handle the key event for these elements. 217 var tagName = document.activeElement.tagName; 218 if (arrayContains(['INPUT', 'SELECT', 'EMBED'], tagName)) { 219 return; 220 } 221 222 // For the most part, if any div of header was the last clicked element, 223 // then the active element is the body. Starting with the last clicked 224 // element, and work up the DOM tree to see if any element has a 225 // scrollbar. If there exists a scrollbar, do not handle the key event 226 // here. 227 var element = e.target; 228 while (element) { 229 if (element.scrollHeight > element.clientHeight || 230 element.scrollWidth > element.clientWidth) { 231 return; 232 } 233 element = element.parentElement; 234 } 235 236 // No scroll bar anywhere, or the active element is something else, like a 237 // button. Note: buttons have a bigger scrollHeight than clientHeight. 238 this.plugin_.sendKeyEvent(e.keyCode); 239 e.preventDefault(); 240 }, 241 242 /** 243 * Shows a custom message on the preview area's overlay. 244 * @param {string} message Custom message to show. 245 */ 246 showCustomMessage: function(message) { 247 this.showMessage_(PreviewArea.MessageId_.CUSTOM, message); 248 }, 249 250 /** @override */ 251 enterDocument: function() { 252 print_preview.Component.prototype.enterDocument.call(this); 253 this.tracker.add( 254 this.openSystemDialogButton_, 255 'click', 256 this.onOpenSystemDialogButtonClick_.bind(this)); 257 258 this.tracker.add( 259 this.printTicketStore_, 260 print_preview.PrintTicketStore.EventType.INITIALIZE, 261 this.onTicketChange_.bind(this)); 262 this.tracker.add( 263 this.printTicketStore_, 264 print_preview.PrintTicketStore.EventType.TICKET_CHANGE, 265 this.onTicketChange_.bind(this)); 266 this.tracker.add( 267 this.printTicketStore_, 268 print_preview.PrintTicketStore.EventType.CAPABILITIES_CHANGE, 269 this.onTicketChange_.bind(this)); 270 this.tracker.add( 271 this.printTicketStore_, 272 print_preview.PrintTicketStore.EventType.DOCUMENT_CHANGE, 273 this.onTicketChange_.bind(this)); 274 275 if (this.checkPluginCompatibility_()) { 276 this.previewGenerator_ = new print_preview.PreviewGenerator( 277 this.destinationStore_, this.printTicketStore_, this.nativeLayer_); 278 this.tracker.add( 279 this.previewGenerator_, 280 print_preview.PreviewGenerator.EventType.PREVIEW_START, 281 this.onPreviewStart_.bind(this)); 282 this.tracker.add( 283 this.previewGenerator_, 284 print_preview.PreviewGenerator.EventType.PAGE_READY, 285 this.onPagePreviewReady_.bind(this)); 286 this.tracker.add( 287 this.previewGenerator_, 288 print_preview.PreviewGenerator.EventType.FAIL, 289 this.onPreviewGenerationFail_.bind(this)); 290 this.tracker.add( 291 this.previewGenerator_, 292 print_preview.PreviewGenerator.EventType.DOCUMENT_READY, 293 this.onDocumentReady_.bind(this)); 294 } else { 295 this.showCustomMessage(localStrings.getString('noPlugin')); 296 } 297 }, 298 299 /** @override */ 300 exitDocument: function() { 301 print_preview.Component.prototype.exitDocument.call(this); 302 if (this.previewGenerator_) { 303 this.previewGenerator_.removeEventListeners(); 304 } 305 this.overlayEl_ = null; 306 this.openSystemDialogButton_ = null; 307 }, 308 309 /** @override */ 310 decorateInternal: function() { 311 this.marginControlContainer_.decorate(this.getElement()); 312 this.overlayEl_ = this.getElement().getElementsByClassName( 313 PreviewArea.Classes_.OVERLAY)[0]; 314 this.openSystemDialogButton_ = this.getElement().getElementsByClassName( 315 PreviewArea.Classes_.OPEN_SYSTEM_DIALOG_BUTTON)[0]; 316 }, 317 318 /** 319 * Checks to see if a suitable plugin for rendering the preview exists. If 320 * one does not exist, then an error message will be displayed. 321 * @return {boolean} Whether Chromium has a suitable plugin for rendering 322 * the preview. 323 * @private 324 */ 325 checkPluginCompatibility_: function() { 326 var compatObj = this.getElement().getElementsByClassName( 327 PreviewArea.Classes_.COMPATIBILITY_OBJECT)[0]; 328 var isCompatible = 329 compatObj.onload && 330 compatObj.goToPage && 331 compatObj.removePrintButton && 332 compatObj.loadPreviewPage && 333 compatObj.printPreviewPageCount && 334 compatObj.resetPrintPreviewUrl && 335 compatObj.onPluginSizeChanged && 336 compatObj.onScroll && 337 compatObj.pageXOffset && 338 compatObj.pageYOffset && 339 compatObj.setZoomLevel && 340 compatObj.setPageNumbers && 341 compatObj.setPageXOffset && 342 compatObj.setPageYOffset && 343 compatObj.getHorizontalScrollbarThickness && 344 compatObj.getVerticalScrollbarThickness && 345 compatObj.getPageLocationNormalized && 346 compatObj.getHeight && 347 compatObj.getWidth; 348 compatObj.parentElement.removeChild(compatObj); 349 return isCompatible; 350 }, 351 352 /** 353 * Shows a given message on the overlay. 354 * @param {!print_preview.PreviewArea.MessageId_} messageId ID of the 355 * message to show. 356 * @param {string=} opt_message Optional message to show that can be used 357 * by some message IDs. 358 * @private 359 */ 360 showMessage_: function(messageId, opt_message) { 361 // Hide all messages. 362 var messageEls = this.getElement().getElementsByClassName( 363 PreviewArea.Classes_.MESSAGE); 364 for (var i = 0, messageEl; messageEl = messageEls[i]; i++) { 365 setIsVisible(messageEl, false); 366 } 367 // Disable jumping animation to conserve cycles. 368 var jumpingDotsEl = this.getElement().querySelector( 369 '.preview-area-loading-message-jumping-dots'); 370 jumpingDotsEl.classList.remove('jumping-dots'); 371 372 // Show specific message. 373 if (messageId == PreviewArea.MessageId_.CUSTOM) { 374 var customMessageTextEl = this.getElement().getElementsByClassName( 375 PreviewArea.Classes_.CUSTOM_MESSAGE_TEXT)[0]; 376 customMessageTextEl.textContent = opt_message; 377 } else if (messageId == PreviewArea.MessageId_.LOADING) { 378 jumpingDotsEl.classList.add('jumping-dots'); 379 } 380 var messageEl = this.getElement().getElementsByClassName( 381 PreviewArea.MessageIdClassMap_[messageId])[0]; 382 setIsVisible(messageEl, true); 383 384 // Show overlay. 385 this.overlayEl_.classList.remove(PreviewArea.Classes_.INVISIBLE); 386 }, 387 388 /** 389 * Hides the message overlay. 390 * @private 391 */ 392 hideOverlay_: function() { 393 this.overlayEl_.classList.add(PreviewArea.Classes_.INVISIBLE); 394 // Disable jumping animation to conserve cycles. 395 var jumpingDotsEl = this.getElement().querySelector( 396 '.preview-area-loading-message-jumping-dots'); 397 jumpingDotsEl.classList.remove('jumping-dots'); 398 }, 399 400 /** 401 * Creates a preview plugin and adds it to the DOM. 402 * @param {string} srcUrl Initial URL of the plugin. 403 * @private 404 */ 405 createPlugin_: function(srcUrl) { 406 if (this.plugin_) { 407 console.warn('Pdf preview plugin already created'); 408 return; 409 } 410 this.plugin_ = document.createElement('embed'); 411 // NOTE: The plugin's 'id' field must be set to 'pdf-viewer' since 412 // chrome/renderer/print_web_view_helper.cc actually references it. 413 this.plugin_.setAttribute('id', 'pdf-viewer'); 414 this.plugin_.setAttribute('class', 'preview-area-plugin'); 415 this.plugin_.setAttribute( 416 'type', 'application/x-google-chrome-print-preview-pdf'); 417 this.plugin_.setAttribute('src', srcUrl); 418 this.plugin_.setAttribute('aria-live', 'polite'); 419 this.plugin_.setAttribute('aria-atomic', 'true'); 420 this.getChildElement('.preview-area-plugin-wrapper'). 421 appendChild(this.plugin_); 422 423 global['onPreviewPluginLoad'] = this.onPluginLoad_.bind(this); 424 this.plugin_.onload('onPreviewPluginLoad()'); 425 426 global['onPreviewPluginVisualStateChange'] = 427 this.onPreviewVisualStateChange_.bind(this); 428 this.plugin_.onScroll('onPreviewPluginVisualStateChange()'); 429 this.plugin_.onPluginSizeChanged('onPreviewPluginVisualStateChange()'); 430 431 this.plugin_.removePrintButton(); 432 this.plugin_.grayscale(!this.printTicketStore_.isColorEnabled()); 433 }, 434 435 /** 436 * Dispatches a PREVIEW_GENERATION_DONE event if all conditions are met. 437 * @private 438 */ 439 dispatchPreviewGenerationDoneIfReady_: function() { 440 if (this.isDocumentReady_ && this.isPluginReloaded_) { 441 cr.dispatchSimpleEvent( 442 this, PreviewArea.EventType.PREVIEW_GENERATION_DONE); 443 this.marginControlContainer_.showMarginControlsIfNeeded(); 444 } 445 }, 446 447 /** 448 * Called when the open-system-dialog button is clicked. Disables the 449 * button, shows the throbber, and dispatches the OPEN_SYSTEM_DIALOG_CLICK 450 * event. 451 * @private 452 */ 453 onOpenSystemDialogButtonClick_: function() { 454 this.openSystemDialogButton_.disabled = true; 455 var openSystemDialogThrobber = this.getElement().getElementsByClassName( 456 PreviewArea.Classes_.OPEN_SYSTEM_DIALOG_BUTTON_THROBBER)[0]; 457 setIsVisible(openSystemDialogThrobber, true); 458 cr.dispatchSimpleEvent( 459 this, PreviewArea.EventType.OPEN_SYSTEM_DIALOG_CLICK); 460 }, 461 462 /** 463 * Called when the print ticket changes. Updates the preview. 464 * @private 465 */ 466 onTicketChange_: function() { 467 if (this.previewGenerator_ && this.previewGenerator_.requestPreview()) { 468 if (this.loadingTimeout_ == null) { 469 this.loadingTimeout_ = setTimeout( 470 this.showMessage_.bind(this, PreviewArea.MessageId_.LOADING), 471 PreviewArea.LOADING_TIMEOUT_); 472 } 473 } else { 474 this.marginControlContainer_.showMarginControlsIfNeeded(); 475 } 476 }, 477 478 /** 479 * Called when the preview generator begins loading the preview. 480 * @param {cr.Event} Contains the URL to initialize the plugin to. 481 * @private 482 */ 483 onPreviewStart_: function(event) { 484 this.isDocumentReady_ = false; 485 this.isPluginReloaded_ = false; 486 if (!this.plugin_) { 487 this.createPlugin_(event.previewUrl); 488 } 489 this.plugin_.goToPage('0'); 490 this.plugin_.resetPrintPreviewUrl(event.previewUrl); 491 this.plugin_.reload(); 492 this.plugin_.grayscale(!this.printTicketStore_.isColorEnabled()); 493 cr.dispatchSimpleEvent( 494 this, PreviewArea.EventType.PREVIEW_GENERATION_IN_PROGRESS); 495 }, 496 497 /** 498 * Called when a page preview has been generated. Updates the plugin with 499 * the new page. 500 * @param {cr.Event} event Contains information about the page preview. 501 * @private 502 */ 503 onPagePreviewReady_: function(event) { 504 this.plugin_.loadPreviewPage(event.previewUrl, event.previewIndex); 505 }, 506 507 /** 508 * Called when the preview generation is complete and the document is ready 509 * to print. 510 * @private 511 */ 512 onDocumentReady_: function(event) { 513 this.isDocumentReady_ = true; 514 this.dispatchPreviewGenerationDoneIfReady_(); 515 }, 516 517 /** 518 * Called when the generation of a preview fails. Shows an error message. 519 * @private 520 */ 521 onPreviewGenerationFail_: function() { 522 this.showMessage_(PreviewArea.MessageId_.PREVIEW_FAILED); 523 cr.dispatchSimpleEvent( 524 this, PreviewArea.EventType.PREVIEW_GENERATION_FAIL); 525 }, 526 527 /** 528 * Called when the plugin loads. This is a consequence of calling 529 * plugin.reload(). Certain plugin state can only be set after the plugin 530 * has loaded. 531 * @private 532 */ 533 onPluginLoad_: function() { 534 if (this.loadingTimeout_) { 535 clearTimeout(this.loadingTimeout_); 536 this.loadingTimeout_ = null; 537 } 538 // Setting the plugin's page count can only be called after the plugin is 539 // loaded and the document must be modifiable. 540 if (this.printTicketStore_.isDocumentModifiable) { 541 this.plugin_.printPreviewPageCount( 542 this.printTicketStore_.getPageNumberSet().size); 543 } 544 this.plugin_.setPageNumbers(JSON.stringify( 545 this.printTicketStore_.getPageNumberSet().asArray())); 546 if (this.zoomLevel_ != null && this.pageOffset_ != null) { 547 this.plugin_.setZoomLevel(this.zoomLevel_); 548 this.plugin_.setPageXOffset(this.pageOffset_.x); 549 this.plugin_.setPageYOffset(this.pageOffset_.y); 550 } else { 551 this.plugin_.fitToHeight(); 552 } 553 this.hideOverlay_(); 554 this.isPluginReloaded_ = true; 555 this.dispatchPreviewGenerationDoneIfReady_(); 556 }, 557 558 /** 559 * Called when the preview plugin's visual state has changed. This is a 560 * consequence of scrolling or zooming the plugin. Updates the custom 561 * margins component if shown. 562 * @private 563 */ 564 onPreviewVisualStateChange_: function() { 565 if (this.isPluginReloaded_) { 566 this.zoomLevel_ = this.plugin_.getZoomLevel(); 567 this.pageOffset_ = new print_preview.Coordinate2d( 568 this.plugin_.pageXOffset(), this.plugin_.pageYOffset()); 569 } 570 var pageLocationNormalizedStr = this.plugin_.getPageLocationNormalized(); 571 if (!pageLocationNormalizedStr) { 572 return; 573 } 574 var normalized = pageLocationNormalizedStr.split(';'); 575 var pluginWidth = this.plugin_.getWidth(); 576 var pluginHeight = this.plugin_.getHeight(); 577 var translationTransform = new print_preview.Coordinate2d( 578 parseFloat(normalized[0]) * pluginWidth, 579 parseFloat(normalized[1]) * pluginHeight); 580 this.marginControlContainer_.updateTranslationTransform( 581 translationTransform); 582 var pageWidthInPixels = parseFloat(normalized[2]) * pluginWidth; 583 this.marginControlContainer_.updateScaleTransform( 584 pageWidthInPixels / this.printTicketStore_.pageSize.width); 585 this.marginControlContainer_.updateClippingMask( 586 new print_preview.Size( 587 pluginWidth - this.plugin_.getVerticalScrollbarThickness(), 588 pluginHeight - this.plugin_.getHorizontalScrollbarThickness())); 589 } 590 }; 591 592 // Export 593 return { 594 PreviewArea: PreviewArea 595 }; 596}); 597