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() {
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.
19 * @extends {print_preview.Component}
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).
28 * Used to get the currently selected destination.
29 * @type {!print_preview.DestinationStore}
32 this.destinationStore_ = destinationStore;
35 * Used to get information about how the preview should be displayed.
36 * @type {!print_preview.PrintTicketStore}
39 this.printTicketStore_ = printTicketStore;
42 * Used to contruct the preview generator.
43 * @type {!print_preview.NativeLayer}
46 this.nativeLayer_ = nativeLayer;
49 * Document data model.
50 * @type {!print_preview.DocumentInfo}
53 this.documentInfo_ = documentInfo;
56 * Used to read generated page previews.
57 * @type {print_preview.PreviewGenerator}
60 this.previewGenerator_ = null;
63 * The embedded pdf plugin object. It's value is null if not yet loaded.
64 * @type {HTMLEmbedElement}
70 * Custom margins component superimposed on the preview plugin.
71 * @type {!print_preview.MarginControlContainer}
74 this.marginControlContainer_ = new print_preview.MarginControlContainer(
76 this.printTicketStore_.marginsType,
77 this.printTicketStore_.customMargins,
78 this.printTicketStore_.measurementSystem);
79 this.addChild(this.marginControlContainer_);
82 * Current zoom level as a percentage.
86 this.zoomLevel_ = null;
89 * Current page offset which can be used to calculate scroll amount.
90 * @type {print_preview.Coordinate2d}
93 this.pageOffset_ = null;
96 * Whether the plugin has finished reloading.
100 this.isPluginReloaded_ = false;
103 * Whether the document preview is ready.
107 this.isDocumentReady_ = false;
110 * Timeout object used to display a loading message if the preview is taking
111 * a long time to generate.
115 this.loadingTimeout_ = null;
119 * @type {HTMLElement}
122 this.overlayEl_ = null;
125 * The "Open system dialog" button.
126 * @type {HTMLButtonElement}
129 this.openSystemDialogButton_ = null;
133 * Event types dispatched by the preview area.
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'
155 * CSS classes used by the preview area.
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'
171 * Enumeration of IDs shown in the preview area.
175 PreviewArea.MessageId_ = {
178 PREVIEW_FAILED: 'preview-failed'
182 * Maps message IDs to the CSS class that contains them.
183 * @type {object.<PreviewArea.MessageId_, string>}
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';
195 * Amount of time in milliseconds to wait after issueing a new preview before
196 * the loading message is shown.
201 PreviewArea.LOADING_TIMEOUT_ = 200;
203 PreviewArea.prototype = {
204 __proto__: print_preview.Component.prototype,
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.
211 get hasCompatiblePlugin() {
212 return this.previewGenerator_ != null;
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.
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.
225 !arrayContains([33, 34, 37, 38, 39, 40], e.keyCode) ||
226 e.metaKey || e.altKey || e.shiftKey || e.ctrlKey) {
230 // Don't handle the key event for these elements.
231 var tagName = document.activeElement.tagName;
232 if (arrayContains(['INPUT', 'SELECT', 'EMBED'], tagName)) {
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
241 var element = e.target;
243 if (element.scrollHeight > element.clientHeight ||
244 element.scrollWidth > element.clientWidth) {
247 element = element.parentElement;
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);
257 * Shows a custom message on the preview area's overlay.
258 * @param {string} message Custom message to show.
260 showCustomMessage: function(message) {
261 this.showMessage_(PreviewArea.MessageId_.CUSTOM, message);
265 enterDocument: function() {
266 print_preview.Component.prototype.enterDocument.call(this);
268 this.openSystemDialogButton_,
270 this.onOpenSystemDialogButtonClick_.bind(this));
273 this.printTicketStore_,
274 print_preview.PrintTicketStore.EventType.INITIALIZE,
275 this.onTicketChange_.bind(this));
277 this.printTicketStore_,
278 print_preview.PrintTicketStore.EventType.TICKET_CHANGE,
279 this.onTicketChange_.bind(this));
281 this.printTicketStore_,
282 print_preview.PrintTicketStore.EventType.CAPABILITIES_CHANGE,
283 this.onTicketChange_.bind(this));
285 this.printTicketStore_,
286 print_preview.PrintTicketStore.EventType.DOCUMENT_CHANGE,
287 this.onTicketChange_.bind(this));
290 this.printTicketStore_.color,
291 print_preview.ticket_items.TicketItem.EventType.CHANGE,
292 this.onTicketChange_.bind(this));
294 this.printTicketStore_.cssBackground,
295 print_preview.ticket_items.TicketItem.EventType.CHANGE,
296 this.onTicketChange_.bind(this));
298 this.printTicketStore_.customMargins,
299 print_preview.ticket_items.TicketItem.EventType.CHANGE,
300 this.onTicketChange_.bind(this));
302 this.printTicketStore_.fitToPage,
303 print_preview.ticket_items.TicketItem.EventType.CHANGE,
304 this.onTicketChange_.bind(this));
306 this.printTicketStore_.headerFooter,
307 print_preview.ticket_items.TicketItem.EventType.CHANGE,
308 this.onTicketChange_.bind(this));
310 this.printTicketStore_.landscape,
311 print_preview.ticket_items.TicketItem.EventType.CHANGE,
312 this.onTicketChange_.bind(this));
314 this.printTicketStore_.marginsType,
315 print_preview.ticket_items.TicketItem.EventType.CHANGE,
316 this.onTicketChange_.bind(this));
318 this.printTicketStore_.pageRange,
319 print_preview.ticket_items.TicketItem.EventType.CHANGE,
320 this.onTicketChange_.bind(this));
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_,
333 this.previewGenerator_,
334 print_preview.PreviewGenerator.EventType.PREVIEW_START,
335 this.onPreviewStart_.bind(this));
337 this.previewGenerator_,
338 print_preview.PreviewGenerator.EventType.PAGE_READY,
339 this.onPagePreviewReady_.bind(this));
341 this.previewGenerator_,
342 print_preview.PreviewGenerator.EventType.FAIL,
343 this.onPreviewGenerationFail_.bind(this));
345 this.previewGenerator_,
346 print_preview.PreviewGenerator.EventType.DOCUMENT_READY,
347 this.onDocumentReady_.bind(this));
349 this.showCustomMessage(localStrings.getString('noPlugin'));
354 exitDocument: function() {
355 print_preview.Component.prototype.exitDocument.call(this);
356 if (this.previewGenerator_) {
357 this.previewGenerator_.removeEventListeners();
359 this.overlayEl_ = null;
360 this.openSystemDialogButton_ = null;
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];
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
379 checkPluginCompatibility_: function() {
380 var compatObj = this.getElement().getElementsByClassName(
381 PreviewArea.Classes_.COMPATIBILITY_OBJECT)[0];
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 &&
402 compatObj.parentElement.removeChild(compatObj);
407 * Shows a given message on the overlay.
408 * @param {!print_preview.PreviewArea.MessageId_} messageId ID of the
410 * @param {string=} opt_message Optional message to show that can be used
411 * by some message IDs.
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);
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');
434 var messageEl = this.getElement().getElementsByClassName(
435 PreviewArea.MessageIdClassMap_[messageId])[0];
436 setIsVisible(messageEl, true);
439 this.overlayEl_.classList.remove(PreviewArea.Classes_.INVISIBLE);
443 * Hides the message overlay.
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');
455 * Creates a preview plugin and adds it to the DOM.
456 * @param {string} srcUrl Initial URL of the plugin.
459 createPlugin_: function(srcUrl) {
461 console.warn('Pdf preview plugin already created');
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
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());
491 * Dispatches a PREVIEW_GENERATION_DONE event if all conditions are met.
494 dispatchPreviewGenerationDoneIfReady_: function() {
495 if (this.isDocumentReady_ && this.isPluginReloaded_) {
496 cr.dispatchSimpleEvent(
497 this, PreviewArea.EventType.PREVIEW_GENERATION_DONE);
498 this.marginControlContainer_.showMarginControlsIfNeeded();
503 * Called when the open-system-dialog button is clicked. Disables the
504 * button, shows the throbber, and dispatches the OPEN_SYSTEM_DIALOG_CLICK
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);
518 * Called when the print ticket changes. Updates the preview.
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_);
529 this.marginControlContainer_.showMarginControlsIfNeeded();
534 * Called when the preview generator begins loading the preview.
535 * @param {Event} Contains the URL to initialize the plugin to.
538 onPreviewStart_: function(event) {
539 this.isDocumentReady_ = false;
540 this.isPluginReloaded_ = false;
542 this.createPlugin_(event.previewUrl);
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);
553 * Called when a page preview has been generated. Updates the plugin with
555 * @param {Event} event Contains information about the page preview.
558 onPagePreviewReady_: function(event) {
559 this.plugin_.loadPreviewPage(event.previewUrl, event.previewIndex);
563 * Called when the preview generation is complete and the document is ready
567 onDocumentReady_: function(event) {
568 this.isDocumentReady_ = true;
569 this.dispatchPreviewGenerationDoneIfReady_();
573 * Called when the generation of a preview fails. Shows an error message.
576 onPreviewGenerationFail_: function() {
577 if (this.loadingTimeout_) {
578 clearTimeout(this.loadingTimeout_);
579 this.loadingTimeout_ = null;
581 this.showMessage_(PreviewArea.MessageId_.PREVIEW_FAILED);
582 cr.dispatchSimpleEvent(
583 this, PreviewArea.EventType.PREVIEW_GENERATION_FAIL);
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
592 onPluginLoad_: function() {
593 if (this.loadingTimeout_) {
594 clearTimeout(this.loadingTimeout_);
595 this.loadingTimeout_ = null;
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);
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);
610 this.plugin_.fitToHeight();
613 this.isPluginReloaded_ = true;
614 this.dispatchPreviewGenerationDoneIfReady_();
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.
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());
629 var pageLocationNormalizedStr = this.plugin_.getPageLocationNormalized();
630 if (!pageLocationNormalizedStr) {
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()));
653 PreviewArea: PreviewArea