/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ /* Copyright 2012 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* globals PDFJS, PDFView, PresentationMode */ 'use strict'; var PDFHistory = { initialized: false, initialDestination: null, initialize: function pdfHistoryInitialize(fingerprint) { if (PDFJS.disableHistory || PDFView.isViewerEmbedded) { // The browsing history is only enabled when the viewer is standalone, // i.e. not when it is embedded in a web page. return; } this.initialized = true; this.reInitialized = false; this.allowHashChange = true; this.historyUnlocked = true; this.previousHash = window.location.hash.substring(1); this.currentBookmark = ''; this.currentPage = 0; this.updatePreviousBookmark = false; this.previousBookmark = ''; this.previousPage = 0; this.nextHashParam = ''; this.fingerprint = fingerprint; this.currentUid = this.uid = 0; this.current = {}; var state = window.history.state; if (this._isStateObjectDefined(state)) { // This corresponds to navigating back to the document // from another page in the browser history. if (state.target.dest) { this.initialDestination = state.target.dest; } else { PDFView.initialBookmark = state.target.hash; } this.currentUid = state.uid; this.uid = state.uid + 1; this.current = state.target; } else { // This corresponds to the loading of a new document. if (state && state.fingerprint && this.fingerprint !== state.fingerprint) { // Reinitialize the browsing history when a new document // is opened in the web viewer. this.reInitialized = true; } this._pushOrReplaceState({ fingerprint: this.fingerprint }, true); } var self = this; window.addEventListener('popstate', function pdfHistoryPopstate(evt) { evt.preventDefault(); evt.stopPropagation(); if (!self.historyUnlocked) { return; } if (evt.state) { // Move back/forward in the history. self._goTo(evt.state); } else { // Handle the user modifying the hash of a loaded document. self.previousHash = window.location.hash.substring(1); // If the history is empty when the hash changes, // update the previous entry in the browser history. if (self.uid === 0) { var previousParams = (self.previousHash && self.currentBookmark && self.previousHash !== self.currentBookmark) ? { hash: self.currentBookmark, page: self.currentPage } : { page: 1 }; self.historyUnlocked = false; self.allowHashChange = false; window.history.back(); self._pushToHistory(previousParams, false, true); window.history.forward(); self.historyUnlocked = true; } self._pushToHistory({ hash: self.previousHash }, false, true); self._updatePreviousBookmark(); } }, false); function pdfHistoryBeforeUnload() { var previousParams = self._getPreviousParams(null, true); if (previousParams) { var replacePrevious = (!self.current.dest && self.current.hash !== self.previousHash); self._pushToHistory(previousParams, false, replacePrevious); self._updatePreviousBookmark(); } // Remove the event listener when navigating away from the document, // since 'beforeunload' prevents Firefox from caching the document. window.removeEventListener('beforeunload', pdfHistoryBeforeUnload, false); } window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false); window.addEventListener('pageshow', function pdfHistoryPageShow(evt) { // If the entire viewer (including the PDF file) is cached in the browser, // we need to reattach the 'beforeunload' event listener since // the 'DOMContentLoaded' event is not fired on 'pageshow'. window.addEventListener('beforeunload', pdfHistoryBeforeUnload, false); }, false); }, _isStateObjectDefined: function pdfHistory_isStateObjectDefined(state) { return (state && state.uid >= 0 && state.fingerprint && this.fingerprint === state.fingerprint && state.target && state.target.hash) ? true : false; }, _pushOrReplaceState: function pdfHistory_pushOrReplaceState(stateObj, replace) { if (replace) { //#if (GENERIC || CHROME) window.history.replaceState(stateObj, '', document.URL); //#else // window.history.replaceState(stateObj, ''); //#endif } else { //#if (GENERIC || CHROME) window.history.pushState(stateObj, '', document.URL); //#else // window.history.pushState(stateObj, ''); //#endif //#if CHROME // if (top === window) { // chrome.runtime.sendMessage('showPageAction'); // } //#endif } }, get isHashChangeUnlocked() { if (!this.initialized) { return true; } // If the current hash changes when moving back/forward in the history, // this will trigger a 'popstate' event *as well* as a 'hashchange' event. // Since the hash generally won't correspond to the exact the position // stored in the history's state object, triggering the 'hashchange' event // can thus corrupt the browser history. // // When the hash changes during a 'popstate' event, we *only* prevent the // first 'hashchange' event and immediately reset allowHashChange. // If it is not reset, the user would not be able to change the hash. var temp = this.allowHashChange; this.allowHashChange = true; return temp; }, _updatePreviousBookmark: function pdfHistory_updatePreviousBookmark() { if (this.updatePreviousBookmark && this.currentBookmark && this.currentPage) { this.previousBookmark = this.currentBookmark; this.previousPage = this.currentPage; this.updatePreviousBookmark = false; } }, updateCurrentBookmark: function pdfHistoryUpdateCurrentBookmark(bookmark, pageNum) { if (this.initialized) { this.currentBookmark = bookmark.substring(1); this.currentPage = pageNum | 0; this._updatePreviousBookmark(); } }, updateNextHashParam: function pdfHistoryUpdateNextHashParam(param) { if (this.initialized) { this.nextHashParam = param; } }, push: function pdfHistoryPush(params, isInitialBookmark) { if (!(this.initialized && this.historyUnlocked)) { return; } if (params.dest && !params.hash) { params.hash = (this.current.hash && this.current.dest && this.current.dest === params.dest) ? this.current.hash : PDFView.getDestinationHash(params.dest).split('#')[1]; } if (params.page) { params.page |= 0; } if (isInitialBookmark) { var target = window.history.state.target; if (!target) { // Invoked when the user specifies an initial bookmark, // thus setting PDFView.initialBookmark, when the document is loaded. this._pushToHistory(params, false); this.previousHash = window.location.hash.substring(1); } this.updatePreviousBookmark = this.nextHashParam ? false : true; if (target) { // If the current document is reloaded, // avoid creating duplicate entries in the history. this._updatePreviousBookmark(); } return; } if (this.nextHashParam) { if (this.nextHashParam === params.hash) { this.nextHashParam = null; this.updatePreviousBookmark = true; return; } else { this.nextHashParam = null; } } if (params.hash) { if (this.current.hash) { if (this.current.hash !== params.hash) { this._pushToHistory(params, true); } else { if (!this.current.page && params.page) { this._pushToHistory(params, false, true); } this.updatePreviousBookmark = true; } } else { this._pushToHistory(params, true); } } else if (this.current.page && params.page && this.current.page !== params.page) { this._pushToHistory(params, true); } }, _getPreviousParams: function pdfHistory_getPreviousParams(onlyCheckPage, beforeUnload) { if (!(this.currentBookmark && this.currentPage)) { return null; } else if (this.updatePreviousBookmark) { this.updatePreviousBookmark = false; } if (this.uid > 0 && !(this.previousBookmark && this.previousPage)) { // Prevent the history from getting stuck in the current state, // effectively preventing the user from going back/forward in the history. // // This happens if the current position in the document didn't change when // the history was previously updated. The reasons for this are either: // 1. The current zoom value is such that the document does not need to, // or cannot, be scrolled to display the destination. // 2. The previous destination is broken, and doesn't actally point to a // position within the document. // (This is either due to a bad PDF generator, or the user making a // mistake when entering a destination in the hash parameters.) return null; } if ((!this.current.dest && !onlyCheckPage) || beforeUnload) { if (this.previousBookmark === this.currentBookmark) { return null; } } else if (this.current.page || onlyCheckPage) { if (this.previousPage === this.currentPage) { return null; } } else { return null; } var params = { hash: this.currentBookmark, page: this.currentPage }; if (PresentationMode.active) { params.hash = null; } return params; }, _stateObj: function pdfHistory_stateObj(params) { return { fingerprint: this.fingerprint, uid: this.uid, target: params }; }, _pushToHistory: function pdfHistory_pushToHistory(params, addPrevious, overwrite) { if (!this.initialized) { return; } if (!params.hash && params.page) { params.hash = ('page=' + params.page); } if (addPrevious && !overwrite) { var previousParams = this._getPreviousParams(); if (previousParams) { var replacePrevious = (!this.current.dest && this.current.hash !== this.previousHash); this._pushToHistory(previousParams, false, replacePrevious); } } this._pushOrReplaceState(this._stateObj(params), (overwrite || this.uid === 0)); this.currentUid = this.uid++; this.current = params; this.updatePreviousBookmark = true; }, _goTo: function pdfHistory_goTo(state) { if (!(this.initialized && this.historyUnlocked && this._isStateObjectDefined(state))) { return; } if (!this.reInitialized && state.uid < this.currentUid) { var previousParams = this._getPreviousParams(true); if (previousParams) { this._pushToHistory(this.current, false); this._pushToHistory(previousParams, false); this.currentUid = state.uid; window.history.back(); return; } } this.historyUnlocked = false; if (state.target.dest) { PDFView.navigateTo(state.target.dest); } else { PDFView.setHash(state.target.hash); } this.currentUid = state.uid; if (state.uid > this.uid) { this.uid = state.uid; } this.current = state.target; this.updatePreviousBookmark = true; var currentHash = window.location.hash.substring(1); if (this.previousHash !== currentHash) { this.allowHashChange = false; } this.previousHash = currentHash; this.historyUnlocked = true; }, back: function pdfHistoryBack() { this.go(-1); }, forward: function pdfHistoryForward() { this.go(1); }, go: function pdfHistoryGo(direction) { if (this.initialized && this.historyUnlocked) { var state = window.history.state; if (direction === -1 && state && state.uid > 0) { window.history.back(); } else if (direction === 1 && state && state.uid < (this.uid - 1)) { window.history.forward(); } } } };