diff --git a/js/jquery.mobile.init.js b/js/jquery.mobile.init.js index fe6f91b7beb..275d017b0ac 100644 --- a/js/jquery.mobile.init.js +++ b/js/jquery.mobile.init.js @@ -82,6 +82,7 @@ //define first page in dom case one backs out to the directory root (not always the first page visited, but defined as fallback) $.mobile.firstPage = $pages.first(); + $.mobile.firstPageUrl = "#" + $.mobile.firstPage.attr("data-" + $.mobile.ns + "url"); //define page container $.mobile.pageContainer = $pages.first().parent().addClass( "ui-mobile-viewport" ); @@ -91,7 +92,7 @@ // if hashchange listening is disabled or there's no hash deeplink, change to the first page in the DOM if( !$.mobile.hashListeningEnabled || !$.mobile.path.stripHash( location.hash ) ){ - $.mobile.changePage( $.mobile.firstPage, false, true, false, true ); + $.mobile.changePage( $.mobile.firstPage, false, true, true, true ); } // otherwise, trigger a hashchange to load a deeplink else { diff --git a/js/jquery.mobile.navigation.js b/js/jquery.mobile.navigation.js index 9e5ee022555..db13dc786a8 100644 --- a/js/jquery.mobile.navigation.js +++ b/js/jquery.mobile.navigation.js @@ -16,10 +16,20 @@ //get path from current hash, or from a file path get: function( newPath ){ + var segments, i; if( newPath === undefined ){ newPath = location.hash; } - return path.stripHash( newPath ).replace(/[^\/]*\.[^\/*]+$/, ''); + // Making a regexp that returns the path + // (and does not have obscure failure cases) + // is not so simple - try a simpler approach instead + i = newPath.indexOf("?"); + if (i > -1) { // Remove any parameters (which might contain slashes!) + newPath = newPath.slice(0, i); + } + segments = path.stripHash( newPath ).split("/"); + segments.pop(); + return segments.join("/") + (segments.length > 0 ? "/" : ""); }, //return the substring of a filepath before the sub-page key, for making a server request @@ -30,7 +40,11 @@ //set location hash to path set: function( path ){ - location.hash = path; + if (path === "#" && location.href.indexOf("#") === -1) { + location.href += "#"; // Only way to set empty hash for non-hash url + } else { + location.hash = path; + } }, //location pathname from intial directory request @@ -68,13 +82,139 @@ return /^\?/.test(url); }, - //return a url path with the window's location protocol/hostname/pathname removed + // There are several supported url formats, which are cleaned (converted to canonical form) in this function. + // The returned value will contain protocol and hostname part only if the url is an external url which should + // not be loaded with XHR. + // Note that the entry page (the page which loads the jQuery Mobile library) should preferably always be an + // index.* file, so that you can refer to it with path that ends in a slash. + // E.g. example.com/a/#b/c.html rather than example.com/a/app.html#b/c.html + // This makes the hashed urls look more clean and intuitive. + // Common use case examples are given below. + // Note: For brevity protocol has been omitted from urls, but it would be present whenever hostname is present. + // Syntax: current_location + url => resulting_location (returned canonical form) + + // 01) Relative: example.com/a/#b/c.html + ../d/e.html => example.com/a/#d/e.html (#d/e.html) + // 02) Hash-relative: example.com/a/#b/c.html + #d/e.html => example.com/a/#d/e.html (#d/e.html) + // 03) Hash-root: example.com/a/#b/c.html + #/d/e.html => example.com/a/#d/e.html (#d/e.html) + // 04) Absolute path: example.com/a/#b/c.html + /d/e.html => example.com/a/#../d/e.html (#../d/e.html) + // 05) Host-absolute (same host): example.com/a/#b/c.html + example.com/d/e.html => example.com/a/#../d/e.html (#../d/e.html) + // 06) Host-absolute (diff host): example.com/a/#b/c.html + other.com/d/e.html => other.com/d/e.html (other.com/d/e.html) + // 07) Hashed relative (same path): example.com/a/#b/c.html + ../#d/e.html => example.com/a/#d/e.html (#d/e.html) + // 08) Hashed relative (diff path): example.com/a/#b/c.html + ../d/#e.html => example.com/d/#e.html (example.com/d/#e.html) + // 09) Hashed absolute (same path): example.com/a/#b/c.html + /a/#d/e.html => example.com/a/#d/e.html (#d/e.html) + // 10) Hashed absolute (diff path): example.com/a/#b/c.html + /d/#e.html => example.com/d/#e.html (example.com/d/#e.html) + // 11) Hashed host-abs (same path): example.com/a/#b/c.html + example.com/a/#d/e.html => example.com/a/#d/e.html (#d/e.html) + // 12) Hashed host-abs (diff path): example.com/a/#b/c.html + example.com/d/#e.html => example.com/d/#e.html (example.com/d/#e.html) + // 13) Hashed host-abs (diff host): example.com/a/#b/c.html + other.com/d/#e.html => other.com/d/#e.html (other.com/d/#e.html) + + // The canonical form is hash-relative (e.g. #d/e.html) for urls on the same host + // and original form (no change) for urls on a different host. + // A hashed url (cases 07-13) must always use the canonical form in the hash part. + // E.g. example.com/a/#/b/c.html is illegal, you must use example.com/a/#b/c.html instead. + // In non-external urls index.* is never explicitly included at the end (ends in slash instead). + // Hash-root form is same as hash-relative, just with a slash prefix. The reason it is supported + // is to allow you to write "#/" to refer to $.mobile.firstPage (and to write other hash urls in + // a way consistent with that). + // You can refer to $.mobile.firstPage in several ways: + // 1) Hash-root: example.com/a/#b/c.html + #/ => example.com/a/# (#) + // 2) Relative: example.com/a/#b/c.html + .. => example.com/a/# (#) + // 3) Absolute path: example.com/a/#b/c.html + /a/ => example.com/a/# (#) + // 4) Hashed relative: example.com/a/#b/c.html + ../# => example.com/a/# (#) + // 5) Hashed absolute: example.com/a/#b/c.html + /a/# => example.com/a/# (#) + // The recommended way is hash-root (#/), because it does not depend on current url. + // You can of course also refer to $.mobile.firstPage using its data-url attribute + // (=== ID if not defined), e.g. "#MyFirstPageId". + // This will be handled equivalently to "#/". + // Note that the location (url bar) will always have a hash character, event when on first page. + clean: function( url ){ + if(path.isQuery(url)){ + return "#" + path.cleanHash(location.hash) + url; + } + // Replace the protocol, host, and pathname only once at the beginning of the url to avoid // problems when it's included as a part of a param - // Also, since all urls are absolute in IE, we need to remove the pathname as well. - var leadingUrlRootRegex = new RegExp("^" + location.protocol + "//" + location.host + location.pathname); - return url.replace(leadingUrlRootRegex, ""); + var leadingUrlRootRegex; + var hashIndex = url.indexOf("#"); + if (hashIndex > 0) { // Hash, but not at beginning + // Add protocol and host to absolute and relative hashed urls so that they will be considered external + // if the pathname (before hash) does not match current pathname. + if (url.charAt(0) === "/") { // hashed absolute + url = location.protocol + "//" + location.host + url; + } else if (url.slice(0, location.protocol.length) !== location.protocol) { // hashed relative + url = location.protocol + "//" + location.host + + path.canonize(location.pathname + path.get() + url.slice(0, hashIndex)) + + url.slice(hashIndex); + } + // Absolute (with hostname) urls that contain a hash are converted to canonical form + // only if the pathname (before hash) matches current location.pathname. + // Otherwise it is considered an external URL and returned as-is + // (because regexp will not match and protocol is thus not removed). + // Such an URL should always be loaded as external, and isExternal will return true for it + // since protocol remains in the URL after path.clean. + leadingUrlRootRegex = new RegExp("^" + location.protocol + "//" + location.host + location.pathname + "(#.*)$"); + } else { + leadingUrlRootRegex = new RegExp("^" + location.protocol + "//" + location.host + "(.*)$"); + } + var checkForRoot = leadingUrlRootRegex.exec(url); + if (checkForRoot) { + url = checkForRoot[1]; + } + url = url.replace(leadingUrlRootRegex, ""); + if (path.hasProtocol(url)) { + return url; // External + } else { + // canonize url + var urlSegments, locSegments, i, canonPath = []; + var checkForIndex = /^(.*\/)index\.[^\/?]*(\?.*)?$/i.exec(url); + if (checkForIndex) { + url = checkForIndex[1] + (checkForIndex[2] ? checkForIndex[2] : ""); + } + if (url.charAt(0) === "#") { + if (url.charAt(1) === "/") { // Hash-absolute + return "#" + url.slice(2); + } else if (url === $.mobile.firstPageUrl) { // First page + return "#"; + } else { // Hash-relative + return url; + } + } else if (url.charAt(0) === "/") { // Absolute + locSegments = location.pathname.split("/"); + urlSegments = url.split("/"); + locSegments.pop(); // Remove filename part + for (i=0; locSegments[i] === urlSegments[i]; i++); // Count common elements + urlSegments.splice(0, i); // Remove common elements from url + for (;i < locSegments.length; i++) { + canonPath.push(".."); + } + canonPath = canonPath.concat(urlSegments); + return "#" + canonPath.join("/"); + } else { // Relative + return "#" + path.canonize(path.get() + url); + } + } + }, + + // Remove dot-dot segments where possible by removing both the dot-dot segment and the preceding segment. + // E.g. a/b/../c becomes a/c, but ../a/b remains the same. + // Dot or empty segments are simply removed. + // e.g. a/./b/c or a//b/c becomes a/b/c + canonize: function( path ) { + var canonPath = path.split("/"), i = 0; + while(i < canonPath.length) { + if (canonPath[i] === ".." && i > 0 && canonPath[i-1] !== "..") { + i--; + canonPath.splice(i,2); + } else if (canonPath[i] === "." || // remove same-dir (dot) segments + // Remove empty segments - cannot remove first/last because they mark beginning/trailing slash + (canonPath[i] === "" && i > 0 && i < canonPath.length-1)) { + canonPath.splice(i,1); + } else { + i++; + } + } + return canonPath.join("/"); + }, //just return the url without an initial # @@ -319,12 +459,19 @@ // changepage function $.mobile.changePage = function( to, transition, reverse, changeHash, fromHashChange ){ + if ($.type(to) === "string") { + to = path.clean(to); + if (to === "#") { + to = $.mobile.firstPage; + } + } + //from is always the currently viewed page var toIsArray = $.type(to) === "array", toIsObject = $.type(to) === "object", from = toIsArray ? to[0] : $.mobile.activePage; - to = toIsArray ? to[1] : to; + to = toIsArray ? to[1] : to; var url = $.type(to) === "string" ? path.stripHash( to ) : "", fileUrl = url, @@ -371,7 +518,7 @@ } if( toIsObject && to.url ){ - url = to.url; + url = path.clean(to.url); data = to.data; type = to.type; isFormRequest = true; @@ -430,12 +577,17 @@ to.data( "page" )._trigger( "beforeshow", null, { prevPage: from || $("") } ); function pageChangeComplete(){ - - if( changeHash !== false && url ){ - //disable hash listening temporarily - urlHistory.ignoreNextHashChange = false; - //update hash and history - path.set( url ); + if (changeHash !== false) { + if (to === $.mobile.firstPage) { + //disable hash listening temporarily + urlHistory.ignoreNextHashChange = false; + path.set("#"); + } else if(url) { + //disable hash listening temporarily + urlHistory.ignoreNextHashChange = false; + //update hash and history + path.set( url ); + } } //if title element wasn't found, try the page div data attr too @@ -760,7 +912,6 @@ //if data-ajax attr is set to false, use the default behavior of a link hasAjaxDisabled = $this.is( ":jqmData(ajax='false')" ); - //if there's a data-rel=back attr, go back in history if( $this.is( ":jqmData(rel='back')" ) ){ window.history.back(); @@ -769,7 +920,7 @@ //prevent # urls from bubbling //path.get() is replaced to combat abs url prefixing in IE - if( url.replace(path.get(), "") == "#" ){ + if( href == "#" ){ //for links created purely for interaction - ignore event.preventDefault(); return; @@ -796,14 +947,6 @@ //this may need to be more specific as we use data-rel more nextPageRole = $this.attr( "data-" + $.mobile.ns + "rel" ); - - //if it's a relative href, prefix href with base url - if( path.isRelative( url ) && !hadProtocol ){ - url = path.makeAbsolute( url ); - } - - url = path.stripHash( url ); - $.mobile.changePage( url, transition, reverse); event.preventDefault(); }); @@ -811,7 +954,7 @@ //hashchange event handler $window.bind( "hashchange", function( e, triggered ) { //find first page via hash - var to = path.stripHash( location.hash ), + var to = location.hash, //transition is false if it's the first page, undefined otherwise (and may be overridden by default) transition = $.mobile.urlHistory.stack.length === 0 ? false : undefined; @@ -834,7 +977,7 @@ //determine if we're heading forward or backward and continue accordingly past //the current dialog urlHistory.directHashChange({ - currentUrl: to, + currentUrl: path.stripHash(to), isBack: function(){ window.history.back(); }, isForward: function(){ window.history.forward(); } }); @@ -845,7 +988,7 @@ var setTo = function(){ to = $.mobile.urlHistory.getActive().page; }; // if the current active page is a dialog and we're navigating // to a dialog use the dialog objected saved in the stack - urlHistory.directHashChange({ currentUrl: to, isBack: setTo, isForward: setTo }); + urlHistory.directHashChange({ currentUrl: path.stripHash(to), isBack: setTo, isForward: setTo }); } } diff --git a/tests/unit/navigation/domcopies.html b/tests/unit/navigation/domcopies.html new file mode 100644 index 00000000000..d8c23e5e2a2 --- /dev/null +++ b/tests/unit/navigation/domcopies.html @@ -0,0 +1,14 @@ + + +
+ + + + + +Dom copies test
+