Skip to content
This repository was archived by the owner on Oct 8, 2021. It is now read-only.

Commit 9c1c15f

Browse files
committed
basic hash assignment handling
1 parent 3974794 commit 9c1c15f

File tree

3 files changed

+163
-57
lines changed

3 files changed

+163
-57
lines changed

js/navigation/navigate.js

Lines changed: 115 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,8 @@ define([
1515

1616
// TODO consider queueing navigation activity until previous activities have completed
1717
// so that end users don't have to think about it. Punting for now
18-
$.navigate = function( url, data ) {
19-
var href, state,
20-
hash = path.parseUrl( url ).hash,
21-
isPath = path.isPath( hash ),
22-
resolutionUrl = isPath ? path.getLocation() : $.mobile.getDocumentUrl();
23-
24-
// #/foo/bar.html => /foo/bar.html
25-
// #foo => #foo
26-
hash = isPath ? hash.replace( "#", "" ) : hash;
27-
28-
// make the hash abolute with the current href
29-
href = path.makeUrlAbsolute( hash, resolutionUrl );
30-
31-
if ( isPath ) {
32-
href = path.resetUIKeys( href );
33-
}
34-
35-
state = $.extend( data, {
36-
url: url,
37-
hash: hash,
38-
title: document.title
39-
});
18+
$.navigate = function( url, data, noEvents ) {
19+
var state;
4020

4121
// NOTE we currently _leave_ the appended hash in the hash in the interest
4222
// of seeing what happens and if we can support that before the hash is
@@ -50,7 +30,12 @@ define([
5030
// We then trigger a new popstate event on the window with a null state
5131
// so that the navigate events can conclude their work properly
5232
history.ignoreNextHashChange = true;
53-
window.location.hash = url;
33+
window.location.hash = path.cleanHash( url );
34+
35+
state = $.extend( data, {
36+
url: url,
37+
title: document.title
38+
});
5439

5540
if( $.support.pushState ) {
5641
popstateEvent = new $.Event( "popstate" );
@@ -59,22 +44,14 @@ define([
5944
state: null
6045
};
6146

62-
// replace the current url with the new href and store the state
63-
// Note that in some cases we might be replacing an url with the
64-
// same url. We do this anyways because we need to make sure that
65-
// all of our history entries have a state object associated with
66-
// them. This allows us to work around the case where $.mobile.back()
67-
// is called to transition from an external page to an embedded page.
68-
// In that particular case, a hashchange event is *NOT* generated by the browser.
69-
// Ensuring each history entry has a state object means that onPopState()
70-
// will always trigger our hashchange callback even when a hashchange event
71-
// is not fired.
72-
window.history.replaceState( state, document.title, href );
47+
$.navigate.squash( url, data );
7348

7449
// Trigger a new faux popstate event to replace the one that we
7550
// caught that was triggered by the hash setting above.
76-
history.ignoreNextPopState = true;
77-
$( window ).trigger( popstateEvent );
51+
if( !noEvents ) {
52+
history.ignoreNextPopState = true;
53+
$( window ).trigger( popstateEvent );
54+
}
7855
}
7956

8057
// record the history entry so that the information can be included
@@ -83,6 +60,40 @@ define([
8360
history.add( url, state );
8461
};
8562

63+
$.navigate.squash = function( url, data ) {
64+
var state, href,
65+
hash = url,
66+
isPath = path.isPath( hash ),
67+
resolutionUrl = isPath ? path.getLocation() : $.mobile.getDocumentUrl();
68+
69+
// make the hash abolute with the current href
70+
href = path.makeUrlAbsolute( hash, resolutionUrl );
71+
72+
if ( isPath ) {
73+
href = path.resetUIKeys( href );
74+
}
75+
76+
state = $.extend( data, {
77+
url: url,
78+
hash: hash,
79+
title: document.title
80+
});
81+
82+
// replace the current url with the new href and store the state
83+
// Note that in some cases we might be replacing an url with the
84+
// same url. We do this anyways because we need to make sure that
85+
// all of our history entries have a state object associated with
86+
// them. This allows us to work around the case where $.mobile.back()
87+
// is called to transition from an external page to an embedded page.
88+
// In that particular case, a hashchange event is *NOT* generated by the browser.
89+
// Ensuring each history entry has a state object means that onPopState()
90+
// will always trigger our hashchange callback even when a hashchange event
91+
// is not fired.
92+
window.history.replaceState( state, document.title, href );
93+
94+
return state;
95+
};
96+
8697
// This binding is intended to catch the popstate events that are fired
8798
// when execution of the `$.navigate` method stops at window.location.hash = url;
8899
// and completely prevent them from propagating. The popstate event will then be
@@ -91,6 +102,8 @@ define([
91102
// TODO grab the original event here and use it for the synthetic event in the
92103
// second half of the navigate execution that will follow this binding
93104
$( window ).bind( "popstate.history", function( event ) {
105+
var hash, state;
106+
94107
// Partly to support our test suite which manually alters the support
95108
// value to test hashchange. Partly to prevent all around weirdness
96109
if( !$.support.pushState ){
@@ -112,16 +125,32 @@ define([
112125
return;
113126
}
114127

115-
// account for initial page load popstate, and other popstates triggered
116-
// by other parts of the application (ie, during the refactor)
128+
// account for direct manipulation of the hash. That is, we will receive a popstate
129+
// when the hash is changed by assignment, and it won't have a state associated. We
130+
// then need to squash the hash. See below for handling of hash assignment that
131+
// matches an existing history entry
117132
if( !event.originalEvent.state ) {
133+
hash = path.parseLocation().hash;
134+
135+
// squash a hash with replacestate
136+
if( path.isPath(hash) ) {
137+
state = $.navigate.squash( hash );
138+
}
139+
140+
// record the new hash as an additional history entry
141+
// to match the browser's treatment of hash assignment
142+
history.add( hash, state );
143+
144+
// do not alter history, we've added a new history entry
145+
// so we know where we are
118146
return;
119147
}
120148

121-
// If this is a popstate that comes from the back or forward buttons
122-
// make sure to set the state of our history stack properly
149+
// If all else fails this is a popstate that comes from the back or forward buttons
150+
// make sure to set the state of our history stack properly, and record the directionality
123151
history.direct({
124-
url: event.originalEvent.state.hash,
152+
url: (event.originalEvent.state || {}).hash || hash,
153+
125154
either: function( historyEntry, direction ) {
126155
event.historyState = historyEntry;
127156
event.historyState.direction = direction;
@@ -135,6 +164,8 @@ define([
135164
// TODO add a check here that `hashchange.navigate` is bound already otherwise it's
136165
// broken (exception?)
137166
$( window ).bind( "hashchange.history", function( event ) {
167+
var hash = path.parseLocation().hash;
168+
138169
// If pushstate is supported the state will be included in the popstate event
139170
// data and appended to the navigate event. Late check here for late settings (eg tests)
140171
if( $.support.pushState ) {
@@ -144,18 +175,30 @@ define([
144175
// If the hashchange has been explicitly ignored or we have no history at
145176
// this point skip the history managment and the addition of the history
146177
// entry to the event for the `navigate` bindings
147-
if( history.ignoreNextHashChange || history.stack.length == 0 ) {
178+
if( history.ignoreNextHashChange ) {
148179
history.ignoreNextHashChange = false;
149180
return;
150181
}
151182

152183
// If this is a hashchange caused by the back or forward button
153184
// make sure to set the state of our history stack properly
154185
history.direct({
155-
url: path.parseLocation().hash,
186+
url: hash,
187+
// When the url is either forward or backward in history include the entry
188+
// here
156189
either: function( historyEntry, direction ) {
157190
event.hashchangeState = historyEntry;
158191
event.hashchangeState.direction = direction;
192+
},
193+
194+
// When we don't find a hash in our history clearly we're aiming to go there
195+
// record the entry as new history
196+
neither: function() {
197+
history.add( hash, {
198+
hash: hash,
199+
title: document.title,
200+
url: location.href
201+
});
159202
}
160203
});
161204
});
@@ -202,18 +245,24 @@ define([
202245
this.stack = this.stack.slice( 0, this.activeIndex + 1 );
203246
},
204247

205-
find: function( url, stack ) {
206-
var entry, i, length = this.stack.length, newActiveIndex;
248+
find: function( url, stack, earlyReturn ) {
249+
stack = stack || this.stack;
250+
251+
var entry, i, length = stack.length, index;
207252

208253
for ( i = 0; i < length; i++ ) {
209-
entry = this.stack[i];
254+
entry = stack[i];
210255

211256
if ( decodeURIComponent( url ) === decodeURIComponent( entry.url ) ) {
212-
return i;
257+
index = i;
258+
259+
if( earlyReturn ) {
260+
return index;
261+
}
213262
}
214263
}
215264

216-
return undefined;
265+
return index;
217266
},
218267

219268
direct: function( opts ) {
@@ -224,11 +273,18 @@ define([
224273
// NOTE the preference for backward history movement is driven by the fact that
225274
// most mobile browsers only have a dedicated back button, and users rarely use
226275
// the forward button in desktop browser anyhow
227-
newActiveIndex = this.find( opts.url, this.stack.slice(0, a - 1).reverse() );
228-
229-
// If nothing was found in backward history check forward
276+
newActiveIndex = this.find( opts.url, this.stack.slice(0, a) );
277+
278+
// If nothing was found in backward history check forward. The `true`
279+
// value passed as the third parameter causes the find method to break
280+
// on the first match in the forward history slice. The starting index
281+
// of the slice must then be added to the result to get the element index
282+
// in the original history stack :( :(
283+
//
284+
// TODO this is hyper confusing and should be cleaned up
230285
if( newActiveIndex === undefined ) {
231-
newActiveIndex = this.find( opts.url, this.stack.slice(a + 1) );
286+
newActiveIndex = this.find( opts.url, this.stack.slice(a + 1), true );
287+
newActiveIndex = newActiveIndex === undefined ? newActiveIndex : newActiveIndex + a + 1;
232288
}
233289

234290
// save new page index, null check to prevent falsey 0 result
@@ -239,6 +295,10 @@ define([
239295
( opts.either || opts.isBack )( this.getActive(), 'back' );
240296
} else if ( newActiveIndex > a ) {
241297
( opts.either || opts.isForward )( this.getActive(), 'forward' );
298+
} else if ( newActiveIndex === a ) {
299+
opts.same ? opts.same( this.getActiveIndex ) : null;
300+
} else if ( opts.neither ){
301+
opts.neither( this.getActive() );
242302
}
243303
},
244304

@@ -247,8 +307,9 @@ define([
247307
ignoreNextHashChange: false
248308
};
249309

250-
// NOTE Set the initial url history state, so that we have a history entry to match
251-
history.add( path.parseLocation().pathname + path.parseLocation().search, {});
310+
311+
// Record the initial page with a replace state where necessary
312+
history.add( location.href, {});
252313
})( jQuery );
253314

254315
//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);

js/navigation/path.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ define([
226226
// TODO leave the dialog hashkey cleaning in nav core
227227
//remove the preceding hash, any query params, and dialog notations
228228
cleanHash: function( hash ) {
229-
return path.stripHash( hash.replace( /\?.*$/, "" ).replace( dialogHashKey, "" ) );
229+
return path.stripHash( hash ).replace( dialogHashKey, "" );
230230
},
231231

232232
isHashValid: function( hash ) {

tests/unit/navigation/navigate_method.js

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ $.testHelper.setPushState();
6262

6363
// Test the inclusion of state for both pushstate and hashchange
6464
// --nav--> #foo {state} --nav--> #bar --back--> #foo {state} --foward--> #bar {state}
65-
asyncTest( "navigating backward should include the history state", function() {
65+
asyncTest( "navigating backward and forward should include the history state", function() {
6666
$.testHelper.eventTarget = $( window );
6767

6868
$.testHelper.eventSequence( "navigate", [
@@ -80,11 +80,13 @@ $.testHelper.setPushState();
8080

8181
function( timedOut, data ) {
8282
equal( data.state.foo, "bar", "the data that was appended in the navigation is popped with the backward movement" );
83+
equal( data.state.direction, "back", "the direction is recorded as backward" );
8384
window.history.forward();
8485
},
8586

8687
function( timedOut, data ) {
8788
equal( data.state.baz, "bak", "the data that was appended in the navigation is popped with the foward movement" );
89+
equal( data.state.direction, "forward", "the direction is recorded as forward" );
8890
start();
8991
}
9092
]);
@@ -120,10 +122,53 @@ $.testHelper.setPushState();
120122

121123
function( timedOut, data ) {
122124
equal( $.navigate.history.stack.length, 3, "the history stack hasn't been truncated" );
123-
equal( $.navigate.history.activeIndex, 0 );
125+
equal( $.navigate.history.activeIndex, 0, "the active history entry is the first" );
124126
equal( data.state.direction, "back", "the direction should be back and not forward" );
125127
start();
126128
}
127129
]);
128130
});
131+
132+
asyncTest( "setting the hash with a url not in history should always create a new history entry", function() {
133+
$.testHelper.eventTarget = $( window );
134+
135+
$.testHelper.eventSequence( "navigate", [
136+
function() {
137+
$.navigate( "#bar" );
138+
},
139+
140+
function() {
141+
location.hash = "#foo";
142+
},
143+
144+
function() {
145+
equal($.navigate.history.stack.length, 2, "there are two entries in the history stack" );
146+
equal($.navigate.history.getActive().url, "#foo", "the url for the active history entry matches the hash" );
147+
start();
148+
}
149+
]);
150+
});
151+
152+
asyncTest( "setting the hash to the existing hash should not result in a new history entry", function() {
153+
$.testHelper.eventTarget = $( window );
154+
155+
$.testHelper.eventSequence( "navigate", [
156+
function() {
157+
location.hash = "#foo";
158+
},
159+
160+
function() {
161+
equal($.navigate.history.stack.length, 1, "there is one entry in the history stack" );
162+
equal($.navigate.history.getActive().url, "#foo", "the url for the active history entry matches the hash" );
163+
location.hash = "#foo";
164+
},
165+
166+
function( timedOut ) {
167+
equal($.navigate.history.stack.length, 1, "there is one entry in the history stack" );
168+
equal($.navigate.history.getActive().url, "#foo", "the url for the active history entry matches the hash" );
169+
ok( timedOut, "there was no navigation event from setting the same hash" );
170+
start();
171+
}
172+
]);
173+
});
129174
})( jQuery );

0 commit comments

Comments
 (0)