@stacknav/angular 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,724 @@
1
+ import { Location, DOCUMENT } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { InjectionToken, makeEnvironmentProviders, inject, Injectable, input, EventEmitter, ViewContainerRef, ChangeDetectorRef, ElementRef, Injector, reflectComponentType, Output, Input, Directive } from '@angular/core';
4
+ import { Router, ROUTER_CONFIGURATION, NavigationStart, NavigationEnd, NavigationCancel, NavigationError, NavigationSkipped, PRIMARY_OUTLET, ChildrenOutletContexts, ActivatedRoute, ROUTER_OUTLET_DATA, BaseRouteReuseStrategy } from '@angular/router';
5
+ import { createDirectionResolver, defaultStrategies, createIOSStack, injectStyles, segmentsOf } from '@stacknav/core';
6
+ import { BehaviorSubject, switchMap, Subscription, combineLatest, of, from } from 'rxjs';
7
+
8
+ /**
9
+ * The `ActivatedRoute` a page component injects. When a page kept alive beneath
10
+ * the stack is reached again, the router hands it a new route object. This
11
+ * proxy keeps the component's subscriptions valid by switching them to whichever
12
+ * route is current.
13
+ */
14
+ class StackNavActivatedRoute {
15
+ current$;
16
+ url;
17
+ params;
18
+ queryParams;
19
+ fragment;
20
+ data;
21
+ title;
22
+ paramMap;
23
+ queryParamMap;
24
+ constructor(route) {
25
+ this.current$ = new BehaviorSubject(route);
26
+ const of = (pick) => this.current$.pipe(switchMap(pick));
27
+ this.url = of((r) => r.url);
28
+ this.params = of((r) => r.params);
29
+ this.queryParams = of((r) => r.queryParams);
30
+ this.fragment = of((r) => r.fragment);
31
+ this.data = of((r) => r.data);
32
+ this.title = of((r) => r.title);
33
+ this.paramMap = of((r) => r.paramMap);
34
+ this.queryParamMap = of((r) => r.queryParamMap);
35
+ }
36
+ /** The router's route object behind the proxy right now. */
37
+ get actual() {
38
+ return this.current$.value;
39
+ }
40
+ /** @internal */
41
+ swap(route) {
42
+ if (route !== this.current$.value)
43
+ this.current$.next(route);
44
+ }
45
+ get snapshot() {
46
+ return this.actual.snapshot;
47
+ }
48
+ get outlet() {
49
+ return this.actual.outlet;
50
+ }
51
+ get component() {
52
+ return this.actual.component;
53
+ }
54
+ get routeConfig() {
55
+ return this.actual.routeConfig;
56
+ }
57
+ get root() {
58
+ return this.actual.root;
59
+ }
60
+ get parent() {
61
+ return this.actual.parent;
62
+ }
63
+ get firstChild() {
64
+ return this.actual.firstChild;
65
+ }
66
+ get children() {
67
+ return this.actual.children;
68
+ }
69
+ get pathFromRoot() {
70
+ return this.actual.pathFromRoot;
71
+ }
72
+ toString() {
73
+ return this.actual.toString();
74
+ }
75
+ }
76
+
77
+ const STACKNAV_CONFIG = /*#__PURE__*/ new InjectionToken('STACKNAV_CONFIG', {
78
+ providedIn: 'root',
79
+ factory: () => resolveConfig({}),
80
+ });
81
+ function defaultLevelOf(snapshot) {
82
+ const v = snapshot.data?.['stackLevel'];
83
+ return typeof v === 'number' ? v : undefined;
84
+ }
85
+ /** The route's URL path from the root down to and including this route, e.g. `items/42;view=full`. */
86
+ function defaultKeyOf(snapshot) {
87
+ return snapshot.pathFromRoot
88
+ .flatMap((s) => s.url.map((u) => u.toString()))
89
+ .join('/');
90
+ }
91
+ function resolveConfig(c) {
92
+ const resolve = typeof c.direction === 'function'
93
+ ? c.direction
94
+ : createDirectionResolver(c.direction ?? defaultStrategies(), c.fallbackDirection ?? 'push');
95
+ return {
96
+ resolve,
97
+ levelOf: c.levelOf ?? defaultLevelOf,
98
+ keyOf: c.keyOf ?? defaultKeyOf,
99
+ infoKey: c.infoKey ?? 'stacknav',
100
+ transition: c.transition ?? {},
101
+ gesture: c.gesture ?? {},
102
+ detachInactiveViews: c.detachInactiveViews ?? false,
103
+ injectStyles: c.injectStyles ?? true,
104
+ animated: c.animated ?? true,
105
+ };
106
+ }
107
+ /**
108
+ * Configures the outlets. Add it next to `provideRouter()`. It changes no
109
+ * router configuration.
110
+ *
111
+ * ```ts
112
+ * bootstrapApplication(App, { providers: [provideRouter(routes), provideStackNav()] });
113
+ * ```
114
+ */
115
+ function provideStackNav(config = {}) {
116
+ return makeEnvironmentProviders([{ provide: STACKNAV_CONFIG, useValue: resolveConfig(config) }]);
117
+ }
118
+
119
+ /**
120
+ * A model of the browser's history as the router walks it: which entry is
121
+ * current, and which came before. It answers two questions for the outlet: is
122
+ * this navigation going back or forward, and is the previous history entry the
123
+ * page beneath the top? Everything comes from public router events, so it needs
124
+ * no router configuration. Internal to the outlet.
125
+ */
126
+ class StackNavHistory {
127
+ router = inject(Router);
128
+ config = inject(STACKNAV_CONFIG);
129
+ cancelResolution = inject(ROUTER_CONFIGURATION, { optional: true })?.canceledNavigationResolution ?? 'replace';
130
+ entries = [];
131
+ cursor = -1;
132
+ pending = null;
133
+ constructor() {
134
+ this.router.events.subscribe((e) => {
135
+ if (e instanceof NavigationStart)
136
+ this.onStart(e);
137
+ else if (e instanceof NavigationEnd)
138
+ this.onEnd(e);
139
+ else if (e instanceof NavigationCancel || e instanceof NavigationError)
140
+ this.onAbort();
141
+ else if (e instanceof NavigationSkipped)
142
+ this.pending = null;
143
+ });
144
+ }
145
+ /** The navigation in flight, if any. Valid while the router activates routes. */
146
+ get current() {
147
+ return this.pending;
148
+ }
149
+ /** URL of the history entry before the current one, or null. */
150
+ get previousUrl() {
151
+ return this.cursor > 0 ? this.entries[this.cursor - 1].url : null;
152
+ }
153
+ get currentUrl() {
154
+ return this.cursor >= 0 ? this.entries[this.cursor].url : null;
155
+ }
156
+ get canGoBack() {
157
+ return this.cursor > 0;
158
+ }
159
+ onStart(e) {
160
+ const nav = this.router.getCurrentNavigation();
161
+ const isHistory = e.navigationTrigger === 'popstate' || e.navigationTrigger === 'hashchange';
162
+ const restoredId = e.restoredState?.navigationId ?? null;
163
+ let historyDelta;
164
+ if (isHistory) {
165
+ const idx = this.indexOf(restoredId);
166
+ if (idx >= 0 && this.cursor >= 0)
167
+ historyDelta = idx - this.cursor;
168
+ else if (restoredId != null && this.cursor >= 0)
169
+ historyDelta = restoredId < this.entries[this.cursor].id ? -1 : 1;
170
+ }
171
+ const hint = isHistory ? undefined : readHint(nav?.extras.info, this.config.infoKey);
172
+ this.pending = {
173
+ id: e.id,
174
+ trigger: isHistory ? 'history' : 'imperative',
175
+ historyDelta,
176
+ hint: hint?.direction,
177
+ animated: hint?.animated,
178
+ replaceUrl: !!nav?.extras.replaceUrl,
179
+ skipLocationChange: !!nav?.extras.skipLocationChange,
180
+ restoredId,
181
+ };
182
+ }
183
+ onEnd(e) {
184
+ const p = this.pending;
185
+ this.pending = null;
186
+ const entry = { id: e.id, url: e.urlAfterRedirects };
187
+ if (!p || this.cursor < 0) {
188
+ this.entries = [entry];
189
+ this.cursor = 0;
190
+ return;
191
+ }
192
+ if (p.trigger === 'history') {
193
+ const idx = this.indexOf(p.restoredId);
194
+ if (idx >= 0) {
195
+ this.cursor = idx;
196
+ this.entries[idx] = entry; // the router rewrites the entry's navigationId on popstate
197
+ }
198
+ else {
199
+ this.entries = [entry];
200
+ this.cursor = 0;
201
+ }
202
+ return;
203
+ }
204
+ if (p.skipLocationChange)
205
+ return;
206
+ if (p.replaceUrl) {
207
+ this.entries[this.cursor] = entry;
208
+ return;
209
+ }
210
+ this.entries.splice(this.cursor + 1);
211
+ this.entries.push(entry);
212
+ this.cursor++;
213
+ }
214
+ /**
215
+ * Handles a history navigation the router refused. With
216
+ * `canceledNavigationResolution: 'computed'` the router walks the browser back
217
+ * to where it was, so nothing changes here. With the default `'replace'` it
218
+ * overwrites the entry the browser landed on with the current URL and the last
219
+ * successful id.
220
+ */
221
+ onAbort() {
222
+ const p = this.pending;
223
+ this.pending = null;
224
+ if (!p || p.trigger !== 'history' || this.cursor < 0 || this.cancelResolution === 'computed')
225
+ return;
226
+ const idx = this.indexOf(p.restoredId);
227
+ if (idx < 0)
228
+ return;
229
+ this.entries[idx] = { ...this.entries[this.cursor] };
230
+ this.cursor = idx;
231
+ }
232
+ /** The entry carrying `id`, preferring the nearest one that is not the current entry. */
233
+ indexOf(id) {
234
+ if (id == null)
235
+ return -1;
236
+ let best = -1;
237
+ for (let i = 0; i < this.entries.length; i++) {
238
+ if (this.entries[i].id !== id)
239
+ continue;
240
+ if (best < 0 || best === this.cursor || (i !== this.cursor && Math.abs(i - this.cursor) < Math.abs(best - this.cursor)))
241
+ best = i;
242
+ }
243
+ return best;
244
+ }
245
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StackNavHistory, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
246
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StackNavHistory, providedIn: 'root' });
247
+ }
248
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StackNavHistory, decorators: [{
249
+ type: Injectable,
250
+ args: [{ providedIn: 'root' }]
251
+ }], ctorParameters: () => [] });
252
+ function readHint(info, key) {
253
+ if (info == null || typeof info !== 'object')
254
+ return undefined;
255
+ const v = info[key];
256
+ if (v == null)
257
+ return undefined;
258
+ return typeof v === 'string' ? { direction: v } : v;
259
+ }
260
+
261
+ /**
262
+ * A router outlet (`RouterOutletContract`) that keeps a stack of pages and moves
263
+ * between them with the iOS push/pop transition. Use it where you would use
264
+ * `<router-outlet>`. The router drives it the same way:
265
+ *
266
+ * ```html
267
+ * <sn-outlet />
268
+ * ```
269
+ *
270
+ * Pages beneath the top stay alive, keeping scroll position, form state and
271
+ * subscriptions. A swipe from the leading edge pops interactively, and the
272
+ * direction of every navigation is decided by the strategies configured in
273
+ * `provideStackNav()`. The element needs a height; it is the pages' scroll
274
+ * container.
275
+ */
276
+ class StackNavOutlet {
277
+ /** Outlet name, as on `router-outlet`. Static. */
278
+ name = PRIMARY_OUTLET;
279
+ /** Per-outlet transition options, merged over `provideStackNav({ transition })`. */
280
+ transition;
281
+ /** Per-outlet gesture options, merged over `provideStackNav({ gesture })`. `false` disables the swipe. */
282
+ gesture;
283
+ /** Same as on `router-outlet`: available to pages through `ROUTER_OUTLET_DATA`. */
284
+ routerOutletData = input(undefined, /* @ts-ignore */
285
+ ...(ngDevMode ? [{ debugName: "routerOutletData" }] : /* istanbul ignore next */ []));
286
+ /** A page component was created. */
287
+ activateEvents = new EventEmitter();
288
+ /** A page component was destroyed. */
289
+ deactivateEvents = new EventEmitter();
290
+ /** A kept page was shown again, or a detached one re-attached. */
291
+ attachEvents = new EventEmitter();
292
+ detachEvents = new EventEmitter();
293
+ /** Every activation, with the direction that was resolved for it. */
294
+ navigatedEvents = new EventEmitter();
295
+ parentContexts = inject(ChildrenOutletContexts);
296
+ location = inject(ViewContainerRef);
297
+ changeDetector = inject(ChangeDetectorRef);
298
+ host = inject(ElementRef).nativeElement;
299
+ config = inject(STACKNAV_CONFIG);
300
+ history = inject(StackNavHistory);
301
+ router = inject(Router);
302
+ browserLocation = inject(Location);
303
+ document = inject(DOCUMENT);
304
+ /** The underlying core stack. Subscribe to `progress` to drive your own chrome. */
305
+ stack;
306
+ /** The direction of the last activation. */
307
+ lastDirection = null;
308
+ /** Inputs are bound when the router was configured with `withComponentInputBinding()`. */
309
+ supportsBindingToComponentInputs;
310
+ views = [];
311
+ byEl = new Map();
312
+ detached = new Map();
313
+ activeView = null;
314
+ leaving = null;
315
+ activated = null;
316
+ _activatedRoute = null;
317
+ subs = new Subscription();
318
+ constructor() {
319
+ if (this.router.componentInputBindingEnabled)
320
+ this.supportsBindingToComponentInputs = true;
321
+ }
322
+ // ------------------------------------------------------------- lifecycle
323
+ ngOnInit() {
324
+ const gesture = this.gesture === false || this.config.gesture === false ? false : { ...(this.config.gesture || {}), ...(this.gesture || {}) };
325
+ this.stack = createIOSStack({
326
+ container: this.host,
327
+ transition: { ...this.config.transition, ...(this.transition || {}) },
328
+ gesture: gesture || {},
329
+ });
330
+ if (gesture === false)
331
+ this.stack.gesture.detach();
332
+ if (this.config.injectStyles)
333
+ injectStyles(this.document);
334
+ this.stack.on('pop', (e) => this.onStackRemoved(e.removed, e.source));
335
+ this.stack.on('replace', (e) => this.onStackRemoved(e.removed, e.source));
336
+ this.stack.on('reset', (e) => this.onStackRemoved(e.removed, e.source));
337
+ if (this.config.detachInactiveViews) {
338
+ this.stack.on('transitionstart', ({ lower, upper }) => {
339
+ for (const entry of [lower, upper]) {
340
+ const view = entry && this.byEl.get(entry.el);
341
+ if (view)
342
+ view.ref.changeDetectorRef.reattach();
343
+ }
344
+ });
345
+ for (const event of ['push', 'pop', 'replace', 'reset'])
346
+ this.stack.on(event, () => this.syncChangeDetection());
347
+ }
348
+ this.subs.add(this.router.events.subscribe((e) => {
349
+ if (e instanceof NavigationCancel || e instanceof NavigationError || e instanceof NavigationSkipped)
350
+ this.restorePending();
351
+ else if (e instanceof NavigationEnd && this.activeView)
352
+ this.activeView.url = e.urlAfterRedirects;
353
+ }));
354
+ this.parentContexts.onChildOutletCreated(this.name, this);
355
+ const context = this.parentContexts.getContext(this.name);
356
+ if (context?.route) {
357
+ if (context.attachRef)
358
+ this.attach(context.attachRef, context.route);
359
+ else
360
+ this.activateWith(context.route, context.injector);
361
+ }
362
+ }
363
+ ngOnDestroy() {
364
+ if (this.parentContexts.getContext(this.name)?.outlet === this)
365
+ this.parentContexts.onChildOutletDestroyed(this.name);
366
+ this.subs.unsubscribe();
367
+ for (const view of this.byEl.values())
368
+ view.inputs?.unsubscribe();
369
+ this.stack?.destroy();
370
+ }
371
+ // -------------------------------------------------------- outlet contract
372
+ get isActivated() {
373
+ return !!this.activated;
374
+ }
375
+ get component() {
376
+ if (!this.activated)
377
+ throw new Error('Outlet is not activated');
378
+ return this.activated.instance;
379
+ }
380
+ get activatedComponentRef() {
381
+ return this.activated;
382
+ }
383
+ get activatedRoute() {
384
+ if (!this.activated)
385
+ throw new Error('Outlet is not activated');
386
+ return this._activatedRoute;
387
+ }
388
+ get activatedRouteData() {
389
+ return this._activatedRoute ? this._activatedRoute.snapshot.data : {};
390
+ }
391
+ /** Pages currently kept, bottom to top. The last one is on screen. */
392
+ get pages() {
393
+ return this.views;
394
+ }
395
+ /** Whether a swipe has a kept page to reveal. */
396
+ get canPop() {
397
+ return this.views.length > 1;
398
+ }
399
+ activateWith(activatedRoute, environmentInjector) {
400
+ if (this.activated)
401
+ throw new Error('Cannot activate an already activated outlet');
402
+ const snapshot = activatedRoute.snapshot;
403
+ const key = this.config.keyOf(snapshot);
404
+ const leaving = this.takeLeaving();
405
+ let existing = this.views.find((v) => v.key === key) ?? null;
406
+ if (existing === leaving)
407
+ existing = null;
408
+ const view = existing ?? this.createView(snapshot.component, activatedRoute, environmentInjector, key);
409
+ this.show(view, activatedRoute, leaving, !!existing);
410
+ }
411
+ deactivate() {
412
+ if (!this.activated)
413
+ return;
414
+ const view = this.activeView;
415
+ this.unbindInputs(view);
416
+ const context = this.parentContexts.getContext(this.name);
417
+ if (context)
418
+ view.savedContexts = context.children.contexts;
419
+ this.activated = null;
420
+ this._activatedRoute = null;
421
+ this.activeView = null;
422
+ if (view.pendingRemoval) {
423
+ this.destroyView(view);
424
+ return;
425
+ }
426
+ // The router deactivates before it activates, synchronously. If no
427
+ // activation follows, the outlet is really empty.
428
+ this.leaving = view;
429
+ queueMicrotask(() => {
430
+ if (this.leaving !== view)
431
+ return;
432
+ this.leaving = null;
433
+ this.views = [];
434
+ void this.stack.reset([]);
435
+ });
436
+ }
437
+ detach() {
438
+ if (!this.activated)
439
+ throw new Error('Outlet is not activated');
440
+ const view = this.activeView;
441
+ const ref = view.ref;
442
+ this.unbindInputs(view);
443
+ this.activated = null;
444
+ this._activatedRoute = null;
445
+ this.activeView = null;
446
+ this.byEl.delete(view.el);
447
+ this.views = this.views.filter((v) => v !== view);
448
+ this.detached.set(ref, view);
449
+ void this.stack.remove(view.el);
450
+ const i = this.location.indexOf(ref.hostView);
451
+ if (i >= 0)
452
+ this.location.detach(i);
453
+ this.detachEvents.emit(ref.instance);
454
+ return ref;
455
+ }
456
+ attach(ref, activatedRoute) {
457
+ const key = this.config.keyOf(activatedRoute.snapshot);
458
+ let view = this.detached.get(ref) ?? null;
459
+ this.detached.delete(ref);
460
+ if (!view)
461
+ view = this.wrap(ref, activatedRoute, key, null);
462
+ this.location.insert(ref.hostView);
463
+ this.park(view.el);
464
+ this.byEl.set(view.el, view);
465
+ const leaving = this.takeLeaving();
466
+ this.show(view, activatedRoute, leaving, true);
467
+ }
468
+ // -------------------------------------------------------------- internals
469
+ takeLeaving() {
470
+ const leaving = this.leaving;
471
+ this.leaving = null;
472
+ return leaving;
473
+ }
474
+ /** Decides the direction, places the page in the stack, and makes it active. */
475
+ show(view, activatedRoute, leaving, reused) {
476
+ const nav = this.history.current;
477
+ const from = leaving ?? this.views[this.views.length - 1] ?? null;
478
+ // Resolvers may have rerun, and a custom keyOf may group several snapshots, so the old snapshot cannot be trusted.
479
+ view.routeRef = this.routeRefOf(activatedRoute.snapshot, view.key);
480
+ const alreadyOnScreen = reused && !this.stack.busy && this.stack.top?.el === view.el;
481
+ let direction = this.config.resolve({
482
+ from: from?.routeRef ?? null,
483
+ to: view.routeRef,
484
+ trigger: nav?.trigger ?? 'imperative',
485
+ historyDelta: nav?.historyDelta,
486
+ hint: nav?.hint,
487
+ stack: this.views.map((v) => v.key),
488
+ });
489
+ // After a swipe the page beneath is already showing and the one that left
490
+ // is gone. A pop onto anything else has nothing to pop, so just show the page.
491
+ if (!leaving && this.stack.top && direction === 'pop' && !reused)
492
+ direction = 'replace';
493
+ const animated = this.config.animated && (nav?.animated ?? true) && (this.views.length > 0 || !!leaving);
494
+ view.route = activatedRoute;
495
+ view.proxy?.swap(activatedRoute);
496
+ const current = this.router.getCurrentNavigation();
497
+ view.url = current ? this.router.serializeUrl(current.finalUrl ?? current.extractedUrl) : this.router.url;
498
+ if (view.savedContexts) {
499
+ this.parentContexts.getOrCreateContext(this.name).children.onOutletReAttached(view.savedContexts);
500
+ view.savedContexts = null;
501
+ }
502
+ this.activated = view.ref;
503
+ this._activatedRoute = activatedRoute;
504
+ this.activeView = view;
505
+ this.lastDirection = direction;
506
+ this.place(view, direction, leaving);
507
+ if (!alreadyOnScreen) {
508
+ void this.stack.present(view.el, direction, { key: view.key, animated, source: sourceOf(nav?.trigger) });
509
+ }
510
+ this.changeDetector.markForCheck();
511
+ this.bindInputs(view);
512
+ // An animated page renders off screen during its first frames. A page that
513
+ // appears immediately (no animation, or a replace, which the stack never
514
+ // animates) would otherwise be blank until the next scheduled tick.
515
+ if ((!animated || direction === 'replace') && !alreadyOnScreen)
516
+ view.ref.changeDetectorRef.detectChanges();
517
+ (reused ? this.attachEvents : this.activateEvents).emit(view.ref.instance);
518
+ this.navigatedEvents.emit({ view, direction, animated, reused });
519
+ }
520
+ /** Mirrors what the stack will do, synchronously, so `pages` and the next direction stay correct. */
521
+ place(view, direction, leaving) {
522
+ const views = this.views;
523
+ const drop = (v) => {
524
+ const i = v ? views.indexOf(v) : -1;
525
+ if (i >= 0)
526
+ views.splice(i, 1);
527
+ };
528
+ if (direction === 'pop' && views.includes(view)) {
529
+ views.splice(views.indexOf(view) + 1);
530
+ return;
531
+ }
532
+ if (direction !== 'push')
533
+ drop(leaving);
534
+ drop(view);
535
+ views.push(view);
536
+ }
537
+ createView(component, route, environmentInjector, key) {
538
+ const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
539
+ const proxy = new StackNavActivatedRoute(route);
540
+ const injector = Injector.create({
541
+ providers: [
542
+ { provide: ActivatedRoute, useValue: proxy },
543
+ { provide: ChildrenOutletContexts, useValue: childContexts },
544
+ { provide: ROUTER_OUTLET_DATA, useValue: this.routerOutletData },
545
+ ],
546
+ parent: this.location.injector,
547
+ });
548
+ const ref = this.location.createComponent(component, { index: this.location.length, injector, environmentInjector });
549
+ const view = this.wrap(ref, route, key, proxy);
550
+ this.park(view.el);
551
+ this.byEl.set(view.el, view);
552
+ return view;
553
+ }
554
+ wrap(ref, route, key, proxy) {
555
+ const el = ref.location.nativeElement;
556
+ return { ref, el, key, routeRef: this.routeRefOf(route.snapshot, key), url: '', route, proxy, savedContexts: null, pendingRemoval: false, inputs: null };
557
+ }
558
+ /** Hidden inside the container until the stack shows it, never a visible sibling of the outlet. */
559
+ park(el) {
560
+ el.classList.add(this.stack.pageClass);
561
+ if (el.parentElement !== this.host)
562
+ this.host.append(el);
563
+ }
564
+ routeRefOf(snapshot, key) {
565
+ return { key, segments: segmentsOf(key), level: this.config.levelOf(snapshot), data: snapshot.data, snapshot };
566
+ }
567
+ onStackRemoved(removed, source) {
568
+ for (const entry of removed) {
569
+ const view = this.byEl.get(entry.el);
570
+ if (!view)
571
+ continue;
572
+ if (source === 'gesture') {
573
+ view.pendingRemoval = true;
574
+ this.views = this.views.filter((v) => v !== view);
575
+ this.navigateBackAfterGesture();
576
+ }
577
+ else {
578
+ this.destroyView(view);
579
+ }
580
+ }
581
+ }
582
+ /** The swipe already revealed the page beneath. Bring the router in line with it. */
583
+ navigateBackAfterGesture() {
584
+ const lower = this.views[this.views.length - 1];
585
+ if (!lower)
586
+ return;
587
+ const previous = this.history.previousUrl;
588
+ if (previous != null && this.sameUrl(previous, lower.url)) {
589
+ this.browserLocation.back();
590
+ }
591
+ else {
592
+ void this.router.navigateByUrl(lower.url, { info: { [this.config.infoKey]: { direction: 'pop', animated: false } } });
593
+ }
594
+ }
595
+ sameUrl(a, b) {
596
+ const norm = (u) => this.router.serializeUrl(this.router.parseUrl(u));
597
+ return norm(a) === norm(b);
598
+ }
599
+ /** The router refused the navigation the swipe asked for, so put the page back. */
600
+ restorePending() {
601
+ for (const view of this.byEl.values()) {
602
+ if (!view.pendingRemoval)
603
+ continue;
604
+ view.pendingRemoval = false;
605
+ this.views.push(view);
606
+ void this.stack.push(view.el, { animated: false, key: view.key, source: 'restore' });
607
+ }
608
+ }
609
+ destroyView(view) {
610
+ this.byEl.delete(view.el);
611
+ this.views = this.views.filter((v) => v !== view);
612
+ this.unbindInputs(view);
613
+ if (this.activeView === view) {
614
+ this.activeView = null;
615
+ this.activated = null;
616
+ this._activatedRoute = null;
617
+ }
618
+ const instance = view.ref.instance;
619
+ view.ref.destroy();
620
+ this.deactivateEvents.emit(instance);
621
+ }
622
+ bindInputs(view) {
623
+ if (!this.router.componentInputBindingEnabled)
624
+ return;
625
+ const mirror = reflectComponentType(view.ref.componentType);
626
+ if (!mirror)
627
+ return;
628
+ const route = view.route;
629
+ view.inputs = combineLatest([route.queryParams, route.params, route.data])
630
+ .pipe(switchMap(([queryParams, params, data], i) => (i === 0 ? of({ ...queryParams, ...params, ...data }) : from(Promise.resolve({ ...queryParams, ...params, ...data })))))
631
+ .subscribe((data) => {
632
+ if (this.activeView !== view)
633
+ return;
634
+ for (const { templateName } of mirror.inputs)
635
+ view.ref.setInput(templateName, data[templateName]);
636
+ });
637
+ }
638
+ unbindInputs(view) {
639
+ view.inputs?.unsubscribe();
640
+ view.inputs = null;
641
+ }
642
+ syncChangeDetection() {
643
+ const top = this.stack.top?.el;
644
+ for (const view of this.byEl.values()) {
645
+ if (view.el === top || view.pendingRemoval)
646
+ view.ref.changeDetectorRef.reattach();
647
+ else
648
+ view.ref.changeDetectorRef.detach();
649
+ }
650
+ }
651
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StackNavOutlet, deps: [], target: i0.ɵɵFactoryTarget.Directive });
652
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: StackNavOutlet, isStandalone: true, selector: "sn-outlet", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: false, isRequired: false, transformFunction: null }, transition: { classPropertyName: "transition", publicName: "transition", isSignal: false, isRequired: false, transformFunction: null }, gesture: { classPropertyName: "gesture", publicName: "gesture", isSignal: false, isRequired: false, transformFunction: null }, routerOutletData: { classPropertyName: "routerOutletData", publicName: "routerOutletData", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activateEvents: "activate", deactivateEvents: "deactivate", attachEvents: "attach", detachEvents: "detach", navigatedEvents: "navigated" }, host: { styleAttribute: "display: block" }, exportAs: ["snOutlet"], ngImport: i0 });
653
+ }
654
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StackNavOutlet, decorators: [{
655
+ type: Directive,
656
+ args: [{
657
+ selector: 'sn-outlet',
658
+ exportAs: 'snOutlet',
659
+ host: { style: 'display: block' },
660
+ }]
661
+ }], ctorParameters: () => [], propDecorators: { name: [{
662
+ type: Input
663
+ }], transition: [{
664
+ type: Input
665
+ }], gesture: [{
666
+ type: Input
667
+ }], routerOutletData: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerOutletData", required: false }] }], activateEvents: [{
668
+ type: Output,
669
+ args: ['activate']
670
+ }], deactivateEvents: [{
671
+ type: Output,
672
+ args: ['deactivate']
673
+ }], attachEvents: [{
674
+ type: Output,
675
+ args: ['attach']
676
+ }], detachEvents: [{
677
+ type: Output,
678
+ args: ['detach']
679
+ }], navigatedEvents: [{
680
+ type: Output,
681
+ args: ['navigated']
682
+ }] } });
683
+ function sourceOf(trigger) {
684
+ return trigger === 'history' ? 'history' : 'api';
685
+ }
686
+
687
+ /**
688
+ * The router's default strategy reuses a component when only the params change
689
+ * (`/items/1` → `/items/2`), so no outlet activation happens and no transition
690
+ * can run. This strategy asks for a fresh page whenever the URL of the matched
691
+ * route differs, which is what a navigation stack expects. Routes opt out with
692
+ * `data: { reuseRoute: true }`.
693
+ *
694
+ * It is not installed automatically. Provide it like any other strategy to get
695
+ * this behaviour:
696
+ *
697
+ * ```ts
698
+ * { provide: RouteReuseStrategy, useClass: StackNavRouteReuseStrategy }
699
+ * ```
700
+ */
701
+ class StackNavRouteReuseStrategy extends BaseRouteReuseStrategy {
702
+ shouldReuseRoute(future, curr) {
703
+ if (future.routeConfig !== curr.routeConfig)
704
+ return false;
705
+ if (future.data?.['reuseRoute'] === true)
706
+ return true;
707
+ return urlOf(future) === urlOf(curr);
708
+ }
709
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StackNavRouteReuseStrategy, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
710
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StackNavRouteReuseStrategy });
711
+ }
712
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: StackNavRouteReuseStrategy, decorators: [{
713
+ type: Injectable
714
+ }] });
715
+ function urlOf(s) {
716
+ return s.url.map((u) => u.toString()).join('/');
717
+ }
718
+
719
+ /**
720
+ * Generated bundle index. Do not edit.
721
+ */
722
+
723
+ export { STACKNAV_CONFIG, StackNavActivatedRoute, StackNavOutlet, StackNavRouteReuseStrategy, defaultKeyOf, defaultLevelOf, provideStackNav, resolveConfig };
724
+ //# sourceMappingURL=stacknav-angular.mjs.map