mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-17 07:40:22 +00:00
feat($location): $location service with html5 history api support
See documentation of $location for more info Breaks $location has no properties, only get/set methods Closes #168 Closes #146 Closes #281 Closes #234
This commit is contained in:
parent
f37f0ea16e
commit
5ba227c7cd
2 changed files with 1062 additions and 480 deletions
|
|
@ -1,275 +1,505 @@
|
|||
'use strict';
|
||||
|
||||
var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/,
|
||||
HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/,
|
||||
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21};
|
||||
PATH_MATCH = /^([^\?#]*)?(\?([^#]*))?(#(.*))?$/,
|
||||
HASH_MATCH = PATH_MATCH,
|
||||
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21};
|
||||
|
||||
|
||||
/**
|
||||
* Encode path using encodeUriSegment, ignoring forward slashes
|
||||
*
|
||||
* @param {string} path Path to encode
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodePath(path) {
|
||||
var segments = path.split('/'),
|
||||
i = segments.length;
|
||||
|
||||
while (i--) {
|
||||
segments[i] = encodeUriSegment(segments[i]);
|
||||
}
|
||||
|
||||
return segments.join('/');
|
||||
}
|
||||
|
||||
|
||||
function matchUrl(url, obj) {
|
||||
var match = URL_MATCH.exec(url),
|
||||
|
||||
match = {
|
||||
protocol: match[1],
|
||||
host: match[3],
|
||||
port: parseInt(match[5]) || DEFAULT_PORTS[match[1]] || null,
|
||||
path: match[6] || '/',
|
||||
search: match[8],
|
||||
hash: match[10]
|
||||
};
|
||||
|
||||
if (obj) {
|
||||
obj.$$protocol = match.protocol;
|
||||
obj.$$host = match.host;
|
||||
obj.$$port = match.port;
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
|
||||
function composeProtocolHostPort(protocol, host, port) {
|
||||
return protocol + '://' + host + (port == DEFAULT_PORTS[protocol] ? '' : ':' + port);
|
||||
}
|
||||
|
||||
|
||||
function pathPrefixFromBase(basePath) {
|
||||
return basePath.substr(0, basePath.lastIndexOf('/'));
|
||||
}
|
||||
|
||||
|
||||
function convertToHtml5Url(url, basePath, hashPrefix) {
|
||||
var match = matchUrl(url);
|
||||
|
||||
// already html5 url
|
||||
if (decodeURIComponent(match.path) != basePath || isUndefined(match.hash) ||
|
||||
match.hash.indexOf(hashPrefix) != 0) {
|
||||
return url;
|
||||
// convert hashbang url -> html5 url
|
||||
} else {
|
||||
return composeProtocolHostPort(match.protocol, match.host, match.port) +
|
||||
pathPrefixFromBase(basePath) + match.hash.substr(hashPrefix.length);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function convertToHashbangUrl(url, basePath, hashPrefix) {
|
||||
var match = matchUrl(url);
|
||||
|
||||
// already hashbang url
|
||||
if (decodeURIComponent(match.path) == basePath) {
|
||||
return url;
|
||||
// convert html5 url -> hashbang url
|
||||
} else {
|
||||
var search = match.search && '?' + match.search || '',
|
||||
hash = match.hash && '#' + match.hash || '',
|
||||
pathPrefix = pathPrefixFromBase(basePath),
|
||||
path = match.path.substr(pathPrefix.length);
|
||||
|
||||
if (match.path.indexOf(pathPrefix) != 0) {
|
||||
throw 'Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !';
|
||||
}
|
||||
|
||||
return composeProtocolHostPort(match.protocol, match.host, match.port) + basePath +
|
||||
'#' + hashPrefix + path + search + hash;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* LocationUrl represents an url
|
||||
* This object is exposed as $location service when html5 is enabled and supported
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} url Html5 url
|
||||
* @param {string} pathPrefix
|
||||
*/
|
||||
function LocationUrl(url, pathPrefix) {
|
||||
pathPrefix = pathPrefix || '';
|
||||
|
||||
/**
|
||||
* Parse given html5 (regular) url string into properties
|
||||
* @param {string} url Html5 url
|
||||
* @private
|
||||
*/
|
||||
this.$$parse = function(url) {
|
||||
var match = matchUrl(url, this);
|
||||
|
||||
if (match.path.indexOf(pathPrefix) != 0) {
|
||||
throw 'Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !';
|
||||
}
|
||||
|
||||
this.$$path = decodeURIComponent(match.path.substr(pathPrefix.length));
|
||||
this.$$search = parseKeyValue(match.search);
|
||||
this.$$hash = match.hash && decodeURIComponent(match.hash) || '';
|
||||
|
||||
this.$$compose();
|
||||
},
|
||||
|
||||
/**
|
||||
* Compose url and update `absUrl` property
|
||||
* @private
|
||||
*/
|
||||
this.$$compose = function() {
|
||||
var search = toKeyValue(this.$$search),
|
||||
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
|
||||
|
||||
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
|
||||
this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) +
|
||||
pathPrefix + this.$$url;
|
||||
};
|
||||
|
||||
this.$$parse(url);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* LocationHashbangUrl represents url
|
||||
* This object is exposed as $location service when html5 history api is disabled or not supported
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} url Legacy url
|
||||
* @param {string} hashPrefix Prefix for hash part (containing path and search)
|
||||
*/
|
||||
function LocationHashbangUrl(url, hashPrefix) {
|
||||
var basePath;
|
||||
|
||||
/**
|
||||
* Parse given hashbang url into properties
|
||||
* @param {string} url Hashbang url
|
||||
* @private
|
||||
*/
|
||||
this.$$parse = function(url) {
|
||||
var match = matchUrl(url, this);
|
||||
|
||||
if (match.hash && match.hash.indexOf(hashPrefix) != 0) {
|
||||
throw 'Invalid url "' + url + '", missing hash prefix "' + hashPrefix + '" !';
|
||||
}
|
||||
|
||||
basePath = match.path + (match.search ? '?' + match.search : '');
|
||||
match = HASH_MATCH.exec((match.hash || '').substr(hashPrefix.length));
|
||||
if (match[1]) {
|
||||
this.$$path = (match[1].charAt(0) == '/' ? '' : '/') + decodeURIComponent(match[1]);
|
||||
} else {
|
||||
this.$$path = '';
|
||||
}
|
||||
|
||||
this.$$search = parseKeyValue(match[3]);
|
||||
this.$$hash = match[5] && decodeURIComponent(match[5]) || '';
|
||||
|
||||
this.$$compose();
|
||||
};
|
||||
|
||||
/**
|
||||
* Compose hashbang url and update `absUrl` property
|
||||
* @private
|
||||
*/
|
||||
this.$$compose = function() {
|
||||
var search = toKeyValue(this.$$search),
|
||||
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
|
||||
|
||||
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
|
||||
this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) +
|
||||
basePath + (this.$$url ? '#' + hashPrefix + this.$$url : '');
|
||||
};
|
||||
|
||||
this.$$parse(url);
|
||||
}
|
||||
|
||||
|
||||
LocationUrl.prototype = LocationHashbangUrl.prototype = {
|
||||
|
||||
/**
|
||||
* Has any change been replacing ?
|
||||
* @private
|
||||
*/
|
||||
$$replace: false,
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name angular.service.$location#absUrl
|
||||
* @methodOf angular.service.$location
|
||||
*
|
||||
* @description
|
||||
* This method is getter only.
|
||||
*
|
||||
* Return full url representation with all segments encoded according to rules specified in
|
||||
* {@link http://www.ietf.org/rfc/rfc3986.txt RFC 3986}.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
absUrl: locationGetter('$$absUrl'),
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name angular.service.$location#url
|
||||
* @methodOf angular.service.$location
|
||||
*
|
||||
* @description
|
||||
* This method is getter / setter.
|
||||
*
|
||||
* Return url (e.g. `/path?a=b#hash`) when called without any parameter.
|
||||
*
|
||||
* Change path, search and hash, when called with parameter and return `$location`.
|
||||
*
|
||||
* @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`)
|
||||
* @return {string}
|
||||
*/
|
||||
url: function(url, replace) {
|
||||
if (isUndefined(url))
|
||||
return this.$$url;
|
||||
|
||||
var match = PATH_MATCH.exec(url);
|
||||
this.path(decodeURIComponent(match[1] || '')).search(match[3] || '')
|
||||
.hash(match[5] || '', replace);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name angular.service.$location#protocol
|
||||
* @methodOf angular.service.$location
|
||||
*
|
||||
* @description
|
||||
* This method is getter only.
|
||||
*
|
||||
* Return protocol of current url.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
protocol: locationGetter('$$protocol'),
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name angular.service.$location#host
|
||||
* @methodOf angular.service.$location
|
||||
*
|
||||
* @description
|
||||
* This method is getter only.
|
||||
*
|
||||
* Return host of current url.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
host: locationGetter('$$host'),
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name angular.service.$location#port
|
||||
* @methodOf angular.service.$location
|
||||
*
|
||||
* @description
|
||||
* This method is getter only.
|
||||
*
|
||||
* Return port of current url.
|
||||
*
|
||||
* @return {Number}
|
||||
*/
|
||||
port: locationGetter('$$port'),
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name angular.service.$location#path
|
||||
* @methodOf angular.service.$location
|
||||
*
|
||||
* @description
|
||||
* This method is getter / setter.
|
||||
*
|
||||
* Return path of current url when called without any parameter.
|
||||
*
|
||||
* Change path when called with parameter and return `$location`.
|
||||
*
|
||||
* Note: Path should always begin with forward slash (/), this method will add the forward slash
|
||||
* if it is missing.
|
||||
*
|
||||
* @param {string=} path New path
|
||||
* @return {string}
|
||||
*/
|
||||
path: locationGetterSetter('$$path', function(path) {
|
||||
return path.charAt(0) == '/' ? path : '/' + path;
|
||||
}),
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name angular.service.$location#search
|
||||
* @methodOf angular.service.$location
|
||||
*
|
||||
* @description
|
||||
* This method is getter / setter.
|
||||
*
|
||||
* Return search part (as object) of current url when called without any parameter.
|
||||
*
|
||||
* Change search part when called with parameter and return `$location`.
|
||||
*
|
||||
* @param {string|object<string,string>=} search New search part - string or hash object
|
||||
* @return {string}
|
||||
*/
|
||||
search: function(search, paramValue) {
|
||||
if (isUndefined(search))
|
||||
return this.$$search;
|
||||
|
||||
if (isDefined(paramValue)) {
|
||||
if (paramValue === null) {
|
||||
delete this.$$search[search];
|
||||
} else {
|
||||
this.$$search[search] = escape(paramValue);
|
||||
}
|
||||
} else {
|
||||
this.$$search = isString(search) ? parseKeyValue(search) : search;
|
||||
}
|
||||
|
||||
this.$$compose();
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name angular.service.$location#hash
|
||||
* @methodOf angular.service.$location
|
||||
*
|
||||
* @description
|
||||
* This method is getter / setter.
|
||||
*
|
||||
* Return hash fragment when called without any parameter.
|
||||
*
|
||||
* Change hash fragment when called with parameter and return `$location`.
|
||||
*
|
||||
* @param {string=} hash New hash fragment
|
||||
* @return {string}
|
||||
*/
|
||||
hash: locationGetterSetter('$$hash', identity),
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name angular.service.$location#replace
|
||||
* @methodOf angular.service.$location
|
||||
*
|
||||
* @description
|
||||
* If called, all changes to $location during current `$digest` will be replacing current history
|
||||
* record, instead of adding new one.
|
||||
*/
|
||||
replace: function() {
|
||||
this.$$replace = true;
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function locationGetter(property) {
|
||||
return function() {
|
||||
return this[property];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function locationGetterSetter(property, preprocess) {
|
||||
return function(value) {
|
||||
if (isUndefined(value))
|
||||
return this[property];
|
||||
|
||||
this[property] = preprocess(value);
|
||||
this.$$compose();
|
||||
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc service
|
||||
* @name angular.service.$location
|
||||
* @requires $browser
|
||||
*
|
||||
* @property {string} href The full URL of the current location.
|
||||
* @property {string} protocol The protocol part of the URL (e.g. http or https).
|
||||
* @property {string} host The host name, ip address or FQDN of the current location.
|
||||
* @property {number} port The port number of the current location (e.g. 80, 443, 8080).
|
||||
* @property {string} path The path of the current location (e.g. /myapp/inbox).
|
||||
* @property {Object.<string|boolean>} search Map of query parameters (e.g. {user:"foo", page:23}).
|
||||
* @property {string} hash The fragment part of the URL of the current location (e.g. #foo).
|
||||
* @property {string} hashPath Similar to `path`, but located in the `hash` fragment
|
||||
* (e.g. ../foo#/some/path => /some/path).
|
||||
* @property {Object.<string|boolean>} hashSearch Similar to `search` but located in `hash`
|
||||
* fragment (e.g. .../foo#/some/path?hashQuery=param => {hashQuery: "param"}).
|
||||
* @requires $browser
|
||||
* @requires $sniffer
|
||||
* @requires $config
|
||||
* @requires $document
|
||||
*
|
||||
* @description
|
||||
* Parses the browser location url and makes it available to your application.
|
||||
* Any changes to the url are reflected into `$location` service and changes to
|
||||
* `$location` are reflected in the browser location url.
|
||||
* The $location service parses the URL in the browser address bar (based on the {@link https://developer.mozilla.org/en/window.location window.location}) and makes the URL available to your application. Changes to the URL in the address bar are reflected into $location service and changes to $location are reflected into the browser address bar.
|
||||
*
|
||||
* Notice that using browser's forward/back buttons changes the $location.
|
||||
* **The $location service:**
|
||||
*
|
||||
* @example
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<div ng:init="$location = $service('$location')">
|
||||
<a id="ex-test" href="#myPath?name=misko">test hash</a>|
|
||||
<a id="ex-reset" href="#!/api/angular.service.$location">reset hash</a><br/>
|
||||
<input type='text' name="$location.hash" size="30">
|
||||
<pre>$location = {{$location}}</pre>
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should initialize the input field', function() {
|
||||
expect(using('.doc-example-live').input('$location.hash').val()).
|
||||
toBe('!/api/angular.service.$location');
|
||||
});
|
||||
|
||||
|
||||
it('should bind $location.hash to the input field', function() {
|
||||
using('.doc-example-live').input('$location.hash').enter('foo');
|
||||
expect(browser().location().hash()).toBe('foo');
|
||||
});
|
||||
|
||||
|
||||
it('should set the hash to a test string with test link is presed', function() {
|
||||
using('.doc-example-live').element('#ex-test').click();
|
||||
expect(using('.doc-example-live').input('$location.hash').val()).
|
||||
toBe('myPath?name=misko');
|
||||
});
|
||||
|
||||
it('should reset $location when reset link is pressed', function() {
|
||||
using('.doc-example-live').input('$location.hash').enter('foo');
|
||||
using('.doc-example-live').element('#ex-reset').click();
|
||||
expect(using('.doc-example-live').input('$location.hash').val()).
|
||||
toBe('!/api/angular.service.$location');
|
||||
});
|
||||
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
* - Exposes the current URL in the browser address bar, so you can
|
||||
* - Watch and observe the URL.
|
||||
* - Change the URL.
|
||||
* - Synchronizes the URL with the browser when the user
|
||||
* - Changes the address bar.
|
||||
* - Clicks the back or forward button (or clicks a History link).
|
||||
* - Clicks on a link.
|
||||
* - Represents the URL object as a set of methods (protocol, host, port, path, search, hash).
|
||||
*
|
||||
* For more information see {@link guide/dev_guide.services.$location Developer Guide: Angular Services: Using $location}
|
||||
*/
|
||||
angularServiceInject("$location", function($browser) {
|
||||
var location = {update: update, updateHash: updateHash};
|
||||
var lastLocation = {}; // last state since last update().
|
||||
angularServiceInject('$location', function($browser, $sniffer, $config, $document) {
|
||||
var scope = this, currentUrl,
|
||||
basePath = $browser.baseHref() || '/',
|
||||
pathPrefix = pathPrefixFromBase(basePath),
|
||||
hashPrefix = $config.hashPrefix || '',
|
||||
initUrl = $browser.url();
|
||||
|
||||
$browser.onUrlChange(bind(this, this.$apply, function() { //register
|
||||
update($browser.url());
|
||||
}))(); //initialize
|
||||
|
||||
this.$watch(sync);
|
||||
|
||||
return location;
|
||||
|
||||
// PUBLIC METHODS
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc method
|
||||
* @name angular.service.$location#update
|
||||
* @methodOf angular.service.$location
|
||||
*
|
||||
* @description
|
||||
* Updates the location object.
|
||||
* Does not immediately update the browser
|
||||
* Browser is updated at the end of $digest()
|
||||
*
|
||||
* Does not immediately update the browser. Instead the browser is updated at the end of $eval()
|
||||
* cycle.
|
||||
*
|
||||
* <pre>
|
||||
$location.update('http://www.angularjs.org/path#hash?search=x');
|
||||
$location.update({host: 'www.google.com', protocol: 'https'});
|
||||
$location.update({hashPath: '/path', hashSearch: {a: 'b', x: true}});
|
||||
</pre>
|
||||
*
|
||||
* @param {string|Object} href Full href as a string or object with properties
|
||||
*/
|
||||
function update(href) {
|
||||
if (isString(href)) {
|
||||
extend(location, parseHref(href));
|
||||
if ($config.html5Mode) {
|
||||
if ($sniffer.history) {
|
||||
currentUrl = new LocationUrl(convertToHtml5Url(initUrl, basePath, hashPrefix), pathPrefix);
|
||||
} else {
|
||||
if (isDefined(href.hash)) {
|
||||
extend(href, isString(href.hash) ? parseHash(href.hash) : href.hash);
|
||||
}
|
||||
|
||||
extend(location, href);
|
||||
|
||||
if (isDefined(href.hashPath || href.hashSearch)) {
|
||||
location.hash = composeHash(location);
|
||||
}
|
||||
|
||||
location.href = composeHref(location);
|
||||
}
|
||||
$browser.url(location.href);
|
||||
copy(location, lastLocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @workInProgress
|
||||
* @ngdoc method
|
||||
* @name angular.service.$location#updateHash
|
||||
* @methodOf angular.service.$location
|
||||
*
|
||||
* @description
|
||||
* Updates the hash fragment part of the url.
|
||||
*
|
||||
* @see update()
|
||||
*
|
||||
* <pre>
|
||||
scope.$location.updateHash('/hp')
|
||||
==> update({hashPath: '/hp'})
|
||||
scope.$location.updateHash({a: true, b: 'val'})
|
||||
==> update({hashSearch: {a: true, b: 'val'}})
|
||||
scope.$location.updateHash('/hp', {a: true})
|
||||
==> update({hashPath: '/hp', hashSearch: {a: true}})
|
||||
</pre>
|
||||
*
|
||||
* @param {string|Object} path A hashPath or hashSearch object
|
||||
* @param {Object=} search A hashSearch object
|
||||
*/
|
||||
function updateHash(path, search) {
|
||||
var hash = {};
|
||||
|
||||
if (isString(path)) {
|
||||
hash.hashPath = path;
|
||||
hash.hashSearch = search || {};
|
||||
} else
|
||||
hash.hashSearch = path;
|
||||
|
||||
hash.hash = composeHash(hash);
|
||||
|
||||
update({hash: hash});
|
||||
}
|
||||
|
||||
|
||||
// INNER METHODS
|
||||
|
||||
/**
|
||||
* Synchronizes all location object properties.
|
||||
*
|
||||
* User is allowed to change properties, so after property change,
|
||||
* location object is not in consistent state.
|
||||
*
|
||||
* Properties are synced with the following precedence order:
|
||||
*
|
||||
* - `$location.href`
|
||||
* - `$location.hash`
|
||||
* - everything else
|
||||
*
|
||||
* Keep in mind that if the following code is executed:
|
||||
*
|
||||
* scope.$location.href = 'http://www.angularjs.org/path#a/b'
|
||||
*
|
||||
* immediately afterwards all other properties are still the old ones...
|
||||
*
|
||||
* This method checks the changes and update location to the consistent state
|
||||
*/
|
||||
function sync() {
|
||||
if (!equals(location, lastLocation)) {
|
||||
if (location.href != lastLocation.href) {
|
||||
update(location.href);
|
||||
} else {
|
||||
if (location.hash != lastLocation.hash) {
|
||||
var hash = parseHash(location.hash);
|
||||
updateHash(hash.hashPath, hash.hashSearch);
|
||||
} else {
|
||||
location.hash = composeHash(location);
|
||||
location.href = composeHref(location);
|
||||
}
|
||||
update(location.href);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compose href string from a location object
|
||||
*
|
||||
* @param {Object} loc The location object with all properties
|
||||
* @return {string} Composed href
|
||||
*/
|
||||
function composeHref(loc) {
|
||||
var url = toKeyValue(loc.search);
|
||||
var port = (loc.port == DEFAULT_PORTS[loc.protocol] ? null : loc.port);
|
||||
|
||||
return loc.protocol + '://' + loc.host +
|
||||
(port ? ':' + port : '') + loc.path +
|
||||
(url ? '?' + url : '') + (loc.hash ? '#' + loc.hash : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose hash string from location object
|
||||
*
|
||||
* @param {Object} loc Object with hashPath and hashSearch properties
|
||||
* @return {string} Hash string
|
||||
*/
|
||||
function composeHash(loc) {
|
||||
var hashSearch = toKeyValue(loc.hashSearch);
|
||||
//TODO: temporary fix for issue #158
|
||||
return escape(loc.hashPath).replace(/%21/gi, '!').replace(/%3A/gi, ':').replace(/%24/gi, '$') +
|
||||
(hashSearch ? '?' + hashSearch : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse href string into location object
|
||||
*
|
||||
* @param {string} href
|
||||
* @return {Object} The location object
|
||||
*/
|
||||
function parseHref(href) {
|
||||
var loc = {};
|
||||
var match = URL_MATCH.exec(href);
|
||||
|
||||
if (match) {
|
||||
loc.href = href.replace(/#$/, '');
|
||||
loc.protocol = match[1];
|
||||
loc.host = match[3] || '';
|
||||
loc.port = match[5] || DEFAULT_PORTS[loc.protocol] || null;
|
||||
loc.path = match[6] || '';
|
||||
loc.search = parseKeyValue(match[8]);
|
||||
loc.hash = match[10] || '';
|
||||
|
||||
extend(loc, parseHash(loc.hash));
|
||||
currentUrl = new LocationHashbangUrl(convertToHashbangUrl(initUrl, basePath, hashPrefix),
|
||||
hashPrefix);
|
||||
}
|
||||
|
||||
return loc;
|
||||
// link rewriting
|
||||
var u = currentUrl,
|
||||
absUrlPrefix = composeProtocolHostPort(u.protocol(), u.host(), u.port()) + pathPrefix;
|
||||
|
||||
$document.bind('click', function(event) {
|
||||
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
|
||||
// currently we open nice url link and redirect then
|
||||
|
||||
if (uppercase(event.target.nodeName) != 'A' || event.ctrlKey || event.which == 2) return;
|
||||
|
||||
var elm = jqLite(event.target),
|
||||
href = elm.attr('href');
|
||||
|
||||
if (!href || isDefined(elm.attr('ng:ext-link')) || elm.attr('target')) return;
|
||||
|
||||
// remove same domain from full url links (IE7 always returns full hrefs)
|
||||
href = href.replace(absUrlPrefix, '');
|
||||
|
||||
// link to different domain (or base path)
|
||||
if (href.substr(0, 4) == 'http') return;
|
||||
|
||||
// remove pathPrefix from absolute links
|
||||
href = href.indexOf(pathPrefix) === 0 ? href.substr(pathPrefix.length) : href;
|
||||
|
||||
currentUrl.url(href);
|
||||
scope.$apply();
|
||||
event.preventDefault();
|
||||
});
|
||||
} else {
|
||||
currentUrl = new LocationHashbangUrl(initUrl, hashPrefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse hash string into object
|
||||
*
|
||||
* @param {string} hash
|
||||
*/
|
||||
function parseHash(hash) {
|
||||
var h = {};
|
||||
var match = HASH_MATCH.exec(hash);
|
||||
// rewrite hashbang url <> html5 url
|
||||
if (currentUrl.absUrl() != initUrl) {
|
||||
$browser.url(currentUrl.absUrl(), true);
|
||||
}
|
||||
|
||||
if (match) {
|
||||
h.hash = hash;
|
||||
h.hashPath = unescape(match[1] || '');
|
||||
h.hashSearch = parseKeyValue(match[3]);
|
||||
// update $location when $browser url changes
|
||||
$browser.onUrlChange(function(newUrl) {
|
||||
if (currentUrl.absUrl() != newUrl) {
|
||||
currentUrl.$$parse(newUrl);
|
||||
scope.$apply();
|
||||
}
|
||||
});
|
||||
|
||||
// update browser
|
||||
var changeCounter = 0;
|
||||
scope.$watch(function() {
|
||||
if ($browser.url() != currentUrl.absUrl()) {
|
||||
changeCounter++;
|
||||
scope.$evalAsync(function() {
|
||||
$browser.url(currentUrl.absUrl(), currentUrl.$$replace);
|
||||
currentUrl.$$replace = false;
|
||||
});
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
}, ['$browser']);
|
||||
return changeCounter;
|
||||
});
|
||||
|
||||
return currentUrl;
|
||||
}, ['$browser', '$sniffer', '$locationConfig', '$document']);
|
||||
|
||||
|
||||
angular.service('$locationConfig', function() {
|
||||
return {
|
||||
html5Mode: false,
|
||||
hashPrefix: ''
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,251 +1,463 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Create jasmine.Spy on given method, but ignore calls without arguments
|
||||
* This is helpful when need to spy only setter methods and ignore getters
|
||||
*/
|
||||
function spyOnlyCallsWithArgs(obj, method) {
|
||||
var spy = spyOn(obj, method);
|
||||
obj[method] = function() {
|
||||
if (arguments.length) return spy.apply(this, arguments);
|
||||
return spy.originalValue.apply(this);
|
||||
};
|
||||
return spy;
|
||||
}
|
||||
|
||||
|
||||
describe('$location', function() {
|
||||
var scope, $location, $browser;
|
||||
var url;
|
||||
|
||||
describe('NewUrl', function() {
|
||||
beforeEach(function() {
|
||||
url = new LocationUrl('http://www.domain.com:9877/path/b?search=a&b=c&d#hash');
|
||||
});
|
||||
|
||||
|
||||
it('should provide common getters', function() {
|
||||
expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#hash');
|
||||
expect(url.protocol()).toBe('http');
|
||||
expect(url.host()).toBe('www.domain.com');
|
||||
expect(url.port()).toBe(9877);
|
||||
expect(url.path()).toBe('/path/b');
|
||||
expect(url.search()).toEqual({search: 'a', b: 'c', d: true});
|
||||
expect(url.hash()).toBe('hash');
|
||||
expect(url.url()).toBe('/path/b?search=a&b=c&d#hash');
|
||||
});
|
||||
|
||||
|
||||
it('path() should change path', function() {
|
||||
url.path('/new/path');
|
||||
expect(url.path()).toBe('/new/path');
|
||||
expect(url.absUrl()).toBe('http://www.domain.com:9877/new/path?search=a&b=c&d#hash');
|
||||
});
|
||||
|
||||
|
||||
it('search() should accept string', function() {
|
||||
url.search('x=y&c');
|
||||
expect(url.search()).toEqual({x: 'y', c: true});
|
||||
expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?x=y&c#hash');
|
||||
});
|
||||
|
||||
|
||||
it('search() should accept object', function() {
|
||||
url.search({one: 1, two: true});
|
||||
expect(url.search()).toEqual({one: 1, two: true});
|
||||
expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?one=1&two#hash');
|
||||
});
|
||||
|
||||
|
||||
it('search() should change single parameter', function() {
|
||||
url.search({id: 'old', preserved: true});
|
||||
url.search('id', 'new');
|
||||
|
||||
expect(url.search()).toEqual({id: 'new', preserved: true});
|
||||
});
|
||||
|
||||
|
||||
it('search() should remove single parameter', function() {
|
||||
url.search({id: 'old', preserved: true});
|
||||
url.search('id', null);
|
||||
|
||||
expect(url.search()).toEqual({preserved: true});
|
||||
});
|
||||
|
||||
|
||||
it('hash() should change hash fragment', function() {
|
||||
url.hash('new-hash');
|
||||
expect(url.hash()).toBe('new-hash');
|
||||
expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#new-hash');
|
||||
});
|
||||
|
||||
|
||||
it('url() should change the path, search and hash', function() {
|
||||
url.url('/some/path?a=b&c=d#hhh');
|
||||
expect(url.url()).toBe('/some/path?a=b&c=d#hhh');
|
||||
expect(url.absUrl()).toBe('http://www.domain.com:9877/some/path?a=b&c=d#hhh');
|
||||
expect(url.path()).toBe('/some/path');
|
||||
expect(url.search()).toEqual({a: 'b', c: 'd'});
|
||||
expect(url.hash()).toBe('hhh');
|
||||
});
|
||||
|
||||
|
||||
it('replace should set $$replace flag and return itself', function() {
|
||||
expect(url.$$replace).toBe(false);
|
||||
|
||||
url.replace();
|
||||
expect(url.$$replace).toBe(true);
|
||||
expect(url.replace()).toBe(url);
|
||||
});
|
||||
|
||||
|
||||
it('should parse new url', function() {
|
||||
url = new LocationUrl('http://host.com/base');
|
||||
expect(url.path()).toBe('/base');
|
||||
|
||||
url = new LocationUrl('http://host.com/base#');
|
||||
expect(url.path()).toBe('/base');
|
||||
});
|
||||
|
||||
|
||||
it('should prefix path with forward-slash', function() {
|
||||
url = new LocationUrl('http://server/a');
|
||||
url.path('b');
|
||||
|
||||
expect(url.path()).toBe('/b');
|
||||
expect(url.absUrl()).toBe('http://server/b');
|
||||
});
|
||||
|
||||
|
||||
it('should set path to forward-slash when empty', function() {
|
||||
url = new LocationUrl('http://server');
|
||||
expect(url.path()).toBe('/');
|
||||
expect(url.absUrl()).toBe('http://server/');
|
||||
});
|
||||
|
||||
|
||||
it('setters should return Url object to allow chaining', function() {
|
||||
expect(url.path('/any')).toBe(url);
|
||||
expect(url.search('')).toBe(url);
|
||||
expect(url.hash('aaa')).toBe(url);
|
||||
expect(url.url('/some')).toBe(url);
|
||||
});
|
||||
|
||||
|
||||
it('should not preserve old properties when parsing new url', function() {
|
||||
url.$$parse('http://www.domain.com:9877/a');
|
||||
|
||||
expect(url.path()).toBe('/a');
|
||||
expect(url.search()).toEqual({});
|
||||
expect(url.hash()).toBe('');
|
||||
expect(url.absUrl()).toBe('http://www.domain.com:9877/a');
|
||||
});
|
||||
|
||||
|
||||
it('should prepend path with basePath', function() {
|
||||
url = new LocationUrl('http://server/base/abc?a', '/base');
|
||||
expect(url.path()).toBe('/abc');
|
||||
expect(url.search()).toEqual({a: true});
|
||||
|
||||
url.path('/new/path');
|
||||
expect(url.absUrl()).toBe('http://server/base/new/path?a');
|
||||
});
|
||||
|
||||
|
||||
it('should throw error when invalid url given', function() {
|
||||
url = new LocationUrl('http://server.org/base/abc', '/base');
|
||||
|
||||
expect(function() {
|
||||
url.$$parse('http://server.org/path#/path');
|
||||
}).toThrow('Invalid url "http://server.org/path#/path", missing path prefix "/base" !');
|
||||
});
|
||||
|
||||
|
||||
describe('encoding', function() {
|
||||
|
||||
it('should encode special characters', function() {
|
||||
url.path('/a <>#');
|
||||
url.search({'i j': '<>#'});
|
||||
url.hash('<>#');
|
||||
|
||||
expect(url.path()).toBe('/a <>#');
|
||||
expect(url.search()).toEqual({'i j': '<>#'});
|
||||
expect(url.hash()).toBe('<>#');
|
||||
expect(url.absUrl()).toBe('http://www.domain.com:9877/a%20%3C%3E%23?i%20j=%3C%3E%23#%3C%3E%23');
|
||||
});
|
||||
|
||||
|
||||
it('should not encode !$:@', function() {
|
||||
url.path('/!$:@');
|
||||
url.search('');
|
||||
url.hash('!$:@');
|
||||
|
||||
expect(url.absUrl()).toBe('http://www.domain.com:9877/!$:@#!$:@');
|
||||
});
|
||||
|
||||
|
||||
it('should decode special characters', function() {
|
||||
url = new LocationUrl('http://host.com/a%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23');
|
||||
expect(url.path()).toBe('/a <>#');
|
||||
expect(url.search()).toEqual({'i j': '<>#'});
|
||||
expect(url.hash()).toBe('x <>#');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('HashbangUrl', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
url = new LocationHashbangUrl('http://www.server.org:1234/base#!/path?a=b&c#hash', '!');
|
||||
});
|
||||
|
||||
|
||||
it('should parse hashband url into path and search', function() {
|
||||
expect(url.protocol()).toBe('http');
|
||||
expect(url.host()).toBe('www.server.org');
|
||||
expect(url.port()).toBe(1234);
|
||||
expect(url.path()).toBe('/path');
|
||||
expect(url.search()).toEqual({a: 'b', c: true});
|
||||
expect(url.hash()).toBe('hash');
|
||||
});
|
||||
|
||||
|
||||
it('absUrl() should return hashbang url', function() {
|
||||
expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/path?a=b&c#hash');
|
||||
|
||||
url.path('/new/path');
|
||||
url.search({one: 1});
|
||||
url.hash('hhh');
|
||||
expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/new/path?one=1#hhh');
|
||||
});
|
||||
|
||||
|
||||
it('should preserve query params in base', function() {
|
||||
url = new LocationHashbangUrl('http://www.server.org:1234/base?base=param#/path?a=b&c#hash', '');
|
||||
expect(url.absUrl()).toBe('http://www.server.org:1234/base?base=param#/path?a=b&c#hash');
|
||||
|
||||
url.path('/new/path');
|
||||
url.search({one: 1});
|
||||
url.hash('hhh');
|
||||
expect(url.absUrl()).toBe('http://www.server.org:1234/base?base=param#/new/path?one=1#hhh');
|
||||
});
|
||||
|
||||
|
||||
it('should prefix path with forward-slash', function() {
|
||||
url = new LocationHashbangUrl('http://host.com/base#path', '');
|
||||
expect(url.path()).toBe('/path');
|
||||
expect(url.absUrl()).toBe('http://host.com/base#/path');
|
||||
|
||||
url.path('wrong');
|
||||
expect(url.path()).toBe('/wrong');
|
||||
expect(url.absUrl()).toBe('http://host.com/base#/wrong');
|
||||
});
|
||||
|
||||
|
||||
it('should set path to forward-slash when empty', function() {
|
||||
url = new LocationHashbangUrl('http://server/base#!', '!');
|
||||
url.path('aaa');
|
||||
|
||||
expect(url.path()).toBe('/aaa');
|
||||
expect(url.absUrl()).toBe('http://server/base#!/aaa');
|
||||
});
|
||||
|
||||
|
||||
it('should not preserve old properties when parsing new url', function() {
|
||||
url.$$parse('http://www.server.org:1234/base#!/');
|
||||
|
||||
expect(url.path()).toBe('/');
|
||||
expect(url.search()).toEqual({});
|
||||
expect(url.hash()).toBe('');
|
||||
expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/');
|
||||
});
|
||||
|
||||
|
||||
it('should throw error when invalid url given', function() {
|
||||
expect(function() {
|
||||
url.$$parse('http://server.org/path#/path');
|
||||
}).toThrow('Invalid url "http://server.org/path#/path", missing hash prefix "!" !');
|
||||
});
|
||||
|
||||
|
||||
describe('encoding', function() {
|
||||
|
||||
it('should encode special characters', function() {
|
||||
url.path('/a <>#');
|
||||
url.search({'i j': '<>#'});
|
||||
url.hash('<>#');
|
||||
|
||||
expect(url.path()).toBe('/a <>#');
|
||||
expect(url.search()).toEqual({'i j': '<>#'});
|
||||
expect(url.hash()).toBe('<>#');
|
||||
expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/a%20%3C%3E%23?i%20j=%3C%3E%23#%3C%3E%23');
|
||||
});
|
||||
|
||||
|
||||
it('should not encode !$:@', function() {
|
||||
url.path('/!$:@');
|
||||
url.search('');
|
||||
url.hash('!$:@');
|
||||
|
||||
expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/!$:@#!$:@');
|
||||
});
|
||||
|
||||
|
||||
it('should decode special characters', function() {
|
||||
url = new LocationHashbangUrl('http://host.com/a#/%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23', '');
|
||||
expect(url.path()).toBe('/ <>#');
|
||||
expect(url.search()).toEqual({'i j': '<>#'});
|
||||
expect(url.hash()).toBe('x <>#');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
var $browser, $location, scope;
|
||||
|
||||
function init(url, html5Mode, basePath, hashPrefix, supportHistory) {
|
||||
scope = angular.scope(null, {
|
||||
$locationConfig: {html5Mode: html5Mode, hashPrefix: hashPrefix},
|
||||
$sniffer: {history: supportHistory}});
|
||||
|
||||
beforeEach(function(){
|
||||
scope = angular.scope();
|
||||
$location = scope.$service('$location');
|
||||
$browser = scope.$service('$browser');
|
||||
});
|
||||
$browser.url(url);
|
||||
$browser.$$baseHref = basePath;
|
||||
$location = scope.$service('$location');
|
||||
}
|
||||
|
||||
function dealocRootElement() {
|
||||
dealoc(scope.$service('$document'));
|
||||
}
|
||||
|
||||
|
||||
afterEach(function(){
|
||||
dealoc(scope);
|
||||
});
|
||||
describe('wiring', function() {
|
||||
|
||||
|
||||
it("should update location object immediately when update is called", function() {
|
||||
var href = 'http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2=';
|
||||
$location.update(href);
|
||||
expect($location.href).toEqual(href);
|
||||
expect($location.protocol).toEqual("http");
|
||||
expect($location.host).toEqual("host");
|
||||
expect($location.port).toEqual("123");
|
||||
expect($location.path).toEqual("/p/a/t/h.html");
|
||||
expect($location.search).toEqual({query:'value'});
|
||||
expect($location.hash).toEqual('path?key=value&flag&key2=');
|
||||
expect($location.hashPath).toEqual('path');
|
||||
expect($location.hashSearch).toEqual({key: 'value', flag: true, key2: ''});
|
||||
});
|
||||
|
||||
|
||||
it('should update location when browser url changed', function() {
|
||||
var origUrl = $location.href;
|
||||
expect(origUrl).toEqual($browser.url());
|
||||
|
||||
var newUrl = 'http://somenew/url#foo';
|
||||
$browser.url(newUrl);
|
||||
$browser.poll();
|
||||
expect($location.href).toEqual(newUrl);
|
||||
});
|
||||
|
||||
|
||||
it('should update browser at the end of $eval', function() {
|
||||
var origBrowserUrl = $browser.url();
|
||||
$location.update('http://www.angularjs.org/');
|
||||
$location.update({path: '/a/b'});
|
||||
expect($location.href).toEqual('http://www.angularjs.org/a/b');
|
||||
expect($browser.url()).toEqual('http://www.angularjs.org/a/b');
|
||||
$location.path = '/c';
|
||||
scope.$digest();
|
||||
expect($browser.url()).toEqual('http://www.angularjs.org/c');
|
||||
});
|
||||
|
||||
|
||||
it('should update hashPath and hashSearch on hash update', function(){
|
||||
$location.update('http://server/#path?a=b');
|
||||
expect($location.hashPath).toEqual('path');
|
||||
expect($location.hashSearch).toEqual({a:'b'});
|
||||
|
||||
$location.update({hash: ''});
|
||||
expect($location.hashPath).toEqual('');
|
||||
expect($location.hashSearch).toEqual({});
|
||||
});
|
||||
|
||||
|
||||
it('should update hash on hashPath or hashSearch update', function() {
|
||||
$location.update('http://server/#path?a=b');
|
||||
scope.$digest();
|
||||
$location.update({hashPath: '', hashSearch: {}});
|
||||
|
||||
expect($location.hash).toEqual('');
|
||||
});
|
||||
|
||||
|
||||
it('should update hashPath and hashSearch on $location.hash change upon eval', function(){
|
||||
$location.update('http://server/#path?a=b');
|
||||
scope.$digest();
|
||||
|
||||
$location.hash = '';
|
||||
scope.$digest();
|
||||
|
||||
expect($location.href).toEqual('http://server/');
|
||||
expect($location.hashPath).toEqual('');
|
||||
expect($location.hashSearch).toEqual({});
|
||||
});
|
||||
|
||||
|
||||
it('should update hash on $location.hashPath or $location.hashSearch change upon eval',
|
||||
function() {
|
||||
$location.update('http://server/#path?a=b');
|
||||
expect($location.href).toEqual('http://server/#path?a=b');
|
||||
expect($location.hashPath).toEqual('path');
|
||||
expect($location.hashSearch).toEqual({a:'b'});
|
||||
|
||||
$location.hashPath = '';
|
||||
$location.hashSearch = {};
|
||||
scope.$digest();
|
||||
|
||||
expect($location.href).toEqual('http://server/');
|
||||
expect($location.hash).toEqual('');
|
||||
});
|
||||
|
||||
|
||||
it('should sync $location upon eval before watches are fired', function(){
|
||||
scope.$location = scope.$service('$location'); //publish to the scope for $watch
|
||||
|
||||
var log = '';
|
||||
scope.$watch('$location.hash', function(scope){
|
||||
log += scope.$location.hashPath + ';';
|
||||
});
|
||||
expect(log).toEqual('');
|
||||
scope.$digest();
|
||||
expect(log).toEqual(';');
|
||||
|
||||
log = '';
|
||||
scope.$location.hash = '/abc';
|
||||
scope.$digest();
|
||||
expect(scope.$location.hash).toEqual('/abc');
|
||||
expect(log).toEqual('/abc;');
|
||||
});
|
||||
|
||||
|
||||
describe('sync', function() {
|
||||
|
||||
it('should update hash with escaped hashPath', function() {
|
||||
$location.hashPath = 'foo=bar';
|
||||
scope.$digest();
|
||||
expect($location.hash).toBe('foo%3Dbar');
|
||||
beforeEach(function() {
|
||||
init('http://new.com/a/b#!', false, '/a/b', '!', true);
|
||||
});
|
||||
|
||||
|
||||
it('should give $location.href the highest precedence', function() {
|
||||
$location.hashPath = 'hashPath';
|
||||
$location.hashSearch = {hash:'search'};
|
||||
$location.hash = 'hash';
|
||||
$location.port = '333';
|
||||
$location.host = 'host';
|
||||
$location.href = 'https://hrefhost:23/hrefpath';
|
||||
it('should update $location when browser url changes', function() {
|
||||
spyOn($location, '$$parse').andCallThrough();
|
||||
$browser.url('http://new.com/a/b#!/aaa');
|
||||
$browser.poll();
|
||||
expect($location.absUrl()).toBe('http://new.com/a/b#!/aaa');
|
||||
expect($location.path()).toBe('/aaa');
|
||||
expect($location.$$parse).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
|
||||
it('should update browser when $location changes', function() {
|
||||
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
|
||||
$location.path('/new/path');
|
||||
expect($browserUrl).not.toHaveBeenCalled();
|
||||
scope.$apply();
|
||||
|
||||
expect($browserUrl).toHaveBeenCalledOnce();
|
||||
expect($browser.url()).toBe('http://new.com/a/b#!/new/path');
|
||||
});
|
||||
|
||||
|
||||
it('should update browser only once per $apply cycle', function() {
|
||||
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
|
||||
$location.path('/new/path');
|
||||
|
||||
scope.$watch(function() {
|
||||
$location.search('a=b');
|
||||
});
|
||||
|
||||
scope.$apply();
|
||||
expect($browserUrl).toHaveBeenCalledOnce();
|
||||
expect($browser.url()).toBe('http://new.com/a/b#!/new/path?a=b');
|
||||
});
|
||||
|
||||
|
||||
it('should replace browser url when url was replaced at least once', function() {
|
||||
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
|
||||
$location.path('/n/url').replace();
|
||||
scope.$apply();
|
||||
|
||||
expect($browserUrl).toHaveBeenCalledOnce();
|
||||
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b#!/n/url', true]);
|
||||
});
|
||||
|
||||
|
||||
it('should update the browser if changed from within a watcher', function() {
|
||||
scope.$watch(function() { return true; }, function() {
|
||||
$location.path('/changed');
|
||||
});
|
||||
|
||||
scope.$digest();
|
||||
|
||||
expect($location).toEqualData({href: 'https://hrefhost:23/hrefpath',
|
||||
protocol: 'https',
|
||||
host: 'hrefhost',
|
||||
port: '23',
|
||||
path: '/hrefpath',
|
||||
search: {},
|
||||
hash: '',
|
||||
hashPath: '',
|
||||
hashSearch: {}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should give $location.hash second highest precedence', function() {
|
||||
$location.hashPath = 'hashPath';
|
||||
$location.hashSearch = {hash:'search'};
|
||||
$location.hash = 'hash';
|
||||
$location.port = '333';
|
||||
$location.host = 'host';
|
||||
$location.path = '/path';
|
||||
|
||||
scope.$digest();
|
||||
|
||||
expect($location).toEqualData({href: 'http://host:333/path#hash',
|
||||
protocol: 'http',
|
||||
host: 'host',
|
||||
port: '333',
|
||||
path: '/path',
|
||||
search: {},
|
||||
hash: 'hash',
|
||||
hashPath: 'hash',
|
||||
hashSearch: {}
|
||||
});
|
||||
expect($browser.url()).toBe('http://new.com/a/b#!/changed');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('update()', function() {
|
||||
// html5 history is disabled
|
||||
describe('disabled history', function() {
|
||||
|
||||
it('should accept hash object and update only given properties', function() {
|
||||
$location.update("http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2=");
|
||||
$location.update({host: 'new', port: 24});
|
||||
|
||||
expect($location.host).toEqual('new');
|
||||
expect($location.port).toEqual(24);
|
||||
expect($location.protocol).toEqual('http');
|
||||
expect($location.href).toEqual("http://new:24/p/a/t/h.html?query=value#path?key=value&flag&key2=");
|
||||
it('should use hashbang url with hash prefix', function() {
|
||||
init('http://domain.com/base/index.html#!/a/b', false, '/base/index.html', '!');
|
||||
expect($browser.url()).toBe('http://domain.com/base/index.html#!/a/b');
|
||||
$location.path('/new');
|
||||
$location.search({a: true});
|
||||
scope.$apply();
|
||||
expect($browser.url()).toBe('http://domain.com/base/index.html#!/new?a');
|
||||
});
|
||||
|
||||
|
||||
it('should remove # if hash is empty', function() {
|
||||
$location.update('http://www.angularjs.org/index.php#');
|
||||
expect($location.href).toEqual('http://www.angularjs.org/index.php');
|
||||
});
|
||||
|
||||
|
||||
it('should clear hash when updating to hash-less URL', function() {
|
||||
$location.update('http://server');
|
||||
expect($location.href).toBe('http://server');
|
||||
expect($location.hash).toBe('');
|
||||
it('should use hashbang url without hash prefix', function() {
|
||||
init('http://domain.com/base/index.html#/a/b', false, '/base/index.html', '');
|
||||
expect($browser.url()).toBe('http://domain.com/base/index.html#/a/b');
|
||||
$location.path('/new');
|
||||
$location.search({a: true});
|
||||
scope.$apply();
|
||||
expect($browser.url()).toBe('http://domain.com/base/index.html#/new?a');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('updateHash()', function() {
|
||||
// html5 history enabled, but not supported by browser
|
||||
describe('history on old browser', function() {
|
||||
|
||||
it('should accept single string argument to update path', function() {
|
||||
$location.updateHash('path');
|
||||
expect($location.hash).toEqual('path');
|
||||
expect($location.hashPath).toEqual('path');
|
||||
afterEach(dealocRootElement);
|
||||
|
||||
it('should use hashbang url with hash prefix', function() {
|
||||
init('http://domain.com/base/index.html#!!/a/b', true, '/base/index.html', '!!', false);
|
||||
expect($browser.url()).toBe('http://domain.com/base/index.html#!!/a/b');
|
||||
$location.path('/new');
|
||||
$location.search({a: true});
|
||||
scope.$apply();
|
||||
expect($browser.url()).toBe('http://domain.com/base/index.html#!!/new?a');
|
||||
});
|
||||
|
||||
|
||||
it('should reset hashSearch when updating with a single string', function() {
|
||||
$location.updateHash({foo:'bar'}); //set some initial state for hashSearch
|
||||
it('should redirect to hashbang url when new url given', function() {
|
||||
init('http://domain.com/base/new-path/index.html', true, '/base/index.html', '!');
|
||||
expect($browser.url()).toBe('http://domain.com/base/index.html#!/new-path/index.html');
|
||||
});
|
||||
});
|
||||
|
||||
$location.updateHash('path');
|
||||
expect($location.hashPath).toEqual('path');
|
||||
expect($location.hashSearch).toEqual({});
|
||||
|
||||
// html5 history enabled and supported by browser
|
||||
describe('history on new browser', function() {
|
||||
|
||||
afterEach(dealocRootElement);
|
||||
|
||||
it('should use new url', function() {
|
||||
init('http://domain.com/base/old/index.html#a', true, '/base/index.html', '', true);
|
||||
expect($browser.url()).toBe('http://domain.com/base/old/index.html#a');
|
||||
$location.path('/new');
|
||||
$location.search({a: true});
|
||||
scope.$apply();
|
||||
expect($browser.url()).toBe('http://domain.com/base/new?a#a');
|
||||
});
|
||||
|
||||
|
||||
it('should accept single object argument to update search', function() {
|
||||
$location.updateHash({a: 'b'});
|
||||
expect($location.hash).toEqual('?a=b');
|
||||
expect($location.hashSearch).toEqual({a: 'b'});
|
||||
it('should rewrite when hashbang url given', function() {
|
||||
init('http://domain.com/base/index.html#!/a/b', true, '/base/index.html', '!', true);
|
||||
expect($browser.url()).toBe('http://domain.com/base/a/b');
|
||||
$location.path('/new');
|
||||
$location.hash('abc');
|
||||
scope.$apply();
|
||||
expect($browser.url()).toBe('http://domain.com/base/new#abc');
|
||||
expect($location.path()).toBe('/new');
|
||||
});
|
||||
|
||||
|
||||
it('should accept path string and search object arguments to update both', function() {
|
||||
$location.updateHash('path', {a: 'b'});
|
||||
expect($location.hash).toEqual('path?a=b');
|
||||
expect($location.hashSearch).toEqual({a: 'b'});
|
||||
expect($location.hashPath).toEqual('path');
|
||||
});
|
||||
|
||||
|
||||
it('should update href and hash when updating to empty string', function() {
|
||||
$location.updateHash('');
|
||||
expect($location.href).toBe('http://server');
|
||||
expect($location.hash).toBe('');
|
||||
|
||||
scope.$digest();
|
||||
|
||||
expect($location.href).toBe('http://server');
|
||||
expect($location.hash).toBe('');
|
||||
it('should rewrite when hashbang url given (without hash prefix)', function() {
|
||||
init('http://domain.com/base/index.html#/a/b', true, '/base/index.html', '', true);
|
||||
expect($browser.url()).toBe('http://domain.com/base/a/b');
|
||||
expect($location.path()).toBe('/a/b');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -255,21 +467,21 @@ describe('$location', function() {
|
|||
it('should parse basic url', function() {
|
||||
var match = URL_MATCH.exec('http://www.angularjs.org/path?search#hash?x=x');
|
||||
|
||||
expect(match[1]).toEqual('http');
|
||||
expect(match[3]).toEqual('www.angularjs.org');
|
||||
expect(match[6]).toEqual('/path');
|
||||
expect(match[8]).toEqual('search');
|
||||
expect(match[10]).toEqual('hash?x=x');
|
||||
expect(match[1]).toBe('http');
|
||||
expect(match[3]).toBe('www.angularjs.org');
|
||||
expect(match[6]).toBe('/path');
|
||||
expect(match[8]).toBe('search');
|
||||
expect(match[10]).toBe('hash?x=x');
|
||||
});
|
||||
|
||||
|
||||
it('should parse file://', function(){
|
||||
var match = URL_MATCH.exec('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html');
|
||||
|
||||
expect(match[1]).toEqual('file');
|
||||
expect(match[3]).toEqual('');
|
||||
expect(match[1]).toBe('file');
|
||||
expect(match[3]).toBe('');
|
||||
expect(match[5]).toBeFalsy();
|
||||
expect(match[6]).toEqual('/Users/Shared/misko/work/angular.js/scenario/widgets.html');
|
||||
expect(match[6]).toBe('/Users/Shared/misko/work/angular.js/scenario/widgets.html');
|
||||
expect(match[8]).toBeFalsy();
|
||||
});
|
||||
|
||||
|
|
@ -277,30 +489,170 @@ describe('$location', function() {
|
|||
it('should parse url with "-" in host', function(){
|
||||
var match = URL_MATCH.exec('http://a-b1.c-d.09/path');
|
||||
|
||||
expect(match[1]).toEqual('http');
|
||||
expect(match[3]).toEqual('a-b1.c-d.09');
|
||||
expect(match[1]).toBe('http');
|
||||
expect(match[3]).toBe('a-b1.c-d.09');
|
||||
expect(match[5]).toBeFalsy();
|
||||
expect(match[6]).toEqual('/path');
|
||||
expect(match[6]).toBe('/path');
|
||||
expect(match[8]).toBeFalsy();
|
||||
});
|
||||
|
||||
|
||||
it('should parse host without "/" at the end', function() {
|
||||
var match = URL_MATCH.exec('http://host.org');
|
||||
expect(match[3]).toEqual('host.org');
|
||||
expect(match[3]).toBe('host.org');
|
||||
|
||||
match = URL_MATCH.exec('http://host.org#');
|
||||
expect(match[3]).toEqual('host.org');
|
||||
expect(match[3]).toBe('host.org');
|
||||
|
||||
match = URL_MATCH.exec('http://host.org?');
|
||||
expect(match[3]).toEqual('host.org');
|
||||
expect(match[3]).toBe('host.org');
|
||||
});
|
||||
|
||||
|
||||
it('should match with just "/" path', function() {
|
||||
var match = URL_MATCH.exec('http://server/#?book=moby');
|
||||
|
||||
expect(match[10]).toEqual('?book=moby');
|
||||
expect(match[10]).toBe('?book=moby');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('PATH_MATCH', function() {
|
||||
|
||||
it('should parse just path', function() {
|
||||
var match = PATH_MATCH.exec('/path');
|
||||
expect(match[1]).toBe('/path');
|
||||
});
|
||||
|
||||
|
||||
it('should parse path with search', function() {
|
||||
var match = PATH_MATCH.exec('/ppp/a?a=b&c');
|
||||
expect(match[1]).toBe('/ppp/a');
|
||||
expect(match[3]).toBe('a=b&c');
|
||||
});
|
||||
|
||||
|
||||
it('should parse path with hash', function() {
|
||||
var match = PATH_MATCH.exec('/ppp/a#abc?');
|
||||
expect(match[1]).toBe('/ppp/a');
|
||||
expect(match[5]).toBe('abc?');
|
||||
});
|
||||
|
||||
|
||||
it('should parse path with both search and hash', function() {
|
||||
var match = PATH_MATCH.exec('/ppp/a?a=b&c#abc/d?');
|
||||
expect(match[3]).toBe('a=b&c');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('link rewriting', function() {
|
||||
|
||||
var root, link, extLink, $browser, originalBrowser, lastEventPreventDefault;
|
||||
|
||||
function init(linkHref, html5Mode, supportHist, attrs) {
|
||||
var jqRoot = jqLite('<div></div>');
|
||||
attrs = attrs ? ' ' + attrs + ' ' : '';
|
||||
link = jqLite('<a href="' + linkHref + '"' + attrs + '>link</a>')[0];
|
||||
root = jqRoot.append(link)[0];
|
||||
|
||||
jqLite(document.body).append(jqRoot);
|
||||
|
||||
var scope = angular.scope(null, {
|
||||
$document: jqRoot,
|
||||
$sniffer: {history: supportHist},
|
||||
$locationConfig: {html5Mode: html5Mode, hashPrefix: '!'}
|
||||
});
|
||||
|
||||
$browser = scope.$service('$browser');
|
||||
$browser.url('http://host.com/base');
|
||||
$browser.$$baseHref = '/base/index.html';
|
||||
var $location = scope.$service('$location');
|
||||
originalBrowser = $browser.url();
|
||||
|
||||
// we have to prevent the default operation, as we need to test absolute links (http://...)
|
||||
// and navigating to these links would kill jstd
|
||||
jqRoot.bind('click', function(e) {
|
||||
lastEventPreventDefault = e.isDefaultPrevented();
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
function triggerAndExpectRewriteTo(url) {
|
||||
browserTrigger(link, 'click');
|
||||
expect(lastEventPreventDefault).toBe(true);
|
||||
expect($browser.url()).toBe(url);
|
||||
}
|
||||
|
||||
function triggerAndExpectNoRewrite() {
|
||||
browserTrigger(link, 'click');
|
||||
expect(lastEventPreventDefault).toBe(false);
|
||||
expect($browser.url()).toBe(originalBrowser);
|
||||
}
|
||||
|
||||
afterEach(function() {
|
||||
dealoc(root);
|
||||
dealoc(document.body);
|
||||
});
|
||||
|
||||
|
||||
it('should rewrite rel link to new url when history enabled on new browser', function() {
|
||||
init('link?a#b', true, true);
|
||||
triggerAndExpectRewriteTo('http://host.com/base/link?a#b');
|
||||
});
|
||||
|
||||
|
||||
it('should rewrite abs link to new url when history enabled on new browser', function() {
|
||||
init('/base/link?a#b', true, true);
|
||||
triggerAndExpectRewriteTo('http://host.com/base/link?a#b');
|
||||
});
|
||||
|
||||
|
||||
it('should rewrite rel link to hashbang url when history enabled on old browser', function() {
|
||||
init('link?a#b', true, false);
|
||||
triggerAndExpectRewriteTo('http://host.com/base/index.html#!/link?a#b');
|
||||
});
|
||||
|
||||
|
||||
it('should rewrite abs link to hashbang url when history enabled on old browser', function() {
|
||||
init('/base/link?a#b', true, false);
|
||||
triggerAndExpectRewriteTo('http://host.com/base/index.html#!/link?a#b');
|
||||
});
|
||||
|
||||
|
||||
it('should not rewrite when history disabled', function() {
|
||||
init('#new', false);
|
||||
triggerAndExpectNoRewrite();
|
||||
});
|
||||
|
||||
|
||||
it('should not rewrite ng:ext-link', function() {
|
||||
init('#new', true, true, 'ng:ext-link');
|
||||
triggerAndExpectNoRewrite();
|
||||
});
|
||||
|
||||
|
||||
it('should not rewrite full url links do different domain', function() {
|
||||
init('http://www.dot.abc/a?b=c', true);
|
||||
triggerAndExpectNoRewrite();
|
||||
});
|
||||
|
||||
|
||||
it('should not rewrite links with target="_blank"', function() {
|
||||
init('/a?b=c', true, true, 'target="_blank"');
|
||||
triggerAndExpectNoRewrite();
|
||||
});
|
||||
|
||||
|
||||
it('should not rewrite links with target specified', function() {
|
||||
init('/a?b=c', true, true, 'target="some-frame"');
|
||||
triggerAndExpectNoRewrite();
|
||||
});
|
||||
|
||||
|
||||
it('should rewrite full url links to same domain and base path', function() {
|
||||
init('http://host.com/base/new', true);
|
||||
triggerAndExpectRewriteTo('http://host.com/base/index.html#!/new');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue