890 lines
34 KiB
JavaScript
890 lines
34 KiB
JavaScript
|
/**
|
||
|
* @license Angular v17.1.3
|
||
|
* (c) 2010-2022 Google LLC. https://angular.io/
|
||
|
* License: MIT
|
||
|
*/
|
||
|
|
||
|
import * as i0 from '@angular/core';
|
||
|
import { ɵisPromise, InjectionToken, Inject, Optional, NgModule } from '@angular/core';
|
||
|
import { ReplaySubject } from 'rxjs';
|
||
|
import { Location, PlatformLocation, LocationStrategy, APP_BASE_HREF, CommonModule, HashLocationStrategy, PathLocationStrategy } from '@angular/common';
|
||
|
import { UpgradeModule } from '@angular/upgrade/static';
|
||
|
|
||
|
function stripPrefix(val, prefix) {
|
||
|
return val.startsWith(prefix) ? val.substring(prefix.length) : val;
|
||
|
}
|
||
|
function deepEqual(a, b) {
|
||
|
if (a === b) {
|
||
|
return true;
|
||
|
}
|
||
|
else if (!a || !b) {
|
||
|
return false;
|
||
|
}
|
||
|
else {
|
||
|
try {
|
||
|
if (a.prototype !== b.prototype || (Array.isArray(a) && Array.isArray(b))) {
|
||
|
return false;
|
||
|
}
|
||
|
return JSON.stringify(a) === JSON.stringify(b);
|
||
|
}
|
||
|
catch (e) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
function isAnchor(el) {
|
||
|
return el.href !== undefined;
|
||
|
}
|
||
|
|
||
|
const PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/;
|
||
|
const DOUBLE_SLASH_REGEX = /^\s*[\\/]{2,}/;
|
||
|
const IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i;
|
||
|
const DEFAULT_PORTS = {
|
||
|
'http:': 80,
|
||
|
'https:': 443,
|
||
|
'ftp:': 21,
|
||
|
};
|
||
|
/**
|
||
|
* Location service that provides a drop-in replacement for the $location service
|
||
|
* provided in AngularJS.
|
||
|
*
|
||
|
* @see [Using the Angular Unified Location Service](guide/upgrade#using-the-unified-angular-location-service)
|
||
|
*
|
||
|
* @publicApi
|
||
|
*/
|
||
|
class $locationShim {
|
||
|
constructor($injector, location, platformLocation, urlCodec, locationStrategy) {
|
||
|
this.location = location;
|
||
|
this.platformLocation = platformLocation;
|
||
|
this.urlCodec = urlCodec;
|
||
|
this.locationStrategy = locationStrategy;
|
||
|
this.initializing = true;
|
||
|
this.updateBrowser = false;
|
||
|
this.$$absUrl = '';
|
||
|
this.$$url = '';
|
||
|
this.$$host = '';
|
||
|
this.$$replace = false;
|
||
|
this.$$path = '';
|
||
|
this.$$search = '';
|
||
|
this.$$hash = '';
|
||
|
this.$$changeListeners = [];
|
||
|
this.cachedState = null;
|
||
|
this.urlChanges = new ReplaySubject(1);
|
||
|
this.lastBrowserUrl = '';
|
||
|
// This variable should be used *only* inside the cacheState function.
|
||
|
this.lastCachedState = null;
|
||
|
const initialUrl = this.browserUrl();
|
||
|
let parsedUrl = this.urlCodec.parse(initialUrl);
|
||
|
if (typeof parsedUrl === 'string') {
|
||
|
throw 'Invalid URL';
|
||
|
}
|
||
|
this.$$protocol = parsedUrl.protocol;
|
||
|
this.$$host = parsedUrl.hostname;
|
||
|
this.$$port = parseInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null;
|
||
|
this.$$parseLinkUrl(initialUrl, initialUrl);
|
||
|
this.cacheState();
|
||
|
this.$$state = this.browserState();
|
||
|
this.location.onUrlChange((newUrl, newState) => {
|
||
|
this.urlChanges.next({ newUrl, newState });
|
||
|
});
|
||
|
if (ɵisPromise($injector)) {
|
||
|
$injector.then(($i) => this.initialize($i));
|
||
|
}
|
||
|
else {
|
||
|
this.initialize($injector);
|
||
|
}
|
||
|
}
|
||
|
initialize($injector) {
|
||
|
const $rootScope = $injector.get('$rootScope');
|
||
|
const $rootElement = $injector.get('$rootElement');
|
||
|
$rootElement.on('click', (event) => {
|
||
|
if (event.ctrlKey ||
|
||
|
event.metaKey ||
|
||
|
event.shiftKey ||
|
||
|
event.which === 2 ||
|
||
|
event.button === 2) {
|
||
|
return;
|
||
|
}
|
||
|
let elm = event.target;
|
||
|
// traverse the DOM up to find first A tag
|
||
|
while (elm && elm.nodeName.toLowerCase() !== 'a') {
|
||
|
// ignore rewriting if no A tag (reached root element, or no parent - removed from document)
|
||
|
if (elm === $rootElement[0] || !(elm = elm.parentNode)) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
if (!isAnchor(elm)) {
|
||
|
return;
|
||
|
}
|
||
|
const absHref = elm.href;
|
||
|
const relHref = elm.getAttribute('href');
|
||
|
// Ignore when url is started with javascript: or mailto:
|
||
|
if (IGNORE_URI_REGEXP.test(absHref)) {
|
||
|
return;
|
||
|
}
|
||
|
if (absHref && !elm.getAttribute('target') && !event.isDefaultPrevented()) {
|
||
|
if (this.$$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 (this.absUrl() !== this.browserUrl()) {
|
||
|
$rootScope.$apply();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
this.urlChanges.subscribe(({ newUrl, newState }) => {
|
||
|
const oldUrl = this.absUrl();
|
||
|
const oldState = this.$$state;
|
||
|
this.$$parse(newUrl);
|
||
|
newUrl = this.absUrl();
|
||
|
this.$$state = newState;
|
||
|
const 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 (this.absUrl() !== newUrl)
|
||
|
return;
|
||
|
// If default was prevented, set back to old state. This is the state that was locally
|
||
|
// cached in the $location service.
|
||
|
if (defaultPrevented) {
|
||
|
this.$$parse(oldUrl);
|
||
|
this.state(oldState);
|
||
|
this.setBrowserUrlWithFallback(oldUrl, false, oldState);
|
||
|
this.$$notifyChangeListeners(this.url(), this.$$state, oldUrl, oldState);
|
||
|
}
|
||
|
else {
|
||
|
this.initializing = false;
|
||
|
$rootScope.$broadcast('$locationChangeSuccess', newUrl, oldUrl, newState, oldState);
|
||
|
this.resetBrowserUpdate();
|
||
|
}
|
||
|
if (!$rootScope.$$phase) {
|
||
|
$rootScope.$digest();
|
||
|
}
|
||
|
});
|
||
|
// update browser
|
||
|
$rootScope.$watch(() => {
|
||
|
if (this.initializing || this.updateBrowser) {
|
||
|
this.updateBrowser = false;
|
||
|
const oldUrl = this.browserUrl();
|
||
|
const newUrl = this.absUrl();
|
||
|
const oldState = this.browserState();
|
||
|
let currentReplace = this.$$replace;
|
||
|
const urlOrStateChanged = !this.urlCodec.areEqual(oldUrl, newUrl) || oldState !== this.$$state;
|
||
|
// Fire location changes one time to on initialization. This must be done on the
|
||
|
// next tick (thus inside $evalAsync()) in order for listeners to be registered
|
||
|
// before the event fires. Mimicing behavior from $locationWatch:
|
||
|
// https://github.com/angular/angular.js/blob/master/src/ng/location.js#L983
|
||
|
if (this.initializing || urlOrStateChanged) {
|
||
|
this.initializing = false;
|
||
|
$rootScope.$evalAsync(() => {
|
||
|
// Get the new URL again since it could have changed due to async update
|
||
|
const newUrl = this.absUrl();
|
||
|
const defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, this.$$state, oldState).defaultPrevented;
|
||
|
// if the location was changed by a `$locationChangeStart` handler then stop
|
||
|
// processing this location change
|
||
|
if (this.absUrl() !== newUrl)
|
||
|
return;
|
||
|
if (defaultPrevented) {
|
||
|
this.$$parse(oldUrl);
|
||
|
this.$$state = oldState;
|
||
|
}
|
||
|
else {
|
||
|
// This block doesn't run when initializing because it's going to perform the update
|
||
|
// to the URL which shouldn't be needed when initializing.
|
||
|
if (urlOrStateChanged) {
|
||
|
this.setBrowserUrlWithFallback(newUrl, currentReplace, oldState === this.$$state ? null : this.$$state);
|
||
|
this.$$replace = false;
|
||
|
}
|
||
|
$rootScope.$broadcast('$locationChangeSuccess', newUrl, oldUrl, this.$$state, oldState);
|
||
|
if (urlOrStateChanged) {
|
||
|
this.$$notifyChangeListeners(this.url(), this.$$state, oldUrl, oldState);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
this.$$replace = false;
|
||
|
});
|
||
|
}
|
||
|
resetBrowserUpdate() {
|
||
|
this.$$replace = false;
|
||
|
this.$$state = this.browserState();
|
||
|
this.updateBrowser = false;
|
||
|
this.lastBrowserUrl = this.browserUrl();
|
||
|
}
|
||
|
browserUrl(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 (typeof state === 'undefined') {
|
||
|
state = null;
|
||
|
}
|
||
|
// setter
|
||
|
if (url) {
|
||
|
let sameState = this.lastHistoryState === state;
|
||
|
// Normalize the inputted URL
|
||
|
url = this.urlCodec.parse(url).href;
|
||
|
// Don't change anything if previous and current URLs and states match.
|
||
|
if (this.lastBrowserUrl === url && sameState) {
|
||
|
return this;
|
||
|
}
|
||
|
this.lastBrowserUrl = url;
|
||
|
this.lastHistoryState = state;
|
||
|
// Remove server base from URL as the Angular APIs for updating URL require
|
||
|
// it to be the path+.
|
||
|
url = this.stripBaseUrl(this.getServerBase(), url) || url;
|
||
|
// Set the URL
|
||
|
if (replace) {
|
||
|
this.locationStrategy.replaceState(state, '', url, '');
|
||
|
}
|
||
|
else {
|
||
|
this.locationStrategy.pushState(state, '', url, '');
|
||
|
}
|
||
|
this.cacheState();
|
||
|
return this;
|
||
|
// getter
|
||
|
}
|
||
|
else {
|
||
|
return this.platformLocation.href;
|
||
|
}
|
||
|
}
|
||
|
cacheState() {
|
||
|
// This should be the only place in $browser where `history.state` is read.
|
||
|
this.cachedState = this.platformLocation.getState();
|
||
|
if (typeof this.cachedState === 'undefined') {
|
||
|
this.cachedState = null;
|
||
|
}
|
||
|
// Prevent callbacks fo fire twice if both hashchange & popstate were fired.
|
||
|
if (deepEqual(this.cachedState, this.lastCachedState)) {
|
||
|
this.cachedState = this.lastCachedState;
|
||
|
}
|
||
|
this.lastCachedState = this.cachedState;
|
||
|
this.lastHistoryState = this.cachedState;
|
||
|
}
|
||
|
/**
|
||
|
* This function emulates the $browser.state() function from AngularJS. It will cause
|
||
|
* history.state to be cached unless changed with deep equality check.
|
||
|
*/
|
||
|
browserState() {
|
||
|
return this.cachedState;
|
||
|
}
|
||
|
stripBaseUrl(base, url) {
|
||
|
if (url.startsWith(base)) {
|
||
|
return url.slice(base.length);
|
||
|
}
|
||
|
return undefined;
|
||
|
}
|
||
|
getServerBase() {
|
||
|
const { protocol, hostname, port } = this.platformLocation;
|
||
|
const baseHref = this.locationStrategy.getBaseHref();
|
||
|
let url = `${protocol}//${hostname}${port ? ':' + port : ''}${baseHref || '/'}`;
|
||
|
return url.endsWith('/') ? url : url + '/';
|
||
|
}
|
||
|
parseAppUrl(url) {
|
||
|
if (DOUBLE_SLASH_REGEX.test(url)) {
|
||
|
throw new Error(`Bad Path - URL cannot start with double slashes: ${url}`);
|
||
|
}
|
||
|
let prefixed = url.charAt(0) !== '/';
|
||
|
if (prefixed) {
|
||
|
url = '/' + url;
|
||
|
}
|
||
|
let match = this.urlCodec.parse(url, this.getServerBase());
|
||
|
if (typeof match === 'string') {
|
||
|
throw new Error(`Bad URL - Cannot parse URL: ${url}`);
|
||
|
}
|
||
|
let path = prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname;
|
||
|
this.$$path = this.urlCodec.decodePath(path);
|
||
|
this.$$search = this.urlCodec.decodeSearch(match.search);
|
||
|
this.$$hash = this.urlCodec.decodeHash(match.hash);
|
||
|
// make sure path starts with '/';
|
||
|
if (this.$$path && this.$$path.charAt(0) !== '/') {
|
||
|
this.$$path = '/' + this.$$path;
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Registers listeners for URL changes. This API is used to catch updates performed by the
|
||
|
* AngularJS framework. These changes are a subset of the `$locationChangeStart` and
|
||
|
* `$locationChangeSuccess` events which fire when AngularJS updates its internally-referenced
|
||
|
* version of the browser URL.
|
||
|
*
|
||
|
* It's possible for `$locationChange` events to happen, but for the browser URL
|
||
|
* (window.location) to remain unchanged. This `onChange` callback will fire only when AngularJS
|
||
|
* actually updates the browser URL (window.location).
|
||
|
*
|
||
|
* @param fn The callback function that is triggered for the listener when the URL changes.
|
||
|
* @param err The callback function that is triggered when an error occurs.
|
||
|
*/
|
||
|
onChange(fn, err = (e) => { }) {
|
||
|
this.$$changeListeners.push([fn, err]);
|
||
|
}
|
||
|
/** @internal */
|
||
|
$$notifyChangeListeners(url = '', state, oldUrl = '', oldState) {
|
||
|
this.$$changeListeners.forEach(([fn, err]) => {
|
||
|
try {
|
||
|
fn(url, state, oldUrl, oldState);
|
||
|
}
|
||
|
catch (e) {
|
||
|
err(e);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
/**
|
||
|
* Parses the provided URL, and sets the current URL to the parsed result.
|
||
|
*
|
||
|
* @param url The URL string.
|
||
|
*/
|
||
|
$$parse(url) {
|
||
|
let pathUrl;
|
||
|
if (url.startsWith('/')) {
|
||
|
pathUrl = url;
|
||
|
}
|
||
|
else {
|
||
|
// Remove protocol & hostname if URL starts with it
|
||
|
pathUrl = this.stripBaseUrl(this.getServerBase(), url);
|
||
|
}
|
||
|
if (typeof pathUrl === 'undefined') {
|
||
|
throw new Error(`Invalid url "${url}", missing path prefix "${this.getServerBase()}".`);
|
||
|
}
|
||
|
this.parseAppUrl(pathUrl);
|
||
|
this.$$path ||= '/';
|
||
|
this.composeUrls();
|
||
|
}
|
||
|
/**
|
||
|
* Parses the provided URL and its relative URL.
|
||
|
*
|
||
|
* @param url The full URL string.
|
||
|
* @param relHref A URL string relative to the full URL string.
|
||
|
*/
|
||
|
$$parseLinkUrl(url, relHref) {
|
||
|
// When relHref is passed, it should be a hash and is handled separately
|
||
|
if (relHref && relHref[0] === '#') {
|
||
|
this.hash(relHref.slice(1));
|
||
|
return true;
|
||
|
}
|
||
|
let rewrittenUrl;
|
||
|
let appUrl = this.stripBaseUrl(this.getServerBase(), url);
|
||
|
if (typeof appUrl !== 'undefined') {
|
||
|
rewrittenUrl = this.getServerBase() + appUrl;
|
||
|
}
|
||
|
else if (this.getServerBase() === url + '/') {
|
||
|
rewrittenUrl = this.getServerBase();
|
||
|
}
|
||
|
// Set the URL
|
||
|
if (rewrittenUrl) {
|
||
|
this.$$parse(rewrittenUrl);
|
||
|
}
|
||
|
return !!rewrittenUrl;
|
||
|
}
|
||
|
setBrowserUrlWithFallback(url, replace, state) {
|
||
|
const oldUrl = this.url();
|
||
|
const oldState = this.$$state;
|
||
|
try {
|
||
|
this.browserUrl(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.
|
||
|
this.$$state = this.browserState();
|
||
|
}
|
||
|
catch (e) {
|
||
|
// Restore old values if pushState fails
|
||
|
this.url(oldUrl);
|
||
|
this.$$state = oldState;
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
composeUrls() {
|
||
|
this.$$url = this.urlCodec.normalize(this.$$path, this.$$search, this.$$hash);
|
||
|
this.$$absUrl = this.getServerBase() + this.$$url.slice(1); // remove '/' from front of URL
|
||
|
this.updateBrowser = true;
|
||
|
}
|
||
|
/**
|
||
|
* Retrieves the full URL representation with all segments encoded according to
|
||
|
* rules specified in
|
||
|
* [RFC 3986](https://tools.ietf.org/html/rfc3986).
|
||
|
*
|
||
|
*
|
||
|
* ```js
|
||
|
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
|
||
|
* let absUrl = $location.absUrl();
|
||
|
* // => "http://example.com/#/some/path?foo=bar&baz=xoxo"
|
||
|
* ```
|
||
|
*/
|
||
|
absUrl() {
|
||
|
return this.$$absUrl;
|
||
|
}
|
||
|
url(url) {
|
||
|
if (typeof url === 'string') {
|
||
|
if (!url.length) {
|
||
|
url = '/';
|
||
|
}
|
||
|
const match = PATH_MATCH.exec(url);
|
||
|
if (!match)
|
||
|
return this;
|
||
|
if (match[1] || url === '')
|
||
|
this.path(this.urlCodec.decodePath(match[1]));
|
||
|
if (match[2] || match[1] || url === '')
|
||
|
this.search(match[3] || '');
|
||
|
this.hash(match[5] || '');
|
||
|
// Chainable method
|
||
|
return this;
|
||
|
}
|
||
|
return this.$$url;
|
||
|
}
|
||
|
/**
|
||
|
* Retrieves the protocol of the current URL.
|
||
|
*
|
||
|
* ```js
|
||
|
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
|
||
|
* let protocol = $location.protocol();
|
||
|
* // => "http"
|
||
|
* ```
|
||
|
*/
|
||
|
protocol() {
|
||
|
return this.$$protocol;
|
||
|
}
|
||
|
/**
|
||
|
* Retrieves the protocol of the current URL.
|
||
|
*
|
||
|
* In contrast 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
|
||
|
* let 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"
|
||
|
* ```
|
||
|
*/
|
||
|
host() {
|
||
|
return this.$$host;
|
||
|
}
|
||
|
/**
|
||
|
* Retrieves the port of the current URL.
|
||
|
*
|
||
|
* ```js
|
||
|
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
|
||
|
* let port = $location.port();
|
||
|
* // => 80
|
||
|
* ```
|
||
|
*/
|
||
|
port() {
|
||
|
return this.$$port;
|
||
|
}
|
||
|
path(path) {
|
||
|
if (typeof path === 'undefined') {
|
||
|
return this.$$path;
|
||
|
}
|
||
|
// null path converts to empty string. Prepend with "/" if needed.
|
||
|
path = path !== null ? path.toString() : '';
|
||
|
path = path.charAt(0) === '/' ? path : '/' + path;
|
||
|
this.$$path = path;
|
||
|
this.composeUrls();
|
||
|
return this;
|
||
|
}
|
||
|
search(search, paramValue) {
|
||
|
switch (arguments.length) {
|
||
|
case 0:
|
||
|
return this.$$search;
|
||
|
case 1:
|
||
|
if (typeof search === 'string' || typeof search === 'number') {
|
||
|
this.$$search = this.urlCodec.decodeSearch(search.toString());
|
||
|
}
|
||
|
else if (typeof search === 'object' && search !== null) {
|
||
|
// Copy the object so it's never mutated
|
||
|
search = { ...search };
|
||
|
// remove object undefined or null properties
|
||
|
for (const key in search) {
|
||
|
if (search[key] == null)
|
||
|
delete search[key];
|
||
|
}
|
||
|
this.$$search = search;
|
||
|
}
|
||
|
else {
|
||
|
throw new Error('LocationProvider.search(): First argument must be a string or an object.');
|
||
|
}
|
||
|
break;
|
||
|
default:
|
||
|
if (typeof search === 'string') {
|
||
|
const currentSearch = this.search();
|
||
|
if (typeof paramValue === 'undefined' || paramValue === null) {
|
||
|
delete currentSearch[search];
|
||
|
return this.search(currentSearch);
|
||
|
}
|
||
|
else {
|
||
|
currentSearch[search] = paramValue;
|
||
|
return this.search(currentSearch);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
this.composeUrls();
|
||
|
return this;
|
||
|
}
|
||
|
hash(hash) {
|
||
|
if (typeof hash === 'undefined') {
|
||
|
return this.$$hash;
|
||
|
}
|
||
|
this.$$hash = hash !== null ? hash.toString() : '';
|
||
|
this.composeUrls();
|
||
|
return this;
|
||
|
}
|
||
|
/**
|
||
|
* Changes to `$location` during the current `$digest` will replace the current
|
||
|
* history record, instead of adding a new one.
|
||
|
*/
|
||
|
replace() {
|
||
|
this.$$replace = true;
|
||
|
return this;
|
||
|
}
|
||
|
state(state) {
|
||
|
if (typeof state === 'undefined') {
|
||
|
return this.$$state;
|
||
|
}
|
||
|
this.$$state = state;
|
||
|
return this;
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* The factory function used to create an instance of the `$locationShim` in Angular,
|
||
|
* and provides an API-compatible `$locationProvider` for AngularJS.
|
||
|
*
|
||
|
* @publicApi
|
||
|
*/
|
||
|
class $locationShimProvider {
|
||
|
constructor(ngUpgrade, location, platformLocation, urlCodec, locationStrategy) {
|
||
|
this.ngUpgrade = ngUpgrade;
|
||
|
this.location = location;
|
||
|
this.platformLocation = platformLocation;
|
||
|
this.urlCodec = urlCodec;
|
||
|
this.locationStrategy = locationStrategy;
|
||
|
}
|
||
|
/**
|
||
|
* Factory method that returns an instance of the $locationShim
|
||
|
*/
|
||
|
$get() {
|
||
|
return new $locationShim(this.ngUpgrade.$injector, this.location, this.platformLocation, this.urlCodec, this.locationStrategy);
|
||
|
}
|
||
|
/**
|
||
|
* Stub method used to keep API compatible with AngularJS. This setting is configured through
|
||
|
* the LocationUpgradeModule's `config` method in your Angular app.
|
||
|
*/
|
||
|
hashPrefix(prefix) {
|
||
|
throw new Error('Configure LocationUpgrade through LocationUpgradeModule.config method.');
|
||
|
}
|
||
|
/**
|
||
|
* Stub method used to keep API compatible with AngularJS. This setting is configured through
|
||
|
* the LocationUpgradeModule's `config` method in your Angular app.
|
||
|
*/
|
||
|
html5Mode(mode) {
|
||
|
throw new Error('Configure LocationUpgrade through LocationUpgradeModule.config method.');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A codec for encoding and decoding URL parts.
|
||
|
*
|
||
|
* @publicApi
|
||
|
**/
|
||
|
class UrlCodec {
|
||
|
}
|
||
|
/**
|
||
|
* A `UrlCodec` that uses logic from AngularJS to serialize and parse URLs
|
||
|
* and URL parameters.
|
||
|
*
|
||
|
* @publicApi
|
||
|
*/
|
||
|
class AngularJSUrlCodec {
|
||
|
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/location.js#L15
|
||
|
encodePath(path) {
|
||
|
const segments = path.split('/');
|
||
|
let i = segments.length;
|
||
|
while (i--) {
|
||
|
// decode forward slashes to prevent them from being double encoded
|
||
|
segments[i] = encodeUriSegment(segments[i].replace(/%2F/g, '/'));
|
||
|
}
|
||
|
path = segments.join('/');
|
||
|
return _stripIndexHtml(((path && path[0] !== '/' && '/') || '') + path);
|
||
|
}
|
||
|
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/location.js#L42
|
||
|
encodeSearch(search) {
|
||
|
if (typeof search === 'string') {
|
||
|
search = parseKeyValue(search);
|
||
|
}
|
||
|
search = toKeyValue(search);
|
||
|
return search ? '?' + search : '';
|
||
|
}
|
||
|
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/location.js#L44
|
||
|
encodeHash(hash) {
|
||
|
hash = encodeUriSegment(hash);
|
||
|
return hash ? '#' + hash : '';
|
||
|
}
|
||
|
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/location.js#L27
|
||
|
decodePath(path, html5Mode = true) {
|
||
|
const segments = path.split('/');
|
||
|
let 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('/');
|
||
|
}
|
||
|
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/location.js#L72
|
||
|
decodeSearch(search) {
|
||
|
return parseKeyValue(search);
|
||
|
}
|
||
|
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/location.js#L73
|
||
|
decodeHash(hash) {
|
||
|
hash = decodeURIComponent(hash);
|
||
|
return hash[0] === '#' ? hash.substring(1) : hash;
|
||
|
}
|
||
|
normalize(pathOrHref, search, hash, baseUrl) {
|
||
|
if (arguments.length === 1) {
|
||
|
const parsed = this.parse(pathOrHref, baseUrl);
|
||
|
if (typeof parsed === 'string') {
|
||
|
return parsed;
|
||
|
}
|
||
|
const serverUrl = `${parsed.protocol}://${parsed.hostname}${parsed.port ? ':' + parsed.port : ''}`;
|
||
|
return this.normalize(this.decodePath(parsed.pathname), this.decodeSearch(parsed.search), this.decodeHash(parsed.hash), serverUrl);
|
||
|
}
|
||
|
else {
|
||
|
const encPath = this.encodePath(pathOrHref);
|
||
|
const encSearch = (search && this.encodeSearch(search)) || '';
|
||
|
const encHash = (hash && this.encodeHash(hash)) || '';
|
||
|
let joinedPath = (baseUrl || '') + encPath;
|
||
|
if (!joinedPath.length || joinedPath[0] !== '/') {
|
||
|
joinedPath = '/' + joinedPath;
|
||
|
}
|
||
|
return joinedPath + encSearch + encHash;
|
||
|
}
|
||
|
}
|
||
|
areEqual(valA, valB) {
|
||
|
return this.normalize(valA) === this.normalize(valB);
|
||
|
}
|
||
|
// https://github.com/angular/angular.js/blob/864c7f0/src/ng/urlUtils.js#L60
|
||
|
parse(url, base) {
|
||
|
try {
|
||
|
// Safari 12 throws an error when the URL constructor is called with an undefined base.
|
||
|
const parsed = !base ? new URL(url) : new URL(url, base);
|
||
|
return {
|
||
|
href: parsed.href,
|
||
|
protocol: parsed.protocol ? parsed.protocol.replace(/:$/, '') : '',
|
||
|
host: parsed.host,
|
||
|
search: parsed.search ? parsed.search.replace(/^\?/, '') : '',
|
||
|
hash: parsed.hash ? parsed.hash.replace(/^#/, '') : '',
|
||
|
hostname: parsed.hostname,
|
||
|
port: parsed.port,
|
||
|
pathname: parsed.pathname.charAt(0) === '/' ? parsed.pathname : '/' + parsed.pathname,
|
||
|
};
|
||
|
}
|
||
|
catch (e) {
|
||
|
throw new Error(`Invalid URL (${url}) with base (${base})`);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
function _stripIndexHtml(url) {
|
||
|
return url.replace(/\/index.html$/, '');
|
||
|
}
|
||
|
/**
|
||
|
* Tries to decode the URI component without throwing an exception.
|
||
|
*
|
||
|
* @param str value potential URI component to check.
|
||
|
* @returns the decoded URI if it can be decoded or else `undefined`.
|
||
|
*/
|
||
|
function tryDecodeURIComponent(value) {
|
||
|
try {
|
||
|
return decodeURIComponent(value);
|
||
|
}
|
||
|
catch (e) {
|
||
|
// Ignore any invalid uri component.
|
||
|
return undefined;
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* Parses an escaped url query string into key-value pairs. Logic taken from
|
||
|
* https://github.com/angular/angular.js/blob/864c7f0/src/Angular.js#L1382
|
||
|
*/
|
||
|
function parseKeyValue(keyValue) {
|
||
|
const obj = {};
|
||
|
(keyValue || '').split('&').forEach((keyValue) => {
|
||
|
let splitPoint, key, val;
|
||
|
if (keyValue) {
|
||
|
key = keyValue = keyValue.replace(/\+/g, '%20');
|
||
|
splitPoint = keyValue.indexOf('=');
|
||
|
if (splitPoint !== -1) {
|
||
|
key = keyValue.substring(0, splitPoint);
|
||
|
val = keyValue.substring(splitPoint + 1);
|
||
|
}
|
||
|
key = tryDecodeURIComponent(key);
|
||
|
if (typeof key !== 'undefined') {
|
||
|
val = typeof val !== 'undefined' ? tryDecodeURIComponent(val) : true;
|
||
|
if (!obj.hasOwnProperty(key)) {
|
||
|
obj[key] = val;
|
||
|
}
|
||
|
else if (Array.isArray(obj[key])) {
|
||
|
obj[key].push(val);
|
||
|
}
|
||
|
else {
|
||
|
obj[key] = [obj[key], val];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
return obj;
|
||
|
}
|
||
|
/**
|
||
|
* Serializes into key-value pairs. Logic taken from
|
||
|
* https://github.com/angular/angular.js/blob/864c7f0/src/Angular.js#L1409
|
||
|
*/
|
||
|
function toKeyValue(obj) {
|
||
|
const parts = [];
|
||
|
for (const key in obj) {
|
||
|
let value = obj[key];
|
||
|
if (Array.isArray(value)) {
|
||
|
value.forEach((arrayValue) => {
|
||
|
parts.push(encodeUriQuery(key, true) +
|
||
|
(arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true)));
|
||
|
});
|
||
|
}
|
||
|
else {
|
||
|
parts.push(encodeUriQuery(key, true) +
|
||
|
(value === true ? '' : '=' + encodeUriQuery(value, true)));
|
||
|
}
|
||
|
}
|
||
|
return parts.length ? parts.join('&') : '';
|
||
|
}
|
||
|
/**
|
||
|
* We need our custom method because encodeURIComponent is too aggressive and doesn't follow
|
||
|
* https://tools.ietf.org/html/rfc3986 with regards to the character set (pchar) allowed in path
|
||
|
* segments:
|
||
|
* segment = *pchar
|
||
|
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
|
||
|
* pct-encoded = "%" HEXDIG HEXDIG
|
||
|
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||
|
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
|
||
|
* / "*" / "+" / "," / ";" / "="
|
||
|
*
|
||
|
* Logic from https://github.com/angular/angular.js/blob/864c7f0/src/Angular.js#L1437
|
||
|
*/
|
||
|
function encodeUriSegment(val) {
|
||
|
return encodeUriQuery(val, true).replace(/%26/g, '&').replace(/%3D/gi, '=').replace(/%2B/gi, '+');
|
||
|
}
|
||
|
/**
|
||
|
* This method is intended for encoding *key* or *value* parts of query component. We need a custom
|
||
|
* method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be
|
||
|
* encoded per https://tools.ietf.org/html/rfc3986:
|
||
|
* query = *( pchar / "/" / "?" )
|
||
|
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
|
||
|
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
|
||
|
* pct-encoded = "%" HEXDIG HEXDIG
|
||
|
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
|
||
|
* / "*" / "+" / "," / ";" / "="
|
||
|
*
|
||
|
* Logic from https://github.com/angular/angular.js/blob/864c7f0/src/Angular.js#L1456
|
||
|
*/
|
||
|
function encodeUriQuery(val, pctEncodeSpaces = false) {
|
||
|
return encodeURIComponent(val)
|
||
|
.replace(/%40/g, '@')
|
||
|
.replace(/%3A/gi, ':')
|
||
|
.replace(/%24/g, '$')
|
||
|
.replace(/%2C/gi, ',')
|
||
|
.replace(/%3B/gi, ';')
|
||
|
.replace(/%20/g, pctEncodeSpaces ? '%20' : '+');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A provider token used to configure the location upgrade module.
|
||
|
*
|
||
|
* @publicApi
|
||
|
*/
|
||
|
const LOCATION_UPGRADE_CONFIGURATION = new InjectionToken(ngDevMode ? 'LOCATION_UPGRADE_CONFIGURATION' : '');
|
||
|
const APP_BASE_HREF_RESOLVED = new InjectionToken(ngDevMode ? 'APP_BASE_HREF_RESOLVED' : '');
|
||
|
/**
|
||
|
* `NgModule` used for providing and configuring Angular's Unified Location Service for upgrading.
|
||
|
*
|
||
|
* @see [Using the Unified Angular Location Service](guide/upgrade#using-the-unified-angular-location-service)
|
||
|
*
|
||
|
* @publicApi
|
||
|
*/
|
||
|
class LocationUpgradeModule {
|
||
|
static config(config) {
|
||
|
return {
|
||
|
ngModule: LocationUpgradeModule,
|
||
|
providers: [
|
||
|
Location,
|
||
|
{
|
||
|
provide: $locationShim,
|
||
|
useFactory: provide$location,
|
||
|
deps: [UpgradeModule, Location, PlatformLocation, UrlCodec, LocationStrategy],
|
||
|
},
|
||
|
{ provide: LOCATION_UPGRADE_CONFIGURATION, useValue: config ? config : {} },
|
||
|
{ provide: UrlCodec, useFactory: provideUrlCodec, deps: [LOCATION_UPGRADE_CONFIGURATION] },
|
||
|
{
|
||
|
provide: APP_BASE_HREF_RESOLVED,
|
||
|
useFactory: provideAppBaseHref,
|
||
|
deps: [LOCATION_UPGRADE_CONFIGURATION, [new Inject(APP_BASE_HREF), new Optional()]],
|
||
|
},
|
||
|
{
|
||
|
provide: LocationStrategy,
|
||
|
useFactory: provideLocationStrategy,
|
||
|
deps: [PlatformLocation, APP_BASE_HREF_RESOLVED, LOCATION_UPGRADE_CONFIGURATION],
|
||
|
},
|
||
|
],
|
||
|
};
|
||
|
}
|
||
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: LocationUpgradeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
||
|
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.1.3", ngImport: i0, type: LocationUpgradeModule, imports: [CommonModule] }); }
|
||
|
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: LocationUpgradeModule, imports: [CommonModule] }); }
|
||
|
}
|
||
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: LocationUpgradeModule, decorators: [{
|
||
|
type: NgModule,
|
||
|
args: [{ imports: [CommonModule] }]
|
||
|
}] });
|
||
|
function provideAppBaseHref(config, appBaseHref) {
|
||
|
if (config && config.appBaseHref != null) {
|
||
|
return config.appBaseHref;
|
||
|
}
|
||
|
else if (appBaseHref != null) {
|
||
|
return appBaseHref;
|
||
|
}
|
||
|
return '';
|
||
|
}
|
||
|
function provideUrlCodec(config) {
|
||
|
const codec = (config && config.urlCodec) || AngularJSUrlCodec;
|
||
|
return new codec();
|
||
|
}
|
||
|
function provideLocationStrategy(platformLocation, baseHref, options = {}) {
|
||
|
return options.useHash
|
||
|
? new HashLocationStrategy(platformLocation, baseHref)
|
||
|
: new PathLocationStrategy(platformLocation, baseHref);
|
||
|
}
|
||
|
function provide$location(ngUpgrade, location, platformLocation, urlCodec, locationStrategy) {
|
||
|
const $locationProvider = new $locationShimProvider(ngUpgrade, location, platformLocation, urlCodec, locationStrategy);
|
||
|
return $locationProvider.$get();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @module
|
||
|
* @description
|
||
|
* Entry point for all public APIs of this package.
|
||
|
*/
|
||
|
// This file only reexports content of the `src` folder. Keep it that way.
|
||
|
|
||
|
// This file is not used to build this module. It is only used during editing
|
||
|
|
||
|
/**
|
||
|
* Generated bundle index. Do not edit.
|
||
|
*/
|
||
|
|
||
|
export { $locationShim, $locationShimProvider, AngularJSUrlCodec, LOCATION_UPGRADE_CONFIGURATION, LocationUpgradeModule, UrlCodec };
|
||
|
//# sourceMappingURL=upgrade.mjs.map
|