mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-17 07:40:22 +00:00
508 lines
14 KiB
JavaScript
508 lines
14 KiB
JavaScript
'use strict';
|
|
|
|
var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/,
|
|
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 mode 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] = encodeUriQuery(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;
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* @ngdoc service
|
|
* @name angular.service.$location
|
|
*
|
|
* @requires $browser
|
|
* @requires $sniffer
|
|
* @requires $locationConfig
|
|
* @requires $document
|
|
*
|
|
* @description
|
|
* 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.
|
|
*
|
|
* **The $location service:**
|
|
*
|
|
* - 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, $sniffer, $locationConfig, $document) {
|
|
var scope = this, currentUrl,
|
|
basePath = $browser.baseHref() || '/',
|
|
pathPrefix = pathPrefixFromBase(basePath),
|
|
hashPrefix = $locationConfig.hashPrefix || '',
|
|
initUrl = $browser.url();
|
|
|
|
if ($locationConfig.html5Mode) {
|
|
if ($sniffer.history) {
|
|
currentUrl = new LocationUrl(convertToHtml5Url(initUrl, basePath, hashPrefix), pathPrefix);
|
|
} else {
|
|
currentUrl = new LocationHashbangUrl(convertToHashbangUrl(initUrl, basePath, hashPrefix),
|
|
hashPrefix);
|
|
}
|
|
|
|
// 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.metaKey ||
|
|
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();
|
|
// hack to work around FF6 bug 684208 when scenario runner clicks on links
|
|
window.angular['ff-684208-preventDefault'] = true;
|
|
});
|
|
} else {
|
|
currentUrl = new LocationHashbangUrl(initUrl, hashPrefix);
|
|
}
|
|
|
|
// rewrite hashbang url <> html5 url
|
|
if (currentUrl.absUrl() != initUrl) {
|
|
$browser.url(currentUrl.absUrl(), true);
|
|
}
|
|
|
|
// 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 changeCounter;
|
|
});
|
|
|
|
return currentUrl;
|
|
}, ['$browser', '$sniffer', '$locationConfig', '$document']);
|
|
|
|
|
|
angular.service('$locationConfig', function() {
|
|
return {
|
|
html5Mode: false,
|
|
hashPrefix: ''
|
|
};
|
|
});
|