From d4acf8d61adfa536dbfd09cf0fc5aec36609a268 Mon Sep 17 00:00:00 2001 From: Oskari Koskimies Date: Fri, 8 Apr 2011 15:22:24 -0700 Subject: [PATCH 1/6] Canonized urls support. --- js/jquery.mobile.init.js | 3 +- js/jquery.mobile.navigation.js | 174 ++++++++++++++++++++--- tests/unit/navigation/index.html | 6 + tests/unit/navigation/navigation_core.js | 100 +++++++++++-- 4 files changed, 251 insertions(+), 32 deletions(-) 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 5fc74b61747..22a4d5084cb 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,12 @@ //set location hash to path set: function( path ){ - location.hash = path; + //console.log("path.set:", 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 @@ -53,13 +68,104 @@ 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. + // Syntax: current_location + url => resulting_location (returned canonical form) + + // 1) Relative: example.com/a/#b/c.html + ../d/e.html => example.com/a/#d/e.html (#d/e.html) + // 2) Hash-relative: example.com/a/#b/c.html + #d/e.html => example.com/a/#d/e.html (#d/e.html) + // 3) Hash-absolute: example.com/a/#b/c.html + #/d/e.html => example.com/a/#d/e.html (#d/e.html) + // 4) Absolute path: example.com/a/#b/c.html + /d/e.html => example.com/a/#../d/e.html (#../d/e.html) + // 5) Absolute same-host: example.com/a/#b/c.html + example.com/d/e.html => example.com/a/#../d/e.html (#../d/e.html) + // 6) Absolute diff-host: example.com/a/#b/c.html + other.com/d/e.html => other.com/d/e.html (other.com/d/e.html) + // 7) Hashed same-host same-path: example.com/a/#b/c.html + example.com/a/#d/e.html => example.com/a/#d/e.html (#d/e.html) + // 8) Hashed same-host diff-path: example.com/a/#b/c.html + example.com/d/#e.html => example.com/d/#e.html (example.com/d/#e.html) + // 9) Hashed 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 7-9) 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-absolute 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-absolute: 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/# (#) + // 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 ){ + //console.log("clean:", 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; + if (url.indexOf("#") > 0) { + // 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); + } + url = url.replace(leadingUrlRootRegex, ""); + //console.log("Without host & pathname:", url); + if (path.hasProtocol(url)) { + return url; // External + } else { + // canonize url + //console.log("canonize url:", url); + var urlSegments, locSegments, i, canonPath = []; + var checkForIndex = /^(.*\/)index\.[^/]*$/i.exec(url); + if (checkForIndex) { + //console.log("Removing index:", checkForIndex); + url = checkForIndex[1]; + } + if (url.charAt(0) === "#") { // Hash-absolute - nothing to do since we already removed extra index file (if present) + if (url.charAt(1) === "/") { + return "#" + url.slice(2); // Allow "absolute" form of hash + } else if (url === $.mobile.firstPageUrl) { + return "#"; + } else { + return url; + } + } else if (url.charAt(0) === "/") { // Absolute + //console.log("url:", url); + //console.log("pathname:", location.pathname); + locSegments = location.pathname.split("/"); + urlSegments = url.split("/"); + locSegments.pop(); // Remove filename part + for (i=0; locSegments[i] === urlSegments[i]; i++); // Count common elements + //console.log("common elements:", i); + urlSegments.splice(0, i); // Remove common elements from url + //console.log("removed common:", urlSegments); + for (;i < locSegments.length; i++) { + canonPath.push(".."); + } + canonPath = canonPath.concat(urlSegments); + return "#" + canonPath.join("/"); + } else { // Relative + //console.log("path:", path.get()); + canonPath = (path.get() + url).split("/"); + //console.log("canonPath:" + canonPath); + // Remove dot-dot parts where possible by removing both the dot-dot and the preceding segment + i = 0; + while(i < canonPath.length) { + if (canonPath[i] === ".." && i > 0 && canonPath[i-1] !== "..") { + i--; + canonPath.splice(i,2); + } else { + i++; + } + } + return "#" + canonPath.join("/"); + } + } }, //just return the url without an initial # @@ -147,9 +253,12 @@ // save new page index, null check to prevent falsey 0 result this.activeIndex = newActiveIndex !== undefined ? newActiveIndex : this.activeIndex; + //console.log("Checking history"); if( back ){ + //console.log("It's back"); opts.isBack(); } else if( forward ){ + //console.log("It's forward"); opts.isForward(); } }, @@ -292,12 +401,21 @@ // changepage function $.mobile.changePage = function( to, transition, reverse, changeHash, fromHashChange ){ + //console.log("to:", to); + + 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, @@ -344,7 +462,7 @@ } if( toIsObject && to.url ){ - url = to.url; + url = path.clean(to.url); data = to.data; type = to.type; isFormRequest = true; @@ -403,12 +521,20 @@ to.data( "page" )._trigger( "beforeshow", null, { prevPage: from || $("") } ); function loadComplete(){ - - if( changeHash !== false && url ){ - //disable hash listening temporarily - urlHistory.ignoreNextHashChange = false; - //update hash and history - path.set( url ); + //console.log("loadComplete:", url); + + if (changeHash !== false) { + if (to === $.mobile.firstPage) { + //console.log("firstPage, setting url to #"); + //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 @@ -734,7 +860,6 @@ //reset our prevDefault value because I'm paranoid. preventClickDefault = stopClickPropagation = false; - //if there's a data-rel=back attr, go back in history if( $this.is( ":jqmData(rel='back')" ) ){ window.history.back(); @@ -744,7 +869,9 @@ //prevent # urls from bubbling //path.get() is replaced to combat abs url prefixing in IE - if( url.replace(path.get(), "") == "#" ){ + //if( url.replace(path.get(), "") == "#" ){ + if( href == "#" ){ + //console.log("Url was:", url); //for links created purely for interaction - ignore //don't call preventDefault on the event here, vclick //may have been triggered by a touchend, before any moues @@ -777,14 +904,14 @@ //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); preventClickDefault = true; }); @@ -802,8 +929,10 @@ //hashchange event handler $window.bind( "hashchange", function( e, triggered ) { + //console.log("hashchange:", location.href); //find first page via hash - var to = path.stripHash( location.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; @@ -823,10 +952,12 @@ // If current active page is not a dialog skip the dialog and continue // in the same direction if(!$.mobile.activePage.is( ".ui-dialog" )) { + //console.log("active page is not .ui-dialog", to); + //console.log("urlHistory", urlHistory.stack); //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(); } }); @@ -834,10 +965,11 @@ // prevent changepage return; } else { + //console.log("active page is .ui-dialog", $.mobile.urlHistory.getActive().page); 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/index.html b/tests/unit/navigation/index.html index 5f1185105a1..9bd1a58cc79 100644 --- a/tests/unit/navigation/index.html +++ b/tests/unit/navigation/index.html @@ -172,5 +172,11 @@

Dialog

Title Heading

+ +
+ +
+ + diff --git a/tests/unit/navigation/navigation_core.js b/tests/unit/navigation/navigation_core.js index 4ec1f48bffd..6a0704f7b05 100644 --- a/tests/unit/navigation/navigation_core.js +++ b/tests/unit/navigation/navigation_core.js @@ -95,7 +95,7 @@ test( "path.get method is working properly", function(){ window.location.hash = "foo"; - same($.mobile.path.get(), "foo", "get method returns location.hash minus hash character"); + same($.mobile.path.get(), "", "get method for location.hash #foo returns empty string"); same($.mobile.path.get( "#foo/bar/baz.html" ), "foo/bar/", "get method with hash arg returns path with no filename or hash prefix"); same($.mobile.path.get( "#foo/bar/baz.html/" ), "foo/bar/baz.html/", "last segment of hash is retained if followed by a trailing slash"); }); @@ -128,16 +128,51 @@ }); test( "path.clean is working properly", function(){ + + $.testHelper.openPage("#data-url-tests/data-url.html"); + var localroot = location.protocol + "//" + location.host + location.pathname, remoteroot = "http://google.com/", fakepath = "#foo/bar/baz.html", - pathWithParam = localroot + "/bar?baz=" + localroot, + pathWithParam = localroot + "bar?baz=" + localroot, localpath = localroot + fakepath, - remotepath = remoteroot + fakepath; + remotepath = remoteroot + fakepath, + relpath1 = "../foo.html", + relpath2 = "../foo/bar.html", + relpath3 = "../../foo/bar.html", + abspath1 = "/foo/bar.html", + abspath2 = location.pathname + "foo/bar.html", + hrelpath1 = "#foo.html", + hrelpath2 = "#foo/bar.html", + hrelpath3 = "#../../foo/bar.html", + habspath1 = "#/foo.html", + habspath2 = "#/foo/bar.html", + habspath3 = "#/../../foo/bar.html", + segments = location.pathname.split("/"), + uppath = "#", + i; + + // pathname ends and begins with slash, so first and last elements are empty + for (i=0; i Date: Fri, 8 Apr 2011 15:45:50 -0700 Subject: [PATCH 2/6] Added missing end parenthesis --- tests/unit/navigation/navigation_core.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/navigation/navigation_core.js b/tests/unit/navigation/navigation_core.js index 3a71dc1cda2..1a8672fd17a 100644 --- a/tests/unit/navigation/navigation_core.js +++ b/tests/unit/navigation/navigation_core.js @@ -499,6 +499,7 @@ same(window.location.hash.replace(/^#/, ""), "", "hash should be empty (not id value)"); start(); }], 1000); + }); asyncTest( "Page links to the current active page result in the same active page", function(){ $.testHelper.openPage("#self-link"); From 0bd848c067ffe483b3b594bd783adf28ad936737 Mon Sep 17 00:00:00 2001 From: Oskari Koskimies Date: Tue, 12 Apr 2011 13:39:00 -0700 Subject: [PATCH 3/6] Added more complete unit tests, modified implementation comments --- js/jquery.mobile.navigation.js | 106 ++++++++++++++++------- tests/unit/navigation/navigation_core.js | 75 ++++++++++++---- 2 files changed, 131 insertions(+), 50 deletions(-) diff --git a/js/jquery.mobile.navigation.js b/js/jquery.mobile.navigation.js index ee88741d9ff..83cd644be58 100644 --- a/js/jquery.mobile.navigation.js +++ b/js/jquery.mobile.navigation.js @@ -69,30 +69,45 @@ }, // There are several supported url formats, which are cleaned (converted to canonical form) in this function. - // Syntax: current_location + url => resulting_location (returned canonical form) - - // 1) Relative: example.com/a/#b/c.html + ../d/e.html => example.com/a/#d/e.html (#d/e.html) - // 2) Hash-relative: example.com/a/#b/c.html + #d/e.html => example.com/a/#d/e.html (#d/e.html) - // 3) Hash-absolute: example.com/a/#b/c.html + #/d/e.html => example.com/a/#d/e.html (#d/e.html) - // 4) Absolute path: example.com/a/#b/c.html + /d/e.html => example.com/a/#../d/e.html (#../d/e.html) - // 5) Absolute same-host: example.com/a/#b/c.html + example.com/d/e.html => example.com/a/#../d/e.html (#../d/e.html) - // 6) Absolute diff-host: example.com/a/#b/c.html + other.com/d/e.html => other.com/d/e.html (other.com/d/e.html) - // 7) Hashed same-host same-path: example.com/a/#b/c.html + example.com/a/#d/e.html => example.com/a/#d/e.html (#d/e.html) - // 8) Hashed same-host diff-path: example.com/a/#b/c.html + example.com/d/#e.html => example.com/d/#e.html (example.com/d/#e.html) - // 9) Hashed diff-host: example.com/a/#b/c.html + other.com/d/#e.html => other.com/d/#e.html (other.com/d/#e.html) + // 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 7-9) must always use the canonical form in the hash part. + // 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-absolute form is same as hash-relative, just with a slash prefix. The reason it is supported + // 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-absolute: 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/# (#) + // 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 has 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 "#/". @@ -101,17 +116,31 @@ //console.log("clean:", 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 - var leadingUrlRootRegex; - if (url.indexOf("#") > 0) { + var leadingUrlRootRegex, checkForRoot; + 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); + leadingUrlRootRegex = new RegExp("^" + location.protocol + "//" + location.host + location.pathname + "(#.*)$"); } else { - leadingUrlRootRegex = new RegExp("^" + location.protocol + "//" + location.host); + leadingUrlRootRegex = new RegExp("^" + location.protocol + "//" + location.host + "(.*)$"); + } + checkForRoot = leadingUrlRootRegex.exec(url); + if (checkForRoot) { + url = checkForRoot[1]; } url = url.replace(leadingUrlRootRegex, ""); //console.log("Without host & pathname:", url); @@ -151,21 +180,32 @@ return "#" + canonPath.join("/"); } else { // Relative //console.log("path:", path.get()); - canonPath = (path.get() + url).split("/"); - //console.log("canonPath:" + canonPath); - // Remove dot-dot parts where possible by removing both the dot-dot and the preceding segment - i = 0; - while(i < canonPath.length) { - if (canonPath[i] === ".." && i > 0 && canonPath[i-1] !== "..") { - i--; - canonPath.splice(i,2); - } else { - i++; - } - } - return "#" + canonPath.join("/"); + 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; + //console.log("canonPath:" + canonPath); + while(i < canonPath.length) { + if (canonPath[i] === ".." && i > 0 && canonPath[i-1] !== "..") { + i--; + canonPath.splice(i,2); + } else if (canonPath[i] === "." || + // 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 # diff --git a/tests/unit/navigation/navigation_core.js b/tests/unit/navigation/navigation_core.js index 1a8672fd17a..b8847518c75 100644 --- a/tests/unit/navigation/navigation_core.js +++ b/tests/unit/navigation/navigation_core.js @@ -145,9 +145,13 @@ hrelpath1 = "#foo.html", hrelpath2 = "#foo/bar.html", hrelpath3 = "#../../foo/bar.html", - habspath1 = "#/foo.html", - habspath2 = "#/foo/bar.html", - habspath3 = "#/../../foo/bar.html", + hrootpath1 = "#/foo.html", + hrootpath2 = "#/foo/bar.html", + hrootpath3 = "#/../../foo/bar.html", + hashedrel1 = "../foo/#bar.html", + hashedrel2 = "../#foo/bar.html", + hashedabs1 = "/foo/#bar.html", + hashedabs2 = location.pathname + "#foo/bar.html", segments = location.pathname.split("/"), uppath = "#", i; @@ -159,20 +163,57 @@ // We are in a subdirectory, so one more .. uppath += "foo/bar.html"; - same( $.mobile.path.clean( localpath ), fakepath, "removes location protocol, host, port, pathname from same-domain path"); - same( $.mobile.path.clean( remotepath ), remotepath, "does nothing to an external domain path"); - same( $.mobile.path.clean( pathWithParam ), "#bar?baz=" + localroot, "doesn't remove params with localroot value"); - same( $.mobile.path.clean( relpath1 ), "#foo.html", ".. removed from current path, no dirs left"); - same( $.mobile.path.clean( relpath2 ), "#foo/bar.html", ".. removed from current path, one dir left"); - same( $.mobile.path.clean( relpath3 ), "#../foo/bar.html", ".. removed from current path, one .. still left"); - same( $.mobile.path.clean( abspath1 ), uppath, "absolute path above localroot"); - same( $.mobile.path.clean( abspath2 ), "#foo/bar.html", "absolute path below localroot"); - same( $.mobile.path.clean( hrelpath1 ), "#foo.html", "hash-relative path stays the same 1"); - same( $.mobile.path.clean( hrelpath2 ), "#foo/bar.html", "hash-relative path stays the same 2"); - same( $.mobile.path.clean( hrelpath3 ), "#../../foo/bar.html", "hash-relative path stays the same 3"); - same( $.mobile.path.clean( habspath1 ), "#foo.html", "hash-absolute path stays the same minus leading slash 1"); - same( $.mobile.path.clean( habspath2 ), "#foo/bar.html", "hash-absolute path stays the same minus leading slash 2"); - same( $.mobile.path.clean( habspath3 ), "#../../foo/bar.html", "hash-absolute path stays the same minus leading slash 3"); + same( $.mobile.path.clean( localpath ), fakepath, + "removes location protocol, host, port, pathname from same-domain path"); + same( $.mobile.path.clean( pathWithParam ), "#bar?baz=" + localroot, + "doesn't remove params with localroot value"); + // relative + same( $.mobile.path.clean( relpath1 ), "#foo.html", + ".. removed from current path, no dirs left"); + same( $.mobile.path.clean( relpath2 ), "#foo/bar.html", + ".. removed from current path, one dir left"); + same( $.mobile.path.clean( relpath3 ), "#../foo/bar.html", + ".. removed from current path, one .. still left"); + // hash-relative + same( $.mobile.path.clean( hrelpath1 ), "#foo.html", + "hash-relative path stays the same 1"); + same( $.mobile.path.clean( hrelpath2 ), "#foo/bar.html", + "hash-relative path stays the same 2"); + same( $.mobile.path.clean( hrelpath3 ), "#../../foo/bar.html", + "hash-relative path stays the same 3"); + // hash-root + same( $.mobile.path.clean( hrootpath1 ), "#foo.html", + "hash-absolute path stays the same minus leading slash 1"); + same( $.mobile.path.clean( hrootpath2 ), "#foo/bar.html", + "hash-absolute path stays the same minus leading slash 2"); + same( $.mobile.path.clean( hrootpath3 ), "#../../foo/bar.html", + "hash-absolute path stays the same minus leading slash 3"); + // absolute + same( $.mobile.path.clean( abspath1 ), uppath, + "absolute path above localroot"); + same( $.mobile.path.clean( abspath2 ), "#foo/bar.html", + "absolute path below localroot"); + // hashed relative + same( $.mobile.path.clean( hashedrel1 ), + location.protocol + "//" + location.host + location.pathname + "foo/#bar.html", + "hashed relative, different path: returns full url"); + same( $.mobile.path.clean( hashedrel2 ), "#foo/bar.html", + "hashed relative, same path: returns hash-relative path"); + // hashed absolute + same( $.mobile.path.clean( hashedabs1 ), + location.protocol + "//" + location.host + "/foo/#bar.html", + "hashed absolute, different path: return full url"); + same( $.mobile.path.clean( hashedabs2 ), "#foo/bar.html", + "hashed absolute, same path: returns hash-relative path"); + // hashed host-absolute + same( $.mobile.path.clean( location.protocol + "//" + location.host + hashedabs1 ), + location.protocol + "//" + location.host + "/foo/#bar.html", + "hashed host-absolute, different path: return full url"); + same( $.mobile.path.clean( location.protocol + "//" + location.host + hashedabs2 ), + "#foo/bar.html", + "hashed host-absolute, same path: return hash-relative path"); + same( $.mobile.path.clean( remotepath ), remotepath, + "Hashed host-absolute, different host: return full url"); }); test( "path.stripHash is working properly", function(){ From d91c68edef5857499bdf868681f55b3e052d666c Mon Sep 17 00:00:00 2001 From: Oskari Koskimies Date: Tue, 12 Apr 2011 13:56:21 -0700 Subject: [PATCH 4/6] Moved the modified path.clean unit tests here. --- tests/unit/navigation/navigation_helpers.js | 90 +++++++++++++++++++-- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/tests/unit/navigation/navigation_helpers.js b/tests/unit/navigation/navigation_helpers.js index b43a97eb855..24b833d1624 100644 --- a/tests/unit/navigation/navigation_helpers.js +++ b/tests/unit/navigation/navigation_helpers.js @@ -56,16 +56,92 @@ }); test( "path.clean is working properly", function(){ + + $.testHelper.openPage("#data-url-tests/data-url.html"); + var localroot = location.protocol + "//" + location.host + location.pathname, remoteroot = "http://google.com/", fakepath = "#foo/bar/baz.html", - pathWithParam = localroot + "/bar?baz=" + localroot, + pathWithParam = localroot + "bar?baz=" + localroot, localpath = localroot + fakepath, - remotepath = remoteroot + fakepath; - - same( $.mobile.path.clean( localpath ), fakepath, "removes location protocol, host, port, pathname from same-domain path"); - same( $.mobile.path.clean( remotepath ), remotepath, "does nothing to an external domain path"); - same( $.mobile.path.clean( pathWithParam ), "/bar?baz=" + localroot, "doesn't remove params with localroot value"); + remotepath = remoteroot + fakepath, + relpath1 = "../foo.html", + relpath2 = "../foo/bar.html", + relpath3 = "../../foo/bar.html", + abspath1 = "/foo/bar.html", + abspath2 = location.pathname + "foo/bar.html", + hrelpath1 = "#foo.html", + hrelpath2 = "#foo/bar.html", + hrelpath3 = "#../../foo/bar.html", + hrootpath1 = "#/foo.html", + hrootpath2 = "#/foo/bar.html", + hrootpath3 = "#/../../foo/bar.html", + hashedrel1 = "../foo/#bar.html", + hashedrel2 = "../#foo/bar.html", + hashedabs1 = "/foo/#bar.html", + hashedabs2 = location.pathname + "#foo/bar.html", + segments = location.pathname.split("/"), + uppath = "#", + i; + + // pathname ends and begins with slash, so first and last elements are empty + for (i=0; i Date: Thu, 14 Apr 2011 04:13:31 -0700 Subject: [PATCH 5/6] Fixed problem with params url and added more unit tests --- js/jquery.mobile.navigation.js | 23 +++++++++---- tests/unit/navigation/domcopies.html | 14 ++++++++ tests/unit/navigation/navigation_core.js | 36 ++++++++++++++++++--- tests/unit/navigation/navigation_helpers.js | 14 ++++++-- 4 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 tests/unit/navigation/domcopies.html diff --git a/js/jquery.mobile.navigation.js b/js/jquery.mobile.navigation.js index 9a902b14ee2..6844d1c32b0 100644 --- a/js/jquery.mobile.navigation.js +++ b/js/jquery.mobile.navigation.js @@ -130,9 +130,15 @@ clean: function( url ){ //console.log("clean:", url); + + if(path.isQuery(url)){ + //console.log("it's a query, returning #" + path.cleanHash(location.hash) + 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 - var leadingUrlRootRegex, checkForRoot; + 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 @@ -154,7 +160,7 @@ } else { leadingUrlRootRegex = new RegExp("^" + location.protocol + "//" + location.host + "(.*)$"); } - checkForRoot = leadingUrlRootRegex.exec(url); + var checkForRoot = leadingUrlRootRegex.exec(url); if (checkForRoot) { url = checkForRoot[1]; } @@ -166,10 +172,13 @@ // canonize url //console.log("canonize url:", url); var urlSegments, locSegments, i, canonPath = []; - var checkForIndex = /^(.*\/)index\.[^/]*$/i.exec(url); + var checkForIndex = /^(.*\/)index\.[^\/?]*(\?.*)?$/i.exec(url); if (checkForIndex) { //console.log("Removing index:", checkForIndex); url = checkForIndex[1]; + if (checkForIndex[2]) { + url += checkForIndex[2]; + } } if (url.charAt(0) === "#") { // Hash-absolute - nothing to do since we already removed extra index file (if present) if (url.charAt(1) === "/") { @@ -212,8 +221,8 @@ if (canonPath[i] === ".." && i > 0 && canonPath[i-1] !== "..") { i--; canonPath.splice(i,2); - } else if (canonPath[i] === "." || - // Cannot remove first/last because they mark beginning/trailing slash + } 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 { @@ -469,7 +478,7 @@ // changepage function $.mobile.changePage = function( to, transition, reverse, changeHash, fromHashChange ){ - //console.log("to:", to); + //console.log("changePage to:", to); if ($.type(to) === "string") { to = path.clean(to); @@ -927,7 +936,7 @@ //if data-ajax attr is set to false, use the default behavior of a link hasAjaxDisabled = $this.is( ":jqmData(ajax='false')" ); - + //console.log("linkHander; url is now: " + url); //if there's a data-rel=back attr, go back in history if( $this.is( ":jqmData(rel='back')" ) ){ window.history.back(); 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

+
+ + + diff --git a/tests/unit/navigation/navigation_core.js b/tests/unit/navigation/navigation_core.js index 8a39acdeb55..38413f4951b 100644 --- a/tests/unit/navigation/navigation_core.js +++ b/tests/unit/navigation/navigation_core.js @@ -29,9 +29,10 @@ }, function(){ - ok(called == 1, "change page should be called once"); + ok(called >= 1, "change page should be called at least once"); + ok(called <= 1, "change page should be called at most once"); start(); - }], 500); + }], 1000); }); asyncTest( "forms with data attribute ajax set to false will not call changePage", function(){ @@ -385,6 +386,34 @@ }], 1000); }); + asyncTest( "navigating to a page via several paths only loads it to DOM once", function(){ + $.testHelper.sequence([ + // transition to the first page + function(){ $.mobile.changePage("#/"); }, + + // transition to title2.html directly + function(){ $.mobile.changePage("domcopies.html"); }, + + // transition to subdirectory + function(){ $.mobile.changePage("#data-url-tests/non-data-url.html"); }, + + // and then back to title2.html + function(){ $.mobile.changePage("../domcopies.html"); }, + + // transition to subdirectory again + function(){ $.mobile.changePage("#data-url-tests/non-data-url.html"); }, + + // and then back to title2.html using absolute url + function(){ $.mobile.changePage(location.pathname + "domcopies.html"); }, + + // make sure we only have one copy of the dom + function(){ + ok($("p.domcopy_count").length <= 1 , "There should be at most one copy of the page in DOM"); + ok($("p.domcopy_count").length >= 1 , "There should be at least one copy of the page in DOM"); + start(); + }], 1000); + }); + asyncTest( "Page links to the current active page result in the same active page", function(){ $.testHelper.openPage("#self-link"); $.testHelper.sequence([ @@ -436,7 +465,6 @@ same(location.hash, "#data-url-tests/non-data-url.html?foo=bar"); start(); } -], 1000); + ], 1000); }); - })(jQuery); diff --git a/tests/unit/navigation/navigation_helpers.js b/tests/unit/navigation/navigation_helpers.js index 24b833d1624..f1f35d3efb0 100644 --- a/tests/unit/navigation/navigation_helpers.js +++ b/tests/unit/navigation/navigation_helpers.js @@ -10,7 +10,7 @@ test( "path.get method is working properly", function(){ window.location.hash = "foo"; - same($.mobile.path.get(), "foo", "get method returns location.hash minus hash character"); + same($.mobile.path.get(), "", "get method for location.hash #foo returns empty string"); same($.mobile.path.get( "#foo/bar/baz.html" ), "foo/bar/", "get method with hash arg returns path with no filename or hash prefix"); same($.mobile.path.get( "#foo/bar/baz.html/" ), "foo/bar/baz.html/", "last segment of hash is retained if followed by a trailing slash"); }); @@ -46,10 +46,10 @@ same( $.mobile.path.makeAbsolute("?foo=bar&bak=baz"), "bar/bing/bang?foo=bar&bak=baz", "appends query string paths to current path"); $.mobile.path.set(""); - same( $.mobile.path.makeAbsolute("?foo=bar&bak=baz"), "/tests/unit/navigation/?foo=bar&bak=baz", "uses pathname for empty hash"); + same( $.mobile.path.makeAbsolute("?foo=bar&bak=baz"), location.pathname + "?foo=bar&bak=baz", "uses pathname for empty hash"); $.mobile.path.set("bar"); - same( $.mobile.path.makeAbsolute("?foo=bar&bak=baz"), "/tests/unit/navigation/?foo=bar&bak=baz", "uses pathname for embedded pages"); + same( $.mobile.path.makeAbsolute("?foo=bar&bak=baz"), location.pathname + "?foo=bar&bak=baz", "uses pathname for embedded pages"); $.mobile.path.set("bar/bing?foo=bar"); same( $.mobile.path.makeAbsolute("?foo=bar&bak=baz"), "bar/bing?foo=bar&bak=baz", "prevents addition of many sets of query params"); @@ -80,6 +80,8 @@ hashedrel2 = "../#foo/bar.html", hashedabs1 = "/foo/#bar.html", hashedabs2 = location.pathname + "#foo/bar.html", + indexpath = "#foo/index.chm", + indexparampath = "#foo/index.chm?foo=" + localroot, segments = location.pathname.split("/"), uppath = "#", i; @@ -142,6 +144,12 @@ "hashed host-absolute, same path: return hash-relative path"); same( $.mobile.path.clean( remotepath ), remotepath, "Hashed host-absolute, different host: return full url"); + // with index.* at the end + same( $.mobile.path.clean( indexpath ), "#foo/", + "Remove index.* from path"); + same( $.mobile.path.clean( indexparampath ), "#foo/?foo=" + localroot, + "Remove index.* from path but preserve parameters"); + }); test( "path.stripHash is working properly", function(){ From fe93ba5f76a2ebd85bf5b3ded30d8ebe30977a90 Mon Sep 17 00:00:00 2001 From: Oskari Koskimies Date: Thu, 14 Apr 2011 12:18:03 -0700 Subject: [PATCH 6/6] Fixed some comments --- js/jquery.mobile.navigation.js | 42 +++++----------------------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/js/jquery.mobile.navigation.js b/js/jquery.mobile.navigation.js index 6844d1c32b0..db13dc786a8 100644 --- a/js/jquery.mobile.navigation.js +++ b/js/jquery.mobile.navigation.js @@ -40,7 +40,6 @@ //set location hash to path set: function( path ){ - //console.log("path.set:", path); if (path === "#" && location.href.indexOf("#") === -1) { location.href += "#"; // Only way to set empty hash for non-hash url } else { @@ -129,10 +128,7 @@ // Note that the location (url bar) will always have a hash character, event when on first page. clean: function( url ){ - //console.log("clean:", url); - if(path.isQuery(url)){ - //console.log("it's a query, returning #" + path.cleanHash(location.hash) + url); return "#" + path.cleanHash(location.hash) + url; } @@ -165,46 +161,35 @@ url = checkForRoot[1]; } url = url.replace(leadingUrlRootRegex, ""); - //console.log("Without host & pathname:", url); if (path.hasProtocol(url)) { return url; // External } else { // canonize url - //console.log("canonize url:", url); var urlSegments, locSegments, i, canonPath = []; var checkForIndex = /^(.*\/)index\.[^\/?]*(\?.*)?$/i.exec(url); if (checkForIndex) { - //console.log("Removing index:", checkForIndex); - url = checkForIndex[1]; - if (checkForIndex[2]) { - url += checkForIndex[2]; - } + url = checkForIndex[1] + (checkForIndex[2] ? checkForIndex[2] : ""); } - if (url.charAt(0) === "#") { // Hash-absolute - nothing to do since we already removed extra index file (if present) - if (url.charAt(1) === "/") { - return "#" + url.slice(2); // Allow "absolute" form of hash - } else if (url === $.mobile.firstPageUrl) { + if (url.charAt(0) === "#") { + if (url.charAt(1) === "/") { // Hash-absolute + return "#" + url.slice(2); + } else if (url === $.mobile.firstPageUrl) { // First page return "#"; - } else { + } else { // Hash-relative return url; } } else if (url.charAt(0) === "/") { // Absolute - //console.log("url:", url); - //console.log("pathname:", location.pathname); locSegments = location.pathname.split("/"); urlSegments = url.split("/"); locSegments.pop(); // Remove filename part for (i=0; locSegments[i] === urlSegments[i]; i++); // Count common elements - //console.log("common elements:", i); urlSegments.splice(0, i); // Remove common elements from url - //console.log("removed common:", urlSegments); for (;i < locSegments.length; i++) { canonPath.push(".."); } canonPath = canonPath.concat(urlSegments); return "#" + canonPath.join("/"); } else { // Relative - //console.log("path:", path.get()); return "#" + path.canonize(path.get() + url); } } @@ -216,7 +201,6 @@ // e.g. a/./b/c or a//b/c becomes a/b/c canonize: function( path ) { var canonPath = path.split("/"), i = 0; - //console.log("canonPath:" + canonPath); while(i < canonPath.length) { if (canonPath[i] === ".." && i > 0 && canonPath[i-1] !== "..") { i--; @@ -322,12 +306,9 @@ // save new page index, null check to prevent falsey 0 result this.activeIndex = newActiveIndex !== undefined ? newActiveIndex : this.activeIndex; - //console.log("Checking history"); if( back ){ - //console.log("It's back"); opts.isBack(); } else if( forward ){ - //console.log("It's forward"); opts.isForward(); } }, @@ -478,8 +459,6 @@ // changepage function $.mobile.changePage = function( to, transition, reverse, changeHash, fromHashChange ){ - //console.log("changePage to:", to); - if ($.type(to) === "string") { to = path.clean(to); if (to === "#") { @@ -598,11 +577,8 @@ to.data( "page" )._trigger( "beforeshow", null, { prevPage: from || $("") } ); function pageChangeComplete(){ - //console.log("pageChangeComplete:", url); - if (changeHash !== false) { if (to === $.mobile.firstPage) { - //console.log("firstPage, setting url to #"); //disable hash listening temporarily urlHistory.ignoreNextHashChange = false; path.set("#"); @@ -936,7 +912,6 @@ //if data-ajax attr is set to false, use the default behavior of a link hasAjaxDisabled = $this.is( ":jqmData(ajax='false')" ); - //console.log("linkHander; url is now: " + url); //if there's a data-rel=back attr, go back in history if( $this.is( ":jqmData(rel='back')" ) ){ window.history.back(); @@ -978,9 +953,7 @@ //hashchange event handler $window.bind( "hashchange", function( e, triggered ) { - //console.log("hashchange:", location.href); //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; @@ -1001,8 +974,6 @@ // If current active page is not a dialog skip the dialog and continue // in the same direction if(!$.mobile.activePage.is( ".ui-dialog" )) { - //console.log("active page is not .ui-dialog", to); - //console.log("urlHistory", urlHistory.stack); //determine if we're heading forward or backward and continue accordingly past //the current dialog urlHistory.directHashChange({ @@ -1014,7 +985,6 @@ // prevent changepage return; } else { - //console.log("active page is .ui-dialog", $.mobile.urlHistory.getActive().page); 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