pdf.js/web/pdf_history.js

427 lines
14 KiB
JavaScript
Raw Normal View History

/* 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 chrome */
2017-04-15 00:32:36 +09:00
import { domEvents } from './dom_events';
function PDFHistory(options) {
this.linkService = options.linkService;
this.eventBus = options.eventBus || domEvents.getGlobalEventBus();
this.initialized = false;
this.initialDestination = null;
this.initialBookmark = null;
}
PDFHistory.prototype = {
/**
* @param {string} fingerprint
*/
initialize: function pdfHistoryInitialize(fingerprint) {
this.initialized = true;
this.reInitialized = false;
this.allowHashChange = true;
this.historyUnlocked = true;
this.isViewerInPresentationMode = false;
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 {
this.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) {
if (!self.historyUnlocked) {
return;
}
if (evt.state) {
// Move back/forward in the history.
self._goTo(evt.state);
return;
}
// If the state is not set, then the user tried to navigate to a
// different hash by manually editing the URL and pressing Enter, or by
// clicking on an in-page link (e.g. the "current view" link).
// Save the current view state to the browser history.
// Note: In Firefox, history.null could also be null after an in-page
// navigation to the same URL, and without dispatching the popstate
// event: https://bugzilla.mozilla.org/show_bug.cgi?id=1183881
if (self.uid === 0) {
// Replace the previous state if it was not explicitly set.
var previousParams = (self.previousHash && self.currentBookmark &&
self.previousHash !== self.currentBookmark) ?
{hash: self.currentBookmark, page: self.currentPage} :
{page: 1};
replacePreviousHistoryState(previousParams, function() {
Refactor the previous history rewriting logic When the user edits the URL and changes the reference fragment (hash), PDF.js intercepts this action, and saves the then-current history state in the previous history entry. This is implemented by navigating back, editing the history and navigating forward again. The current logic has a flaw: It assumes that calling history.back() and history.forward() immediately updates the history state. This is however not guaranteed by the web standards, which states that calling e.g. history.back "must traverse the history by a delta -1", which means that the browser must QUEUE a task to traverse the session history, per spec: http://w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-back https://html.spec.whatwg.org/multipage/browsers.html#dom-history-back Firefox and Internet Explorer deviate from the standards by immediately changing the history state instead of queuing the navigation. WebKit derived browsers (Chrome, Opera, Safari) and Opera presto do not. The user-visible consequence of strictly adhering to the standards in PDF.js can be shown as follows: 1. Edit the URL. 2. Append #page=2 for example. 3. Press Enter. -> Presto and WebKit: PDF.js reverts to the previous URL. -> Gecko and Trident: PDF.js keeps the new URL, as expected. To fix the issue, modification of the previous history item happens in a few asynchronous steps, guided by the popstate event to detect when the history navigation request has been committed. -- Some more implementation notes: I have removed the preventDefault and stopPropagation calls, because popstate is not cancelable, and window is already the last target of the event propagation. The previous allowHashChange logic was hard to follow, because it did not explain that hashchange will be called twice; once during the popstate handler for history.back() (which will reset allowHashChange), and again for history.forward() (where allowHashChange will be false). The purpose of allowHashChange is now more explicit, by incorporating the logic in the replacePreviousHistoryState helper function.
2015-07-15 06:33:37 +09:00
updateHistoryWithCurrentHash();
});
} else {
updateHistoryWithCurrentHash();
}
});
Refactor the previous history rewriting logic When the user edits the URL and changes the reference fragment (hash), PDF.js intercepts this action, and saves the then-current history state in the previous history entry. This is implemented by navigating back, editing the history and navigating forward again. The current logic has a flaw: It assumes that calling history.back() and history.forward() immediately updates the history state. This is however not guaranteed by the web standards, which states that calling e.g. history.back "must traverse the history by a delta -1", which means that the browser must QUEUE a task to traverse the session history, per spec: http://w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-back https://html.spec.whatwg.org/multipage/browsers.html#dom-history-back Firefox and Internet Explorer deviate from the standards by immediately changing the history state instead of queuing the navigation. WebKit derived browsers (Chrome, Opera, Safari) and Opera presto do not. The user-visible consequence of strictly adhering to the standards in PDF.js can be shown as follows: 1. Edit the URL. 2. Append #page=2 for example. 3. Press Enter. -> Presto and WebKit: PDF.js reverts to the previous URL. -> Gecko and Trident: PDF.js keeps the new URL, as expected. To fix the issue, modification of the previous history item happens in a few asynchronous steps, guided by the popstate event to detect when the history navigation request has been committed. -- Some more implementation notes: I have removed the preventDefault and stopPropagation calls, because popstate is not cancelable, and window is already the last target of the event propagation. The previous allowHashChange logic was hard to follow, because it did not explain that hashchange will be called twice; once during the popstate handler for history.back() (which will reset allowHashChange), and again for history.forward() (where allowHashChange will be false). The purpose of allowHashChange is now more explicit, by incorporating the logic in the replacePreviousHistoryState helper function.
2015-07-15 06:33:37 +09:00
function updateHistoryWithCurrentHash() {
self.previousHash = window.location.hash.slice(1);
self._pushToHistory({hash: self.previousHash}, false, true);
self._updatePreviousBookmark();
}
Refactor the previous history rewriting logic When the user edits the URL and changes the reference fragment (hash), PDF.js intercepts this action, and saves the then-current history state in the previous history entry. This is implemented by navigating back, editing the history and navigating forward again. The current logic has a flaw: It assumes that calling history.back() and history.forward() immediately updates the history state. This is however not guaranteed by the web standards, which states that calling e.g. history.back "must traverse the history by a delta -1", which means that the browser must QUEUE a task to traverse the session history, per spec: http://w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-back https://html.spec.whatwg.org/multipage/browsers.html#dom-history-back Firefox and Internet Explorer deviate from the standards by immediately changing the history state instead of queuing the navigation. WebKit derived browsers (Chrome, Opera, Safari) and Opera presto do not. The user-visible consequence of strictly adhering to the standards in PDF.js can be shown as follows: 1. Edit the URL. 2. Append #page=2 for example. 3. Press Enter. -> Presto and WebKit: PDF.js reverts to the previous URL. -> Gecko and Trident: PDF.js keeps the new URL, as expected. To fix the issue, modification of the previous history item happens in a few asynchronous steps, guided by the popstate event to detect when the history navigation request has been committed. -- Some more implementation notes: I have removed the preventDefault and stopPropagation calls, because popstate is not cancelable, and window is already the last target of the event propagation. The previous allowHashChange logic was hard to follow, because it did not explain that hashchange will be called twice; once during the popstate handler for history.back() (which will reset allowHashChange), and again for history.forward() (where allowHashChange will be false). The purpose of allowHashChange is now more explicit, by incorporating the logic in the replacePreviousHistoryState helper function.
2015-07-15 06:33:37 +09:00
function replacePreviousHistoryState(params, callback) {
// To modify the previous history entry, the following happens:
// 1. history.back()
// 2. _pushToHistory, which calls history.replaceState( ... )
// 3. history.forward()
// Because a navigation via the history API does not immediately update
// the history state, the popstate event is used for synchronization.
self.historyUnlocked = false;
// Suppress the hashchange event to avoid side effects caused by
// navigating back and forward.
self.allowHashChange = false;
window.addEventListener('popstate', rewriteHistoryAfterBack);
history.back();
function rewriteHistoryAfterBack() {
window.removeEventListener('popstate', rewriteHistoryAfterBack);
window.addEventListener('popstate', rewriteHistoryAfterForward);
self._pushToHistory(params, false, true);
history.forward();
Refactor the previous history rewriting logic When the user edits the URL and changes the reference fragment (hash), PDF.js intercepts this action, and saves the then-current history state in the previous history entry. This is implemented by navigating back, editing the history and navigating forward again. The current logic has a flaw: It assumes that calling history.back() and history.forward() immediately updates the history state. This is however not guaranteed by the web standards, which states that calling e.g. history.back "must traverse the history by a delta -1", which means that the browser must QUEUE a task to traverse the session history, per spec: http://w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-back https://html.spec.whatwg.org/multipage/browsers.html#dom-history-back Firefox and Internet Explorer deviate from the standards by immediately changing the history state instead of queuing the navigation. WebKit derived browsers (Chrome, Opera, Safari) and Opera presto do not. The user-visible consequence of strictly adhering to the standards in PDF.js can be shown as follows: 1. Edit the URL. 2. Append #page=2 for example. 3. Press Enter. -> Presto and WebKit: PDF.js reverts to the previous URL. -> Gecko and Trident: PDF.js keeps the new URL, as expected. To fix the issue, modification of the previous history item happens in a few asynchronous steps, guided by the popstate event to detect when the history navigation request has been committed. -- Some more implementation notes: I have removed the preventDefault and stopPropagation calls, because popstate is not cancelable, and window is already the last target of the event propagation. The previous allowHashChange logic was hard to follow, because it did not explain that hashchange will be called twice; once during the popstate handler for history.back() (which will reset allowHashChange), and again for history.forward() (where allowHashChange will be false). The purpose of allowHashChange is now more explicit, by incorporating the logic in the replacePreviousHistoryState helper function.
2015-07-15 06:33:37 +09:00
}
function rewriteHistoryAfterForward() {
window.removeEventListener('popstate', rewriteHistoryAfterForward);
self.allowHashChange = true;
self.historyUnlocked = true;
callback();
}
}
Refactor the previous history rewriting logic When the user edits the URL and changes the reference fragment (hash), PDF.js intercepts this action, and saves the then-current history state in the previous history entry. This is implemented by navigating back, editing the history and navigating forward again. The current logic has a flaw: It assumes that calling history.back() and history.forward() immediately updates the history state. This is however not guaranteed by the web standards, which states that calling e.g. history.back "must traverse the history by a delta -1", which means that the browser must QUEUE a task to traverse the session history, per spec: http://w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-back https://html.spec.whatwg.org/multipage/browsers.html#dom-history-back Firefox and Internet Explorer deviate from the standards by immediately changing the history state instead of queuing the navigation. WebKit derived browsers (Chrome, Opera, Safari) and Opera presto do not. The user-visible consequence of strictly adhering to the standards in PDF.js can be shown as follows: 1. Edit the URL. 2. Append #page=2 for example. 3. Press Enter. -> Presto and WebKit: PDF.js reverts to the previous URL. -> Gecko and Trident: PDF.js keeps the new URL, as expected. To fix the issue, modification of the previous history item happens in a few asynchronous steps, guided by the popstate event to detect when the history navigation request has been committed. -- Some more implementation notes: I have removed the preventDefault and stopPropagation calls, because popstate is not cancelable, and window is already the last target of the event propagation. The previous allowHashChange logic was hard to follow, because it did not explain that hashchange will be called twice; once during the popstate handler for history.back() (which will reset allowHashChange), and again for history.forward() (where allowHashChange will be false). The purpose of allowHashChange is now more explicit, by incorporating the logic in the replacePreviousHistoryState helper function.
2015-07-15 06:33:37 +09:00
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);
}
window.addEventListener('beforeunload', pdfHistoryBeforeUnload);
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);
});
self.eventBus.on('presentationmodechanged', function(e) {
self.isViewerInPresentationMode = e.active;
});
},
clearHistoryState: function pdfHistory_clearHistoryState() {
this._pushOrReplaceState(null, true);
},
_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) {
// history.state.chromecomState is managed by chromecom.js.
if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME') &&
window.history.state && 'chromecomState' in window.history.state) {
stateObj = stateObj || {};
stateObj.chromecomState = window.history.state.chromecomState;
}
if (replace) {
if (typeof PDFJSDev === 'undefined' ||
PDFJSDev.test('GENERIC || CHROME')) {
window.history.replaceState(stateObj, '', document.URL);
} else {
window.history.replaceState(stateObj, '');
}
} else {
if (typeof PDFJSDev === 'undefined' ||
PDFJSDev.test('GENERIC || CHROME')) {
window.history.pushState(stateObj, '', document.URL);
} else {
window.history.pushState(stateObj, '');
}
if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('CHROME') &&
top === window) {
chrome.runtime.sendMessage('showPageAction');
}
}
},
get isHashChangeUnlocked() {
if (!this.initialized) {
return true;
}
return this.allowHashChange;
},
_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 :
this.linkService.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 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;
}
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 (this.isViewerInPresentationMode) {
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) {
this.linkService.navigateTo(state.target.dest);
} else {
this.linkService.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();
}
}
}
};
export {
PDFHistory,
};