@angular/common
Version:
Angular - commonly needed directives and services
641 lines (633 loc) • 17.9 kB
JavaScript
/**
* @license Angular v21.0.6
* (c) 2010-2025 Google LLC. https://angular.dev/
* License: MIT
*/
import { ɵnormalizeQueryParams as _normalizeQueryParams, LocationStrategy } from '@angular/common';
import * as i0 from '@angular/core';
import { InjectionToken, Injectable, Inject, Optional, inject, DOCUMENT } from '@angular/core';
import { Subject } from 'rxjs';
import { PlatformNavigation } from './_platform_navigation-chunk.mjs';
import { ɵFakeNavigation as _FakeNavigation } from '@angular/core/testing';
export { ɵFakeNavigation } from '@angular/core/testing';
import { Location, LocationStrategy as LocationStrategy$1 } from './_location-chunk.mjs';
import { PlatformLocation } from './_platform_location-chunk.mjs';
const urlParse = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
function parseUrl(urlStr, baseHref) {
const verifyProtocol = /^((http[s]?|ftp):\/\/)/;
let serverBase;
if (!verifyProtocol.test(urlStr)) {
serverBase = 'http://empty.com/';
}
let parsedUrl;
try {
parsedUrl = new URL(urlStr, serverBase);
} catch (e) {
const result = urlParse.exec(serverBase || '' + urlStr);
if (!result) {
throw new Error(`Invalid URL: ${urlStr} with base: ${baseHref}`);
}
const hostSplit = result[4].split(':');
parsedUrl = {
protocol: result[1],
hostname: hostSplit[0],
port: hostSplit[1] || '',
pathname: result[5],
search: result[6],
hash: result[8]
};
}
if (parsedUrl.pathname && parsedUrl.pathname.indexOf(baseHref) === 0) {
parsedUrl.pathname = parsedUrl.pathname.substring(baseHref.length);
}
return {
hostname: !serverBase && parsedUrl.hostname || '',
protocol: !serverBase && parsedUrl.protocol || '',
port: !serverBase && parsedUrl.port || '',
pathname: parsedUrl.pathname || '/',
search: parsedUrl.search || '',
hash: parsedUrl.hash || ''
};
}
const MOCK_PLATFORM_LOCATION_CONFIG = new InjectionToken('MOCK_PLATFORM_LOCATION_CONFIG');
class MockPlatformLocation {
baseHref = '';
hashUpdate = new Subject();
popStateSubject = new Subject();
urlChangeIndex = 0;
urlChanges = [{
hostname: '',
protocol: '',
port: '',
pathname: '/',
search: '',
hash: '',
state: null
}];
constructor(config) {
if (config) {
this.baseHref = config.appBaseHref || '';
const parsedChanges = this.parseChanges(null, config.startUrl || 'http://_empty_/', this.baseHref);
this.urlChanges[0] = {
...parsedChanges
};
}
}
get hostname() {
return this.urlChanges[this.urlChangeIndex].hostname;
}
get protocol() {
return this.urlChanges[this.urlChangeIndex].protocol;
}
get port() {
return this.urlChanges[this.urlChangeIndex].port;
}
get pathname() {
return this.urlChanges[this.urlChangeIndex].pathname;
}
get search() {
return this.urlChanges[this.urlChangeIndex].search;
}
get hash() {
return this.urlChanges[this.urlChangeIndex].hash;
}
get state() {
return this.urlChanges[this.urlChangeIndex].state;
}
getBaseHrefFromDOM() {
return this.baseHref;
}
onPopState(fn) {
const subscription = this.popStateSubject.subscribe(fn);
return () => subscription.unsubscribe();
}
onHashChange(fn) {
const subscription = this.hashUpdate.subscribe(fn);
return () => subscription.unsubscribe();
}
get href() {
let url = `${this.protocol}//${this.hostname}${this.port ? ':' + this.port : ''}`;
url += `${this.pathname === '/' ? '' : this.pathname}${this.search}${this.hash}`;
return url;
}
get url() {
return `${this.pathname}${this.search}${this.hash}`;
}
parseChanges(state, url, baseHref = '') {
state = JSON.parse(JSON.stringify(state));
return {
...parseUrl(url, baseHref),
state
};
}
replaceState(state, title, newUrl) {
const {
pathname,
search,
state: parsedState,
hash
} = this.parseChanges(state, newUrl);
this.urlChanges[this.urlChangeIndex] = {
...this.urlChanges[this.urlChangeIndex],
pathname,
search,
hash,
state: parsedState
};
}
pushState(state, title, newUrl) {
const {
pathname,
search,
state: parsedState,
hash
} = this.parseChanges(state, newUrl);
if (this.urlChangeIndex > 0) {
this.urlChanges.splice(this.urlChangeIndex + 1);
}
this.urlChanges.push({
...this.urlChanges[this.urlChangeIndex],
pathname,
search,
hash,
state: parsedState
});
this.urlChangeIndex = this.urlChanges.length - 1;
}
forward() {
const oldUrl = this.url;
const oldHash = this.hash;
if (this.urlChangeIndex < this.urlChanges.length) {
this.urlChangeIndex++;
}
this.emitEvents(oldHash, oldUrl);
}
back() {
const oldUrl = this.url;
const oldHash = this.hash;
if (this.urlChangeIndex > 0) {
this.urlChangeIndex--;
}
this.emitEvents(oldHash, oldUrl);
}
historyGo(relativePosition = 0) {
const oldUrl = this.url;
const oldHash = this.hash;
const nextPageIndex = this.urlChangeIndex + relativePosition;
if (nextPageIndex >= 0 && nextPageIndex < this.urlChanges.length) {
this.urlChangeIndex = nextPageIndex;
}
this.emitEvents(oldHash, oldUrl);
}
getState() {
return this.state;
}
emitEvents(oldHash, oldUrl) {
this.popStateSubject.next({
type: 'popstate',
state: this.getState(),
oldUrl,
newUrl: this.url
});
if (oldHash !== this.hash) {
this.hashUpdate.next({
type: 'hashchange',
state: null,
oldUrl,
newUrl: this.url
});
}
}
static ɵfac = i0.ɵɵngDeclareFactory({
minVersion: "12.0.0",
version: "21.0.6",
ngImport: i0,
type: MockPlatformLocation,
deps: [{
token: MOCK_PLATFORM_LOCATION_CONFIG,
optional: true
}],
target: i0.ɵɵFactoryTarget.Injectable
});
static ɵprov = i0.ɵɵngDeclareInjectable({
minVersion: "12.0.0",
version: "21.0.6",
ngImport: i0,
type: MockPlatformLocation
});
}
i0.ɵɵngDeclareClassMetadata({
minVersion: "12.0.0",
version: "21.0.6",
ngImport: i0,
type: MockPlatformLocation,
decorators: [{
type: Injectable
}],
ctorParameters: () => [{
type: undefined,
decorators: [{
type: Inject,
args: [MOCK_PLATFORM_LOCATION_CONFIG]
}, {
type: Optional
}]
}]
});
class FakeNavigationPlatformLocation {
_platformNavigation;
constructor() {
const platformNavigation = inject(PlatformNavigation);
if (!(platformNavigation instanceof _FakeNavigation)) {
throw new Error('FakePlatformNavigation cannot be used without FakeNavigation. Use ' + '`provideFakeNavigation` to have all these services provided together.');
}
this._platformNavigation = platformNavigation;
}
config = inject(MOCK_PLATFORM_LOCATION_CONFIG, {
optional: true
});
getBaseHrefFromDOM() {
return this.config?.appBaseHref ?? '';
}
onPopState(fn) {
this._platformNavigation.window.addEventListener('popstate', fn);
return () => this._platformNavigation.window.removeEventListener('popstate', fn);
}
onHashChange(fn) {
this._platformNavigation.window.addEventListener('hashchange', fn);
return () => this._platformNavigation.window.removeEventListener('hashchange', fn);
}
get href() {
return this._platformNavigation.currentEntry.url;
}
get protocol() {
return new URL(this._platformNavigation.currentEntry.url).protocol;
}
get hostname() {
return new URL(this._platformNavigation.currentEntry.url).hostname;
}
get port() {
return new URL(this._platformNavigation.currentEntry.url).port;
}
get pathname() {
return new URL(this._platformNavigation.currentEntry.url).pathname;
}
get search() {
return new URL(this._platformNavigation.currentEntry.url).search;
}
get hash() {
return new URL(this._platformNavigation.currentEntry.url).hash;
}
pushState(state, title, url) {
this._platformNavigation.pushState(state, title, url);
}
replaceState(state, title, url) {
this._platformNavigation.replaceState(state, title, url);
}
forward() {
this._platformNavigation.forward();
}
back() {
this._platformNavigation.back();
}
historyGo(relativePosition = 0) {
this._platformNavigation.go(relativePosition);
}
getState() {
return this._platformNavigation.currentEntry.getHistoryState();
}
static ɵfac = i0.ɵɵngDeclareFactory({
minVersion: "12.0.0",
version: "21.0.6",
ngImport: i0,
type: FakeNavigationPlatformLocation,
deps: [],
target: i0.ɵɵFactoryTarget.Injectable
});
static ɵprov = i0.ɵɵngDeclareInjectable({
minVersion: "12.0.0",
version: "21.0.6",
ngImport: i0,
type: FakeNavigationPlatformLocation
});
}
i0.ɵɵngDeclareClassMetadata({
minVersion: "12.0.0",
version: "21.0.6",
ngImport: i0,
type: FakeNavigationPlatformLocation,
decorators: [{
type: Injectable
}],
ctorParameters: () => []
});
const FAKE_NAVIGATION = new InjectionToken('fakeNavigation', {
factory: () => {
const config = inject(MOCK_PLATFORM_LOCATION_CONFIG, {
optional: true
});
const baseFallback = 'http://_empty_/';
const startUrl = new URL(config?.startUrl || baseFallback, baseFallback);
const fakeNavigation = new _FakeNavigation(inject(DOCUMENT), startUrl.href);
fakeNavigation.setSynchronousTraversalsForTesting(true);
return fakeNavigation;
}
});
function provideFakePlatformNavigation() {
return [{
provide: PlatformNavigation,
useFactory: () => inject(FAKE_NAVIGATION)
}, {
provide: PlatformLocation,
useClass: FakeNavigationPlatformLocation
}];
}
class SpyLocation {
urlChanges = [];
_history = [new LocationState('', '', null)];
_historyIndex = 0;
_subject = new Subject();
_basePath = '';
_locationStrategy = null;
_urlChangeListeners = [];
_urlChangeSubscription = null;
ngOnDestroy() {
this._urlChangeSubscription?.unsubscribe();
this._urlChangeListeners = [];
}
setInitialPath(url) {
this._history[this._historyIndex].path = url;
}
setBaseHref(url) {
this._basePath = url;
}
path() {
return this._history[this._historyIndex].path;
}
getState() {
return this._history[this._historyIndex].state;
}
isCurrentPathEqualTo(path, query = '') {
const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;
const currPath = this.path().endsWith('/') ? this.path().substring(0, this.path().length - 1) : this.path();
return currPath == givenPath + (query.length > 0 ? '?' + query : '');
}
simulateUrlPop(pathname) {
this._subject.next({
'url': pathname,
'pop': true,
'type': 'popstate'
});
}
simulateHashChange(pathname) {
const path = this.prepareExternalUrl(pathname);
this.pushHistory(path, '', null);
this.urlChanges.push('hash: ' + pathname);
this._subject.next({
'url': pathname,
'pop': true,
'type': 'popstate'
});
this._subject.next({
'url': pathname,
'pop': true,
'type': 'hashchange'
});
}
prepareExternalUrl(url) {
if (url.length > 0 && !url.startsWith('/')) {
url = '/' + url;
}
return this._basePath + url;
}
go(path, query = '', state = null) {
path = this.prepareExternalUrl(path);
this.pushHistory(path, query, state);
const locationState = this._history[this._historyIndex - 1];
if (locationState.path == path && locationState.query == query) {
return;
}
const url = path + (query.length > 0 ? '?' + query : '');
this.urlChanges.push(url);
this._notifyUrlChangeListeners(path + _normalizeQueryParams(query), state);
}
replaceState(path, query = '', state = null) {
path = this.prepareExternalUrl(path);
const history = this._history[this._historyIndex];
history.state = state;
if (history.path == path && history.query == query) {
return;
}
history.path = path;
history.query = query;
const url = path + (query.length > 0 ? '?' + query : '');
this.urlChanges.push('replace: ' + url);
this._notifyUrlChangeListeners(path + _normalizeQueryParams(query), state);
}
forward() {
if (this._historyIndex < this._history.length - 1) {
this._historyIndex++;
this._subject.next({
'url': this.path(),
'state': this.getState(),
'pop': true,
'type': 'popstate'
});
}
}
back() {
if (this._historyIndex > 0) {
this._historyIndex--;
this._subject.next({
'url': this.path(),
'state': this.getState(),
'pop': true,
'type': 'popstate'
});
}
}
historyGo(relativePosition = 0) {
const nextPageIndex = this._historyIndex + relativePosition;
if (nextPageIndex >= 0 && nextPageIndex < this._history.length) {
this._historyIndex = nextPageIndex;
this._subject.next({
'url': this.path(),
'state': this.getState(),
'pop': true,
'type': 'popstate'
});
}
}
onUrlChange(fn) {
this._urlChangeListeners.push(fn);
this._urlChangeSubscription ??= this.subscribe(v => {
this._notifyUrlChangeListeners(v.url, v.state);
});
return () => {
const fnIndex = this._urlChangeListeners.indexOf(fn);
this._urlChangeListeners.splice(fnIndex, 1);
if (this._urlChangeListeners.length === 0) {
this._urlChangeSubscription?.unsubscribe();
this._urlChangeSubscription = null;
}
};
}
_notifyUrlChangeListeners(url = '', state) {
this._urlChangeListeners.forEach(fn => fn(url, state));
}
subscribe(onNext, onThrow, onReturn) {
return this._subject.subscribe({
next: onNext,
error: onThrow ?? undefined,
complete: onReturn ?? undefined
});
}
normalize(url) {
return null;
}
pushHistory(path, query, state) {
if (this._historyIndex > 0) {
this._history.splice(this._historyIndex + 1);
}
this._history.push(new LocationState(path, query, state));
this._historyIndex = this._history.length - 1;
}
static ɵfac = i0.ɵɵngDeclareFactory({
minVersion: "12.0.0",
version: "21.0.6",
ngImport: i0,
type: SpyLocation,
deps: [],
target: i0.ɵɵFactoryTarget.Injectable
});
static ɵprov = i0.ɵɵngDeclareInjectable({
minVersion: "12.0.0",
version: "21.0.6",
ngImport: i0,
type: SpyLocation
});
}
i0.ɵɵngDeclareClassMetadata({
minVersion: "12.0.0",
version: "21.0.6",
ngImport: i0,
type: SpyLocation,
decorators: [{
type: Injectable
}]
});
class LocationState {
path;
query;
state;
constructor(path, query, state) {
this.path = path;
this.query = query;
this.state = state;
}
}
class MockLocationStrategy extends LocationStrategy {
internalBaseHref = '/';
internalPath = '/';
internalTitle = '';
urlChanges = [];
_subject = new Subject();
stateChanges = [];
constructor() {
super();
}
simulatePopState(url) {
this.internalPath = url;
this._subject.next(new _MockPopStateEvent(this.path()));
}
path(includeHash = false) {
return this.internalPath;
}
prepareExternalUrl(internal) {
if (internal.startsWith('/') && this.internalBaseHref.endsWith('/')) {
return this.internalBaseHref + internal.substring(1);
}
return this.internalBaseHref + internal;
}
pushState(ctx, title, path, query) {
this.stateChanges.push(ctx);
this.internalTitle = title;
const url = path + (query.length > 0 ? '?' + query : '');
this.internalPath = url;
const externalUrl = this.prepareExternalUrl(url);
this.urlChanges.push(externalUrl);
}
replaceState(ctx, title, path, query) {
this.stateChanges[(this.stateChanges.length || 1) - 1] = ctx;
this.internalTitle = title;
const url = path + (query.length > 0 ? '?' + query : '');
this.internalPath = url;
const externalUrl = this.prepareExternalUrl(url);
this.urlChanges.push('replace: ' + externalUrl);
}
onPopState(fn) {
this._subject.subscribe({
next: fn
});
}
getBaseHref() {
return this.internalBaseHref;
}
back() {
if (this.urlChanges.length > 0) {
this.urlChanges.pop();
this.stateChanges.pop();
const nextUrl = this.urlChanges.length > 0 ? this.urlChanges[this.urlChanges.length - 1] : '';
this.simulatePopState(nextUrl);
}
}
forward() {
throw 'not implemented';
}
getState() {
return this.stateChanges[(this.stateChanges.length || 1) - 1];
}
static ɵfac = i0.ɵɵngDeclareFactory({
minVersion: "12.0.0",
version: "21.0.6",
ngImport: i0,
type: MockLocationStrategy,
deps: [],
target: i0.ɵɵFactoryTarget.Injectable
});
static ɵprov = i0.ɵɵngDeclareInjectable({
minVersion: "12.0.0",
version: "21.0.6",
ngImport: i0,
type: MockLocationStrategy
});
}
i0.ɵɵngDeclareClassMetadata({
minVersion: "12.0.0",
version: "21.0.6",
ngImport: i0,
type: MockLocationStrategy,
decorators: [{
type: Injectable
}],
ctorParameters: () => []
});
class _MockPopStateEvent {
newUrl;
pop = true;
type = 'popstate';
constructor(newUrl) {
this.newUrl = newUrl;
}
}
function provideLocationMocks() {
return [{
provide: Location,
useClass: SpyLocation
}, {
provide: LocationStrategy$1,
useClass: MockLocationStrategy
}];
}
export { MOCK_PLATFORM_LOCATION_CONFIG, MockLocationStrategy, MockPlatformLocation, SpyLocation, provideLocationMocks, FakeNavigationPlatformLocation as ɵFakeNavigationPlatformLocation, provideFakePlatformNavigation as ɵprovideFakePlatformNavigation };
//# sourceMappingURL=testing.mjs.map