Permalink
Browse files

include the navigate method to do history tracking and state inclusio…

…n for hashchange
  • Loading branch information...
1 parent ddcf7c7 commit 4677f5a7bf3853b423ad682fbd78ad3889bd5d74 @johnbender johnbender committed Oct 11, 2012
@@ -0,0 +1,178 @@
+//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
+//>>description: placeholder
+//>>label: AJAX Navigation System
+//>>group: Navigation
+define([
+ "jquery",
+ "./../jquery.mobile.core",
+ "./../jquery.mobile.support",
+ "./events/navigate",
+ "./path"], function( $ ) {
+//>>excludeEnd("jqmBuildExclude");
+
+(function( $, undefined ) {
+ var path = $.mobile.path, history;
+
+ // 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,
+ // firefox auto decodes the url when using location.hash but not href
+ 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
+ });
+
+ // 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
+ // pushed down
+
+ // set the hash to be squashed by replace state or picked up by
+ // the navigation special event
+ window.location.hash = url;
+ history.ignoreNextHashChange = true;
+
+ if( $.support.pushState ) {
+ // 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 );
+ }
+
+ // record the history entry so that the information can be included
+ // in hashchange event driven navigate events in a similar fashion to
+ // the state that's provided by popstate
+
+ history.add( url, state );
+ };
+
+ // NOTE must bind before `navigate` special event hashchange binding otherwise the
+ // navigation data won't be attached to the hashchange event in time for those
+ // bindings to attach it to the `navigate` special event
+ // TODO add a check here that `hashchange.navigate` is bound already otherwise it's
+ // broken (exception?)
+ $( window ).bind( "hashchange.history", function( event ) {
+ // 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 ) {
+ return;
+ }
+
+ // 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 ) {
+ history.ignoreNextHashChange = false;
+ return;
+ }
+
+
+ history.direct({
+ currentUrl: path.parseLocation().hash ,
+ either: function( historyEntry ) {
+ event.hashchangeState = historyEntry;
+ }
+ });
+ });
+
+ // expose the history on the navigate method in anticipation of full integration with
+ // existing navigation functionalty that is tightly coupled to the history information
+ $.navigate.history = history = {
+ // Array of pages that are visited during a single page load.
+ // Each has a url and optional transition, title, and pageUrl (which represents the file path, in cases where URL is obscured, such as dialogs)
+ stack: [],
+
+ //maintain an index number for the active page in the stack
+ activeIndex: 0,
+
+ //get active
+ getActive: function() {
+ return this.stack[ this.activeIndex ];
+ },
+
+ getPrev: function() {
+ return this.stack[ this.activeIndex - 1 ];
+ },
+
+ getNext: function() {
+ return this.stack[ this.activeIndex + 1 ];
+ },
+
+ // addNew is used whenever a new page is added
+ add: function( url, data ){
+ data = data || {};
+
+ //if there's forward history, wipe it
+ if ( this.getNext() ) {
+ this.clearForward();
+ }
+
+ data.url = url;
+ this.stack.push( data );
+ this.activeIndex = this.stack.length - 1;
+ },
+
+ //wipe urls ahead of active index
+ clearForward: function() {
+ this.stack = this.stack.slice( 0, this.activeIndex + 1 );
+ },
+
+ direct: function( opts ) {
+ var back, forward, newActiveIndex, prev = this.getActive(), a = this.activeIndex;
+
+ // check if url is in history and if it's ahead or behind current page
+ $.each( this.stack, function( i, historyEntry ) {
+ //if the url is in the stack, it's a forward or a back
+ if ( decodeURIComponent( opts.currentUrl ) === decodeURIComponent( historyEntry.url ) ) {
+ //define back and forward by whether url is older or newer than current page
+ back = i < this.activeIndex;
+ forward = !back;
+ newActiveIndex = i;
+ }
+ });
+
+ // save new page index, null check to prevent falsey 0 result
+ this.activeIndex = newActiveIndex !== undefined ? newActiveIndex : this.activeIndex;
+
+ if ( back ) {
+ ( opts.either || opts.isBack )( this.getActive() );
+ } else if ( forward ) {
+ ( opts.either || opts.isForward )( this.getActive() );
+ }
+ },
+
+ //disable hashchange event listener internally to ignore one change
+ //toggled internally when location.hash is updated to match the url of a successful page load
+ ignoreNextHashChange: false
+ };
+
+ // Set the initial url history state
+ history.add( path.parseLocation().pathname + path.parseLocation().search, {});
+})( jQuery );
+
+//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
+});
+//>>excludeEnd("jqmBuildExclude");
View
@@ -4,7 +4,7 @@
//>>group: Navigation
define([
"jquery",
- "./jquery.mobile.core" ], function( $ ) {
+ "./../jquery.mobile.core" ], function( $ ) {
//>>excludeEnd("jqmBuildExclude");
var path, documentBase, $base, dialogHashKey = "&ui-state=dialog";
@@ -316,6 +316,16 @@ define([
path.documentBaseDiffers = (path.documentUrl.hrefNoHash !== path.documentBase.hrefNoHash);
+ //return the original document url
+ $.mobile.getDocumentUrl = function( asParsedObject ) {
+ return asParsedObject ? $.extend( {}, path.documentUrl ) : path.documentUrl.href;
+ };
+
+ //return the original document base url
+ $.mobile.getDocumentBase = function( asParsedObject ) {
+ return asParsedObject ? $.extend( {}, path.documentBase ) : path.documentBase.href;
+ };
+
//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
});
//>>excludeEnd("jqmBuildExclude");
@@ -1,6 +1,6 @@
$.testHelper.setPushState();
-(function( $) {
+(function( $ ) {
module( "navigate", {
setup: function() {
location.hash = "";
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <title>jQuery Mobile Navigate Events Test Suite</title>
+
+ <script src="../../../external/requirejs/require.js"></script>
+ <script src="../../../js/jquery.tag.inserter.js"></script>
+ <script src="../../../tests/jquery.testHelper.js"></script>
+ <script src="../../../external/qunit.js"></script>
+ <link rel="stylesheet" href="../../../../external/qunit.css"/>
+ <script>
+ $.testHelper.asyncLoad([
+ [
+ "navigation/events/navigate",
+ "navigation/navigate"
+ ],
+ [ "navigate_method.js" ]
+ ]);
+ </script>
+ <script src="../swarminject.js"></script>
+</head>
+<body>
+ <div id="qunit"></div>
+</body>
+</html>
@@ -0,0 +1,75 @@
+// Check is the ?push-state=false is in the url and alter the tests accordingly
+$.testHelper.setPushState();
+
+(function( $ ) {
+ var url = $.mobile.path.parseLocation(),
+ home = url.pathname + url.search;
+
+ module( "navigate", {
+ setup: function() {
+ stop();
+
+ $( window ).one( "navigate", function() {
+ start();
+ });
+
+ if( location.hash !== "#reset" ) {
+ $.navigate( "#reset" );
+ }
+
+ $.navigate.history.stack = [];
+ }
+ });
+
+ test( "navigation changes the url", function() {
+ ok( location.hash.indexOf( "foo" ) == -1, "the hash is clean" );
+
+ $.navigate( "#foo" );
+
+ equal( location.hash, "#foo", "the hash has been altered" );
+ });
+
+ if( $.support.pushState ) {
+ test( "navigation should squish the hash", function() {
+ var destination = home + "#foo";
+
+ ok( location.hash.indexOf( "foo" ) == -1, "the hash is clean" );
+ ok( $.mobile.path.isPath(destination), "the destination is a path" );
+
+ $.navigate( destination );
+
+ equal( $.mobile.path.parseLocation().pathname, url.pathname, "the resulting url has the same pathname as the original test url" );
+ equal( location.hash, "#foo", "the hash has been altered" );
+ });
+ } else {
+ test( "navigation should append the hash with a path", function() {
+ var destination = home + "#foo";
+
+ ok( location.hash.indexOf(home) == -1, "the hash is clean" );
+ ok( $.mobile.path.isPath(destination), "the destination is a path" );
+
+ $.navigate( destination );
+
+ equal( $.mobile.path.parseLocation().hash, "#" + destination, "the resulting url has the same pathname as the original test url" );
+ });
+ }
+
+ if( !$.support.pushState ) {
+ asyncTest( "navigating backward should include the history state", function() {
+ $( window ).one( "navigate", function() {
+ $.navigate( "#bar" );
+
+ $( window ).one( "navigate", function() {
+ window.history.back();
+
+ $( window ).one( "navigate", function( event, data ) {
+ equal( data.state.foo, "bar", "the data that was appended in the navigation is popped with the backward movement" );
+ start();
+ });
+ });
+ });
+
+ $.navigate( "#foo", { foo: "bar" });
+ });
+ }
+})( jQuery );

0 comments on commit 4677f5a

Please sign in to comment.