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