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