diff options
Diffstat (limited to 'src/ng/location.js')
| -rw-r--r-- | src/ng/location.js | 556 |
1 files changed, 556 insertions, 0 deletions
diff --git a/src/ng/location.js b/src/ng/location.js new file mode 100644 index 00000000..1accb993 --- /dev/null +++ b/src/ng/location.js @@ -0,0 +1,556 @@ +'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: 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) { + 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 = { + + /** + * Has any change been replacing ? + * @private + */ + $$replace: false, + + /** + * @ngdoc method + * @name angular.module.ng.$location#absUrl + * @methodOf angular.module.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} + */ + absUrl: locationGetter('$$absUrl'), + + /** + * @ngdoc method + * @name angular.module.ng.$location#url + * @methodOf angular.module.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: 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 angular.module.ng.$location#protocol + * @methodOf angular.module.ng.$location + * + * @description + * This method is getter only. + * + * Return protocol of current url. + * + * @return {string} + */ + protocol: locationGetter('$$protocol'), + + /** + * @ngdoc method + * @name angular.module.ng.$location#host + * @methodOf angular.module.ng.$location + * + * @description + * This method is getter only. + * + * Return host of current url. + * + * @return {string} + */ + host: locationGetter('$$host'), + + /** + * @ngdoc method + * @name angular.module.ng.$location#port + * @methodOf angular.module.ng.$location + * + * @description + * This method is getter only. + * + * Return port of current url. + * + * @return {Number} + */ + port: locationGetter('$$port'), + + /** + * @ngdoc method + * @name angular.module.ng.$location#path + * @methodOf angular.module.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: locationGetterSetter('$$path', function(path) { + return path.charAt(0) == '/' ? path : '/' + path; + }), + + /** + * @ngdoc method + * @name angular.module.ng.$location#search + * @methodOf angular.module.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: 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.module.ng.$location#hash + * @methodOf angular.module.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: locationGetterSetter('$$hash', identity), + + /** + * @ngdoc method + * @name angular.module.ng.$location#replace + * @methodOf angular.module.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 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 angular.module.ng.$location + * + * @requires $browser + * @requires $sniffer + * @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} + */ + +/** + * @ngdoc object + * @name angular.module.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 angular.module.ng.$locationProvider#hashPrefix + * @methodOf angular.module.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 angular.module.ng.$locationProvider#html5Mode + * @methodOf angular.module.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', '$document', + function( $rootScope, $browser, $sniffer, $document) { + var currentUrl, + basePath = $browser.baseHref() || '/', + pathPrefix = pathPrefixFromBase(basePath), + initUrl = $browser.url(); + + if (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 (event.ctrlKey || event.metaKey || event.which == 2) return; + + var elm = jqLite(event.target); + + // traverse the DOM up to find first A tag + while (elm.length && lowercase(elm[0].nodeName) !== 'a') { + elm = elm.parent(); + } + + var 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); + $rootScope.$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) { + $rootScope.$evalAsync(function() { + currentUrl.$$parse(newUrl); + }); + if (!$rootScope.$$phase) $rootScope.$digest(); + } + }); + + // update browser + var changeCounter = 0; + $rootScope.$watch(function $locationWatch() { + if ($browser.url() != currentUrl.absUrl()) { + changeCounter++; + $rootScope.$evalAsync(function() { + $browser.url(currentUrl.absUrl(), currentUrl.$$replace); + currentUrl.$$replace = false; + }); + } + + return changeCounter; + }); + + return currentUrl; +}]; +} |
