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_.distillPage,
354 print_preview.ticket_items.TicketItem.EventType.CHANGE,
355 this.onTicketChange_.bind(this));
357 this.printTicketStore_.headerFooter,
358 print_preview.ticket_items.TicketItem.EventType.CHANGE,
359 this.onTicketChange_.bind(this));
361 this.printTicketStore_.landscape,
362 print_preview.ticket_items.TicketItem.EventType.CHANGE,
363 this.onTicketChange_.bind(this));
365 this.printTicketStore_.marginsType,
366 print_preview.ticket_items.TicketItem.EventType.CHANGE,
367 this.onTicketChange_.bind(this));
369 this.printTicketStore_.pageRange,
370 print_preview.ticket_items.TicketItem.EventType.CHANGE,
371 this.onTicketChange_.bind(this));
373 this.printTicketStore_.selectionOnly,
374 print_preview.ticket_items.TicketItem.EventType.CHANGE,
375 this.onTicketChange_.bind(this));
377 if (this.checkPluginCompatibility_()) {
378 this.previewGenerator_ = new print_preview.PreviewGenerator(
379 this.destinationStore_,
380 this.printTicketStore_,
384 this.previewGenerator_,
385 print_preview.PreviewGenerator.EventType.PREVIEW_START,
386 this.onPreviewStart_.bind(this));
388 this.previewGenerator_,
389 print_preview.PreviewGenerator.EventType.PAGE_READY,
390 this.onPagePreviewReady_.bind(this));
392 this.previewGenerator_,
393 print_preview.PreviewGenerator.EventType.FAIL,
394 this.onPreviewGenerationFail_.bind(this));
396 this.previewGenerator_,
397 print_preview.PreviewGenerator.EventType.DOCUMENT_READY,
398 this.onDocumentReady_.bind(this));
400 this.showCustomMessage(loadTimeData.getString('noPlugin'));
405 exitDocument: function() {
406 print_preview.Component.prototype.exitDocument.call(this);
407 if (this.previewGenerator_) {
408 this.previewGenerator_.removeEventListeners();
410 this.overlayEl_ = null;
411 this.openSystemDialogButton_ = null;
415 decorateInternal: function() {
416 this.marginControlContainer_.decorate(this.getElement());
417 this.overlayEl_ = this.getElement().getElementsByClassName(
418 PreviewArea.Classes_.OVERLAY)[0];
419 this.openSystemDialogButton_ = this.getElement().getElementsByClassName(
420 PreviewArea.Classes_.OPEN_SYSTEM_DIALOG_BUTTON)[0];
424 * Checks to see if a suitable plugin for rendering the preview exists. If
425 * one does not exist, then an error message will be displayed.
426 * @return {boolean} true if Chromium has a plugin for rendering the
430 checkPluginCompatibility_: function() {
431 // TODO(raymes): It's harder to test compatibility of the out of process
432 // plugin because it's asynchronous. We could do a better job at some
434 var oopCompatObj = this.getElement().getElementsByClassName(
435 PreviewArea.Classes_.OUT_OF_PROCESS_COMPATIBILITY_OBJECT)[0];
436 var isOOPCompatible = oopCompatObj.postMessage;
437 oopCompatObj.parentElement.removeChild(oopCompatObj);
439 return isOOPCompatible;
443 * Shows a given message on the overlay.
444 * @param {!print_preview.PreviewArea.MessageId_} messageId ID of the
446 * @param {string=} opt_message Optional message to show that can be used
447 * by some message IDs.
450 showMessage_: function(messageId, opt_message) {
451 // Hide all messages.
452 var messageEls = this.getElement().getElementsByClassName(
453 PreviewArea.Classes_.MESSAGE);
454 for (var i = 0, messageEl; messageEl = messageEls[i]; i++) {
455 setIsVisible(messageEl, false);
457 // Disable jumping animation to conserve cycles.
458 var jumpingDotsEl = this.getElement().querySelector(
459 '.preview-area-loading-message-jumping-dots');
460 jumpingDotsEl.classList.remove('jumping-dots');
462 // Show specific message.
463 if (messageId == PreviewArea.MessageId_.CUSTOM) {
464 var customMessageTextEl = this.getElement().getElementsByClassName(
465 PreviewArea.Classes_.CUSTOM_MESSAGE_TEXT)[0];
466 customMessageTextEl.textContent = opt_message;
467 } else if (messageId == PreviewArea.MessageId_.LOADING) {
468 jumpingDotsEl.classList.add('jumping-dots');
470 var messageEl = this.getElement().getElementsByClassName(
471 PreviewArea.MessageIdClassMap_[messageId])[0];
472 setIsVisible(messageEl, true);
474 this.setOverlayVisible_(true);
478 * Set the visibility of the message overlay.
479 * @param {boolean} visible Whether to make the overlay visible or not
482 setOverlayVisible_: function(visible) {
483 this.overlayEl_.classList.toggle(
484 PreviewArea.Classes_.INVISIBLE,
486 this.overlayEl_.setAttribute('aria-hidden', !visible);
488 // Hide/show all controls that will overlap when the overlay is visible.
489 var marginControls = this.getElement().getElementsByClassName(
490 PreviewArea.Classes_.MARGIN_CONTROL);
491 for (var i = 0; i < marginControls.length; ++i) {
492 marginControls[i].setAttribute('aria-hidden', visible);
494 var previewAreaControls = this.getElement().getElementsByClassName(
495 PreviewArea.Classes_.PREVIEW_AREA);
496 for (var i = 0; i < previewAreaControls.length; ++i) {
497 previewAreaControls[i].setAttribute('aria-hidden', visible);
501 // Disable jumping animation to conserve cycles.
502 var jumpingDotsEl = this.getElement().querySelector(
503 '.preview-area-loading-message-jumping-dots');
504 jumpingDotsEl.classList.remove('jumping-dots');
509 * Creates a preview plugin and adds it to the DOM.
510 * @param {string} srcUrl Initial URL of the plugin.
513 createPlugin_: function(srcUrl) {
515 console.warn('Pdf preview plugin already created');
519 this.plugin_ = /** @type {print_preview.PDFPlugin} */(
520 PDFCreateOutOfProcessPlugin(srcUrl));
521 this.plugin_.setKeyEventCallback(this.keyEventCallback_);
523 this.plugin_.setAttribute('class', 'preview-area-plugin');
524 this.plugin_.setAttribute('aria-live', 'polite');
525 this.plugin_.setAttribute('aria-atomic', 'true');
526 // NOTE: The plugin's 'id' field must be set to 'pdf-viewer' since
527 // chrome/renderer/printing/print_web_view_helper.cc actually references
529 this.plugin_.setAttribute('id', 'pdf-viewer');
530 this.getChildElement('.preview-area-plugin-wrapper').
531 appendChild(/** @type {Node} */(this.plugin_));
535 this.printTicketStore_.pageRange.getPageNumberSet().asArray();
536 var grayscale = !this.printTicketStore_.color.getValue();
537 this.plugin_.setLoadCallback(this.onPluginLoad_.bind(this));
538 this.plugin_.setViewportChangedCallback(
539 this.onPreviewVisualStateChange_.bind(this));
540 this.plugin_.resetPrintPreviewMode(srcUrl, grayscale, pageNumbers,
541 this.documentInfo_.isModifiable);
545 * Dispatches a PREVIEW_GENERATION_DONE event if all conditions are met.
548 dispatchPreviewGenerationDoneIfReady_: function() {
549 if (this.isDocumentReady_ && this.isPluginReloaded_) {
550 cr.dispatchSimpleEvent(
551 this, PreviewArea.EventType.PREVIEW_GENERATION_DONE);
552 this.marginControlContainer_.showMarginControlsIfNeeded();
557 * Called when the open-system-dialog button is clicked. Disables the
558 * button, shows the throbber, and dispatches the OPEN_SYSTEM_DIALOG_CLICK
562 onOpenSystemDialogButtonClick_: function() {
563 this.openSystemDialogButton_.disabled = true;
564 var openSystemDialogThrobber = this.getElement().getElementsByClassName(
565 PreviewArea.Classes_.OPEN_SYSTEM_DIALOG_BUTTON_THROBBER)[0];
566 setIsVisible(openSystemDialogThrobber, true);
567 cr.dispatchSimpleEvent(
568 this, PreviewArea.EventType.OPEN_SYSTEM_DIALOG_CLICK);
572 * Called when the print ticket changes. Updates the preview.
575 onTicketChange_: function() {
576 if (this.previewGenerator_ && this.previewGenerator_.requestPreview()) {
577 cr.dispatchSimpleEvent(
578 this, PreviewArea.EventType.PREVIEW_GENERATION_IN_PROGRESS);
579 if (this.loadingTimeout_ == null) {
580 this.loadingTimeout_ = setTimeout(
581 this.showMessage_.bind(this, PreviewArea.MessageId_.LOADING),
582 PreviewArea.LOADING_TIMEOUT_);
585 this.marginControlContainer_.showMarginControlsIfNeeded();
590 * Called when the preview generator begins loading the preview.
591 * @param {Event} event Contains the URL to initialize the plugin to.
594 onPreviewStart_: function(event) {
595 this.isDocumentReady_ = false;
596 this.isPluginReloaded_ = false;
598 this.createPlugin_(event.previewUrl);
600 var grayscale = !this.printTicketStore_.color.getValue();
602 this.printTicketStore_.pageRange.getPageNumberSet().asArray();
603 var url = event.previewUrl;
604 this.plugin_.resetPrintPreviewMode(url, grayscale, pageNumbers,
605 this.documentInfo_.isModifiable);
607 cr.dispatchSimpleEvent(
608 this, PreviewArea.EventType.PREVIEW_GENERATION_IN_PROGRESS);
612 * Called when a page preview has been generated. Updates the plugin with
614 * @param {Event} event Contains information about the page preview.
617 onPagePreviewReady_: function(event) {
618 this.plugin_.loadPreviewPage(event.previewUrl, event.previewIndex);
622 * Called when the preview generation is complete and the document is ready
626 onDocumentReady_: function(event) {
627 this.isDocumentReady_ = true;
628 this.dispatchPreviewGenerationDoneIfReady_();
632 * Called when the generation of a preview fails. Shows an error message.
635 onPreviewGenerationFail_: function() {
636 if (this.loadingTimeout_) {
637 clearTimeout(this.loadingTimeout_);
638 this.loadingTimeout_ = null;
640 this.showMessage_(PreviewArea.MessageId_.PREVIEW_FAILED);
641 cr.dispatchSimpleEvent(
642 this, PreviewArea.EventType.PREVIEW_GENERATION_FAIL);
646 * Called when the plugin loads. This is a consequence of calling
647 * plugin.reload(). Certain plugin state can only be set after the plugin
651 onPluginLoad_: function() {
652 if (this.loadingTimeout_) {
653 clearTimeout(this.loadingTimeout_);
654 this.loadingTimeout_ = null;
657 this.setOverlayVisible_(false);
658 this.isPluginReloaded_ = true;
659 this.dispatchPreviewGenerationDoneIfReady_();
663 * Called when the preview plugin's visual state has changed. This is a
664 * consequence of scrolling or zooming the plugin. Updates the custom
665 * margins component if shown.
668 onPreviewVisualStateChange_: function(pageX,
673 this.marginControlContainer_.updateTranslationTransform(
674 new print_preview.Coordinate2d(pageX, pageY));
675 this.marginControlContainer_.updateScaleTransform(
676 pageWidth / this.documentInfo_.pageSize.width);
677 this.marginControlContainer_.updateClippingMask(
678 new print_preview.Size(viewportWidth, viewportHeight));
682 * Called when dragging margins starts or stops.
683 * @param {boolean} isDragging True if the margin is currently being dragged
684 * and false otherwise.
686 onMarginDragChanged_: function(isDragging) {
690 // When hovering over the plugin (which may be in a separate iframe)
691 // pointer events will be sent to the frame. When dragging the margins,
692 // we don't want this to happen as it can cause the margin to stop
694 this.plugin_.style.pointerEvents = isDragging ? 'none' : 'auto';
700 PreviewArea: PreviewArea