Skip to content

Commit

Permalink
Scroll to top during navigation processing (experimental).
Browse files Browse the repository at this point in the history
- Scroll to top when processing the first html fragment of a response.
- If a position is stored in history, don't scroll to top and instead scroll
  to the saved position after processing is complete.
- Guard the new scroll-to-top behavior with an `experimental-scroll-position`
  config.

Progress on youtube#285.
  • Loading branch information
nicksay committed Feb 13, 2015
1 parent d6bf185 commit ad6a5bb
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 30 deletions.
42 changes: 30 additions & 12 deletions src/client/nav/nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -550,9 +551,11 @@ spf.nav.navigateSendRequest_ = function(url, options, info) {
* @param {Array.<number>=} 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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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);
}
};

Expand Down Expand Up @@ -1570,6 +1581,7 @@ spf.nav.createOptions_ = function(opt_options) {
* position: (Array.<number>|undefined),
* referer: (string|undefined),
* reverse: (boolean|undefined),
* scrolled: (boolean|undefined),
* type: (string|undefined)
* }}
* @private
Expand Down Expand Up @@ -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
Expand Down
42 changes: 29 additions & 13 deletions src/client/nav/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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_()) {
Expand Down Expand Up @@ -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} */ (
Expand Down Expand Up @@ -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'
};
Expand Down Expand Up @@ -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);
Expand Down
15 changes: 10 additions & 5 deletions src/client/nav/response_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -532,14 +532,15 @@ 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' },
'bar': { 'dir': 'rtl', 'class': 'last' }
}
};

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');
Expand All @@ -550,36 +551,40 @@ 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',
'bar': 'two'
}
};

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();
});

Expand Down

0 comments on commit ad6a5bb

Please sign in to comment.