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.
6 * @typedef {{accessibility: Function,
7 * documentLoadComplete: Function,
9 * getHorizontalScrollbarThickness: Function,
10 * getPageLocationNormalized: Function,
11 * getVerticalScrollbarThickness: Function,
13 * getZoomLevel: Function,
15 * grayscale: Function,
16 * loadPreviewPage: Function,
18 * onPluginSizeChanged: Function,
20 * pageXOffset: Function,
21 * pageYOffset: Function,
22 * printPreviewPageCount: Function,
24 * removePrintButton: Function,
25 * resetPrintPreviewUrl: Function,
26 * sendKeyEvent: Function,
27 * setPageNumbers: Function,
28 * setPageXOffset: Function,
29 * setPageYOffset: Function,
30 * setZoomLevel: Function,
31 * fitToHeight: Function,
32 * fitToWidth: Function,
36 print_preview.PDFPlugin;
38 cr.define('print_preview', function() {
42 * Creates a PreviewArea object. It represents the area where the preview
43 * document is displayed.
44 * @param {!print_preview.DestinationStore} destinationStore Used to get the
45 * currently selected destination.
46 * @param {!print_preview.PrintTicketStore} printTicketStore Used to get
47 * information about how the preview should be displayed.
48 * @param {!print_preview.NativeLayer} nativeLayer Needed to communicate with
49 * Chromium's preview generation system.
50 * @param {!print_preview.DocumentInfo} documentInfo Document data model.
52 * @extends {print_preview.Component}
55 destinationStore, printTicketStore, nativeLayer, documentInfo) {
56 print_preview.Component.call(this);
57 // TODO(rltoscano): Understand the dependencies of printTicketStore needed
58 // here, and add only those here (not the entire print ticket store).
61 * Used to get the currently selected destination.
62 * @type {!print_preview.DestinationStore}
65 this.destinationStore_ = destinationStore;
68 * Used to get information about how the preview should be displayed.
69 * @type {!print_preview.PrintTicketStore}
72 this.printTicketStore_ = printTicketStore;
75 * Used to contruct the preview generator.
76 * @type {!print_preview.NativeLayer}
79 this.nativeLayer_ = nativeLayer;
82 * Document data model.
83 * @type {!print_preview.DocumentInfo}
86 this.documentInfo_ = documentInfo;
89 * Used to read generated page previews.
90 * @type {print_preview.PreviewGenerator}
93 this.previewGenerator_ = null;
96 * The embedded pdf plugin object. It's value is null if not yet loaded.
97 * @type {HTMLEmbedElement|print_preview.PDFPlugin}
103 * Custom margins component superimposed on the preview plugin.
104 * @type {!print_preview.MarginControlContainer}
107 this.marginControlContainer_ = new print_preview.MarginControlContainer(
109 this.printTicketStore_.marginsType,
110 this.printTicketStore_.customMargins,
111 this.printTicketStore_.measurementSystem,
112 this.onMarginDragChanged_.bind(this));
113 this.addChild(this.marginControlContainer_);
116 * Current zoom level as a percentage.
120 this.zoomLevel_ = null;
123 * Current page offset which can be used to calculate scroll amount.
124 * @type {print_preview.Coordinate2d}
127 this.pageOffset_ = null;
130 * Whether the plugin has finished reloading.
134 this.isPluginReloaded_ = false;
137 * Whether the document preview is ready.
141 this.isDocumentReady_ = false;
144 * Timeout object used to display a loading message if the preview is taking
145 * a long time to generate.
149 this.loadingTimeout_ = null;
153 * @type {HTMLElement}
156 this.overlayEl_ = null;
159 * The "Open system dialog" button.
160 * @type {HTMLButtonElement}
163 this.openSystemDialogButton_ = null;
167 * Event types dispatched by the preview area.
170 PreviewArea.EventType = {
171 // Dispatched when the "Open system dialog" button is clicked.
172 OPEN_SYSTEM_DIALOG_CLICK:
173 'print_preview.PreviewArea.OPEN_SYSTEM_DIALOG_CLICK',
175 // Dispatched when the document preview is complete.
176 PREVIEW_GENERATION_DONE:
177 'print_preview.PreviewArea.PREVIEW_GENERATION_DONE',
179 // Dispatched when the document preview failed to be generated.
180 PREVIEW_GENERATION_FAIL:
181 'print_preview.PreviewArea.PREVIEW_GENERATION_FAIL',
183 // Dispatched when a new document preview is being generated.
184 PREVIEW_GENERATION_IN_PROGRESS:
185 'print_preview.PreviewArea.PREVIEW_GENERATION_IN_PROGRESS'
189 * CSS classes used by the preview area.
193 PreviewArea.Classes_ = {
194 OUT_OF_PROCESS_COMPATIBILITY_OBJECT:
195 'preview-area-compatibility-object-out-of-process',
196 CUSTOM_MESSAGE_TEXT: 'preview-area-custom-message-text',
197 MESSAGE: 'preview-area-message',
198 INVISIBLE: 'invisible',
199 OPEN_SYSTEM_DIALOG_BUTTON: 'preview-area-open-system-dialog-button',
200 OPEN_SYSTEM_DIALOG_BUTTON_THROBBER:
201 'preview-area-open-system-dialog-button-throbber',
202 OVERLAY: 'preview-area-overlay-layer',
203 MARGIN_CONTROL: 'margin-control',
204 PREVIEW_AREA: 'preview-area-plugin-wrapper'
208 * Enumeration of IDs shown in the preview area.
212 PreviewArea.MessageId_ = {
215 PREVIEW_FAILED: 'preview-failed'
219 * Maps message IDs to the CSS class that contains them.
220 * @type {Object<print_preview.PreviewArea.MessageId_, string>}
223 PreviewArea.MessageIdClassMap_ = {};
224 PreviewArea.MessageIdClassMap_[PreviewArea.MessageId_.CUSTOM] =
225 'preview-area-custom-message';
226 PreviewArea.MessageIdClassMap_[PreviewArea.MessageId_.LOADING] =
227 'preview-area-loading-message';
228 PreviewArea.MessageIdClassMap_[PreviewArea.MessageId_.PREVIEW_FAILED] =
229 'preview-area-preview-failed-message';
232 * Amount of time in milliseconds to wait after issueing a new preview before
233 * the loading message is shown.
238 PreviewArea.LOADING_TIMEOUT_ = 200;
240 PreviewArea.prototype = {
241 __proto__: print_preview.Component.prototype,
244 * Should only be called after calling this.render().
245 * @return {boolean} Whether the preview area has a compatible plugin to
246 * display the print preview in.
248 get hasCompatiblePlugin() {
249 return this.previewGenerator_ != null;
253 * Processes a keyboard event that could possibly be used to change state of
254 * the preview plugin.
255 * @param {KeyboardEvent} e Keyboard event to process.
257 handleDirectionalKeyEvent: function(e) {
258 // Make sure the PDF plugin is there.
259 // We only care about: PageUp, PageDown, Left, Up, Right, Down.
260 // If the user is holding a modifier key, ignore.
262 !arrayContains([33, 34, 37, 38, 39, 40], e.keyCode) ||
263 e.metaKey || e.altKey || e.shiftKey || e.ctrlKey) {
267 // Don't handle the key event for these elements.
268 var tagName = document.activeElement.tagName;
269 if (arrayContains(['INPUT', 'SELECT', 'EMBED'], tagName)) {
273 // For the most part, if any div of header was the last clicked element,
274 // then the active element is the body. Starting with the last clicked
275 // element, and work up the DOM tree to see if any element has a
276 // scrollbar. If there exists a scrollbar, do not handle the key event
278 var element = e.target;
280 if (element.scrollHeight > element.clientHeight ||
281 element.scrollWidth > element.clientWidth) {
284 element = element.parentElement;
287 // No scroll bar anywhere, or the active element is something else, like a
288 // button. Note: buttons have a bigger scrollHeight than clientHeight.
289 this.plugin_.sendKeyEvent(e);
294 * Set a callback that gets called when a key event is received that
295 * originates in the plugin.
296 * @param {function(Event)} callback The callback to be called with a key
299 setPluginKeyEventCallback: function(callback) {
300 this.keyEventCallback_ = callback;
304 * Shows a custom message on the preview area's overlay.
305 * @param {string} message Custom message to show.
307 showCustomMessage: function(message) {
308 this.showMessage_(PreviewArea.MessageId_.CUSTOM, message);
312 enterDocument: function() {
313 print_preview.Component.prototype.enterDocument.call(this);
315 assert(this.openSystemDialogButton_),
317 this.onOpenSystemDialogButtonClick_.bind(this));
320 this.printTicketStore_,
321 print_preview.PrintTicketStore.EventType.INITIALIZE,
322 this.onTicketChange_.bind(this));
324 this.printTicketStore_,
325 print_preview.PrintTicketStore.EventType.TICKET_CHANGE,
326 this.onTicketChange_.bind(this));
328 this.printTicketStore_,
329 print_preview.PrintTicketStore.EventType.CAPABILITIES_CHANGE,
330 this.onTicketChange_.bind(this));
332 this.printTicketStore_,
333 print_preview.PrintTicketStore.EventType.DOCUMENT_CHANGE,
334 this.onTicketChange_.bind(this));
337 this.printTicketStore_.color,
338 print_preview.ticket_items.TicketItem.EventType.CHANGE,
339 this.onTicketChange_.bind(this));
341 this.printTicketStore_.cssBackground,
342 print_preview.ticket_items.TicketItem.EventType.CHANGE,
343 this.onTicketChange_.bind(this));
345 this.printTicketStore_.customMargins,
346 print_preview.ticket_items.TicketItem.EventType.CHANGE,
347 this.onTicketChange_.bind(this));
349 this.printTicketStore_.fitToPage,
350 print_preview.ticket_items.TicketItem.EventType.CHANGE,
351 this.onTicketChange_.bind(this));
353 this.printTicketStore_.headerFooter,
354 print_preview.ticket_items.TicketItem.EventType.CHANGE,
355 this.onTicketChange_.bind(this));
357 this.printTicketStore_.landscape,
358 print_preview.ticket_items.TicketItem.EventType.CHANGE,
359 this.onTicketChange_.bind(this));
361 this.printTicketStore_.marginsType,
362 print_preview.ticket_items.TicketItem.EventType.CHANGE,
363 this.onTicketChange_.bind(this));
365 this.printTicketStore_.pageRange,
366 print_preview.ticket_items.TicketItem.EventType.CHANGE,
367 this.onTicketChange_.bind(this));
369 this.printTicketStore_.selectionOnly,
370 print_preview.ticket_items.TicketItem.EventType.CHANGE,
371 this.onTicketChange_.bind(this));
373 if (this.checkPluginCompatibility_()) {
374 this.previewGenerator_ = new print_preview.PreviewGenerator(
375 this.destinationStore_,
376 this.printTicketStore_,
380 this.previewGenerator_,
381 print_preview.PreviewGenerator.EventType.PREVIEW_START,
382 this.onPreviewStart_.bind(this));
384 this.previewGenerator_,
385 print_preview.PreviewGenerator.EventType.PAGE_READY,
386 this.onPagePreviewReady_.bind(this));
388 this.previewGenerator_,
389 print_preview.PreviewGenerator.EventType.FAIL,
390 this.onPreviewGenerationFail_.bind(this));
392 this.previewGenerator_,
393 print_preview.PreviewGenerator.EventType.DOCUMENT_READY,
394 this.onDocumentReady_.bind(this));
396 this.showCustomMessage(loadTimeData.getString('noPlugin'));
401 exitDocument: function() {
402 print_preview.Component.prototype.exitDocument.call(this);
403 if (this.previewGenerator_) {
404 this.previewGenerator_.removeEventListeners();
406 this.overlayEl_ = null;
407 this.openSystemDialogButton_ = null;
411 decorateInternal: function() {
412 this.marginControlContainer_.decorate(this.getElement());
413 this.overlayEl_ = this.getElement().getElementsByClassName(
414 PreviewArea.Classes_.OVERLAY)[0];
415 this.openSystemDialogButton_ = this.getElement().getElementsByClassName(
416 PreviewArea.Classes_.OPEN_SYSTEM_DIALOG_BUTTON)[0];
420 * Checks to see if a suitable plugin for rendering the preview exists. If
421 * one does not exist, then an error message will be displayed.
422 * @return {boolean} true if Chromium has a plugin for rendering the
426 checkPluginCompatibility_: function() {
427 // TODO(raymes): It's harder to test compatibility of the out of process
428 // plugin because it's asynchronous. We could do a better job at some
430 var oopCompatObj = this.getElement().getElementsByClassName(
431 PreviewArea.Classes_.OUT_OF_PROCESS_COMPATIBILITY_OBJECT)[0];
432 var isOOPCompatible = oopCompatObj.postMessage;
433 oopCompatObj.parentElement.removeChild(oopCompatObj);
435 return isOOPCompatible;
439 * Shows a given message on the overlay.
440 * @param {!print_preview.PreviewArea.MessageId_} messageId ID of the
442 * @param {string=} opt_message Optional message to show that can be used
443 * by some message IDs.
446 showMessage_: function(messageId, opt_message) {
447 // Hide all messages.
448 var messageEls = this.getElement().getElementsByClassName(
449 PreviewArea.Classes_.MESSAGE);
450 for (var i = 0, messageEl; messageEl = messageEls[i]; i++) {
451 setIsVisible(messageEl, false);
453 // Disable jumping animation to conserve cycles.
454 var jumpingDotsEl = this.getElement().querySelector(
455 '.preview-area-loading-message-jumping-dots');
456 jumpingDotsEl.classList.remove('jumping-dots');
458 // Show specific message.
459 if (messageId == PreviewArea.MessageId_.CUSTOM) {
460 var customMessageTextEl = this.getElement().getElementsByClassName(
461 PreviewArea.Classes_.CUSTOM_MESSAGE_TEXT)[0];
462 customMessageTextEl.textContent = opt_message;
463 } else if (messageId == PreviewArea.MessageId_.LOADING) {
464 jumpingDotsEl.classList.add('jumping-dots');
466 var messageEl = this.getElement().getElementsByClassName(
467 PreviewArea.MessageIdClassMap_[messageId])[0];
468 setIsVisible(messageEl, true);
470 this.setOverlayVisible_(true);
474 * Set the visibility of the message overlay.
475 * @param {boolean} visible Whether to make the overlay visible or not
478 setOverlayVisible_: function(visible) {
479 this.overlayEl_.classList.toggle(
480 PreviewArea.Classes_.INVISIBLE,
482 this.overlayEl_.setAttribute('aria-hidden', !visible);
484 // Hide/show all controls that will overlap when the overlay is visible.
485 var marginControls = this.getElement().getElementsByClassName(
486 PreviewArea.Classes_.MARGIN_CONTROL);
487 for (var i = 0; i < marginControls.length; ++i) {
488 marginControls[i].setAttribute('aria-hidden', visible);
490 var previewAreaControls = this.getElement().getElementsByClassName(
491 PreviewArea.Classes_.PREVIEW_AREA);
492 for (var i = 0; i < previewAreaControls.length; ++i) {
493 previewAreaControls[i].setAttribute('aria-hidden', visible);
497 // Disable jumping animation to conserve cycles.
498 var jumpingDotsEl = this.getElement().querySelector(
499 '.preview-area-loading-message-jumping-dots');
500 jumpingDotsEl.classList.remove('jumping-dots');
505 * Creates a preview plugin and adds it to the DOM.
506 * @param {string} srcUrl Initial URL of the plugin.
509 createPlugin_: function(srcUrl) {
511 console.warn('Pdf preview plugin already created');
515 this.plugin_ = /** @type {print_preview.PDFPlugin} */(
516 PDFCreateOutOfProcessPlugin(srcUrl));
517 this.plugin_.setKeyEventCallback(this.keyEventCallback_);
519 this.plugin_.setAttribute('class', 'preview-area-plugin');
520 this.plugin_.setAttribute('aria-live', 'polite');
521 this.plugin_.setAttribute('aria-atomic', 'true');
522 // NOTE: The plugin's 'id' field must be set to 'pdf-viewer' since
523 // chrome/renderer/printing/print_web_view_helper.cc actually references
525 this.plugin_.setAttribute('id', 'pdf-viewer');
526 this.getChildElement('.preview-area-plugin-wrapper').
527 appendChild(/** @type {Node} */(this.plugin_));
531 this.printTicketStore_.pageRange.getPageNumberSet().asArray();
532 var grayscale = !this.printTicketStore_.color.getValue();
533 this.plugin_.setLoadCallback(this.onPluginLoad_.bind(this));
534 this.plugin_.setViewportChangedCallback(
535 this.onPreviewVisualStateChange_.bind(this));
536 this.plugin_.resetPrintPreviewMode(srcUrl, grayscale, pageNumbers,
537 this.documentInfo_.isModifiable);
541 * Dispatches a PREVIEW_GENERATION_DONE event if all conditions are met.
544 dispatchPreviewGenerationDoneIfReady_: function() {
545 if (this.isDocumentReady_ && this.isPluginReloaded_) {
546 cr.dispatchSimpleEvent(
547 this, PreviewArea.EventType.PREVIEW_GENERATION_DONE);
548 this.marginControlContainer_.showMarginControlsIfNeeded();
553 * Called when the open-system-dialog button is clicked. Disables the
554 * button, shows the throbber, and dispatches the OPEN_SYSTEM_DIALOG_CLICK
558 onOpenSystemDialogButtonClick_: function() {
559 this.openSystemDialogButton_.disabled = true;
560 var openSystemDialogThrobber = this.getElement().getElementsByClassName(
561 PreviewArea.Classes_.OPEN_SYSTEM_DIALOG_BUTTON_THROBBER)[0];
562 setIsVisible(openSystemDialogThrobber, true);
563 cr.dispatchSimpleEvent(
564 this, PreviewArea.EventType.OPEN_SYSTEM_DIALOG_CLICK);
568 * Called when the print ticket changes. Updates the preview.
571 onTicketChange_: function() {
572 if (this.previewGenerator_ && this.previewGenerator_.requestPreview()) {
573 cr.dispatchSimpleEvent(
574 this, PreviewArea.EventType.PREVIEW_GENERATION_IN_PROGRESS);
575 if (this.loadingTimeout_ == null) {
576 this.loadingTimeout_ = setTimeout(
577 this.showMessage_.bind(this, PreviewArea.MessageId_.LOADING),
578 PreviewArea.LOADING_TIMEOUT_);
581 this.marginControlContainer_.showMarginControlsIfNeeded();
586 * Called when the preview generator begins loading the preview.
587 * @param {Event} event Contains the URL to initialize the plugin to.
590 onPreviewStart_: function(event) {
591 this.isDocumentReady_ = false;
592 this.isPluginReloaded_ = false;
594 this.createPlugin_(event.previewUrl);
596 var grayscale = !this.printTicketStore_.color.getValue();
598 this.printTicketStore_.pageRange.getPageNumberSet().asArray();
599 var url = event.previewUrl;
600 this.plugin_.resetPrintPreviewMode(url, grayscale, pageNumbers,
601 this.documentInfo_.isModifiable);
603 cr.dispatchSimpleEvent(
604 this, PreviewArea.EventType.PREVIEW_GENERATION_IN_PROGRESS);
608 * Called when a page preview has been generated. Updates the plugin with
610 * @param {Event} event Contains information about the page preview.
613 onPagePreviewReady_: function(event) {
614 this.plugin_.loadPreviewPage(event.previewUrl, event.previewIndex);
618 * Called when the preview generation is complete and the document is ready
622 onDocumentReady_: function(event) {
623 this.isDocumentReady_ = true;
624 this.dispatchPreviewGenerationDoneIfReady_();
628 * Called when the generation of a preview fails. Shows an error message.
631 onPreviewGenerationFail_: function() {
632 if (this.loadingTimeout_) {
633 clearTimeout(this.loadingTimeout_);
634 this.loadingTimeout_ = null;
636 this.showMessage_(PreviewArea.MessageId_.PREVIEW_FAILED);
637 cr.dispatchSimpleEvent(
638 this, PreviewArea.EventType.PREVIEW_GENERATION_FAIL);
642 * Called when the plugin loads. This is a consequence of calling
643 * plugin.reload(). Certain plugin state can only be set after the plugin
647 onPluginLoad_: function() {
648 if (this.loadingTimeout_) {
649 clearTimeout(this.loadingTimeout_);
650 this.loadingTimeout_ = null;
653 this.setOverlayVisible_(false);
654 this.isPluginReloaded_ = true;
655 this.dispatchPreviewGenerationDoneIfReady_();
659 * Called when the preview plugin's visual state has changed. This is a
660 * consequence of scrolling or zooming the plugin. Updates the custom
661 * margins component if shown.
664 onPreviewVisualStateChange_: function(pageX,
669 this.marginControlContainer_.updateTranslationTransform(
670 new print_preview.Coordinate2d(pageX, pageY));
671 this.marginControlContainer_.updateScaleTransform(
672 pageWidth / this.documentInfo_.pageSize.width);
673 this.marginControlContainer_.updateClippingMask(
674 new print_preview.Size(viewportWidth, viewportHeight));
678 * Called when dragging margins starts or stops.
679 * @param {boolean} isDragging True if the margin is currently being dragged
680 * and false otherwise.
682 onMarginDragChanged_: function(isDragging) {
686 // When hovering over the plugin (which may be in a separate iframe)
687 // pointer events will be sent to the frame. When dragging the margins,
688 // we don't want this to happen as it can cause the margin to stop
690 this.plugin_.style.pointerEvents = isDragging ? 'none' : 'auto';
696 PreviewArea: PreviewArea