*   *Kk\o{wi@Ѣj?iRsj  W  u &/* Copyright 2015 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. */ body { background-color: rgb(82, 86, 89); font-family: 'Roboto', 'Noto', sans-serif; margin: 0; } viewer-page-indicator { visibility: hidden; z-index: 2; } viewer-pdf-toolbar { position: fixed; width: 100%; z-index: 4; } #plugin { height: 100%; position: fixed; width: 100%; z-index: 1; } #sizer { position: absolute; z-index: 0; } @media(max-height: 250px) { viewer-pdf-toolbar { display: none; } } @media(max-height: 200px) { viewer-zoom-toolbar { display: none; } } @media(max-width: 300px) { viewer-zoom-toolbar { display: none; } }
// Copyright 2014 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'; /** * Global PDFViewer object, accessible for testing. * @type Object */ var viewer; (function() { /** * Stores any pending messages received which should be passed to the * PDFViewer when it is created. * @type Array */ var pendingMessages = []; /** * Handles events that are received prior to the PDFViewer being created. * @param {Object} message A message event received. */ function handleScriptingMessage(message) { pendingMessages.push(message); } /** * Initialize the global PDFViewer and pass any outstanding messages to it. * @param {Object} browserApi An object providing an API to the browser. */ function initViewer(browserApi) { // PDFViewer will handle any messages after it is created. window.removeEventListener('message', handleScriptingMessage, false); viewer = new PDFViewer(browserApi); while (pendingMessages.length > 0) viewer.handleScriptingMessage(pendingMessages.shift()); } /** * Entrypoint for starting the PDF viewer. This function obtains the browser * API for the PDF and constructs a PDFViewer object with it. */ cr.sendWithPromise('initialize').then(function(opts) { // Set up an event listener to catch scripting messages which are sent prior // to the PDFViewer being created. window.addEventListener('message', handleScriptingMessage, false); createBrowserApi(opts).then(initViewer); }); })() // 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'; /** * An enum containing a value specifying whether the PDF is currently loading, * has finished loading or failed to load. */ var LoadState = { LOADING: 'loading', SUCCESS: 'success', FAILED: 'failed' }; /** * @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; } /** * Return the filename component of a URL, percent decoded if possible. * @param {string} url The URL to get the filename from. * @return {string} The filename component. */ function getFilenameFromURL(url) { // Ignore the query and fragment. var mainUrl = url.split(/#|\?/)[0]; var components = mainUrl.split(/\/|\\/); var filename = components[components.length - 1]; try { return decodeURIComponent(filename); } catch (e) { if (e instanceof URIError) return filename; throw e; } } /** * Whether keydown events should currently be ignored. Events are ignored when * an editable element has focus, to allow for proper editing controls. * @param {HTMLElement} activeElement The currently selected DOM node. * @return {boolean} True if keydown events should be ignored. */ function shouldIgnoreKeyEvents(activeElement) { while (activeElement.shadowRoot != null && activeElement.shadowRoot.activeElement != null) { activeElement = activeElement.shadowRoot.activeElement; } return (activeElement.isContentEditable || activeElement.tagName == 'INPUT' || activeElement.tagName == 'TEXTAREA'); } /** * The minimum number of pixels to offset the toolbar by from the bottom and * right side of the screen. */ PDFViewer.MIN_TOOLBAR_OFFSET = 15; /** * The height of the toolbar along the top of the page. The document will be * shifted down by this much in the viewport. */ PDFViewer.MATERIAL_TOOLBAR_HEIGHT = 56; /** * Minimum height for the material toolbar to show (px). Should match the media * query in index-material.css. If the window is smaller than this at load, * leave no space for the toolbar. */ PDFViewer.TOOLBAR_WINDOW_MIN_HEIGHT = 250; /** * The light-gray background color used for print preview. */ PDFViewer.LIGHT_BACKGROUND_COLOR = '0xFFCCCCCC'; /** * The dark-gray background color used for the regular viewer. */ PDFViewer.DARK_BACKGROUND_COLOR = '0xFF525659'; /** * Creates a new PDFViewer. There should only be one of these objects per * document. * @constructor * @param {!BrowserApi} browserApi An object providing an API to the browser. */ function PDFViewer(browserApi) { this.browserApi_ = browserApi; this.originalUrl_ = this.browserApi_.getStreamInfo().originalUrl; this.loadState_ = LoadState.LOADING; this.parentWindow_ = null; this.parentOrigin_ = null; this.isFormFieldFocused_ = false; this.delayedScriptingMessages_ = []; this.isPrintPreview_ = location.origin === 'chrome://print'; // Parse open pdf parameters. this.paramsParser_ = new OpenPDFParamsParser(this.getNamedDestination_.bind(this)); var toolbarEnabled = this.paramsParser_.getUiUrlParams(this.originalUrl_).toolbar && !this.isPrintPreview_; var toolbarSpacerEnabled = this.paramsParser_.getUiUrlParams(this.originalUrl_).toolbarSpacer; // 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'); if (this.isPrintPreview_) this.pageIndicator_ = $('page-indicator'); this.passwordScreen_ = $('password-screen'); this.passwordScreen_.addEventListener('password-submitted', this.onPasswordSubmitted_.bind(this)); this.errorScreen_ = $('error-screen'); // Can only reload if we are in a normal tab. this.errorScreen_.reloadFn = function() { chrome.send('reload'); }; // Create the viewport. var shortWindow = window.innerHeight < PDFViewer.TOOLBAR_WINDOW_MIN_HEIGHT; var topToolbarHeight = (toolbarEnabled) ? PDFViewer.MATERIAL_TOOLBAR_HEIGHT : 0; topToolbarHeight = (toolbarSpacerEnabled) ? topToolbarHeight : 0 this.viewport_ = new Viewport(window, this.sizer_, this.viewportChanged_.bind(this), this.beforeZoom_.bind(this), this.afterZoom_.bind(this), getScrollbarWidth(), this.browserApi_.getDefaultZoom(), topToolbarHeight); // 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('embed'); // 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.plugin_.setAttribute('src', this.originalUrl_); this.plugin_.setAttribute('stream-url', this.browserApi_.getStreamInfo().streamUrl); var headers = ''; for (var header in this.browserApi_.getStreamInfo().responseHeaders) { headers += header + ': ' + this.browserApi_.getStreamInfo().responseHeaders[header] + '\n'; } this.plugin_.setAttribute('headers', headers); var backgroundColor = PDFViewer.DARK_BACKGROUND_COLOR; this.plugin_.setAttribute('background-color', backgroundColor); this.plugin_.setAttribute('top-toolbar-height', topToolbarHeight); if (this.browserApi_.getStreamInfo().embedded) { this.plugin_.setAttribute('top-level-url', this.browserApi_.getStreamInfo().tabUrl); } else { this.plugin_.setAttribute('full-frame', ''); } document.body.appendChild(this.plugin_); // Setup the button event listeners. this.zoomToolbar_ = $('zoom-toolbar'); this.zoomToolbar_.addEventListener('fit-to-width', this.viewport_.fitToWidth.bind(this.viewport_)); this.zoomToolbar_.addEventListener('fit-to-page', this.fitToPage_.bind(this)); this.zoomToolbar_.addEventListener('zoom-in', this.viewport_.zoomIn.bind(this.viewport_)); this.zoomToolbar_.addEventListener('zoom-out', this.viewport_.zoomOut.bind(this.viewport_)); this.gestureDetector_ = new GestureDetector(this.plugin_); this.gestureDetector_.addEventListener( 'pinchstart', this.viewport_.pinchZoomStart.bind(this.viewport_)); this.sentPinchEvent_ = false; this.gestureDetector_.addEventListener( 'pinchupdate', this.onPinchUpdate_.bind(this)); this.gestureDetector_.addEventListener( 'pinchend', this.onPinchEnd_.bind(this)); if (toolbarEnabled) { this.toolbar_ = $('toolbar'); this.toolbar_.hidden = false; this.toolbar_.addEventListener('save', this.save_.bind(this)); this.toolbar_.addEventListener('print', this.print_.bind(this)); this.toolbar_.addEventListener('rotate-right', this.rotateClockwise_.bind(this)); // Must attach to mouseup on the plugin element, since it eats mousedown // and click events. this.plugin_.addEventListener('mouseup', this.toolbar_.hideDropdowns.bind(this.toolbar_)); this.toolbar_.docTitle = getFilenameFromURL(this.originalUrl_); } document.body.addEventListener('change-page', function(e) { this.viewport_.goToPage(e.detail.page); }.bind(this)); document.body.addEventListener('navigate', function(e) { var disposition = e.detail.newtab ? Navigator.WindowOpenDisposition.NEW_BACKGROUND_TAB : Navigator.WindowOpenDisposition.CURRENT_TAB; this.navigator_.navigate(e.detail.uri, disposition); }.bind(this)); this.toolbarManager_ = new ToolbarManager(window, this.toolbar_, this.zoomToolbar_); // Set up the ZoomManager. this.zoomManager_ = new ZoomManager( this.viewport_, this.browserApi_.setZoom.bind(this.browserApi_), this.browserApi_.getInitialZoom()); this.browserApi_.addZoomEventListener( this.zoomManager_.onBrowserZoomChange.bind(this.zoomManager_)); // Setup the keyboard event listener. document.addEventListener('keydown', this.handleKeyEvent_.bind(this)); document.addEventListener('mousemove', this.handleMouseEvent_.bind(this)); document.addEventListener('mouseout', this.handleMouseEvent_.bind(this)); document.addEventListener('wheel', this.handleMouseEvent_.bind(this)); var tabId = this.browserApi_.getStreamInfo().tabId; this.navigator_ = new Navigator( this.originalUrl_, this.viewport_, this.paramsParser_, new NavigatorDelegate(tabId)); this.viewportScroller_ = new ViewportScroller(this.viewport_, this.plugin_, window); // Request translated strings. cr.sendWithPromise('getStrings').then(this.handleStrings_.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.fromScriptingAPI; if (shouldIgnoreKeyEvents(document.activeElement) || e.defaultPrevented) return; this.toolbarManager_.hideToolbarsAfterTimeout(e); var pageUpHandler = function() { // Go to the previous page if we are fit-to-page. if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE || (e.ctrlKey || e.metaKey)) { 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; } }.bind(this); var pageDownHandler = function() { // Go to the next page if we are fit-to-page. if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE || (e.ctrlKey || e.metaKey)) { 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; } }.bind(this); switch (e.keyCode) { case 9: // Tab key. this.toolbarManager_.showToolbarsForKeyboardNavigation(); return; case 27: // Escape key. if (!this.isPrintPreview_) { this.toolbarManager_.hideSingleToolbarLayer(); return; } break; // Ensure escape falls through to the print-preview handler. case 32: // Space key. if (e.shiftKey) pageUpHandler(); else pageDownHandler(); return; case 33: // Page up key. pageUpHandler(); return; case 34: // Page down key. pageDownHandler(); return; case 37: // Left arrow key. if (!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) { // Go to the previous page if there are no horizontal scrollbars and // no form field is focused. if (!(this.viewport_.documentHasScrollbars().horizontal || this.isFormFieldFocused_)) { 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. if (!(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) { // Go to the next page if there are no horizontal scrollbars and no // form field is focused. if (!(this.viewport_.documentHasScrollbars().horizontal || this.isFormFieldFocused_)) { 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 65: // 'a' key. if (e.ctrlKey || e.metaKey) { this.plugin_.postMessage({ type: 'selectAll' }); // Since we do selection ourselves. e.preventDefault(); } return; case 71: // 'g' key. if (this.toolbar_ && (e.ctrlKey || e.metaKey) && e.altKey) { this.toolbarManager_.showToolbars(); this.toolbar_.selectPageNumber(); } return; case 80: // 'p' key if ((e.ctrlKey || e.metaKey) && e.altKey) { this.zoomToolbar_.fitToPage(); } return; case 87: // 'w' key if ((e.ctrlKey || e.metaKey) && e.altKey) { this.zoomToolbar_.fitToWidth(); } return; case 219: // Left bracket key. if (e.ctrlKey) this.rotateCounterClockwise_(); return; case 220: // Backslash key. if (e.ctrlKey) this.zoomToolbar_.fitToggleFromHotKey(); return; case 221: // Right bracket key. if (e.ctrlKey) this.rotateClockwise_(); return; } // Give print preview a chance to handle the key event. if (!fromScriptingAPI && this.isPrintPreview_) { this.sendScriptingMessage_({ type: 'sendKeyEvent', keyEvent: SerializeKeyEvent(e) }); } else { // Show toolbars as a fallback. if (!(e.shiftKey || e.ctrlKey || e.altKey)) this.toolbarManager_.showToolbars(); } }, handleMouseEvent_: function(e) { if (e.type == 'mousemove') this.toolbarManager_.handleMouseMove(e); else if (e.type == 'mouseout') this.toolbarManager_.hideToolbarsForMouseOut(); else if (e.type == 'wheel' && (e.ctrlKey || e.metaKey)) { if (e.deltaY < 0) { this.viewport_.zoomIn.apply(this.viewport_); } else if (e.deltaY > 0) { this.viewport_.zoomOut.apply(this.viewport_); } } }, /** * @private * Rotate the plugin clockwise. */ rotateClockwise_: function() { this.plugin_.postMessage({ type: 'rotateClockwise' }); }, /** * @private * Rotate the plugin counter-clockwise. */ rotateCounterClockwise_: function() { this.plugin_.postMessage({ type: 'rotateCounterclockwise' }); }, /** * @private * Set zoom to "fit to page". */ fitToPage_: function() { this.viewport_.fitToPage(); this.toolbarManager_.forceHideTopToolbar(); }, /** * @private * Notify the plugin to print. */ print_: function() { this.plugin_.postMessage({ type: 'print' }); }, /** * @private * Notify the plugin to save. */ save_: function() { this.plugin_.postMessage({ type: 'save' }); }, /** * Fetches the page number corresponding to the given named destination from * the plugin. * @param {string} name The namedDestination to fetch page number from plugin. */ getNamedDestination_: function(name) { this.plugin_.postMessage({ type: 'getNamedDestination', namedDestination: name }); }, /** * @private * Sends a 'documentLoaded' message to the PDFScriptingAPI if the document has * finished loading. */ sendDocumentLoadedMessage_: function() { if (this.loadState_ == LoadState.LOADING) return; window.dispatchEvent( new CustomEvent('pdf-loaded', { detail: this.loadState_ })) this.sendScriptingMessage_({ type: 'documentLoaded', load_state: this.loadState_ }); }, /** * @private * Handle open pdf parameters. This function updates the viewport as per * the parameters mentioned in the url while opening pdf. The order is * important as later actions can override the effects of previous actions. * @param {Object} viewportPosition The initial position of the viewport to be * displayed. */ handleURLParams_: function(viewportPosition) { if (viewportPosition.page != undefined) this.viewport_.goToPage(viewportPosition.page); if (viewportPosition.position) { // Make sure we don't cancel effect of page parameter. this.viewport_.position = { x: this.viewport_.position.x + viewportPosition.position.x, y: this.viewport_.position.y + viewportPosition.position.y }; } if (viewportPosition.zoom) this.viewport_.setZoom(viewportPosition.zoom); if (viewportPosition.view) { switch (viewportPosition.view.toLowerCase()) { case 'fitw': this.zoomToolbar_.fitToWidth(); break; case 'fitp': this.zoomToolbar_.fitToPage(); break; } } }, /** * @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) { if (this.toolbar_) this.toolbar_.loadProgress = progress; if (progress == -1) { // Document load failed. this.errorScreen_.show(); this.sizer_.style.display = 'none'; if (this.passwordScreen_.active) { this.passwordScreen_.deny(); this.passwordScreen_.active = false; } this.loadState_ = LoadState.FAILED; this.sendDocumentLoadedMessage_(); } else if (progress == 100) { // Document load complete. if (this.lastViewportPosition_) this.viewport_.position = this.lastViewportPosition_; this.paramsParser_.getViewportFromUrlParams( this.originalUrl_, this.handleURLParams_.bind(this)); this.loadState_ = LoadState.SUCCESS; this.sendDocumentLoadedMessage_(); while (this.delayedScriptingMessages_.length > 0) this.handleScriptingMessage(this.delayedScriptingMessages_.shift()); this.toolbarManager_.hideToolbarsAfterTimeout(); } }, /** * @private * Load a dictionary of translated strings into the UI. Used as a callback for * chrome.resourcesPrivate. * @param {Object} strings Dictionary of translated strings */ handleStrings_: function(strings) { document.documentElement.dir = strings.textdirection; document.documentElement.lang = strings.language; $('toolbar').strings = strings; $('zoom-toolbar').strings = strings; $('password-screen').strings = strings; $('error-screen').strings = strings; }, /** * @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_); // If we received the document dimensions, the password was good so we // can dismiss the password screen. if (this.passwordScreen_.active) this.passwordScreen_.accept(); if (this.pageIndicator_) this.pageIndicator_.initialFadeIn(); if (this.toolbar_) { this.toolbar_.docLength = this.documentDimensions_.pageDimensions.length; } 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; window.location.href = href; 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 'getSelectedTextReply': this.sendScriptingMessage_(message.data); break; case 'goToPage': this.viewport_.goToPage(message.data.page); break; case 'loadProgress': this.updateProgress_(message.data.progress); break; case 'navigate': // If in print preview, always open a new tab. if (this.isPrintPreview_) { this.navigator_.navigate( message.data.url, Navigator.WindowOpenDisposition.NEW_BACKGROUND_TAB); } else { this.navigator_.navigate(message.data.url, message.data.disposition); } 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 'cancelStreamUrl': chrome.mimeHandlerPrivate.abortStream(); break; case 'metadata': if (message.data.title) { document.title = message.data.title; } else { document.title = getFilenameFromURL(this.originalUrl_); } this.bookmarks_ = message.data.bookmarks; if (this.toolbar_) { this.toolbar_.docTitle = document.title; this.toolbar_.bookmarks = this.bookmarks; } break; case 'setIsSelecting': this.viewportScroller_.setEnableScrolling(message.data.isSelecting); break; case 'getNamedDestinationReply': this.paramsParser_.onNamedDestinationReceived(message.data.pageNumber); break; case 'formFocusChange': this.isFormFieldFocused_ = message.data.focused; break; } }, /** * @private * A callback that's called before the zoom changes. Notify the plugin to stop * reacting to scroll events while zoom is taking place to avoid flickering. */ beforeZoom_: function() { this.plugin_.postMessage({ type: 'stopScrolling' }); if (this.viewport_.pinchPhase == Viewport.PinchPhase.PINCH_START) { var position = this.viewport_.position; var zoom = this.viewport_.zoom; var pinchPhase = this.viewport_.pinchPhase; this.plugin_.postMessage({ type: 'viewport', zoom: zoom, xOffset: position.x, yOffset: position.y, pinchPhase: pinchPhase }); } }, /** * @private * A callback that's called after the zoom changes. Notify the plugin of the * zoom change and to continue reacting to scroll events. */ afterZoom_: function() { var position = this.viewport_.position; var zoom = this.viewport_.zoom; var pinchVector = this.viewport_.pinchPanVector || {x: 0, y: 0}; var pinchCenter = this.viewport_.pinchCenter || {x: 0, y: 0}; var pinchPhase = this.viewport_.pinchPhase; this.plugin_.postMessage({ type: 'viewport', zoom: zoom, xOffset: position.x, yOffset: position.y, pinchPhase: pinchPhase, pinchX: pinchCenter.x, pinchY: pinchCenter.y, pinchVectorX: pinchVector.x, pinchVectorY: pinchVector.y }); this.zoomManager_.onPdfZoomChange(); }, /** * @private * A callback that's called when an update to a pinch zoom is detected. * @param {!Object} e the pinch event. */ onPinchUpdate_: function(e) { // Throttle number of pinch events to one per frame. if (!this.sentPinchEvent_) { this.sentPinchEvent_ = true; window.requestAnimationFrame(function() { this.sentPinchEvent_ = false; this.viewport_.pinchZoom(e); }.bind(this)); } }, /** * @private * A callback that's called when the end of a pinch zoom is detected. * @param {!Object} e the pinch event. */ onPinchEnd_: function(e) { // Using rAF for pinch end prevents pinch updates scheduled by rAF getting // sent after the pinch end. window.requestAnimationFrame(function() { this.viewport_.pinchZoomEnd(e); }.bind(this)); }, /** * @private * A callback that's called after the viewport changes. */ viewportChanged_: function() { if (!this.documentDimensions_) return; // Offset the toolbar position so that it doesn't move if scrollbars appear. var hasScrollbars = this.viewport_.documentHasScrollbars(); var scrollbarWidth = this.viewport_.scrollbarWidth; var verticalScrollbarWidth = hasScrollbars.vertical ? scrollbarWidth : 0; var horizontalScrollbarWidth = hasScrollbars.horizontal ? scrollbarWidth : 0; // Shift the zoom toolbar to the left by half a scrollbar width. This // gives a compromise: if there is no scrollbar visible then the toolbar // will be half a scrollbar width further left than the spec but if there // is a scrollbar visible it will be half a scrollbar width further right // than the spec. In RTL layout, the zoom toolbar is on the left side, but // the scrollbar is still on the right, so this is not necessary. if (!isRTL()) { this.zoomToolbar_.style.right = -verticalScrollbarWidth + (scrollbarWidth / 2) + 'px'; } // Having a horizontal scrollbar is much rarer so we don't offset the // toolbar from the bottom any more than what the spec says. This means // that when there is a scrollbar visible, it will be a full scrollbar // width closer to the bottom of the screen than usual, but this is ok. this.zoomToolbar_.style.bottom = -horizontalScrollbarWidth + 'px'; // Update the page indicator. var visiblePage = this.viewport_.getMostVisiblePage(); if (this.toolbar_) this.toolbar_.pageNo = visiblePage + 1; // TODO(raymes): Give pageIndicator_ the same API as toolbar_. if (this.pageIndicator_) { this.pageIndicator_.index = visiblePage; if (this.documentDimensions_.pageDimensions.length > 1 && hasScrollbars.vertical) { this.pageIndicator_.style.visibility = 'visible'; } else { this.pageIndicator_.style.visibility = 'hidden'; } } 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 }); }, /** * 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) { if (this.parentWindow_ != message.source) { this.parentWindow_ = message.source; this.parentOrigin_ = message.origin; // Ensure that we notify the embedder if the document is loaded. if (this.loadState_ != LoadState.LOADING) this.sendDocumentLoadedMessage_(); } if (this.handlePrintPreviewScriptingMessage_(message)) return; // Delay scripting messages from users of the scripting API until the // document is loaded. This simplifies use of the APIs. if (this.loadState_ != LoadState.SUCCESS) { this.delayedScriptingMessages_.push(message); return; } switch (message.data.type.toString()) { case 'getSelectedText': case 'print': case 'selectAll': this.plugin_.postMessage(message.data); break; } }, /** * @private * Handle scripting messages specific to print preview. * @param {MessageObject} message the message to handle. * @return {boolean} true if the message was handled, false otherwise. */ handlePrintPreviewScriptingMessage_: function(message) { if (!this.isPrintPreview_) return false; switch (message.data.type.toString()) { case 'loadPreviewPage': this.plugin_.postMessage(message.data); return true; case 'resetPrintPreviewMode': this.loadState_ = LoadState.LOADING; 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) }); return true; case 'sendKeyEvent': this.handleKeyEvent_(DeserializeKeyEvent(message.data.keyEvent)); return true; } return false; }, /** * @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) { if (this.parentWindow_ && this.parentOrigin_) { var targetOrigin; // Only send data back to the embedder if it is from the same origin, // unless we're sending it to ourselves (which could happen in the case // of tests). We also allow documentLoaded messages through as this won't // leak important information. if (this.parentOrigin_ == window.location.origin) targetOrigin = this.parentOrigin_; else if (message.type == 'documentLoaded') targetOrigin = '*'; else targetOrigin = this.originalUrl_; this.parentWindow_.postMessage(message, targetOrigin); } }, /** * @type {Viewport} the viewport of the PDF viewer. */ get viewport() { return this.viewport_; }, /** * Each bookmark is an Object containing a: * - title * - page (optional) * - array of children (themselves bookmarks) * @type {Array} the top-level bookmarks of the PDF. */ get bookmarks() { return this.bookmarks_; } }; // Copyright 2015 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'; /** Idle time in ms before the UI is hidden. */ var HIDE_TIMEOUT = 2000; /** Time in ms after force hide before toolbar is shown again. */ var FORCE_HIDE_TIMEOUT = 1000; /** * Velocity required in a mousemove to reveal the UI (pixels/ms). This is * intended to be high enough that a fast flick of the mouse is required to * reach it. */ var SHOW_VELOCITY = 10; /** Distance from the top of the screen required to reveal the toolbars. */ var TOP_TOOLBAR_REVEAL_DISTANCE = 100; /** Distance from the bottom-right of the screen required to reveal toolbars. */ var SIDE_TOOLBAR_REVEAL_DISTANCE_RIGHT = 150; var SIDE_TOOLBAR_REVEAL_DISTANCE_BOTTOM = 250; /** * @param {MouseEvent} e Event to test. * @return {boolean} True if the mouse is close to the top of the screen. */ function isMouseNearTopToolbar(e) { return e.y < TOP_TOOLBAR_REVEAL_DISTANCE; } /** * @param {MouseEvent} e Event to test. * @param {Window} window Window to test against. * @return {boolean} True if the mouse is close to the bottom-right of the * screen. */ function isMouseNearSideToolbar(e, window) { var atSide = e.x > window.innerWidth - SIDE_TOOLBAR_REVEAL_DISTANCE_RIGHT; if (isRTL()) atSide = e.x < SIDE_TOOLBAR_REVEAL_DISTANCE_RIGHT; var atBottom = e.y > window.innerHeight - SIDE_TOOLBAR_REVEAL_DISTANCE_BOTTOM; return atSide && atBottom; } /** * Constructs a Toolbar Manager, responsible for co-ordinating between multiple * toolbar elements. * @constructor * @param {Object} window The window containing the UI. * @param {Object} toolbar The top toolbar element. * @param {Object} zoomToolbar The zoom toolbar element. */ function ToolbarManager(window, toolbar, zoomToolbar) { this.window_ = window; this.toolbar_ = toolbar; this.zoomToolbar_ = zoomToolbar; this.toolbarTimeout_ = null; this.isMouseNearTopToolbar_ = false; this.isMouseNearSideToolbar_ = false; this.sideToolbarAllowedOnly_ = false; this.sideToolbarAllowedOnlyTimer_ = null; this.keyboardNavigationActive = false; this.lastMovementTimestamp = null; this.window_.addEventListener('resize', this.resizeDropdowns_.bind(this)); this.resizeDropdowns_(); } ToolbarManager.prototype = { handleMouseMove: function(e) { this.isMouseNearTopToolbar_ = this.toolbar_ && isMouseNearTopToolbar(e); this.isMouseNearSideToolbar_ = isMouseNearSideToolbar(e, this.window_); this.keyboardNavigationActive = false; var touchInteractionActive = (e.sourceCapabilities && e.sourceCapabilities.firesTouchEvents); // Allow the top toolbar to be shown if the mouse moves away from the side // toolbar (as long as the timeout has elapsed). if (!this.isMouseNearSideToolbar_ && !this.sideToolbarAllowedOnlyTimer_) this.sideToolbarAllowedOnly_ = false; // Allow the top toolbar to be shown if the mouse moves to the top edge. if (this.isMouseNearTopToolbar_) this.sideToolbarAllowedOnly_ = false; // Tapping the screen with toolbars open tries to close them. if (touchInteractionActive && this.zoomToolbar_.isVisible()) { this.hideToolbarsIfAllowed(); return; } // Show the toolbars if the mouse is near the top or bottom-right of the // screen, if the mouse moved fast, or if the touchscreen was tapped. if (this.isMouseNearTopToolbar_ || this.isMouseNearSideToolbar_ || this.isHighVelocityMouseMove_(e) || touchInteractionActive) { if (this.sideToolbarAllowedOnly_) this.zoomToolbar_.show(); else this.showToolbars(); } this.hideToolbarsAfterTimeout(); }, /** * Whether a mousemove event is high enough velocity to reveal the toolbars. * @param {MouseEvent} e Event to test. * @return {boolean} true if the event is a high velocity mousemove, false * otherwise. * @private */ isHighVelocityMouseMove_: function(e) { if (e.type == 'mousemove') { if (this.lastMovementTimestamp == null) { this.lastMovementTimestamp = this.getCurrentTimestamp_(); } else { var movement = Math.sqrt(e.movementX * e.movementX + e.movementY * e.movementY); var newTime = this.getCurrentTimestamp_(); var interval = newTime - this.lastMovementTimestamp; this.lastMovementTimestamp = newTime; if (interval != 0) return movement / interval > SHOW_VELOCITY; } } return false; }, /** * Wrapper around Date.now() to make it easily replaceable for testing. * @return {int} * @private */ getCurrentTimestamp_: function() { return Date.now(); }, /** * Display both UI toolbars. */ showToolbars: function() { if (this.toolbar_) this.toolbar_.show(); this.zoomToolbar_.show(); }, /** * Show toolbars and mark that navigation is being performed with * tab/shift-tab. This disables toolbar hiding until the mouse is moved or * escape is pressed. */ showToolbarsForKeyboardNavigation: function() { this.keyboardNavigationActive = true; this.showToolbars(); }, /** * Hide toolbars after a delay, regardless of the position of the mouse. * Intended to be called when the mouse has moved out of the parent window. */ hideToolbarsForMouseOut: function() { this.isMouseNearTopToolbar_ = false; this.isMouseNearSideToolbar_ = false; this.hideToolbarsAfterTimeout(); }, /** * Check if the toolbars are able to be closed, and close them if they are. * Toolbars may be kept open based on mouse/keyboard activity and active * elements. */ hideToolbarsIfAllowed: function() { if (this.isMouseNearSideToolbar_ || this.isMouseNearTopToolbar_) return; if (this.toolbar_ && this.toolbar_.shouldKeepOpen()) return; if (this.keyboardNavigationActive) return; // Remove focus to make any visible tooltips disappear -- otherwise they'll // still be visible on screen when the toolbar is off screen. if ((this.toolbar_ && document.activeElement == this.toolbar_) || document.activeElement == this.zoomToolbar_) { document.activeElement.blur(); } if (this.toolbar_) this.toolbar_.hide(); this.zoomToolbar_.hide(); }, /** * Hide the toolbar after the HIDE_TIMEOUT has elapsed. */ hideToolbarsAfterTimeout: function() { if (this.toolbarTimeout_) this.window_.clearTimeout(this.toolbarTimeout_); this.toolbarTimeout_ = this.window_.setTimeout( this.hideToolbarsIfAllowed.bind(this), HIDE_TIMEOUT); }, /** * Hide the 'topmost' layer of toolbars. Hides any dropdowns that are open, or * hides the basic toolbars otherwise. */ hideSingleToolbarLayer: function() { if (!this.toolbar_ || !this.toolbar_.hideDropdowns()) { this.keyboardNavigationActive = false; this.hideToolbarsIfAllowed(); } }, /** * Hide the top toolbar and keep it hidden until both: * - The mouse is moved away from the right side of the screen * - 1 second has passed. * * The top toolbar can be immediately re-opened by moving the mouse to the top * of the screen. */ forceHideTopToolbar: function() { if (!this.toolbar_) return; this.toolbar_.hide(); this.sideToolbarAllowedOnly_ = true; this.sideToolbarAllowedOnlyTimer_ = this.window_.setTimeout(function() { this.sideToolbarAllowedOnlyTimer_ = null; }.bind(this), FORCE_HIDE_TIMEOUT); }, /** * Updates the size of toolbar dropdowns based on the positions of the rest of * the UI. * @private */ resizeDropdowns_: function() { if (!this.toolbar_) return; var lowerBound = this.window_.innerHeight - this.zoomToolbar_.clientHeight; this.toolbar_.setDropdownLowerBound(lowerBound); } }; // Copyright 2014 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. /** * Returns the height of the intersection of two rectangles. * @param {Object} rect1 the first rect * @param {Object} rect2 the second rect * @return {number} the height of the intersection of the rects */ function getIntersectionHeight(rect1, rect2) { return Math.max(0, Math.min(rect1.y + rect1.height, rect2.y + rect2.height) - Math.max(rect1.y, rect2.y)); } /** * Makes sure that the scale level doesn't get out of the limits. * @param {number} scale The new scale level. * @return {number} The scale clamped within the limits. */ function clampScale(scale) { return Math.min(5, Math.max(0.25, scale)); } /** * Computes vector between two points. * @param {!Object} p1 The first point. * @param {!Object} p2 The second point. * @return {!Object} The vector. */ function vectorDelta(p1, p2) { return { x: p2.x - p1.x, y: p2.y - p1.y }; } function frameToPluginCoordinate(coordinateInFrame) { var container = $('plugin'); return { x: coordinateInFrame.x - container.getBoundingClientRect().left, y: coordinateInFrame.y - container.getBoundingClientRect().top }; } /** * Create a new viewport. * @constructor * @param {Window} window the window * @param {Object} sizer is the element which represents the size of the * document in the viewport * @param {Function} viewportChangedCallback is run when the viewport changes * @param {Function} beforeZoomCallback is run before a change in zoom * @param {Function} afterZoomCallback is run after a change in zoom * @param {number} scrollbarWidth the width of scrollbars on the page * @param {number} defaultZoom The default zoom level. * @param {number} topToolbarHeight The number of pixels that should initially * be left blank above the document for the toolbar. */ function Viewport(window, sizer, viewportChangedCallback, beforeZoomCallback, afterZoomCallback, scrollbarWidth, defaultZoom, topToolbarHeight) { this.window_ = window; this.sizer_ = sizer; this.viewportChangedCallback_ = viewportChangedCallback; this.beforeZoomCallback_ = beforeZoomCallback; this.afterZoomCallback_ = afterZoomCallback; this.allowedToChangeZoom_ = false; this.zoom_ = 1; this.documentDimensions_ = null; this.pageDimensions_ = []; this.scrollbarWidth_ = scrollbarWidth; this.fittingType_ = Viewport.FittingType.NONE; this.defaultZoom_ = defaultZoom; this.topToolbarHeight_ = topToolbarHeight; this.prevScale_ = 1; this.pinchPhase_ = Viewport.PinchPhase.PINCH_NONE; this.pinchPanVector_ = null; this.pinchCenter_ = null; this.firstPinchCenterInFrame_ = null; window.addEventListener('scroll', this.updateViewport_.bind(this)); window.addEventListener('resize', this.resize_.bind(this)); } /** * Enumeration of page fitting types. * @enum {string} */ Viewport.FittingType = { NONE: 'none', FIT_TO_PAGE: 'fit-to-page', FIT_TO_WIDTH: 'fit-to-width' }; /** * Enumeration of pinch states. * This should match PinchPhase enum in pdf/out_of_process_instance.h * @enum {number} */ Viewport.PinchPhase = { PINCH_NONE: 0, PINCH_START: 1, PINCH_UPDATE_ZOOM_OUT: 2, PINCH_UPDATE_ZOOM_IN: 3, PINCH_END: 4 }; /** * The increment to scroll a page by in pixels when up/down/left/right arrow * keys are pressed. Usually we just let the browser handle scrolling on the * window when these keys are pressed but in certain cases we need to simulate * these events. */ Viewport.SCROLL_INCREMENT = 40; /** * Predefined zoom factors to be used when zooming in/out. These are in * ascending order. This should match the lists in * components/ui/zoom/page_zoom_constants.h and * chrome/browser/resources/settings/appearance_page/appearance_page.js */ Viewport.ZOOM_FACTORS = [0.25, 1 / 3, 0.5, 2 / 3, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5]; /** * The minimum and maximum range to be used to clip zoom factor. */ Viewport.ZOOM_FACTOR_RANGE = { min: Viewport.ZOOM_FACTORS[0], max: Viewport.ZOOM_FACTORS[Viewport.ZOOM_FACTORS.length - 1] }; /** * The width of the page shadow around pages in pixels. */ Viewport.PAGE_SHADOW = {top: 3, bottom: 7, left: 5, right: 5}; Viewport.prototype = { /** * Returns the zoomed and rounded document dimensions for the given zoom. * Rounding is necessary when interacting with the renderer which tends to * operate in integral values (for example for determining if scrollbars * should be shown). * @param {number} zoom The zoom to use to compute the scaled dimensions. * @return {Object} A dictionary with scaled 'width'/'height' of the document. * @private */ getZoomedDocumentDimensions_: function(zoom) { if (!this.documentDimensions_) return null; return { width: Math.round(this.documentDimensions_.width * zoom), height: Math.round(this.documentDimensions_.height * zoom) }; }, /** * @private * Returns true if the document needs scrollbars at the given zoom level. * @param {number} zoom compute whether scrollbars are needed at this zoom * @return {Object} with 'horizontal' and 'vertical' keys which map to bool * values indicating if the horizontal and vertical scrollbars are needed * respectively. */ documentNeedsScrollbars_: function(zoom) { var zoomedDimensions = this.getZoomedDocumentDimensions_(zoom); if (!zoomedDimensions) { return { horizontal: false, vertical: false }; } // If scrollbars are required for one direction, expand the document in the // other direction to take the width of the scrollbars into account when // deciding whether the other direction needs scrollbars. if (zoomedDimensions.width > this.window_.innerWidth) zoomedDimensions.height += this.scrollbarWidth_; else if (zoomedDimensions.height > this.window_.innerHeight) zoomedDimensions.width += this.scrollbarWidth_; return { horizontal: zoomedDimensions.width > this.window_.innerWidth, vertical: zoomedDimensions.height + this.topToolbarHeight_ > this.window_.innerHeight }; }, /** * Returns true if the document needs scrollbars at the current zoom level. * @return {Object} with 'x' and 'y' keys which map to bool values * indicating if the horizontal and vertical scrollbars are needed * respectively. */ documentHasScrollbars: function() { return this.documentNeedsScrollbars_(this.zoom_); }, /** * @private * Helper function called when the zoomed document size changes. */ contentSizeChanged_: function() { var zoomedDimensions = this.getZoomedDocumentDimensions_(this.zoom_); if (zoomedDimensions) { this.sizer_.style.width = zoomedDimensions.width + 'px'; this.sizer_.style.height = zoomedDimensions.height + this.topToolbarHeight_ + 'px'; } }, /** * @private * Called when the viewport should be updated. */ updateViewport_: function() { this.viewportChangedCallback_(); }, /** * @private * Called when the viewport size changes. */ resize_: function() { if (this.fittingType_ == Viewport.FittingType.FIT_TO_PAGE) this.fitToPageInternal_(false); else if (this.fittingType_ == Viewport.FittingType.FIT_TO_WIDTH) this.fitToWidth(); else this.updateViewport_(); }, /** * @type {Object} the scroll position of the viewport. */ get position() { return { x: this.window_.pageXOffset, y: this.window_.pageYOffset - this.topToolbarHeight_ }; }, /** * Scroll the viewport to the specified position. * @type {Object} position the position to scroll to. */ set position(position) { this.window_.scrollTo(position.x, position.y + this.topToolbarHeight_); }, /** * @type {Object} the size of the viewport excluding scrollbars. */ get size() { var needsScrollbars = this.documentNeedsScrollbars_(this.zoom_); var scrollbarWidth = needsScrollbars.vertical ? this.scrollbarWidth_ : 0; var scrollbarHeight = needsScrollbars.horizontal ? this.scrollbarWidth_ : 0; return { width: this.window_.innerWidth - scrollbarWidth, height: this.window_.innerHeight - scrollbarHeight }; }, /** * @type {number} the zoom level of the viewport. */ get zoom() { return this.zoom_; }, /** * @type {Viewport.PinchPhase} The phase of the current pinch gesture for * the viewport. */ get pinchPhase() { return this.pinchPhase_; }, /** * @type {Object} The panning caused by the current pinch gesture (as * the deltas of the x and y coordinates). */ get pinchPanVector() { return this.pinchPanVector_; }, /** * @type {Object} The coordinates of the center of the current pinch gesture. */ get pinchCenter() { return this.pinchCenter_; }, /** * @private * Used to wrap a function that might perform zooming on the viewport. This is * required so that we can notify the plugin that zooming is in progress * so that while zooming is taking place it can stop reacting to scroll events * from the viewport. This is to avoid flickering. */ mightZoom_: function(f) { this.beforeZoomCallback_(); this.allowedToChangeZoom_ = true; f(); this.allowedToChangeZoom_ = false; this.afterZoomCallback_(); }, /** * @private * Sets the zoom of the viewport. * @param {number} newZoom the zoom level to zoom to. */ setZoomInternal_: function(newZoom) { if (!this.allowedToChangeZoom_) { throw 'Called Viewport.setZoomInternal_ without calling ' + 'Viewport.mightZoom_.'; } // Record the scroll position (relative to the top-left of the window). var currentScrollPos = { x: this.position.x / this.zoom_, y: this.position.y / this.zoom_ }; this.zoom_ = newZoom; this.contentSizeChanged_(); // Scroll to the scaled scroll position. this.position = { x: currentScrollPos.x * newZoom, y: currentScrollPos.y * newZoom }; }, /** * @private * Sets the zoom of the viewport. * Same as setZoomInternal_ but for pinch zoom we have some more operations. * @param {number} scaleDelta The zoom delta. * @param {!Object} center The pinch center in content coordinates. */ setPinchZoomInternal_: function(scaleDelta, center) { assert(this.allowedToChangeZoom_, 'Called Viewport.setPinchZoomInternal_ without calling ' + 'Viewport.mightZoom_.'); this.zoom_ = clampScale(this.zoom_ * scaleDelta); var newCenterInContent = this.frameToContent(center); var delta = { x: (newCenterInContent.x - this.oldCenterInContent.x), y: (newCenterInContent.y - this.oldCenterInContent.y) }; // Record the scroll position (relative to the pinch center). var currentScrollPos = { x: this.position.x - delta.x * this.zoom_, y: this.position.y - delta.y * this.zoom_ }; this.contentSizeChanged_(); // Scroll to the scaled scroll position. this.position = { x: currentScrollPos.x, y: currentScrollPos.y }; }, /** * @private * Converts a point from frame to content coordinates. * @param {!Object} framePoint The frame coordinates. * @return {!Object} The content coordinates. */ frameToContent: function(framePoint) { // TODO(mcnee) Add a helper Point class to avoid duplicating operations // on plain {x,y} objects. return { x: (framePoint.x + this.position.x) / this.zoom_, y: (framePoint.y + this.position.y) / this.zoom_ }; }, /** * Sets the zoom to the given zoom level. * @param {number} newZoom the zoom level to zoom to. */ setZoom: function(newZoom) { this.fittingType_ = Viewport.FittingType.NONE; newZoom = Math.max(Viewport.ZOOM_FACTOR_RANGE.min, Math.min(newZoom, Viewport.ZOOM_FACTOR_RANGE.max)); this.mightZoom_(function() { this.setZoomInternal_(newZoom); this.updateViewport_(); }.bind(this)); }, /** * @type {number} the width of scrollbars in the viewport in pixels. */ get scrollbarWidth() { return this.scrollbarWidth_; }, /** * @type {Viewport.FittingType} the fitting type the viewport is currently in. */ get fittingType() { return this.fittingType_; }, /** * @private * @param {integer} y the y-coordinate to get the page at. * @return {integer} the index of a page overlapping the given y-coordinate. */ getPageAtY_: function(y) { var min = 0; var max = this.pageDimensions_.length - 1; while (max >= min) { var page = Math.floor(min + ((max - min) / 2)); // There might be a gap between the pages, in which case use the bottom // of the previous page as the top for finding the page. var top = 0; if (page > 0) { top = this.pageDimensions_[page - 1].y + this.pageDimensions_[page - 1].height; } var bottom = this.pageDimensions_[page].y + this.pageDimensions_[page].height; if (top <= y && bottom > y) return page; else if (top > y) max = page - 1; else min = page + 1; } return 0; }, /** * Returns the page with the greatest proportion of its height in the current * viewport. * @return {int} the index of the most visible page. */ getMostVisiblePage: function() { var firstVisiblePage = this.getPageAtY_(this.position.y / this.zoom_); if (firstVisiblePage == this.pageDimensions_.length - 1) return firstVisiblePage; var viewportRect = { x: this.position.x / this.zoom_, y: this.position.y / this.zoom_, width: this.size.width / this.zoom_, height: this.size.height / this.zoom_ }; var firstVisiblePageVisibility = getIntersectionHeight( this.pageDimensions_[firstVisiblePage], viewportRect) / this.pageDimensions_[firstVisiblePage].height; var nextPageVisibility = getIntersectionHeight( this.pageDimensions_[firstVisiblePage + 1], viewportRect) / this.pageDimensions_[firstVisiblePage + 1].height; if (nextPageVisibility > firstVisiblePageVisibility) return firstVisiblePage + 1; return firstVisiblePage; }, /** * @private * Compute the zoom level for fit-to-page or fit-to-width. |pageDimensions| is * the dimensions for a given page and if |widthOnly| is true, it indicates * that fit-to-page zoom should be computed rather than fit-to-page. * @param {Object} pageDimensions the dimensions of a given page * @param {boolean} widthOnly a bool indicating whether fit-to-page or * fit-to-width should be computed. * @return {number} the zoom to use */ computeFittingZoom_: function(pageDimensions, widthOnly) { // First compute the zoom without scrollbars. var zoomWidth = this.window_.innerWidth / pageDimensions.width; var zoom; var zoomHeight; if (widthOnly) { zoom = zoomWidth; } else { zoomHeight = this.window_.innerHeight / pageDimensions.height; zoom = Math.min(zoomWidth, zoomHeight); } // Check if there needs to be any scrollbars. var needsScrollbars = this.documentNeedsScrollbars_(zoom); // If the document fits, just return the zoom. if (!needsScrollbars.horizontal && !needsScrollbars.vertical) return zoom; var zoomedDimensions = this.getZoomedDocumentDimensions_(zoom); // Check if adding a scrollbar will result in needing the other scrollbar. var scrollbarWidth = this.scrollbarWidth_; if (needsScrollbars.horizontal && zoomedDimensions.height > this.window_.innerHeight - scrollbarWidth) { needsScrollbars.vertical = true; } if (needsScrollbars.vertical && zoomedDimensions.width > this.window_.innerWidth - scrollbarWidth) { needsScrollbars.horizontal = true; } // Compute available window space. var windowWithScrollbars = { width: this.window_.innerWidth, height: this.window_.innerHeight }; if (needsScrollbars.horizontal) windowWithScrollbars.height -= scrollbarWidth; if (needsScrollbars.vertical) windowWithScrollbars.width -= scrollbarWidth; // Recompute the zoom. zoomWidth = windowWithScrollbars.width / pageDimensions.width; if (widthOnly) { zoom = zoomWidth; } else { zoomHeight = windowWithScrollbars.height / pageDimensions.height; zoom = Math.min(zoomWidth, zoomHeight); } return zoom; }, /** * Zoom the viewport so that the page-width consumes the entire viewport. */ fitToWidth: function() { this.mightZoom_(function() { this.fittingType_ = Viewport.FittingType.FIT_TO_WIDTH; if (!this.documentDimensions_) return; // When computing fit-to-width, the maximum width of a page in the // document is used, which is equal to the size of the document width. this.setZoomInternal_(this.computeFittingZoom_(this.documentDimensions_, true)); var page = this.getMostVisiblePage(); this.updateViewport_(); }.bind(this)); }, /** * @private * Zoom the viewport so that a page consumes the entire viewport. * @param {boolean} scrollToTopOfPage Set to true if the viewport should be * scrolled to the top of the current page. Set to false if the viewport * should remain at the current scroll position. */ fitToPageInternal_: function(scrollToTopOfPage) { this.mightZoom_(function() { this.fittingType_ = Viewport.FittingType.FIT_TO_PAGE; if (!this.documentDimensions_) return; var page = this.getMostVisiblePage(); // Fit to the current page's height and the widest page's width. var dimensions = { width: this.documentDimensions_.width, height: this.pageDimensions_[page].height, }; this.setZoomInternal_(this.computeFittingZoom_(dimensions, false)); if (scrollToTopOfPage) { this.position = { x: 0, y: this.pageDimensions_[page].y * this.zoom_ }; } this.updateViewport_(); }.bind(this)); }, /** * Zoom the viewport so that a page consumes the entire viewport. Also scrolls * the viewport to the top of the current page. */ fitToPage: function() { this.fitToPageInternal_(true); }, /** * Zoom out to the next predefined zoom level. */ zoomOut: function() { this.mightZoom_(function() { this.fittingType_ = Viewport.FittingType.NONE; var nextZoom = Viewport.ZOOM_FACTORS[0]; for (var i = 0; i < Viewport.ZOOM_FACTORS.length; i++) { if (Viewport.ZOOM_FACTORS[i] < this.zoom_) nextZoom = Viewport.ZOOM_FACTORS[i]; } this.setZoomInternal_(nextZoom); this.updateViewport_(); }.bind(this)); }, /** * Zoom in to the next predefined zoom level. */ zoomIn: function() { this.mightZoom_(function() { this.fittingType_ = Viewport.FittingType.NONE; var nextZoom = Viewport.ZOOM_FACTORS[Viewport.ZOOM_FACTORS.length - 1]; for (var i = Viewport.ZOOM_FACTORS.length - 1; i >= 0; i--) { if (Viewport.ZOOM_FACTORS[i] > this.zoom_) nextZoom = Viewport.ZOOM_FACTORS[i]; } this.setZoomInternal_(nextZoom); this.updateViewport_(); }.bind(this)); }, /** * Pinch zoom event handler. * @param {!Object} e The pinch event. */ pinchZoom: function(e) { this.mightZoom_(function() { this.pinchPhase_ = e.direction == 'out' ? Viewport.PinchPhase.PINCH_UPDATE_ZOOM_OUT : Viewport.PinchPhase.PINCH_UPDATE_ZOOM_IN; var scaleDelta = e.startScaleRatio / this.prevScale_; this.pinchPanVector_ = vectorDelta(e.center, this.firstPinchCenterInFrame_); var needsScrollbars = this.documentNeedsScrollbars_( clampScale(this.zoom_ * scaleDelta)); this.pinchCenter_ = e.center; // If there's no horizontal scrolling, keep the content centered so the // user can't zoom in on the non-content area. // TODO(mcnee) Investigate other ways of scaling when we don't have // horizontal scrolling. We want to keep the document centered, // but this causes a potentially awkward transition when we start // using the gesture center. if (!needsScrollbars.horizontal) { this.pinchCenter_ = { x: this.window_.innerWidth / 2, y: this.window_.innerHeight / 2 }; } else if (this.keepContentCentered_) { this.oldCenterInContent = this.frameToContent(frameToPluginCoordinate(e.center)); this.keepContentCentered_ = false; } this.setPinchZoomInternal_( scaleDelta, frameToPluginCoordinate(e.center)); this.updateViewport_(); this.prevScale_ = e.startScaleRatio; }.bind(this)); }, pinchZoomStart: function(e) { this.pinchPhase_ = Viewport.PinchPhase.PINCH_START; this.prevScale_ = 1; this.oldCenterInContent = this.frameToContent(frameToPluginCoordinate(e.center)); var needsScrollbars = this.documentNeedsScrollbars_(this.zoom_); this.keepContentCentered_ = !needsScrollbars.horizontal; // We keep track of begining of the pinch. // By doing so we will be able to compute the pan distance. this.firstPinchCenterInFrame_ = e.center; }, pinchZoomEnd: function(e) { this.mightZoom_(function() { this.pinchPhase_ = Viewport.PinchPhase.PINCH_END; var scaleDelta = e.startScaleRatio / this.prevScale_; this.pinchCenter_ = e.center; this.setPinchZoomInternal_( scaleDelta, frameToPluginCoordinate(e.center)); this.updateViewport_(); }.bind(this)); this.pinchPhase_ = Viewport.PinchPhase.PINCH_NONE; this.pinchPanVector_ = null; this.pinchCenter_ = null; this.firstPinchCenterInFrame_ = null; }, /** * Go to the given page index. * @param {number} page the index of the page to go to. zero-based. */ goToPage: function(page) { this.mightZoom_(function() { if (this.pageDimensions_.length === 0) return; if (page < 0) page = 0; if (page >= this.pageDimensions_.length) page = this.pageDimensions_.length - 1; var dimensions = this.pageDimensions_[page]; var toolbarOffset = 0; // Unless we're in fit to page mode, scroll above the page by // |this.topToolbarHeight_| so that the toolbar isn't covering it // initially. if (this.fittingType_ != Viewport.FittingType.FIT_TO_PAGE) toolbarOffset = this.topToolbarHeight_; this.position = { x: dimensions.x * this.zoom_, y: dimensions.y * this.zoom_ - toolbarOffset }; this.updateViewport_(); }.bind(this)); }, /** * Set the dimensions of the document. * @param {Object} documentDimensions the dimensions of the document */ setDocumentDimensions: function(documentDimensions) { this.mightZoom_(function() { var initialDimensions = !this.documentDimensions_; this.documentDimensions_ = documentDimensions; this.pageDimensions_ = this.documentDimensions_.pageDimensions; if (initialDimensions) { this.setZoomInternal_( Math.min(this.defaultZoom_, this.computeFittingZoom_(this.documentDimensions_, true))); this.position = { x: 0, y: -this.topToolbarHeight_ }; } this.contentSizeChanged_(); this.resize_(); }.bind(this)); }, /** * Get the coordinates of the page contents (excluding the page shadow) * relative to the screen. * @param {number} page the index of the page to get the rect for. * @return {Object} a rect representing the page in screen coordinates. */ getPageScreenRect: function(page) { if (!this.documentDimensions_) { return { x: 0, y: 0, width: 0, height: 0 }; } if (page >= this.pageDimensions_.length) page = this.pageDimensions_.length - 1; var pageDimensions = this.pageDimensions_[page]; // Compute the page dimensions minus the shadows. var insetDimensions = { x: pageDimensions.x + Viewport.PAGE_SHADOW.left, y: pageDimensions.y + Viewport.PAGE_SHADOW.top, width: pageDimensions.width - Viewport.PAGE_SHADOW.left - Viewport.PAGE_SHADOW.right, height: pageDimensions.height - Viewport.PAGE_SHADOW.top - Viewport.PAGE_SHADOW.bottom }; // Compute the x-coordinate of the page within the document. // TODO(raymes): This should really be set when the PDF plugin passes the // page coordinates, but it isn't yet. var x = (this.documentDimensions_.width - pageDimensions.width) / 2 + Viewport.PAGE_SHADOW.left; // Compute the space on the left of the document if the document fits // completely in the screen. var spaceOnLeft = (this.size.width - this.documentDimensions_.width * this.zoom_) / 2; spaceOnLeft = Math.max(spaceOnLeft, 0); return { x: x * this.zoom_ + spaceOnLeft - this.window_.pageXOffset, y: insetDimensions.y * this.zoom_ - this.window_.pageYOffset, width: insetDimensions.width * this.zoom_, height: insetDimensions.height * this.zoom_ }; } }; // Copyright 2014 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'; /** * Creates a new OpenPDFParamsParser. This parses the open pdf parameters * passed in the url to set initial viewport settings for opening the pdf. * @param {Object} getNamedDestinationsFunction The function called to fetch * the page number for a named destination. */ function OpenPDFParamsParser(getNamedDestinationsFunction) { this.outstandingRequests_ = []; this.getNamedDestinationsFunction_ = getNamedDestinationsFunction; } OpenPDFParamsParser.prototype = { /** * @private * Parse zoom parameter of open PDF parameters. If this * parameter is passed while opening PDF then PDF should be opened * at the specified zoom level. * @param {number} zoom value. * @param {Object} viewportPosition to store zoom and position value. */ parseZoomParam_: function(paramValue, viewportPosition) { var paramValueSplit = paramValue.split(','); if ((paramValueSplit.length != 1) && (paramValueSplit.length != 3)) return; // User scale of 100 means zoom value of 100% i.e. zoom factor of 1.0. var zoomFactor = parseFloat(paramValueSplit[0]) / 100; if (isNaN(zoomFactor)) return; // Handle #zoom=scale. if (paramValueSplit.length == 1) { viewportPosition['zoom'] = zoomFactor; return; } // Handle #zoom=scale,left,top. var position = {x: parseFloat(paramValueSplit[1]), y: parseFloat(paramValueSplit[2])}; viewportPosition['position'] = position; viewportPosition['zoom'] = zoomFactor; }, /** * Parse the parameters encoded in the fragment of a URL into a dictionary. * @private * @param {string} url to parse * @return {Object} Key-value pairs of URL parameters */ parseUrlParams_: function(url) { var params = {}; var paramIndex = url.search('#'); if (paramIndex == -1) return params; var paramTokens = url.substring(paramIndex + 1).split('&'); if ((paramTokens.length == 1) && (paramTokens[0].search('=') == -1)) { // Handle the case of http://foo.com/bar#NAMEDDEST. This is not // explicitly mentioned except by example in the Adobe // "PDF Open Parameters" document. params['nameddest'] = paramTokens[0]; return params; } for (var i = 0; i < paramTokens.length; ++i) { var keyValueSplit = paramTokens[i].split('='); if (keyValueSplit.length != 2) continue; params[keyValueSplit[0]] = keyValueSplit[1]; } return params; }, /** * Parse PDF url parameters used for controlling the state of UI. These need * to be available when the UI is being initialized, rather than when the PDF * is finished loading. * @param {string} url that needs to be parsed. * @return {Object} parsed url parameters. */ getUiUrlParams: function(url) { var params = this.parseUrlParams_(url); var uiParams = {toolbar: true, toolbarSpacer: true}; if ('toolbar' in params && params['toolbar'] == 0) uiParams.toolbar = false; if ('toolbar' in params && params['toolbar'] == 2) uiParams.toolbarSpacer = false; return uiParams; }, /** * Parse PDF url parameters. These parameters are mentioned in the url * and specify actions to be performed when opening pdf files. * See http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/ * pdfs/pdf_open_parameters.pdf for details. * @param {string} url that needs to be parsed. * @param {Function} callback function to be called with viewport info. */ getViewportFromUrlParams: function(url, callback) { var viewportPosition = {}; viewportPosition['url'] = url; var paramsDictionary = this.parseUrlParams_(url); if ('page' in paramsDictionary) { // |pageNumber| is 1-based, but goToPage() take a zero-based page number. var pageNumber = parseInt(paramsDictionary['page']); if (!isNaN(pageNumber) && pageNumber > 0) viewportPosition['page'] = pageNumber - 1; } if ('zoom' in paramsDictionary) this.parseZoomParam_(paramsDictionary['zoom'], viewportPosition); if ('view' in paramsDictionary) viewportPosition['view'] = paramsDictionary['view']; if (viewportPosition.page === undefined && 'nameddest' in paramsDictionary) { this.outstandingRequests_.push({ callback: callback, viewportPosition: viewportPosition }); this.getNamedDestinationsFunction_(paramsDictionary['nameddest']); } else { callback(viewportPosition); } }, /** * This is called when a named destination is received and the page number * corresponding to the request for which a named destination is passed. * @param {number} pageNumber The page corresponding to the named destination * requested. */ onNamedDestinationReceived: function(pageNumber) { var outstandingRequest = this.outstandingRequests_.shift(); if (pageNumber != -1) outstandingRequest.viewportPosition.page = pageNumber; outstandingRequest.callback(outstandingRequest.viewportPosition); }, }; // Copyright 2015 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'; /** * Creates a new NavigatorDelegate for calling browser-specific functions to * do the actual navigating. * @param {boolean} isInTab Indicates if the PDF viewer is displayed in a tab. */ function NavigatorDelegate(isInTab) { this.isInTab_ = isInTab; } /** * Creates a new Navigator for navigating to links inside or outside the PDF. * @param {string} originalUrl The original page URL. * @param {Object} viewport The viewport info of the page. * @param {Object} paramsParser The object for URL parsing. * @param {Object} navigatorDelegate The object with callback functions that * get called when navigation happens in the current tab, a new tab, * and a new window. */ function Navigator(originalUrl, viewport, paramsParser, navigatorDelegate) { this.originalUrl_ = originalUrl; this.viewport_ = viewport; this.paramsParser_ = paramsParser; this.navigatorDelegate_ = navigatorDelegate; } NavigatorDelegate.prototype = { /** * @public * Called when navigation should happen in the current tab. * @param {string} url The url to be opened in the current tab. */ navigateInCurrentTab: function(url) { // When the PDFviewer is inside a browser tab, prefer the tabs API because // it can navigate from one file:// URL to another. if (chrome.tabs && this.isInTab_) chrome.tabs.update({url: url}); else window.location.href = url; }, /** * @public * Called when navigation should happen in the new tab. * @param {string} url The url to be opened in the new tab. * @param {boolean} active Indicates if the new tab should be the active tab. */ navigateInNewTab: function(url, active) { // Prefer the tabs API because it guarantees we can just open a new tab. // window.open doesn't have this guarantee. if (chrome.tabs) chrome.tabs.create({url: url, active: active}); else window.open(url); }, /** * @public * Called when navigation should happen in the new window. * @param {string} url The url to be opened in the new window. */ navigateInNewWindow: function(url) { // Prefer the windows API because it guarantees we can just open a new // window. window.open with '_blank' argument doesn't have this guarantee. if (chrome.windows) chrome.windows.create({url: url}); else window.open(url, '_blank'); } }; /** * Represents options when navigating to a new url. C++ counterpart of * the enum is in ui/base/window_open_disposition.h. This enum represents * the only values that are passed from Plugin. * @enum {number} */ Navigator.WindowOpenDisposition = { CURRENT_TAB: 1, NEW_FOREGROUND_TAB: 3, NEW_BACKGROUND_TAB: 4, NEW_WINDOW: 6, SAVE_TO_DISK: 7 }; Navigator.prototype = { /** * @private * Function to navigate to the given URL. This might involve navigating * within the PDF page or opening a new url (in the same tab or a new tab). * @param {string} url The URL to navigate to. * @param {number} disposition The window open disposition when * navigating to the new URL. */ navigate: function(url, disposition) { if (url.length == 0) return; // If |urlFragment| starts with '#', then it's for the same URL with a // different URL fragment. if (url.charAt(0) == '#') { // if '#' is already present in |originalUrl| then remove old fragment // and add new url fragment. var hashIndex = this.originalUrl_.search('#'); if (hashIndex != -1) url = this.originalUrl_.substring(0, hashIndex) + url; else url = this.originalUrl_ + url; } // If there's no scheme, then take a guess at the scheme. if (url.indexOf('://') == -1 && url.indexOf('mailto:') == -1) url = this.guessUrlWithoutScheme_(url); if (!this.isValidUrl_(url)) return; switch (disposition) { case Navigator.WindowOpenDisposition.CURRENT_TAB: this.paramsParser_.getViewportFromUrlParams( url, this.onViewportReceived_.bind(this)); break; case Navigator.WindowOpenDisposition.NEW_BACKGROUND_TAB: this.navigatorDelegate_.navigateInNewTab(url, false); break; case Navigator.WindowOpenDisposition.NEW_FOREGROUND_TAB: this.navigatorDelegate_.navigateInNewTab(url, true); break; case Navigator.WindowOpenDisposition.NEW_WINDOW: this.navigatorDelegate_.navigateInNewWindow(url); break; case Navigator.WindowOpenDisposition.SAVE_TO_DISK: // TODO(jaepark): Alt + left clicking a link in PDF should // download the link. this.paramsParser_.getViewportFromUrlParams( url, this.onViewportReceived_.bind(this)); break; default: break; } }, /** * @private * Called when the viewport position is received. * @param {Object} viewportPosition Dictionary containing the viewport * position. */ onViewportReceived_: function(viewportPosition) { var originalUrl = this.originalUrl_; var hashIndex = originalUrl.search('#'); if (hashIndex != -1) originalUrl = originalUrl.substring(0, hashIndex); var newUrl = viewportPosition.url; hashIndex = newUrl.search('#'); if (hashIndex != -1) newUrl = newUrl.substring(0, hashIndex); var pageNumber = viewportPosition.page; if (pageNumber != undefined && originalUrl == newUrl) this.viewport_.goToPage(pageNumber); else this.navigatorDelegate_.navigateInCurrentTab(viewportPosition.url); }, /** * @private * Checks if the URL starts with a scheme and is not just a scheme. * @param {string} The input URL * @return {boolean} Whether the url is valid. */ isValidUrl_: function(url) { // Make sure |url| starts with a valid scheme. if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('ftp://') && !url.startsWith('file://') && !url.startsWith('mailto:')) { return false; } // Navigations to file:-URLs are only allowed from file:-URLs. if (url.startsWith('file:') && !this.originalUrl_.startsWith('file:')) return false; // Make sure |url| is not only a scheme. if (url == 'http://' || url == 'https://' || url == 'ftp://' || url == 'file://' || url == 'mailto:') { return false; } return true; }, /** * @private * Attempt to figure out what a URL is when there is no scheme. * @param {string} The input URL * @return {string} The URL with a scheme or the original URL if it is not * possible to determine the scheme. */ guessUrlWithoutScheme_: function(url) { // If the original URL is mailto:, that does not make sense to start with, // and neither does adding |url| to it. // If the original URL is not a valid URL, this cannot make a valid URL. // In both cases, just bail out. if (this.originalUrl_.startsWith('mailto:') || !this.isValidUrl_(this.originalUrl_)) { return url; } // Check for absolute paths. if (url.startsWith('/')) { var schemeEndIndex = this.originalUrl_.indexOf('://'); var firstSlash = this.originalUrl_.indexOf('/', schemeEndIndex + 3); // e.g. http://www.foo.com/bar -> http://www.foo.com var domain = firstSlash != -1 ? this.originalUrl_.substr(0, firstSlash) : this.originalUrl_; return domain + url; } // Check for obvious relative paths. var isRelative = false; if (url.startsWith('.') || url.startsWith('\\')) isRelative = true; // In Adobe Acrobat Reader XI, it looks as though links with less than // 2 dot separators in the domain are considered relative links, and // those with 2 of more are considered http URLs. e.g. // // www.foo.com/bar -> http // foo.com/bar -> relative link if (!isRelative) { var domainSeparatorIndex = url.indexOf('/'); var domainName = domainSeparatorIndex == -1 ? url : url.substr(0, domainSeparatorIndex); var domainDotCount = (domainName.match(/\./g) || []).length; if (domainDotCount < 2) isRelative = true; } if (isRelative) { var slashIndex = this.originalUrl_.lastIndexOf('/'); var path = slashIndex != -1 ? this.originalUrl_.substr(0, slashIndex) : this.originalUrl_; return path + '/' + url; } return 'http://' + url; } }; // Copyright 2015 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'; /** * @private * The period of time in milliseconds to wait between updating the viewport * position by the scroll velocity. */ ViewportScroller.DRAG_TIMER_INTERVAL_MS_ = 100; /** * @private * The maximum drag scroll distance per DRAG_TIMER_INTERVAL in pixels. */ ViewportScroller.MAX_DRAG_SCROLL_DISTANCE_ = 100; /** * Creates a new ViewportScroller. * A ViewportScroller scrolls the page in response to drag selection with the * mouse. * @param {Object} viewport The viewport info of the page. * @param {Object} plugin The PDF plugin element. * @param {Object} window The window containing the viewer. */ function ViewportScroller(viewport, plugin, window) { this.viewport_ = viewport; this.plugin_ = plugin; this.window_ = window; this.mousemoveCallback_ = null; this.timerId_ = null; this.scrollVelocity_ = null; this.lastFrameTime_ = 0; } ViewportScroller.prototype = { /** * @private * Start scrolling the page by |scrollVelocity_| every * |DRAG_TIMER_INTERVAL_MS_|. */ startDragScrollTimer_: function() { if (this.timerId_ === null) { this.timerId_ = this.window_.setInterval(this.dragScrollPage_.bind(this), ViewportScroller.DRAG_TIMER_INTERVAL_MS_); this.lastFrameTime_ = Date.now(); } }, /** * @private * Stops the drag scroll timer if it is active. */ stopDragScrollTimer_: function() { if (this.timerId_ !== null) { this.window_.clearInterval(this.timerId_); this.timerId_ = null; this.lastFrameTime_ = 0; } }, /** * @private * Scrolls the viewport by the current scroll velocity. */ dragScrollPage_: function() { var position = this.viewport_.position; var currentFrameTime = Date.now(); var timeAdjustment = (currentFrameTime - this.lastFrameTime_) / ViewportScroller.DRAG_TIMER_INTERVAL_MS_; position.y += (this.scrollVelocity_.y * timeAdjustment); position.x += (this.scrollVelocity_.x * timeAdjustment); this.viewport_.position = position; this.lastFrameTime_ = currentFrameTime; }, /** * @private * Calculate the velocity to scroll while dragging using the distance of the * cursor outside the viewport. * @param {Object} event The mousemove event. * @return {Object} Object with x and y direction scroll velocity. */ calculateVelocity_: function(event) { var x = Math.min(Math.max(-event.offsetX, event.offsetX - this.plugin_.offsetWidth, 0), ViewportScroller.MAX_DRAG_SCROLL_DISTANCE_) * Math.sign(event.offsetX); var y = Math.min(Math.max(-event.offsetY, event.offsetY - this.plugin_.offsetHeight, 0), ViewportScroller.MAX_DRAG_SCROLL_DISTANCE_) * Math.sign(event.offsetY); return { x: x, y: y }; }, /** * @private * Handles mousemove events. It updates the scroll velocity and starts and * stops timer based on scroll velocity. * @param {Object} event The mousemove event. */ onMousemove_: function(event) { this.scrollVelocity_ = this.calculateVelocity_(event); if (!this.scrollVelocity_.x && !this.scrollVelocity_.y) this.stopDragScrollTimer_(); else if (!this.timerId_) this.startDragScrollTimer_(); }, /** * Sets whether to scroll the viewport when the mouse is outside the * viewport. * @param {boolean} isSelecting Represents selection status. */ setEnableScrolling: function(isSelecting) { if (isSelecting) { if (!this.mousemoveCallback_) this.mousemoveCallback_ = this.onMousemove_.bind(this); this.plugin_.addEventListener('mousemove', this.mousemoveCallback_, false); } else { this.stopDragScrollTimer_(); if (this.mousemoveCallback_) { this.plugin_.removeEventListener('mousemove', this.mousemoveCallback_, false); } } } }; // Copyright 2016 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'; /** * A class that listens for touch events and produces events when these * touches form gestures (e.g. pinching). */ class GestureDetector { /** * Constructs a GestureDetector. * @param {!Element} element The element to monitor for touch gestures. */ constructor(element) { this.element_ = element; this.element_.addEventListener( 'touchstart', this.onTouchStart_.bind(this), { passive: false }); this.element_.addEventListener( 'touchmove', this.onTouch_.bind(this), { passive: true }); this.element_.addEventListener( 'touchend', this.onTouch_.bind(this), { passive: true }); this.element_.addEventListener( 'touchcancel', this.onTouch_.bind(this), { passive: true }); this.pinchStartEvent_ = null; this.lastEvent_ = null; this.listeners_ = new Map([ ['pinchstart', []], ['pinchupdate', []], ['pinchend', []] ]); } /** * Add a |listener| to be notified of |type| events. * @param {string} type The event type to be notified for. * @param {Function} listener The callback. */ addEventListener(type, listener) { if (this.listeners_.has(type)) { this.listeners_.get(type).push(listener); } } /** * Call the relevant listeners with the given |pinchEvent|. * @private * @param {!Object} pinchEvent The event to notify the listeners of. */ notify_(pinchEvent) { let listeners = this.listeners_.get(pinchEvent.type); for (let l of listeners) l(pinchEvent); } /** * The callback for touchstart events on the element. * @private * @param {!TouchEvent} event Touch event on the element. */ onTouchStart_(event) { // We must preventDefault if there is a two finger touch. By doing so // native pinch-zoom does not interfere with our way of handling the event. if (event.touches.length == 2) { event.preventDefault(); this.pinchStartEvent_ = event; this.lastEvent_ = event; this.notify_({ type: 'pinchstart', center: GestureDetector.center_(event) }); } } /** * The callback for touch move, end, and cancel events on the element. * @private * @param {!TouchEvent} event Touch event on the element. */ onTouch_(event) { if (!this.pinchStartEvent_) return; // Check if the pinch ends with the current event. if (event.touches.length < 2 || this.lastEvent_.touches.length !== event.touches.length) { let startScaleRatio = GestureDetector.pinchScaleRatio_( this.lastEvent_, this.pinchStartEvent_); let center = GestureDetector.center_(this.lastEvent_); let endEvent = { type: 'pinchend', startScaleRatio: startScaleRatio, center: center }; this.pinchStartEvent_ = null; this.lastEvent_ = null; this.notify_(endEvent); return; } let scaleRatio = GestureDetector.pinchScaleRatio_(event, this.lastEvent_); let startScaleRatio = GestureDetector.pinchScaleRatio_( event, this.pinchStartEvent_); let center = GestureDetector.center_(event); this.notify_({ type: 'pinchupdate', scaleRatio: scaleRatio, direction: scaleRatio > 1.0 ? 'in' : 'out', startScaleRatio: startScaleRatio, center: center }); this.lastEvent_ = event; } /** * Computes the change in scale between this touch event * and a previous one. * @private * @param {!TouchEvent} event Latest touch event on the element. * @param {!TouchEvent} prevEvent A previous touch event on the element. * @return {?number} The ratio of the scale of this event and the * scale of the previous one. */ static pinchScaleRatio_(event, prevEvent) { let distance1 = GestureDetector.distance_(prevEvent); let distance2 = GestureDetector.distance_(event); return distance1 === 0 ? null : distance2 / distance1; } /** * Computes the distance between fingers. * @private * @param {!TouchEvent} event Touch event with at least 2 touch points. * @return {number} Distance between touch[0] and touch[1]. */ static distance_(event) { let touch1 = event.touches[0]; let touch2 = event.touches[1]; let dx = touch1.clientX - touch2.clientX; let dy = touch1.clientY - touch2.clientY; return Math.sqrt(dx * dx + dy * dy); } /** * Computes the midpoint between fingers. * @private * @param {!TouchEvent} event Touch event with at least 2 touch points. * @return {!Object} Midpoint between touch[0] and touch[1]. */ static center_(event) { let touch1 = event.touches[0]; let touch2 = event.touches[1]; return { x: (touch1.clientX + touch2.clientX) / 2, y: (touch1.clientY + touch2.clientY) / 2 }; } }; // Copyright 2015 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'; /** * A class that manages updating the browser with zoom changes. */ class ZoomManager { /** * Constructs a ZoomManager * @param {!Viewport} viewport A Viewport for which to manage zoom. * @param {Function} setBrowserZoomFunction A function that sets the browser * zoom to the provided value. * @param {number} initialZoom The initial browser zoom level. */ constructor(viewport, setBrowserZoomFunction, initialZoom) { this.viewport_ = viewport; this.setBrowserZoomFunction_ = setBrowserZoomFunction; this.browserZoom_ = initialZoom; this.changingBrowserZoom_ = null; } /** * Invoked when a browser-initiated zoom-level change occurs. * @param {number} newZoom the zoom level to zoom to. */ onBrowserZoomChange(newZoom) { // If we are changing the browser zoom level, ignore any browser zoom level // change events. Either, the change occurred before our update and will be // overwritten, or the change being reported is the change we are making, // which we have already handled. if (this.changingBrowserZoom_) return; if (this.floatingPointEquals(this.browserZoom_, newZoom)) return; this.browserZoom_ = newZoom; this.viewport_.setZoom(newZoom); } /** * Invoked when an extension-initiated zoom-level change occurs. */ onPdfZoomChange() { // If we are already changing the browser zoom level in response to a // previous extension-initiated zoom-level change, ignore this zoom change. // Once the browser zoom level is changed, we check whether the extension's // zoom level matches the most recently sent zoom level. if (this.changingBrowserZoom_) return; let zoom = this.viewport_.zoom; if (this.floatingPointEquals(this.browserZoom_, zoom)) return; this.changingBrowserZoom_ = this.setBrowserZoomFunction_(zoom).then( function() { this.browserZoom_ = zoom; this.changingBrowserZoom_ = null; // The extension's zoom level may have changed while the browser zoom // change was in progress. We call back into onPdfZoomChange to ensure the // browser zoom is up to date. this.onPdfZoomChange(); }.bind(this)); } /** * Returns whether two numbers are approximately equal. * @param {number} a The first number. * @param {number} b The second number. */ floatingPointEquals(a, b) { let MIN_ZOOM_DELTA = 0.01; // If the zoom level is close enough to the current zoom level, don't // change it. This avoids us getting into an infinite loop of zoom changes // due to floating point error. return Math.abs(a - b) <= MIN_ZOOM_DELTA; } }; // Copyright 2015 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'; /** * Returns a promise that will resolve to the default zoom factor. * @return {Promise} A promise that will resolve to the default zoom * factor. */ function lookupDefaultZoom() { return cr.sendWithPromise('getDefaultZoom'); } /** * Returns a promise that will resolve to the initial zoom factor * upon starting the plugin. This may differ from the default zoom * if, for example, the page is zoomed before the plugin is run. * @return {Promise} A promise that will resolve to the initial zoom * factor. */ function lookupInitialZoom() { return cr.sendWithPromise('getInitialZoom'); } /** * A class providing an interface to the browser. */ class BrowserApi { /** * @constructor * @param {!Object} streamInfo The stream object which points to the data * contained in the PDF. * @param {number} defaultZoom The default browser zoom. * @param {number} initialZoom The initial browser zoom * upon starting the plugin. * @param {boolean} manageZoom Whether to manage zoom. */ constructor(streamInfo, defaultZoom, initialZoom, manageZoom) { this.streamInfo_ = streamInfo; this.defaultZoom_ = defaultZoom; this.initialZoom_ = initialZoom; this.manageZoom_ = manageZoom; } /** * Returns a promise to a BrowserApi. * @param {!Object} streamInfo The stream object pointing to the data * contained in the PDF. * @param {boolean} manageZoom Whether to manage zoom. */ static create(streamInfo, manageZoom) { return Promise.all([ lookupDefaultZoom(), lookupInitialZoom() ]).then(function(zoomFactors) { return new BrowserApi( streamInfo, zoomFactors[0], zoomFactors[1], manageZoom); }); } /** * Returns the stream info pointing to the data contained in the PDF. * @return {Object} The stream info object. */ getStreamInfo() { return this.streamInfo_; } /** * Sets the browser zoom. * @param {number} zoom The zoom factor to send to the browser. * @return {Promise} A promise that will be resolved when the browser zoom * has been updated. */ setZoom(zoom) { if (!this.manageZoom_) return Promise.resolve(); return cr.sendWithPromise('setZoom', zoom); } /** * Returns the default browser zoom factor. * @return {number} The default browser zoom factor. */ getDefaultZoom() { return this.defaultZoom_; } /** * Returns the initial browser zoom factor. * @return {number} The initial browser zoom factor. */ getInitialZoom() { return this.initialZoom_; } /** * Adds an event listener to be notified when the browser zoom changes. * @param {function} listener The listener to be called with the new zoom * factor. */ addZoomEventListener(listener) { if (!this.manageZoom_) return; cr.addWebUIListener('onZoomLevelChanged', function(newZoomFactor) { listener(newZoomFactor); }); } }; /** * Creates a BrowserApi instance for an extension not running as a mime handler. * @return {Promise} A promise to a BrowserApi instance constructed * from the URL. */ function createBrowserApi(opts) { let streamInfo = { streamUrl: opts.streamURL, originalUrl: opts.originalURL, responseHeaders: opts.responseHeaders, embedded: window.parent != window, tabId: -1, }; return new Promise(function(resolve, reject) { resolve(BrowserApi.create(streamInfo, true)); }); } /* Copyright 2015 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. */ :root { --iron-icon-height: 20px; --iron-icon-width: 20px; --paper-icon-button: { height: 32px; padding: 6px; width: 32px; }; --paper-icon-button-ink-color: rgb(189, 189, 189); --viewer-icon-ink-color: rgb(189, 189, 189); } /* Copyright 2015 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. */ #item { @apply(--layout-center); @apply(--layout-horizontal); color: rgb(80, 80, 80); cursor: pointer; font-size: 77.8%; height: 30px; position: relative; } #item:hover { background-color: rgb(237, 237, 237); color: rgb(20, 20, 20); } paper-ripple { /* Allowing the ripple to capture pointer events prevents a focus rectangle * for showing up for clicks, while still allowing it with tab-navigation. * This undoes a paper-ripple bugfix aimed at non-Chrome browsers. * TODO(tsergeant): Improve focus in viewer-bookmark so this can be removed * (https://crbug.com/5448190). */ pointer-events: auto; } #title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #expand { --iron-icon-height: 16px; --iron-icon-width: 16px; --paper-icon-button-ink-color: var(--paper-grey-900); height: 28px; min-width: 28px; padding: 6px; transition: transform 150ms; width: 28px; } :host-context([dir=rtl]) #expand { transform: rotate(180deg); } :host([children-shown]) #expand { transform: rotate(90deg); } // Copyright 2015 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. (function() { /** Amount that each level of bookmarks is indented by (px). */ var BOOKMARK_INDENT = 20; Polymer({ is: 'viewer-bookmark', properties: { /** * A bookmark object, each containing a: * - title * - page (optional) * - children (an array of bookmarks) */ bookmark: { type: Object, observer: 'bookmarkChanged_' }, depth: { type: Number, observer: 'depthChanged' }, childDepth: Number, childrenShown: { type: Boolean, reflectToAttribute: true, value: false }, keyEventTarget: { type: Object, value: function() { return this.$.item; } } }, behaviors: [ Polymer.IronA11yKeysBehavior ], keyBindings: { 'enter': 'onEnter_', 'space': 'onSpace_' }, bookmarkChanged_: function() { this.$.expand.style.visibility = this.bookmark.children.length > 0 ? 'visible' : 'hidden'; }, depthChanged: function() { this.childDepth = this.depth + 1; this.$.item.style.webkitPaddingStart = (this.depth * BOOKMARK_INDENT) + 'px'; }, onClick: function() { if (this.bookmark.hasOwnProperty('page')) this.fire('change-page', {page: this.bookmark.page}); else if (this.bookmark.hasOwnProperty('uri')) this.fire('navigate', {uri: this.bookmark.uri, newtab: true}); }, onEnter_: function(e) { // Don't allow events which have propagated up from the expand button to // trigger a click. if (e.detail.keyboardEvent.target != this.$.expand) this.onClick(); }, onSpace_: function(e) { // paper-icon-button stops propagation of space events, so there's no need // to check the event source here. this.onClick(); // Prevent default space scroll behavior. e.detail.keyboardEvent.preventDefault(); }, toggleChildren: function(e) { this.childrenShown = !this.childrenShown; e.stopPropagation(); // Prevent the above onClick handler from firing. } }); })(); // Copyright 2015 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. Polymer({ is: 'viewer-bookmarks-content' }); /* Copyright 2015 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. */ .last-item { margin-bottom: 24px; } // Copyright 2014 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. Polymer({ is: 'viewer-error-screen', properties: { reloadFn: { type: Object, value: null, observer: 'reloadFnChanged_' }, strings: Object, }, reloadFnChanged_: function() { // The default margins in paper-dialog don't work well with hiding/showing // the .buttons div. We need to manually manage the bottom margin to get // around this. if (this.reloadFn) this.$['load-failed-message'].classList.remove('last-item'); else this.$['load-failed-message'].classList.add('last-item'); }, show: function() { this.$.dialog.open(); }, reload: function() { if (this.reloadFn) this.reloadFn(); } }); /* 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. */ :host { -webkit-transition: opacity 400ms ease-in-out; pointer-events: none; position: fixed; right: 0; } #text { background-color: rgba(0, 0, 0, 0.5); border-radius: 5px; color: white; float: left; font-family: sans-serif; font-size: 12px; font-weight: bold; line-height: 48px; text-align: center; text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8); width: 62px; } #triangle-right { border-bottom: 6px solid transparent; border-left: 8px solid rgba(0, 0, 0, 0.5); border-top: 6px solid transparent; display: inline; float: left; height: 0; margin-top: 18px; width: 0; } // Copyright 2014 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. Polymer({ is: 'viewer-page-indicator', properties: { label: { type: String, value: '1' }, index: { type: Number, observer: 'indexChanged' }, pageLabels: { type: Array, value: null, observer: 'pageLabelsChanged' } }, timerId: undefined, ready: function() { var callback = this.fadeIn.bind(this, 2000); window.addEventListener('scroll', function() { requestAnimationFrame(callback); }); }, initialFadeIn: function() { this.fadeIn(6000); }, fadeIn: function(displayTime) { var percent = window.scrollY / (document.body.scrollHeight - document.documentElement.clientHeight); this.style.top = percent * (document.documentElement.clientHeight - this.offsetHeight) + 'px'; this.style.opacity = 1; clearTimeout(this.timerId); this.timerId = setTimeout(function() { this.style.opacity = 0; this.timerId = undefined; }.bind(this), displayTime); }, pageLabelsChanged: function() { this.indexChanged(); }, indexChanged: function() { if (this.pageLabels) this.label = this.pageLabels[this.index]; else this.label = String(this.index + 1); } }); /* Copyright 2015 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. */ :host { color: #fff; font-size: 94.4%; } :host ::selection { background: rgba(255, 255, 255, 0.3); } #pageselector { --paper-input-container-underline: { visibility: hidden; }; --paper-input-container-underline-focus: { visibility: hidden; }; display: inline-block; padding: 0; width: 1ch; } #input { -webkit-margin-start: -3px; color: #fff; line-height: 18px; padding: 3px; text-align: end; vertical-align: baseline; } #input:focus, #input:hover { background-color: rgba(0, 0, 0, 0.5); border-radius: 2px; } #slash { padding: 0 3px; } #pagelength-spacer { display: inline-block; text-align: start; } #slash, #pagelength { font-size: 76.5%; } // Copyright 2015 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. Polymer({ is: 'viewer-page-selector', properties: { /** * The number of pages the document contains. */ docLength: { type: Number, value: 1, observer: 'docLengthChanged' }, /** * The current page being viewed (1-based). A change to pageNo is mirrored * immediately to the input field. A change to the input field is not * mirrored back until pageNoCommitted() is called and change-page is fired. */ pageNo: { type: Number, value: 1 }, strings: Object, }, pageNoCommitted: function() { var page = parseInt(this.$.input.value); if (!isNaN(page) && page <= this.docLength && page > 0) this.fire('change-page', {page: page - 1}); else this.$.input.value = this.pageNo; this.$.input.blur(); }, docLengthChanged: function() { var numDigits = this.docLength.toString().length; this.$.pageselector.style.width = numDigits + 'ch'; // Set both sides of the slash to the same width, so that the layout is // exactly centered. this.$['pagelength-spacer'].style.width = numDigits + 'ch'; }, select: function() { this.$.input.select(); }, /** * @return {boolean} True if the selector input field is currently focused. */ isActive: function() { return this.shadowRoot.activeElement == this.$.input; } }); // Copyright 2014 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. Polymer({ is: 'viewer-password-screen', properties: { invalid: Boolean, active: { type: Boolean, value: false, observer: 'activeChanged' }, strings: Object, }, ready: function() { this.activeChanged(); }, accept: function() { this.active = false; }, deny: function() { this.$.password.disabled = false; this.$.submit.disabled = false; this.invalid = true; this.$.password.focus(); this.$.password.select(); }, handleKey: function(e) { if (e.keyCode == 13) this.submit(); }, submit: function() { if (this.$.password.value.length == 0) return; this.$.password.disabled = true; this.$.submit.disabled = true; this.fire('password-submitted', {password: this.$.password.value}); }, activeChanged: function() { if (this.active) { this.$.dialog.open(); this.$.password.focus(); } else { this.$.dialog.close(); } } }); /* Copyright 2015 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. */ :host ::selection { background: rgba(255, 255, 255, 0.3); } /* We introduce a wrapper aligner element to help with laying out the main * toolbar content without changing the bottom-aligned progress bar. */ #aligner { @apply(--layout-horizontal); @apply(--layout-center); padding: 0 16px; width: 100%; } #title { @apply(--layout-flex-5); font-size: 77.8%; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #pageselector-container { @apply(--layout-flex-1); text-align: center; /* The container resizes according to the width of the toolbar. On small * screens with large numbers of pages, overflow page numbers without * wrapping. */ white-space: nowrap; } #buttons { @apply(--layout-flex-5); -webkit-user-select: none; text-align: end; } paper-icon-button { -webkit-margin-end: 12px; } viewer-toolbar-dropdown { -webkit-margin-end: 4px; } paper-progress { --paper-progress-active-color: var(--google-blue-300); --paper-progress-container-color: transparent; --paper-progress-height: 3px; transition: opacity 150ms; width: 100%; } paper-toolbar { --paper-toolbar-background: rgb(50, 54, 57); --paper-toolbar-height: 48px; @apply(--shadow-elevation-2dp); color: rgb(241, 241, 241); font-size: 1.5em; } .invisible { visibility: hidden; } @media(max-width: 675px) { #bookmarks, #rotate-left { display: none; } #pageselector-container { flex: 2; } } @media(max-width: 450px) { #rotate-right { display: none; } } @media(max-width: 400px) { #buttons, #pageselector-container { display: none; } } // Copyright 2015 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. (function() { Polymer({ is: 'viewer-pdf-toolbar', behaviors: [ Polymer.NeonAnimationRunnerBehavior ], properties: { /** * The current loading progress of the PDF document (0 - 100). */ loadProgress: { type: Number, observer: 'loadProgressChanged' }, /** * The title of the PDF document. */ docTitle: String, /** * The number of the page being viewed (1-based). */ pageNo: Number, /** * Tree of PDF bookmarks (or null if the document has no bookmarks). */ bookmarks: { type: Object, value: null }, /** * The number of pages in the PDF document. */ docLength: Number, /** * Whether the toolbar is opened and visible. */ opened: { type: Boolean, value: true }, strings: Object, animationConfig: { value: function() { return { 'entry': { name: 'transform-animation', node: this, transformFrom: 'translateY(-100%)', transformTo: 'translateY(0%)', timing: { easing: 'cubic-bezier(0, 0, 0.2, 1)', duration: 250 } }, 'exit': { name: 'slide-up-animation', node: this, timing: { easing: 'cubic-bezier(0.4, 0, 1, 1)', duration: 250 } } }; } } }, listeners: { 'neon-animation-finish': '_onAnimationFinished' }, _onAnimationFinished: function() { this.style.transform = this.opened ? 'none' : 'translateY(-100%)'; }, loadProgressChanged: function() { if (this.loadProgress >= 100) { this.$.pageselector.classList.toggle('invisible', false); this.$.buttons.classList.toggle('invisible', false); this.$.progress.style.opacity = 0; } }, hide: function() { if (this.opened) this.toggleVisibility(); }, show: function() { if (!this.opened) { this.toggleVisibility(); } }, toggleVisibility: function() { this.opened = !this.opened; this.cancelAnimation(); this.playAnimation(this.opened ? 'entry' : 'exit'); }, selectPageNumber: function() { this.$.pageselector.select(); }, shouldKeepOpen: function() { return this.$.bookmarks.dropdownOpen || this.loadProgress < 100 || this.$.pageselector.isActive(); }, hideDropdowns: function() { if (this.$.bookmarks.dropdownOpen) { this.$.bookmarks.toggleDropdown(); return true; } return false; }, setDropdownLowerBound: function(lowerBound) { this.$.bookmarks.lowerBound = lowerBound; }, rotateRight: function() { this.fire('rotate-right'); }, download: function() { this.fire('save'); }, print: function() { this.fire('print'); } }); })(); /* Copyright 2015 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. */ :host { text-align: start; } #container { position: absolute; /* Controls the position of the dropdown relative to the right of the screen. * Default is aligned with the right of the toolbar buttons. * TODO(tsergeant): Change the layout of the dropdown so this is not required. */ right: var(--viewer-toolbar-dropdown-right-distance, 36px); } :host-context([dir=rtl]) #container { left: var(--viewer-toolbar-dropdown-right-distance, 36px); right: auto; } paper-material { background-color: rgb(256, 256, 256); border-radius: 4px; overflow-y: hidden; padding-bottom: 2px; width: 260px; } #scroll-container { max-height: 300px; overflow-y: auto; padding: 6px 0 4px 0; } #icon { cursor: pointer; display: inline-block; } :host([dropdown-open]) #icon { background-color: rgb(25, 27, 29); border-radius: 4px; } #arrow { -webkit-margin-start: -12px; -webkit-padding-end: 4px; } h1 { border-bottom: 1px solid rgb(219, 219, 219); color: rgb(33, 33, 33); font-size: 77.8%; font-weight: 500; margin: 0; padding: 14px 28px; } // Copyright 2015 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. (function() { /** * Size of additional padding in the inner scrollable section of the dropdown. */ var DROPDOWN_INNER_PADDING = 12; /** Size of vertical padding on the outer #dropdown element. */ var DROPDOWN_OUTER_PADDING = 2; /** Minimum height of toolbar dropdowns (px). */ var MIN_DROPDOWN_HEIGHT = 200; Polymer({ is: 'viewer-toolbar-dropdown', properties: { /** String to be displayed at the top of the dropdown. */ header: String, /** Icon to display when the dropdown is closed. */ closedIcon: String, /** Icon to display when the dropdown is open. */ openIcon: String, /** True if the dropdown is currently open. */ dropdownOpen: { type: Boolean, reflectToAttribute: true, value: false }, /** Toolbar icon currently being displayed. */ dropdownIcon: { type: String, computed: 'computeIcon_(dropdownOpen, closedIcon, openIcon)' }, /** Lowest vertical point that the dropdown should occupy (px). */ lowerBound: { type: Number, observer: 'lowerBoundChanged_' }, /** * True if the max-height CSS property for the dropdown scroll container * is valid. If false, the height will be updated the next time the * dropdown is visible. */ maxHeightValid_: false, /** Current animation being played, or null if there is none. */ animation_: Object }, computeIcon_: function(dropdownOpen, closedIcon, openIcon) { return dropdownOpen ? openIcon : closedIcon; }, lowerBoundChanged_: function() { this.maxHeightValid_ = false; if (this.dropdownOpen) this.updateMaxHeight(); }, toggleDropdown: function() { this.dropdownOpen = !this.dropdownOpen; if (this.dropdownOpen) { this.$.dropdown.style.display = 'block'; if (!this.maxHeightValid_) this.updateMaxHeight(); } this.cancelAnimation_(); this.playAnimation_(this.dropdownOpen); }, updateMaxHeight: function() { var scrollContainer = this.$['scroll-container']; var height = this.lowerBound - scrollContainer.getBoundingClientRect().top - DROPDOWN_INNER_PADDING; height = Math.max(height, MIN_DROPDOWN_HEIGHT); scrollContainer.style.maxHeight = height + 'px'; this.maxHeightValid_ = true; }, cancelAnimation_: function() { if (this._animation) this._animation.cancel(); }, /** * Start an animation on the dropdown. * @param {boolean} isEntry True to play entry animation, false to play * exit. * @private */ playAnimation_: function(isEntry) { this.animation_ = isEntry ? this.animateEntry_() : this.animateExit_(); this.animation_.onfinish = function() { this.animation_ = null; if (!this.dropdownOpen) this.$.dropdown.style.display = 'none'; }.bind(this); }, animateEntry_: function() { var maxHeight = this.$.dropdown.getBoundingClientRect().height - DROPDOWN_OUTER_PADDING; if (maxHeight < 0) maxHeight = 0; var fade = new KeyframeEffect(this.$.dropdown, [ {opacity: 0}, {opacity: 1} ], {duration: 150, easing: 'cubic-bezier(0, 0, 0.2, 1)'}); var slide = new KeyframeEffect(this.$.dropdown, [ {height: '20px', transform: 'translateY(-10px)'}, {height: maxHeight + 'px', transform: 'translateY(0)'} ], {duration: 250, easing: 'cubic-bezier(0, 0, 0.2, 1)'}); return document.timeline.play(new GroupEffect([fade, slide])); }, animateExit_: function() { return this.$.dropdown.animate([ {transform: 'translateY(0)', opacity: 1}, {transform: 'translateY(-5px)', opacity: 0} ], {duration: 100, easing: 'cubic-bezier(0.4, 0, 1, 1)'}); } }); })(); /* Copyright 2015 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. */ #wrapper { transition: transform 250ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } :host([closed]) #wrapper { /* 132px roughly flips the location of the button across the right edge of the * page. */ transform: translateX(132px); transition-timing-function: cubic-bezier(0.4, 0, 1, 1); } :host-context([dir=rtl]):host([closed]) #wrapper { transform: translateX(-132px); } paper-fab { --paper-fab-keyboard-focus-background: var(--viewer-icon-ink-color); --paper-fab-mini: { height: 36px; padding: 8px; width: 36px; }; @apply(--shadow-elevation-4dp); background-color: rgb(242, 242, 242); color: rgb(96, 96, 96); overflow: visible; } // Copyright 2015 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. Polymer({ is: 'viewer-zoom-button', properties: { /** * Icons to be displayed on the FAB. Multiple icons should be separated with * spaces, and will be cycled through every time the FAB is clicked. */ icons: String, /** * Array version of the list of icons. Polymer does not allow array * properties to be set from HTML, so we must use a string property and * perform the conversion manually. * @private */ icons_: { type: Array, value: [''], computed: 'computeIconsArray_(icons)' }, tooltips: Array, closed: { type: Boolean, reflectToAttribute: true, value: false }, delay: { type: Number, observer: 'delayChanged_' }, /** * Index of the icon currently being displayed. */ activeIndex: { type: Number, value: 0 }, /** * Icon currently being displayed on the FAB. * @private */ visibleIcon_: { type: String, computed: 'computeVisibleIcon_(icons_, activeIndex)' }, visibleTooltip_: { type: String, computed: 'computeVisibleTooltip_(tooltips, activeIndex)' } }, computeIconsArray_: function(icons) { return icons.split(' '); }, computeVisibleIcon_: function(icons, activeIndex) { return icons[activeIndex]; }, computeVisibleTooltip_: function(tooltips, activeIndex) { return tooltips[activeIndex]; }, delayChanged_: function() { this.$.wrapper.style.transitionDelay = this.delay + 'ms'; }, show: function() { this.closed = false; }, hide: function() { this.closed = true; }, fireClick: function() { // We cannot attach an on-click to the entire viewer-zoom-button, as this // will include clicks on the margins. Instead, proxy clicks on the FAB // through. this.fire('fabclick'); this.activeIndex = (this.activeIndex + 1) % this.icons_.length; } }); /* Copyright 2015 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. */ :host { -webkit-user-select: none; bottom: 0; padding: 48px 0; position: fixed; right: 0; z-index: 3; } :host-context([dir=rtl]) { left: 0; right: auto; } #zoom-buttons { position: relative; right: 48px; } :host-context([dir=rtl]) #zoom-buttons { left: 48px; right: auto; } viewer-zoom-button { display: block; } /* A small gap between the zoom in/zoom out buttons. */ #zoom-out-button { margin-top: 10px; } /* A larger gap between the fit button and bottom two buttons. */ #zoom-in-button { margin-top: 24px; } // Copyright 2015 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. (function() { var FIT_TO_PAGE = 0; var FIT_TO_WIDTH = 1; Polymer({ is: 'viewer-zoom-toolbar', properties: { strings: { type: Object, observer: 'updateTooltips_' }, visible_: { type: Boolean, value: true } }, isVisible: function() { return this.visible_; }, /** * @private * Change button tooltips to match any changes to localized strings. */ updateTooltips_: function() { this.$['fit-button'].tooltips = [ this.strings.tooltipFitToPage, this.strings.tooltipFitToWidth ]; this.$['zoom-in-button'].tooltips = [this.strings.tooltipZoomIn]; this.$['zoom-out-button'].tooltips = [this.strings.tooltipZoomOut]; }, fitToPage: function() { this.fire('fit-to-page'); this.$['fit-button'].activeIndex = FIT_TO_WIDTH; }, fitToWidth: function() { this.fire('fit-to-width'); this.$['fit-button'].activeIndex = FIT_TO_PAGE; }, /** * Handle clicks of the fit-button. */ fitToggle: function() { if (this.$['fit-button'].activeIndex == FIT_TO_WIDTH) this.fire('fit-to-width'); else this.fire('fit-to-page'); }, /** * Handle the keyboard shortcut equivalent of fit-button clicks. */ fitToggleFromHotKey: function() { this.fitToggle(); // Toggle the button state since there was no mouse click. var button = this.$['fit-button']; if (button.activeIndex == FIT_TO_WIDTH) button.activeIndex = FIT_TO_PAGE; else button.activeIndex = FIT_TO_WIDTH; }, /** * Handle clicks of the zoom-in-button. */ zoomIn: function() { this.fire('zoom-in'); }, /** * Handle clicks of the zoom-out-button. */ zoomOut: function() { this.fire('zoom-out'); }, show: function() { if (!this.visible_) { this.visible_ = true; this.$['fit-button'].show(); this.$['zoom-in-button'].show(); this.$['zoom-out-button'].show(); } }, hide: function() { if (this.visible_) { this.visible_ = false; this.$['fit-button'].hide(); this.$['zoom-in-button'].hide(); this.$['zoom-out-button'].hide(); } }, }); })();