Permalink
Browse files

Merge branch 'replacestate-external'

  • Loading branch information...
2 parents 44eaf77 + df9e233 commit 49cf3af91a7f1fd2a05063bfbfffc15dbb74492b @johnbender johnbender committed Aug 25, 2011
View
@@ -45,6 +45,7 @@ JSFILES = js/jquery.ui.widget.js \
js/jquery.mobile.page.js \
js/jquery.mobile.core.js \
js/jquery.mobile.navigation.js \
+ js/jquery.mobile.navigation.pushstate.js \
js/jquery.mobile.transition.js \
js/jquery.mobile.degradeInputs.js \
js/jquery.mobile.dialog.js \
View
@@ -29,6 +29,7 @@
js/jquery.mobile.page.js,
js/jquery.mobile.core.js,
js/jquery.mobile.navigation.js,
+ js/jquery.mobile.navigation.pushstate.js,
js/jquery.mobile.transition.js,
js/jquery.mobile.degradeInputs.js,
js/jquery.mobile.dialog.js,
View
@@ -11,6 +11,7 @@
'jquery.mobile.page.js',
'jquery.mobile.core.js',
'jquery.mobile.navigation.js',
+ 'jquery.mobile.navigation.pushstate.js',
'jquery.mobile.transition.js',
'jquery.mobile.degradeInputs.js',
'jquery.mobile.dialog.js',
@@ -47,10 +47,12 @@
// Error response message - appears when an Ajax page request fails
pageLoadErrorMessage: "Error Loading Page",
-
+
//automatically initialize the DOM when it's ready
autoInitializePage: true,
+ pushStateEnabled: true,
+
// Support conditions that must be met in order to proceed
// default enhanced qualifications are media query support OR IE 7+
gradeA: function(){
@@ -164,3 +166,4 @@
return $.find( expr, null, null, [ node ] ).length > 0;
};
})( jQuery, this );
+
@@ -0,0 +1,103 @@
+/*
+* jQuery Mobile Framework : history.pushState support, layered on top of hashchange
+* Copyright (c) jQuery Project
+* Dual licensed under the MIT or GPL Version 2 licenses.
+* http://jquery.org/license
+*/
+( function( $, window ) {
+ // For now, let's Monkeypatch this onto the end of $.mobile._registerInternalEvents
+ // Scope self to pushStateHandler so we can reference it sanely within the
+ // methods handed off as event handlers
+ var pushStateHandler = {},
+ self = pushStateHandler,
+ $win = $( window ),
+ url = $.mobile.path.parseUrl( location.href );
+
+ $.extend( pushStateHandler, {
+ // TODO move to a path helper, this is rather common functionality
+ initialFilePath: (function() {
+ return url.pathname + url.search;
+ })(),
+
+ initialHref: url.hrefNoHash,
+
+ // Flag for tracking if a Hashchange naturally occurs after each popstate + replace
+ hashchangeFired: false,
+
+ state: function() {
+ return {
+ hash: location.hash || "#" + self.initialFilePath,
+ title: document.title,
+
+ // persist across refresh
+ initialHref: self.initialHref
+ };
+ },
+
+ // on hash change we want to clean up the url
+ // NOTE this takes place *after* the vanilla navigation hash change
+ // handling has taken place and set the state of the DOM
+ onHashChange: function( e ) {
+ var href, state;
+
+ self.hashchangeFired = true;
+
+ // only replaceState when the hash doesn't represent an embeded page
+ if( $.mobile.path.isPath(location.hash) ) {
+
+ // propulate the hash when its not available
+ state = self.state();
+
+ // make the hash abolute with the current href
+ href = $.mobile.path.makeUrlAbsolute( state.hash.replace("#", ""), location.href );
+
+ // replace the current url with the new href and store the state
+ history.replaceState( state, document.title, href );
+ }
+ },
+
+ // on popstate (ie back or forward) we need to replace the hash that was there previously
+ // cleaned up by the additional hash handling
+ onPopState: function( e ) {
+ var poppedState = e.originalEvent.state;
+
+ // if there's no state its not a popstate we care about, ie chrome's initial popstate
+ // or forward popstate
+ if( poppedState ) {
+
+ // replace the current url with the equivelant hash so that the hashchange binding in vanilla nav
+ // can do its thing one triggered below
+ history.replaceState( poppedState, poppedState.title, poppedState.initialHref + poppedState.hash );
+
+ // Urls that reference subpages will fire their own hashchange, so we don't want to trigger 2 in that case.
+ self.hashchangeFired = false;
+
+ setTimeout(function() {
+ if( !self.hashchangeFired ) {
+ $win.trigger( "hashchange" );
+ }
+
+ self.hashchangeFired = false;
+ }, 0);
+ }
+ },
+
+ init: function() {
+ $win.bind( "hashchange", self.onHashChange );
+
+ // Handle popstate events the occur through history changes
+ $win.bind( "popstate", self.onPopState );
+
+ // if there's no hash, we need to replacestate for returning to home
+ if ( location.hash === "" ) {
+ history.replaceState( self.state(), document.title, location.href );
+ }
+ }
+ });
+
+ $( function() {
+ if( $.mobile.pushStateEnabled && $.support.pushState ){
+ pushStateHandler.init();
+ }
+ });
+})( jQuery, this );
@@ -67,7 +67,7 @@ $.extend( $.support, {
orientation: "orientation" in window,
touch: "ontouchend" in document,
cssTransitions: "WebKitTransitionEvent" in window,
- pushState: !!history.pushState,
+ pushState: "pushState" in history && "replaceState" in history,
mediaquery: $.mobile.media( "only all" ),
cssPseudoElement: !!propExists( "content" ),
boxShadow: !!propExists( "boxShadow" ) && !bb,
@@ -16,6 +16,18 @@
}
},
+ // TODO prevent test suite loads when the browser doesn't support push state
+ // and push-state false is defined.
+ setPushStateFor: function( libs ) {
+ if( $.support.pushState && location.search.indexOf( "push-state" ) >= 0 ) {
+ $.support.pushState = false;
+ }
+
+ $.each(libs, function(i, l) {
+ $( "<script>", { src: l }).appendTo("head");
+ });
+ },
+
reloads: {},
reloadLib: function(libName){
@@ -26,8 +38,8 @@
};
}
- var lib = this.reloads[libName].lib.clone(),
- src = lib.attr('src');
+ var lib = this.reloads[libName].lib.clone(),
+ src = lib.attr('src');
//NOTE append "cache breaker" to force reload
lib.attr('src', src + "?" + this.reloads[libName].count++);
@@ -116,6 +128,17 @@
return returnVal;
};
+ },
+
+ assertUrlLocation: function( args ) {
+ var parts = $.mobile.path.parseUrl( location.href ),
+ pathnameOnward = location.href.replace( parts.domain, "" );
+
+ if( $.support.pushState ) {
+ same( pathnameOnward, args.hashOrPush || args.push, args.report );
+ } else {
+ same( parts.hash, "#" + (args.hashOrPush || args.hash), args.report );
+ }
}
};
})(jQuery);
@@ -4,11 +4,13 @@
// TODO split out into seperate test files
(function($){
+ var home = $.mobile.path.parseUrl( location.href ).pathname;
$.mobile.defaultTransition = "none";
- module('Basic Linked list', {
+
+ module( "Basic Linked list", {
setup: function(){
- $.testHelper.openPage("#basic-linked-test");
+ $.testHelper.openPage( "#basic-linked-test" );
}
});
@@ -213,7 +215,7 @@
asyncTest( "changes to the read only page when hash is changed", function() {
$.testHelper.pageSequence([
function(){
- $.testHelper.openPage("#read-only-list-test")
+ $.testHelper.openPage("#read-only-list-test");
},
function(){
@@ -588,6 +590,8 @@
ok( $("#enhancetest").trigger("create").find(".ui-listview").length, "enhancements applied" );
});
+ module( "Cached Linked List" );
+
var findNestedPages = function(selector){
return $( selector + " #topmost" ).listview( 'childPages' );
};
@@ -596,7 +600,7 @@
$.testHelper.pageSequence([
function(){
//reset for relative url refs
- $.testHelper.openPage( "#" + location.pathname );
+ $.testHelper.openPage( "#" + home );
},
function(){
@@ -605,7 +609,11 @@
function(){
ok( findNestedPages( "#uncached-nested-list" ).length > 0, "verify that there are nested pages" );
- $.testHelper.openPage( "#" + location.pathname + "cache-tests/clear.html" );
+ $.testHelper.openPage( "#" + home );
+ },
+
+ function() {
+ $.testHelper.openPage( "#cache-tests/clear.html" );
},
function(){
@@ -620,7 +628,7 @@
$.testHelper.pageSequence([
function(){
//reset for relative url refs
- $.testHelper.openPage( "#" + location.pathname );
+ $.testHelper.openPage( "#" + home );
},
function(){
@@ -629,7 +637,11 @@
function(){
ok( findNestedPages( "#cached-nested-list" ).length > 0, "verify that there are nested pages" );
- $.testHelper.openPage( "#" + location.pathname + "cache-tests/clear.html" );
+ $.testHelper.openPage( "#" + home );
+ },
+
+ function() {
+ $.testHelper.openPage( "#cache-tests/clear.html" );
},
function(){
@@ -643,7 +655,7 @@
$.testHelper.pageSequence([
function(){
//reset for relative url refs
- $.testHelper.openPage( "#" + location.pathname );
+ $.testHelper.openPage( "#" + home );
},
function(){
@@ -652,7 +664,11 @@
function(){
same( $("#cached-nested-list").length, 1 );
- $.testHelper.openPage("#" + location.pathname + "cache-tests/clear.html");
+ $.testHelper.openPage( "#" + home );
+ },
+
+ function() {
+ $.testHelper.openPage( "#cache-tests/clear.html" );
},
function(){
@@ -667,6 +683,11 @@
expect( listPage.find("li").length );
$.testHelper.pageSequence( [
+ function(){
+ //reset for relative url refs
+ $.testHelper.openPage( "#" + home );
+ },
+
function() {
$.testHelper.openPage( "#search-filter-test" );
},
@@ -26,8 +26,11 @@
<link rel="stylesheet" href="../../../../../themes/default/"/>
<link rel="stylesheet" href="../../../../../external/qunit.css"/>
<script src="../../../../../external/qunit.js"></script>
-
- <script src="../../navigation_base.js"></script>
+ <script type="text/javascript">
+ $.testHelper.setPushStateFor([
+ "../../navigation_base.js"
+ ]);
+ </script>
</head>
<body>
Oops, something went wrong. Retry.

3 comments on commit 49cf3af

@chrisjacob

I assume this has had JQM's usually extensive testing run over it (you guys rock).... but just to check; have the cross-browser compatibility issues with the HTML5 History API (raised here #16 (comment)) been addressed? If there are "known bugs" with the current solution, could those be added to the documentation? Are there parts of History.js (https://github.com/balupton/history.js) that could be ported over to maintain a consistent/reliable pushState experience cross browser (mobile and desktop)?

P.S I wish I could be more hand's on - but testing/coding this stuff is beyond me. So thx for all your hard work!

@johnbender

@chrisjacob

We've spotted a few anomalies (at least 2 browser bugs filed) but all of our testing points to success in the way we implemented our "pushState" suport through replaceState. We're hoping to address bugs here where necessary.

As for History.js, its really hard to justify adding an enormous external dependency like that to the project even though it may suck to reproduce much of the functionality ourselves. Even so, I'll talk with the team about evaluating it since we've talked about a nav refactor at some point anyhow.

@chrisjacob

@johnbender

Thank you for the quick feedback. I hope you can at least cherry pick some of @balupton 's hard work ^_^

Please sign in to comment.