mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-20 00:10:26 +00:00
594 lines
16 KiB
JavaScript
594 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
var SERVER_MATCH = /^([^:]+):\/\/(\w+:{0,1}\w*@)?(\{?[\w\.-]*\}?)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/,
|
|
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 = SERVER_MATCH.exec(url);
|
|
|
|
obj.$$protocol = match[1];
|
|
obj.$$host = match[3];
|
|
obj.$$port = int(match[5]) || DEFAULT_PORTS[match[1]] || null;
|
|
}
|
|
|
|
function matchAppUrl(url, obj) {
|
|
var match = PATH_MATCH.exec(url);
|
|
|
|
obj.$$path = decodeURIComponent(match[1]);
|
|
obj.$$search = parseKeyValue(match[3]);
|
|
obj.$$hash = decodeURIComponent(match[5] || '');
|
|
|
|
// make sure path starts with '/';
|
|
if (obj.$$path && obj.$$path.charAt(0) != '/') obj.$$path = '/' + obj.$$path;
|
|
}
|
|
|
|
|
|
function composeProtocolHostPort(protocol, host, port) {
|
|
return protocol + '://' + host + (port == DEFAULT_PORTS[protocol] ? '' : ':' + port);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} begin
|
|
* @param {string} whole
|
|
* @param {string} otherwise
|
|
* @returns {string} returns text from whole after begin or otherwise if it does not begin with expected string.
|
|
*/
|
|
function beginsWith(begin, whole, otherwise) {
|
|
return whole.indexOf(begin) == 0 ? whole.substr(begin.length) : otherwise;
|
|
}
|
|
|
|
|
|
function stripHash(url) {
|
|
var index = url.indexOf('#');
|
|
return index == -1 ? url : url.substr(0, index);
|
|
}
|
|
|
|
|
|
function stripFile(url) {
|
|
return url.substr(0, stripHash(url).lastIndexOf('/') + 1);
|
|
}
|
|
|
|
/* return the server only */
|
|
function serverBase(url) {
|
|
return url.substring(0, url.indexOf('/', url.indexOf('//') + 2));
|
|
}
|
|
|
|
|
|
/**
|
|
* LocationHtml5Url represents an url
|
|
* This object is exposed as $location service when HTML5 mode is enabled and supported
|
|
*
|
|
* @constructor
|
|
* @param {string} appBase application base URL
|
|
* @param {string} basePrefix url path prefix
|
|
*/
|
|
function LocationHtml5Url(appBase, basePrefix) {
|
|
basePrefix = basePrefix || '';
|
|
var appBaseNoFile = stripFile(appBase);
|
|
/**
|
|
* Parse given html5 (regular) url string into properties
|
|
* @param {string} newAbsoluteUrl HTML5 url
|
|
* @private
|
|
*/
|
|
this.$$parse = function(url) {
|
|
var parsed = {}
|
|
matchUrl(url, parsed);
|
|
var pathUrl = beginsWith(appBaseNoFile, url);
|
|
if (!isString(pathUrl)) {
|
|
throw Error('Invalid url "' + url + '", missing path prefix "' + appBaseNoFile + '".');
|
|
}
|
|
matchAppUrl(pathUrl, parsed);
|
|
extend(this, parsed);
|
|
if (!this.$$path) {
|
|
this.$$path = '/';
|
|
}
|
|
|
|
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 = appBaseNoFile + this.$$url.substr(1); // first char is always '/'
|
|
};
|
|
|
|
this.$$rewrite = function(url) {
|
|
var appUrl, prevAppUrl;
|
|
|
|
if ( (appUrl = beginsWith(appBase, url)) !== undefined ) {
|
|
prevAppUrl = appUrl;
|
|
if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) {
|
|
return appBaseNoFile + (beginsWith('/', appUrl) || appUrl);
|
|
} else {
|
|
return appBase + prevAppUrl;
|
|
}
|
|
} else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) {
|
|
return appBaseNoFile + appUrl;
|
|
} else if (appBaseNoFile == url + '/') {
|
|
return appBaseNoFile;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* LocationHashbangUrl represents url
|
|
* This object is exposed as $location service when html5 history api is disabled or not supported
|
|
*
|
|
* @constructor
|
|
* @param {string} appBase application base URL
|
|
* @param {string} hashPrefix hashbang prefix
|
|
*/
|
|
function LocationHashbangUrl(appBase, hashPrefix) {
|
|
var appBaseNoFile = stripFile(appBase);
|
|
|
|
/**
|
|
* Parse given hashbang url into properties
|
|
* @param {string} url Hashbang url
|
|
* @private
|
|
*/
|
|
this.$$parse = function(url) {
|
|
matchUrl(url, this);
|
|
var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url);
|
|
if (!isString(withoutBaseUrl)) {
|
|
throw new Error('Invalid url "' + url + '", does not start with "' + appBase + '".');
|
|
}
|
|
var withoutHashUrl = withoutBaseUrl.charAt(0) == '#' ? beginsWith(hashPrefix, withoutBaseUrl) : withoutBaseUrl;
|
|
if (!isString(withoutHashUrl)) {
|
|
throw new Error('Invalid url "' + url + '", missing hash prefix "' + hashPrefix + '".');
|
|
}
|
|
matchAppUrl(withoutHashUrl, this);
|
|
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 = appBase + (this.$$url ? hashPrefix + this.$$url : '');
|
|
};
|
|
|
|
this.$$rewrite = function(url) {
|
|
if(stripHash(appBase) == stripHash(url)) {
|
|
return url;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* LocationHashbangUrl represents url
|
|
* This object is exposed as $location service when html5 history api is enabled but the browser
|
|
* does not support it.
|
|
*
|
|
* @constructor
|
|
* @param {string} appBase application base URL
|
|
* @param {string} hashPrefix hashbang prefix
|
|
*/
|
|
function LocationHashbangInHtml5Url(appBase, hashPrefix) {
|
|
LocationHashbangUrl.apply(this, arguments);
|
|
|
|
var appBaseNoFile = stripFile(appBase);
|
|
|
|
this.$$rewrite = function(url) {
|
|
var appUrl;
|
|
|
|
if ( appBase == stripHash(url) ) {
|
|
return url;
|
|
} else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) {
|
|
return appBase + hashPrefix + appUrl;
|
|
} else if ( appBaseNoFile === url + '/') {
|
|
return appBaseNoFile;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
LocationHashbangInHtml5Url.prototype =
|
|
LocationHashbangUrl.prototype =
|
|
LocationHtml5Url.prototype = {
|
|
|
|
/**
|
|
* Has any change been replacing ?
|
|
* @private
|
|
*/
|
|
$$replace: false,
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#absUrl
|
|
* @methodOf ng.$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} full url
|
|
*/
|
|
absUrl: locationGetter('$$absUrl'),
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#url
|
|
* @methodOf ng.$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
|
|
*/
|
|
url: function(url, replace) {
|
|
if (isUndefined(url))
|
|
return this.$$url;
|
|
|
|
var match = PATH_MATCH.exec(url);
|
|
if (match[1]) this.path(decodeURIComponent(match[1]));
|
|
if (match[2] || match[1]) this.search(match[3] || '');
|
|
this.hash(match[5] || '', replace);
|
|
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#protocol
|
|
* @methodOf ng.$location
|
|
*
|
|
* @description
|
|
* This method is getter only.
|
|
*
|
|
* Return protocol of current url.
|
|
*
|
|
* @return {string} protocol of current url
|
|
*/
|
|
protocol: locationGetter('$$protocol'),
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#host
|
|
* @methodOf ng.$location
|
|
*
|
|
* @description
|
|
* This method is getter only.
|
|
*
|
|
* Return host of current url.
|
|
*
|
|
* @return {string} host of current url.
|
|
*/
|
|
host: locationGetter('$$host'),
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#port
|
|
* @methodOf ng.$location
|
|
*
|
|
* @description
|
|
* This method is getter only.
|
|
*
|
|
* Return port of current url.
|
|
*
|
|
* @return {Number} port
|
|
*/
|
|
port: locationGetter('$$port'),
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#path
|
|
* @methodOf ng.$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
|
|
*/
|
|
path: locationGetterSetter('$$path', function(path) {
|
|
return path.charAt(0) == '/' ? path : '/' + path;
|
|
}),
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#search
|
|
* @methodOf ng.$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 params - string or hash object
|
|
* @param {string=} paramValue If `search` is a string, then `paramValue` will override only a
|
|
* single search parameter. If the value is `null`, the parameter will be deleted.
|
|
*
|
|
* @return {string} search
|
|
*/
|
|
search: function(search, paramValue) {
|
|
if (isUndefined(search))
|
|
return this.$$search;
|
|
|
|
if (isDefined(paramValue)) {
|
|
if (paramValue === null) {
|
|
delete this.$$search[search];
|
|
} else {
|
|
this.$$search[search] = paramValue;
|
|
}
|
|
} else {
|
|
this.$$search = isString(search) ? parseKeyValue(search) : search;
|
|
}
|
|
|
|
this.$$compose();
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#hash
|
|
* @methodOf ng.$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
|
|
*/
|
|
hash: locationGetterSetter('$$hash', identity),
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ng.$location#replace
|
|
* @methodOf ng.$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 object
|
|
* @name ng.$location
|
|
*
|
|
* @requires $browser
|
|
* @requires $sniffer
|
|
* @requires $rootElement
|
|
*
|
|
* @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}
|
|
*/
|
|
|
|
/**
|
|
* @ngdoc object
|
|
* @name ng.$locationProvider
|
|
* @description
|
|
* Use the `$locationProvider` to configure how the application deep linking paths are stored.
|
|
*/
|
|
function $LocationProvider(){
|
|
var hashPrefix = '',
|
|
html5Mode = false;
|
|
|
|
/**
|
|
* @ngdoc property
|
|
* @name ng.$locationProvider#hashPrefix
|
|
* @methodOf ng.$locationProvider
|
|
* @description
|
|
* @param {string=} prefix Prefix for hash part (containing path and search)
|
|
* @returns {*} current value if used as getter or itself (chaining) if used as setter
|
|
*/
|
|
this.hashPrefix = function(prefix) {
|
|
if (isDefined(prefix)) {
|
|
hashPrefix = prefix;
|
|
return this;
|
|
} else {
|
|
return hashPrefix;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @ngdoc property
|
|
* @name ng.$locationProvider#html5Mode
|
|
* @methodOf ng.$locationProvider
|
|
* @description
|
|
* @param {string=} mode Use HTML5 strategy if available.
|
|
* @returns {*} current value if used as getter or itself (chaining) if used as setter
|
|
*/
|
|
this.html5Mode = function(mode) {
|
|
if (isDefined(mode)) {
|
|
html5Mode = mode;
|
|
return this;
|
|
} else {
|
|
return html5Mode;
|
|
}
|
|
};
|
|
|
|
this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement',
|
|
function( $rootScope, $browser, $sniffer, $rootElement) {
|
|
var $location,
|
|
LocationMode,
|
|
baseHref = $browser.baseHref(),
|
|
initialUrl = $browser.url(),
|
|
appBase;
|
|
|
|
if (html5Mode) {
|
|
appBase = baseHref ? serverBase(initialUrl) + baseHref : initialUrl;
|
|
LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url;
|
|
} else {
|
|
appBase = stripHash(initialUrl);
|
|
LocationMode = LocationHashbangUrl;
|
|
}
|
|
$location = new LocationMode(appBase, '#' + hashPrefix);
|
|
$location.$$parse($location.$$rewrite(initialUrl));
|
|
|
|
$rootElement.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 (event.ctrlKey || event.metaKey || event.which == 2) return;
|
|
|
|
var elm = jqLite(event.target);
|
|
|
|
// traverse the DOM up to find first A tag
|
|
while (lowercase(elm[0].nodeName) !== 'a') {
|
|
// ignore rewriting if no A tag (reached root element, or no parent - removed from document)
|
|
if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return;
|
|
}
|
|
|
|
var absHref = elm.prop('href');
|
|
var rewrittenUrl = $location.$$rewrite(absHref);
|
|
|
|
if (absHref && !elm.attr('target') && rewrittenUrl && !event.isDefaultPrevented()) {
|
|
event.preventDefault();
|
|
if (rewrittenUrl != $browser.url()) {
|
|
// update location manually
|
|
$location.$$parse(rewrittenUrl);
|
|
$rootScope.$apply();
|
|
// hack to work around FF6 bug 684208 when scenario runner clicks on links
|
|
window.angular['ff-684208-preventDefault'] = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
// rewrite hashbang url <> html5 url
|
|
if ($location.absUrl() != initialUrl) {
|
|
$browser.url($location.absUrl(), true);
|
|
}
|
|
|
|
// update $location when $browser url changes
|
|
$browser.onUrlChange(function(newUrl) {
|
|
if ($location.absUrl() != newUrl) {
|
|
if ($rootScope.$broadcast('$locationChangeStart', newUrl, $location.absUrl()).defaultPrevented) {
|
|
$browser.url($location.absUrl());
|
|
return;
|
|
}
|
|
$rootScope.$evalAsync(function() {
|
|
var oldUrl = $location.absUrl();
|
|
|
|
$location.$$parse(newUrl);
|
|
afterLocationChange(oldUrl);
|
|
});
|
|
if (!$rootScope.$$phase) $rootScope.$digest();
|
|
}
|
|
});
|
|
|
|
// update browser
|
|
var changeCounter = 0;
|
|
$rootScope.$watch(function $locationWatch() {
|
|
var oldUrl = $browser.url();
|
|
var currentReplace = $location.$$replace;
|
|
|
|
if (!changeCounter || oldUrl != $location.absUrl()) {
|
|
changeCounter++;
|
|
$rootScope.$evalAsync(function() {
|
|
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl).
|
|
defaultPrevented) {
|
|
$location.$$parse(oldUrl);
|
|
} else {
|
|
$browser.url($location.absUrl(), currentReplace);
|
|
afterLocationChange(oldUrl);
|
|
}
|
|
});
|
|
}
|
|
$location.$$replace = false;
|
|
|
|
return changeCounter;
|
|
});
|
|
|
|
return $location;
|
|
|
|
function afterLocationChange(oldUrl) {
|
|
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl);
|
|
}
|
|
}];
|
|
}
|