michael@0: /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ michael@0: /* Copyright 2012 Mozilla Foundation michael@0: * michael@0: * Licensed under the Apache License, Version 2.0 (the "License"); michael@0: * you may not use this file except in compliance with the License. michael@0: * You may obtain a copy of the License at michael@0: * michael@0: * http://www.apache.org/licenses/LICENSE-2.0 michael@0: * michael@0: * Unless required by applicable law or agreed to in writing, software michael@0: * distributed under the License is distributed on an "AS IS" BASIS, michael@0: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. michael@0: * See the License for the specific language governing permissions and michael@0: * limitations under the License. michael@0: */ michael@0: /* globals PDFJS, PDFBug, FirefoxCom, Stats, Cache, PDFFindBar, CustomStyle, michael@0: PDFFindController, ProgressBar, TextLayerBuilder, DownloadManager, michael@0: getFileName, scrollIntoView, getPDFFileNameFromURL, PDFHistory, michael@0: Preferences, ViewHistory, PageView, ThumbnailView, URL, michael@0: noContextMenuHandler, SecondaryToolbar, PasswordPrompt, michael@0: PresentationMode, HandTool, Promise, DocumentProperties */ michael@0: michael@0: 'use strict'; michael@0: michael@0: var DEFAULT_URL = 'compressed.tracemonkey-pldi-09.pdf'; michael@0: var DEFAULT_SCALE = 'auto'; michael@0: var DEFAULT_SCALE_DELTA = 1.1; michael@0: var UNKNOWN_SCALE = 0; michael@0: var CACHE_SIZE = 20; michael@0: var CSS_UNITS = 96.0 / 72.0; michael@0: var SCROLLBAR_PADDING = 40; michael@0: var VERTICAL_PADDING = 5; michael@0: var MAX_AUTO_SCALE = 1.25; michael@0: var MIN_SCALE = 0.25; michael@0: var MAX_SCALE = 4.0; michael@0: var VIEW_HISTORY_MEMORY = 20; michael@0: var SCALE_SELECT_CONTAINER_PADDING = 8; michael@0: var SCALE_SELECT_PADDING = 22; michael@0: var THUMBNAIL_SCROLL_MARGIN = -19; michael@0: var USE_ONLY_CSS_ZOOM = false; michael@0: var CLEANUP_TIMEOUT = 30000; michael@0: var IGNORE_CURRENT_POSITION_ON_ZOOM = false; michael@0: var RenderingStates = { michael@0: INITIAL: 0, michael@0: RUNNING: 1, michael@0: PAUSED: 2, michael@0: FINISHED: 3 michael@0: }; michael@0: var FindStates = { michael@0: FIND_FOUND: 0, michael@0: FIND_NOTFOUND: 1, michael@0: FIND_WRAPPED: 2, michael@0: FIND_PENDING: 3 michael@0: }; michael@0: michael@0: PDFJS.imageResourcesPath = './images/'; michael@0: PDFJS.workerSrc = '../build/pdf.worker.js'; michael@0: PDFJS.cMapUrl = '../web/cmaps/'; michael@0: PDFJS.cMapPacked = true; michael@0: michael@0: var mozL10n = document.mozL10n || document.webL10n; michael@0: michael@0: michael@0: // optimised CSS custom property getter/setter michael@0: var CustomStyle = (function CustomStyleClosure() { michael@0: michael@0: // As noted on: http://www.zachstronaut.com/posts/2009/02/17/ michael@0: // animate-css-transforms-firefox-webkit.html michael@0: // in some versions of IE9 it is critical that ms appear in this list michael@0: // before Moz michael@0: var prefixes = ['ms', 'Moz', 'Webkit', 'O']; michael@0: var _cache = {}; michael@0: michael@0: function CustomStyle() {} michael@0: michael@0: CustomStyle.getProp = function get(propName, element) { michael@0: // check cache only when no element is given michael@0: if (arguments.length == 1 && typeof _cache[propName] == 'string') { michael@0: return _cache[propName]; michael@0: } michael@0: michael@0: element = element || document.documentElement; michael@0: var style = element.style, prefixed, uPropName; michael@0: michael@0: // test standard property first michael@0: if (typeof style[propName] == 'string') { michael@0: return (_cache[propName] = propName); michael@0: } michael@0: michael@0: // capitalize michael@0: uPropName = propName.charAt(0).toUpperCase() + propName.slice(1); michael@0: michael@0: // test vendor specific properties michael@0: for (var i = 0, l = prefixes.length; i < l; i++) { michael@0: prefixed = prefixes[i] + uPropName; michael@0: if (typeof style[prefixed] == 'string') { michael@0: return (_cache[propName] = prefixed); michael@0: } michael@0: } michael@0: michael@0: //if all fails then set to undefined michael@0: return (_cache[propName] = 'undefined'); michael@0: }; michael@0: michael@0: CustomStyle.setProp = function set(propName, element, str) { michael@0: var prop = this.getProp(propName); michael@0: if (prop != 'undefined') { michael@0: element.style[prop] = str; michael@0: } michael@0: }; michael@0: michael@0: return CustomStyle; michael@0: })(); michael@0: michael@0: function getFileName(url) { michael@0: var anchor = url.indexOf('#'); michael@0: var query = url.indexOf('?'); michael@0: var end = Math.min( michael@0: anchor > 0 ? anchor : url.length, michael@0: query > 0 ? query : url.length); michael@0: return url.substring(url.lastIndexOf('/', end) + 1, end); michael@0: } michael@0: michael@0: /** michael@0: * Returns scale factor for the canvas. It makes sense for the HiDPI displays. michael@0: * @return {Object} The object with horizontal (sx) and vertical (sy) michael@0: scales. The scaled property is set to false if scaling is michael@0: not required, true otherwise. michael@0: */ michael@0: function getOutputScale(ctx) { michael@0: var devicePixelRatio = window.devicePixelRatio || 1; michael@0: var backingStoreRatio = ctx.webkitBackingStorePixelRatio || michael@0: ctx.mozBackingStorePixelRatio || michael@0: ctx.msBackingStorePixelRatio || michael@0: ctx.oBackingStorePixelRatio || michael@0: ctx.backingStorePixelRatio || 1; michael@0: var pixelRatio = devicePixelRatio / backingStoreRatio; michael@0: return { michael@0: sx: pixelRatio, michael@0: sy: pixelRatio, michael@0: scaled: pixelRatio != 1 michael@0: }; michael@0: } michael@0: michael@0: /** michael@0: * Scrolls specified element into view of its parent. michael@0: * element {Object} The element to be visible. michael@0: * spot {Object} An object with optional top and left properties, michael@0: * specifying the offset from the top left edge. michael@0: */ michael@0: function scrollIntoView(element, spot) { michael@0: // Assuming offsetParent is available (it's not available when viewer is in michael@0: // hidden iframe or object). We have to scroll: if the offsetParent is not set michael@0: // producing the error. See also animationStartedClosure. michael@0: var parent = element.offsetParent; michael@0: var offsetY = element.offsetTop + element.clientTop; michael@0: var offsetX = element.offsetLeft + element.clientLeft; michael@0: if (!parent) { michael@0: console.error('offsetParent is not set -- cannot scroll'); michael@0: return; michael@0: } michael@0: while (parent.clientHeight === parent.scrollHeight) { michael@0: if (parent.dataset._scaleY) { michael@0: offsetY /= parent.dataset._scaleY; michael@0: offsetX /= parent.dataset._scaleX; michael@0: } michael@0: offsetY += parent.offsetTop; michael@0: offsetX += parent.offsetLeft; michael@0: parent = parent.offsetParent; michael@0: if (!parent) { michael@0: return; // no need to scroll michael@0: } michael@0: } michael@0: if (spot) { michael@0: if (spot.top !== undefined) { michael@0: offsetY += spot.top; michael@0: } michael@0: if (spot.left !== undefined) { michael@0: offsetX += spot.left; michael@0: parent.scrollLeft = offsetX; michael@0: } michael@0: } michael@0: parent.scrollTop = offsetY; michael@0: } michael@0: michael@0: /** michael@0: * Event handler to suppress context menu. michael@0: */ michael@0: function noContextMenuHandler(e) { michael@0: e.preventDefault(); michael@0: } michael@0: michael@0: /** michael@0: * Returns the filename or guessed filename from the url (see issue 3455). michael@0: * url {String} The original PDF location. michael@0: * @return {String} Guessed PDF file name. michael@0: */ michael@0: function getPDFFileNameFromURL(url) { michael@0: var reURI = /^(?:([^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/; michael@0: // SCHEME HOST 1.PATH 2.QUERY 3.REF michael@0: // Pattern to get last matching NAME.pdf michael@0: var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i; michael@0: var splitURI = reURI.exec(url); michael@0: var suggestedFilename = reFilename.exec(splitURI[1]) || michael@0: reFilename.exec(splitURI[2]) || michael@0: reFilename.exec(splitURI[3]); michael@0: if (suggestedFilename) { michael@0: suggestedFilename = suggestedFilename[0]; michael@0: if (suggestedFilename.indexOf('%') != -1) { michael@0: // URL-encoded %2Fpath%2Fto%2Ffile.pdf should be file.pdf michael@0: try { michael@0: suggestedFilename = michael@0: reFilename.exec(decodeURIComponent(suggestedFilename))[0]; michael@0: } catch(e) { // Possible (extremely rare) errors: michael@0: // URIError "Malformed URI", e.g. for "%AA.pdf" michael@0: // TypeError "null has no properties", e.g. for "%2F.pdf" michael@0: } michael@0: } michael@0: } michael@0: return suggestedFilename || 'document.pdf'; michael@0: } michael@0: michael@0: var ProgressBar = (function ProgressBarClosure() { michael@0: michael@0: function clamp(v, min, max) { michael@0: return Math.min(Math.max(v, min), max); michael@0: } michael@0: michael@0: function ProgressBar(id, opts) { michael@0: michael@0: // Fetch the sub-elements for later. michael@0: this.div = document.querySelector(id + ' .progress'); michael@0: michael@0: // Get the loading bar element, so it can be resized to fit the viewer. michael@0: this.bar = this.div.parentNode; michael@0: michael@0: // Get options, with sensible defaults. michael@0: this.height = opts.height || 100; michael@0: this.width = opts.width || 100; michael@0: this.units = opts.units || '%'; michael@0: michael@0: // Initialize heights. michael@0: this.div.style.height = this.height + this.units; michael@0: this.percent = 0; michael@0: } michael@0: michael@0: ProgressBar.prototype = { michael@0: michael@0: updateBar: function ProgressBar_updateBar() { michael@0: if (this._indeterminate) { michael@0: this.div.classList.add('indeterminate'); michael@0: this.div.style.width = this.width + this.units; michael@0: return; michael@0: } michael@0: michael@0: this.div.classList.remove('indeterminate'); michael@0: var progressSize = this.width * this._percent / 100; michael@0: this.div.style.width = progressSize + this.units; michael@0: }, michael@0: michael@0: get percent() { michael@0: return this._percent; michael@0: }, michael@0: michael@0: set percent(val) { michael@0: this._indeterminate = isNaN(val); michael@0: this._percent = clamp(val, 0, 100); michael@0: this.updateBar(); michael@0: }, michael@0: michael@0: setWidth: function ProgressBar_setWidth(viewer) { michael@0: if (viewer) { michael@0: var container = viewer.parentNode; michael@0: var scrollbarWidth = container.offsetWidth - viewer.offsetWidth; michael@0: if (scrollbarWidth > 0) { michael@0: this.bar.setAttribute('style', 'width: calc(100% - ' + michael@0: scrollbarWidth + 'px);'); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: hide: function ProgressBar_hide() { michael@0: this.bar.classList.add('hidden'); michael@0: this.bar.removeAttribute('style'); michael@0: } michael@0: }; michael@0: michael@0: return ProgressBar; michael@0: })(); michael@0: michael@0: var Cache = function cacheCache(size) { michael@0: var data = []; michael@0: this.push = function cachePush(view) { michael@0: var i = data.indexOf(view); michael@0: if (i >= 0) { michael@0: data.splice(i); michael@0: } michael@0: data.push(view); michael@0: if (data.length > size) { michael@0: data.shift().destroy(); michael@0: } michael@0: }; michael@0: }; michael@0: michael@0: michael@0: michael@0: michael@0: var DEFAULT_PREFERENCES = { michael@0: showPreviousViewOnLoad: true, michael@0: defaultZoomValue: '', michael@0: ifAvailableShowOutlineOnLoad: false, michael@0: enableHandToolOnLoad: false, michael@0: enableWebGL: false michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Preferences - Utility for storing persistent settings. michael@0: * Used for settings that should be applied to all opened documents, michael@0: * or every time the viewer is loaded. michael@0: */ michael@0: var Preferences = { michael@0: prefs: Object.create(DEFAULT_PREFERENCES), michael@0: isInitializedPromiseResolved: false, michael@0: initializedPromise: null, michael@0: michael@0: /** michael@0: * Initialize and fetch the current preference values from storage. michael@0: * @return {Promise} A promise that is resolved when the preferences michael@0: * have been initialized. michael@0: */ michael@0: initialize: function preferencesInitialize() { michael@0: return this.initializedPromise = michael@0: this._readFromStorage(DEFAULT_PREFERENCES).then(function(prefObj) { michael@0: this.isInitializedPromiseResolved = true; michael@0: if (prefObj) { michael@0: this.prefs = prefObj; michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Stub function for writing preferences to storage. michael@0: * NOTE: This should be overridden by a build-specific function defined below. michael@0: * @param {Object} prefObj The preferences that should be written to storage. michael@0: * @return {Promise} A promise that is resolved when the preference values michael@0: * have been written. michael@0: */ michael@0: _writeToStorage: function preferences_writeToStorage(prefObj) { michael@0: return Promise.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * Stub function for reading preferences from storage. michael@0: * NOTE: This should be overridden by a build-specific function defined below. michael@0: * @param {Object} prefObj The preferences that should be read from storage. michael@0: * @return {Promise} A promise that is resolved with an {Object} containing michael@0: * the preferences that have been read. michael@0: */ michael@0: _readFromStorage: function preferences_readFromStorage(prefObj) { michael@0: return Promise.resolve(); michael@0: }, michael@0: michael@0: /** michael@0: * Reset the preferences to their default values and update storage. michael@0: * @return {Promise} A promise that is resolved when the preference values michael@0: * have been reset. michael@0: */ michael@0: reset: function preferencesReset() { michael@0: return this.initializedPromise.then(function() { michael@0: this.prefs = Object.create(DEFAULT_PREFERENCES); michael@0: return this._writeToStorage(DEFAULT_PREFERENCES); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Replace the current preference values with the ones from storage. michael@0: * @return {Promise} A promise that is resolved when the preference values michael@0: * have been updated. michael@0: */ michael@0: reload: function preferencesReload() { michael@0: return this.initializedPromise.then(function () { michael@0: this._readFromStorage(DEFAULT_PREFERENCES).then(function(prefObj) { michael@0: if (prefObj) { michael@0: this.prefs = prefObj; michael@0: } michael@0: }.bind(this)); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Set the value of a preference. michael@0: * @param {string} name The name of the preference that should be changed. michael@0: * @param {boolean|number|string} value The new value of the preference. michael@0: * @return {Promise} A promise that is resolved when the value has been set, michael@0: * provided that the preference exists and the types match. michael@0: */ michael@0: set: function preferencesSet(name, value) { michael@0: return this.initializedPromise.then(function () { michael@0: if (DEFAULT_PREFERENCES[name] === undefined) { michael@0: throw new Error('preferencesSet: \'' + name + '\' is undefined.'); michael@0: } else if (value === undefined) { michael@0: throw new Error('preferencesSet: no value is specified.'); michael@0: } michael@0: var valueType = typeof value; michael@0: var defaultType = typeof DEFAULT_PREFERENCES[name]; michael@0: michael@0: if (valueType !== defaultType) { michael@0: if (valueType === 'number' && defaultType === 'string') { michael@0: value = value.toString(); michael@0: } else { michael@0: throw new Error('Preferences_set: \'' + value + '\' is a \"' + michael@0: valueType + '\", expected \"' + defaultType + '\".'); michael@0: } michael@0: } else { michael@0: if (valueType === 'number' && (value | 0) !== value) { michael@0: throw new Error('Preferences_set: \'' + value + michael@0: '\' must be an \"integer\".'); michael@0: } michael@0: } michael@0: this.prefs[name] = value; michael@0: return this._writeToStorage(this.prefs); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Get the value of a preference. michael@0: * @param {string} name The name of the preference whose value is requested. michael@0: * @return {Promise} A promise that is resolved with a {boolean|number|string} michael@0: * containing the value of the preference. michael@0: */ michael@0: get: function preferencesGet(name) { michael@0: return this.initializedPromise.then(function () { michael@0: var defaultValue = DEFAULT_PREFERENCES[name]; michael@0: michael@0: if (defaultValue === undefined) { michael@0: throw new Error('preferencesGet: \'' + name + '\' is undefined.'); michael@0: } else { michael@0: var prefValue = this.prefs[name]; michael@0: michael@0: if (prefValue !== undefined) { michael@0: return prefValue; michael@0: } michael@0: } michael@0: return defaultValue; michael@0: }.bind(this)); michael@0: } michael@0: }; michael@0: michael@0: michael@0: michael@0: michael@0: michael@0: michael@0: michael@0: var FirefoxCom = (function FirefoxComClosure() { michael@0: return { michael@0: /** michael@0: * Creates an event that the extension is listening for and will michael@0: * synchronously respond to. michael@0: * NOTE: It is reccomended to use request() instead since one day we may not michael@0: * be able to synchronously reply. michael@0: * @param {String} action The action to trigger. michael@0: * @param {String} data Optional data to send. michael@0: * @return {*} The response. michael@0: */ michael@0: requestSync: function(action, data) { michael@0: var request = document.createTextNode(''); michael@0: document.documentElement.appendChild(request); michael@0: michael@0: var sender = document.createEvent('CustomEvent'); michael@0: sender.initCustomEvent('pdf.js.message', true, false, michael@0: {action: action, data: data, sync: true}); michael@0: request.dispatchEvent(sender); michael@0: var response = sender.detail.response; michael@0: document.documentElement.removeChild(request); michael@0: return response; michael@0: }, michael@0: /** michael@0: * Creates an event that the extension is listening for and will michael@0: * asynchronously respond by calling the callback. michael@0: * @param {String} action The action to trigger. michael@0: * @param {String} data Optional data to send. michael@0: * @param {Function} callback Optional response callback that will be called michael@0: * with one data argument. michael@0: */ michael@0: request: function(action, data, callback) { michael@0: var request = document.createTextNode(''); michael@0: if (callback) { michael@0: document.addEventListener('pdf.js.response', function listener(event) { michael@0: var node = event.target; michael@0: var response = event.detail.response; michael@0: michael@0: document.documentElement.removeChild(node); michael@0: michael@0: document.removeEventListener('pdf.js.response', listener, false); michael@0: return callback(response); michael@0: }, false); michael@0: } michael@0: document.documentElement.appendChild(request); michael@0: michael@0: var sender = document.createEvent('CustomEvent'); michael@0: sender.initCustomEvent('pdf.js.message', true, false, michael@0: {action: action, data: data, sync: false, michael@0: callback: callback}); michael@0: return request.dispatchEvent(sender); michael@0: } michael@0: }; michael@0: })(); michael@0: michael@0: var DownloadManager = (function DownloadManagerClosure() { michael@0: function DownloadManager() {} michael@0: michael@0: DownloadManager.prototype = { michael@0: downloadUrl: function DownloadManager_downloadUrl(url, filename) { michael@0: FirefoxCom.request('download', { michael@0: originalUrl: url, michael@0: filename: filename michael@0: }); michael@0: }, michael@0: michael@0: downloadData: function DownloadManager_downloadData(data, filename, michael@0: contentType) { michael@0: var blobUrl = PDFJS.createObjectURL(data, contentType); michael@0: michael@0: FirefoxCom.request('download', { michael@0: blobUrl: blobUrl, michael@0: originalUrl: blobUrl, michael@0: filename: filename, michael@0: isAttachment: true michael@0: }); michael@0: }, michael@0: michael@0: download: function DownloadManager_download(blob, url, filename) { michael@0: var blobUrl = window.URL.createObjectURL(blob); michael@0: michael@0: FirefoxCom.request('download', { michael@0: blobUrl: blobUrl, michael@0: originalUrl: url, michael@0: filename: filename michael@0: }, michael@0: function response(err) { michael@0: if (err && this.onerror) { michael@0: this.onerror(err); michael@0: } michael@0: window.URL.revokeObjectURL(blobUrl); michael@0: }.bind(this) michael@0: ); michael@0: } michael@0: }; michael@0: michael@0: return DownloadManager; michael@0: })(); michael@0: michael@0: Preferences._writeToStorage = function (prefObj) { michael@0: return new Promise(function (resolve) { michael@0: FirefoxCom.request('setPreferences', prefObj, resolve); michael@0: }); michael@0: }; michael@0: michael@0: Preferences._readFromStorage = function (prefObj) { michael@0: return new Promise(function (resolve) { michael@0: FirefoxCom.request('getPreferences', prefObj, function (prefStr) { michael@0: var readPrefs = JSON.parse(prefStr); michael@0: resolve(readPrefs); michael@0: }); michael@0: }); michael@0: }; michael@0: michael@0: michael@0: michael@0: var cache = new Cache(CACHE_SIZE); michael@0: var currentPageNumber = 1; michael@0: michael@0: michael@0: /** michael@0: * View History - This is a utility for saving various view parameters for michael@0: * recently opened files. michael@0: * michael@0: * The way that the view parameters are stored depends on how PDF.js is built, michael@0: * for 'node make ' the following cases exist: michael@0: * - FIREFOX or MOZCENTRAL - uses sessionStorage. michael@0: * - B2G - uses asyncStorage. michael@0: * - GENERIC or CHROME - uses localStorage, if it is available. michael@0: */ michael@0: var ViewHistory = (function ViewHistoryClosure() { michael@0: function ViewHistory(fingerprint) { michael@0: this.fingerprint = fingerprint; michael@0: var initializedPromiseResolve; michael@0: this.isInitializedPromiseResolved = false; michael@0: this.initializedPromise = new Promise(function (resolve) { michael@0: initializedPromiseResolve = resolve; michael@0: }); michael@0: michael@0: var resolvePromise = (function ViewHistoryResolvePromise(db) { michael@0: this.isInitializedPromiseResolved = true; michael@0: this.initialize(db || '{}'); michael@0: initializedPromiseResolve(); michael@0: }).bind(this); michael@0: michael@0: michael@0: var sessionHistory; michael@0: try { michael@0: // Workaround for security error when the preference michael@0: // network.cookie.lifetimePolicy is set to 1, see Mozilla Bug 365772. michael@0: sessionHistory = sessionStorage.getItem('pdfjsHistory'); michael@0: } catch (ex) {} michael@0: resolvePromise(sessionHistory); michael@0: michael@0: } michael@0: michael@0: ViewHistory.prototype = { michael@0: initialize: function ViewHistory_initialize(database) { michael@0: database = JSON.parse(database); michael@0: if (!('files' in database)) { michael@0: database.files = []; michael@0: } michael@0: if (database.files.length >= VIEW_HISTORY_MEMORY) { michael@0: database.files.shift(); michael@0: } michael@0: var index; michael@0: for (var i = 0, length = database.files.length; i < length; i++) { michael@0: var branch = database.files[i]; michael@0: if (branch.fingerprint === this.fingerprint) { michael@0: index = i; michael@0: break; michael@0: } michael@0: } michael@0: if (typeof index !== 'number') { michael@0: index = database.files.push({fingerprint: this.fingerprint}) - 1; michael@0: } michael@0: this.file = database.files[index]; michael@0: this.database = database; michael@0: }, michael@0: michael@0: set: function ViewHistory_set(name, val) { michael@0: if (!this.isInitializedPromiseResolved) { michael@0: return; michael@0: } michael@0: var file = this.file; michael@0: file[name] = val; michael@0: var database = JSON.stringify(this.database); michael@0: michael@0: michael@0: try { michael@0: // See comment in try-catch block above. michael@0: sessionStorage.setItem('pdfjsHistory', database); michael@0: } catch (ex) {} michael@0: michael@0: }, michael@0: michael@0: get: function ViewHistory_get(name, defaultValue) { michael@0: if (!this.isInitializedPromiseResolved) { michael@0: return defaultValue; michael@0: } michael@0: return this.file[name] || defaultValue; michael@0: } michael@0: }; michael@0: michael@0: return ViewHistory; michael@0: })(); michael@0: michael@0: michael@0: /** michael@0: * Creates a "search bar" given set of DOM elements michael@0: * that act as controls for searching, or for setting michael@0: * search preferences in the UI. This object also sets michael@0: * up the appropriate events for the controls. Actual michael@0: * searching is done by PDFFindController michael@0: */ michael@0: var PDFFindBar = { michael@0: opened: false, michael@0: bar: null, michael@0: toggleButton: null, michael@0: findField: null, michael@0: highlightAll: null, michael@0: caseSensitive: null, michael@0: findMsg: null, michael@0: findStatusIcon: null, michael@0: findPreviousButton: null, michael@0: findNextButton: null, michael@0: michael@0: initialize: function(options) { michael@0: if(typeof PDFFindController === 'undefined' || PDFFindController === null) { michael@0: throw 'PDFFindBar cannot be initialized ' + michael@0: 'without a PDFFindController instance.'; michael@0: } michael@0: michael@0: this.bar = options.bar; michael@0: this.toggleButton = options.toggleButton; michael@0: this.findField = options.findField; michael@0: this.highlightAll = options.highlightAllCheckbox; michael@0: this.caseSensitive = options.caseSensitiveCheckbox; michael@0: this.findMsg = options.findMsg; michael@0: this.findStatusIcon = options.findStatusIcon; michael@0: this.findPreviousButton = options.findPreviousButton; michael@0: this.findNextButton = options.findNextButton; michael@0: michael@0: var self = this; michael@0: this.toggleButton.addEventListener('click', function() { michael@0: self.toggle(); michael@0: }); michael@0: michael@0: this.findField.addEventListener('input', function() { michael@0: self.dispatchEvent(''); michael@0: }); michael@0: michael@0: this.bar.addEventListener('keydown', function(evt) { michael@0: switch (evt.keyCode) { michael@0: case 13: // Enter michael@0: if (evt.target === self.findField) { michael@0: self.dispatchEvent('again', evt.shiftKey); michael@0: } michael@0: break; michael@0: case 27: // Escape michael@0: self.close(); michael@0: break; michael@0: } michael@0: }); michael@0: michael@0: this.findPreviousButton.addEventListener('click', michael@0: function() { self.dispatchEvent('again', true); } michael@0: ); michael@0: michael@0: this.findNextButton.addEventListener('click', function() { michael@0: self.dispatchEvent('again', false); michael@0: }); michael@0: michael@0: this.highlightAll.addEventListener('click', function() { michael@0: self.dispatchEvent('highlightallchange'); michael@0: }); michael@0: michael@0: this.caseSensitive.addEventListener('click', function() { michael@0: self.dispatchEvent('casesensitivitychange'); michael@0: }); michael@0: }, michael@0: michael@0: dispatchEvent: function(aType, aFindPrevious) { michael@0: var event = document.createEvent('CustomEvent'); michael@0: event.initCustomEvent('find' + aType, true, true, { michael@0: query: this.findField.value, michael@0: caseSensitive: this.caseSensitive.checked, michael@0: highlightAll: this.highlightAll.checked, michael@0: findPrevious: aFindPrevious michael@0: }); michael@0: return window.dispatchEvent(event); michael@0: }, michael@0: michael@0: updateUIState: function(state, previous) { michael@0: var notFound = false; michael@0: var findMsg = ''; michael@0: var status = ''; michael@0: michael@0: switch (state) { michael@0: case FindStates.FIND_FOUND: michael@0: break; michael@0: michael@0: case FindStates.FIND_PENDING: michael@0: status = 'pending'; michael@0: break; michael@0: michael@0: case FindStates.FIND_NOTFOUND: michael@0: findMsg = mozL10n.get('find_not_found', null, 'Phrase not found'); michael@0: notFound = true; michael@0: break; michael@0: michael@0: case FindStates.FIND_WRAPPED: michael@0: if (previous) { michael@0: findMsg = mozL10n.get('find_reached_top', null, michael@0: 'Reached top of document, continued from bottom'); michael@0: } else { michael@0: findMsg = mozL10n.get('find_reached_bottom', null, michael@0: 'Reached end of document, continued from top'); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: if (notFound) { michael@0: this.findField.classList.add('notFound'); michael@0: } else { michael@0: this.findField.classList.remove('notFound'); michael@0: } michael@0: michael@0: this.findField.setAttribute('data-status', status); michael@0: this.findMsg.textContent = findMsg; michael@0: }, michael@0: michael@0: open: function() { michael@0: if (!this.opened) { michael@0: this.opened = true; michael@0: this.toggleButton.classList.add('toggled'); michael@0: this.bar.classList.remove('hidden'); michael@0: } michael@0: michael@0: this.findField.select(); michael@0: this.findField.focus(); michael@0: }, michael@0: michael@0: close: function() { michael@0: if (!this.opened) { michael@0: return; michael@0: } michael@0: this.opened = false; michael@0: this.toggleButton.classList.remove('toggled'); michael@0: this.bar.classList.add('hidden'); michael@0: michael@0: PDFFindController.active = false; michael@0: }, michael@0: michael@0: toggle: function() { michael@0: if (this.opened) { michael@0: this.close(); michael@0: } else { michael@0: this.open(); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: michael@0: michael@0: /** michael@0: * Provides a "search" or "find" functionality for the PDF. michael@0: * This object actually performs the search for a given string. michael@0: */ michael@0: michael@0: var PDFFindController = { michael@0: startedTextExtraction: false, michael@0: michael@0: extractTextPromises: [], michael@0: michael@0: pendingFindMatches: {}, michael@0: michael@0: // If active, find results will be highlighted. michael@0: active: false, michael@0: michael@0: // Stores the text for each page. michael@0: pageContents: [], michael@0: michael@0: pageMatches: [], michael@0: michael@0: // Currently selected match. michael@0: selected: { michael@0: pageIdx: -1, michael@0: matchIdx: -1 michael@0: }, michael@0: michael@0: // Where find algorithm currently is in the document. michael@0: offset: { michael@0: pageIdx: null, michael@0: matchIdx: null michael@0: }, michael@0: michael@0: resumePageIdx: null, michael@0: michael@0: state: null, michael@0: michael@0: dirtyMatch: false, michael@0: michael@0: findTimeout: null, michael@0: michael@0: pdfPageSource: null, michael@0: michael@0: integratedFind: false, michael@0: michael@0: initialize: function(options) { michael@0: if(typeof PDFFindBar === 'undefined' || PDFFindBar === null) { michael@0: throw 'PDFFindController cannot be initialized ' + michael@0: 'without a PDFFindBar instance'; michael@0: } michael@0: michael@0: this.pdfPageSource = options.pdfPageSource; michael@0: this.integratedFind = options.integratedFind; michael@0: michael@0: var events = [ michael@0: 'find', michael@0: 'findagain', michael@0: 'findhighlightallchange', michael@0: 'findcasesensitivitychange' michael@0: ]; michael@0: michael@0: this.firstPagePromise = new Promise(function (resolve) { michael@0: this.resolveFirstPage = resolve; michael@0: }.bind(this)); michael@0: this.handleEvent = this.handleEvent.bind(this); michael@0: michael@0: for (var i = 0; i < events.length; i++) { michael@0: window.addEventListener(events[i], this.handleEvent); michael@0: } michael@0: }, michael@0: michael@0: reset: function pdfFindControllerReset() { michael@0: this.startedTextExtraction = false; michael@0: this.extractTextPromises = []; michael@0: this.active = false; michael@0: }, michael@0: michael@0: calcFindMatch: function(pageIndex) { michael@0: var pageContent = this.pageContents[pageIndex]; michael@0: var query = this.state.query; michael@0: var caseSensitive = this.state.caseSensitive; michael@0: var queryLen = query.length; michael@0: michael@0: if (queryLen === 0) { michael@0: // Do nothing the matches should be wiped out already. michael@0: return; michael@0: } michael@0: michael@0: if (!caseSensitive) { michael@0: pageContent = pageContent.toLowerCase(); michael@0: query = query.toLowerCase(); michael@0: } michael@0: michael@0: var matches = []; michael@0: michael@0: var matchIdx = -queryLen; michael@0: while (true) { michael@0: matchIdx = pageContent.indexOf(query, matchIdx + queryLen); michael@0: if (matchIdx === -1) { michael@0: break; michael@0: } michael@0: michael@0: matches.push(matchIdx); michael@0: } michael@0: this.pageMatches[pageIndex] = matches; michael@0: this.updatePage(pageIndex); michael@0: if (this.resumePageIdx === pageIndex) { michael@0: this.resumePageIdx = null; michael@0: this.nextPageMatch(); michael@0: } michael@0: }, michael@0: michael@0: extractText: function() { michael@0: if (this.startedTextExtraction) { michael@0: return; michael@0: } michael@0: this.startedTextExtraction = true; michael@0: michael@0: this.pageContents = []; michael@0: var extractTextPromisesResolves = []; michael@0: for (var i = 0, ii = this.pdfPageSource.pdfDocument.numPages; i < ii; i++) { michael@0: this.extractTextPromises.push(new Promise(function (resolve) { michael@0: extractTextPromisesResolves.push(resolve); michael@0: })); michael@0: } michael@0: michael@0: var self = this; michael@0: function extractPageText(pageIndex) { michael@0: self.pdfPageSource.pages[pageIndex].getTextContent().then( michael@0: function textContentResolved(textContent) { michael@0: var textItems = textContent.items; michael@0: var str = ''; michael@0: michael@0: for (var i = 0; i < textItems.length; i++) { michael@0: str += textItems[i].str; michael@0: } michael@0: michael@0: // Store the pageContent as a string. michael@0: self.pageContents.push(str); michael@0: michael@0: extractTextPromisesResolves[pageIndex](pageIndex); michael@0: if ((pageIndex + 1) < self.pdfPageSource.pages.length) { michael@0: extractPageText(pageIndex + 1); michael@0: } michael@0: } michael@0: ); michael@0: } michael@0: extractPageText(0); michael@0: }, michael@0: michael@0: handleEvent: function(e) { michael@0: if (this.state === null || e.type !== 'findagain') { michael@0: this.dirtyMatch = true; michael@0: } michael@0: this.state = e.detail; michael@0: this.updateUIState(FindStates.FIND_PENDING); michael@0: michael@0: this.firstPagePromise.then(function() { michael@0: this.extractText(); michael@0: michael@0: clearTimeout(this.findTimeout); michael@0: if (e.type === 'find') { michael@0: // Only trigger the find action after 250ms of silence. michael@0: this.findTimeout = setTimeout(this.nextMatch.bind(this), 250); michael@0: } else { michael@0: this.nextMatch(); michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: updatePage: function(idx) { michael@0: var page = this.pdfPageSource.pages[idx]; michael@0: michael@0: if (this.selected.pageIdx === idx) { michael@0: // If the page is selected, scroll the page into view, which triggers michael@0: // rendering the page, which adds the textLayer. Once the textLayer is michael@0: // build, it will scroll onto the selected match. michael@0: page.scrollIntoView(); michael@0: } michael@0: michael@0: if (page.textLayer) { michael@0: page.textLayer.updateMatches(); michael@0: } michael@0: }, michael@0: michael@0: nextMatch: function() { michael@0: var previous = this.state.findPrevious; michael@0: var currentPageIndex = this.pdfPageSource.page - 1; michael@0: var numPages = this.pdfPageSource.pages.length; michael@0: michael@0: this.active = true; michael@0: michael@0: if (this.dirtyMatch) { michael@0: // Need to recalculate the matches, reset everything. michael@0: this.dirtyMatch = false; michael@0: this.selected.pageIdx = this.selected.matchIdx = -1; michael@0: this.offset.pageIdx = currentPageIndex; michael@0: this.offset.matchIdx = null; michael@0: this.hadMatch = false; michael@0: this.resumePageIdx = null; michael@0: this.pageMatches = []; michael@0: var self = this; michael@0: michael@0: for (var i = 0; i < numPages; i++) { michael@0: // Wipe out any previous highlighted matches. michael@0: this.updatePage(i); michael@0: michael@0: // As soon as the text is extracted start finding the matches. michael@0: if (!(i in this.pendingFindMatches)) { michael@0: this.pendingFindMatches[i] = true; michael@0: this.extractTextPromises[i].then(function(pageIdx) { michael@0: delete self.pendingFindMatches[pageIdx]; michael@0: self.calcFindMatch(pageIdx); michael@0: }); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // If there's no query there's no point in searching. michael@0: if (this.state.query === '') { michael@0: this.updateUIState(FindStates.FIND_FOUND); michael@0: return; michael@0: } michael@0: michael@0: // If we're waiting on a page, we return since we can't do anything else. michael@0: if (this.resumePageIdx) { michael@0: return; michael@0: } michael@0: michael@0: var offset = this.offset; michael@0: // If there's already a matchIdx that means we are iterating through a michael@0: // page's matches. michael@0: if (offset.matchIdx !== null) { michael@0: var numPageMatches = this.pageMatches[offset.pageIdx].length; michael@0: if ((!previous && offset.matchIdx + 1 < numPageMatches) || michael@0: (previous && offset.matchIdx > 0)) { michael@0: // The simple case, we just have advance the matchIdx to select the next michael@0: // match on the page. michael@0: this.hadMatch = true; michael@0: offset.matchIdx = previous ? offset.matchIdx - 1 : offset.matchIdx + 1; michael@0: this.updateMatch(true); michael@0: return; michael@0: } michael@0: // We went beyond the current page's matches, so we advance to the next michael@0: // page. michael@0: this.advanceOffsetPage(previous); michael@0: } michael@0: // Start searching through the page. michael@0: this.nextPageMatch(); michael@0: }, michael@0: michael@0: matchesReady: function(matches) { michael@0: var offset = this.offset; michael@0: var numMatches = matches.length; michael@0: var previous = this.state.findPrevious; michael@0: if (numMatches) { michael@0: // There were matches for the page, so initialize the matchIdx. michael@0: this.hadMatch = true; michael@0: offset.matchIdx = previous ? numMatches - 1 : 0; michael@0: this.updateMatch(true); michael@0: // matches were found michael@0: return true; michael@0: } else { michael@0: // No matches attempt to search the next page. michael@0: this.advanceOffsetPage(previous); michael@0: if (offset.wrapped) { michael@0: offset.matchIdx = null; michael@0: if (!this.hadMatch) { michael@0: // No point in wrapping there were no matches. michael@0: this.updateMatch(false); michael@0: // while matches were not found, searching for a page michael@0: // with matches should nevertheless halt. michael@0: return true; michael@0: } michael@0: } michael@0: // matches were not found (and searching is not done) michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: nextPageMatch: function() { michael@0: if (this.resumePageIdx !== null) { michael@0: console.error('There can only be one pending page.'); michael@0: } michael@0: do { michael@0: var pageIdx = this.offset.pageIdx; michael@0: var matches = this.pageMatches[pageIdx]; michael@0: if (!matches) { michael@0: // The matches don't exist yet for processing by "matchesReady", michael@0: // so set a resume point for when they do exist. michael@0: this.resumePageIdx = pageIdx; michael@0: break; michael@0: } michael@0: } while (!this.matchesReady(matches)); michael@0: }, michael@0: michael@0: advanceOffsetPage: function(previous) { michael@0: var offset = this.offset; michael@0: var numPages = this.extractTextPromises.length; michael@0: offset.pageIdx = previous ? offset.pageIdx - 1 : offset.pageIdx + 1; michael@0: offset.matchIdx = null; michael@0: if (offset.pageIdx >= numPages || offset.pageIdx < 0) { michael@0: offset.pageIdx = previous ? numPages - 1 : 0; michael@0: offset.wrapped = true; michael@0: return; michael@0: } michael@0: }, michael@0: michael@0: updateMatch: function(found) { michael@0: var state = FindStates.FIND_NOTFOUND; michael@0: var wrapped = this.offset.wrapped; michael@0: this.offset.wrapped = false; michael@0: if (found) { michael@0: var previousPage = this.selected.pageIdx; michael@0: this.selected.pageIdx = this.offset.pageIdx; michael@0: this.selected.matchIdx = this.offset.matchIdx; michael@0: state = wrapped ? FindStates.FIND_WRAPPED : FindStates.FIND_FOUND; michael@0: // Update the currently selected page to wipe out any selected matches. michael@0: if (previousPage !== -1 && previousPage !== this.selected.pageIdx) { michael@0: this.updatePage(previousPage); michael@0: } michael@0: } michael@0: this.updateUIState(state, this.state.findPrevious); michael@0: if (this.selected.pageIdx !== -1) { michael@0: this.updatePage(this.selected.pageIdx, true); michael@0: } michael@0: }, michael@0: michael@0: updateUIState: function(state, previous) { michael@0: if (this.integratedFind) { michael@0: FirefoxCom.request('updateFindControlState', michael@0: {result: state, findPrevious: previous}); michael@0: return; michael@0: } michael@0: PDFFindBar.updateUIState(state, previous); michael@0: } michael@0: }; michael@0: michael@0: michael@0: michael@0: var PDFHistory = { michael@0: initialized: false, michael@0: initialDestination: null, michael@0: michael@0: initialize: function pdfHistoryInitialize(fingerprint) { michael@0: if (PDFJS.disableHistory || PDFView.isViewerEmbedded) { michael@0: // The browsing history is only enabled when the viewer is standalone, michael@0: // i.e. not when it is embedded in a web page. michael@0: return; michael@0: } michael@0: this.initialized = true; michael@0: this.reInitialized = false; michael@0: this.allowHashChange = true; michael@0: this.historyUnlocked = true; michael@0: michael@0: this.previousHash = window.location.hash.substring(1); michael@0: this.currentBookmark = ''; michael@0: this.currentPage = 0; michael@0: this.updatePreviousBookmark = false; michael@0: this.previousBookmark = ''; michael@0: this.previousPage = 0; michael@0: this.nextHashParam = ''; michael@0: michael@0: this.fingerprint = fingerprint; michael@0: this.currentUid = this.uid = 0; michael@0: this.current = {}; michael@0: michael@0: var state = window.history.state; michael@0: if (this._isStateObjectDefined(state)) { michael@0: // This corresponds to navigating back to the document michael@0: // from another page in the browser history. michael@0: if (state.target.dest) { michael@0: this.initialDestination = state.target.dest; michael@0: } else { michael@0: PDFView.initialBookmark = state.target.hash; michael@0: } michael@0: this.currentUid = state.uid; michael@0: this.uid = state.uid + 1; michael@0: this.current = state.target; michael@0: } else { michael@0: // This corresponds to the loading of a new document. michael@0: if (state && state.fingerprint && michael@0: this.fingerprint !== state.fingerprint) { michael@0: // Reinitialize the browsing history when a new document michael@0: // is opened in the web viewer. michael@0: this.reInitialized = true; michael@0: } michael@0: this._pushOrReplaceState({ fingerprint: this.fingerprint }, true); michael@0: } michael@0: michael@0: var self = this; michael@0: window.addEventListener('popstate', function pdfHistoryPopstate(evt) { michael@0: evt.preventDefault(); michael@0: evt.stopPropagation(); michael@0: michael@0: if (!self.historyUnlocked) { michael@0: return; michael@0: } michael@0: if (evt.state) { michael@0: // Move back/forward in the history. michael@0: self._goTo(evt.state); michael@0: } else { michael@0: // Handle the user modifying the hash of a loaded document. michael@0: self.previousHash = window.location.hash.substring(1); michael@0: michael@0: // If the history is empty when the hash changes, michael@0: // update the previous entry in the browser history. michael@0: if (self.uid === 0) { michael@0: var previousParams = (self.previousHash && self.currentBookmark && michael@0: self.previousHash !== self.currentBookmark) ? michael@0: { hash: self.currentBookmark, page: self.currentPage } : michael@0: { page: 1 }; michael@0: self.historyUnlocked = false; michael@0: self.allowHashChange = false; michael@0: window.history.back(); michael@0: self._pushToHistory(previousParams, false, true); michael@0: window.history.forward(); michael@0: self.historyUnlocked = true; michael@0: } michael@0: self._pushToHistory({ hash: self.previousHash }, false, true); michael@0: self._updatePreviousBookmark(); michael@0: } michael@0: }, false); michael@0: michael@0: function pdfHistoryBeforeUnload() { michael@0: var previousParams = self._getPreviousParams(null, true); michael@0: if (previousParams) { michael@0: var replacePrevious = (!self.current.dest && michael@0: self.current.hash !== self.previousHash); michael@0: self._pushToHistory(previousParams, false, replacePrevious); michael@0: self._updatePreviousBookmark(); michael@0: } michael@0: // Remove the event listener when navigating away from the document, michael@0: // since 'beforeunload' prevents Firefox from caching the document. michael@0: window.removeEventListener('beforeunload', pdfHistoryBeforeUnload, false); michael@0: } michael@0: window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false); michael@0: michael@0: window.addEventListener('pageshow', function pdfHistoryPageShow(evt) { michael@0: // If the entire viewer (including the PDF file) is cached in the browser, michael@0: // we need to reattach the 'beforeunload' event listener since michael@0: // the 'DOMContentLoaded' event is not fired on 'pageshow'. michael@0: window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false); michael@0: }, false); michael@0: }, michael@0: michael@0: _isStateObjectDefined: function pdfHistory_isStateObjectDefined(state) { michael@0: return (state && state.uid >= 0 && michael@0: state.fingerprint && this.fingerprint === state.fingerprint && michael@0: state.target && state.target.hash) ? true : false; michael@0: }, michael@0: michael@0: _pushOrReplaceState: function pdfHistory_pushOrReplaceState(stateObj, michael@0: replace) { michael@0: if (replace) { michael@0: window.history.replaceState(stateObj, ''); michael@0: } else { michael@0: window.history.pushState(stateObj, ''); michael@0: } michael@0: }, michael@0: michael@0: get isHashChangeUnlocked() { michael@0: if (!this.initialized) { michael@0: return true; michael@0: } michael@0: // If the current hash changes when moving back/forward in the history, michael@0: // this will trigger a 'popstate' event *as well* as a 'hashchange' event. michael@0: // Since the hash generally won't correspond to the exact the position michael@0: // stored in the history's state object, triggering the 'hashchange' event michael@0: // can thus corrupt the browser history. michael@0: // michael@0: // When the hash changes during a 'popstate' event, we *only* prevent the michael@0: // first 'hashchange' event and immediately reset allowHashChange. michael@0: // If it is not reset, the user would not be able to change the hash. michael@0: michael@0: var temp = this.allowHashChange; michael@0: this.allowHashChange = true; michael@0: return temp; michael@0: }, michael@0: michael@0: _updatePreviousBookmark: function pdfHistory_updatePreviousBookmark() { michael@0: if (this.updatePreviousBookmark && michael@0: this.currentBookmark && this.currentPage) { michael@0: this.previousBookmark = this.currentBookmark; michael@0: this.previousPage = this.currentPage; michael@0: this.updatePreviousBookmark = false; michael@0: } michael@0: }, michael@0: michael@0: updateCurrentBookmark: function pdfHistoryUpdateCurrentBookmark(bookmark, michael@0: pageNum) { michael@0: if (this.initialized) { michael@0: this.currentBookmark = bookmark.substring(1); michael@0: this.currentPage = pageNum | 0; michael@0: this._updatePreviousBookmark(); michael@0: } michael@0: }, michael@0: michael@0: updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) { michael@0: if (this.initialized) { michael@0: this.nextHashParam = param; michael@0: } michael@0: }, michael@0: michael@0: push: function pdfHistoryPush(params, isInitialBookmark) { michael@0: if (!(this.initialized && this.historyUnlocked)) { michael@0: return; michael@0: } michael@0: if (params.dest && !params.hash) { michael@0: params.hash = (this.current.hash && this.current.dest && michael@0: this.current.dest === params.dest) ? michael@0: this.current.hash : michael@0: PDFView.getDestinationHash(params.dest).split('#')[1]; michael@0: } michael@0: if (params.page) { michael@0: params.page |= 0; michael@0: } michael@0: if (isInitialBookmark) { michael@0: var target = window.history.state.target; michael@0: if (!target) { michael@0: // Invoked when the user specifies an initial bookmark, michael@0: // thus setting PDFView.initialBookmark, when the document is loaded. michael@0: this._pushToHistory(params, false); michael@0: this.previousHash = window.location.hash.substring(1); michael@0: } michael@0: this.updatePreviousBookmark = this.nextHashParam ? false : true; michael@0: if (target) { michael@0: // If the current document is reloaded, michael@0: // avoid creating duplicate entries in the history. michael@0: this._updatePreviousBookmark(); michael@0: } michael@0: return; michael@0: } michael@0: if (this.nextHashParam) { michael@0: if (this.nextHashParam === params.hash) { michael@0: this.nextHashParam = null; michael@0: this.updatePreviousBookmark = true; michael@0: return; michael@0: } else { michael@0: this.nextHashParam = null; michael@0: } michael@0: } michael@0: michael@0: if (params.hash) { michael@0: if (this.current.hash) { michael@0: if (this.current.hash !== params.hash) { michael@0: this._pushToHistory(params, true); michael@0: } else { michael@0: if (!this.current.page && params.page) { michael@0: this._pushToHistory(params, false, true); michael@0: } michael@0: this.updatePreviousBookmark = true; michael@0: } michael@0: } else { michael@0: this._pushToHistory(params, true); michael@0: } michael@0: } else if (this.current.page && params.page && michael@0: this.current.page !== params.page) { michael@0: this._pushToHistory(params, true); michael@0: } michael@0: }, michael@0: michael@0: _getPreviousParams: function pdfHistory_getPreviousParams(onlyCheckPage, michael@0: beforeUnload) { michael@0: if (!(this.currentBookmark && this.currentPage)) { michael@0: return null; michael@0: } else if (this.updatePreviousBookmark) { michael@0: this.updatePreviousBookmark = false; michael@0: } michael@0: if (this.uid > 0 && !(this.previousBookmark && this.previousPage)) { michael@0: // Prevent the history from getting stuck in the current state, michael@0: // effectively preventing the user from going back/forward in the history. michael@0: // michael@0: // This happens if the current position in the document didn't change when michael@0: // the history was previously updated. The reasons for this are either: michael@0: // 1. The current zoom value is such that the document does not need to, michael@0: // or cannot, be scrolled to display the destination. michael@0: // 2. The previous destination is broken, and doesn't actally point to a michael@0: // position within the document. michael@0: // (This is either due to a bad PDF generator, or the user making a michael@0: // mistake when entering a destination in the hash parameters.) michael@0: return null; michael@0: } michael@0: if ((!this.current.dest && !onlyCheckPage) || beforeUnload) { michael@0: if (this.previousBookmark === this.currentBookmark) { michael@0: return null; michael@0: } michael@0: } else if (this.current.page || onlyCheckPage) { michael@0: if (this.previousPage === this.currentPage) { michael@0: return null; michael@0: } michael@0: } else { michael@0: return null; michael@0: } michael@0: var params = { hash: this.currentBookmark, page: this.currentPage }; michael@0: if (PresentationMode.active) { michael@0: params.hash = null; michael@0: } michael@0: return params; michael@0: }, michael@0: michael@0: _stateObj: function pdfHistory_stateObj(params) { michael@0: return { fingerprint: this.fingerprint, uid: this.uid, target: params }; michael@0: }, michael@0: michael@0: _pushToHistory: function pdfHistory_pushToHistory(params, michael@0: addPrevious, overwrite) { michael@0: if (!this.initialized) { michael@0: return; michael@0: } michael@0: if (!params.hash && params.page) { michael@0: params.hash = ('page=' + params.page); michael@0: } michael@0: if (addPrevious && !overwrite) { michael@0: var previousParams = this._getPreviousParams(); michael@0: if (previousParams) { michael@0: var replacePrevious = (!this.current.dest && michael@0: this.current.hash !== this.previousHash); michael@0: this._pushToHistory(previousParams, false, replacePrevious); michael@0: } michael@0: } michael@0: this._pushOrReplaceState(this._stateObj(params), michael@0: (overwrite || this.uid === 0)); michael@0: this.currentUid = this.uid++; michael@0: this.current = params; michael@0: this.updatePreviousBookmark = true; michael@0: }, michael@0: michael@0: _goTo: function pdfHistory_goTo(state) { michael@0: if (!(this.initialized && this.historyUnlocked && michael@0: this._isStateObjectDefined(state))) { michael@0: return; michael@0: } michael@0: if (!this.reInitialized && state.uid < this.currentUid) { michael@0: var previousParams = this._getPreviousParams(true); michael@0: if (previousParams) { michael@0: this._pushToHistory(this.current, false); michael@0: this._pushToHistory(previousParams, false); michael@0: this.currentUid = state.uid; michael@0: window.history.back(); michael@0: return; michael@0: } michael@0: } michael@0: this.historyUnlocked = false; michael@0: michael@0: if (state.target.dest) { michael@0: PDFView.navigateTo(state.target.dest); michael@0: } else { michael@0: PDFView.setHash(state.target.hash); michael@0: } michael@0: this.currentUid = state.uid; michael@0: if (state.uid > this.uid) { michael@0: this.uid = state.uid; michael@0: } michael@0: this.current = state.target; michael@0: this.updatePreviousBookmark = true; michael@0: michael@0: var currentHash = window.location.hash.substring(1); michael@0: if (this.previousHash !== currentHash) { michael@0: this.allowHashChange = false; michael@0: } michael@0: this.previousHash = currentHash; michael@0: michael@0: this.historyUnlocked = true; michael@0: }, michael@0: michael@0: back: function pdfHistoryBack() { michael@0: this.go(-1); michael@0: }, michael@0: michael@0: forward: function pdfHistoryForward() { michael@0: this.go(1); michael@0: }, michael@0: michael@0: go: function pdfHistoryGo(direction) { michael@0: if (this.initialized && this.historyUnlocked) { michael@0: var state = window.history.state; michael@0: if (direction === -1 && state && state.uid > 0) { michael@0: window.history.back(); michael@0: } else if (direction === 1 && state && state.uid < (this.uid - 1)) { michael@0: window.history.forward(); michael@0: } michael@0: } michael@0: } michael@0: }; michael@0: michael@0: michael@0: var SecondaryToolbar = { michael@0: opened: false, michael@0: previousContainerHeight: null, michael@0: newContainerHeight: null, michael@0: michael@0: initialize: function secondaryToolbarInitialize(options) { michael@0: this.toolbar = options.toolbar; michael@0: this.presentationMode = options.presentationMode; michael@0: this.documentProperties = options.documentProperties; michael@0: this.buttonContainer = this.toolbar.firstElementChild; michael@0: michael@0: // Define the toolbar buttons. michael@0: this.toggleButton = options.toggleButton; michael@0: this.presentationModeButton = options.presentationModeButton; michael@0: this.openFile = options.openFile; michael@0: this.print = options.print; michael@0: this.download = options.download; michael@0: this.viewBookmark = options.viewBookmark; michael@0: this.firstPage = options.firstPage; michael@0: this.lastPage = options.lastPage; michael@0: this.pageRotateCw = options.pageRotateCw; michael@0: this.pageRotateCcw = options.pageRotateCcw; michael@0: this.documentPropertiesButton = options.documentPropertiesButton; michael@0: michael@0: // Attach the event listeners. michael@0: var elements = [ michael@0: // Button to toggle the visibility of the secondary toolbar: michael@0: { element: this.toggleButton, handler: this.toggle }, michael@0: // All items within the secondary toolbar michael@0: // (except for toggleHandTool, hand_tool.js is responsible for it): michael@0: { element: this.presentationModeButton, michael@0: handler: this.presentationModeClick }, michael@0: { element: this.openFile, handler: this.openFileClick }, michael@0: { element: this.print, handler: this.printClick }, michael@0: { element: this.download, handler: this.downloadClick }, michael@0: { element: this.viewBookmark, handler: this.viewBookmarkClick }, michael@0: { element: this.firstPage, handler: this.firstPageClick }, michael@0: { element: this.lastPage, handler: this.lastPageClick }, michael@0: { element: this.pageRotateCw, handler: this.pageRotateCwClick }, michael@0: { element: this.pageRotateCcw, handler: this.pageRotateCcwClick }, michael@0: { element: this.documentPropertiesButton, michael@0: handler: this.documentPropertiesClick } michael@0: ]; michael@0: michael@0: for (var item in elements) { michael@0: var element = elements[item].element; michael@0: if (element) { michael@0: element.addEventListener('click', elements[item].handler.bind(this)); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: // Event handling functions. michael@0: presentationModeClick: function secondaryToolbarPresentationModeClick(evt) { michael@0: this.presentationMode.request(); michael@0: this.close(); michael@0: }, michael@0: michael@0: openFileClick: function secondaryToolbarOpenFileClick(evt) { michael@0: document.getElementById('fileInput').click(); michael@0: this.close(); michael@0: }, michael@0: michael@0: printClick: function secondaryToolbarPrintClick(evt) { michael@0: window.print(); michael@0: this.close(); michael@0: }, michael@0: michael@0: downloadClick: function secondaryToolbarDownloadClick(evt) { michael@0: PDFView.download(); michael@0: this.close(); michael@0: }, michael@0: michael@0: viewBookmarkClick: function secondaryToolbarViewBookmarkClick(evt) { michael@0: this.close(); michael@0: }, michael@0: michael@0: firstPageClick: function secondaryToolbarFirstPageClick(evt) { michael@0: PDFView.page = 1; michael@0: this.close(); michael@0: }, michael@0: michael@0: lastPageClick: function secondaryToolbarLastPageClick(evt) { michael@0: PDFView.page = PDFView.pdfDocument.numPages; michael@0: this.close(); michael@0: }, michael@0: michael@0: pageRotateCwClick: function secondaryToolbarPageRotateCwClick(evt) { michael@0: PDFView.rotatePages(90); michael@0: }, michael@0: michael@0: pageRotateCcwClick: function secondaryToolbarPageRotateCcwClick(evt) { michael@0: PDFView.rotatePages(-90); michael@0: }, michael@0: michael@0: documentPropertiesClick: function secondaryToolbarDocumentPropsClick(evt) { michael@0: this.documentProperties.show(); michael@0: this.close(); michael@0: }, michael@0: michael@0: // Misc. functions for interacting with the toolbar. michael@0: setMaxHeight: function secondaryToolbarSetMaxHeight(container) { michael@0: if (!container || !this.buttonContainer) { michael@0: return; michael@0: } michael@0: this.newContainerHeight = container.clientHeight; michael@0: if (this.previousContainerHeight === this.newContainerHeight) { michael@0: return; michael@0: } michael@0: this.buttonContainer.setAttribute('style', michael@0: 'max-height: ' + (this.newContainerHeight - SCROLLBAR_PADDING) + 'px;'); michael@0: this.previousContainerHeight = this.newContainerHeight; michael@0: }, michael@0: michael@0: open: function secondaryToolbarOpen() { michael@0: if (this.opened) { michael@0: return; michael@0: } michael@0: this.opened = true; michael@0: this.toggleButton.classList.add('toggled'); michael@0: this.toolbar.classList.remove('hidden'); michael@0: }, michael@0: michael@0: close: function secondaryToolbarClose(target) { michael@0: if (!this.opened) { michael@0: return; michael@0: } else if (target && !this.toolbar.contains(target)) { michael@0: return; michael@0: } michael@0: this.opened = false; michael@0: this.toolbar.classList.add('hidden'); michael@0: this.toggleButton.classList.remove('toggled'); michael@0: }, michael@0: michael@0: toggle: function secondaryToolbarToggle() { michael@0: if (this.opened) { michael@0: this.close(); michael@0: } else { michael@0: this.open(); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: michael@0: var PasswordPrompt = { michael@0: visible: false, michael@0: updatePassword: null, michael@0: reason: null, michael@0: overlayContainer: null, michael@0: passwordField: null, michael@0: passwordText: null, michael@0: passwordSubmit: null, michael@0: passwordCancel: null, michael@0: michael@0: initialize: function secondaryToolbarInitialize(options) { michael@0: this.overlayContainer = options.overlayContainer; michael@0: this.passwordField = options.passwordField; michael@0: this.passwordText = options.passwordText; michael@0: this.passwordSubmit = options.passwordSubmit; michael@0: this.passwordCancel = options.passwordCancel; michael@0: michael@0: // Attach the event listeners. michael@0: this.passwordSubmit.addEventListener('click', michael@0: this.verifyPassword.bind(this)); michael@0: michael@0: this.passwordCancel.addEventListener('click', this.hide.bind(this)); michael@0: michael@0: this.passwordField.addEventListener('keydown', michael@0: function (e) { michael@0: if (e.keyCode === 13) { // Enter key michael@0: this.verifyPassword(); michael@0: } michael@0: }.bind(this)); michael@0: michael@0: window.addEventListener('keydown', michael@0: function (e) { michael@0: if (e.keyCode === 27) { // Esc key michael@0: this.hide(); michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: show: function passwordPromptShow() { michael@0: if (this.visible) { michael@0: return; michael@0: } michael@0: this.visible = true; michael@0: this.overlayContainer.classList.remove('hidden'); michael@0: this.overlayContainer.firstElementChild.classList.remove('hidden'); michael@0: this.passwordField.focus(); michael@0: michael@0: var promptString = mozL10n.get('password_label', null, michael@0: 'Enter the password to open this PDF file.'); michael@0: michael@0: if (this.reason === PDFJS.PasswordResponses.INCORRECT_PASSWORD) { michael@0: promptString = mozL10n.get('password_invalid', null, michael@0: 'Invalid password. Please try again.'); michael@0: } michael@0: michael@0: this.passwordText.textContent = promptString; michael@0: }, michael@0: michael@0: hide: function passwordPromptClose() { michael@0: if (!this.visible) { michael@0: return; michael@0: } michael@0: this.visible = false; michael@0: this.passwordField.value = ''; michael@0: this.overlayContainer.classList.add('hidden'); michael@0: this.overlayContainer.firstElementChild.classList.add('hidden'); michael@0: }, michael@0: michael@0: verifyPassword: function passwordPromptVerifyPassword() { michael@0: var password = this.passwordField.value; michael@0: if (password && password.length > 0) { michael@0: this.hide(); michael@0: return this.updatePassword(password); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: michael@0: var DELAY_BEFORE_HIDING_CONTROLS = 3000; // in ms michael@0: var SELECTOR = 'presentationControls'; michael@0: var DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS = 1000; // in ms michael@0: michael@0: var PresentationMode = { michael@0: active: false, michael@0: args: null, michael@0: contextMenuOpen: false, michael@0: michael@0: initialize: function presentationModeInitialize(options) { michael@0: this.container = options.container; michael@0: this.secondaryToolbar = options.secondaryToolbar; michael@0: michael@0: this.viewer = this.container.firstElementChild; michael@0: michael@0: this.firstPage = options.firstPage; michael@0: this.lastPage = options.lastPage; michael@0: this.pageRotateCw = options.pageRotateCw; michael@0: this.pageRotateCcw = options.pageRotateCcw; michael@0: michael@0: this.firstPage.addEventListener('click', function() { michael@0: this.contextMenuOpen = false; michael@0: this.secondaryToolbar.firstPageClick(); michael@0: }.bind(this)); michael@0: this.lastPage.addEventListener('click', function() { michael@0: this.contextMenuOpen = false; michael@0: this.secondaryToolbar.lastPageClick(); michael@0: }.bind(this)); michael@0: michael@0: this.pageRotateCw.addEventListener('click', function() { michael@0: this.contextMenuOpen = false; michael@0: this.secondaryToolbar.pageRotateCwClick(); michael@0: }.bind(this)); michael@0: this.pageRotateCcw.addEventListener('click', function() { michael@0: this.contextMenuOpen = false; michael@0: this.secondaryToolbar.pageRotateCcwClick(); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: get isFullscreen() { michael@0: return (document.fullscreenElement || michael@0: document.mozFullScreen || michael@0: document.webkitIsFullScreen || michael@0: document.msFullscreenElement); michael@0: }, michael@0: michael@0: /** michael@0: * Initialize a timeout that is used to reset PDFView.currentPosition when the michael@0: * browser transitions to fullscreen mode. Since resize events are triggered michael@0: * multiple times during the switch to fullscreen mode, this is necessary in michael@0: * order to prevent the page from being scrolled partially, or completely, michael@0: * out of view when Presentation Mode is enabled. michael@0: * Note: This is only an issue at certain zoom levels, e.g. 'page-width'. michael@0: */ michael@0: _setSwitchInProgress: function presentationMode_setSwitchInProgress() { michael@0: if (this.switchInProgress) { michael@0: clearTimeout(this.switchInProgress); michael@0: } michael@0: this.switchInProgress = setTimeout(function switchInProgressTimeout() { michael@0: delete this.switchInProgress; michael@0: }.bind(this), DELAY_BEFORE_RESETTING_SWITCH_IN_PROGRESS); michael@0: michael@0: PDFView.currentPosition = null; michael@0: }, michael@0: michael@0: _resetSwitchInProgress: function presentationMode_resetSwitchInProgress() { michael@0: if (this.switchInProgress) { michael@0: clearTimeout(this.switchInProgress); michael@0: delete this.switchInProgress; michael@0: } michael@0: }, michael@0: michael@0: request: function presentationModeRequest() { michael@0: if (!PDFView.supportsFullscreen || this.isFullscreen || michael@0: !this.viewer.hasChildNodes()) { michael@0: return false; michael@0: } michael@0: this._setSwitchInProgress(); michael@0: michael@0: if (this.container.requestFullscreen) { michael@0: this.container.requestFullscreen(); michael@0: } else if (this.container.mozRequestFullScreen) { michael@0: this.container.mozRequestFullScreen(); michael@0: } else if (this.container.webkitRequestFullScreen) { michael@0: this.container.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); michael@0: } else if (this.container.msRequestFullscreen) { michael@0: this.container.msRequestFullscreen(); michael@0: } else { michael@0: return false; michael@0: } michael@0: michael@0: this.args = { michael@0: page: PDFView.page, michael@0: previousScale: PDFView.currentScaleValue michael@0: }; michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: enter: function presentationModeEnter() { michael@0: this.active = true; michael@0: this._resetSwitchInProgress(); michael@0: michael@0: // Ensure that the correct page is scrolled into view when entering michael@0: // Presentation Mode, by waiting until fullscreen mode in enabled. michael@0: // Note: This is only necessary in non-Mozilla browsers. michael@0: setTimeout(function enterPresentationModeTimeout() { michael@0: PDFView.page = this.args.page; michael@0: PDFView.setScale('page-fit', true); michael@0: }.bind(this), 0); michael@0: michael@0: window.addEventListener('mousemove', this.mouseMove, false); michael@0: window.addEventListener('mousedown', this.mouseDown, false); michael@0: window.addEventListener('contextmenu', this.contextMenu, false); michael@0: michael@0: this.showControls(); michael@0: HandTool.enterPresentationMode(); michael@0: this.contextMenuOpen = false; michael@0: this.container.setAttribute('contextmenu', 'viewerContextMenu'); michael@0: }, michael@0: michael@0: exit: function presentationModeExit() { michael@0: var page = PDFView.page; michael@0: michael@0: // Ensure that the correct page is scrolled into view when exiting michael@0: // Presentation Mode, by waiting until fullscreen mode is disabled. michael@0: // Note: This is only necessary in non-Mozilla browsers. michael@0: setTimeout(function exitPresentationModeTimeout() { michael@0: this.active = false; michael@0: PDFView.setScale(this.args.previousScale); michael@0: PDFView.page = page; michael@0: this.args = null; michael@0: }.bind(this), 0); michael@0: michael@0: window.removeEventListener('mousemove', this.mouseMove, false); michael@0: window.removeEventListener('mousedown', this.mouseDown, false); michael@0: window.removeEventListener('contextmenu', this.contextMenu, false); michael@0: michael@0: this.hideControls(); michael@0: PDFView.clearMouseScrollState(); michael@0: HandTool.exitPresentationMode(); michael@0: this.container.removeAttribute('contextmenu'); michael@0: this.contextMenuOpen = false; michael@0: michael@0: // Ensure that the thumbnail of the current page is visible michael@0: // when exiting presentation mode. michael@0: scrollIntoView(document.getElementById('thumbnailContainer' + page)); michael@0: }, michael@0: michael@0: showControls: function presentationModeShowControls() { michael@0: if (this.controlsTimeout) { michael@0: clearTimeout(this.controlsTimeout); michael@0: } else { michael@0: this.container.classList.add(SELECTOR); michael@0: } michael@0: this.controlsTimeout = setTimeout(function hideControlsTimeout() { michael@0: this.container.classList.remove(SELECTOR); michael@0: delete this.controlsTimeout; michael@0: }.bind(this), DELAY_BEFORE_HIDING_CONTROLS); michael@0: }, michael@0: michael@0: hideControls: function presentationModeHideControls() { michael@0: if (!this.controlsTimeout) { michael@0: return; michael@0: } michael@0: this.container.classList.remove(SELECTOR); michael@0: clearTimeout(this.controlsTimeout); michael@0: delete this.controlsTimeout; michael@0: }, michael@0: michael@0: mouseMove: function presentationModeMouseMove(evt) { michael@0: PresentationMode.showControls(); michael@0: }, michael@0: michael@0: mouseDown: function presentationModeMouseDown(evt) { michael@0: var self = PresentationMode; michael@0: if (self.contextMenuOpen) { michael@0: self.contextMenuOpen = false; michael@0: evt.preventDefault(); michael@0: return; michael@0: } michael@0: michael@0: if (evt.button === 0) { michael@0: // Enable clicking of links in presentation mode. Please note: michael@0: // Only links pointing to destinations in the current PDF document work. michael@0: var isInternalLink = (evt.target.href && michael@0: evt.target.classList.contains('internalLink')); michael@0: if (!isInternalLink) { michael@0: // Unless an internal link was clicked, advance one page. michael@0: evt.preventDefault(); michael@0: PDFView.page += (evt.shiftKey ? -1 : 1); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: contextMenu: function presentationModeContextMenu(evt) { michael@0: PresentationMode.contextMenuOpen = true; michael@0: } michael@0: }; michael@0: michael@0: (function presentationModeClosure() { michael@0: function presentationModeChange(e) { michael@0: if (PresentationMode.isFullscreen) { michael@0: PresentationMode.enter(); michael@0: } else { michael@0: PresentationMode.exit(); michael@0: } michael@0: } michael@0: michael@0: window.addEventListener('fullscreenchange', presentationModeChange, false); michael@0: window.addEventListener('mozfullscreenchange', presentationModeChange, false); michael@0: window.addEventListener('webkitfullscreenchange', presentationModeChange, michael@0: false); michael@0: window.addEventListener('MSFullscreenChange', presentationModeChange, false); michael@0: })(); michael@0: michael@0: michael@0: /* Copyright 2013 Rob Wu michael@0: * https://github.com/Rob--W/grab-to-pan.js michael@0: * michael@0: * Licensed under the Apache License, Version 2.0 (the "License"); michael@0: * you may not use this file except in compliance with the License. michael@0: * You may obtain a copy of the License at michael@0: * michael@0: * http://www.apache.org/licenses/LICENSE-2.0 michael@0: * michael@0: * Unless required by applicable law or agreed to in writing, software michael@0: * distributed under the License is distributed on an "AS IS" BASIS, michael@0: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. michael@0: * See the License for the specific language governing permissions and michael@0: * limitations under the License. michael@0: */ michael@0: michael@0: 'use strict'; michael@0: michael@0: var GrabToPan = (function GrabToPanClosure() { michael@0: /** michael@0: * Construct a GrabToPan instance for a given HTML element. michael@0: * @param options.element {Element} michael@0: * @param options.ignoreTarget {function} optional. See `ignoreTarget(node)` michael@0: * @param options.onActiveChanged {function(boolean)} optional. Called michael@0: * when grab-to-pan is (de)activated. The first argument is a boolean that michael@0: * shows whether grab-to-pan is activated. michael@0: */ michael@0: function GrabToPan(options) { michael@0: this.element = options.element; michael@0: this.document = options.element.ownerDocument; michael@0: if (typeof options.ignoreTarget === 'function') { michael@0: this.ignoreTarget = options.ignoreTarget; michael@0: } michael@0: this.onActiveChanged = options.onActiveChanged; michael@0: michael@0: // Bind the contexts to ensure that `this` always points to michael@0: // the GrabToPan instance. michael@0: this.activate = this.activate.bind(this); michael@0: this.deactivate = this.deactivate.bind(this); michael@0: this.toggle = this.toggle.bind(this); michael@0: this._onmousedown = this._onmousedown.bind(this); michael@0: this._onmousemove = this._onmousemove.bind(this); michael@0: this._endPan = this._endPan.bind(this); michael@0: michael@0: // This overlay will be inserted in the document when the mouse moves during michael@0: // a grab operation, to ensure that the cursor has the desired appearance. michael@0: var overlay = this.overlay = document.createElement('div'); michael@0: overlay.className = 'grab-to-pan-grabbing'; michael@0: } michael@0: GrabToPan.prototype = { michael@0: /** michael@0: * Class name of element which can be grabbed michael@0: */ michael@0: CSS_CLASS_GRAB: 'grab-to-pan-grab', michael@0: michael@0: /** michael@0: * Bind a mousedown event to the element to enable grab-detection. michael@0: */ michael@0: activate: function GrabToPan_activate() { michael@0: if (!this.active) { michael@0: this.active = true; michael@0: this.element.addEventListener('mousedown', this._onmousedown, true); michael@0: this.element.classList.add(this.CSS_CLASS_GRAB); michael@0: if (this.onActiveChanged) { michael@0: this.onActiveChanged(true); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Removes all events. Any pending pan session is immediately stopped. michael@0: */ michael@0: deactivate: function GrabToPan_deactivate() { michael@0: if (this.active) { michael@0: this.active = false; michael@0: this.element.removeEventListener('mousedown', this._onmousedown, true); michael@0: this._endPan(); michael@0: this.element.classList.remove(this.CSS_CLASS_GRAB); michael@0: if (this.onActiveChanged) { michael@0: this.onActiveChanged(false); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: toggle: function GrabToPan_toggle() { michael@0: if (this.active) { michael@0: this.deactivate(); michael@0: } else { michael@0: this.activate(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Whether to not pan if the target element is clicked. michael@0: * Override this method to change the default behaviour. michael@0: * michael@0: * @param node {Element} The target of the event michael@0: * @return {boolean} Whether to not react to the click event. michael@0: */ michael@0: ignoreTarget: function GrabToPan_ignoreTarget(node) { michael@0: // Use matchesSelector to check whether the clicked element michael@0: // is (a child of) an input element / link michael@0: return node[matchesSelector]( michael@0: 'a[href], a[href] *, input, textarea, button, button *, select, option' michael@0: ); michael@0: }, michael@0: michael@0: /** michael@0: * @private michael@0: */ michael@0: _onmousedown: function GrabToPan__onmousedown(event) { michael@0: if (event.button !== 0 || this.ignoreTarget(event.target)) { michael@0: return; michael@0: } michael@0: if (event.originalTarget) { michael@0: try { michael@0: /* jshint expr:true */ michael@0: event.originalTarget.tagName; michael@0: } catch (e) { michael@0: // Mozilla-specific: element is a scrollbar (XUL element) michael@0: return; michael@0: } michael@0: } michael@0: michael@0: this.scrollLeftStart = this.element.scrollLeft; michael@0: this.scrollTopStart = this.element.scrollTop; michael@0: this.clientXStart = event.clientX; michael@0: this.clientYStart = event.clientY; michael@0: this.document.addEventListener('mousemove', this._onmousemove, true); michael@0: this.document.addEventListener('mouseup', this._endPan, true); michael@0: // When a scroll event occurs before a mousemove, assume that the user michael@0: // dragged a scrollbar (necessary for Opera Presto, Safari and IE) michael@0: // (not needed for Chrome/Firefox) michael@0: this.element.addEventListener('scroll', this._endPan, true); michael@0: event.preventDefault(); michael@0: event.stopPropagation(); michael@0: this.document.documentElement.classList.add(this.CSS_CLASS_GRABBING); michael@0: }, michael@0: michael@0: /** michael@0: * @private michael@0: */ michael@0: _onmousemove: function GrabToPan__onmousemove(event) { michael@0: this.element.removeEventListener('scroll', this._endPan, true); michael@0: if (isLeftMouseReleased(event)) { michael@0: this._endPan(); michael@0: return; michael@0: } michael@0: var xDiff = event.clientX - this.clientXStart; michael@0: var yDiff = event.clientY - this.clientYStart; michael@0: this.element.scrollTop = this.scrollTopStart - yDiff; michael@0: this.element.scrollLeft = this.scrollLeftStart - xDiff; michael@0: if (!this.overlay.parentNode) { michael@0: document.body.appendChild(this.overlay); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * @private michael@0: */ michael@0: _endPan: function GrabToPan__endPan() { michael@0: this.element.removeEventListener('scroll', this._endPan, true); michael@0: this.document.removeEventListener('mousemove', this._onmousemove, true); michael@0: this.document.removeEventListener('mouseup', this._endPan, true); michael@0: if (this.overlay.parentNode) { michael@0: this.overlay.parentNode.removeChild(this.overlay); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: // Get the correct (vendor-prefixed) name of the matches method. michael@0: var matchesSelector; michael@0: ['webkitM', 'mozM', 'msM', 'oM', 'm'].some(function(prefix) { michael@0: var name = prefix + 'atches'; michael@0: if (name in document.documentElement) { michael@0: matchesSelector = name; michael@0: } michael@0: name += 'Selector'; michael@0: if (name in document.documentElement) { michael@0: matchesSelector = name; michael@0: } michael@0: return matchesSelector; // If found, then truthy, and [].some() ends. michael@0: }); michael@0: michael@0: // Browser sniffing because it's impossible to feature-detect michael@0: // whether event.which for onmousemove is reliable michael@0: var isNotIEorIsIE10plus = !document.documentMode || document.documentMode > 9; michael@0: var chrome = window.chrome; michael@0: var isChrome15OrOpera15plus = chrome && (chrome.webstore || chrome.app); michael@0: // ^ Chrome 15+ ^ Opera 15+ michael@0: var isSafari6plus = /Apple/.test(navigator.vendor) && michael@0: /Version\/([6-9]\d*|[1-5]\d+)/.test(navigator.userAgent); michael@0: michael@0: /** michael@0: * Whether the left mouse is not pressed. michael@0: * @param event {MouseEvent} michael@0: * @return {boolean} True if the left mouse button is not pressed. michael@0: * False if unsure or if the left mouse button is pressed. michael@0: */ michael@0: function isLeftMouseReleased(event) { michael@0: if ('buttons' in event && isNotIEorIsIE10plus) { michael@0: // http://www.w3.org/TR/DOM-Level-3-Events/#events-MouseEvent-buttons michael@0: // Firefox 15+ michael@0: // Internet Explorer 10+ michael@0: return !(event.buttons | 1); michael@0: } michael@0: if (isChrome15OrOpera15plus || isSafari6plus) { michael@0: // Chrome 14+ michael@0: // Opera 15+ michael@0: // Safari 6.0+ michael@0: return event.which === 0; michael@0: } michael@0: } michael@0: michael@0: return GrabToPan; michael@0: })(); michael@0: michael@0: var HandTool = { michael@0: initialize: function handToolInitialize(options) { michael@0: var toggleHandTool = options.toggleHandTool; michael@0: this.handTool = new GrabToPan({ michael@0: element: options.container, michael@0: onActiveChanged: function(isActive) { michael@0: if (!toggleHandTool) { michael@0: return; michael@0: } michael@0: if (isActive) { michael@0: toggleHandTool.title = michael@0: mozL10n.get('hand_tool_disable.title', null, 'Disable hand tool'); michael@0: toggleHandTool.firstElementChild.textContent = michael@0: mozL10n.get('hand_tool_disable_label', null, 'Disable hand tool'); michael@0: } else { michael@0: toggleHandTool.title = michael@0: mozL10n.get('hand_tool_enable.title', null, 'Enable hand tool'); michael@0: toggleHandTool.firstElementChild.textContent = michael@0: mozL10n.get('hand_tool_enable_label', null, 'Enable hand tool'); michael@0: } michael@0: } michael@0: }); michael@0: if (toggleHandTool) { michael@0: toggleHandTool.addEventListener('click', this.toggle.bind(this), false); michael@0: michael@0: window.addEventListener('localized', function (evt) { michael@0: Preferences.get('enableHandToolOnLoad').then(function (prefValue) { michael@0: if (prefValue) { michael@0: this.handTool.activate(); michael@0: } michael@0: }.bind(this)); michael@0: }.bind(this)); michael@0: } michael@0: }, michael@0: michael@0: toggle: function handToolToggle() { michael@0: this.handTool.toggle(); michael@0: SecondaryToolbar.close(); michael@0: }, michael@0: michael@0: enterPresentationMode: function handToolEnterPresentationMode() { michael@0: if (this.handTool.active) { michael@0: this.wasActive = true; michael@0: this.handTool.deactivate(); michael@0: } michael@0: }, michael@0: michael@0: exitPresentationMode: function handToolExitPresentationMode() { michael@0: if (this.wasActive) { michael@0: this.wasActive = null; michael@0: this.handTool.activate(); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: michael@0: var DocumentProperties = { michael@0: overlayContainer: null, michael@0: fileName: '', michael@0: fileSize: '', michael@0: visible: false, michael@0: michael@0: // Document property fields (in the viewer). michael@0: fileNameField: null, michael@0: fileSizeField: null, michael@0: titleField: null, michael@0: authorField: null, michael@0: subjectField: null, michael@0: keywordsField: null, michael@0: creationDateField: null, michael@0: modificationDateField: null, michael@0: creatorField: null, michael@0: producerField: null, michael@0: versionField: null, michael@0: pageCountField: null, michael@0: michael@0: initialize: function documentPropertiesInitialize(options) { michael@0: this.overlayContainer = options.overlayContainer; michael@0: michael@0: // Set the document property fields. michael@0: this.fileNameField = options.fileNameField; michael@0: this.fileSizeField = options.fileSizeField; michael@0: this.titleField = options.titleField; michael@0: this.authorField = options.authorField; michael@0: this.subjectField = options.subjectField; michael@0: this.keywordsField = options.keywordsField; michael@0: this.creationDateField = options.creationDateField; michael@0: this.modificationDateField = options.modificationDateField; michael@0: this.creatorField = options.creatorField; michael@0: this.producerField = options.producerField; michael@0: this.versionField = options.versionField; michael@0: this.pageCountField = options.pageCountField; michael@0: michael@0: // Bind the event listener for the Close button. michael@0: if (options.closeButton) { michael@0: options.closeButton.addEventListener('click', this.hide.bind(this)); michael@0: } michael@0: michael@0: this.dataAvailablePromise = new Promise(function (resolve) { michael@0: this.resolveDataAvailable = resolve; michael@0: }.bind(this)); michael@0: michael@0: // Bind the event listener for the Esc key (to close the dialog). michael@0: window.addEventListener('keydown', michael@0: function (e) { michael@0: if (e.keyCode === 27) { // Esc key michael@0: this.hide(); michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: getProperties: function documentPropertiesGetProperties() { michael@0: if (!this.visible) { michael@0: // If the dialog was closed before dataAvailablePromise was resolved, michael@0: // don't bother updating the properties. michael@0: return; michael@0: } michael@0: // Get the file name. michael@0: this.fileName = getPDFFileNameFromURL(PDFView.url); michael@0: michael@0: // Get the file size. michael@0: PDFView.pdfDocument.getDownloadInfo().then(function(data) { michael@0: this.setFileSize(data.length); michael@0: this.updateUI(this.fileSizeField, this.fileSize); michael@0: }.bind(this)); michael@0: michael@0: // Get the other document properties. michael@0: PDFView.pdfDocument.getMetadata().then(function(data) { michael@0: var fields = [ michael@0: { field: this.fileNameField, content: this.fileName }, michael@0: // The fileSize field is updated once getDownloadInfo is resolved. michael@0: { field: this.titleField, content: data.info.Title }, michael@0: { field: this.authorField, content: data.info.Author }, michael@0: { field: this.subjectField, content: data.info.Subject }, michael@0: { field: this.keywordsField, content: data.info.Keywords }, michael@0: { field: this.creationDateField, michael@0: content: this.parseDate(data.info.CreationDate) }, michael@0: { field: this.modificationDateField, michael@0: content: this.parseDate(data.info.ModDate) }, michael@0: { field: this.creatorField, content: data.info.Creator }, michael@0: { field: this.producerField, content: data.info.Producer }, michael@0: { field: this.versionField, content: data.info.PDFFormatVersion }, michael@0: { field: this.pageCountField, content: PDFView.pdfDocument.numPages } michael@0: ]; michael@0: michael@0: // Show the properties in the dialog. michael@0: for (var item in fields) { michael@0: var element = fields[item]; michael@0: this.updateUI(element.field, element.content); michael@0: } michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: updateUI: function documentPropertiesUpdateUI(field, content) { michael@0: if (field && content !== undefined && content !== '') { michael@0: field.textContent = content; michael@0: } michael@0: }, michael@0: michael@0: setFileSize: function documentPropertiesSetFileSize(fileSize) { michael@0: var kb = fileSize / 1024; michael@0: if (kb < 1024) { michael@0: this.fileSize = mozL10n.get('document_properties_kb', { michael@0: size_kb: (+kb.toPrecision(3)).toLocaleString(), michael@0: size_b: fileSize.toLocaleString() michael@0: }, '{{size_kb}} KB ({{size_b}} bytes)'); michael@0: } else { michael@0: this.fileSize = mozL10n.get('document_properties_mb', { michael@0: size_mb: (+(kb / 1024).toPrecision(3)).toLocaleString(), michael@0: size_b: fileSize.toLocaleString() michael@0: }, '{{size_mb}} MB ({{size_b}} bytes)'); michael@0: } michael@0: }, michael@0: michael@0: show: function documentPropertiesShow() { michael@0: if (this.visible) { michael@0: return; michael@0: } michael@0: this.visible = true; michael@0: this.overlayContainer.classList.remove('hidden'); michael@0: this.overlayContainer.lastElementChild.classList.remove('hidden'); michael@0: michael@0: this.dataAvailablePromise.then(function () { michael@0: this.getProperties(); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: hide: function documentPropertiesClose() { michael@0: if (!this.visible) { michael@0: return; michael@0: } michael@0: this.visible = false; michael@0: this.overlayContainer.classList.add('hidden'); michael@0: this.overlayContainer.lastElementChild.classList.add('hidden'); michael@0: }, michael@0: michael@0: parseDate: function documentPropertiesParseDate(inputDate) { michael@0: // This is implemented according to the PDF specification (see michael@0: // http://www.gnupdf.org/Date for an overview), but note that michael@0: // Adobe Reader doesn't handle changing the date to universal time michael@0: // and doesn't use the user's time zone (they're effectively ignoring michael@0: // the HH' and mm' parts of the date string). michael@0: var dateToParse = inputDate; michael@0: if (dateToParse === undefined) { michael@0: return ''; michael@0: } michael@0: michael@0: // Remove the D: prefix if it is available. michael@0: if (dateToParse.substring(0,2) === 'D:') { michael@0: dateToParse = dateToParse.substring(2); michael@0: } michael@0: michael@0: // Get all elements from the PDF date string. michael@0: // JavaScript's Date object expects the month to be between michael@0: // 0 and 11 instead of 1 and 12, so we're correcting for this. michael@0: var year = parseInt(dateToParse.substring(0,4), 10); michael@0: var month = parseInt(dateToParse.substring(4,6), 10) - 1; michael@0: var day = parseInt(dateToParse.substring(6,8), 10); michael@0: var hours = parseInt(dateToParse.substring(8,10), 10); michael@0: var minutes = parseInt(dateToParse.substring(10,12), 10); michael@0: var seconds = parseInt(dateToParse.substring(12,14), 10); michael@0: var utRel = dateToParse.substring(14,15); michael@0: var offsetHours = parseInt(dateToParse.substring(15,17), 10); michael@0: var offsetMinutes = parseInt(dateToParse.substring(18,20), 10); michael@0: michael@0: // As per spec, utRel = 'Z' means equal to universal time. michael@0: // The other cases ('-' and '+') have to be handled here. michael@0: if (utRel == '-') { michael@0: hours += offsetHours; michael@0: minutes += offsetMinutes; michael@0: } else if (utRel == '+') { michael@0: hours -= offsetHours; michael@0: minutes += offsetMinutes; michael@0: } michael@0: michael@0: // Return the new date format from the user's locale. michael@0: var date = new Date(Date.UTC(year, month, day, hours, minutes, seconds)); michael@0: var dateString = date.toLocaleDateString(); michael@0: var timeString = date.toLocaleTimeString(); michael@0: return mozL10n.get('document_properties_date_string', michael@0: {date: dateString, time: timeString}, michael@0: '{{date}}, {{time}}'); michael@0: } michael@0: }; michael@0: michael@0: michael@0: var PDFView = { michael@0: pages: [], michael@0: thumbnails: [], michael@0: currentScale: UNKNOWN_SCALE, michael@0: currentScaleValue: null, michael@0: initialBookmark: document.location.hash.substring(1), michael@0: container: null, michael@0: thumbnailContainer: null, michael@0: initialized: false, michael@0: fellback: false, michael@0: pdfDocument: null, michael@0: sidebarOpen: false, michael@0: pageViewScroll: null, michael@0: thumbnailViewScroll: null, michael@0: pageRotation: 0, michael@0: mouseScrollTimeStamp: 0, michael@0: mouseScrollDelta: 0, michael@0: lastScroll: 0, michael@0: previousPageNumber: 1, michael@0: isViewerEmbedded: (window.parent !== window), michael@0: idleTimeout: null, michael@0: currentPosition: null, michael@0: michael@0: // called once when the document is loaded michael@0: initialize: function pdfViewInitialize() { michael@0: var self = this; michael@0: var container = this.container = document.getElementById('viewerContainer'); michael@0: this.pageViewScroll = {}; michael@0: this.watchScroll(container, this.pageViewScroll, updateViewarea); michael@0: michael@0: var thumbnailContainer = this.thumbnailContainer = michael@0: document.getElementById('thumbnailView'); michael@0: this.thumbnailViewScroll = {}; michael@0: this.watchScroll(thumbnailContainer, this.thumbnailViewScroll, michael@0: this.renderHighestPriority.bind(this)); michael@0: michael@0: Preferences.initialize(); michael@0: michael@0: PDFFindBar.initialize({ michael@0: bar: document.getElementById('findbar'), michael@0: toggleButton: document.getElementById('viewFind'), michael@0: findField: document.getElementById('findInput'), michael@0: highlightAllCheckbox: document.getElementById('findHighlightAll'), michael@0: caseSensitiveCheckbox: document.getElementById('findMatchCase'), michael@0: findMsg: document.getElementById('findMsg'), michael@0: findStatusIcon: document.getElementById('findStatusIcon'), michael@0: findPreviousButton: document.getElementById('findPrevious'), michael@0: findNextButton: document.getElementById('findNext') michael@0: }); michael@0: michael@0: PDFFindController.initialize({ michael@0: pdfPageSource: this, michael@0: integratedFind: this.supportsIntegratedFind michael@0: }); michael@0: michael@0: HandTool.initialize({ michael@0: container: container, michael@0: toggleHandTool: document.getElementById('toggleHandTool') michael@0: }); michael@0: michael@0: SecondaryToolbar.initialize({ michael@0: toolbar: document.getElementById('secondaryToolbar'), michael@0: presentationMode: PresentationMode, michael@0: toggleButton: document.getElementById('secondaryToolbarToggle'), michael@0: presentationModeButton: michael@0: document.getElementById('secondaryPresentationMode'), michael@0: openFile: document.getElementById('secondaryOpenFile'), michael@0: print: document.getElementById('secondaryPrint'), michael@0: download: document.getElementById('secondaryDownload'), michael@0: viewBookmark: document.getElementById('secondaryViewBookmark'), michael@0: firstPage: document.getElementById('firstPage'), michael@0: lastPage: document.getElementById('lastPage'), michael@0: pageRotateCw: document.getElementById('pageRotateCw'), michael@0: pageRotateCcw: document.getElementById('pageRotateCcw'), michael@0: documentProperties: DocumentProperties, michael@0: documentPropertiesButton: document.getElementById('documentProperties') michael@0: }); michael@0: michael@0: PasswordPrompt.initialize({ michael@0: overlayContainer: document.getElementById('overlayContainer'), michael@0: passwordField: document.getElementById('password'), michael@0: passwordText: document.getElementById('passwordText'), michael@0: passwordSubmit: document.getElementById('passwordSubmit'), michael@0: passwordCancel: document.getElementById('passwordCancel') michael@0: }); michael@0: michael@0: PresentationMode.initialize({ michael@0: container: container, michael@0: secondaryToolbar: SecondaryToolbar, michael@0: firstPage: document.getElementById('contextFirstPage'), michael@0: lastPage: document.getElementById('contextLastPage'), michael@0: pageRotateCw: document.getElementById('contextPageRotateCw'), michael@0: pageRotateCcw: document.getElementById('contextPageRotateCcw') michael@0: }); michael@0: michael@0: DocumentProperties.initialize({ michael@0: overlayContainer: document.getElementById('overlayContainer'), michael@0: closeButton: document.getElementById('documentPropertiesClose'), michael@0: fileNameField: document.getElementById('fileNameField'), michael@0: fileSizeField: document.getElementById('fileSizeField'), michael@0: titleField: document.getElementById('titleField'), michael@0: authorField: document.getElementById('authorField'), michael@0: subjectField: document.getElementById('subjectField'), michael@0: keywordsField: document.getElementById('keywordsField'), michael@0: creationDateField: document.getElementById('creationDateField'), michael@0: modificationDateField: document.getElementById('modificationDateField'), michael@0: creatorField: document.getElementById('creatorField'), michael@0: producerField: document.getElementById('producerField'), michael@0: versionField: document.getElementById('versionField'), michael@0: pageCountField: document.getElementById('pageCountField') michael@0: }); michael@0: michael@0: container.addEventListener('scroll', function() { michael@0: self.lastScroll = Date.now(); michael@0: }, false); michael@0: michael@0: var initializedPromise = Promise.all([ michael@0: Preferences.get('enableWebGL').then(function (value) { michael@0: PDFJS.disableWebGL = !value; michael@0: }) michael@0: // TODO move more preferences and other async stuff here michael@0: ]); michael@0: michael@0: return initializedPromise.then(function () { michael@0: PDFView.initialized = true; michael@0: }); michael@0: }, michael@0: michael@0: getPage: function pdfViewGetPage(n) { michael@0: return this.pdfDocument.getPage(n); michael@0: }, michael@0: michael@0: // Helper function to keep track whether a div was scrolled up or down and michael@0: // then call a callback. michael@0: watchScroll: function pdfViewWatchScroll(viewAreaElement, state, callback) { michael@0: state.down = true; michael@0: state.lastY = viewAreaElement.scrollTop; michael@0: viewAreaElement.addEventListener('scroll', function webViewerScroll(evt) { michael@0: var currentY = viewAreaElement.scrollTop; michael@0: var lastY = state.lastY; michael@0: if (currentY > lastY) { michael@0: state.down = true; michael@0: } else if (currentY < lastY) { michael@0: state.down = false; michael@0: } michael@0: // else do nothing and use previous value michael@0: state.lastY = currentY; michael@0: callback(); michael@0: }, true); michael@0: }, michael@0: michael@0: _setScaleUpdatePages: function pdfView_setScaleUpdatePages( michael@0: newScale, newValue, resetAutoSettings, noScroll) { michael@0: this.currentScaleValue = newValue; michael@0: if (newScale === this.currentScale) { michael@0: return; michael@0: } michael@0: for (var i = 0, ii = this.pages.length; i < ii; i++) { michael@0: this.pages[i].update(newScale); michael@0: } michael@0: this.currentScale = newScale; michael@0: michael@0: if (!noScroll) { michael@0: var page = this.page, dest; michael@0: if (this.currentPosition && !IGNORE_CURRENT_POSITION_ON_ZOOM) { michael@0: page = this.currentPosition.page; michael@0: dest = [null, { name: 'XYZ' }, this.currentPosition.left, michael@0: this.currentPosition.top, null]; michael@0: } michael@0: this.pages[page - 1].scrollIntoView(dest); michael@0: } michael@0: var event = document.createEvent('UIEvents'); michael@0: event.initUIEvent('scalechange', false, false, window, 0); michael@0: event.scale = newScale; michael@0: event.resetAutoSettings = resetAutoSettings; michael@0: window.dispatchEvent(event); michael@0: }, michael@0: michael@0: setScale: function pdfViewSetScale(value, resetAutoSettings, noScroll) { michael@0: if (value === 'custom') { michael@0: return; michael@0: } michael@0: var scale = parseFloat(value); michael@0: michael@0: if (scale > 0) { michael@0: this._setScaleUpdatePages(scale, value, true, noScroll); michael@0: } else { michael@0: var currentPage = this.pages[this.page - 1]; michael@0: if (!currentPage) { michael@0: return; michael@0: } michael@0: var hPadding = PresentationMode.active ? 0 : SCROLLBAR_PADDING; michael@0: var vPadding = PresentationMode.active ? 0 : VERTICAL_PADDING; michael@0: var pageWidthScale = (this.container.clientWidth - hPadding) / michael@0: currentPage.width * currentPage.scale; michael@0: var pageHeightScale = (this.container.clientHeight - vPadding) / michael@0: currentPage.height * currentPage.scale; michael@0: switch (value) { michael@0: case 'page-actual': michael@0: scale = 1; michael@0: break; michael@0: case 'page-width': michael@0: scale = pageWidthScale; michael@0: break; michael@0: case 'page-height': michael@0: scale = pageHeightScale; michael@0: break; michael@0: case 'page-fit': michael@0: scale = Math.min(pageWidthScale, pageHeightScale); michael@0: break; michael@0: case 'auto': michael@0: scale = Math.min(MAX_AUTO_SCALE, pageWidthScale); michael@0: break; michael@0: default: michael@0: console.error('pdfViewSetScale: \'' + value + michael@0: '\' is an unknown zoom value.'); michael@0: return; michael@0: } michael@0: this._setScaleUpdatePages(scale, value, resetAutoSettings, noScroll); michael@0: michael@0: selectScaleOption(value); michael@0: } michael@0: }, michael@0: michael@0: zoomIn: function pdfViewZoomIn(ticks) { michael@0: var newScale = this.currentScale; michael@0: do { michael@0: newScale = (newScale * DEFAULT_SCALE_DELTA).toFixed(2); michael@0: newScale = Math.ceil(newScale * 10) / 10; michael@0: newScale = Math.min(MAX_SCALE, newScale); michael@0: } while (--ticks && newScale < MAX_SCALE); michael@0: this.setScale(newScale, true); michael@0: }, michael@0: michael@0: zoomOut: function pdfViewZoomOut(ticks) { michael@0: var newScale = this.currentScale; michael@0: do { michael@0: newScale = (newScale / DEFAULT_SCALE_DELTA).toFixed(2); michael@0: newScale = Math.floor(newScale * 10) / 10; michael@0: newScale = Math.max(MIN_SCALE, newScale); michael@0: } while (--ticks && newScale > MIN_SCALE); michael@0: this.setScale(newScale, true); michael@0: }, michael@0: michael@0: set page(val) { michael@0: var pages = this.pages; michael@0: var event = document.createEvent('UIEvents'); michael@0: event.initUIEvent('pagechange', false, false, window, 0); michael@0: michael@0: if (!(0 < val && val <= pages.length)) { michael@0: this.previousPageNumber = val; michael@0: event.pageNumber = this.page; michael@0: window.dispatchEvent(event); michael@0: return; michael@0: } michael@0: michael@0: pages[val - 1].updateStats(); michael@0: this.previousPageNumber = currentPageNumber; michael@0: currentPageNumber = val; michael@0: event.pageNumber = val; michael@0: window.dispatchEvent(event); michael@0: michael@0: // checking if the this.page was called from the updateViewarea function: michael@0: // avoiding the creation of two "set page" method (internal and public) michael@0: if (updateViewarea.inProgress) { michael@0: return; michael@0: } michael@0: // Avoid scrolling the first page during loading michael@0: if (this.loading && val === 1) { michael@0: return; michael@0: } michael@0: pages[val - 1].scrollIntoView(); michael@0: }, michael@0: michael@0: get page() { michael@0: return currentPageNumber; michael@0: }, michael@0: michael@0: get supportsPrinting() { michael@0: var canvas = document.createElement('canvas'); michael@0: var value = 'mozPrintCallback' in canvas; michael@0: // shadow michael@0: Object.defineProperty(this, 'supportsPrinting', { value: value, michael@0: enumerable: true, michael@0: configurable: true, michael@0: writable: false }); michael@0: return value; michael@0: }, michael@0: michael@0: get supportsFullscreen() { michael@0: var doc = document.documentElement; michael@0: var support = doc.requestFullscreen || doc.mozRequestFullScreen || michael@0: doc.webkitRequestFullScreen || doc.msRequestFullscreen; michael@0: michael@0: if (document.fullscreenEnabled === false || michael@0: document.mozFullScreenEnabled === false || michael@0: document.webkitFullscreenEnabled === false || michael@0: document.msFullscreenEnabled === false) { michael@0: support = false; michael@0: } michael@0: michael@0: Object.defineProperty(this, 'supportsFullscreen', { value: support, michael@0: enumerable: true, michael@0: configurable: true, michael@0: writable: false }); michael@0: return support; michael@0: }, michael@0: michael@0: get supportsIntegratedFind() { michael@0: var support = false; michael@0: support = FirefoxCom.requestSync('supportsIntegratedFind'); michael@0: Object.defineProperty(this, 'supportsIntegratedFind', { value: support, michael@0: enumerable: true, michael@0: configurable: true, michael@0: writable: false }); michael@0: return support; michael@0: }, michael@0: michael@0: get supportsDocumentFonts() { michael@0: var support = true; michael@0: support = FirefoxCom.requestSync('supportsDocumentFonts'); michael@0: Object.defineProperty(this, 'supportsDocumentFonts', { value: support, michael@0: enumerable: true, michael@0: configurable: true, michael@0: writable: false }); michael@0: return support; michael@0: }, michael@0: michael@0: get supportsDocumentColors() { michael@0: var support = true; michael@0: support = FirefoxCom.requestSync('supportsDocumentColors'); michael@0: Object.defineProperty(this, 'supportsDocumentColors', { value: support, michael@0: enumerable: true, michael@0: configurable: true, michael@0: writable: false }); michael@0: return support; michael@0: }, michael@0: michael@0: get loadingBar() { michael@0: var bar = new ProgressBar('#loadingBar', {}); michael@0: Object.defineProperty(this, 'loadingBar', { value: bar, michael@0: enumerable: true, michael@0: configurable: true, michael@0: writable: false }); michael@0: return bar; michael@0: }, michael@0: michael@0: get isHorizontalScrollbarEnabled() { michael@0: return (PresentationMode.active ? false : michael@0: (this.container.scrollWidth > this.container.clientWidth)); michael@0: }, michael@0: michael@0: initPassiveLoading: function pdfViewInitPassiveLoading() { michael@0: var pdfDataRangeTransport = { michael@0: rangeListeners: [], michael@0: progressListeners: [], michael@0: michael@0: addRangeListener: function PdfDataRangeTransport_addRangeListener( michael@0: listener) { michael@0: this.rangeListeners.push(listener); michael@0: }, michael@0: michael@0: addProgressListener: function PdfDataRangeTransport_addProgressListener( michael@0: listener) { michael@0: this.progressListeners.push(listener); michael@0: }, michael@0: michael@0: onDataRange: function PdfDataRangeTransport_onDataRange(begin, chunk) { michael@0: var listeners = this.rangeListeners; michael@0: for (var i = 0, n = listeners.length; i < n; ++i) { michael@0: listeners[i](begin, chunk); michael@0: } michael@0: }, michael@0: michael@0: onDataProgress: function PdfDataRangeTransport_onDataProgress(loaded) { michael@0: var listeners = this.progressListeners; michael@0: for (var i = 0, n = listeners.length; i < n; ++i) { michael@0: listeners[i](loaded); michael@0: } michael@0: }, michael@0: michael@0: requestDataRange: function PdfDataRangeTransport_requestDataRange( michael@0: begin, end) { michael@0: FirefoxCom.request('requestDataRange', { begin: begin, end: end }); michael@0: } michael@0: }; michael@0: michael@0: window.addEventListener('message', function windowMessage(e) { michael@0: if (e.source !== null) { michael@0: // The message MUST originate from Chrome code. michael@0: console.warn('Rejected untrusted message from ' + e.origin); michael@0: return; michael@0: } michael@0: var args = e.data; michael@0: michael@0: if (typeof args !== 'object' || !('pdfjsLoadAction' in args)) { michael@0: return; michael@0: } michael@0: switch (args.pdfjsLoadAction) { michael@0: case 'supportsRangedLoading': michael@0: PDFView.open(args.pdfUrl, 0, undefined, pdfDataRangeTransport, { michael@0: length: args.length, michael@0: initialData: args.data michael@0: }); michael@0: break; michael@0: case 'range': michael@0: pdfDataRangeTransport.onDataRange(args.begin, args.chunk); michael@0: break; michael@0: case 'rangeProgress': michael@0: pdfDataRangeTransport.onDataProgress(args.loaded); michael@0: break; michael@0: case 'progress': michael@0: PDFView.progress(args.loaded / args.total); michael@0: break; michael@0: case 'complete': michael@0: if (!args.data) { michael@0: PDFView.error(mozL10n.get('loading_error', null, michael@0: 'An error occurred while loading the PDF.'), e); michael@0: break; michael@0: } michael@0: PDFView.open(args.data, 0); michael@0: break; michael@0: } michael@0: }); michael@0: FirefoxCom.requestSync('initPassiveLoading', null); michael@0: }, michael@0: michael@0: setTitleUsingUrl: function pdfViewSetTitleUsingUrl(url) { michael@0: this.url = url; michael@0: try { michael@0: this.setTitle(decodeURIComponent(getFileName(url)) || url); michael@0: } catch (e) { michael@0: // decodeURIComponent may throw URIError, michael@0: // fall back to using the unprocessed url in that case michael@0: this.setTitle(url); michael@0: } michael@0: }, michael@0: michael@0: setTitle: function pdfViewSetTitle(title) { michael@0: document.title = title; michael@0: }, michael@0: michael@0: close: function pdfViewClose() { michael@0: var errorWrapper = document.getElementById('errorWrapper'); michael@0: errorWrapper.setAttribute('hidden', 'true'); michael@0: michael@0: if (!this.pdfDocument) { michael@0: return; michael@0: } michael@0: michael@0: this.pdfDocument.destroy(); michael@0: this.pdfDocument = null; michael@0: michael@0: var thumbsView = document.getElementById('thumbnailView'); michael@0: while (thumbsView.hasChildNodes()) { michael@0: thumbsView.removeChild(thumbsView.lastChild); michael@0: } michael@0: michael@0: if ('_loadingInterval' in thumbsView) { michael@0: clearInterval(thumbsView._loadingInterval); michael@0: } michael@0: michael@0: var container = document.getElementById('viewer'); michael@0: while (container.hasChildNodes()) { michael@0: container.removeChild(container.lastChild); michael@0: } michael@0: michael@0: if (typeof PDFBug !== 'undefined') { michael@0: PDFBug.cleanup(); michael@0: } michael@0: }, michael@0: michael@0: // TODO(mack): This function signature should really be pdfViewOpen(url, args) michael@0: open: function pdfViewOpen(url, scale, password, michael@0: pdfDataRangeTransport, args) { michael@0: if (this.pdfDocument) { michael@0: // Reload the preferences if a document was previously opened. michael@0: Preferences.reload(); michael@0: } michael@0: this.close(); michael@0: michael@0: var parameters = {password: password}; michael@0: if (typeof url === 'string') { // URL michael@0: this.setTitleUsingUrl(url); michael@0: parameters.url = url; michael@0: } else if (url && 'byteLength' in url) { // ArrayBuffer michael@0: parameters.data = url; michael@0: } michael@0: if (args) { michael@0: for (var prop in args) { michael@0: parameters[prop] = args[prop]; michael@0: } michael@0: } michael@0: michael@0: var self = this; michael@0: self.loading = true; michael@0: self.downloadComplete = false; michael@0: michael@0: var passwordNeeded = function passwordNeeded(updatePassword, reason) { michael@0: PasswordPrompt.updatePassword = updatePassword; michael@0: PasswordPrompt.reason = reason; michael@0: PasswordPrompt.show(); michael@0: }; michael@0: michael@0: function getDocumentProgress(progressData) { michael@0: self.progress(progressData.loaded / progressData.total); michael@0: } michael@0: michael@0: PDFJS.getDocument(parameters, pdfDataRangeTransport, passwordNeeded, michael@0: getDocumentProgress).then( michael@0: function getDocumentCallback(pdfDocument) { michael@0: self.load(pdfDocument, scale); michael@0: self.loading = false; michael@0: }, michael@0: function getDocumentError(message, exception) { michael@0: var loadingErrorMessage = mozL10n.get('loading_error', null, michael@0: 'An error occurred while loading the PDF.'); michael@0: michael@0: if (exception && exception.name === 'InvalidPDFException') { michael@0: // change error message also for other builds michael@0: loadingErrorMessage = mozL10n.get('invalid_file_error', null, michael@0: 'Invalid or corrupted PDF file.'); michael@0: } michael@0: michael@0: if (exception && exception.name === 'MissingPDFException') { michael@0: // special message for missing PDF's michael@0: loadingErrorMessage = mozL10n.get('missing_file_error', null, michael@0: 'Missing PDF file.'); michael@0: michael@0: } michael@0: michael@0: var moreInfo = { michael@0: message: message michael@0: }; michael@0: self.error(loadingErrorMessage, moreInfo); michael@0: self.loading = false; michael@0: } michael@0: ); michael@0: }, michael@0: michael@0: download: function pdfViewDownload() { michael@0: function downloadByUrl() { michael@0: downloadManager.downloadUrl(url, filename); michael@0: } michael@0: michael@0: var url = this.url.split('#')[0]; michael@0: var filename = getPDFFileNameFromURL(url); michael@0: var downloadManager = new DownloadManager(); michael@0: downloadManager.onerror = function (err) { michael@0: // This error won't really be helpful because it's likely the michael@0: // fallback won't work either (or is already open). michael@0: PDFView.error('PDF failed to download.'); michael@0: }; michael@0: michael@0: if (!this.pdfDocument) { // the PDF is not ready yet michael@0: downloadByUrl(); michael@0: return; michael@0: } michael@0: michael@0: if (!this.downloadComplete) { // the PDF is still downloading michael@0: downloadByUrl(); michael@0: return; michael@0: } michael@0: michael@0: this.pdfDocument.getData().then( michael@0: function getDataSuccess(data) { michael@0: var blob = PDFJS.createBlob(data, 'application/pdf'); michael@0: downloadManager.download(blob, url, filename); michael@0: }, michael@0: downloadByUrl // Error occurred try downloading with just the url. michael@0: ).then(null, downloadByUrl); michael@0: }, michael@0: michael@0: fallback: function pdfViewFallback(featureId) { michael@0: // Only trigger the fallback once so we don't spam the user with messages michael@0: // for one PDF. michael@0: if (this.fellback) michael@0: return; michael@0: this.fellback = true; michael@0: var url = this.url.split('#')[0]; michael@0: FirefoxCom.request('fallback', { featureId: featureId, url: url }, michael@0: function response(download) { michael@0: if (!download) { michael@0: return; michael@0: } michael@0: PDFView.download(); michael@0: }); michael@0: }, michael@0: michael@0: navigateTo: function pdfViewNavigateTo(dest) { michael@0: var destString = ''; michael@0: var self = this; michael@0: michael@0: var goToDestination = function(destRef) { michael@0: self.pendingRefStr = null; michael@0: // dest array looks like that: michael@0: var pageNumber = destRef instanceof Object ? michael@0: self.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : michael@0: (destRef + 1); michael@0: if (pageNumber) { michael@0: if (pageNumber > self.pages.length) { michael@0: pageNumber = self.pages.length; michael@0: } michael@0: var currentPage = self.pages[pageNumber - 1]; michael@0: currentPage.scrollIntoView(dest); michael@0: michael@0: // Update the browsing history. michael@0: PDFHistory.push({ dest: dest, hash: destString, page: pageNumber }); michael@0: } else { michael@0: self.pdfDocument.getPageIndex(destRef).then(function (pageIndex) { michael@0: var pageNum = pageIndex + 1; michael@0: self.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] = pageNum; michael@0: goToDestination(destRef); michael@0: }); michael@0: } michael@0: }; michael@0: michael@0: this.destinationsPromise.then(function() { michael@0: if (typeof dest === 'string') { michael@0: destString = dest; michael@0: dest = self.destinations[dest]; michael@0: } michael@0: if (!(dest instanceof Array)) { michael@0: return; // invalid destination michael@0: } michael@0: goToDestination(dest[0]); michael@0: }); michael@0: }, michael@0: michael@0: getDestinationHash: function pdfViewGetDestinationHash(dest) { michael@0: if (typeof dest === 'string') { michael@0: return PDFView.getAnchorUrl('#' + escape(dest)); michael@0: } michael@0: if (dest instanceof Array) { michael@0: var destRef = dest[0]; // see navigateTo method for dest format michael@0: var pageNumber = destRef instanceof Object ? michael@0: this.pagesRefMap[destRef.num + ' ' + destRef.gen + ' R'] : michael@0: (destRef + 1); michael@0: if (pageNumber) { michael@0: var pdfOpenParams = PDFView.getAnchorUrl('#page=' + pageNumber); michael@0: var destKind = dest[1]; michael@0: if (typeof destKind === 'object' && 'name' in destKind && michael@0: destKind.name == 'XYZ') { michael@0: var scale = (dest[4] || this.currentScaleValue); michael@0: var scaleNumber = parseFloat(scale); michael@0: if (scaleNumber) { michael@0: scale = scaleNumber * 100; michael@0: } michael@0: pdfOpenParams += '&zoom=' + scale; michael@0: if (dest[2] || dest[3]) { michael@0: pdfOpenParams += ',' + (dest[2] || 0) + ',' + (dest[3] || 0); michael@0: } michael@0: } michael@0: return pdfOpenParams; michael@0: } michael@0: } michael@0: return ''; michael@0: }, michael@0: michael@0: /** michael@0: * Prefix the full url on anchor links to make sure that links are resolved michael@0: * relative to the current URL instead of the one defined in . michael@0: * @param {String} anchor The anchor hash, including the #. michael@0: */ michael@0: getAnchorUrl: function getAnchorUrl(anchor) { michael@0: return this.url.split('#')[0] + anchor; michael@0: }, michael@0: michael@0: /** michael@0: * Show the error box. michael@0: * @param {String} message A message that is human readable. michael@0: * @param {Object} moreInfo (optional) Further information about the error michael@0: * that is more technical. Should have a 'message' michael@0: * and optionally a 'stack' property. michael@0: */ michael@0: error: function pdfViewError(message, moreInfo) { michael@0: var moreInfoText = mozL10n.get('error_version_info', michael@0: {version: PDFJS.version || '?', build: PDFJS.build || '?'}, michael@0: 'PDF.js v{{version}} (build: {{build}})') + '\n'; michael@0: if (moreInfo) { michael@0: moreInfoText += michael@0: mozL10n.get('error_message', {message: moreInfo.message}, michael@0: 'Message: {{message}}'); michael@0: if (moreInfo.stack) { michael@0: moreInfoText += '\n' + michael@0: mozL10n.get('error_stack', {stack: moreInfo.stack}, michael@0: 'Stack: {{stack}}'); michael@0: } else { michael@0: if (moreInfo.filename) { michael@0: moreInfoText += '\n' + michael@0: mozL10n.get('error_file', {file: moreInfo.filename}, michael@0: 'File: {{file}}'); michael@0: } michael@0: if (moreInfo.lineNumber) { michael@0: moreInfoText += '\n' + michael@0: mozL10n.get('error_line', {line: moreInfo.lineNumber}, michael@0: 'Line: {{line}}'); michael@0: } michael@0: } michael@0: } michael@0: michael@0: console.error(message + '\n' + moreInfoText); michael@0: this.fallback(); michael@0: }, michael@0: michael@0: progress: function pdfViewProgress(level) { michael@0: var percent = Math.round(level * 100); michael@0: // When we transition from full request to range requests, it's possible michael@0: // that we discard some of the loaded data. This can cause the loading michael@0: // bar to move backwards. So prevent this by only updating the bar if it michael@0: // increases. michael@0: if (percent > PDFView.loadingBar.percent) { michael@0: PDFView.loadingBar.percent = percent; michael@0: } michael@0: }, michael@0: michael@0: load: function pdfViewLoad(pdfDocument, scale) { michael@0: var self = this; michael@0: var isOnePageRenderedResolved = false; michael@0: var resolveOnePageRendered = null; michael@0: var onePageRendered = new Promise(function (resolve) { michael@0: resolveOnePageRendered = resolve; michael@0: }); michael@0: function bindOnAfterDraw(pageView, thumbnailView) { michael@0: // when page is painted, using the image as thumbnail base michael@0: pageView.onAfterDraw = function pdfViewLoadOnAfterDraw() { michael@0: if (!isOnePageRenderedResolved) { michael@0: isOnePageRenderedResolved = true; michael@0: resolveOnePageRendered(); michael@0: } michael@0: thumbnailView.setImage(pageView.canvas); michael@0: }; michael@0: } michael@0: michael@0: PDFFindController.reset(); michael@0: michael@0: this.pdfDocument = pdfDocument; michael@0: michael@0: DocumentProperties.resolveDataAvailable(); michael@0: michael@0: var downloadedPromise = pdfDocument.getDownloadInfo().then(function() { michael@0: self.downloadComplete = true; michael@0: PDFView.loadingBar.hide(); michael@0: var outerContainer = document.getElementById('outerContainer'); michael@0: outerContainer.classList.remove('loadingInProgress'); michael@0: }); michael@0: michael@0: var pagesCount = pdfDocument.numPages; michael@0: michael@0: var id = pdfDocument.fingerprint; michael@0: document.getElementById('numPages').textContent = michael@0: mozL10n.get('page_of', {pageCount: pagesCount}, 'of {{pageCount}}'); michael@0: document.getElementById('pageNumber').max = pagesCount; michael@0: michael@0: PDFView.documentFingerprint = id; michael@0: var store = PDFView.store = new ViewHistory(id); michael@0: michael@0: this.pageRotation = 0; michael@0: michael@0: var pages = this.pages = []; michael@0: var pagesRefMap = this.pagesRefMap = {}; michael@0: var thumbnails = this.thumbnails = []; michael@0: michael@0: var resolvePagesPromise; michael@0: var pagesPromise = new Promise(function (resolve) { michael@0: resolvePagesPromise = resolve; michael@0: }); michael@0: this.pagesPromise = pagesPromise; michael@0: michael@0: var firstPagePromise = pdfDocument.getPage(1); michael@0: var container = document.getElementById('viewer'); michael@0: var thumbsView = document.getElementById('thumbnailView'); michael@0: michael@0: // Fetch a single page so we can get a viewport that will be the default michael@0: // viewport for all pages michael@0: firstPagePromise.then(function(pdfPage) { michael@0: var viewport = pdfPage.getViewport((scale || 1.0) * CSS_UNITS); michael@0: for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { michael@0: var viewportClone = viewport.clone(); michael@0: var pageView = new PageView(container, pageNum, scale, michael@0: self.navigateTo.bind(self), michael@0: viewportClone); michael@0: var thumbnailView = new ThumbnailView(thumbsView, pageNum, michael@0: viewportClone); michael@0: bindOnAfterDraw(pageView, thumbnailView); michael@0: pages.push(pageView); michael@0: thumbnails.push(thumbnailView); michael@0: } michael@0: michael@0: // Fetch all the pages since the viewport is needed before printing michael@0: // starts to create the correct size canvas. Wait until one page is michael@0: // rendered so we don't tie up too many resources early on. michael@0: onePageRendered.then(function () { michael@0: if (!PDFJS.disableAutoFetch) { michael@0: var getPagesLeft = pagesCount; michael@0: for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { michael@0: pdfDocument.getPage(pageNum).then(function (pageNum, pdfPage) { michael@0: var pageView = pages[pageNum - 1]; michael@0: if (!pageView.pdfPage) { michael@0: pageView.setPdfPage(pdfPage); michael@0: } michael@0: var refStr = pdfPage.ref.num + ' ' + pdfPage.ref.gen + ' R'; michael@0: pagesRefMap[refStr] = pageNum; michael@0: getPagesLeft--; michael@0: if (!getPagesLeft) { michael@0: resolvePagesPromise(); michael@0: } michael@0: }.bind(null, pageNum)); michael@0: } michael@0: } else { michael@0: // XXX: Printing is semi-broken with auto fetch disabled. michael@0: resolvePagesPromise(); michael@0: } michael@0: }); michael@0: michael@0: downloadedPromise.then(function () { michael@0: var event = document.createEvent('CustomEvent'); michael@0: event.initCustomEvent('documentload', true, true, {}); michael@0: window.dispatchEvent(event); michael@0: }); michael@0: michael@0: PDFView.loadingBar.setWidth(container); michael@0: michael@0: PDFFindController.resolveFirstPage(); michael@0: michael@0: // Initialize the browsing history. michael@0: PDFHistory.initialize(self.documentFingerprint); michael@0: }); michael@0: michael@0: // Fetch the necessary preference values. michael@0: var showPreviousViewOnLoad; michael@0: var showPreviousViewOnLoadPromise = michael@0: Preferences.get('showPreviousViewOnLoad').then(function (prefValue) { michael@0: showPreviousViewOnLoad = prefValue; michael@0: }); michael@0: var defaultZoomValue; michael@0: var defaultZoomValuePromise = michael@0: Preferences.get('defaultZoomValue').then(function (prefValue) { michael@0: defaultZoomValue = prefValue; michael@0: }); michael@0: michael@0: var storePromise = store.initializedPromise; michael@0: Promise.all([firstPagePromise, storePromise, showPreviousViewOnLoadPromise, michael@0: defaultZoomValuePromise]).then(function resolved() { michael@0: var storedHash = null; michael@0: if (showPreviousViewOnLoad && store.get('exists', false)) { michael@0: var pageNum = store.get('page', '1'); michael@0: var zoom = defaultZoomValue || store.get('zoom', PDFView.currentScale); michael@0: var left = store.get('scrollLeft', '0'); michael@0: var top = store.get('scrollTop', '0'); michael@0: michael@0: storedHash = 'page=' + pageNum + '&zoom=' + zoom + ',' + michael@0: left + ',' + top; michael@0: } else if (defaultZoomValue) { michael@0: storedHash = 'page=1&zoom=' + defaultZoomValue; michael@0: } michael@0: self.setInitialView(storedHash, scale); michael@0: michael@0: // Make all navigation keys work on document load, michael@0: // unless the viewer is embedded in a web page. michael@0: if (!self.isViewerEmbedded) { michael@0: self.container.focus(); michael@0: self.container.blur(); michael@0: } michael@0: }, function rejected(errorMsg) { michael@0: console.error(errorMsg); michael@0: michael@0: firstPagePromise.then(function () { michael@0: self.setInitialView(null, scale); michael@0: }); michael@0: }); michael@0: michael@0: pagesPromise.then(function() { michael@0: if (PDFView.supportsPrinting) { michael@0: pdfDocument.getJavaScript().then(function(javaScript) { michael@0: if (javaScript.length) { michael@0: console.warn('Warning: JavaScript is not supported'); michael@0: PDFView.fallback(PDFJS.UNSUPPORTED_FEATURES.javaScript); michael@0: } michael@0: // Hack to support auto printing. michael@0: var regex = /\bprint\s*\(/g; michael@0: for (var i = 0, ii = javaScript.length; i < ii; i++) { michael@0: var js = javaScript[i]; michael@0: if (js && regex.test(js)) { michael@0: setTimeout(function() { michael@0: window.print(); michael@0: }); michael@0: return; michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: }); michael@0: michael@0: var destinationsPromise = michael@0: this.destinationsPromise = pdfDocument.getDestinations(); michael@0: destinationsPromise.then(function(destinations) { michael@0: self.destinations = destinations; michael@0: }); michael@0: michael@0: // outline depends on destinations and pagesRefMap michael@0: var promises = [pagesPromise, destinationsPromise, michael@0: PDFView.animationStartedPromise]; michael@0: Promise.all(promises).then(function() { michael@0: pdfDocument.getOutline().then(function(outline) { michael@0: self.outline = new DocumentOutlineView(outline); michael@0: document.getElementById('viewOutline').disabled = !outline; michael@0: michael@0: if (outline) { michael@0: Preferences.get('ifAvailableShowOutlineOnLoad').then( michael@0: function (prefValue) { michael@0: if (prefValue) { michael@0: if (!self.sidebarOpen) { michael@0: document.getElementById('sidebarToggle').click(); michael@0: } michael@0: self.switchSidebarView('outline'); michael@0: } michael@0: }); michael@0: } michael@0: }); michael@0: pdfDocument.getAttachments().then(function(attachments) { michael@0: self.attachments = new DocumentAttachmentsView(attachments); michael@0: document.getElementById('viewAttachments').disabled = !attachments; michael@0: }); michael@0: }); michael@0: michael@0: pdfDocument.getMetadata().then(function(data) { michael@0: var info = data.info, metadata = data.metadata; michael@0: self.documentInfo = info; michael@0: self.metadata = metadata; michael@0: michael@0: // Provides some basic debug information michael@0: console.log('PDF ' + pdfDocument.fingerprint + ' [' + michael@0: info.PDFFormatVersion + ' ' + (info.Producer || '-').trim() + michael@0: ' / ' + (info.Creator || '-').trim() + ']' + michael@0: ' (PDF.js: ' + (PDFJS.version || '-') + michael@0: (!PDFJS.disableWebGL ? ' [WebGL]' : '') + ')'); michael@0: michael@0: var pdfTitle; michael@0: if (metadata && metadata.has('dc:title')) { michael@0: pdfTitle = metadata.get('dc:title'); michael@0: } michael@0: michael@0: if (!pdfTitle && info && info['Title']) { michael@0: pdfTitle = info['Title']; michael@0: } michael@0: michael@0: if (pdfTitle) { michael@0: self.setTitle(pdfTitle + ' - ' + document.title); michael@0: } michael@0: michael@0: if (info.IsAcroFormPresent) { michael@0: console.warn('Warning: AcroForm/XFA is not supported'); michael@0: PDFView.fallback(PDFJS.UNSUPPORTED_FEATURES.forms); michael@0: } michael@0: michael@0: var versionId = String(info.PDFFormatVersion).slice(-1) | 0; michael@0: var generatorId = 0; michael@0: var KNOWN_GENERATORS = ["acrobat distiller", "acrobat pdfwritter", michael@0: "adobe livecycle", "adobe pdf library", "adobe photoshop", "ghostscript", michael@0: "tcpdf", "cairo", "dvipdfm", "dvips", "pdftex", "pdfkit", "itext", michael@0: "prince", "quarkxpress", "mac os x", "microsoft", "openoffice", "oracle", michael@0: "luradocument", "pdf-xchange", "antenna house", "aspose.cells", "fpdf"]; michael@0: var generatorId = 0; michael@0: if (info.Producer) { michael@0: KNOWN_GENERATORS.some(function (generator, s, i) { michael@0: if (generator.indexOf(s) < 0) { michael@0: return false; michael@0: } michael@0: generatorId = i + 1; michael@0: return true; michael@0: }.bind(null, info.Producer.toLowerCase())); michael@0: } michael@0: var formType = !info.IsAcroFormPresent ? null : info.IsXFAPresent ? michael@0: 'xfa' : 'acroform'; michael@0: FirefoxCom.request('reportTelemetry', JSON.stringify({ michael@0: type: 'documentInfo', michael@0: version: versionId, michael@0: generator: generatorId, michael@0: formType: formType michael@0: })); michael@0: }); michael@0: }, michael@0: michael@0: setInitialView: function pdfViewSetInitialView(storedHash, scale) { michael@0: // Reset the current scale, as otherwise the page's scale might not get michael@0: // updated if the zoom level stayed the same. michael@0: this.currentScale = 0; michael@0: this.currentScaleValue = null; michael@0: // When opening a new file (when one is already loaded in the viewer): michael@0: // Reset 'currentPageNumber', since otherwise the page's scale will be wrong michael@0: // if 'currentPageNumber' is larger than the number of pages in the file. michael@0: document.getElementById('pageNumber').value = currentPageNumber = 1; michael@0: // Reset the current position when loading a new file, michael@0: // to prevent displaying the wrong position in the document. michael@0: this.currentPosition = null; michael@0: michael@0: if (PDFHistory.initialDestination) { michael@0: this.navigateTo(PDFHistory.initialDestination); michael@0: PDFHistory.initialDestination = null; michael@0: } else if (this.initialBookmark) { michael@0: this.setHash(this.initialBookmark); michael@0: PDFHistory.push({ hash: this.initialBookmark }, !!this.initialBookmark); michael@0: this.initialBookmark = null; michael@0: } else if (storedHash) { michael@0: this.setHash(storedHash); michael@0: } else if (scale) { michael@0: this.setScale(scale, true); michael@0: this.page = 1; michael@0: } michael@0: michael@0: if (PDFView.currentScale === UNKNOWN_SCALE) { michael@0: // Scale was not initialized: invalid bookmark or scale was not specified. michael@0: // Setting the default one. michael@0: this.setScale(DEFAULT_SCALE, true); michael@0: } michael@0: }, michael@0: michael@0: renderHighestPriority: function pdfViewRenderHighestPriority() { michael@0: if (PDFView.idleTimeout) { michael@0: clearTimeout(PDFView.idleTimeout); michael@0: PDFView.idleTimeout = null; michael@0: } michael@0: michael@0: // Pages have a higher priority than thumbnails, so check them first. michael@0: var visiblePages = this.getVisiblePages(); michael@0: var pageView = this.getHighestPriority(visiblePages, this.pages, michael@0: this.pageViewScroll.down); michael@0: if (pageView) { michael@0: this.renderView(pageView, 'page'); michael@0: return; michael@0: } michael@0: // No pages needed rendering so check thumbnails. michael@0: if (this.sidebarOpen) { michael@0: var visibleThumbs = this.getVisibleThumbs(); michael@0: var thumbView = this.getHighestPriority(visibleThumbs, michael@0: this.thumbnails, michael@0: this.thumbnailViewScroll.down); michael@0: if (thumbView) { michael@0: this.renderView(thumbView, 'thumbnail'); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: PDFView.idleTimeout = setTimeout(function () { michael@0: PDFView.cleanup(); michael@0: }, CLEANUP_TIMEOUT); michael@0: }, michael@0: michael@0: cleanup: function pdfViewCleanup() { michael@0: for (var i = 0, ii = this.pages.length; i < ii; i++) { michael@0: if (this.pages[i] && michael@0: this.pages[i].renderingState !== RenderingStates.FINISHED) { michael@0: this.pages[i].reset(); michael@0: } michael@0: } michael@0: this.pdfDocument.cleanup(); michael@0: }, michael@0: michael@0: getHighestPriority: function pdfViewGetHighestPriority(visible, views, michael@0: scrolledDown) { michael@0: // The state has changed figure out which page has the highest priority to michael@0: // render next (if any). michael@0: // Priority: michael@0: // 1 visible pages michael@0: // 2 if last scrolled down page after the visible pages michael@0: // 2 if last scrolled up page before the visible pages michael@0: var visibleViews = visible.views; michael@0: michael@0: var numVisible = visibleViews.length; michael@0: if (numVisible === 0) { michael@0: return false; michael@0: } michael@0: for (var i = 0; i < numVisible; ++i) { michael@0: var view = visibleViews[i].view; michael@0: if (!this.isViewFinished(view)) { michael@0: return view; michael@0: } michael@0: } michael@0: michael@0: // All the visible views have rendered, try to render next/previous pages. michael@0: if (scrolledDown) { michael@0: var nextPageIndex = visible.last.id; michael@0: // ID's start at 1 so no need to add 1. michael@0: if (views[nextPageIndex] && !this.isViewFinished(views[nextPageIndex])) { michael@0: return views[nextPageIndex]; michael@0: } michael@0: } else { michael@0: var previousPageIndex = visible.first.id - 2; michael@0: if (views[previousPageIndex] && michael@0: !this.isViewFinished(views[previousPageIndex])) { michael@0: return views[previousPageIndex]; michael@0: } michael@0: } michael@0: // Everything that needs to be rendered has been. michael@0: return false; michael@0: }, michael@0: michael@0: isViewFinished: function pdfViewIsViewFinished(view) { michael@0: return view.renderingState === RenderingStates.FINISHED; michael@0: }, michael@0: michael@0: // Render a page or thumbnail view. This calls the appropriate function based michael@0: // on the views state. If the view is already rendered it will return false. michael@0: renderView: function pdfViewRender(view, type) { michael@0: var state = view.renderingState; michael@0: switch (state) { michael@0: case RenderingStates.FINISHED: michael@0: return false; michael@0: case RenderingStates.PAUSED: michael@0: PDFView.highestPriorityPage = type + view.id; michael@0: view.resume(); michael@0: break; michael@0: case RenderingStates.RUNNING: michael@0: PDFView.highestPriorityPage = type + view.id; michael@0: break; michael@0: case RenderingStates.INITIAL: michael@0: PDFView.highestPriorityPage = type + view.id; michael@0: view.draw(this.renderHighestPriority.bind(this)); michael@0: break; michael@0: } michael@0: return true; michael@0: }, michael@0: michael@0: setHash: function pdfViewSetHash(hash) { michael@0: if (!hash) { michael@0: return; michael@0: } michael@0: michael@0: if (hash.indexOf('=') >= 0) { michael@0: var params = PDFView.parseQueryString(hash); michael@0: // borrowing syntax from "Parameters for Opening PDF Files" michael@0: if ('nameddest' in params) { michael@0: PDFHistory.updateNextHashParam(params.nameddest); michael@0: PDFView.navigateTo(params.nameddest); michael@0: return; michael@0: } michael@0: var pageNumber, dest; michael@0: if ('page' in params) { michael@0: pageNumber = (params.page | 0) || 1; michael@0: } michael@0: if ('zoom' in params) { michael@0: var zoomArgs = params.zoom.split(','); // scale,left,top michael@0: // building destination array michael@0: michael@0: // If the zoom value, it has to get divided by 100. If it is a string, michael@0: // it should stay as it is. michael@0: var zoomArg = zoomArgs[0]; michael@0: var zoomArgNumber = parseFloat(zoomArg); michael@0: if (zoomArgNumber) { michael@0: zoomArg = zoomArgNumber / 100; michael@0: } michael@0: dest = [null, {name: 'XYZ'}, michael@0: zoomArgs.length > 1 ? (zoomArgs[1] | 0) : null, michael@0: zoomArgs.length > 2 ? (zoomArgs[2] | 0) : null, michael@0: zoomArg]; michael@0: } michael@0: if (dest) { michael@0: var currentPage = this.pages[(pageNumber || this.page) - 1]; michael@0: currentPage.scrollIntoView(dest); michael@0: } else if (pageNumber) { michael@0: this.page = pageNumber; // simple page michael@0: } michael@0: if ('pagemode' in params) { michael@0: var toggle = document.getElementById('sidebarToggle'); michael@0: if (params.pagemode === 'thumbs' || params.pagemode === 'bookmarks' || michael@0: params.pagemode === 'attachments') { michael@0: if (!this.sidebarOpen) { michael@0: toggle.click(); michael@0: } michael@0: this.switchSidebarView(params.pagemode === 'bookmarks' ? michael@0: 'outline' : michael@0: params.pagemode); michael@0: } else if (params.pagemode === 'none' && this.sidebarOpen) { michael@0: toggle.click(); michael@0: } michael@0: } michael@0: } else if (/^\d+$/.test(hash)) { // page number michael@0: this.page = hash; michael@0: } else { // named destination michael@0: PDFHistory.updateNextHashParam(unescape(hash)); michael@0: PDFView.navigateTo(unescape(hash)); michael@0: } michael@0: }, michael@0: michael@0: switchSidebarView: function pdfViewSwitchSidebarView(view) { michael@0: var thumbsView = document.getElementById('thumbnailView'); michael@0: var outlineView = document.getElementById('outlineView'); michael@0: var attachmentsView = document.getElementById('attachmentsView'); michael@0: michael@0: var thumbsButton = document.getElementById('viewThumbnail'); michael@0: var outlineButton = document.getElementById('viewOutline'); michael@0: var attachmentsButton = document.getElementById('viewAttachments'); michael@0: michael@0: switch (view) { michael@0: case 'thumbs': michael@0: var wasAnotherViewVisible = thumbsView.classList.contains('hidden'); michael@0: michael@0: thumbsButton.classList.add('toggled'); michael@0: outlineButton.classList.remove('toggled'); michael@0: attachmentsButton.classList.remove('toggled'); michael@0: thumbsView.classList.remove('hidden'); michael@0: outlineView.classList.add('hidden'); michael@0: attachmentsView.classList.add('hidden'); michael@0: michael@0: PDFView.renderHighestPriority(); michael@0: michael@0: if (wasAnotherViewVisible) { michael@0: // Ensure that the thumbnail of the current page is visible michael@0: // when switching from another view. michael@0: scrollIntoView(document.getElementById('thumbnailContainer' + michael@0: this.page)); michael@0: } michael@0: break; michael@0: michael@0: case 'outline': michael@0: thumbsButton.classList.remove('toggled'); michael@0: outlineButton.classList.add('toggled'); michael@0: attachmentsButton.classList.remove('toggled'); michael@0: thumbsView.classList.add('hidden'); michael@0: outlineView.classList.remove('hidden'); michael@0: attachmentsView.classList.add('hidden'); michael@0: michael@0: if (outlineButton.getAttribute('disabled')) { michael@0: return; michael@0: } michael@0: break; michael@0: michael@0: case 'attachments': michael@0: thumbsButton.classList.remove('toggled'); michael@0: outlineButton.classList.remove('toggled'); michael@0: attachmentsButton.classList.add('toggled'); michael@0: thumbsView.classList.add('hidden'); michael@0: outlineView.classList.add('hidden'); michael@0: attachmentsView.classList.remove('hidden'); michael@0: michael@0: if (attachmentsButton.getAttribute('disabled')) { michael@0: return; michael@0: } michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: getVisiblePages: function pdfViewGetVisiblePages() { michael@0: if (!PresentationMode.active) { michael@0: return this.getVisibleElements(this.container, this.pages, true); michael@0: } else { michael@0: // The algorithm in getVisibleElements doesn't work in all browsers and michael@0: // configurations when presentation mode is active. michael@0: var visible = []; michael@0: var currentPage = this.pages[this.page - 1]; michael@0: visible.push({ id: currentPage.id, view: currentPage }); michael@0: return { first: currentPage, last: currentPage, views: visible }; michael@0: } michael@0: }, michael@0: michael@0: getVisibleThumbs: function pdfViewGetVisibleThumbs() { michael@0: return this.getVisibleElements(this.thumbnailContainer, this.thumbnails); michael@0: }, michael@0: michael@0: // Generic helper to find out what elements are visible within a scroll pane. michael@0: getVisibleElements: function pdfViewGetVisibleElements( michael@0: scrollEl, views, sortByVisibility) { michael@0: var top = scrollEl.scrollTop, bottom = top + scrollEl.clientHeight; michael@0: var left = scrollEl.scrollLeft, right = left + scrollEl.clientWidth; michael@0: michael@0: var visible = [], view; michael@0: var currentHeight, viewHeight, hiddenHeight, percentHeight; michael@0: var currentWidth, viewWidth; michael@0: for (var i = 0, ii = views.length; i < ii; ++i) { michael@0: view = views[i]; michael@0: currentHeight = view.el.offsetTop + view.el.clientTop; michael@0: viewHeight = view.el.clientHeight; michael@0: if ((currentHeight + viewHeight) < top) { michael@0: continue; michael@0: } michael@0: if (currentHeight > bottom) { michael@0: break; michael@0: } michael@0: currentWidth = view.el.offsetLeft + view.el.clientLeft; michael@0: viewWidth = view.el.clientWidth; michael@0: if ((currentWidth + viewWidth) < left || currentWidth > right) { michael@0: continue; michael@0: } michael@0: hiddenHeight = Math.max(0, top - currentHeight) + michael@0: Math.max(0, currentHeight + viewHeight - bottom); michael@0: percentHeight = ((viewHeight - hiddenHeight) * 100 / viewHeight) | 0; michael@0: michael@0: visible.push({ id: view.id, x: currentWidth, y: currentHeight, michael@0: view: view, percent: percentHeight }); michael@0: } michael@0: michael@0: var first = visible[0]; michael@0: var last = visible[visible.length - 1]; michael@0: michael@0: if (sortByVisibility) { michael@0: visible.sort(function(a, b) { michael@0: var pc = a.percent - b.percent; michael@0: if (Math.abs(pc) > 0.001) { michael@0: return -pc; michael@0: } michael@0: return a.id - b.id; // ensure stability michael@0: }); michael@0: } michael@0: return {first: first, last: last, views: visible}; michael@0: }, michael@0: michael@0: // Helper function to parse query string (e.g. ?param1=value&parm2=...). michael@0: parseQueryString: function pdfViewParseQueryString(query) { michael@0: var parts = query.split('&'); michael@0: var params = {}; michael@0: for (var i = 0, ii = parts.length; i < ii; ++i) { michael@0: var param = parts[i].split('='); michael@0: var key = param[0]; michael@0: var value = param.length > 1 ? param[1] : null; michael@0: params[decodeURIComponent(key)] = decodeURIComponent(value); michael@0: } michael@0: return params; michael@0: }, michael@0: michael@0: beforePrint: function pdfViewSetupBeforePrint() { michael@0: if (!this.supportsPrinting) { michael@0: var printMessage = mozL10n.get('printing_not_supported', null, michael@0: 'Warning: Printing is not fully supported by this browser.'); michael@0: this.error(printMessage); michael@0: return; michael@0: } michael@0: michael@0: var alertNotReady = false; michael@0: var i, ii; michael@0: if (!this.pages.length) { michael@0: alertNotReady = true; michael@0: } else { michael@0: for (i = 0, ii = this.pages.length; i < ii; ++i) { michael@0: if (!this.pages[i].pdfPage) { michael@0: alertNotReady = true; michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: if (alertNotReady) { michael@0: var notReadyMessage = mozL10n.get('printing_not_ready', null, michael@0: 'Warning: The PDF is not fully loaded for printing.'); michael@0: window.alert(notReadyMessage); michael@0: return; michael@0: } michael@0: michael@0: var body = document.querySelector('body'); michael@0: body.setAttribute('data-mozPrintCallback', true); michael@0: for (i = 0, ii = this.pages.length; i < ii; ++i) { michael@0: this.pages[i].beforePrint(); michael@0: } michael@0: }, michael@0: michael@0: afterPrint: function pdfViewSetupAfterPrint() { michael@0: var div = document.getElementById('printContainer'); michael@0: while (div.hasChildNodes()) { michael@0: div.removeChild(div.lastChild); michael@0: } michael@0: }, michael@0: michael@0: rotatePages: function pdfViewRotatePages(delta) { michael@0: var currentPage = this.pages[this.page - 1]; michael@0: var i, l; michael@0: this.pageRotation = (this.pageRotation + 360 + delta) % 360; michael@0: michael@0: for (i = 0, l = this.pages.length; i < l; i++) { michael@0: var page = this.pages[i]; michael@0: page.update(page.scale, this.pageRotation); michael@0: } michael@0: michael@0: for (i = 0, l = this.thumbnails.length; i < l; i++) { michael@0: var thumb = this.thumbnails[i]; michael@0: thumb.update(this.pageRotation); michael@0: } michael@0: michael@0: this.setScale(this.currentScaleValue, true, true); michael@0: michael@0: this.renderHighestPriority(); michael@0: michael@0: if (currentPage) { michael@0: currentPage.scrollIntoView(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * This function flips the page in presentation mode if the user scrolls up michael@0: * or down with large enough motion and prevents page flipping too often. michael@0: * michael@0: * @this {PDFView} michael@0: * @param {number} mouseScrollDelta The delta value from the mouse event. michael@0: */ michael@0: mouseScroll: function pdfViewMouseScroll(mouseScrollDelta) { michael@0: var MOUSE_SCROLL_COOLDOWN_TIME = 50; michael@0: michael@0: var currentTime = (new Date()).getTime(); michael@0: var storedTime = this.mouseScrollTimeStamp; michael@0: michael@0: // In case one page has already been flipped there is a cooldown time michael@0: // which has to expire before next page can be scrolled on to. michael@0: if (currentTime > storedTime && michael@0: currentTime - storedTime < MOUSE_SCROLL_COOLDOWN_TIME) { michael@0: return; michael@0: } michael@0: michael@0: // In case the user decides to scroll to the opposite direction than before michael@0: // clear the accumulated delta. michael@0: if ((this.mouseScrollDelta > 0 && mouseScrollDelta < 0) || michael@0: (this.mouseScrollDelta < 0 && mouseScrollDelta > 0)) { michael@0: this.clearMouseScrollState(); michael@0: } michael@0: michael@0: this.mouseScrollDelta += mouseScrollDelta; michael@0: michael@0: var PAGE_FLIP_THRESHOLD = 120; michael@0: if (Math.abs(this.mouseScrollDelta) >= PAGE_FLIP_THRESHOLD) { michael@0: michael@0: var PageFlipDirection = { michael@0: UP: -1, michael@0: DOWN: 1 michael@0: }; michael@0: michael@0: // In presentation mode scroll one page at a time. michael@0: var pageFlipDirection = (this.mouseScrollDelta > 0) ? michael@0: PageFlipDirection.UP : michael@0: PageFlipDirection.DOWN; michael@0: this.clearMouseScrollState(); michael@0: var currentPage = this.page; michael@0: michael@0: // In case we are already on the first or the last page there is no need michael@0: // to do anything. michael@0: if ((currentPage == 1 && pageFlipDirection == PageFlipDirection.UP) || michael@0: (currentPage == this.pages.length && michael@0: pageFlipDirection == PageFlipDirection.DOWN)) { michael@0: return; michael@0: } michael@0: michael@0: this.page += pageFlipDirection; michael@0: this.mouseScrollTimeStamp = currentTime; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * This function clears the member attributes used with mouse scrolling in michael@0: * presentation mode. michael@0: * michael@0: * @this {PDFView} michael@0: */ michael@0: clearMouseScrollState: function pdfViewClearMouseScrollState() { michael@0: this.mouseScrollTimeStamp = 0; michael@0: this.mouseScrollDelta = 0; michael@0: } michael@0: }; michael@0: michael@0: michael@0: var PageView = function pageView(container, id, scale, michael@0: navigateTo, defaultViewport) { michael@0: this.id = id; michael@0: michael@0: this.rotation = 0; michael@0: this.scale = scale || 1.0; michael@0: this.viewport = defaultViewport; michael@0: this.pdfPageRotate = defaultViewport.rotation; michael@0: michael@0: this.renderingState = RenderingStates.INITIAL; michael@0: this.resume = null; michael@0: michael@0: this.textLayer = null; michael@0: michael@0: this.zoomLayer = null; michael@0: michael@0: this.annotationLayer = null; michael@0: michael@0: var anchor = document.createElement('a'); michael@0: anchor.name = '' + this.id; michael@0: michael@0: var div = this.el = document.createElement('div'); michael@0: div.id = 'pageContainer' + this.id; michael@0: div.className = 'page'; michael@0: div.style.width = Math.floor(this.viewport.width) + 'px'; michael@0: div.style.height = Math.floor(this.viewport.height) + 'px'; michael@0: michael@0: container.appendChild(anchor); michael@0: container.appendChild(div); michael@0: michael@0: this.setPdfPage = function pageViewSetPdfPage(pdfPage) { michael@0: this.pdfPage = pdfPage; michael@0: this.pdfPageRotate = pdfPage.rotate; michael@0: var totalRotation = (this.rotation + this.pdfPageRotate) % 360; michael@0: this.viewport = pdfPage.getViewport(this.scale * CSS_UNITS, totalRotation); michael@0: this.stats = pdfPage.stats; michael@0: this.reset(); michael@0: }; michael@0: michael@0: this.destroy = function pageViewDestroy() { michael@0: this.zoomLayer = null; michael@0: this.reset(); michael@0: if (this.pdfPage) { michael@0: this.pdfPage.destroy(); michael@0: } michael@0: }; michael@0: michael@0: this.reset = function pageViewReset(keepAnnotations) { michael@0: if (this.renderTask) { michael@0: this.renderTask.cancel(); michael@0: } michael@0: this.resume = null; michael@0: this.renderingState = RenderingStates.INITIAL; michael@0: michael@0: div.style.width = Math.floor(this.viewport.width) + 'px'; michael@0: div.style.height = Math.floor(this.viewport.height) + 'px'; michael@0: michael@0: var childNodes = div.childNodes; michael@0: for (var i = div.childNodes.length - 1; i >= 0; i--) { michael@0: var node = childNodes[i]; michael@0: if ((this.zoomLayer && this.zoomLayer === node) || michael@0: (keepAnnotations && this.annotationLayer === node)) { michael@0: continue; michael@0: } michael@0: div.removeChild(node); michael@0: } michael@0: div.removeAttribute('data-loaded'); michael@0: michael@0: if (keepAnnotations) { michael@0: if (this.annotationLayer) { michael@0: // Hide annotationLayer until all elements are resized michael@0: // so they are not displayed on the already-resized page michael@0: this.annotationLayer.setAttribute('hidden', 'true'); michael@0: } michael@0: } else { michael@0: this.annotationLayer = null; michael@0: } michael@0: michael@0: delete this.canvas; michael@0: michael@0: this.loadingIconDiv = document.createElement('div'); michael@0: this.loadingIconDiv.className = 'loadingIcon'; michael@0: div.appendChild(this.loadingIconDiv); michael@0: }; michael@0: michael@0: this.update = function pageViewUpdate(scale, rotation) { michael@0: this.scale = scale || this.scale; michael@0: michael@0: if (typeof rotation !== 'undefined') { michael@0: this.rotation = rotation; michael@0: } michael@0: michael@0: var totalRotation = (this.rotation + this.pdfPageRotate) % 360; michael@0: this.viewport = this.viewport.clone({ michael@0: scale: this.scale * CSS_UNITS, michael@0: rotation: totalRotation michael@0: }); michael@0: michael@0: if (USE_ONLY_CSS_ZOOM && this.canvas) { michael@0: this.cssTransform(this.canvas); michael@0: return; michael@0: } else if (this.canvas && !this.zoomLayer) { michael@0: this.zoomLayer = this.canvas.parentNode; michael@0: this.zoomLayer.style.position = 'absolute'; michael@0: } michael@0: if (this.zoomLayer) { michael@0: this.cssTransform(this.zoomLayer.firstChild); michael@0: } michael@0: this.reset(true); michael@0: }; michael@0: michael@0: this.cssTransform = function pageCssTransform(canvas) { michael@0: // Scale canvas, canvas wrapper, and page container. michael@0: var width = this.viewport.width; michael@0: var height = this.viewport.height; michael@0: canvas.style.width = canvas.parentNode.style.width = div.style.width = michael@0: Math.floor(width) + 'px'; michael@0: canvas.style.height = canvas.parentNode.style.height = div.style.height = michael@0: Math.floor(height) + 'px'; michael@0: // The canvas may have been originally rotated, so rotate relative to that. michael@0: var relativeRotation = this.viewport.rotation - canvas._viewport.rotation; michael@0: var absRotation = Math.abs(relativeRotation); michael@0: var scaleX = 1, scaleY = 1; michael@0: if (absRotation === 90 || absRotation === 270) { michael@0: // Scale x and y because of the rotation. michael@0: scaleX = height / width; michael@0: scaleY = width / height; michael@0: } michael@0: var cssTransform = 'rotate(' + relativeRotation + 'deg) ' + michael@0: 'scale(' + scaleX + ',' + scaleY + ')'; michael@0: CustomStyle.setProp('transform', canvas, cssTransform); michael@0: michael@0: if (this.textLayer) { michael@0: // Rotating the text layer is more complicated since the divs inside the michael@0: // the text layer are rotated. michael@0: // TODO: This could probably be simplified by drawing the text layer in michael@0: // one orientation then rotating overall. michael@0: var textRelativeRotation = this.viewport.rotation - michael@0: this.textLayer.viewport.rotation; michael@0: var textAbsRotation = Math.abs(textRelativeRotation); michael@0: var scale = (width / canvas.width); michael@0: if (textAbsRotation === 90 || textAbsRotation === 270) { michael@0: scale = width / canvas.height; michael@0: } michael@0: var textLayerDiv = this.textLayer.textLayerDiv; michael@0: var transX, transY; michael@0: switch (textAbsRotation) { michael@0: case 0: michael@0: transX = transY = 0; michael@0: break; michael@0: case 90: michael@0: transX = 0; michael@0: transY = '-' + textLayerDiv.style.height; michael@0: break; michael@0: case 180: michael@0: transX = '-' + textLayerDiv.style.width; michael@0: transY = '-' + textLayerDiv.style.height; michael@0: break; michael@0: case 270: michael@0: transX = '-' + textLayerDiv.style.width; michael@0: transY = 0; michael@0: break; michael@0: default: michael@0: console.error('Bad rotation value.'); michael@0: break; michael@0: } michael@0: CustomStyle.setProp('transform', textLayerDiv, michael@0: 'rotate(' + textAbsRotation + 'deg) ' + michael@0: 'scale(' + scale + ', ' + scale + ') ' + michael@0: 'translate(' + transX + ', ' + transY + ')'); michael@0: CustomStyle.setProp('transformOrigin', textLayerDiv, '0% 0%'); michael@0: } michael@0: michael@0: if (USE_ONLY_CSS_ZOOM && this.annotationLayer) { michael@0: setupAnnotations(div, this.pdfPage, this.viewport); michael@0: } michael@0: }; michael@0: michael@0: Object.defineProperty(this, 'width', { michael@0: get: function PageView_getWidth() { michael@0: return this.viewport.width; michael@0: }, michael@0: enumerable: true michael@0: }); michael@0: michael@0: Object.defineProperty(this, 'height', { michael@0: get: function PageView_getHeight() { michael@0: return this.viewport.height; michael@0: }, michael@0: enumerable: true michael@0: }); michael@0: michael@0: var self = this; michael@0: michael@0: function setupAnnotations(pageDiv, pdfPage, viewport) { michael@0: michael@0: function bindLink(link, dest) { michael@0: link.href = PDFView.getDestinationHash(dest); michael@0: link.onclick = function pageViewSetupLinksOnclick() { michael@0: if (dest) { michael@0: PDFView.navigateTo(dest); michael@0: } michael@0: return false; michael@0: }; michael@0: if (dest) { michael@0: link.className = 'internalLink'; michael@0: } michael@0: } michael@0: michael@0: function bindNamedAction(link, action) { michael@0: link.href = PDFView.getAnchorUrl(''); michael@0: link.onclick = function pageViewSetupNamedActionOnClick() { michael@0: // See PDF reference, table 8.45 - Named action michael@0: switch (action) { michael@0: case 'GoToPage': michael@0: document.getElementById('pageNumber').focus(); michael@0: break; michael@0: michael@0: case 'GoBack': michael@0: PDFHistory.back(); michael@0: break; michael@0: michael@0: case 'GoForward': michael@0: PDFHistory.forward(); michael@0: break; michael@0: michael@0: case 'Find': michael@0: if (!PDFView.supportsIntegratedFind) { michael@0: PDFFindBar.toggle(); michael@0: } michael@0: break; michael@0: michael@0: case 'NextPage': michael@0: PDFView.page++; michael@0: break; michael@0: michael@0: case 'PrevPage': michael@0: PDFView.page--; michael@0: break; michael@0: michael@0: case 'LastPage': michael@0: PDFView.page = PDFView.pages.length; michael@0: break; michael@0: michael@0: case 'FirstPage': michael@0: PDFView.page = 1; michael@0: break; michael@0: michael@0: default: michael@0: break; // No action according to spec michael@0: } michael@0: return false; michael@0: }; michael@0: link.className = 'internalLink'; michael@0: } michael@0: michael@0: pdfPage.getAnnotations().then(function(annotationsData) { michael@0: viewport = viewport.clone({ dontFlip: true }); michael@0: var transform = viewport.transform; michael@0: var transformStr = 'matrix(' + transform.join(',') + ')'; michael@0: var data, element, i, ii; michael@0: michael@0: if (self.annotationLayer) { michael@0: // If an annotationLayer already exists, refresh its children's michael@0: // transformation matrices michael@0: for (i = 0, ii = annotationsData.length; i < ii; i++) { michael@0: data = annotationsData[i]; michael@0: element = self.annotationLayer.querySelector( michael@0: '[data-annotation-id="' + data.id + '"]'); michael@0: if (element) { michael@0: CustomStyle.setProp('transform', element, transformStr); michael@0: } michael@0: } michael@0: // See this.reset() michael@0: self.annotationLayer.removeAttribute('hidden'); michael@0: } else { michael@0: for (i = 0, ii = annotationsData.length; i < ii; i++) { michael@0: data = annotationsData[i]; michael@0: var annotation = PDFJS.Annotation.fromData(data); michael@0: if (!annotation || !annotation.hasHtml()) { michael@0: continue; michael@0: } michael@0: michael@0: element = annotation.getHtmlElement(pdfPage.commonObjs); michael@0: element.setAttribute('data-annotation-id', data.id); michael@0: mozL10n.translate(element); michael@0: michael@0: data = annotation.getData(); michael@0: var rect = data.rect; michael@0: var view = pdfPage.view; michael@0: rect = PDFJS.Util.normalizeRect([ michael@0: rect[0], michael@0: view[3] - rect[1] + view[1], michael@0: rect[2], michael@0: view[3] - rect[3] + view[1] michael@0: ]); michael@0: element.style.left = rect[0] + 'px'; michael@0: element.style.top = rect[1] + 'px'; michael@0: element.style.position = 'absolute'; michael@0: michael@0: CustomStyle.setProp('transform', element, transformStr); michael@0: var transformOriginStr = -rect[0] + 'px ' + -rect[1] + 'px'; michael@0: CustomStyle.setProp('transformOrigin', element, transformOriginStr); michael@0: michael@0: if (data.subtype === 'Link' && !data.url) { michael@0: var link = element.getElementsByTagName('a')[0]; michael@0: if (link) { michael@0: if (data.action) { michael@0: bindNamedAction(link, data.action); michael@0: } else { michael@0: bindLink(link, ('dest' in data) ? data.dest : null); michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (!self.annotationLayer) { michael@0: var annotationLayerDiv = document.createElement('div'); michael@0: annotationLayerDiv.className = 'annotationLayer'; michael@0: pageDiv.appendChild(annotationLayerDiv); michael@0: self.annotationLayer = annotationLayerDiv; michael@0: } michael@0: michael@0: self.annotationLayer.appendChild(element); michael@0: } michael@0: } michael@0: }); michael@0: } michael@0: michael@0: this.getPagePoint = function pageViewGetPagePoint(x, y) { michael@0: return this.viewport.convertToPdfPoint(x, y); michael@0: }; michael@0: michael@0: this.scrollIntoView = function pageViewScrollIntoView(dest) { michael@0: if (PresentationMode.active) { michael@0: if (PDFView.page !== this.id) { michael@0: // Avoid breaking PDFView.getVisiblePages in presentation mode. michael@0: PDFView.page = this.id; michael@0: return; michael@0: } michael@0: dest = null; michael@0: PDFView.setScale(PDFView.currentScaleValue, true, true); michael@0: } michael@0: if (!dest) { michael@0: scrollIntoView(div); michael@0: return; michael@0: } michael@0: michael@0: var x = 0, y = 0; michael@0: var width = 0, height = 0, widthScale, heightScale; michael@0: var changeOrientation = (this.rotation % 180 === 0 ? false : true); michael@0: var pageWidth = (changeOrientation ? this.height : this.width) / michael@0: this.scale / CSS_UNITS; michael@0: var pageHeight = (changeOrientation ? this.width : this.height) / michael@0: this.scale / CSS_UNITS; michael@0: var scale = 0; michael@0: switch (dest[1].name) { michael@0: case 'XYZ': michael@0: x = dest[2]; michael@0: y = dest[3]; michael@0: scale = dest[4]; michael@0: // If x and/or y coordinates are not supplied, default to michael@0: // _top_ left of the page (not the obvious bottom left, michael@0: // since aligning the bottom of the intended page with the michael@0: // top of the window is rarely helpful). michael@0: x = x !== null ? x : 0; michael@0: y = y !== null ? y : pageHeight; michael@0: break; michael@0: case 'Fit': michael@0: case 'FitB': michael@0: scale = 'page-fit'; michael@0: break; michael@0: case 'FitH': michael@0: case 'FitBH': michael@0: y = dest[2]; michael@0: scale = 'page-width'; michael@0: break; michael@0: case 'FitV': michael@0: case 'FitBV': michael@0: x = dest[2]; michael@0: width = pageWidth; michael@0: height = pageHeight; michael@0: scale = 'page-height'; michael@0: break; michael@0: case 'FitR': michael@0: x = dest[2]; michael@0: y = dest[3]; michael@0: width = dest[4] - x; michael@0: height = dest[5] - y; michael@0: widthScale = (PDFView.container.clientWidth - SCROLLBAR_PADDING) / michael@0: width / CSS_UNITS; michael@0: heightScale = (PDFView.container.clientHeight - SCROLLBAR_PADDING) / michael@0: height / CSS_UNITS; michael@0: scale = Math.min(Math.abs(widthScale), Math.abs(heightScale)); michael@0: break; michael@0: default: michael@0: return; michael@0: } michael@0: michael@0: if (scale && scale !== PDFView.currentScale) { michael@0: PDFView.setScale(scale, true, true); michael@0: } else if (PDFView.currentScale === UNKNOWN_SCALE) { michael@0: PDFView.setScale(DEFAULT_SCALE, true, true); michael@0: } michael@0: michael@0: if (scale === 'page-fit' && !dest[4]) { michael@0: scrollIntoView(div); michael@0: return; michael@0: } michael@0: michael@0: var boundingRect = [ michael@0: this.viewport.convertToViewportPoint(x, y), michael@0: this.viewport.convertToViewportPoint(x + width, y + height) michael@0: ]; michael@0: var left = Math.min(boundingRect[0][0], boundingRect[1][0]); michael@0: var top = Math.min(boundingRect[0][1], boundingRect[1][1]); michael@0: michael@0: scrollIntoView(div, { left: left, top: top }); michael@0: }; michael@0: michael@0: this.getTextContent = function pageviewGetTextContent() { michael@0: return PDFView.getPage(this.id).then(function(pdfPage) { michael@0: return pdfPage.getTextContent(); michael@0: }); michael@0: }; michael@0: michael@0: this.draw = function pageviewDraw(callback) { michael@0: var pdfPage = this.pdfPage; michael@0: michael@0: if (this.pagePdfPromise) { michael@0: return; michael@0: } michael@0: if (!pdfPage) { michael@0: var promise = PDFView.getPage(this.id); michael@0: promise.then(function(pdfPage) { michael@0: delete this.pagePdfPromise; michael@0: this.setPdfPage(pdfPage); michael@0: this.draw(callback); michael@0: }.bind(this)); michael@0: this.pagePdfPromise = promise; michael@0: return; michael@0: } michael@0: michael@0: if (this.renderingState !== RenderingStates.INITIAL) { michael@0: console.error('Must be in new state before drawing'); michael@0: } michael@0: michael@0: this.renderingState = RenderingStates.RUNNING; michael@0: michael@0: var viewport = this.viewport; michael@0: // Wrap the canvas so if it has a css transform for highdpi the overflow michael@0: // will be hidden in FF. michael@0: var canvasWrapper = document.createElement('div'); michael@0: canvasWrapper.style.width = div.style.width; michael@0: canvasWrapper.style.height = div.style.height; michael@0: canvasWrapper.classList.add('canvasWrapper'); michael@0: michael@0: var canvas = document.createElement('canvas'); michael@0: canvas.id = 'page' + this.id; michael@0: canvasWrapper.appendChild(canvas); michael@0: if (this.annotationLayer) { michael@0: // annotationLayer needs to stay on top michael@0: div.insertBefore(canvasWrapper, this.annotationLayer); michael@0: } else { michael@0: div.appendChild(canvasWrapper); michael@0: } michael@0: this.canvas = canvas; michael@0: michael@0: var ctx = canvas.getContext('2d'); michael@0: var outputScale = getOutputScale(ctx); michael@0: michael@0: if (USE_ONLY_CSS_ZOOM) { michael@0: var actualSizeViewport = viewport.clone({ scale: CSS_UNITS }); michael@0: // Use a scale that will make the canvas be the original intended size michael@0: // of the page. michael@0: outputScale.sx *= actualSizeViewport.width / viewport.width; michael@0: outputScale.sy *= actualSizeViewport.height / viewport.height; michael@0: outputScale.scaled = true; michael@0: } michael@0: michael@0: canvas.width = (Math.floor(viewport.width) * outputScale.sx) | 0; michael@0: canvas.height = (Math.floor(viewport.height) * outputScale.sy) | 0; michael@0: canvas.style.width = Math.floor(viewport.width) + 'px'; michael@0: canvas.style.height = Math.floor(viewport.height) + 'px'; michael@0: // Add the viewport so it's known what it was originally drawn with. michael@0: canvas._viewport = viewport; michael@0: michael@0: var textLayerDiv = null; michael@0: if (!PDFJS.disableTextLayer) { michael@0: textLayerDiv = document.createElement('div'); michael@0: textLayerDiv.className = 'textLayer'; michael@0: textLayerDiv.style.width = canvas.style.width; michael@0: textLayerDiv.style.height = canvas.style.height; michael@0: if (this.annotationLayer) { michael@0: // annotationLayer needs to stay on top michael@0: div.insertBefore(textLayerDiv, this.annotationLayer); michael@0: } else { michael@0: div.appendChild(textLayerDiv); michael@0: } michael@0: } michael@0: var textLayer = this.textLayer = michael@0: textLayerDiv ? new TextLayerBuilder({ michael@0: textLayerDiv: textLayerDiv, michael@0: pageIndex: this.id - 1, michael@0: lastScrollSource: PDFView, michael@0: viewport: this.viewport, michael@0: isViewerInPresentationMode: PresentationMode.active michael@0: }) : null; michael@0: // TODO(mack): use data attributes to store these michael@0: ctx._scaleX = outputScale.sx; michael@0: ctx._scaleY = outputScale.sy; michael@0: if (outputScale.scaled) { michael@0: ctx.scale(outputScale.sx, outputScale.sy); michael@0: } michael@0: michael@0: // Rendering area michael@0: michael@0: var self = this; michael@0: function pageViewDrawCallback(error) { michael@0: // The renderTask may have been replaced by a new one, so only remove the michael@0: // reference to the renderTask if it matches the one that is triggering michael@0: // this callback. michael@0: if (renderTask === self.renderTask) { michael@0: self.renderTask = null; michael@0: } michael@0: michael@0: if (error === 'cancelled') { michael@0: return; michael@0: } michael@0: michael@0: self.renderingState = RenderingStates.FINISHED; michael@0: michael@0: if (self.loadingIconDiv) { michael@0: div.removeChild(self.loadingIconDiv); michael@0: delete self.loadingIconDiv; michael@0: } michael@0: michael@0: if (self.zoomLayer) { michael@0: div.removeChild(self.zoomLayer); michael@0: self.zoomLayer = null; michael@0: } michael@0: michael@0: if (self.textLayer && self.textLayer.textDivs && michael@0: self.textLayer.textDivs.length > 0 && michael@0: !PDFView.supportsDocumentColors) { michael@0: console.error(mozL10n.get('document_colors_disabled', null, michael@0: 'PDF documents are not allowed to use their own colors: ' + michael@0: '\'Allow pages to choose their own colors\' ' + michael@0: 'is deactivated in the browser.')); michael@0: PDFView.fallback(); michael@0: } michael@0: if (error) { michael@0: PDFView.error(mozL10n.get('rendering_error', null, michael@0: 'An error occurred while rendering the page.'), error); michael@0: } michael@0: michael@0: self.stats = pdfPage.stats; michael@0: self.updateStats(); michael@0: if (self.onAfterDraw) { michael@0: self.onAfterDraw(); michael@0: } michael@0: michael@0: cache.push(self); michael@0: michael@0: var event = document.createEvent('CustomEvent'); michael@0: event.initCustomEvent('pagerender', true, true, { michael@0: pageNumber: pdfPage.pageNumber michael@0: }); michael@0: div.dispatchEvent(event); michael@0: michael@0: FirefoxCom.request('reportTelemetry', JSON.stringify({ michael@0: type: 'pageInfo' michael@0: })); michael@0: // TODO add stream types report here michael@0: callback(); michael@0: } michael@0: michael@0: var renderContext = { michael@0: canvasContext: ctx, michael@0: viewport: this.viewport, michael@0: textLayer: textLayer, michael@0: // intent: 'default', // === 'display' michael@0: continueCallback: function pdfViewcContinueCallback(cont) { michael@0: if (PDFView.highestPriorityPage !== 'page' + self.id) { michael@0: self.renderingState = RenderingStates.PAUSED; michael@0: self.resume = function resumeCallback() { michael@0: self.renderingState = RenderingStates.RUNNING; michael@0: cont(); michael@0: }; michael@0: return; michael@0: } michael@0: cont(); michael@0: } michael@0: }; michael@0: var renderTask = this.renderTask = this.pdfPage.render(renderContext); michael@0: michael@0: this.renderTask.promise.then( michael@0: function pdfPageRenderCallback() { michael@0: pageViewDrawCallback(null); michael@0: if (textLayer) { michael@0: self.getTextContent().then( michael@0: function textContentResolved(textContent) { michael@0: textLayer.setTextContent(textContent); michael@0: } michael@0: ); michael@0: } michael@0: }, michael@0: function pdfPageRenderError(error) { michael@0: pageViewDrawCallback(error); michael@0: } michael@0: ); michael@0: michael@0: setupAnnotations(div, pdfPage, this.viewport); michael@0: div.setAttribute('data-loaded', true); michael@0: }; michael@0: michael@0: this.beforePrint = function pageViewBeforePrint() { michael@0: var pdfPage = this.pdfPage; michael@0: michael@0: var viewport = pdfPage.getViewport(1); michael@0: // Use the same hack we use for high dpi displays for printing to get better michael@0: // output until bug 811002 is fixed in FF. michael@0: var PRINT_OUTPUT_SCALE = 2; michael@0: var canvas = document.createElement('canvas'); michael@0: canvas.width = Math.floor(viewport.width) * PRINT_OUTPUT_SCALE; michael@0: canvas.height = Math.floor(viewport.height) * PRINT_OUTPUT_SCALE; michael@0: canvas.style.width = (PRINT_OUTPUT_SCALE * viewport.width) + 'pt'; michael@0: canvas.style.height = (PRINT_OUTPUT_SCALE * viewport.height) + 'pt'; michael@0: var cssScale = 'scale(' + (1 / PRINT_OUTPUT_SCALE) + ', ' + michael@0: (1 / PRINT_OUTPUT_SCALE) + ')'; michael@0: CustomStyle.setProp('transform' , canvas, cssScale); michael@0: CustomStyle.setProp('transformOrigin' , canvas, '0% 0%'); michael@0: michael@0: var printContainer = document.getElementById('printContainer'); michael@0: var canvasWrapper = document.createElement('div'); michael@0: canvasWrapper.style.width = viewport.width + 'pt'; michael@0: canvasWrapper.style.height = viewport.height + 'pt'; michael@0: canvasWrapper.appendChild(canvas); michael@0: printContainer.appendChild(canvasWrapper); michael@0: michael@0: canvas.mozPrintCallback = function(obj) { michael@0: var ctx = obj.context; michael@0: michael@0: ctx.save(); michael@0: ctx.fillStyle = 'rgb(255, 255, 255)'; michael@0: ctx.fillRect(0, 0, canvas.width, canvas.height); michael@0: ctx.restore(); michael@0: ctx.scale(PRINT_OUTPUT_SCALE, PRINT_OUTPUT_SCALE); michael@0: michael@0: var renderContext = { michael@0: canvasContext: ctx, michael@0: viewport: viewport, michael@0: intent: 'print' michael@0: }; michael@0: michael@0: pdfPage.render(renderContext).promise.then(function() { michael@0: // Tell the printEngine that rendering this canvas/page has finished. michael@0: obj.done(); michael@0: }, function(error) { michael@0: console.error(error); michael@0: // Tell the printEngine that rendering this canvas/page has failed. michael@0: // This will make the print proces stop. michael@0: if ('abort' in obj) { michael@0: obj.abort(); michael@0: } else { michael@0: obj.done(); michael@0: } michael@0: }); michael@0: }; michael@0: }; michael@0: michael@0: this.updateStats = function pageViewUpdateStats() { michael@0: if (!this.stats) { michael@0: return; michael@0: } michael@0: michael@0: if (PDFJS.pdfBug && Stats.enabled) { michael@0: var stats = this.stats; michael@0: Stats.add(this.id, stats); michael@0: } michael@0: }; michael@0: }; michael@0: michael@0: michael@0: var ThumbnailView = function thumbnailView(container, id, defaultViewport) { michael@0: var anchor = document.createElement('a'); michael@0: anchor.href = PDFView.getAnchorUrl('#page=' + id); michael@0: anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}'); michael@0: anchor.onclick = function stopNavigation() { michael@0: PDFView.page = id; michael@0: return false; michael@0: }; michael@0: michael@0: this.pdfPage = undefined; michael@0: this.viewport = defaultViewport; michael@0: this.pdfPageRotate = defaultViewport.rotation; michael@0: michael@0: this.rotation = 0; michael@0: this.pageWidth = this.viewport.width; michael@0: this.pageHeight = this.viewport.height; michael@0: this.pageRatio = this.pageWidth / this.pageHeight; michael@0: this.id = id; michael@0: michael@0: this.canvasWidth = 98; michael@0: this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight; michael@0: this.scale = (this.canvasWidth / this.pageWidth); michael@0: michael@0: var div = this.el = document.createElement('div'); michael@0: div.id = 'thumbnailContainer' + id; michael@0: div.className = 'thumbnail'; michael@0: michael@0: if (id === 1) { michael@0: // Highlight the thumbnail of the first page when no page number is michael@0: // specified (or exists in cache) when the document is loaded. michael@0: div.classList.add('selected'); michael@0: } michael@0: michael@0: var ring = document.createElement('div'); michael@0: ring.className = 'thumbnailSelectionRing'; michael@0: ring.style.width = this.canvasWidth + 'px'; michael@0: ring.style.height = this.canvasHeight + 'px'; michael@0: michael@0: div.appendChild(ring); michael@0: anchor.appendChild(div); michael@0: container.appendChild(anchor); michael@0: michael@0: this.hasImage = false; michael@0: this.renderingState = RenderingStates.INITIAL; michael@0: michael@0: this.setPdfPage = function thumbnailViewSetPdfPage(pdfPage) { michael@0: this.pdfPage = pdfPage; michael@0: this.pdfPageRotate = pdfPage.rotate; michael@0: var totalRotation = (this.rotation + this.pdfPageRotate) % 360; michael@0: this.viewport = pdfPage.getViewport(1, totalRotation); michael@0: this.update(); michael@0: }; michael@0: michael@0: this.update = function thumbnailViewUpdate(rotation) { michael@0: if (rotation !== undefined) { michael@0: this.rotation = rotation; michael@0: } michael@0: var totalRotation = (this.rotation + this.pdfPageRotate) % 360; michael@0: this.viewport = this.viewport.clone({ michael@0: scale: 1, michael@0: rotation: totalRotation michael@0: }); michael@0: this.pageWidth = this.viewport.width; michael@0: this.pageHeight = this.viewport.height; michael@0: this.pageRatio = this.pageWidth / this.pageHeight; michael@0: michael@0: this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight; michael@0: this.scale = (this.canvasWidth / this.pageWidth); michael@0: michael@0: div.removeAttribute('data-loaded'); michael@0: ring.textContent = ''; michael@0: ring.style.width = this.canvasWidth + 'px'; michael@0: ring.style.height = this.canvasHeight + 'px'; michael@0: michael@0: this.hasImage = false; michael@0: this.renderingState = RenderingStates.INITIAL; michael@0: this.resume = null; michael@0: }; michael@0: michael@0: this.getPageDrawContext = function thumbnailViewGetPageDrawContext() { michael@0: var canvas = document.createElement('canvas'); michael@0: canvas.id = 'thumbnail' + id; michael@0: michael@0: canvas.width = this.canvasWidth; michael@0: canvas.height = this.canvasHeight; michael@0: canvas.className = 'thumbnailImage'; michael@0: canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas', michael@0: {page: id}, 'Thumbnail of Page {{page}}')); michael@0: michael@0: div.setAttribute('data-loaded', true); michael@0: michael@0: ring.appendChild(canvas); michael@0: michael@0: var ctx = canvas.getContext('2d'); michael@0: ctx.save(); michael@0: ctx.fillStyle = 'rgb(255, 255, 255)'; michael@0: ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight); michael@0: ctx.restore(); michael@0: return ctx; michael@0: }; michael@0: michael@0: this.drawingRequired = function thumbnailViewDrawingRequired() { michael@0: return !this.hasImage; michael@0: }; michael@0: michael@0: this.draw = function thumbnailViewDraw(callback) { michael@0: if (!this.pdfPage) { michael@0: var promise = PDFView.getPage(this.id); michael@0: promise.then(function(pdfPage) { michael@0: this.setPdfPage(pdfPage); michael@0: this.draw(callback); michael@0: }.bind(this)); michael@0: return; michael@0: } michael@0: michael@0: if (this.renderingState !== RenderingStates.INITIAL) { michael@0: console.error('Must be in new state before drawing'); michael@0: } michael@0: michael@0: this.renderingState = RenderingStates.RUNNING; michael@0: if (this.hasImage) { michael@0: callback(); michael@0: return; michael@0: } michael@0: michael@0: var self = this; michael@0: var ctx = this.getPageDrawContext(); michael@0: var drawViewport = this.viewport.clone({ scale: this.scale }); michael@0: var renderContext = { michael@0: canvasContext: ctx, michael@0: viewport: drawViewport, michael@0: continueCallback: function(cont) { michael@0: if (PDFView.highestPriorityPage !== 'thumbnail' + self.id) { michael@0: self.renderingState = RenderingStates.PAUSED; michael@0: self.resume = function() { michael@0: self.renderingState = RenderingStates.RUNNING; michael@0: cont(); michael@0: }; michael@0: return; michael@0: } michael@0: cont(); michael@0: } michael@0: }; michael@0: this.pdfPage.render(renderContext).promise.then( michael@0: function pdfPageRenderCallback() { michael@0: self.renderingState = RenderingStates.FINISHED; michael@0: callback(); michael@0: }, michael@0: function pdfPageRenderError(error) { michael@0: self.renderingState = RenderingStates.FINISHED; michael@0: callback(); michael@0: } michael@0: ); michael@0: this.hasImage = true; michael@0: }; michael@0: michael@0: this.setImage = function thumbnailViewSetImage(img) { michael@0: if (!this.pdfPage) { michael@0: var promise = PDFView.getPage(this.id); michael@0: promise.then(function(pdfPage) { michael@0: this.setPdfPage(pdfPage); michael@0: this.setImage(img); michael@0: }.bind(this)); michael@0: return; michael@0: } michael@0: if (this.hasImage || !img) { michael@0: return; michael@0: } michael@0: this.renderingState = RenderingStates.FINISHED; michael@0: var ctx = this.getPageDrawContext(); michael@0: ctx.drawImage(img, 0, 0, img.width, img.height, michael@0: 0, 0, ctx.canvas.width, ctx.canvas.height); michael@0: michael@0: this.hasImage = true; michael@0: }; michael@0: }; michael@0: michael@0: michael@0: var FIND_SCROLL_OFFSET_TOP = -50; michael@0: var FIND_SCROLL_OFFSET_LEFT = -400; michael@0: michael@0: /** michael@0: * TextLayerBuilder provides text-selection michael@0: * functionality for the PDF. It does this michael@0: * by creating overlay divs over the PDF michael@0: * text. This divs contain text that matches michael@0: * the PDF text they are overlaying. This michael@0: * object also provides for a way to highlight michael@0: * text that is being searched for. michael@0: */ michael@0: var TextLayerBuilder = function textLayerBuilder(options) { michael@0: var textLayerFrag = document.createDocumentFragment(); michael@0: michael@0: this.textLayerDiv = options.textLayerDiv; michael@0: this.layoutDone = false; michael@0: this.divContentDone = false; michael@0: this.pageIdx = options.pageIndex; michael@0: this.matches = []; michael@0: this.lastScrollSource = options.lastScrollSource; michael@0: this.viewport = options.viewport; michael@0: this.isViewerInPresentationMode = options.isViewerInPresentationMode; michael@0: this.textDivs = []; michael@0: michael@0: if (typeof PDFFindController === 'undefined') { michael@0: window.PDFFindController = null; michael@0: } michael@0: michael@0: if (typeof this.lastScrollSource === 'undefined') { michael@0: this.lastScrollSource = null; michael@0: } michael@0: michael@0: this.renderLayer = function textLayerBuilderRenderLayer() { michael@0: var textDivs = this.textDivs; michael@0: var canvas = document.createElement('canvas'); michael@0: var ctx = canvas.getContext('2d'); michael@0: michael@0: // No point in rendering so many divs as it'd make the browser unusable michael@0: // even after the divs are rendered michael@0: var MAX_TEXT_DIVS_TO_RENDER = 100000; michael@0: if (textDivs.length > MAX_TEXT_DIVS_TO_RENDER) { michael@0: return; michael@0: } michael@0: michael@0: for (var i = 0, ii = textDivs.length; i < ii; i++) { michael@0: var textDiv = textDivs[i]; michael@0: if ('isWhitespace' in textDiv.dataset) { michael@0: continue; michael@0: } michael@0: michael@0: ctx.font = textDiv.style.fontSize + ' ' + textDiv.style.fontFamily; michael@0: var width = ctx.measureText(textDiv.textContent).width; michael@0: michael@0: if (width > 0) { michael@0: textLayerFrag.appendChild(textDiv); michael@0: var textScale = textDiv.dataset.canvasWidth / width; michael@0: var rotation = textDiv.dataset.angle; michael@0: var transform = 'scale(' + textScale + ', 1)'; michael@0: transform = 'rotate(' + rotation + 'deg) ' + transform; michael@0: CustomStyle.setProp('transform' , textDiv, transform); michael@0: CustomStyle.setProp('transformOrigin' , textDiv, '0% 0%'); michael@0: } michael@0: } michael@0: michael@0: this.textLayerDiv.appendChild(textLayerFrag); michael@0: this.renderingDone = true; michael@0: this.updateMatches(); michael@0: }; michael@0: michael@0: this.setupRenderLayoutTimer = function textLayerSetupRenderLayoutTimer() { michael@0: // Schedule renderLayout() if user has been scrolling, otherwise michael@0: // run it right away michael@0: var RENDER_DELAY = 200; // in ms michael@0: var self = this; michael@0: var lastScroll = (this.lastScrollSource === null ? michael@0: 0 : this.lastScrollSource.lastScroll); michael@0: michael@0: if (Date.now() - lastScroll > RENDER_DELAY) { michael@0: // Render right away michael@0: this.renderLayer(); michael@0: } else { michael@0: // Schedule michael@0: if (this.renderTimer) { michael@0: clearTimeout(this.renderTimer); michael@0: } michael@0: this.renderTimer = setTimeout(function() { michael@0: self.setupRenderLayoutTimer(); michael@0: }, RENDER_DELAY); michael@0: } michael@0: }; michael@0: michael@0: this.appendText = function textLayerBuilderAppendText(geom, styles) { michael@0: var style = styles[geom.fontName]; michael@0: var textDiv = document.createElement('div'); michael@0: this.textDivs.push(textDiv); michael@0: if (!/\S/.test(geom.str)) { michael@0: textDiv.dataset.isWhitespace = true; michael@0: return; michael@0: } michael@0: var tx = PDFJS.Util.transform(this.viewport.transform, geom.transform); michael@0: var angle = Math.atan2(tx[1], tx[0]); michael@0: if (style.vertical) { michael@0: angle += Math.PI / 2; michael@0: } michael@0: var fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3])); michael@0: var fontAscent = (style.ascent ? style.ascent * fontHeight : michael@0: (style.descent ? (1 + style.descent) * fontHeight : fontHeight)); michael@0: michael@0: textDiv.style.position = 'absolute'; michael@0: textDiv.style.left = (tx[4] + (fontAscent * Math.sin(angle))) + 'px'; michael@0: textDiv.style.top = (tx[5] - (fontAscent * Math.cos(angle))) + 'px'; michael@0: textDiv.style.fontSize = fontHeight + 'px'; michael@0: textDiv.style.fontFamily = style.fontFamily; michael@0: michael@0: textDiv.textContent = geom.str; michael@0: textDiv.dataset.fontName = geom.fontName; michael@0: textDiv.dataset.angle = angle * (180 / Math.PI); michael@0: if (style.vertical) { michael@0: textDiv.dataset.canvasWidth = geom.height * this.viewport.scale; michael@0: } else { michael@0: textDiv.dataset.canvasWidth = geom.width * this.viewport.scale; michael@0: } michael@0: michael@0: }; michael@0: michael@0: this.setTextContent = function textLayerBuilderSetTextContent(textContent) { michael@0: this.textContent = textContent; michael@0: michael@0: var textItems = textContent.items; michael@0: for (var i = 0; i < textItems.length; i++) { michael@0: this.appendText(textItems[i], textContent.styles); michael@0: } michael@0: this.divContentDone = true; michael@0: michael@0: this.setupRenderLayoutTimer(); michael@0: }; michael@0: michael@0: this.convertMatches = function textLayerBuilderConvertMatches(matches) { michael@0: var i = 0; michael@0: var iIndex = 0; michael@0: var bidiTexts = this.textContent.items; michael@0: var end = bidiTexts.length - 1; michael@0: var queryLen = (PDFFindController === null ? michael@0: 0 : PDFFindController.state.query.length); michael@0: michael@0: var ret = []; michael@0: michael@0: // Loop over all the matches. michael@0: for (var m = 0; m < matches.length; m++) { michael@0: var matchIdx = matches[m]; michael@0: // # Calculate the begin position. michael@0: michael@0: // Loop over the divIdxs. michael@0: while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) { michael@0: iIndex += bidiTexts[i].str.length; michael@0: i++; michael@0: } michael@0: michael@0: // TODO: Do proper handling here if something goes wrong. michael@0: if (i == bidiTexts.length) { michael@0: console.error('Could not find matching mapping'); michael@0: } michael@0: michael@0: var match = { michael@0: begin: { michael@0: divIdx: i, michael@0: offset: matchIdx - iIndex michael@0: } michael@0: }; michael@0: michael@0: // # Calculate the end position. michael@0: matchIdx += queryLen; michael@0: michael@0: // Somewhat same array as above, but use a > instead of >= to get the end michael@0: // position right. michael@0: while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) { michael@0: iIndex += bidiTexts[i].str.length; michael@0: i++; michael@0: } michael@0: michael@0: match.end = { michael@0: divIdx: i, michael@0: offset: matchIdx - iIndex michael@0: }; michael@0: ret.push(match); michael@0: } michael@0: michael@0: return ret; michael@0: }; michael@0: michael@0: this.renderMatches = function textLayerBuilder_renderMatches(matches) { michael@0: // Early exit if there is nothing to render. michael@0: if (matches.length === 0) { michael@0: return; michael@0: } michael@0: michael@0: var bidiTexts = this.textContent.items; michael@0: var textDivs = this.textDivs; michael@0: var prevEnd = null; michael@0: var isSelectedPage = (PDFFindController === null ? michael@0: false : (this.pageIdx === PDFFindController.selected.pageIdx)); michael@0: michael@0: var selectedMatchIdx = (PDFFindController === null ? michael@0: -1 : PDFFindController.selected.matchIdx); michael@0: michael@0: var highlightAll = (PDFFindController === null ? michael@0: false : PDFFindController.state.highlightAll); michael@0: michael@0: var infty = { michael@0: divIdx: -1, michael@0: offset: undefined michael@0: }; michael@0: michael@0: function beginText(begin, className) { michael@0: var divIdx = begin.divIdx; michael@0: var div = textDivs[divIdx]; michael@0: div.textContent = ''; michael@0: appendTextToDiv(divIdx, 0, begin.offset, className); michael@0: } michael@0: michael@0: function appendText(from, to, className) { michael@0: appendTextToDiv(from.divIdx, from.offset, to.offset, className); michael@0: } michael@0: michael@0: function appendTextToDiv(divIdx, fromOffset, toOffset, className) { michael@0: var div = textDivs[divIdx]; michael@0: michael@0: var content = bidiTexts[divIdx].str.substring(fromOffset, toOffset); michael@0: var node = document.createTextNode(content); michael@0: if (className) { michael@0: var span = document.createElement('span'); michael@0: span.className = className; michael@0: span.appendChild(node); michael@0: div.appendChild(span); michael@0: return; michael@0: } michael@0: div.appendChild(node); michael@0: } michael@0: michael@0: function highlightDiv(divIdx, className) { michael@0: textDivs[divIdx].className = className; michael@0: } michael@0: michael@0: var i0 = selectedMatchIdx, i1 = i0 + 1, i; michael@0: michael@0: if (highlightAll) { michael@0: i0 = 0; michael@0: i1 = matches.length; michael@0: } else if (!isSelectedPage) { michael@0: // Not highlighting all and this isn't the selected page, so do nothing. michael@0: return; michael@0: } michael@0: michael@0: for (i = i0; i < i1; i++) { michael@0: var match = matches[i]; michael@0: var begin = match.begin; michael@0: var end = match.end; michael@0: michael@0: var isSelected = isSelectedPage && i === selectedMatchIdx; michael@0: var highlightSuffix = (isSelected ? ' selected' : ''); michael@0: if (isSelected && !this.isViewerInPresentationMode) { michael@0: scrollIntoView(textDivs[begin.divIdx], { top: FIND_SCROLL_OFFSET_TOP, michael@0: left: FIND_SCROLL_OFFSET_LEFT }); michael@0: } michael@0: michael@0: // Match inside new div. michael@0: if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { michael@0: // If there was a previous div, then add the text at the end michael@0: if (prevEnd !== null) { michael@0: appendText(prevEnd, infty); michael@0: } michael@0: // clears the divs and set the content until the begin point. michael@0: beginText(begin); michael@0: } else { michael@0: appendText(prevEnd, begin); michael@0: } michael@0: michael@0: if (begin.divIdx === end.divIdx) { michael@0: appendText(begin, end, 'highlight' + highlightSuffix); michael@0: } else { michael@0: appendText(begin, infty, 'highlight begin' + highlightSuffix); michael@0: for (var n = begin.divIdx + 1; n < end.divIdx; n++) { michael@0: highlightDiv(n, 'highlight middle' + highlightSuffix); michael@0: } michael@0: beginText(end, 'highlight end' + highlightSuffix); michael@0: } michael@0: prevEnd = end; michael@0: } michael@0: michael@0: if (prevEnd) { michael@0: appendText(prevEnd, infty); michael@0: } michael@0: }; michael@0: michael@0: this.updateMatches = function textLayerUpdateMatches() { michael@0: // Only show matches, once all rendering is done. michael@0: if (!this.renderingDone) { michael@0: return; michael@0: } michael@0: michael@0: // Clear out all matches. michael@0: var matches = this.matches; michael@0: var textDivs = this.textDivs; michael@0: var bidiTexts = this.textContent.items; michael@0: var clearedUntilDivIdx = -1; michael@0: michael@0: // Clear out all current matches. michael@0: for (var i = 0; i < matches.length; i++) { michael@0: var match = matches[i]; michael@0: var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx); michael@0: for (var n = begin; n <= match.end.divIdx; n++) { michael@0: var div = textDivs[n]; michael@0: div.textContent = bidiTexts[n].str; michael@0: div.className = ''; michael@0: } michael@0: clearedUntilDivIdx = match.end.divIdx + 1; michael@0: } michael@0: michael@0: if (PDFFindController === null || !PDFFindController.active) { michael@0: return; michael@0: } michael@0: michael@0: // Convert the matches on the page controller into the match format used michael@0: // for the textLayer. michael@0: this.matches = matches = (this.convertMatches(PDFFindController === null ? michael@0: [] : (PDFFindController.pageMatches[this.pageIdx] || []))); michael@0: michael@0: this.renderMatches(this.matches); michael@0: }; michael@0: }; michael@0: michael@0: michael@0: michael@0: var DocumentOutlineView = function documentOutlineView(outline) { michael@0: var outlineView = document.getElementById('outlineView'); michael@0: while (outlineView.firstChild) { michael@0: outlineView.removeChild(outlineView.firstChild); michael@0: } michael@0: michael@0: if (!outline) { michael@0: if (!outlineView.classList.contains('hidden')) { michael@0: PDFView.switchSidebarView('thumbs'); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: function bindItemLink(domObj, item) { michael@0: domObj.href = PDFView.getDestinationHash(item.dest); michael@0: domObj.onclick = function documentOutlineViewOnclick(e) { michael@0: PDFView.navigateTo(item.dest); michael@0: return false; michael@0: }; michael@0: } michael@0: michael@0: michael@0: var queue = [{parent: outlineView, items: outline}]; michael@0: while (queue.length > 0) { michael@0: var levelData = queue.shift(); michael@0: var i, n = levelData.items.length; michael@0: for (i = 0; i < n; i++) { michael@0: var item = levelData.items[i]; michael@0: var div = document.createElement('div'); michael@0: div.className = 'outlineItem'; michael@0: var a = document.createElement('a'); michael@0: bindItemLink(a, item); michael@0: a.textContent = item.title; michael@0: div.appendChild(a); michael@0: michael@0: if (item.items.length > 0) { michael@0: var itemsDiv = document.createElement('div'); michael@0: itemsDiv.className = 'outlineItems'; michael@0: div.appendChild(itemsDiv); michael@0: queue.push({parent: itemsDiv, items: item.items}); michael@0: } michael@0: michael@0: levelData.parent.appendChild(div); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: var DocumentAttachmentsView = function documentAttachmentsView(attachments) { michael@0: var attachmentsView = document.getElementById('attachmentsView'); michael@0: while (attachmentsView.firstChild) { michael@0: attachmentsView.removeChild(attachmentsView.firstChild); michael@0: } michael@0: michael@0: if (!attachments) { michael@0: if (!attachmentsView.classList.contains('hidden')) { michael@0: PDFView.switchSidebarView('thumbs'); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: function bindItemLink(domObj, item) { michael@0: domObj.href = '#'; michael@0: domObj.onclick = function documentAttachmentsViewOnclick(e) { michael@0: var downloadManager = new DownloadManager(); michael@0: downloadManager.downloadData(item.content, getFileName(item.filename), michael@0: ''); michael@0: return false; michael@0: }; michael@0: } michael@0: michael@0: var names = Object.keys(attachments).sort(function(a,b) { michael@0: return a.toLowerCase().localeCompare(b.toLowerCase()); michael@0: }); michael@0: for (var i = 0, ii = names.length; i < ii; i++) { michael@0: var item = attachments[names[i]]; michael@0: var div = document.createElement('div'); michael@0: div.className = 'attachmentsItem'; michael@0: var a = document.createElement('a'); michael@0: bindItemLink(a, item); michael@0: a.textContent = getFileName(item.filename); michael@0: div.appendChild(a); michael@0: attachmentsView.appendChild(div); michael@0: } michael@0: }; michael@0: michael@0: michael@0: function webViewerLoad(evt) { michael@0: PDFView.initialize().then(webViewerInitialized); michael@0: } michael@0: michael@0: function webViewerInitialized() { michael@0: var file = window.location.href.split('#')[0]; michael@0: michael@0: document.getElementById('openFile').setAttribute('hidden', 'true'); michael@0: document.getElementById('secondaryOpenFile').setAttribute('hidden', 'true'); michael@0: michael@0: // Special debugging flags in the hash section of the URL. michael@0: var hash = document.location.hash.substring(1); michael@0: var hashParams = PDFView.parseQueryString(hash); michael@0: michael@0: if ('disableWorker' in hashParams) { michael@0: PDFJS.disableWorker = (hashParams['disableWorker'] === 'true'); michael@0: } michael@0: michael@0: if ('disableRange' in hashParams) { michael@0: PDFJS.disableRange = (hashParams['disableRange'] === 'true'); michael@0: } michael@0: michael@0: if ('disableAutoFetch' in hashParams) { michael@0: PDFJS.disableAutoFetch = (hashParams['disableAutoFetch'] === 'true'); michael@0: } michael@0: michael@0: if ('disableFontFace' in hashParams) { michael@0: PDFJS.disableFontFace = (hashParams['disableFontFace'] === 'true'); michael@0: } michael@0: michael@0: if ('disableHistory' in hashParams) { michael@0: PDFJS.disableHistory = (hashParams['disableHistory'] === 'true'); michael@0: } michael@0: michael@0: if ('webgl' in hashParams) { michael@0: PDFJS.disableWebGL = (hashParams['webgl'] !== 'true'); michael@0: } michael@0: michael@0: if ('useOnlyCssZoom' in hashParams) { michael@0: USE_ONLY_CSS_ZOOM = (hashParams['useOnlyCssZoom'] === 'true'); michael@0: } michael@0: michael@0: if ('verbosity' in hashParams) { michael@0: PDFJS.verbosity = hashParams['verbosity'] | 0; michael@0: } michael@0: michael@0: if ('ignoreCurrentPositionOnZoom' in hashParams) { michael@0: IGNORE_CURRENT_POSITION_ON_ZOOM = michael@0: (hashParams['ignoreCurrentPositionOnZoom'] === 'true'); michael@0: } michael@0: michael@0: michael@0: michael@0: if (!PDFView.supportsDocumentFonts) { michael@0: PDFJS.disableFontFace = true; michael@0: console.warn(mozL10n.get('web_fonts_disabled', null, michael@0: 'Web fonts are disabled: unable to use embedded PDF fonts.')); michael@0: } michael@0: michael@0: if ('textLayer' in hashParams) { michael@0: switch (hashParams['textLayer']) { michael@0: case 'off': michael@0: PDFJS.disableTextLayer = true; michael@0: break; michael@0: case 'visible': michael@0: case 'shadow': michael@0: case 'hover': michael@0: var viewer = document.getElementById('viewer'); michael@0: viewer.classList.add('textLayer-' + hashParams['textLayer']); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if ('pdfBug' in hashParams && FirefoxCom.requestSync('pdfBugEnabled')) { michael@0: PDFJS.pdfBug = true; michael@0: var pdfBug = hashParams['pdfBug']; michael@0: var enabled = pdfBug.split(','); michael@0: PDFBug.enable(enabled); michael@0: PDFBug.init(); michael@0: } michael@0: michael@0: if (!PDFView.supportsPrinting) { michael@0: document.getElementById('print').classList.add('hidden'); michael@0: document.getElementById('secondaryPrint').classList.add('hidden'); michael@0: } michael@0: michael@0: if (!PDFView.supportsFullscreen) { michael@0: document.getElementById('presentationMode').classList.add('hidden'); michael@0: document.getElementById('secondaryPresentationMode'). michael@0: classList.add('hidden'); michael@0: } michael@0: michael@0: if (PDFView.supportsIntegratedFind) { michael@0: document.getElementById('viewFind').classList.add('hidden'); michael@0: } michael@0: michael@0: // Listen for unsuporrted features to trigger the fallback UI. michael@0: PDFJS.UnsupportedManager.listen(PDFView.fallback.bind(PDFView)); michael@0: michael@0: // Suppress context menus for some controls michael@0: document.getElementById('scaleSelect').oncontextmenu = noContextMenuHandler; michael@0: michael@0: var mainContainer = document.getElementById('mainContainer'); michael@0: var outerContainer = document.getElementById('outerContainer'); michael@0: mainContainer.addEventListener('transitionend', function(e) { michael@0: if (e.target == mainContainer) { michael@0: var event = document.createEvent('UIEvents'); michael@0: event.initUIEvent('resize', false, false, window, 0); michael@0: window.dispatchEvent(event); michael@0: outerContainer.classList.remove('sidebarMoving'); michael@0: } michael@0: }, true); michael@0: michael@0: document.getElementById('sidebarToggle').addEventListener('click', michael@0: function() { michael@0: this.classList.toggle('toggled'); michael@0: outerContainer.classList.add('sidebarMoving'); michael@0: outerContainer.classList.toggle('sidebarOpen'); michael@0: PDFView.sidebarOpen = outerContainer.classList.contains('sidebarOpen'); michael@0: PDFView.renderHighestPriority(); michael@0: }); michael@0: michael@0: document.getElementById('viewThumbnail').addEventListener('click', michael@0: function() { michael@0: PDFView.switchSidebarView('thumbs'); michael@0: }); michael@0: michael@0: document.getElementById('viewOutline').addEventListener('click', michael@0: function() { michael@0: PDFView.switchSidebarView('outline'); michael@0: }); michael@0: michael@0: document.getElementById('viewAttachments').addEventListener('click', michael@0: function() { michael@0: PDFView.switchSidebarView('attachments'); michael@0: }); michael@0: michael@0: document.getElementById('previous').addEventListener('click', michael@0: function() { michael@0: PDFView.page--; michael@0: }); michael@0: michael@0: document.getElementById('next').addEventListener('click', michael@0: function() { michael@0: PDFView.page++; michael@0: }); michael@0: michael@0: document.getElementById('zoomIn').addEventListener('click', michael@0: function() { michael@0: PDFView.zoomIn(); michael@0: }); michael@0: michael@0: document.getElementById('zoomOut').addEventListener('click', michael@0: function() { michael@0: PDFView.zoomOut(); michael@0: }); michael@0: michael@0: document.getElementById('pageNumber').addEventListener('click', michael@0: function() { michael@0: this.select(); michael@0: }); michael@0: michael@0: document.getElementById('pageNumber').addEventListener('change', michael@0: function() { michael@0: // Handle the user inputting a floating point number. michael@0: PDFView.page = (this.value | 0); michael@0: michael@0: if (this.value !== (this.value | 0).toString()) { michael@0: this.value = PDFView.page; michael@0: } michael@0: }); michael@0: michael@0: document.getElementById('scaleSelect').addEventListener('change', michael@0: function() { michael@0: PDFView.setScale(this.value); michael@0: }); michael@0: michael@0: document.getElementById('presentationMode').addEventListener('click', michael@0: SecondaryToolbar.presentationModeClick.bind(SecondaryToolbar)); michael@0: michael@0: document.getElementById('openFile').addEventListener('click', michael@0: SecondaryToolbar.openFileClick.bind(SecondaryToolbar)); michael@0: michael@0: document.getElementById('print').addEventListener('click', michael@0: SecondaryToolbar.printClick.bind(SecondaryToolbar)); michael@0: michael@0: document.getElementById('download').addEventListener('click', michael@0: SecondaryToolbar.downloadClick.bind(SecondaryToolbar)); michael@0: michael@0: PDFView.setTitleUsingUrl(file); michael@0: PDFView.initPassiveLoading(); michael@0: return; michael@0: michael@0: if (file) { michael@0: PDFView.open(file, 0); michael@0: } michael@0: } michael@0: michael@0: document.addEventListener('DOMContentLoaded', webViewerLoad, true); michael@0: michael@0: function updateViewarea() { michael@0: michael@0: if (!PDFView.initialized) { michael@0: return; michael@0: } michael@0: var visible = PDFView.getVisiblePages(); michael@0: var visiblePages = visible.views; michael@0: if (visiblePages.length === 0) { michael@0: return; michael@0: } michael@0: michael@0: PDFView.renderHighestPriority(); michael@0: michael@0: var currentId = PDFView.page; michael@0: var firstPage = visible.first; michael@0: michael@0: for (var i = 0, ii = visiblePages.length, stillFullyVisible = false; michael@0: i < ii; ++i) { michael@0: var page = visiblePages[i]; michael@0: michael@0: if (page.percent < 100) { michael@0: break; michael@0: } michael@0: if (page.id === PDFView.page) { michael@0: stillFullyVisible = true; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (!stillFullyVisible) { michael@0: currentId = visiblePages[0].id; michael@0: } michael@0: michael@0: if (!PresentationMode.active) { michael@0: updateViewarea.inProgress = true; // used in "set page" michael@0: PDFView.page = currentId; michael@0: updateViewarea.inProgress = false; michael@0: } michael@0: michael@0: var currentScale = PDFView.currentScale; michael@0: var currentScaleValue = PDFView.currentScaleValue; michael@0: var normalizedScaleValue = parseFloat(currentScaleValue) === currentScale ? michael@0: Math.round(currentScale * 10000) / 100 : currentScaleValue; michael@0: michael@0: var pageNumber = firstPage.id; michael@0: var pdfOpenParams = '#page=' + pageNumber; michael@0: pdfOpenParams += '&zoom=' + normalizedScaleValue; michael@0: var currentPage = PDFView.pages[pageNumber - 1]; michael@0: var container = PDFView.container; michael@0: var topLeft = currentPage.getPagePoint((container.scrollLeft - firstPage.x), michael@0: (container.scrollTop - firstPage.y)); michael@0: var intLeft = Math.round(topLeft[0]); michael@0: var intTop = Math.round(topLeft[1]); michael@0: pdfOpenParams += ',' + intLeft + ',' + intTop; michael@0: michael@0: if (PresentationMode.active || PresentationMode.switchInProgress) { michael@0: PDFView.currentPosition = null; michael@0: } else { michael@0: PDFView.currentPosition = { page: pageNumber, left: intLeft, top: intTop }; michael@0: } michael@0: michael@0: var store = PDFView.store; michael@0: store.initializedPromise.then(function() { michael@0: store.set('exists', true); michael@0: store.set('page', pageNumber); michael@0: store.set('zoom', normalizedScaleValue); michael@0: store.set('scrollLeft', intLeft); michael@0: store.set('scrollTop', intTop); michael@0: }); michael@0: var href = PDFView.getAnchorUrl(pdfOpenParams); michael@0: document.getElementById('viewBookmark').href = href; michael@0: document.getElementById('secondaryViewBookmark').href = href; michael@0: michael@0: // Update the current bookmark in the browsing history. michael@0: PDFHistory.updateCurrentBookmark(pdfOpenParams, pageNumber); michael@0: } michael@0: michael@0: window.addEventListener('resize', function webViewerResize(evt) { michael@0: if (PDFView.initialized && michael@0: (document.getElementById('pageWidthOption').selected || michael@0: document.getElementById('pageFitOption').selected || michael@0: document.getElementById('pageAutoOption').selected)) { michael@0: PDFView.setScale(document.getElementById('scaleSelect').value); michael@0: } michael@0: updateViewarea(); michael@0: michael@0: // Set the 'max-height' CSS property of the secondary toolbar. michael@0: SecondaryToolbar.setMaxHeight(PDFView.container); michael@0: }); michael@0: michael@0: window.addEventListener('hashchange', function webViewerHashchange(evt) { michael@0: if (PDFHistory.isHashChangeUnlocked) { michael@0: PDFView.setHash(document.location.hash.substring(1)); michael@0: } michael@0: }); michael@0: michael@0: michael@0: function selectScaleOption(value) { michael@0: var options = document.getElementById('scaleSelect').options; michael@0: var predefinedValueFound = false; michael@0: for (var i = 0; i < options.length; i++) { michael@0: var option = options[i]; michael@0: if (option.value != value) { michael@0: option.selected = false; michael@0: continue; michael@0: } michael@0: option.selected = true; michael@0: predefinedValueFound = true; michael@0: } michael@0: return predefinedValueFound; michael@0: } michael@0: michael@0: window.addEventListener('localized', function localized(evt) { michael@0: document.getElementsByTagName('html')[0].dir = mozL10n.getDirection(); michael@0: michael@0: PDFView.animationStartedPromise.then(function() { michael@0: // Adjust the width of the zoom box to fit the content. michael@0: // Note: This is only done if the zoom box is actually visible, michael@0: // since otherwise element.clientWidth will return 0. michael@0: var container = document.getElementById('scaleSelectContainer'); michael@0: if (container.clientWidth > 0) { michael@0: var select = document.getElementById('scaleSelect'); michael@0: select.setAttribute('style', 'min-width: inherit;'); michael@0: var width = select.clientWidth + SCALE_SELECT_CONTAINER_PADDING; michael@0: select.setAttribute('style', 'min-width: ' + michael@0: (width + SCALE_SELECT_PADDING) + 'px;'); michael@0: container.setAttribute('style', 'min-width: ' + width + 'px; ' + michael@0: 'max-width: ' + width + 'px;'); michael@0: } michael@0: michael@0: // Set the 'max-height' CSS property of the secondary toolbar. michael@0: SecondaryToolbar.setMaxHeight(PDFView.container); michael@0: }); michael@0: }, true); michael@0: michael@0: window.addEventListener('scalechange', function scalechange(evt) { michael@0: document.getElementById('zoomOut').disabled = (evt.scale === MIN_SCALE); michael@0: document.getElementById('zoomIn').disabled = (evt.scale === MAX_SCALE); michael@0: michael@0: var customScaleOption = document.getElementById('customScaleOption'); michael@0: customScaleOption.selected = false; michael@0: michael@0: if (!evt.resetAutoSettings && michael@0: (document.getElementById('pageWidthOption').selected || michael@0: document.getElementById('pageFitOption').selected || michael@0: document.getElementById('pageAutoOption').selected)) { michael@0: updateViewarea(); michael@0: return; michael@0: } michael@0: michael@0: var predefinedValueFound = selectScaleOption('' + evt.scale); michael@0: if (!predefinedValueFound) { michael@0: customScaleOption.textContent = Math.round(evt.scale * 10000) / 100 + '%'; michael@0: customScaleOption.selected = true; michael@0: } michael@0: updateViewarea(); michael@0: }, true); michael@0: michael@0: window.addEventListener('pagechange', function pagechange(evt) { michael@0: var page = evt.pageNumber; michael@0: if (PDFView.previousPageNumber !== page) { michael@0: document.getElementById('pageNumber').value = page; michael@0: var selected = document.querySelector('.thumbnail.selected'); michael@0: if (selected) { michael@0: selected.classList.remove('selected'); michael@0: } michael@0: var thumbnail = document.getElementById('thumbnailContainer' + page); michael@0: thumbnail.classList.add('selected'); michael@0: var visibleThumbs = PDFView.getVisibleThumbs(); michael@0: var numVisibleThumbs = visibleThumbs.views.length; michael@0: michael@0: // If the thumbnail isn't currently visible, scroll it into view. michael@0: if (numVisibleThumbs > 0) { michael@0: var first = visibleThumbs.first.id; michael@0: // Account for only one thumbnail being visible. michael@0: var last = (numVisibleThumbs > 1 ? visibleThumbs.last.id : first); michael@0: if (page <= first || page >= last) { michael@0: scrollIntoView(thumbnail, { top: THUMBNAIL_SCROLL_MARGIN }); michael@0: } michael@0: } michael@0: } michael@0: document.getElementById('previous').disabled = (page <= 1); michael@0: document.getElementById('next').disabled = (page >= PDFView.pages.length); michael@0: }, true); michael@0: michael@0: function handleMouseWheel(evt) { michael@0: var MOUSE_WHEEL_DELTA_FACTOR = 40; michael@0: var ticks = (evt.type === 'DOMMouseScroll') ? -evt.detail : michael@0: evt.wheelDelta / MOUSE_WHEEL_DELTA_FACTOR; michael@0: var direction = (ticks < 0) ? 'zoomOut' : 'zoomIn'; michael@0: michael@0: if (evt.ctrlKey) { // Only zoom the pages, not the entire viewer michael@0: evt.preventDefault(); michael@0: PDFView[direction](Math.abs(ticks)); michael@0: } else if (PresentationMode.active) { michael@0: PDFView.mouseScroll(ticks * MOUSE_WHEEL_DELTA_FACTOR); michael@0: } michael@0: } michael@0: michael@0: window.addEventListener('DOMMouseScroll', handleMouseWheel); michael@0: window.addEventListener('mousewheel', handleMouseWheel); michael@0: michael@0: window.addEventListener('click', function click(evt) { michael@0: if (!PresentationMode.active) { michael@0: if (SecondaryToolbar.opened && PDFView.container.contains(evt.target)) { michael@0: SecondaryToolbar.close(); michael@0: } michael@0: } else if (evt.button === 0) { michael@0: // Necessary since preventDefault() in 'mousedown' won't stop michael@0: // the event propagation in all circumstances in presentation mode. michael@0: evt.preventDefault(); michael@0: } michael@0: }, false); michael@0: michael@0: window.addEventListener('keydown', function keydown(evt) { michael@0: if (PasswordPrompt.visible) { michael@0: return; michael@0: } michael@0: michael@0: var handled = false; michael@0: var cmd = (evt.ctrlKey ? 1 : 0) | michael@0: (evt.altKey ? 2 : 0) | michael@0: (evt.shiftKey ? 4 : 0) | michael@0: (evt.metaKey ? 8 : 0); michael@0: michael@0: // First, handle the key bindings that are independent whether an input michael@0: // control is selected or not. michael@0: if (cmd === 1 || cmd === 8 || cmd === 5 || cmd === 12) { michael@0: // either CTRL or META key with optional SHIFT. michael@0: switch (evt.keyCode) { michael@0: case 70: // f michael@0: if (!PDFView.supportsIntegratedFind) { michael@0: PDFFindBar.open(); michael@0: handled = true; michael@0: } michael@0: break; michael@0: case 71: // g michael@0: if (!PDFView.supportsIntegratedFind) { michael@0: PDFFindBar.dispatchEvent('again', cmd === 5 || cmd === 12); michael@0: handled = true; michael@0: } michael@0: break; michael@0: case 61: // FF/Mac '=' michael@0: case 107: // FF '+' and '=' michael@0: case 187: // Chrome '+' michael@0: case 171: // FF with German keyboard michael@0: PDFView.zoomIn(); michael@0: handled = true; michael@0: break; michael@0: case 173: // FF/Mac '-' michael@0: case 109: // FF '-' michael@0: case 189: // Chrome '-' michael@0: PDFView.zoomOut(); michael@0: handled = true; michael@0: break; michael@0: case 48: // '0' michael@0: case 96: // '0' on Numpad of Swedish keyboard michael@0: // keeping it unhandled (to restore page zoom to 100%) michael@0: setTimeout(function () { michael@0: // ... and resetting the scale after browser adjusts its scale michael@0: PDFView.setScale(DEFAULT_SCALE, true); michael@0: }); michael@0: handled = false; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: michael@0: // CTRL+ALT or Option+Command michael@0: if (cmd === 3 || cmd === 10) { michael@0: switch (evt.keyCode) { michael@0: case 80: // p michael@0: SecondaryToolbar.presentationModeClick(); michael@0: handled = true; michael@0: break; michael@0: case 71: // g michael@0: // focuses input#pageNumber field michael@0: document.getElementById('pageNumber').select(); michael@0: handled = true; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (handled) { michael@0: evt.preventDefault(); michael@0: return; michael@0: } michael@0: michael@0: // Some shortcuts should not get handled if a control/input element michael@0: // is selected. michael@0: var curElement = document.activeElement || document.querySelector(':focus'); michael@0: var curElementTagName = curElement && curElement.tagName.toUpperCase(); michael@0: if (curElementTagName === 'INPUT' || michael@0: curElementTagName === 'TEXTAREA' || michael@0: curElementTagName === 'SELECT') { michael@0: // Make sure that the secondary toolbar is closed when Escape is pressed. michael@0: if (evt.keyCode !== 27) { // 'Esc' michael@0: return; michael@0: } michael@0: } michael@0: michael@0: if (cmd === 0) { // no control key pressed at all. michael@0: switch (evt.keyCode) { michael@0: case 38: // up arrow michael@0: case 33: // pg up michael@0: case 8: // backspace michael@0: if (!PresentationMode.active && michael@0: PDFView.currentScaleValue !== 'page-fit') { michael@0: break; michael@0: } michael@0: /* in presentation mode */ michael@0: /* falls through */ michael@0: case 37: // left arrow michael@0: // horizontal scrolling using arrow keys michael@0: if (PDFView.isHorizontalScrollbarEnabled) { michael@0: break; michael@0: } michael@0: /* falls through */ michael@0: case 75: // 'k' michael@0: case 80: // 'p' michael@0: PDFView.page--; michael@0: handled = true; michael@0: break; michael@0: case 27: // esc key michael@0: if (SecondaryToolbar.opened) { michael@0: SecondaryToolbar.close(); michael@0: handled = true; michael@0: } michael@0: if (!PDFView.supportsIntegratedFind && PDFFindBar.opened) { michael@0: PDFFindBar.close(); michael@0: handled = true; michael@0: } michael@0: break; michael@0: case 40: // down arrow michael@0: case 34: // pg down michael@0: case 32: // spacebar michael@0: if (!PresentationMode.active && michael@0: PDFView.currentScaleValue !== 'page-fit') { michael@0: break; michael@0: } michael@0: /* falls through */ michael@0: case 39: // right arrow michael@0: // horizontal scrolling using arrow keys michael@0: if (PDFView.isHorizontalScrollbarEnabled) { michael@0: break; michael@0: } michael@0: /* falls through */ michael@0: case 74: // 'j' michael@0: case 78: // 'n' michael@0: PDFView.page++; michael@0: handled = true; michael@0: break; michael@0: michael@0: case 36: // home michael@0: if (PresentationMode.active) { michael@0: PDFView.page = 1; michael@0: handled = true; michael@0: } michael@0: break; michael@0: case 35: // end michael@0: if (PresentationMode.active) { michael@0: PDFView.page = PDFView.pdfDocument.numPages; michael@0: handled = true; michael@0: } michael@0: break; michael@0: michael@0: case 72: // 'h' michael@0: if (!PresentationMode.active) { michael@0: HandTool.toggle(); michael@0: } michael@0: break; michael@0: case 82: // 'r' michael@0: PDFView.rotatePages(90); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (cmd === 4) { // shift-key michael@0: switch (evt.keyCode) { michael@0: case 32: // spacebar michael@0: if (!PresentationMode.active && michael@0: PDFView.currentScaleValue !== 'page-fit') { michael@0: break; michael@0: } michael@0: PDFView.page--; michael@0: handled = true; michael@0: break; michael@0: michael@0: case 82: // 'r' michael@0: PDFView.rotatePages(-90); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (!handled && !PresentationMode.active) { michael@0: // 33=Page Up 34=Page Down 35=End 36=Home michael@0: // 37=Left 38=Up 39=Right 40=Down michael@0: if (evt.keyCode >= 33 && evt.keyCode <= 40 && michael@0: !PDFView.container.contains(curElement)) { michael@0: // The page container is not focused, but a page navigation key has been michael@0: // pressed. Change the focus to the viewer container to make sure that michael@0: // navigation by keyboard works as expected. michael@0: PDFView.container.focus(); michael@0: } michael@0: // 32=Spacebar michael@0: if (evt.keyCode === 32 && curElementTagName !== 'BUTTON') { michael@0: // Workaround for issue in Firefox, that prevents scroll keys from michael@0: // working when elements with 'tabindex' are focused. (#3498) michael@0: PDFView.container.blur(); michael@0: } michael@0: } michael@0: michael@0: if (cmd === 2) { // alt-key michael@0: switch (evt.keyCode) { michael@0: case 37: // left arrow michael@0: if (PresentationMode.active) { michael@0: PDFHistory.back(); michael@0: handled = true; michael@0: } michael@0: break; michael@0: case 39: // right arrow michael@0: if (PresentationMode.active) { michael@0: PDFHistory.forward(); michael@0: handled = true; michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: michael@0: if (handled) { michael@0: evt.preventDefault(); michael@0: PDFView.clearMouseScrollState(); michael@0: } michael@0: }); michael@0: michael@0: window.addEventListener('beforeprint', function beforePrint(evt) { michael@0: PDFView.beforePrint(); michael@0: }); michael@0: michael@0: window.addEventListener('afterprint', function afterPrint(evt) { michael@0: PDFView.afterPrint(); michael@0: }); michael@0: michael@0: (function animationStartedClosure() { michael@0: // The offsetParent is not set until the pdf.js iframe or object is visible. michael@0: // Waiting for first animation. michael@0: var requestAnimationFrame = window.requestAnimationFrame || michael@0: window.mozRequestAnimationFrame || michael@0: window.webkitRequestAnimationFrame || michael@0: window.oRequestAnimationFrame || michael@0: window.msRequestAnimationFrame || michael@0: function startAtOnce(callback) { callback(); }; michael@0: PDFView.animationStartedPromise = new Promise(function (resolve) { michael@0: requestAnimationFrame(function onAnimationFrame() { michael@0: resolve(); michael@0: }); michael@0: }); michael@0: })(); michael@0: michael@0: