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

Commit 4677f5a

Browse files
committed
include the navigate method to do history tracking and state inclusion for hashchange
1 parent ddcf7c7 commit 4677f5a

File tree

5 files changed

+291
-2
lines changed

5 files changed

+291
-2
lines changed

js/navigation/navigate.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
2+
//>>description: placeholder
3+
//>>label: AJAX Navigation System
4+
//>>group: Navigation
5+
define([
6+
"jquery",
7+
"./../jquery.mobile.core",
8+
"./../jquery.mobile.support",
9+
"./events/navigate",
10+
"./path"], function( $ ) {
11+
//>>excludeEnd("jqmBuildExclude");
12+
13+
(function( $, undefined ) {
14+
var path = $.mobile.path, history;
15+
16+
// TODO consider queueing navigation activity until previous activities have completed
17+
// so that end users don't have to think about it. Punting for now
18+
$.navigate = function( url, data ) {
19+
var href, state,
20+
// firefox auto decodes the url when using location.hash but not href
21+
hash = path.parseUrl(url).hash,
22+
isPath = path.isPath( hash ),
23+
resolutionUrl = isPath ? path.getLocation() : $.mobile.getDocumentUrl();
24+
25+
// #/foo/bar.html => /foo/bar.html
26+
// #foo => #foo
27+
hash = isPath ? hash.replace( "#", "" ) : hash;
28+
29+
// make the hash abolute with the current href
30+
href = path.makeUrlAbsolute( hash, resolutionUrl );
31+
32+
if ( isPath ) {
33+
href = path.resetUIKeys( href );
34+
}
35+
36+
state = $.extend( data, {
37+
url: url,
38+
hash: hash,
39+
title: document.title
40+
});
41+
42+
// NOTE we currently _leave_ the appended hash in the hash in the interest
43+
// of seeing what happens and if we can support that before the hash is
44+
// pushed down
45+
46+
// set the hash to be squashed by replace state or picked up by
47+
// the navigation special event
48+
window.location.hash = url;
49+
history.ignoreNextHashChange = true;
50+
51+
if( $.support.pushState ) {
52+
// replace the current url with the new href and store the state
53+
// Note that in some cases we might be replacing an url with the
54+
// same url. We do this anyways because we need to make sure that
55+
// all of our history entries have a state object associated with
56+
// them. This allows us to work around the case where $.mobile.back()
57+
// is called to transition from an external page to an embedded page.
58+
// In that particular case, a hashchange event is *NOT* generated by the browser.
59+
// Ensuring each history entry has a state object means that onPopState()
60+
// will always trigger our hashchange callback even when a hashchange event
61+
// is not fired.
62+
window.history.replaceState( state, document.title, href );
63+
}
64+
65+
// record the history entry so that the information can be included
66+
// in hashchange event driven navigate events in a similar fashion to
67+
// the state that's provided by popstate
68+
69+
history.add( url, state );
70+
};
71+
72+
// NOTE must bind before `navigate` special event hashchange binding otherwise the
73+
// navigation data won't be attached to the hashchange event in time for those
74+
// bindings to attach it to the `navigate` special event
75+
// TODO add a check here that `hashchange.navigate` is bound already otherwise it's
76+
// broken (exception?)
77+
$( window ).bind( "hashchange.history", function( event ) {
78+
// If pushstate is supported the state will be included in the popstate event
79+
// data and appended to the navigate event. Late check here for late settings (eg tests)
80+
if( $.support.pushState ) {
81+
return;
82+
}
83+
84+
// If the hashchange has been explicitly ignored or we have no history at
85+
// this point skip the history managment and the addition of the history
86+
// entry to the event for the `navigate` bindings
87+
if( history.ignoreNextHashChange || history.stack.length == 0 ) {
88+
history.ignoreNextHashChange = false;
89+
return;
90+
}
91+
92+
93+
history.direct({
94+
currentUrl: path.parseLocation().hash ,
95+
either: function( historyEntry ) {
96+
event.hashchangeState = historyEntry;
97+
}
98+
});
99+
});
100+
101+
// expose the history on the navigate method in anticipation of full integration with
102+
// existing navigation functionalty that is tightly coupled to the history information
103+
$.navigate.history = history = {
104+
// Array of pages that are visited during a single page load.
105+
// Each has a url and optional transition, title, and pageUrl (which represents the file path, in cases where URL is obscured, such as dialogs)
106+
stack: [],
107+
108+
//maintain an index number for the active page in the stack
109+
activeIndex: 0,
110+
111+
//get active
112+
getActive: function() {
113+
return this.stack[ this.activeIndex ];
114+
},
115+
116+
getPrev: function() {
117+
return this.stack[ this.activeIndex - 1 ];
118+
},
119+
120+
getNext: function() {
121+
return this.stack[ this.activeIndex + 1 ];
122+
},
123+
124+
// addNew is used whenever a new page is added
125+
add: function( url, data ){
126+
data = data || {};
127+
128+
//if there's forward history, wipe it
129+
if ( this.getNext() ) {
130+
this.clearForward();
131+
}
132+
133+
data.url = url;
134+
this.stack.push( data );
135+
this.activeIndex = this.stack.length - 1;
136+
},
137+
138+
//wipe urls ahead of active index
139+
clearForward: function() {
140+
this.stack = this.stack.slice( 0, this.activeIndex + 1 );
141+
},
142+
143+
direct: function( opts ) {
144+
var back, forward, newActiveIndex, prev = this.getActive(), a = this.activeIndex;
145+
146+
// check if url is in history and if it's ahead or behind current page
147+
$.each( this.stack, function( i, historyEntry ) {
148+
//if the url is in the stack, it's a forward or a back
149+
if ( decodeURIComponent( opts.currentUrl ) === decodeURIComponent( historyEntry.url ) ) {
150+
//define back and forward by whether url is older or newer than current page
151+
back = i < this.activeIndex;
152+
forward = !back;
153+
newActiveIndex = i;
154+
}
155+
});
156+
157+
// save new page index, null check to prevent falsey 0 result
158+
this.activeIndex = newActiveIndex !== undefined ? newActiveIndex : this.activeIndex;
159+
160+
if ( back ) {
161+
( opts.either || opts.isBack )( this.getActive() );
162+
} else if ( forward ) {
163+
( opts.either || opts.isForward )( this.getActive() );
164+
}
165+
},
166+
167+
//disable hashchange event listener internally to ignore one change
168+
//toggled internally when location.hash is updated to match the url of a successful page load
169+
ignoreNextHashChange: false
170+
};
171+
172+
// Set the initial url history state
173+
history.add( path.parseLocation().pathname + path.parseLocation().search, {});
174+
})( jQuery );
175+
176+
//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
177+
});
178+
//>>excludeEnd("jqmBuildExclude");

