From 077195d8ce971f935d68190a65ec5278eacfb4d0 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 7 Sep 2017 11:37:23 +0200 Subject: [PATCH 1/3] Ensure that the `PDFHistory._updateViewareaTimeout` is always reset when the history is updated --- web/pdf_history.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/pdf_history.js b/web/pdf_history.js index 7d90fe001..157013af8 100644 --- a/web/pdf_history.js +++ b/web/pdf_history.js @@ -358,6 +358,13 @@ class PDFHistory { * @private */ _updateInternalState(destination, uid, removeTemporary = false) { + if (this._updateViewareaTimeout) { + // When updating `this._destination`, make sure that we always wait for + // the next 'updateviewarea' event before (potentially) attempting to + // push the current position to the browser history. + clearTimeout(this._updateViewareaTimeout); + this._updateViewareaTimeout = null; + } if (removeTemporary && destination && destination.temporary) { // When the `destination` comes from the browser history, // we no longer treat it as a *temporary* position. From b7c4d788ed60a0852ac7ab243b9e66fdac891a76 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 7 Sep 2017 12:06:43 +0200 Subject: [PATCH 2/3] Prevent a temporary position from being added to the history while a destination is scrolled into view Since e.g. zooming can occur when navigating to a new destionation, ensure that a resulting 'updateviewarea' event doesn't trigger adding of a *temporary* position to the browser history at a bad time. --- web/pdf_history.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/pdf_history.js b/web/pdf_history.js index 157013af8..5c834f386 100644 --- a/web/pdf_history.js +++ b/web/pdf_history.js @@ -189,6 +189,17 @@ class PDFHistory { hash, page: pageNumber, }, forceReplace); + + if (!this._popStateInProgress) { + // Prevent the browser history from updating while the new destination is + // being scrolled into view, to avoid potentially inconsistent state. + this._popStateInProgress = true; + // We defer the resetting of `this._popStateInProgress`, to account for + // e.g. zooming occuring when the new destination is being navigated to. + Promise.resolve().then(() => { + this._popStateInProgress = false; + }); + } } /** From 938dffb06b8dcd54ea5c7c2ad531d145b5fdaa5e Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 7 Sep 2017 12:19:59 +0200 Subject: [PATCH 3/3] Reduce the value of `UPDATE_VIEWAREA_TIMEOUT` and simplify the 'popstate' event handler to avoid subtle bugs When testing the new `PDFHistory` implementation in practice, I felt that the current value of `UPDATE_VIEWAREA_TIMEOUT` is too large to be truly useful. The purpose of the timeout is to attempt to address (the PDF.js part of) https://bugzilla.mozilla.org/show_bug.cgi?id=1153393, and it's currently fairly easy for the user e.g. close the browser before the timeout had a change to finish. Obviously, the timeout is a best-effort solution, but with the current value of `UPDATE_VIEWAREA_TIMEOUT` it's not as useful as one would want. Please note that lowering it shouldn't be a problem, since it still prevents the browser history from updating at *every* 'updateviewarea' event or during (quick) scrolling, which is all that's really needed to not impact the UX negatively. --- Furthermore, with this lower timeout, we can also simplify the part of the 'popstate' event handler that attempted to update the browser history with the current position before moving back. In most cases, the current position will now already exist in the history, and this *greatly* decreases the complexity of this code path. The main impetus for this change though, is that I unfortunately found that given the asynchronous nature of updating the browser history, there is some *edge* cases where that code could cause history corruption. In practice, the user could thus get "stuck" at a particular history entry and not be able to move back. I haven't got any reliable STR for this, since it's so difficult to trigger, but it involved navigating around in a document such that a number of destinations are added to the browser history and then changing the rotation before going back/forward in the history. Rather that attempting to patch this code, and making it even more difficult to understand than it already is or adding more asynchronous behaviour, by far the easiest solution is to remove it and simply rely on the (lowered) `UPDATE_VIEWAREA_TIMEOUT` instead. --- web/pdf_history.js | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/web/pdf_history.js b/web/pdf_history.js index 5c834f386..522af3e6a 100644 --- a/web/pdf_history.js +++ b/web/pdf_history.js @@ -21,7 +21,7 @@ const HASH_CHANGE_TIMEOUT = 1000; // milliseconds // Heuristic value used when adding the current position to the browser history. const POSITION_UPDATED_THRESHOLD = 50; // Heuristic value used when adding a temporary position to the browser history. -const UPDATE_VIEWAREA_TIMEOUT = 2000; // milliseconds +const UPDATE_VIEWAREA_TIMEOUT = 1000; // milliseconds /** * @typedef {Object} PDFHistoryOptions @@ -493,34 +493,6 @@ class PDFHistory { }); } - // This case corresponds to navigation backwards in the browser history. - if (state.uid < this._currentUid && this._position && this._destination) { - let shouldGoBack = false; - - if (this._destination.temporary) { - // If the `this._destination` contains a *temporary* position, always - // push the `this._position` to the browser history before moving back. - this._pushOrReplaceState(this._position); - shouldGoBack = true; - } else if (this._destination.page && - this._destination.page !== this._position.first && - this._destination.page !== this._position.page) { - // If the `page` of the `this._destination` is no longer visible, - // push the `this._position` to the browser history before moving back. - this._pushOrReplaceState(this._destination); - this._pushOrReplaceState(this._position); - shouldGoBack = true; - } - if (shouldGoBack) { - // After `window.history.back()`, we must not enter this block on the - // resulting 'popstate' event, since that may cause an infinite loop. - this._currentUid = state.uid; - - window.history.back(); - return; - } - } - // Navigate to the new destination. let destination = state.destination; this._updateInternalState(destination, state.uid,