Permalink
Browse files

basic hash assignment handling

  • Loading branch information...
johnbender committed Oct 30, 2012
1 parent 3974794 commit 9c1c15f467398d855ca7eda4185f1634b6e6951b
Showing with 163 additions and 57 deletions.
  1. +115 −54 js/navigation/navigate.js
  2. +1 −1 js/navigation/path.js
  3. +47 −2 tests/unit/navigation/navigate_method.js
View
@@ -15,28 +15,8 @@ define([
// TODO consider queueing navigation activity until previous activities have completed
// so that end users don't have to think about it. Punting for now
$.navigate = function( url, data ) {
var href, state,
hash = path.parseUrl( url ).hash,
isPath = path.isPath( hash ),
resolutionUrl = isPath ? path.getLocation() : $.mobile.getDocumentUrl();
// #/foo/bar.html => /foo/bar.html
// #foo => #foo
hash = isPath ? hash.replace( "#", "" ) : hash;
// make the hash abolute with the current href
href = path.makeUrlAbsolute( hash, resolutionUrl );
if ( isPath ) {
href = path.resetUIKeys( href );
}
state = $.extend( data, {
url: url,
hash: hash,
title: document.title
});
$.navigate = function( url, data, noEvents ) {
var state;
// NOTE we currently _leave_ the appended hash in the hash in the interest
// of seeing what happens and if we can support that before the hash is
@@ -50,7 +30,12 @@ define([
// We then trigger a new popstate event on the window with a null state
// so that the navigate events can conclude their work properly
history.ignoreNextHashChange = true;
window.location.hash = url;
window.location.hash = path.cleanHash( url );
state = $.extend( data, {
url: url,
title: document.title
});
if( $.support.pushState ) {
popstateEvent = new $.Event( "popstate" );
@@ -59,22 +44,14 @@ define([
state: null
};
// replace the current url with the new href and store the state
// Note that in some cases we might be replacing an url with the
// same url. We do this anyways because we need to make sure that
// all of our history entries have a state object associated with
// them. This allows us to work around the case where $.mobile.back()
// is called to transition from an external page to an embedded page.
// In that particular case, a hashchange event is *NOT* generated by the browser.
// Ensuring each history entry has a state object means that onPopState()
// will always trigger our hashchange callback even when a hashchange event
// is not fired.
window.history.replaceState( state, document.title, href );
$.navigate.squash( url, data );
// Trigger a new faux popstate event to replace the one that we
// caught that was triggered by the hash setting above.
history.ignoreNextPopState = true;
$( window ).trigger( popstateEvent );
if( !noEvents ) {
history.ignoreNextPopState = true;
$( window ).trigger( popstateEvent );
}
}
// record the history entry so that the information can be included
@@ -83,6 +60,40 @@ define([
history.add( url, state );
};
$.navigate.squash = function( url, data ) {
var state, href,
hash = url,
isPath = path.isPath( hash ),
resolutionUrl = isPath ? path.getLocation() : $.mobile.getDocumentUrl();
// make the hash abolute with the current href
href = path.makeUrlAbsolute( hash, resolutionUrl );
if ( isPath ) {
href = path.resetUIKeys( href );
}
state = $.extend( data, {
url: url,
hash: hash,
title: document.title
});
// replace the current url with the new href and store the state
// Note that in some cases we might be replacing an url with the
// same url. We do this anyways because we need to make sure that
// all of our history entries have a state object associated with
// them. This allows us to work around the case where $.mobile.back()
// is called to transition from an external page to an embedded page.
// In that particular case, a hashchange event is *NOT* generated by the browser.
// Ensuring each history entry has a state object means that onPopState()
// will always trigger our hashchange callback even when a hashchange event
// is not fired.
window.history.replaceState( state, document.title, href );
return state;
};
// This binding is intended to catch the popstate events that are fired
// when execution of the `$.navigate` method stops at window.location.hash = url;
// and completely prevent them from propagating. The popstate event will then be
@@ -91,6 +102,8 @@ define([
// TODO grab the original event here and use it for the synthetic event in the
// second half of the navigate execution that will follow this binding
$( window ).bind( "popstate.history", function( event ) {
var hash, state;
// Partly to support our test suite which manually alters the support
// value to test hashchange. Partly to prevent all around weirdness
if( !$.support.pushState ){
@@ -112,16 +125,32 @@ define([
return;
}
// account for initial page load popstate, and other popstates triggered
// by other parts of the application (ie, during the refactor)
// account for direct manipulation of the hash. That is, we will receive a popstate
// when the hash is changed by assignment, and it won't have a state associated. We
// then need to squash the hash. See below for handling of hash assignment that
// matches an existing history entry
if( !event.originalEvent.state ) {
hash = path.parseLocation().hash;
// squash a hash with replacestate
if( path.isPath(hash) ) {
state = $.navigate.squash( hash );
}
// record the new hash as an additional history entry
// to match the browser's treatment of hash assignment
history.add( hash, state );
// do not alter history, we've added a new history entry
// so we know where we are
return;
}
// If this is a popstate that comes from the back or forward buttons
// make sure to set the state of our history stack properly
// If all else fails this is a popstate that comes from the back or forward buttons
// make sure to set the state of our history stack properly, and record the directionality
history.direct({
url: event.originalEvent.state.hash,
url: (event.originalEvent.state || {}).hash || hash,
either: function( historyEntry, direction ) {
event.historyState = historyEntry;
event.historyState.direction = direction;
@@ -135,6 +164,8 @@ define([
// TODO add a check here that `hashchange.navigate` is bound already otherwise it's
// broken (exception?)
$( window ).bind( "hashchange.history", function( event ) {
var hash = path.parseLocation().hash;
// If pushstate is supported the state will be included in the popstate event
// data and appended to the navigate event. Late check here for late settings (eg tests)
if( $.support.pushState ) {
@@ -144,18 +175,30 @@ define([
// If the hashchange has been explicitly ignored or we have no history at
// this point skip the history managment and the addition of the history
// entry to the event for the `navigate` bindings
if( history.ignoreNextHashChange || history.stack.length == 0 ) {
if( history.ignoreNextHashChange ) {
history.ignoreNextHashChange = false;
return;
}
// If this is a hashchange caused by the back or forward button
// make sure to set the state of our history stack properly
history.direct({
url: path.parseLocation().hash,
url: hash,
// When the url is either forward or backward in history include the entry
// here
either: function( historyEntry, direction ) {
event.hashchangeState = historyEntry;
event.hashchangeState.direction = direction;
},
// When we don't find a hash in our history clearly we're aiming to go there
// record the entry as new history
neither: function() {
history.add( hash, {
hash: hash,
title: document.title,
url: location.href
});
}
});
});
@@ -202,18 +245,24 @@ define([
this.stack = this.stack.slice( 0, this.activeIndex + 1 );
},
find: function( url, stack ) {
var entry, i, length = this.stack.length, newActiveIndex;
find: function( url, stack, earlyReturn ) {
stack = stack || this.stack;
var entry, i, length = stack.length, index;
for ( i = 0; i < length; i++ ) {
entry = this.stack[i];
entry = stack[i];
if ( decodeURIComponent( url ) === decodeURIComponent( entry.url ) ) {
return i;
index = i;
if( earlyReturn ) {
return index;
}
}
}
return undefined;
return index;
},
direct: function( opts ) {
@@ -224,11 +273,18 @@ define([
// NOTE the preference for backward history movement is driven by the fact that
// most mobile browsers only have a dedicated back button, and users rarely use
// the forward button in desktop browser anyhow
newActiveIndex = this.find( opts.url, this.stack.slice(0, a - 1).reverse() );
// If nothing was found in backward history check forward
newActiveIndex = this.find( opts.url, this.stack.slice(0, a) );
// If nothing was found in backward history check forward. The `true`
// value passed as the third parameter causes the find method to break
// on the first match in the forward history slice. The starting index
// of the slice must then be added to the result to get the element index
// in the original history stack :( :(
//
// TODO this is hyper confusing and should be cleaned up
if( newActiveIndex === undefined ) {
newActiveIndex = this.find( opts.url, this.stack.slice(a + 1) );
newActiveIndex = this.find( opts.url, this.stack.slice(a + 1), true );
newActiveIndex = newActiveIndex === undefined ? newActiveIndex : newActiveIndex + a + 1;
}
// save new page index, null check to prevent falsey 0 result
@@ -239,6 +295,10 @@ define([
( opts.either || opts.isBack )( this.getActive(), 'back' );
} else if ( newActiveIndex > a ) {
( opts.either || opts.isForward )( this.getActive(), 'forward' );
} else if ( newActiveIndex === a ) {
opts.same ? opts.same( this.getActiveIndex ) : null;
} else if ( opts.neither ){
opts.neither( this.getActive() );
}
},
@@ -247,8 +307,9 @@ define([
ignoreNextHashChange: false
};
// NOTE Set the initial url history state, so that we have a history entry to match
history.add( path.parseLocation().pathname + path.parseLocation().search, {});
// Record the initial page with a replace state where necessary
history.add( location.href, {});
})( jQuery );
//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
View
@@ -226,7 +226,7 @@ define([
// TODO leave the dialog hashkey cleaning in nav core
//remove the preceding hash, any query params, and dialog notations
cleanHash: function( hash ) {
return path.stripHash( hash.replace( /\?.*$/, "" ).replace( dialogHashKey, "" ) );
return path.stripHash( hash ).replace( dialogHashKey, "" );
},
isHashValid: function( hash ) {
@@ -62,7 +62,7 @@ $.testHelper.setPushState();
// Test the inclusion of state for both pushstate and hashchange
// --nav--> #foo {state} --nav--> #bar --back--> #foo {state} --foward--> #bar {state}
asyncTest( "navigating backward should include the history state", function() {
asyncTest( "navigating backward and forward should include the history state", function() {
$.testHelper.eventTarget = $( window );
$.testHelper.eventSequence( "navigate", [
@@ -80,11 +80,13 @@ $.testHelper.setPushState();
function( timedOut, data ) {
equal( data.state.foo, "bar", "the data that was appended in the navigation is popped with the backward movement" );
equal( data.state.direction, "back", "the direction is recorded as backward" );
window.history.forward();
},
function( timedOut, data ) {
equal( data.state.baz, "bak", "the data that was appended in the navigation is popped with the foward movement" );
equal( data.state.direction, "forward", "the direction is recorded as forward" );
start();
}
]);
@@ -120,10 +122,53 @@ $.testHelper.setPushState();
function( timedOut, data ) {
equal( $.navigate.history.stack.length, 3, "the history stack hasn't been truncated" );
equal( $.navigate.history.activeIndex, 0 );
equal( $.navigate.history.activeIndex, 0, "the active history entry is the first" );
equal( data.state.direction, "back", "the direction should be back and not forward" );
start();
}
]);
});
asyncTest( "setting the hash with a url not in history should always create a new history entry", function() {
$.testHelper.eventTarget = $( window );
$.testHelper.eventSequence( "navigate", [
function() {
$.navigate( "#bar" );
},
function() {
location.hash = "#foo";
},
function() {
equal($.navigate.history.stack.length, 2, "there are two entries in the history stack" );
equal($.navigate.history.getActive().url, "#foo", "the url for the active history entry matches the hash" );
start();
}
]);
});
asyncTest( "setting the hash to the existing hash should not result in a new history entry", function() {
$.testHelper.eventTarget = $( window );
$.testHelper.eventSequence( "navigate", [
function() {
location.hash = "#foo";
},
function() {
equal($.navigate.history.stack.length, 1, "there is one entry in the history stack" );
equal($.navigate.history.getActive().url, "#foo", "the url for the active history entry matches the hash" );
location.hash = "#foo";
},
function( timedOut ) {
equal($.navigate.history.stack.length, 1, "there is one entry in the history stack" );
equal($.navigate.history.getActive().url, "#foo", "the url for the active history entry matches the hash" );
ok( timedOut, "there was no navigation event from setting the same hash" );
start();
}
]);
});
})( jQuery );

0 comments on commit 9c1c15f

Please sign in to comment.