diff --git a/src/client/nav/nav.js b/src/client/nav/nav.js index 8205c844..a72a477f 100644 --- a/src/client/nav/nav.js +++ b/src/client/nav/nav.js @@ -415,7 +415,8 @@ spf.nav.navigate_ = function(url, options, info) { spf.nav.navigateAddHistory_(url, info.referer, handleError); } // Then attempt to scroll. - spf.nav.navigateScroll_(url, info.position); + spf.nav.navigateScroll_(url, info.position, true); + info.scrolled = true; return; } @@ -550,9 +551,11 @@ spf.nav.navigateSendRequest_ = function(url, options, info) { * @param {Array.=} opt_position The window position to scroll to * in [x, y] format. If present, this value is used instead of any target * in the URL hash. + * @param {boolean=} opt_top Whether to scroll to the top if no target was + * found and no position was provided. * @private */ -spf.nav.navigateScroll_ = function(url, opt_position) { +spf.nav.navigateScroll_ = function(url, opt_position, opt_top) { // If a position is defined, scroll to it. if (opt_position) { spf.debug.debug(' scrolling to position', opt_position); @@ -568,7 +571,7 @@ spf.nav.navigateScroll_ = function(url, opt_position) { spf.debug.debug(' scrolling into view', result[2]); el.scrollIntoView(); } - } else { + } else if (opt_top && spf.config.get('experimental-scroll-position')) { spf.debug.debug(' scrolling to top'); window.scroll(0, 0); } @@ -659,9 +662,9 @@ spf.nav.handleNavigatePart_ = function(options, info, url, partial) { } try { - spf.nav.response.process(url, partial, function() { + spf.nav.response.process(url, partial, info, function() { spf.nav.dispatchPartDone_(url, partial, options); - }, true, info.reverse); + }); } catch (err) { // If an exception is caught during processing, log, execute the error // handler, and bail. @@ -723,10 +726,18 @@ spf.nav.handleNavigateSuccess_ = function(options, info, url, response) { // so an empty object is used to ensure events/callbacks are properly // queued after existing ones from any ongoing part prcoessing. var r = /** @type {spf.SingleResponse} */ (multipart ? {} : response); - spf.nav.response.process(url, r, function() { - spf.nav.navigateScroll_(url, info.position); + spf.nav.response.process(url, r, info, function() { + // If this navigation was from history, attempt to scroll to the previous + // position after all processing is complete. This should not be done + // earlier because the prevous position might rely on page width/height + // that is changed during the processing. + // Fallback to scrolling to the top if neither a hash target nor a + // history position exists and the window was not previously scrolled + // during response processing. + spf.nav.navigateScroll_(url, info.position, !info.scrolled); + info.scrolled = true; spf.nav.dispatchDone_(url, response, options); - }, true, info.reverse); + }); } catch (err) { // If an exception is caught during processing, log, execute the error // handler and bail. @@ -1047,7 +1058,7 @@ spf.nav.handleLoadPart_ = function(isPrefetch, options, info, url, partial) { var processFn = isPrefetch ? spf.nav.response.preprocess : spf.nav.response.process; - processFn(url, partial, function() { + processFn(url, partial, info, function() { // Note: pass "true" to only execute callbacks and not dispatch events. spf.nav.dispatchPartDone_(url, partial, options, true); }); @@ -1131,7 +1142,7 @@ spf.nav.handleLoadSuccess_ = function(isPrefetch, options, info, url, // so an empty object is used to ensure the callback is properly // queued after existing ones from any ongoing part prcoessing. var r = /** @type {spf.SingleResponse} */ (multipart ? {} : response); - processFn(url, r, function() { + processFn(url, r, info, function() { // Note: pass "true" to only execute callbacks and not dispatch events. spf.nav.dispatchDone_(url, response, options, true); }); @@ -1195,12 +1206,12 @@ spf.nav.process = function(response, opt_callback) { var parts = response['parts']; for (var i = 0; i < parts.length; i++) { var fn = spf.bind(done, null, i, parts.length - 1); - spf.nav.response.process(url, parts[i], fn); + spf.nav.response.process(url, parts[i], null, fn); } } else { response = /** @type {spf.SingleResponse} */ (response); var fn = spf.bind(done, null, 0, 0); - spf.nav.response.process(url, response, fn); + spf.nav.response.process(url, response, null, fn); } }; @@ -1570,6 +1581,7 @@ spf.nav.createOptions_ = function(opt_options) { * position: (Array.|undefined), * referer: (string|undefined), * reverse: (boolean|undefined), + * scrolled: (boolean|undefined), * type: (string|undefined) * }} * @private @@ -1629,6 +1641,12 @@ spf.nav.Info = function(opt_info) { * @type {boolean} */ this.reverse = !!opt_info.reverse; + /** + * Whether the window has been scrolled to `position` or to the top during + * this navigation request. + * @type {boolean} + */ + this.scrolled = !!opt_info.scrolled; /** * The type of request, one of the following: "navigate", "navigate-back", * "navigate-forward", "load", "prefetch". If not yet determined (i.e. before diff --git a/src/client/nav/response.js b/src/client/nav/response.js index 27d5f0a1..a20018d5 100644 --- a/src/client/nav/response.js +++ b/src/client/nav/response.js @@ -112,18 +112,18 @@ spf.nav.response.parse = function(text, opt_multipart, opt_lastDitch) { * * @param {string} url The URL of the response being processed. * @param {spf.SingleResponse} response The SPF response object to process. + * @param {spf.nav.Info=} opt_info The navigation info object. * @param {function(string, spf.SingleResponse)=} opt_callback Function to * execute when processing is done; the first argument is `url`, * the second argument is `response`. - * @param {boolean=} opt_navigate Whether this is a navigation request. Only - * navigation requests will process history changes. - * @param {boolean=} opt_reverse Whether this is "backwards" navigation. True - * when the "back" button is clicked and a request is in response to a - * popState event. */ -spf.nav.response.process = function(url, response, opt_callback, opt_navigate, - opt_reverse) { - spf.debug.info('nav.response.process ', response, opt_reverse); +spf.nav.response.process = function(url, response, opt_info, opt_callback) { + spf.debug.info('nav.response.process ', response, opt_info); + + var isNavigate = opt_info && spf.string.startsWith(opt_info.type, 'navigate'); + var isReverse = opt_info && opt_info.reverse; + var hasPosition = opt_info && !!opt_info.position; + var hasScrolled = opt_info && opt_info.scrolled; // Convert the URL to absolute, to be used for finding the task queue. var key = 'process ' + spf.url.absolute(url); @@ -144,7 +144,8 @@ spf.nav.response.process = function(url, response, opt_callback, opt_navigate, } // Add the new history state (immediate), if needed. - if (opt_navigate && response['url']) { + // Only navigation requests should process URL changes. + if (isNavigate && response['url']) { var fullUrl = spf.url.absolute(response['url']); // Update the history state if the url doesn't match. if (fullUrl != spf.nav.response.getCurrentUrl_()) { @@ -203,6 +204,20 @@ spf.nav.response.process = function(url, response, opt_callback, opt_navigate, fn = spf.bind(function(id, body, timing) { var el = document.getElementById(id); if (el) { + // Scroll to the top before the first content update, if needed. + // Only non-history navigation requests scroll to the top immediately. + // Other history navigation requests handle scrolling after all + // processing is done to avoid jumping to the top and back down to the + // saved position afterwards. + if (isNavigate && !hasPosition && !hasScrolled && + spf.config.get('experimental-scroll-position')) { + spf.debug.debug(' scrolling to top'); + window.scroll(0, 0); + hasScrolled = true; + if (opt_info) { + opt_info.scrolled = true; + } + } // Extract scripts and styles from the fragment. var extracted = spf.nav.response.extract_(body); var animationClass = /** @type {string} */ ( @@ -246,16 +261,16 @@ spf.nav.response.process = function(url, response, opt_callback, opt_navigate, var animationFn; var animationData = { extracted: extracted, - reverse: !!opt_reverse, + reverse: isReverse, currentEl: null, // Set in Step 1. pendingEl: null, // Set in Step 1. parentEl: el, currentClass: animationClass + '-old', pendingClass: animationClass + '-new', - startClass: !!opt_reverse ? + startClass: isReverse ? animationClass + '-reverse-start' : animationClass + '-forward-start', - endClass: !!opt_reverse ? + endClass: isReverse ? animationClass + '-reverse-end' : animationClass + '-forward-end' }; @@ -388,11 +403,12 @@ spf.nav.response.process = function(url, response, opt_callback, opt_navigate, * * @param {string} url The URL of the response being preprocessed. * @param {spf.SingleResponse} response The SPF response object to preprocess. + * @param {spf.nav.Info=} opt_info The navigation info object. * @param {function(string, spf.SingleResponse)=} opt_callback Function to * execute when preprocessing is done; the first argument is `url`, * the second argument is `response`. */ -spf.nav.response.preprocess = function(url, response, opt_callback) { +spf.nav.response.preprocess = function(url, response, opt_info, opt_callback) { spf.debug.info('nav.response.preprocess ', response); // Convert the URL to absolute, to be used for finding the task queue. var key = 'preprocess ' + spf.url.absolute(url); diff --git a/src/client/nav/response_test.js b/src/client/nav/response_test.js index 0e5d2acb..67ee610c 100644 --- a/src/client/nav/response_test.js +++ b/src/client/nav/response_test.js @@ -532,6 +532,7 @@ describe('spf.nav.response', function() { {'class': 'first'}); var bar = spf.testing.dom.createElement('bar', undefined, {'dir': 'ltr'}); + var info = { type: 'navigate' }; var response = { 'attr': { 'foo': { 'dir': 'rtl', 'class': 'last' }, @@ -539,7 +540,7 @@ describe('spf.nav.response', function() { } }; - spf.nav.response.process('/page', response, null, true); + spf.nav.response.process('/page', response, info); expect(foo.className).toEqual('last'); expect(foo.getAttribute('dir')).toEqual('rtl'); expect(bar.className).toEqual('last'); @@ -550,6 +551,7 @@ describe('spf.nav.response', function() { var foo = spf.testing.dom.createElement('foo', 'one'); var bar = spf.testing.dom.createElement('bar'); + var info = { type: 'navigate' }; var response = { 'body': { 'foo': 'two', @@ -557,29 +559,32 @@ describe('spf.nav.response', function() { } }; - spf.nav.response.process('/page', response, null, true); + spf.nav.response.process('/page', response, info); expect(foo.innerHTML).toEqual('two'); expect(bar.innerHTML).toEqual('two'); }); it('updates history for navigate with redirect url', function() { + var info = { type: 'navigate' }; var response = { 'url': 'http://www.youtube.com/watch?v=3' }; - spf.nav.response.process('/watch?v=2', response, null, true); + spf.nav.response.process('/watch?v=2', response, info); expect(spf.history.replace).toHaveBeenCalledWith(response['url']); }); it('does not update history for navigate without redirect url', function() { + var info = { type: 'navigate' }; var response = {}; - spf.nav.response.process('/watch?v=2', response, null, true); + spf.nav.response.process('/watch?v=2', response, info); expect(spf.history.replace).not.toHaveBeenCalled(); }); it('does not update history for load with redirect url', function() { + var info = { type: 'load' }; var response = { 'url': 'http://www.youtube.com/watch?v=3' }; - spf.nav.response.process('/watch?v=2', response, null, false); + spf.nav.response.process('/watch?v=2', response, info); expect(spf.history.replace).not.toHaveBeenCalled(); });