/
githubmirror
/
angular.js
Обзор
Документация
Войти
/
githubmirror
/
angular.js
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/ng/browser.js
347 строк
11 KB
Jason Bedard
fix($browser): normalize inputted URLs
22 ноя 2018, 02:08
22 ноя 2018, 02:08
4a3ae43
Код
Авторство
О чём код?
'use strict'; /* global getHash: true, stripHash: false */ function getHash(url) { var index = url.indexOf('#'); return index === -1 ? '' : url.substr(index); } function trimEmptyHash(url) { return url.replace(/#$/, ''); } /** * ! This is a private undocumented service ! * * @name $browser * @requires $log * @description * This object has two goals: * * - hide all the global state in the browser caused by the window object * - abstract away all the browser specific features and inconsistencies * * For tests we provide {@link ngMock.$browser mock implementation} of the `$browser` * service, which can be used for convenient testing of the application without the interaction with * the real browser apis. */ /** * @param {object} window The global window object. * @param {object} document jQuery wrapped document. * @param {object} $log window.console or an object with the same interface. * @param {object} $sniffer $sniffer service */ function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) { var self = this, location = window.location, history = window.history, setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, pendingDeferIds = {}, taskTracker = $$taskTrackerFactory($log); self.isMock = false; ////////////////////////////////////////////////////////////// // Task-tracking API ////////////////////////////////////////////////////////////// // TODO(vojta): remove this temporary api self.$$completeOutstandingRequest = taskTracker.completeTask; self.$$incOutstandingRequestCount = taskTracker.incTaskCount; // TODO(vojta): prefix this method with $$ ? self.notifyWhenNoOutstandingRequests = taskTracker.notifyWhenNoPendingTasks; ////////////////////////////////////////////////////////////// // URL API ////////////////////////////////////////////////////////////// var cachedState, lastHistoryState, lastBrowserUrl = location.href, baseElement = document.find('base'), pendingLocation = null, getCurrentState = !$sniffer.history ? noop : function getCurrentState() { try { return history.state; } catch (e) { // MSIE can reportedly throw when there is no state (UNCONFIRMED). } }; cacheState(); /** * @name $browser#url * * @description * GETTER: * Without any argument, this method just returns current value of `location.href` (with a * trailing `#` stripped of if the hash is empty). * * SETTER: * With at least one argument, this method sets url to new value. * If html5 history api supported, `pushState`/`replaceState` is used, otherwise * `location.href`/`location.replace` is used. * Returns its own instance to allow chaining. * * NOTE: this api is intended for use only by the `$location` service. Please use the * {@link ng.$location $location service} to change url. * * @param {string} url New url (when used as setter) * @param {boolean=} replace Should new url replace current history record? * @param {object=} state State object to use with `pushState`/`replaceState` */ self.url = function(url, replace, state) { // In modern browsers `history.state` is `null` by default; treating it separately // from `undefined` would cause `$browser.url('/foo')` to change `history.state` // to undefined via `pushState`. Instead, let's change `undefined` to `null` here. if (isUndefined(state)) { state = null; } // Android Browser BFCache causes location, history reference to become stale. if (location !== window.location) location = window.location; if (history !== window.history) history = window.history; // setter if (url) { var sameState = lastHistoryState === state; // Normalize the inputted URL url = urlResolve(url).href; // Don't change anything if previous and current URLs and states match. This also prevents // IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode. // See https://github.com/angular/angular.js/commit/ffb2701 if (lastBrowserUrl === url && (!$sniffer.history || sameState)) { return self; } var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url); lastBrowserUrl = url; lastHistoryState = state; // Don't use history API if only the hash changed // due to a bug in IE10/IE11 which leads // to not firing a `hashchange` nor `popstate` event // in some cases (see #9143). if ($sniffer.history && (!sameBase || !sameState)) { history[replace ? 'replaceState' : 'pushState'](state, '', url); cacheState(); } else { if (!sameBase) { pendingLocation = url; } if (replace) { location.replace(url); } else if (!sameBase) { location.href = url; } else { location.hash = getHash(url); } if (location.href !== url) { pendingLocation = url; } } if (pendingLocation) { pendingLocation = url; } return self; // getter } else { // - pendingLocation is needed as browsers don't allow to read out // the new location.href if a reload happened or if there is a bug like in iOS 9 (see // https://openradar.appspot.com/22186109). return trimEmptyHash(pendingLocation || location.href); } }; /** * @name $browser#state * * @description * This method is a getter. * * Return history.state or null if history.state is undefined. * * @returns {object} state */ self.state = function() { return cachedState; }; var urlChangeListeners = [], urlChangeInit = false; function cacheStateAndFireUrlChange() { pendingLocation = null; fireStateOrUrlChange(); } // This variable should be used *only* inside the cacheState function. var lastCachedState = null; function cacheState() { // This should be the only place in $browser where `history.state` is read. cachedState = getCurrentState(); cachedState = isUndefined(cachedState) ? null : cachedState; // Prevent callbacks fo fire twice if both hashchange & popstate were fired. if (equals(cachedState, lastCachedState)) { cachedState = lastCachedState; } lastCachedState = cachedState; lastHistoryState = cachedState; } function fireStateOrUrlChange() { var prevLastHistoryState = lastHistoryState; cacheState(); if (lastBrowserUrl === self.url() && prevLastHistoryState === cachedState) { return; } lastBrowserUrl = self.url(); lastHistoryState = cachedState; forEach(urlChangeListeners, function(listener) { listener(self.url(), cachedState); }); } /** * @name $browser#onUrlChange * * @description * Register callback function that will be called, when url changes. * * It's only called when the url is changed from outside of AngularJS: * - user types different url into address bar * - user clicks on history (forward/back) button * - user clicks on a link * * It's not called when url is changed by $browser.url() method * * The listener gets called with new url as parameter. * * NOTE: this api is intended for use only by the $location service. Please use the * {@link ng.$location $location service} to monitor url changes in AngularJS apps. * * @param {function(string)} listener Listener function to be called when url changes. * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. */ self.onUrlChange = function(callback) { // TODO(vojta): refactor to use node's syntax for events if (!urlChangeInit) { // We listen on both (hashchange/popstate) when available, as some browsers don't // fire popstate when user changes the address bar and don't fire hashchange when url // changed by push/replaceState // html5 history api - popstate event if ($sniffer.history) jqLite(window).on('popstate', cacheStateAndFireUrlChange); // hashchange event jqLite(window).on('hashchange', cacheStateAndFireUrlChange); urlChangeInit = true; } urlChangeListeners.push(callback); return callback; }; /** * @private * Remove popstate and hashchange handler from window. * * NOTE: this api is intended for use only by $rootScope. */ self.$$applicationDestroyed = function() { jqLite(window).off('hashchange popstate', cacheStateAndFireUrlChange); }; /** * Checks whether the url has changed outside of AngularJS. * Needs to be exported to be able to check for changes that have been done in sync, * as hashchange/popstate events fire in async. */ self.$$checkUrlChange = fireStateOrUrlChange; ////////////////////////////////////////////////////////////// // Misc API ////////////////////////////////////////////////////////////// /** * @name $browser#baseHref * * @description * Returns current <base href> * (always relative - without domain) * * @returns {string} The current base href */ self.baseHref = function() { var href = baseElement.attr('href'); return href ? href.replace(/^(https?:)?\/\/[^/]*/, '') : ''; }; /** * @name $browser#defer * @param {function()} fn A function, who's execution should be deferred. * @param {number=} [delay=0] Number of milliseconds to defer the function execution. * @param {string=} [taskType=DEFAULT_TASK_TYPE] The type of task that is deferred. * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. * * @description * Executes a fn asynchronously via `setTimeout(fn, delay)`. * * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed * via `$browser.defer.flush()`. * */ self.defer = function(fn, delay, taskType) { var timeoutId; delay = delay || 0; taskType = taskType || taskTracker.DEFAULT_TASK_TYPE; taskTracker.incTaskCount(taskType); timeoutId = setTimeout(function() { delete pendingDeferIds[timeoutId]; taskTracker.completeTask(fn, taskType); }, delay); pendingDeferIds[timeoutId] = taskType; return timeoutId; }; /** * @name $browser#defer.cancel * * @description * Cancels a deferred task identified with `deferId`. * * @param {*} deferId Token returned by the `$browser.defer` function. * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully * canceled. */ self.defer.cancel = function(deferId) { if (pendingDeferIds.hasOwnProperty(deferId)) { var taskType = pendingDeferIds[deferId]; delete pendingDeferIds[deferId]; clearTimeout(deferId); taskTracker.completeTask(noop, taskType); return true; } return false; }; } /** @this */ function $BrowserProvider() { this.$get = ['$window', '$log', '$sniffer', '$document', '$$taskTrackerFactory', function($window, $log, $sniffer, $document, $$taskTrackerFactory) { return new Browser($window, $document, $log, $sniffer, $$taskTrackerFactory); }]; }