src/ng/location.js
'use strict';
/* global stripHash: true */
var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/,
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21};
var $locationMinErr = minErr('$location');
/**
* 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--) {
// decode forward slashes to prevent them from being double encoded
segments[i] = encodeUriSegment(segments[i].replace(/%2F/g, '/'));
}
return segments.join('/');
}
function decodePath(path, html5Mode) {
var segments = path.split('/'),
i = segments.length;
while (i--) {
segments[i] = decodeURIComponent(segments[i]);
if (html5Mode) {
// encode forward slashes to prevent them from being mistaken for path separators
segments[i] = segments[i].replace(/\//g, '%2F');
}
}
return segments.join('/');
}
function normalizePath(pathValue, searchValue, hashValue) {
var search = toKeyValue(searchValue),
hash = hashValue ? '#' + encodeUriSegment(hashValue) : '',
path = encodePath(pathValue);
return path + (search ? '?' + search : '') + hash;
}
function parseAbsoluteUrl(absoluteUrl, locationObj) {
var parsedUrl = urlResolve(absoluteUrl);
locationObj.$$protocol = parsedUrl.protocol;
locationObj.$$host = parsedUrl.hostname;
locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null;
}
var DOUBLE_SLASH_REGEX = /^\s*[\\/]{2,}/;
function parseAppUrl(url, locationObj, html5Mode) {
if (DOUBLE_SLASH_REGEX.test(url)) {
throw $locationMinErr('badpath', 'Invalid url "{0}".', url);
}
var prefixed = (url.charAt(0) !== '/');
if (prefixed) {
url = '/' + url;
}
var match = urlResolve(url);
var path = prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname;
locationObj.$$path = decodePath(path, html5Mode);
locationObj.$$search = parseKeyValue(match.search);
locationObj.$$hash = decodeURIComponent(match.hash);
// make sure path starts with '/';
if (locationObj.$$path && locationObj.$$path.charAt(0) !== '/') {
locationObj.$$path = '/' + locationObj.$$path;
}
}
function startsWith(str, search) {
return str.slice(0, search.length) === search;
}
/**
*
* @param {string} base
* @param {string} url
* @returns {string} returns text from `url` after `base` or `undefined` if it does not begin with
* the expected string.
*/
function stripBaseUrl(base, url) {
if (startsWith(url, base)) {
return url.substr(base.length);
}
}
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 (scheme://host:port) */
function serverBase(url) {
return url.substring(0, url.indexOf('/', url.indexOf('//') + 2));
}
/**
* LocationHtml5Url represents a URL
* This object is exposed as $location service when HTML5 mode is enabled and supported
*
* @constructor
* @param {string} appBase application base URL
* @param {string} appBaseNoFile application base URL stripped of any filename
* @param {string} basePrefix URL path prefix
*/
function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
this.$$html5 = true;
basePrefix = basePrefix || '';
parseAbsoluteUrl(appBase, this);
/**
* Parse given HTML5 (regular) URL string into properties
* @param {string} url HTML5 URL
* @private
*/
this.$$parse = function(url) {
var pathUrl = stripBaseUrl(appBaseNoFile, url);
if (!isString(pathUrl)) {
throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url,
appBaseNoFile);
}
parseAppUrl(pathUrl, this, true);
if (!this.$$path) {
this.$$path = '/';
}
this.$$compose();
};
this.$$normalizeUrl = function(url) {
return appBaseNoFile + url.substr(1); // first char is always '/'
};
this.$$parseLinkUrl = function(url, relHref) {
if (relHref && relHref[0] === '#') {
// special case for links to hash fragments:
// keep the old url and only replace the hash fragment
this.hash(relHref.slice(1));
return true;
}
var appUrl, prevAppUrl;
var rewrittenUrl;
if (isDefined(appUrl = stripBaseUrl(appBase, url))) {
prevAppUrl = appUrl;
if (basePrefix && isDefined(appUrl = stripBaseUrl(basePrefix, appUrl))) {
rewrittenUrl = appBaseNoFile + (stripBaseUrl('/', appUrl) || appUrl);
} else {
rewrittenUrl = appBase + prevAppUrl;
}
} else if (isDefined(appUrl = stripBaseUrl(appBaseNoFile, url))) {
rewrittenUrl = appBaseNoFile + appUrl;
} else if (appBaseNoFile === url + '/') {
rewrittenUrl = appBaseNoFile;
}
if (rewrittenUrl) {
this.$$parse(rewrittenUrl);
}
return !!rewrittenUrl;
};
}
/**
* LocationHashbangUrl represents URL
* This object is exposed as $location service when developer doesn't opt into html5 mode.
* It also serves as the base class for html5 mode fallback on legacy browsers.
*
* @constructor
* @param {string} appBase application base URL
* @param {string} appBaseNoFile application base URL stripped of any filename
* @param {string} hashPrefix hashbang prefix
*/
function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
parseAbsoluteUrl(appBase, this);
/**
* Parse given hashbang URL into properties
* @param {string} url Hashbang URL
* @private
*/
this.$$parse = function(url) {
var withoutBaseUrl = stripBaseUrl(appBase, url) || stripBaseUrl(appBaseNoFile, url);
var withoutHashUrl;
if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') {
// The rest of the URL starts with a hash so we have
// got either a hashbang path or a plain hash fragment
withoutHashUrl = stripBaseUrl(hashPrefix, withoutBaseUrl);
if (isUndefined(withoutHashUrl)) {
// There was no hashbang prefix so we just have a hash fragment
withoutHashUrl = withoutBaseUrl;
}
} else {
// There was no hashbang path nor hash fragment:
// If we are in HTML5 mode we use what is left as the path;
// Otherwise we ignore what is left
if (this.$$html5) {
withoutHashUrl = withoutBaseUrl;
} else {
withoutHashUrl = '';
if (isUndefined(withoutBaseUrl)) {
appBase = url;
/** @type {?} */ (this).replace();
}
}
}
parseAppUrl(withoutHashUrl, this, false);
this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase);
this.$$compose();
/*
* In Windows, on an anchor node on documents loaded from
* the filesystem, the browser will return a pathname
* prefixed with the drive name ('/C:/path') when a
* pathname without a drive is set:
* * a.setAttribute('href', '/foo')
* * a.pathname === '/C:/foo' //true
*
* Inside of AngularJS, we're always using pathnames that
* do not include drive names for routing.
*/
function removeWindowsDriveName(path, url, base) {
/*
Matches paths for file protocol on windows,
such as /C:/foo/bar, and captures only /foo/bar.
*/
var windowsFilePathExp = /^\/[A-Z]:(\/.*)/;
var firstPathSegmentMatch;
//Get the relative path from the input URL.
if (startsWith(url, base)) {
url = url.replace(base, '');
}
// The input URL intentionally contains a first path segment that ends with a colon.
if (windowsFilePathExp.exec(url)) {
return path;
}
firstPathSegmentMatch = windowsFilePathExp.exec(path);
return firstPathSegmentMatch ? firstPathSegmentMatch[1] : path;
}
};
this.$$normalizeUrl = function(url) {
return appBase + (url ? hashPrefix + url : '');
};
this.$$parseLinkUrl = function(url, relHref) {
if (stripHash(appBase) === stripHash(url)) {
this.$$parse(url);
return true;
}
return false;
};
}
/**
* 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} appBaseNoFile application base URL stripped of any filename
* @param {string} hashPrefix hashbang prefix
*/
function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
this.$$html5 = true;
LocationHashbangUrl.apply(this, arguments);
this.$$parseLinkUrl = function(url, relHref) {
if (relHref && relHref[0] === '#') {
// special case for links to hash fragments:
// keep the old url and only replace the hash fragment
this.hash(relHref.slice(1));
return true;
}
var rewrittenUrl;
var appUrl;
if (appBase === stripHash(url)) {
rewrittenUrl = url;
} else if ((appUrl = stripBaseUrl(appBaseNoFile, url))) {
rewrittenUrl = appBase + hashPrefix + appUrl;
} else if (appBaseNoFile === url + '/') {
rewrittenUrl = appBaseNoFile;
}
if (rewrittenUrl) {
this.$$parse(rewrittenUrl);
}
return !!rewrittenUrl;
};
this.$$normalizeUrl = function(url) {
// include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#'
return appBase + hashPrefix + url;
};
}
var locationPrototype = {
/**
* Ensure absolute URL is initialized.
* @private
*/
$$absUrl:'',
/**
* Are we in html5 mode?
* @private
*/
$$html5: false,
/**
* Has any change been replacing?
* @private
*/
$$replace: false,
/**
* Compose url and update `url` and `absUrl` property
* @private
*/
$$compose: function() {
this.$$url = normalizePath(this.$$path, this.$$search, this.$$hash);
this.$$absUrl = this.$$normalizeUrl(this.$$url);
this.$$urlUpdatedByLocation = true;
},
/**
* @ngdoc method
* @name $location#absUrl
*
* @description
* This method is getter only.
*
* Return full URL representation with all segments encoded according to rules specified in
* [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt).
*
*
* ```js
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var absUrl = $location.absUrl();
* // => "http://example.com/#/some/path?foo=bar&baz=xoxo"
* ```
*
* @return {string} full URL
*/
absUrl: locationGetter('$$absUrl'),
/**
* @ngdoc method
* @name $location#url
*
* @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`.
*
*
* ```js
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var url = $location.url();
* // => "/some/path?foo=bar&baz=xoxo"
* ```
*
* @param {string=} url New URL without base prefix (e.g. `/path?a=b#hash`)
* @return {string} url
*/
url: function(url) {
if (isUndefined(url)) {
return this.$$url;
}
var match = PATH_MATCH.exec(url);
if (match[1] || url === '') this.path(decodeURIComponent(match[1]));
if (match[2] || match[1] || url === '') this.search(match[3] || '');
this.hash(match[5] || '');
return this;
},
/**
* @ngdoc method
* @name $location#protocol
*
* @description
* This method is getter only.
*
* Return protocol of current URL.
*
*
* ```js
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var protocol = $location.protocol();
* // => "http"
* ```
*
* @return {string} protocol of current URL
*/
protocol: locationGetter('$$protocol'),
/**
* @ngdoc method
* @name $location#host
*
* @description
* This method is getter only.
*
* Return host of current URL.
*
* Note: compared to the non-AngularJS version `location.host` which returns `hostname:port`, this returns the `hostname` portion only.
*
*
* ```js
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var host = $location.host();
* // => "example.com"
*
* // given URL http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo
* host = $location.host();
* // => "example.com"
* host = location.host;
* // => "example.com:8080"
* ```
*
* @return {string} host of current URL.
*/
host: locationGetter('$$host'),
/**
* @ngdoc method
* @name $location#port
*
* @description
* This method is getter only.
*
* Return port of current URL.
*
*
* ```js
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var port = $location.port();
* // => 80
* ```
*
* @return {Number} port
*/
port: locationGetter('$$port'),
/**
* @ngdoc method
* @name $location#path
*
* @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.
*
*
* ```js
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var path = $location.path();
* // => "/some/path"
* ```
*
* @param {(string|number)=} path New path
* @return {(string|object)} path if called with no parameters, or `$location` if called with a parameter
*/
path: locationGetterSetter('$$path', function(path) {
path = path !== null ? path.toString() : '';
return path.charAt(0) === '/' ? path : '/' + path;
}),
/**
* @ngdoc method
* @name $location#search
*
* @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`.
*
*
* ```js
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var searchObject = $location.search();
* // => {foo: 'bar', baz: 'xoxo'}
*
* // set foo to 'yipee'
* $location.search('foo', 'yipee');
* // $location.search() => {foo: 'yipee', baz: 'xoxo'}
* ```
*
* @param {string|Object.<string>|Object.<Array.<string>>} search New search params - string or
* hash object.
*
* When called with a single argument the method acts as a setter, setting the `search` component
* of `$location` to the specified value.
*
* If the argument is a hash object containing an array of values, these values will be encoded
* as duplicate search parameters in the URL.
*
* @param {(string|Number|Array<string>|boolean)=} paramValue If `search` is a string or number, then `paramValue`
* will override only a single search property.
*
* If `paramValue` is an array, it will override the property of the `search` component of
* `$location` specified via the first argument.
*
* If `paramValue` is `null`, the property specified via the first argument will be deleted.
*
* If `paramValue` is `true`, the property specified via the first argument will be added with no
* value nor trailing equal sign.
*
* @return {Object} If called with no arguments returns the parsed `search` object. If called with
* one or more arguments returns `$location` object itself.
*/
search: function(search, paramValue) {
switch (arguments.length) {
case 0:
return this.$$search;
case 1:
if (isString(search) || isNumber(search)) {
search = search.toString();
this.$$search = parseKeyValue(search);
} else if (isObject(search)) {
search = copy(search, {});
// remove object undefined or null properties
forEach(search, function(value, key) {
if (value == null) delete search[key];
});
this.$$search = search;
} else {
throw $locationMinErr('isrcharg',
'The first argument of the `$location#search()` call must be a string or an object.');
}
break;
default:
if (isUndefined(paramValue) || paramValue === null) {
delete this.$$search[search];
} else {
this.$$search[search] = paramValue;
}
}
this.$$compose();
return this;
},
/**
* @ngdoc method
* @name $location#hash
*
* @description
* This method is getter / setter.
*
* Returns the hash fragment when called without any parameters.
*
* Changes the hash fragment when called with a parameter and returns `$location`.
*
*
* ```js
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue
* var hash = $location.hash();
* // => "hashValue"
* ```
*
* @param {(string|number)=} hash New hash fragment
* @return {string} hash
*/
hash: locationGetterSetter('$$hash', function(hash) {
return hash !== null ? hash.toString() : '';
}),
/**
* @ngdoc method
* @name $location#replace
*
* @description
* If called, all changes to $location during the current `$digest` will replace the current history
* record, instead of adding a new one.
*/
replace: function() {
this.$$replace = true;
return this;
}
};
forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function(Location) {
Location.prototype = Object.create(locationPrototype);
/**
* @ngdoc method
* @name $location#state
*
* @description
* This method is getter / setter.
*
* Return the history state object when called without any parameter.
*
* Change the history state object when called with one parameter and return `$location`.
* The state object is later passed to `pushState` or `replaceState`.
*
* NOTE: This method is supported only in HTML5 mode and only in browsers supporting
* the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support
* older browsers (like IE9 or Android < 4.0), don't use this method.
*
* @param {object=} state State object for pushState or replaceState
* @return {object} state
*/
Location.prototype.state = function(state) {
if (!arguments.length) {
return this.$$state;
}
if (Location !== LocationHtml5Url || !this.$$html5) {
throw $locationMinErr('nostate', 'History API state support is available only ' +
'in HTML5 mode and only in browsers supporting HTML5 History API');
}
// The user might modify `stateObject` after invoking `$location.state(stateObject)`
// but we're changing the $$state reference to $browser.state() during the $digest
// so the modification window is narrow.
this.$$state = isUndefined(state) ? null : state;
this.$$urlUpdatedByLocation = true;
return this;
};
});
function locationGetter(property) {
return /** @this */ function() {
return this[property];
};
}
function locationGetterSetter(property, preprocess) {
return /** @this */ function(value) {
if (isUndefined(value)) {
return this[property];
}
this[property] = preprocess(value);
this.$$compose();
return this;
};
}
/**
* @ngdoc service
* @name $location
*
* @requires $rootElement
*
* @description
* The $location service parses the URL in the browser address bar (based on the
* [window.location](https://developer.mozilla.org/en/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/$location Developer Guide: Using $location}
*/
/**
* @ngdoc provider
* @name $locationProvider
* @this
*
* @description
* Use the `$locationProvider` to configure how the application deep linking paths are stored.
*/
function $LocationProvider() {
var hashPrefix = '!',
html5Mode = {
enabled: false,
requireBase: true,
rewriteLinks: true
};
/**
* @ngdoc method
* @name $locationProvider#hashPrefix
* @description
* The default value for the prefix is `'!'`.
* @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 method
* @name $locationProvider#html5Mode
* @description
* @param {(boolean|Object)=} mode If boolean, sets `html5Mode.enabled` to value.
* If object, sets `enabled`, `requireBase` and `rewriteLinks` to respective values. Supported
* properties:
* - **enabled** – `{boolean}` – (default: false) If true, will rely on `history.pushState` to
* change urls where supported. Will fall back to hash-prefixed paths in browsers that do not
* support `pushState`.
* - **requireBase** - `{boolean}` - (default: `true`) When html5Mode is enabled, specifies
* whether or not a <base> tag is required to be present. If `enabled` and `requireBase` are
* true, and a base tag is not present, an error will be thrown when `$location` is injected.
* See the {@link guide/$location $location guide for more information}
* - **rewriteLinks** - `{boolean|string}` - (default: `true`) When html5Mode is enabled,
* enables/disables URL rewriting for relative links. If set to a string, URL rewriting will
* only happen on links with an attribute that matches the given string. For example, if set
* to `'internal-link'`, then the URL will only be rewritten for `<a internal-link>` links.
* Note that [attribute name normalization](guide/directive#normalization) does not apply
* here, so `'internalLink'` will **not** match `'internal-link'`.
*
* @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter
*/
this.html5Mode = function(mode) {
if (isBoolean(mode)) {
html5Mode.enabled = mode;
return this;
} else if (isObject(mode)) {
if (isBoolean(mode.enabled)) {
html5Mode.enabled = mode.enabled;
}
if (isBoolean(mode.requireBase)) {
html5Mode.requireBase = mode.requireBase;
}
if (isBoolean(mode.rewriteLinks) || isString(mode.rewriteLinks)) {
html5Mode.rewriteLinks = mode.rewriteLinks;
}
return this;
} else {
return html5Mode;
}
};
/**
* @ngdoc event
* @name $location#$locationChangeStart
* @eventType broadcast on root scope
* @description
* Broadcasted before a URL will change.
*
* This change can be prevented by calling
* `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} for more
* details about event object. Upon successful change
* {@link ng.$location#$locationChangeSuccess $locationChangeSuccess} is fired.
*
* The `newState` and `oldState` parameters may be defined only in HTML5 mode and when
* the browser supports the HTML5 History API.
*
* @param {Object} angularEvent Synthetic event object.
* @param {string} newUrl New URL
* @param {string=} oldUrl URL that was before it was changed.
* @param {string=} newState New history state object
* @param {string=} oldState History state object that was before it was changed.
*/
/**
* @ngdoc event
* @name $location#$locationChangeSuccess
* @eventType broadcast on root scope
* @description
* Broadcasted after a URL was changed.
*
* The `newState` and `oldState` parameters may be defined only in HTML5 mode and when
* the browser supports the HTML5 History API.
*
* @param {Object} angularEvent Synthetic event object.
* @param {string} newUrl New URL
* @param {string=} oldUrl URL that was before it was changed.
* @param {string=} newState New history state object
* @param {string=} oldState History state object that was before it was changed.
*/
this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', '$window',
function($rootScope, $browser, $sniffer, $rootElement, $window) {
var $location,
LocationMode,
baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to ''
initialUrl = $browser.url(),
appBase;
if (html5Mode.enabled) {
if (!baseHref && html5Mode.requireBase) {
throw $locationMinErr('nobase',
'$location in HTML5 mode requires a <base> tag to be present!');
}
appBase = serverBase(initialUrl) + (baseHref || '/');
LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url;
} else {
appBase = stripHash(initialUrl);
LocationMode = LocationHashbangUrl;
}
var appBaseNoFile = stripFile(appBase);
$location = new LocationMode(appBase, appBaseNoFile, '#' + hashPrefix);
$location.$$parseLinkUrl(initialUrl, initialUrl);
$location.$$state = $browser.state();
var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i;
// Determine if two URLs are equal despite potentially having different encoding/normalizing
// such as $location.absUrl() vs $browser.url()
// See https://github.com/angular/angular.js/issues/16592
function urlsEqual(a, b) {
return a === b || urlResolve(a).href === urlResolve(b).href;
}
function setBrowserUrlWithFallback(url, replace, state) {
var oldUrl = $location.url();
var oldState = $location.$$state;
try {
$browser.url(url, replace, state);
// Make sure $location.state() returns referentially identical (not just deeply equal)
// state object; this makes possible quick checking if the state changed in the digest
// loop. Checking deep equality would be too expensive.
$location.$$state = $browser.state();
} catch (e) {
// Restore old values if pushState fails
$location.url(oldUrl);
$location.$$state = oldState;
throw e;
}
}
$rootElement.on('click', function(event) {
var rewriteLinks = html5Mode.rewriteLinks;
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
// currently we open nice url link and redirect then
if (!rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which === 2 || event.button === 2) return;
var elm = jqLite(event.target);
// traverse the DOM up to find first A tag
while (nodeName_(elm[0]) !== '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;
}
if (isString(rewriteLinks) && isUndefined(elm.attr(rewriteLinks))) return;
var absHref = elm.prop('href');
// get the actual href attribute - see
// http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx
var relHref = elm.attr('href') || elm.attr('xlink:href');
if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') {
// SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during
// an animation.
absHref = urlResolve(absHref.animVal).href;
}
// Ignore when url is started with javascript: or mailto:
if (IGNORE_URI_REGEXP.test(absHref)) return;
if (absHref && !elm.attr('target') && !event.isDefaultPrevented()) {
if ($location.$$parseLinkUrl(absHref, relHref)) {
// We do a preventDefault for all urls that are part of the AngularJS application,
// in html5mode and also without, so that we are able to abort navigation without
// getting double entries in the location history.
event.preventDefault();
// update location manually
if ($location.absUrl() !== $browser.url()) {
$rootScope.$apply();
}
}
}
});
// rewrite hashbang url <> html5 url
if ($location.absUrl() !== initialUrl) {
$browser.url($location.absUrl(), true);
}
var initializing = true;
// update $location when $browser url changes
$browser.onUrlChange(function(newUrl, newState) {
if (!startsWith(newUrl, appBaseNoFile)) {
// If we are navigating outside of the app then force a reload
$window.location.href = newUrl;
return;
}
$rootScope.$evalAsync(function() {
var oldUrl = $location.absUrl();
var oldState = $location.$$state;
var defaultPrevented;
$location.$$parse(newUrl);
$location.$$state = newState;
defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
newState, oldState).defaultPrevented;
// if the location was changed by a `$locationChangeStart` handler then stop
// processing this location change
if ($location.absUrl() !== newUrl) return;
if (defaultPrevented) {
$location.$$parse(oldUrl);
$location.$$state = oldState;
setBrowserUrlWithFallback(oldUrl, false, oldState);
} else {
initializing = false;
afterLocationChange(oldUrl, oldState);
}
});
if (!$rootScope.$$phase) $rootScope.$digest();
});
// update browser
$rootScope.$watch(function $locationWatch() {
if (initializing || $location.$$urlUpdatedByLocation) {
$location.$$urlUpdatedByLocation = false;
var oldUrl = $browser.url();
var newUrl = $location.absUrl();
var oldState = $browser.state();
var currentReplace = $location.$$replace;
var urlOrStateChanged = !urlsEqual(oldUrl, newUrl) ||
($location.$$html5 && $sniffer.history && oldState !== $location.$$state);
if (initializing || urlOrStateChanged) {
initializing = false;
$rootScope.$evalAsync(function() {
var newUrl = $location.absUrl();
var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
$location.$$state, oldState).defaultPrevented;
// if the location was changed by a `$locationChangeStart` handler then stop
// processing this location change
if ($location.absUrl() !== newUrl) return;
if (defaultPrevented) {
$location.$$parse(oldUrl);
$location.$$state = oldState;
} else {
if (urlOrStateChanged) {
setBrowserUrlWithFallback(newUrl, currentReplace,
oldState === $location.$$state ? null : $location.$$state);
}
afterLocationChange(oldUrl, oldState);
}
});
}
}
$location.$$replace = false;
// we don't need to return anything because $evalAsync will make the digest loop dirty when
// there is a change
});
return $location;
function afterLocationChange(oldUrl, oldState) {
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl,
$location.$$state, oldState);
}
}];
}