'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 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=} 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 $config * @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, $config, $document) { var scope = this, currentUrl, basePath = $browser.baseHref() || '/', pathPrefix = pathPrefixFromBase(basePath), hashPrefix = $config.hashPrefix || '', initUrl = $browser.url(); if ($config.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.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: '' }; });