blob: 60384e5db6e1fe6a49a7d6fd4bac41e6c537691f [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
<include src="../../../../ui/webui/resources/js/util.js">
<include src="pdf_scripting_api.js">
<include src="viewport.js">
/**
* @return {number} Width of a scrollbar in pixels
*/
function getScrollbarWidth() {
var div = document.createElement('div');
div.style.visibility = 'hidden';
div.style.overflow = 'scroll';
div.style.width = '50px';
div.style.height = '50px';
div.style.position = 'absolute';
document.body.appendChild(div);
var result = div.offsetWidth - div.clientWidth;
div.parentNode.removeChild(div);
return result;
}
/**
* The minimum number of pixels to offset the toolbar by from the bottom and
* right side of the screen.
*/
PDFViewer.MIN_TOOLBAR_OFFSET = 15;
/**
* Creates a new PDFViewer. There should only be one of these objects per
* document.
*/
function PDFViewer() {
this.loaded = false;
// The sizer element is placed behind the plugin element to cause scrollbars
// to be displayed in the window. It is sized according to the document size
// of the pdf and zoom level.
this.sizer_ = $('sizer');
this.toolbar_ = $('toolbar');
this.pageIndicator_ = $('page-indicator');
this.progressBar_ = $('progress-bar');
this.passwordScreen_ = $('password-screen');
this.passwordScreen_.addEventListener('password-submitted',
this.onPasswordSubmitted_.bind(this));
this.errorScreen_ = $('error-screen');
// Create the viewport.
this.viewport_ = new Viewport(window,
this.sizer_,
this.viewportChangedCallback_.bind(this),
getScrollbarWidth());
// Create the plugin object dynamically so we can set its src. The plugin
// element is sized to fill the entire window and is set to be fixed
// positioning, acting as a viewport. The plugin renders into this viewport
// according to the scroll position of the window.
this.plugin_ = document.createElement('object');
// NOTE: The plugin's 'id' field must be set to 'plugin' since
// chrome/renderer/printing/print_web_view_helper.cc actually references it.
this.plugin_.id = 'plugin';
this.plugin_.type = 'application/x-google-chrome-pdf';
this.plugin_.addEventListener('message', this.handlePluginMessage_.bind(this),
false);
// Handle scripting messages from outside the extension that wish to interact
// with it. We also send a message indicating that extension has loaded and
// is ready to receive messages.
window.addEventListener('message', this.handleScriptingMessage_.bind(this),
false);
this.sendScriptingMessage_({type: 'readyToReceive'});
// If the viewer is started from a MIME type request, there will be a
// background page and stream details object with the details of the request.
// Otherwise, we take the query string of the URL to indicate the URL of the
// PDF to load. This is used for print preview in particular.
if (chrome.extension.getBackgroundPage &&
chrome.extension.getBackgroundPage()) {
this.streamDetails =
chrome.extension.getBackgroundPage().popStreamDetails();
}
if (!this.streamDetails) {
// The URL of this page will be of the form
// "chrome-extension://<extension id>?<pdf url>". We pull out the <pdf url>
// part here.
var url = window.location.search.substring(1);
this.streamDetails = {
streamUrl: url,
originalUrl: url,
responseHeaders: ''
};
}
this.plugin_.setAttribute('src', this.streamDetails.originalUrl);
this.plugin_.setAttribute('stream-url', this.streamDetails.streamUrl);
var headers = '';
for (var header in this.streamDetails.responseHeaders) {
headers += header + ': ' +
this.streamDetails.responseHeaders[header] + '\n';
}
this.plugin_.setAttribute('headers', headers);
if (window.top == window)
this.plugin_.setAttribute('full-frame', '');
document.body.appendChild(this.plugin_);
// Setup the button event listeners.
$('fit-to-width-button').addEventListener('click',
this.viewport_.fitToWidth.bind(this.viewport_));
$('fit-to-page-button').addEventListener('click',
this.viewport_.fitToPage.bind(this.viewport_));
$('zoom-in-button').addEventListener('click',
this.viewport_.zoomIn.bind(this.viewport_));
$('zoom-out-button').addEventListener('click',
this.viewport_.zoomOut.bind(this.viewport_));
$('save-button-link').href = this.streamDetails.originalUrl;
$('print-button').addEventListener('click', this.print_.bind(this));
// Setup the keyboard event listener.
document.onkeydown = this.handleKeyEvent_.bind(this);
}
PDFViewer.prototype = {
/**
* @private
* Handle key events. These may come from the user directly or via the
* scripting API.
* @param {KeyboardEvent} e the event to handle.
*/
handleKeyEvent_: function(e) {
var position = this.viewport_.position;
// Certain scroll events may be sent from outside of the extension.
var fromScriptingAPI = e.type == 'scriptingKeypress';
switch (e.keyCode) {
case 33: // Page up key.
// Go to the previous page if we are fit-to-page.
if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
// Since we do the movement of the page.
e.preventDefault();
} else if (fromScriptingAPI) {
position.y -= this.viewport.size.height;
this.viewport.position = position;
}
return;
case 34: // Page down key.
// Go to the next page if we are fit-to-page.
if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
// Since we do the movement of the page.
e.preventDefault();
} else if (fromScriptingAPI) {
position.y += this.viewport.size.height;
this.viewport.position = position;
}
return;
case 37: // Left arrow key.
// Go to the previous page if there are no horizontal scrollbars.
if (!this.viewport_.documentHasScrollbars().x) {
this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1);
// Since we do the movement of the page.
e.preventDefault();
} else if (fromScriptingAPI) {
position.x -= Viewport.SCROLL_INCREMENT;
this.viewport.position = position;
}
return;
case 38: // Up arrow key.
if (fromScriptingAPI) {
position.y -= Viewport.SCROLL_INCREMENT;
this.viewport.position = position;
}
return;
case 39: // Right arrow key.
// Go to the next page if there are no horizontal scrollbars.
if (!this.viewport_.documentHasScrollbars().x) {
this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1);
// Since we do the movement of the page.
e.preventDefault();
} else if (fromScriptingAPI) {
position.x += Viewport.SCROLL_INCREMENT;
this.viewport.position = position;
}
return;
case 40: // Down arrow key.
if (fromScriptingAPI) {
position.y += Viewport.SCROLL_INCREMENT;
this.viewport.position = position;
}
return;
case 187: // +/= key.
case 107: // Numpad + key.
if (e.ctrlKey || e.metaKey) {
this.viewport_.zoomIn();
// Since we do the zooming of the page.
e.preventDefault();
}
return;
case 189: // -/_ key.
case 109: // Numpad - key.
if (e.ctrlKey || e.metaKey) {
this.viewport_.zoomOut();
// Since we do the zooming of the page.
e.preventDefault();
}
return;
case 83: // s key.
if (e.ctrlKey || e.metaKey) {
// Simulate a click on the button so that the <a download ...>
// attribute is used.
$('save-button-link').click();
// Since we do the saving of the page.
e.preventDefault();
}
return;
case 80: // p key.
if (e.ctrlKey || e.metaKey) {
this.print_();
// Since we do the printing of the page.
e.preventDefault();
}
return;
}
},
/**
* @private
* Notify the plugin to print.
*/
print_: function() {
this.plugin_.postMessage({
type: 'print',
});
},
/**
* @private
* Update the loading progress of the document in response to a progress
* message being received from the plugin.
* @param {number} progress the progress as a percentage.
*/
updateProgress_: function(progress) {
this.progressBar_.progress = progress;
if (progress == -1) {
// Document load failed.
this.errorScreen_.style.visibility = 'visible';
this.sizer_.style.display = 'none';
this.toolbar_.style.visibility = 'hidden';
if (this.passwordScreen_.active) {
this.passwordScreen_.deny();
this.passwordScreen_.active = false;
}
} else if (progress == 100) {
// Document load complete.
this.loaded = true;
var loadEvent = new Event('pdfload');
window.dispatchEvent(loadEvent);
this.sendScriptingMessage_({
type: 'documentLoaded'
});
if (this.lastViewportPosition_)
this.viewport_.position = this.lastViewportPosition_;
}
},
/**
* @private
* An event handler for handling password-submitted events. These are fired
* when an event is entered into the password screen.
* @param {Object} event a password-submitted event.
*/
onPasswordSubmitted_: function(event) {
this.plugin_.postMessage({
type: 'getPasswordComplete',
password: event.detail.password
});
},
/**
* @private
* An event handler for handling message events received from the plugin.
* @param {MessageObject} message a message event.
*/
handlePluginMessage_: function(message) {
switch (message.data.type.toString()) {
case 'documentDimensions':
this.documentDimensions_ = message.data;
this.viewport_.setDocumentDimensions(this.documentDimensions_);
this.toolbar_.style.visibility = 'visible';
// If we received the document dimensions, the password was good so we
// can dismiss the password screen.
if (this.passwordScreen_.active)
this.passwordScreen_.accept();
this.pageIndicator_.initialFadeIn();
this.toolbar_.initialFadeIn();
break;
case 'email':
var href = 'mailto:' + message.data.to + '?cc=' + message.data.cc +
'&bcc=' + message.data.bcc + '&subject=' + message.data.subject +
'&body=' + message.data.body;
var w = window.open(href, '_blank', 'width=1,height=1');
if (w)
w.close();
break;
case 'getAccessibilityJSONReply':
this.sendScriptingMessage_(message.data);
break;
case 'getPassword':
// If the password screen isn't up, put it up. Otherwise we're
// responding to an incorrect password so deny it.
if (!this.passwordScreen_.active)
this.passwordScreen_.active = true;
else
this.passwordScreen_.deny();
break;
case 'goToPage':
this.viewport_.goToPage(message.data.page);
break;
case 'loadProgress':
this.updateProgress_(message.data.progress);
break;
case 'navigate':
if (message.data.newTab)
window.open(message.data.url);
else
window.location.href = message.data.url;
break;
case 'setScrollPosition':
var position = this.viewport_.position;
if (message.data.x != undefined)
position.x = message.data.x;
if (message.data.y != undefined)
position.y = message.data.y;
this.viewport_.position = position;
break;
case 'setTranslatedStrings':
this.passwordScreen_.text = message.data.getPasswordString;
this.progressBar_.text = message.data.loadingString;
this.errorScreen_.text = message.data.loadFailedString;
break;
case 'cancelStreamUrl':
chrome.streamsPrivate.abort(this.streamDetails.streamUrl);
break;
}
},
/**
* @private
* A callback that's called when the viewport changes.
*/
viewportChangedCallback_: function() {
if (!this.documentDimensions_)
return;
// Update the buttons selected.
$('fit-to-page-button').classList.remove('polymer-selected');
$('fit-to-width-button').classList.remove('polymer-selected');
if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) {
$('fit-to-page-button').classList.add('polymer-selected');
} else if (this.viewport_.fittingType ==
Viewport.FittingType.FIT_TO_WIDTH) {
$('fit-to-width-button').classList.add('polymer-selected');
}
var hasScrollbars = this.viewport_.documentHasScrollbars();
var scrollbarWidth = this.viewport_.scrollbarWidth;
// Offset the toolbar position so that it doesn't move if scrollbars appear.
var toolbarRight = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
var toolbarBottom = Math.max(PDFViewer.MIN_TOOLBAR_OFFSET, scrollbarWidth);
if (hasScrollbars.vertical)
toolbarRight -= scrollbarWidth;
if (hasScrollbars.horizontal)
toolbarBottom -= scrollbarWidth;
this.toolbar_.style.right = toolbarRight + 'px';
this.toolbar_.style.bottom = toolbarBottom + 'px';
// Update the page indicator.
var visiblePage = this.viewport_.getMostVisiblePage();
this.pageIndicator_.index = visiblePage;
if (this.documentDimensions_.pageDimensions.length > 1 &&
hasScrollbars.vertical) {
this.pageIndicator_.style.visibility = 'visible';
} else {
this.pageIndicator_.style.visibility = 'hidden';
}
var position = this.viewport_.position;
var zoom = this.viewport_.zoom;
// Notify the plugin of the viewport change.
this.plugin_.postMessage({
type: 'viewport',
zoom: zoom,
xOffset: position.x,
yOffset: position.y
});
var visiblePageDimensions = this.viewport_.getPageScreenRect(visiblePage);
var size = this.viewport_.size;
this.sendScriptingMessage_({
type: 'viewport',
pageX: visiblePageDimensions.x,
pageY: visiblePageDimensions.y,
pageWidth: visiblePageDimensions.width,
viewportWidth: size.width,
viewportHeight: size.height,
});
},
/**
* @private
* Handle a scripting message from outside the extension (typically sent by
* PDFScriptingAPI in a page containing the extension) to interact with the
* plugin.
* @param {MessageObject} message the message to handle.
*/
handleScriptingMessage_: function(message) {
switch (message.data.type.toString()) {
case 'getAccessibilityJSON':
case 'loadPreviewPage':
this.plugin_.postMessage(message.data);
break;
case 'resetPrintPreviewMode':
if (!this.inPrintPreviewMode_) {
this.inPrintPreviewMode_ = true;
this.viewport_.fitToPage();
}
// Stash the scroll location so that it can be restored when the new
// document is loaded.
this.lastViewportPosition_ = this.viewport_.position;
// TODO(raymes): Disable these properly in the plugin.
var printButton = $('print-button');
if (printButton)
printButton.parentNode.removeChild(printButton);
var saveButton = $('save-button');
if (saveButton)
saveButton.parentNode.removeChild(saveButton);
this.pageIndicator_.pageLabels = message.data.pageNumbers;
this.plugin_.postMessage({
type: 'resetPrintPreviewMode',
url: message.data.url,
grayscale: message.data.grayscale,
// If the PDF isn't modifiable we send 0 as the page count so that no
// blank placeholder pages get appended to the PDF.
pageCount: (message.data.modifiable ?
message.data.pageNumbers.length : 0)
});
break;
case 'sendKeyEvent':
var e = document.createEvent('Event');
e.initEvent('scriptingKeypress');
e.keyCode = message.data.keyCode;
this.handleKeyEvent_(e);
break;
}
},
/**
* @private
* Send a scripting message outside the extension (typically to
* PDFScriptingAPI in a page containing the extension).
* @param {Object} message the message to send.
*/
sendScriptingMessage_: function(message) {
window.parent.postMessage(message, '*');
},
/**
* @type {Viewport} the viewport of the PDF viewer.
*/
get viewport() {
return this.viewport_;
}
};
var viewer = new PDFViewer();