js/navigation/path.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//>>group: Navigation
55
define([
66
"jquery",
7-
"./jquery.mobile.core" ], function( $ ) {
7+
"./../jquery.mobile.core" ], function( $ ) {
88
//>>excludeEnd("jqmBuildExclude");
99

1010
var path, documentBase, $base, dialogHashKey = "&ui-state=dialog";
@@ -316,6 +316,16 @@ define([
316316

317317
path.documentBaseDiffers = (path.documentUrl.hrefNoHash !== path.documentBase.hrefNoHash);
318318

319+
//return the original document url
320+
$.mobile.getDocumentUrl = function( asParsedObject ) {
321+
return asParsedObject ? $.extend( {}, path.documentUrl ) : path.documentUrl.href;
322+
};
323+
324+
//return the original document base url
325+
$.mobile.getDocumentBase = function( asParsedObject ) {
326+
return asParsedObject ? $.extend( {}, path.documentBase ) : path.documentBase.href;
327+
};
328+
319329
//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
320330
});
321331
//>>excludeEnd("jqmBuildExclude");

tests/unit/navigation/event/navigate_core.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
$.testHelper.setPushState();
22

3-
(function( $) {
3+
(function( $ ) {
44
module( "navigate", {
55
setup: function() {
66
location.hash = "";
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<title>jQuery Mobile Navigate Events Test Suite</title>
6+
7+
<script src="../../../external/requirejs/require.js"></script>
8+
<script src="../../../js/jquery.tag.inserter.js"></script>
9+
<script src="../../../tests/jquery.testHelper.js"></script>
10+
<script src="../../../external/qunit.js"></script>
11+
<link rel="stylesheet" href="../../../../external/qunit.css"/>
12+
<script>
13+
$.testHelper.asyncLoad([
14+
[
15+
"navigation/events/navigate",
16+
"navigation/navigate"
17+
],
18+
[ "navigate_method.js" ]
19+
]);
20+
</script>
21+
<script src="../swarminject.js"></script>
22+
</head>
23+
<body>
24+
<div id="qunit"></div>
25+
</body>
26+
</html>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Check is the ?push-state=false is in the url and alter the tests accordingly
2+
$.testHelper.setPushState();
3+
4+
(function( $ ) {
5+
var url = $.mobile.path.parseLocation(),
6+
home = url.pathname + url.search;
7+
8+
module( "navigate", {
9+
setup: function() {
10+
stop();
11+
12+
$( window ).one( "navigate", function() {
13+
start();
14+
});
15+
16+
if( location.hash !== "#reset" ) {
17+
$.navigate( "#reset" );
18+
}
19+
20+
$.navigate.history.stack = [];
21+
}
22+
});
23+
24+
test( "navigation changes the url", function() {
25+
ok( location.hash.indexOf( "foo" ) == -1, "the hash is clean" );
26+
27+
$.navigate( "#foo" );
28+
29+
equal( location.hash, "#foo", "the hash has been altered" );
30+
});
31+
32+
if( $.support.pushState ) {
33+
test( "navigation should squish the hash", function() {
34+
var destination = home + "#foo";
35+
36+
ok( location.hash.indexOf( "foo" ) == -1, "the hash is clean" );
37+
ok( $.mobile.path.isPath(destination), "the destination is a path" );
38+
39+
$.navigate( destination );
40+
41+
equal( $.mobile.path.parseLocation().pathname, url.pathname, "the resulting url has the same pathname as the original test url" );
42+
equal( location.hash, "#foo", "the hash has been altered" );
43+
});
44+
} else {
45+
test( "navigation should append the hash with a path", function() {
46+
var destination = home + "#foo";
47+
48+
ok( location.hash.indexOf(home) == -1, "the hash is clean" );
49+
ok( $.mobile.path.isPath(destination), "the destination is a path" );
50+
51+
$.navigate( destination );
52+
53+
equal( $.mobile.path.parseLocation().hash, "#" + destination, "the resulting url has the same pathname as the original test url" );
54+
});
55+
}
56+
57+
if( !$.support.pushState ) {
58+
asyncTest( "navigating backward should include the history state", function() {
59+
$( window ).one( "navigate", function() {
60+
$.navigate( "#bar" );
61+
62+
$( window ).one( "navigate", function() {
63+
window.history.back();
64+
65+
$( window ).one( "navigate", function( event, data ) {
66+
equal( data.state.foo, "bar", "the data that was appended in the navigation is popped with the backward movement" );
67+
start();
68+
});
69+
});
70+
});
71+
72+
$.navigate( "#foo", { foo: "bar" });
73+
});
74+
}
75+
})( jQuery );

0 commit comments

Comments
 (0)