1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | import { EventEmitter, Injectable, InjectionToken, Inject, Optional } from '@angular/core';
|
8 | import { LocationStrategy } from '@angular/common';
|
9 | import { Subject } from 'rxjs';
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | class SpyLocation {
|
22 | constructor() {
|
23 | this.urlChanges = [];
|
24 | this._history = [new LocationState('', '', null)];
|
25 | this._historyIndex = 0;
|
26 | |
27 |
|
28 |
|
29 | this._subject = new EventEmitter();
|
30 | |
31 |
|
32 |
|
33 | this._baseHref = '';
|
34 | |
35 |
|
36 |
|
37 | this._platformStrategy = ( (null));
|
38 | |
39 |
|
40 |
|
41 | this._platformLocation = ( (null));
|
42 | |
43 |
|
44 |
|
45 | this._urlChangeListeners = [];
|
46 | }
|
47 | |
48 |
|
49 |
|
50 |
|
51 | setInitialPath(url) { this._history[this._historyIndex].path = url; }
|
52 | |
53 |
|
54 |
|
55 |
|
56 | setBaseHref(url) { this._baseHref = url; }
|
57 | |
58 |
|
59 |
|
60 | path() { return this._history[this._historyIndex].path; }
|
61 | |
62 |
|
63 |
|
64 | getState() { return this._history[this._historyIndex].state; }
|
65 | |
66 |
|
67 |
|
68 |
|
69 |
|
70 | isCurrentPathEqualTo(path, query = '') {
|
71 |
|
72 | const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;
|
73 |
|
74 | const currPath = this.path().endsWith('/') ? this.path().substring(0, this.path().length - 1) : this.path();
|
75 | return currPath == givenPath + (query.length > 0 ? ('?' + query) : '');
|
76 | }
|
77 | |
78 |
|
79 |
|
80 |
|
81 | simulateUrlPop(pathname) {
|
82 | this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'popstate' });
|
83 | }
|
84 | |
85 |
|
86 |
|
87 |
|
88 | simulateHashChange(pathname) {
|
89 |
|
90 | this.setInitialPath(pathname);
|
91 | this.urlChanges.push('hash: ' + pathname);
|
92 | this._subject.emit({ 'url': pathname, 'pop': true, 'type': 'hashchange' });
|
93 | }
|
94 | |
95 |
|
96 |
|
97 |
|
98 | prepareExternalUrl(url) {
|
99 | if (url.length > 0 && !url.startsWith('/')) {
|
100 | url = '/' + url;
|
101 | }
|
102 | return this._baseHref + url;
|
103 | }
|
104 | |
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 | go(path, query = '', state = null) {
|
111 | path = this.prepareExternalUrl(path);
|
112 | if (this._historyIndex > 0) {
|
113 | this._history.splice(this._historyIndex + 1);
|
114 | }
|
115 | this._history.push(new LocationState(path, query, state));
|
116 | this._historyIndex = this._history.length - 1;
|
117 |
|
118 | const locationState = this._history[this._historyIndex - 1];
|
119 | if (locationState.path == path && locationState.query == query) {
|
120 | return;
|
121 | }
|
122 |
|
123 | const url = path + (query.length > 0 ? ('?' + query) : '');
|
124 | this.urlChanges.push(url);
|
125 | this._subject.emit({ 'url': url, 'pop': false });
|
126 | }
|
127 | |
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 | replaceState(path, query = '', state = null) {
|
134 | path = this.prepareExternalUrl(path);
|
135 |
|
136 | const history = this._history[this._historyIndex];
|
137 | if (history.path == path && history.query == query) {
|
138 | return;
|
139 | }
|
140 | history.path = path;
|
141 | history.query = query;
|
142 | history.state = state;
|
143 |
|
144 | const url = path + (query.length > 0 ? ('?' + query) : '');
|
145 | this.urlChanges.push('replace: ' + url);
|
146 | }
|
147 | |
148 |
|
149 |
|
150 | forward() {
|
151 | if (this._historyIndex < (this._history.length - 1)) {
|
152 | this._historyIndex++;
|
153 | this._subject.emit({ 'url': this.path(), 'state': this.getState(), 'pop': true });
|
154 | }
|
155 | }
|
156 | |
157 |
|
158 |
|
159 | back() {
|
160 | if (this._historyIndex > 0) {
|
161 | this._historyIndex--;
|
162 | this._subject.emit({ 'url': this.path(), 'state': this.getState(), 'pop': true });
|
163 | }
|
164 | }
|
165 | |
166 |
|
167 |
|
168 |
|
169 | onUrlChange(fn) {
|
170 | this._urlChangeListeners.push(fn);
|
171 | this.subscribe(( |
172 |
|
173 |
|
174 |
|
175 | v => { this._notifyUrlChangeListeners(v.url, v.state); }));
|
176 | }
|
177 | |
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 | _notifyUrlChangeListeners(url = '', state) {
|
184 | this._urlChangeListeners.forEach(( |
185 |
|
186 |
|
187 |
|
188 | fn => fn(url, state)));
|
189 | }
|
190 | |
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 | subscribe(onNext, onThrow, onReturn) {
|
197 | return this._subject.subscribe({ next: onNext, error: onThrow, complete: onReturn });
|
198 | }
|
199 | |
200 |
|
201 |
|
202 |
|
203 | normalize(url) { return ( (null)); }
|
204 | }
|
205 | SpyLocation.decorators = [
|
206 | { type: Injectable }
|
207 | ];
|
208 | if (false) {
|
209 |
|
210 | SpyLocation.prototype.urlChanges;
|
211 | |
212 |
|
213 |
|
214 |
|
215 | SpyLocation.prototype._history;
|
216 | |
217 |
|
218 |
|
219 |
|
220 | SpyLocation.prototype._historyIndex;
|
221 | |
222 |
|
223 |
|
224 |
|
225 | SpyLocation.prototype._subject;
|
226 | |
227 |
|
228 |
|
229 |
|
230 | SpyLocation.prototype._baseHref;
|
231 | |
232 |
|
233 |
|
234 |
|
235 | SpyLocation.prototype._platformStrategy;
|
236 | |
237 |
|
238 |
|
239 |
|
240 | SpyLocation.prototype._platformLocation;
|
241 | |
242 |
|
243 |
|
244 |
|
245 | SpyLocation.prototype._urlChangeListeners;
|
246 | }
|
247 | class LocationState {
|
248 | |
249 |
|
250 |
|
251 |
|
252 |
|
253 | constructor(path, query, state) {
|
254 | this.path = path;
|
255 | this.query = query;
|
256 | this.state = state;
|
257 | }
|
258 | }
|
259 | if (false) {
|
260 |
|
261 | LocationState.prototype.path;
|
262 |
|
263 | LocationState.prototype.query;
|
264 |
|
265 | LocationState.prototype.state;
|
266 | }
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 | class MockLocationStrategy extends LocationStrategy {
|
280 | constructor() {
|
281 | super();
|
282 | this.internalBaseHref = '/';
|
283 | this.internalPath = '/';
|
284 | this.internalTitle = '';
|
285 | this.urlChanges = [];
|
286 | |
287 |
|
288 |
|
289 | this._subject = new EventEmitter();
|
290 | this.stateChanges = [];
|
291 | }
|
292 | |
293 |
|
294 |
|
295 |
|
296 | simulatePopState(url) {
|
297 | this.internalPath = url;
|
298 | this._subject.emit(new _MockPopStateEvent(this.path()));
|
299 | }
|
300 | |
301 |
|
302 |
|
303 |
|
304 | path(includeHash = false) { return this.internalPath; }
|
305 | |
306 |
|
307 |
|
308 |
|
309 | prepareExternalUrl(internal) {
|
310 | if (internal.startsWith('/') && this.internalBaseHref.endsWith('/')) {
|
311 | return this.internalBaseHref + internal.substring(1);
|
312 | }
|
313 | return this.internalBaseHref + internal;
|
314 | }
|
315 | |
316 |
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 | pushState(ctx, title, path, query) {
|
323 |
|
324 | this.stateChanges.push(ctx);
|
325 | this.internalTitle = title;
|
326 |
|
327 | const url = path + (query.length > 0 ? ('?' + query) : '');
|
328 | this.internalPath = url;
|
329 |
|
330 | const externalUrl = this.prepareExternalUrl(url);
|
331 | this.urlChanges.push(externalUrl);
|
332 | }
|
333 | |
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 | replaceState(ctx, title, path, query) {
|
341 |
|
342 | this.stateChanges[(this.stateChanges.length || 1) - 1] = ctx;
|
343 | this.internalTitle = title;
|
344 |
|
345 | const url = path + (query.length > 0 ? ('?' + query) : '');
|
346 | this.internalPath = url;
|
347 |
|
348 | const externalUrl = this.prepareExternalUrl(url);
|
349 | this.urlChanges.push('replace: ' + externalUrl);
|
350 | }
|
351 | |
352 |
|
353 |
|
354 |
|
355 | onPopState(fn) { this._subject.subscribe({ next: fn }); }
|
356 | |
357 |
|
358 |
|
359 | getBaseHref() { return this.internalBaseHref; }
|
360 | |
361 |
|
362 |
|
363 | back() {
|
364 | if (this.urlChanges.length > 0) {
|
365 | this.urlChanges.pop();
|
366 | this.stateChanges.pop();
|
367 |
|
368 | const nextUrl = this.urlChanges.length > 0 ? this.urlChanges[this.urlChanges.length - 1] : '';
|
369 | this.simulatePopState(nextUrl);
|
370 | }
|
371 | }
|
372 | |
373 |
|
374 |
|
375 | forward() { throw 'not implemented'; }
|
376 | |
377 |
|
378 |
|
379 | getState() { return this.stateChanges[(this.stateChanges.length || 1) - 1]; }
|
380 | }
|
381 | MockLocationStrategy.decorators = [
|
382 | { type: Injectable }
|
383 | ];
|
384 |
|
385 | MockLocationStrategy.ctorParameters = () => [];
|
386 | if (false) {
|
387 |
|
388 | MockLocationStrategy.prototype.internalBaseHref;
|
389 |
|
390 | MockLocationStrategy.prototype.internalPath;
|
391 |
|
392 | MockLocationStrategy.prototype.internalTitle;
|
393 |
|
394 | MockLocationStrategy.prototype.urlChanges;
|
395 | |
396 |
|
397 |
|
398 |
|
399 | MockLocationStrategy.prototype._subject;
|
400 | |
401 |
|
402 |
|
403 |
|
404 | MockLocationStrategy.prototype.stateChanges;
|
405 | }
|
406 | class _MockPopStateEvent {
|
407 | |
408 |
|
409 |
|
410 | constructor(newUrl) {
|
411 | this.newUrl = newUrl;
|
412 | this.pop = true;
|
413 | this.type = 'popstate';
|
414 | }
|
415 | }
|
416 | if (false) {
|
417 |
|
418 | _MockPopStateEvent.prototype.pop;
|
419 |
|
420 | _MockPopStateEvent.prototype.type;
|
421 |
|
422 | _MockPopStateEvent.prototype.newUrl;
|
423 | }
|
424 |
|
425 |
|
426 |
|
427 |
|
428 |
|
429 |
|
430 |
|
431 |
|
432 |
|
433 |
|
434 |
|
435 |
|
436 |
|
437 |
|
438 |
|
439 |
|
440 |
|
441 |
|
442 |
|
443 |
|
444 |
|
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 | const urlParse = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;
|
451 |
|
452 |
|
453 |
|
454 |
|
455 |
|
456 | function parseUrl(urlStr, baseHref) {
|
457 |
|
458 | const verifyProtocol = /^((http[s]?|ftp):\/\/)/;
|
459 |
|
460 | let serverBase;
|
461 |
|
462 |
|
463 | if (!verifyProtocol.test(urlStr)) {
|
464 | serverBase = 'http://empty.com/';
|
465 | }
|
466 |
|
467 | let parsedUrl;
|
468 | try {
|
469 | parsedUrl = new URL(urlStr, serverBase);
|
470 | }
|
471 | catch (e) {
|
472 |
|
473 | const result = urlParse.exec(serverBase || '' + urlStr);
|
474 | if (!result) {
|
475 | throw new Error(`Invalid URL: ${urlStr} with base: ${baseHref}`);
|
476 | }
|
477 |
|
478 | const hostSplit = result[4].split(':');
|
479 | parsedUrl = {
|
480 | protocol: result[1],
|
481 | hostname: hostSplit[0],
|
482 | port: hostSplit[1] || '',
|
483 | pathname: result[5],
|
484 | search: result[6],
|
485 | hash: result[8],
|
486 | };
|
487 | }
|
488 | if (parsedUrl.pathname && parsedUrl.pathname.indexOf(baseHref) === 0) {
|
489 | parsedUrl.pathname = parsedUrl.pathname.substring(baseHref.length);
|
490 | }
|
491 | return {
|
492 | hostname: !serverBase && parsedUrl.hostname || '',
|
493 | protocol: !serverBase && parsedUrl.protocol || '',
|
494 | port: !serverBase && parsedUrl.port || '',
|
495 | pathname: parsedUrl.pathname || '/',
|
496 | search: parsedUrl.search || '',
|
497 | hash: parsedUrl.hash || '',
|
498 | };
|
499 | }
|
500 |
|
501 |
|
502 |
|
503 |
|
504 |
|
505 |
|
506 | function MockPlatformLocationConfig() { }
|
507 | if (false) {
|
508 |
|
509 | MockPlatformLocationConfig.prototype.startUrl;
|
510 |
|
511 | MockPlatformLocationConfig.prototype.appBaseHref;
|
512 | }
|
513 |
|
514 |
|
515 |
|
516 |
|
517 |
|
518 |
|
519 | const MOCK_PLATFORM_LOCATION_CONFIG = new InjectionToken('MOCK_PLATFORM_LOCATION_CONFIG');
|
520 |
|
521 |
|
522 |
|
523 |
|
524 |
|
525 | class MockPlatformLocation {
|
526 | |
527 |
|
528 |
|
529 | constructor(config) {
|
530 | this.baseHref = '';
|
531 | this.hashUpdate = new Subject();
|
532 | this.urlChanges = [{ hostname: '', protocol: '', port: '', pathname: '/', search: '', hash: '', state: null }];
|
533 | if (config) {
|
534 | this.baseHref = config.appBaseHref || '';
|
535 |
|
536 | const parsedChanges = this.parseChanges(null, config.startUrl || 'http://<empty>/', this.baseHref);
|
537 | this.urlChanges[0] = Object.assign({}, parsedChanges);
|
538 | }
|
539 | }
|
540 | |
541 |
|
542 |
|
543 | get hostname() { return this.urlChanges[0].hostname; }
|
544 | |
545 |
|
546 |
|
547 | get protocol() { return this.urlChanges[0].protocol; }
|
548 | |
549 |
|
550 |
|
551 | get port() { return this.urlChanges[0].port; }
|
552 | |
553 |
|
554 |
|
555 | get pathname() { return this.urlChanges[0].pathname; }
|
556 | |
557 |
|
558 |
|
559 | get search() { return this.urlChanges[0].search; }
|
560 | |
561 |
|
562 |
|
563 | get hash() { return this.urlChanges[0].hash; }
|
564 | |
565 |
|
566 |
|
567 | get state() { return this.urlChanges[0].state; }
|
568 | |
569 |
|
570 |
|
571 | getBaseHrefFromDOM() { return this.baseHref; }
|
572 | |
573 |
|
574 |
|
575 |
|
576 | onPopState(fn) {
|
577 |
|
578 |
|
579 | }
|
580 | |
581 |
|
582 |
|
583 |
|
584 | onHashChange(fn) { this.hashUpdate.subscribe(fn); }
|
585 | |
586 |
|
587 |
|
588 | get href() {
|
589 |
|
590 | let url = `${this.protocol}//${this.hostname}${this.port ? ':' + this.port : ''}`;
|
591 | url += `${this.pathname === '/' ? '' : this.pathname}${this.search}${this.hash}`;
|
592 | return url;
|
593 | }
|
594 | |
595 |
|
596 |
|
597 | get url() { return `${this.pathname}${this.search}${this.hash}`; }
|
598 | |
599 |
|
600 |
|
601 |
|
602 |
|
603 |
|
604 |
|
605 | parseChanges(state, url, baseHref = '') {
|
606 |
|
607 | state = JSON.parse(JSON.stringify(state));
|
608 | return Object.assign(Object.assign({}, parseUrl(url, baseHref)), { state });
|
609 | }
|
610 | |
611 |
|
612 |
|
613 |
|
614 |
|
615 |
|
616 | replaceState(state, title, newUrl) {
|
617 | const { pathname, search, state: parsedState, hash } = this.parseChanges(state, newUrl);
|
618 | this.urlChanges[0] = Object.assign(Object.assign({}, this.urlChanges[0]), { pathname, search, hash, state: parsedState });
|
619 | }
|
620 | |
621 |
|
622 |
|
623 |
|
624 |
|
625 |
|
626 | pushState(state, title, newUrl) {
|
627 | const { pathname, search, state: parsedState, hash } = this.parseChanges(state, newUrl);
|
628 | this.urlChanges.unshift(Object.assign(Object.assign({}, this.urlChanges[0]), { pathname, search, hash, state: parsedState }));
|
629 | }
|
630 | |
631 |
|
632 |
|
633 | forward() { throw new Error('Not implemented'); }
|
634 | |
635 |
|
636 |
|
637 | back() {
|
638 |
|
639 | const oldUrl = this.url;
|
640 |
|
641 | const oldHash = this.hash;
|
642 | this.urlChanges.shift();
|
643 |
|
644 | const newHash = this.hash;
|
645 | if (oldHash !== newHash) {
|
646 | scheduleMicroTask(( |
647 |
|
648 |
|
649 | () => this.hashUpdate.next(( ({
|
650 | type: 'hashchange', state: null, oldUrl, newUrl: this.url
|
651 | })))));
|
652 | }
|
653 | }
|
654 | |
655 |
|
656 |
|
657 | getState() { return this.state; }
|
658 | }
|
659 | MockPlatformLocation.decorators = [
|
660 | { type: Injectable }
|
661 | ];
|
662 |
|
663 | MockPlatformLocation.ctorParameters = () => [
|
664 | { type: undefined, decorators: [{ type: Inject, args: [MOCK_PLATFORM_LOCATION_CONFIG,] }, { type: Optional }] }
|
665 | ];
|
666 | if (false) {
|
667 | |
668 |
|
669 |
|
670 |
|
671 | MockPlatformLocation.prototype.baseHref;
|
672 | |
673 |
|
674 |
|
675 |
|
676 | MockPlatformLocation.prototype.hashUpdate;
|
677 | |
678 |
|
679 |
|
680 |
|
681 | MockPlatformLocation.prototype.urlChanges;
|
682 | }
|
683 |
|
684 |
|
685 |
|
686 |
|
687 | function scheduleMicroTask(cb) {
|
688 | Promise.resolve(null).then(cb);
|
689 | }
|
690 |
|
691 |
|
692 |
|
693 |
|
694 |
|
695 |
|
696 |
|
697 |
|
698 |
|
699 |
|
700 |
|
701 |
|
702 |
|
703 |
|
704 |
|
705 |
|
706 |
|
707 |
|
708 |
|
709 |
|
710 |
|
711 |
|
712 |
|
713 | export { MOCK_PLATFORM_LOCATION_CONFIG, MockLocationStrategy, MockPlatformLocation, SpyLocation };
|
714 |
|