'use strict'; var URL_MATCH = /^([^:]+):\/\/(\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 stripHash(url) { return url.split('#')[0]; } function matchUrl(url, obj) { var match = URL_MATCH.exec(url); match = { protocol: match[1], host: match[3], port: int(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 && !isUndefined(match.hash) && match.hash.indexOf(hashPrefix) === 0) { 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 Error('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, appBaseUrl) { pathPrefix = pathPrefix || ''; /** * Parse given html5 (regular) url string into properties * @param {string} newAbsoluteUrl HTML5 url * @private */ this.$$parse = function(newAbsoluteUrl) { var match = matchUrl(newAbsoluteUrl, this); if (match.path.indexOf(pathPrefix) !== 0) { throw Error('Invalid url "' + newAbsoluteUrl + '", 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.$$rewriteAppUrl = function(absoluteLinkUrl) { if(absoluteLinkUrl.indexOf(appBaseUrl) == 0) { return absoluteLinkUrl; } } 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, appBaseUrl) { 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 Error('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.$$rewriteAppUrl = function(absoluteLinkUrl) { if(absoluteLinkUrl.indexOf(appBaseUrl) == 0) { return absoluteLinkUrl; } } this.$$parse(url); } LocationUrl.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=} 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; } }; LocationHashbangUrl.prototype = inherit(LocationUrl.prototype); function LocationHashbangInHtml5Url(url, hashPrefix, appBaseUrl, baseExtra) { LocationHashbangUrl.apply(this, arguments); this.$$rewriteAppUrl = function(absoluteLinkUrl) { if (absoluteLinkUrl.indexOf(appBaseUrl) == 0) { return appBaseUrl + baseExtra + '#' + hashPrefix + absoluteLinkUrl.substr(appBaseUrl.length); } } } LocationHashbangInHtml5Url.prototype = inherit(LocationHashbangUrl.prototype); 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 {boolean=} 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, basePath, pathPrefix, initUrl = $browser.url(), initUrlParts = matchUrl(initUrl), appBaseUrl; if (html5Mode) { basePath = $browser.baseHref() || '/'; pathPrefix = pathPrefixFromBase(basePath); appBaseUrl = composeProtocolHostPort(initUrlParts.protocol, initUrlParts.host, initUrlParts.port) + pathPrefix + '/'; if ($sniffer.history) { $location = new LocationUrl( convertToHtml5Url(initUrl, basePath, hashPrefix), pathPrefix, appBaseUrl); } else { $location = new LocationHashbangInHtml5Url( convertToHashbangUrl(initUrl, basePath, hashPrefix), hashPrefix, appBaseUrl, basePath.substr(pathPrefix.length + 1)); } } else { appBaseUrl = composeProtocolHostPort(initUrlParts.protocol, initUrlParts.host, initUrlParts.port) + (initUrlParts.path || '') + (initUrlParts.search ? ('?' + initUrlParts.search) : '') + '#' + hashPrefix + '/'; $location = new LocationHashbangUrl(initUrl, hashPrefix, appBaseUrl); } $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'), rewrittenUrl = $location.$$rewriteAppUrl(absHref); if (absHref && !elm.attr('target') && rewrittenUrl) { // update location manually $location.$$parse(rewrittenUrl); $rootScope.$apply(); event.preventDefault(); // 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() != initUrl) { $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); } }]; }