1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | import * as i0 from '@angular/core';
|
8 | import { EventEmitter, Injectable, InjectionToken, Inject, Optional } from '@angular/core';
|
9 | import { LocationStrategy } from '@angular/common';
|
10 | import { Subject } from 'rxjs';
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | function joinWithSlash(start, end) {
|
29 | if (start.length == 0) {
|
30 | return end;
|
31 | }
|
32 | if (end.length == 0) {
|
33 | return start;
|
34 | }
|
35 | let slashes = 0;
|
36 | if (start.endsWith('/')) {
|
37 | slashes++;
|
38 | }
|
39 | if (end.startsWith('/')) {
|
40 | slashes++;
|
41 | }
|
42 | if (slashes == 2) {
|
43 | return start + end.substring(1);
|
44 | }
|
45 | if (slashes == 1) {
|
46 | return start + end;
|
47 | }
|
48 | return start + '/' + end;
|
49 | }
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 | function stripTrailingSlash(url) {
|
60 | const match = url.match(/#|\?|$/);
|
61 | const pathEndIdx = match && match.index || url.length;
|
62 | const droppedSlashIdx = pathEndIdx - (url[pathEndIdx - 1] === '/' ? 1 : 0);
|
63 | return url.slice(0, droppedSlashIdx) + url.slice(pathEndIdx);
|
64 | }
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 | function normalizeQueryParams(params) {
|
73 | return params && params[0] !== '?' ? '?' + params : params;
|
74 | }
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 | class SpyLocation {
|
82 | constructor() {
|
83 | this.urlChanges = [];
|
84 | this._history = [new LocationState('', '', null)];
|
85 | this._historyIndex = 0;
|
86 |
|
87 | this._subject = new EventEmitter();
|
88 |
|
89 | this._baseHref = '';
|
90 |
|
91 | this._locationStrategy = null;
|
92 |
|
93 | this._urlChangeListeners = [];
|
94 |
|
95 | this._urlChangeSubscription = null;
|
96 | }
|
97 | ngOnDestroy() {
|
98 | this._urlChangeSubscription?.unsubscribe();
|
99 | this._urlChangeListeners = [];
|
100 | }
|
101 | setInitialPath(url) {
|
102 | this._history[this._historyIndex].path = url;
|
103 | }
|
104 | setBaseHref(url) {
|
105 | this._baseHref = url;
|
106 | }
|
107 | path() {
|
108 | return this._history[this._historyIndex].path;
|
109 | }
|
110 | getState() {
|
111 | return this._history[this._historyIndex].state;
|
112 | }
|
113 | isCurrentPathEqualTo(path, query = '') {
|
114 | const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;
|
115 | const currPath = this.path().endsWith('/') ? this.path().substring(0, this.path().length - 1) : this.path();
|
116 | return currPath == givenPath + (query.length > 0 ? ('?' + query) : '');
|
117 | }
|
118 | simulateUrlPop(pathname) {
|
119 | this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'popstate' });
|
120 | }
|
121 | simulateHashChange(pathname) {
|
122 | const path = this.prepareExternalUrl(pathname);
|
123 | this.pushHistory(path, '', null);
|
124 | this.urlChanges.push('hash: ' + pathname);
|
125 |
|
126 |
|
127 | this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'popstate' });
|
128 | this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'hashchange' });
|
129 | }
|
130 | prepareExternalUrl(url) {
|
131 | if (url.length > 0 && !url.startsWith('/')) {
|
132 | url = '/' + url;
|
133 | }
|
134 | return this._baseHref + url;
|
135 | }
|
136 | go(path, query = '', state = null) {
|
137 | path = this.prepareExternalUrl(path);
|
138 | this.pushHistory(path, query, state);
|
139 | const locationState = this._history[this._historyIndex - 1];
|
140 | if (locationState.path == path && locationState.query == query) {
|
141 | return;
|
142 | }
|
143 | const url = path + (query.length > 0 ? ('?' + query) : '');
|
144 | this.urlChanges.push(url);
|
145 | this._notifyUrlChangeListeners(path + normalizeQueryParams(query), state);
|
146 | }
|
147 | replaceState(path, query = '', state = null) {
|
148 | path = this.prepareExternalUrl(path);
|
149 | const history = this._history[this._historyIndex];
|
150 | if (history.path == path && history.query == query) {
|
151 | return;
|
152 | }
|
153 | history.path = path;
|
154 | history.query = query;
|
155 | history.state = state;
|
156 | const url = path + (query.length > 0 ? ('?' + query) : '');
|
157 | this.urlChanges.push('replace: ' + url);
|
158 | this._notifyUrlChangeListeners(path + normalizeQueryParams(query), state);
|
159 | }
|
160 | forward() {
|
161 | if (this._historyIndex < (this._history.length - 1)) {
|
162 | this._historyIndex++;
|
163 | this._subject.emit({ 'url': this.path(), 'state': this.getState(), 'pop': true, 'type': 'popstate' });
|
164 | }
|
165 | }
|
166 | back() {
|
167 | if (this._historyIndex > 0) {
|
168 | this._historyIndex--;
|
169 | this._subject.emit({ 'url': this.path(), 'state': this.getState(), 'pop': true, 'type': 'popstate' });
|
170 | }
|
171 | }
|
172 | historyGo(relativePosition = 0) {
|
173 | const nextPageIndex = this._historyIndex + relativePosition;
|
174 | if (nextPageIndex >= 0 && nextPageIndex < this._history.length) {
|
175 | this._historyIndex = nextPageIndex;
|
176 | this._subject.emit({ 'url': this.path(), 'state': this.getState(), 'pop': true, 'type': 'popstate' });
|
177 | }
|
178 | }
|
179 | onUrlChange(fn) {
|
180 | this._urlChangeListeners.push(fn);
|
181 | if (!this._urlChangeSubscription) {
|
182 | this._urlChangeSubscription = this.subscribe(v => {
|
183 | this._notifyUrlChangeListeners(v.url, v.state);
|
184 | });
|
185 | }
|
186 | return () => {
|
187 | const fnIndex = this._urlChangeListeners.indexOf(fn);
|
188 | this._urlChangeListeners.splice(fnIndex, 1);
|
189 | if (this._urlChangeListeners.length === 0) {
|
190 | this._urlChangeSubscription?.unsubscribe();
|
191 | this._urlChangeSubscription = null;
|
192 | }
|
193 | };
|
194 | }
|
195 |
|
196 | _notifyUrlChangeListeners(url = '', state) {
|
197 | this._urlChangeListeners.forEach(fn => fn(url, state));
|
198 | }
|
199 | subscribe(onNext, onThrow, onReturn) {
|
200 | return this._subject.subscribe({ next: onNext, error: onThrow, complete: onReturn });
|
201 | }
|
202 | normalize(url) {
|
203 | return null;
|
204 | }
|
205 | pushHistory(path, query, state) {
|
206 | if (this._historyIndex > 0) {
|
207 | this._history.splice(this._historyIndex + 1);
|
208 | }
|
209 | this._history.push(new LocationState(path, query, state));
|
210 | this._historyIndex = this._history.length - 1;
|
211 | }
|
212 | }
|
213 | SpyLocation.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: SpyLocation, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
214 | SpyLocation.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: SpyLocation });
|
215 | i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: SpyLocation, decorators: [{
|
216 | type: Injectable
|
217 | }] });
|
218 | class LocationState {
|
219 | constructor(path, query, state) {
|
220 | this.path = path;
|
221 | this.query = query;
|
222 | this.state = state;
|
223 | }
|
224 | }
|
225 |
|
226 |
|
227 |
|
228 |
|
229 |
|
230 |
|
231 |
|
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 | class MockLocationStrategy extends LocationStrategy {
|
240 | constructor() {
|
241 | super();
|
242 | this.internalBaseHref = '/';
|
243 | this.internalPath = '/';
|
244 | this.internalTitle = '';
|
245 | this.urlChanges = [];
|
246 |
|
247 | this._subject = new EventEmitter();
|
248 | this.stateChanges = [];
|
249 | }
|
250 | simulatePopState(url) {
|
251 | this.internalPath = url;
|
252 | this._subject.emit(new _MockPopStateEvent(this.path()));
|
253 | }
|
254 | path(includeHash = false) {
|
255 | return this.internalPath;
|
256 | }
|
257 | prepareExternalUrl(internal) {
|
258 | if (internal.startsWith('/') && this.internalBaseHref.endsWith('/')) {
|
259 | return this.internalBaseHref + internal.substring(1);
|
260 | }
|
261 | return this.internalBaseHref + internal;
|
262 | }
|
263 | pushState(ctx, title, path, query) {
|
264 |
|
265 | this.stateChanges.push(ctx);
|
266 | this.internalTitle = title;
|
267 | const url = path + (query.length > 0 ? ('?' + query) : '');
|
268 | this.internalPath = url;
|
269 | const externalUrl = this.prepareExternalUrl(url);
|
270 | this.urlChanges.push(externalUrl);
|
271 | }
|
272 | replaceState(ctx, title, path, query) {
|
273 |
|
274 | this.stateChanges[(this.stateChanges.length || 1) - 1] = ctx;
|
275 | this.internalTitle = title;
|
276 | const url = path + (query.length > 0 ? ('?' + query) : '');
|
277 | this.internalPath = url;
|
278 | const externalUrl = this.prepareExternalUrl(url);
|
279 | this.urlChanges.push('replace: ' + externalUrl);
|
280 | }
|
281 | onPopState(fn) {
|
282 | this._subject.subscribe({ next: fn });
|
283 | }
|
284 | getBaseHref() {
|
285 | return this.internalBaseHref;
|
286 | }
|
287 | back() {
|
288 | if (this.urlChanges.length > 0) {
|
289 | this.urlChanges.pop();
|
290 | this.stateChanges.pop();
|
291 | const nextUrl = this.urlChanges.length > 0 ? this.urlChanges[this.urlChanges.length - 1] : '';
|
292 | this.simulatePopState(nextUrl);
|
293 | }
|
294 | }
|
295 | forward() {
|
296 | throw 'not implemented';
|
297 | }
|
298 | getState() {
|
299 | return this.stateChanges[(this.stateChanges.length || 1) - 1];
|
300 | }
|
301 | }
|
302 | MockLocationStrategy.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: MockLocationStrategy, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
303 | MockLocationStrategy.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: MockLocationStrategy });
|
304 | i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: MockLocationStrategy, decorators: [{
|
305 | type: Injectable
|
306 | }], ctorParameters: function () { return []; } });
|
307 | class _MockPopStateEvent {
|
308 | constructor(newUrl) {
|
309 | this.newUrl = newUrl;
|
310 | this.pop = true;
|
311 | this.type = 'popstate';
|
312 | }
|
313 | }
|
314 |
|
315 |
|
316 |
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 |
|
325 |
|
326 |
|
327 |
|
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 | const urlParse = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
|
335 | function parseUrl(urlStr, baseHref) {
|
336 | const verifyProtocol = /^((http[s]?|ftp):\/\/)/;
|
337 | let serverBase;
|
338 |
|
339 |
|
340 | if (!verifyProtocol.test(urlStr)) {
|
341 | serverBase = 'http://empty.com/';
|
342 | }
|
343 | let parsedUrl;
|
344 | try {
|
345 | parsedUrl = new URL(urlStr, serverBase);
|
346 | }
|
347 | catch (e) {
|
348 | const result = urlParse.exec(serverBase || '' + urlStr);
|
349 | if (!result) {
|
350 | throw new Error(`Invalid URL: ${urlStr} with base: ${baseHref}`);
|
351 | }
|
352 | const hostSplit = result[4].split(':');
|
353 | parsedUrl = {
|
354 | protocol: result[1],
|
355 | hostname: hostSplit[0],
|
356 | port: hostSplit[1] || '',
|
357 | pathname: result[5],
|
358 | search: result[6],
|
359 | hash: result[8],
|
360 | };
|
361 | }
|
362 | if (parsedUrl.pathname && parsedUrl.pathname.indexOf(baseHref) === 0) {
|
363 | parsedUrl.pathname = parsedUrl.pathname.substring(baseHref.length);
|
364 | }
|
365 | return {
|
366 | hostname: !serverBase && parsedUrl.hostname || '',
|
367 | protocol: !serverBase && parsedUrl.protocol || '',
|
368 | port: !serverBase && parsedUrl.port || '',
|
369 | pathname: parsedUrl.pathname || '/',
|
370 | search: parsedUrl.search || '',
|
371 | hash: parsedUrl.hash || '',
|
372 | };
|
373 | }
|
374 |
|
375 |
|
376 |
|
377 |
|
378 |
|
379 | const MOCK_PLATFORM_LOCATION_CONFIG = new InjectionToken('MOCK_PLATFORM_LOCATION_CONFIG');
|
380 |
|
381 |
|
382 |
|
383 |
|
384 |
|
385 | class MockPlatformLocation {
|
386 | constructor(config) {
|
387 | this.baseHref = '';
|
388 | this.hashUpdate = new Subject();
|
389 | this.urlChangeIndex = 0;
|
390 | this.urlChanges = [{ hostname: '', protocol: '', port: '', pathname: '/', search: '', hash: '', state: null }];
|
391 | if (config) {
|
392 | this.baseHref = config.appBaseHref || '';
|
393 | const parsedChanges = this.parseChanges(null, config.startUrl || 'http://_empty_/', this.baseHref);
|
394 | this.urlChanges[0] = { ...parsedChanges };
|
395 | }
|
396 | }
|
397 | get hostname() {
|
398 | return this.urlChanges[this.urlChangeIndex].hostname;
|
399 | }
|
400 | get protocol() {
|
401 | return this.urlChanges[this.urlChangeIndex].protocol;
|
402 | }
|
403 | get port() {
|
404 | return this.urlChanges[this.urlChangeIndex].port;
|
405 | }
|
406 | get pathname() {
|
407 | return this.urlChanges[this.urlChangeIndex].pathname;
|
408 | }
|
409 | get search() {
|
410 | return this.urlChanges[this.urlChangeIndex].search;
|
411 | }
|
412 | get hash() {
|
413 | return this.urlChanges[this.urlChangeIndex].hash;
|
414 | }
|
415 | get state() {
|
416 | return this.urlChanges[this.urlChangeIndex].state;
|
417 | }
|
418 | getBaseHrefFromDOM() {
|
419 | return this.baseHref;
|
420 | }
|
421 | onPopState(fn) {
|
422 |
|
423 |
|
424 | return () => { };
|
425 | }
|
426 | onHashChange(fn) {
|
427 | const subscription = this.hashUpdate.subscribe(fn);
|
428 | return () => subscription.unsubscribe();
|
429 | }
|
430 | get href() {
|
431 | let url = `${this.protocol}//${this.hostname}${this.port ? ':' + this.port : ''}`;
|
432 | url += `${this.pathname === '/' ? '' : this.pathname}${this.search}${this.hash}`;
|
433 | return url;
|
434 | }
|
435 | get url() {
|
436 | return `${this.pathname}${this.search}${this.hash}`;
|
437 | }
|
438 | parseChanges(state, url, baseHref = '') {
|
439 |
|
440 | state = JSON.parse(JSON.stringify(state));
|
441 | return { ...parseUrl(url, baseHref), state };
|
442 | }
|
443 | replaceState(state, title, newUrl) {
|
444 | const { pathname, search, state: parsedState, hash } = this.parseChanges(state, newUrl);
|
445 | this.urlChanges[this.urlChangeIndex] =
|
446 | { ...this.urlChanges[this.urlChangeIndex], pathname, search, hash, state: parsedState };
|
447 | }
|
448 | pushState(state, title, newUrl) {
|
449 | const { pathname, search, state: parsedState, hash } = this.parseChanges(state, newUrl);
|
450 | if (this.urlChangeIndex > 0) {
|
451 | this.urlChanges.splice(this.urlChangeIndex + 1);
|
452 | }
|
453 | this.urlChanges.push({ ...this.urlChanges[this.urlChangeIndex], pathname, search, hash, state: parsedState });
|
454 | this.urlChangeIndex = this.urlChanges.length - 1;
|
455 | }
|
456 | forward() {
|
457 | const oldUrl = this.url;
|
458 | const oldHash = this.hash;
|
459 | if (this.urlChangeIndex < this.urlChanges.length) {
|
460 | this.urlChangeIndex++;
|
461 | }
|
462 | this.scheduleHashUpdate(oldHash, oldUrl);
|
463 | }
|
464 | back() {
|
465 | const oldUrl = this.url;
|
466 | const oldHash = this.hash;
|
467 | if (this.urlChangeIndex > 0) {
|
468 | this.urlChangeIndex--;
|
469 | }
|
470 | this.scheduleHashUpdate(oldHash, oldUrl);
|
471 | }
|
472 | historyGo(relativePosition = 0) {
|
473 | const oldUrl = this.url;
|
474 | const oldHash = this.hash;
|
475 | const nextPageIndex = this.urlChangeIndex + relativePosition;
|
476 | if (nextPageIndex >= 0 && nextPageIndex < this.urlChanges.length) {
|
477 | this.urlChangeIndex = nextPageIndex;
|
478 | }
|
479 | this.scheduleHashUpdate(oldHash, oldUrl);
|
480 | }
|
481 | getState() {
|
482 | return this.state;
|
483 | }
|
484 | scheduleHashUpdate(oldHash, oldUrl) {
|
485 | if (oldHash !== this.hash) {
|
486 | scheduleMicroTask(() => this.hashUpdate.next({ type: 'hashchange', state: null, oldUrl, newUrl: this.url }));
|
487 | }
|
488 | }
|
489 | }
|
490 | MockPlatformLocation.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: MockPlatformLocation, deps: [{ token: MOCK_PLATFORM_LOCATION_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
491 | MockPlatformLocation.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: MockPlatformLocation });
|
492 | i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.0.4", ngImport: i0, type: MockPlatformLocation, decorators: [{
|
493 | type: Injectable
|
494 | }], ctorParameters: function () { return [{ type: undefined, decorators: [{
|
495 | type: Inject,
|
496 | args: [MOCK_PLATFORM_LOCATION_CONFIG]
|
497 | }, {
|
498 | type: Optional
|
499 | }] }]; } });
|
500 | function scheduleMicroTask(cb) {
|
501 | Promise.resolve(null).then(cb);
|
502 | }
|
503 |
|
504 |
|
505 |
|
506 |
|
507 |
|
508 |
|
509 |
|
510 |
|
511 |
|
512 |
|
513 |
|
514 |
|
515 |
|
516 |
|
517 |
|
518 |
|
519 |
|
520 |
|
521 |
|
522 |
|
523 |
|
524 |
|
525 |
|
526 |
|
527 |
|
528 |
|
529 |
|
530 |
|
531 |
|
532 |
|
533 | export { MOCK_PLATFORM_LOCATION_CONFIG, MockLocationStrategy, MockPlatformLocation, SpyLocation };
|
534 |
|