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