ngx-stack 22.0.0
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.
- package/LICENSE +21 -0
- package/README.md +760 -0
- package/fesm2022/ngx-stack.mjs +2391 -0
- package/fesm2022/ngx-stack.mjs.map +1 -0
- package/package.json +56 -0
- package/schematics/migration.json +10 -0
- package/schematics/migrations/v23/index.js +62 -0
- package/schematics/package.json +3 -0
- package/types/ngx-stack.d.ts +1075 -0
|
@@ -0,0 +1,1075 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, EnvironmentProviders, ComponentRef, Signal, OnDestroy, ViewContainerRef, EnvironmentInjector, ChangeDetectorRef, Type, Injector, OnInit, EventEmitter } from '@angular/core';
|
|
3
|
+
import { ActivatedRoute, OutletContext, NavigationExtras, RouterOutletContract, Data, RouteReuseStrategy, DetachedRouteHandle, ActivatedRouteSnapshot, Routes } from '@angular/router';
|
|
4
|
+
import * as ngx_stack from 'ngx-stack';
|
|
5
|
+
|
|
6
|
+
/** Which way the stack is moving. */
|
|
7
|
+
type StackDirection = 'forward' | 'back';
|
|
8
|
+
interface TransitionContext {
|
|
9
|
+
/**
|
|
10
|
+
* The page that will be on top of the stack once the transition finishes. On `forward` this is
|
|
11
|
+
* the newly created page; on `back` it is the page being uncovered.
|
|
12
|
+
*/
|
|
13
|
+
enteringEl: HTMLElement;
|
|
14
|
+
/**
|
|
15
|
+
* The page that was on top when the transition started, or `null` when the stack was empty
|
|
16
|
+
* (the root page).
|
|
17
|
+
*/
|
|
18
|
+
leavingEl: HTMLElement | null;
|
|
19
|
+
/** The element both pages live in. */
|
|
20
|
+
hostEl: HTMLElement;
|
|
21
|
+
direction: StackDirection;
|
|
22
|
+
/**
|
|
23
|
+
* The app is running right-to-left. Everything horizontal mirrors: "forward" arrives from the
|
|
24
|
+
* left rather than the right, and the swipe-back lives on the right edge. A transition that
|
|
25
|
+
* ignores this will feel backwards in Arabic and Hebrew — the new page will appear to come
|
|
26
|
+
* from where the user just came *from*.
|
|
27
|
+
*/
|
|
28
|
+
rtl: boolean;
|
|
29
|
+
/** Host width in px, for transitions that want absolute distances. */
|
|
30
|
+
width: number;
|
|
31
|
+
/** Requested duration in ms, from the config. */
|
|
32
|
+
duration: number;
|
|
33
|
+
}
|
|
34
|
+
interface ElementAnimation {
|
|
35
|
+
el: HTMLElement;
|
|
36
|
+
/**
|
|
37
|
+
* The first keyframe is the state at progress 0, the last at progress 1. A swipe-back gesture
|
|
38
|
+
* seeks between them, so keyframes must be continuous and reversible — avoid discrete steps or
|
|
39
|
+
* properties that can't be interpolated.
|
|
40
|
+
*/
|
|
41
|
+
keyframes: Keyframe[];
|
|
42
|
+
/** Overrides the spec-level easing for this element. */
|
|
43
|
+
easing?: string;
|
|
44
|
+
}
|
|
45
|
+
interface TransitionSpec {
|
|
46
|
+
duration: number;
|
|
47
|
+
easing: string;
|
|
48
|
+
animations: ElementAnimation[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Describes how two pages swap places. Called once per transition, and also at the start of a
|
|
52
|
+
* swipe-back — in that case the resulting animation is seeked by the finger rather than played,
|
|
53
|
+
* so it must be a pure function of progress.
|
|
54
|
+
*/
|
|
55
|
+
type StackTransition = (ctx: TransitionContext) => TransitionSpec;
|
|
56
|
+
/** The dim overlay that sits on top of a page while it is covered by another. */
|
|
57
|
+
declare function scrimOf(pageEl: HTMLElement): HTMLElement | null;
|
|
58
|
+
|
|
59
|
+
/** The three surfaces that get their own transition. */
|
|
60
|
+
type StackPlatformKind = 'ios' | 'android' | 'web';
|
|
61
|
+
interface StackPlatform {
|
|
62
|
+
/** Which transition this device gets by default. Decided by OS, not by browser vs native. */
|
|
63
|
+
readonly kind: StackPlatformKind;
|
|
64
|
+
readonly isIos: boolean;
|
|
65
|
+
readonly isAndroid: boolean;
|
|
66
|
+
/** Running inside a Capacitor native webview. */
|
|
67
|
+
readonly isCapacitor: boolean;
|
|
68
|
+
/** Running inside a Cordova / PhoneGap native webview. */
|
|
69
|
+
readonly isCordova: boolean;
|
|
70
|
+
/** Either of the above: a native webview shell rather than a real browser. */
|
|
71
|
+
readonly isNative: boolean;
|
|
72
|
+
/** Installed to the home screen and running without browser chrome. */
|
|
73
|
+
readonly isStandalonePwa: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* True when the browser runs its own interactive back gesture at the screen edge, so we have to
|
|
76
|
+
* share (or fight over) those pixels.
|
|
77
|
+
*
|
|
78
|
+
* That means iOS Safari and iOS PWAs. Native shells — Capacitor and Cordova alike — leave
|
|
79
|
+
* `allowsBackForwardNavigationGestures` off in their WKWebView, so the edge is ours alone. Which
|
|
80
|
+
* is why native is the easy target and the browser is the awkward one.
|
|
81
|
+
*/
|
|
82
|
+
readonly hasSystemBackGesture: boolean;
|
|
83
|
+
}
|
|
84
|
+
declare const NGX_STACK_PLATFORM: InjectionToken<StackPlatform>;
|
|
85
|
+
|
|
86
|
+
/** A transition per platform. Anything you leave out keeps the built-in for that platform. */
|
|
87
|
+
interface StackTransitionMap {
|
|
88
|
+
ios?: StackTransition;
|
|
89
|
+
android?: StackTransition;
|
|
90
|
+
web?: StackTransition;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* What to do about the browser's *own* edge-swipe back gesture.
|
|
94
|
+
*
|
|
95
|
+
* Only relevant on iOS Safari and iOS standalone PWAs — WebKit reserves the screen edge for
|
|
96
|
+
* its own interactive back navigation and gives you no way to turn it off. Under Capacitor
|
|
97
|
+
* the webview has no such gesture, so this setting does nothing there.
|
|
98
|
+
*/
|
|
99
|
+
type SystemGesturePolicy =
|
|
100
|
+
/**
|
|
101
|
+
* Start our gesture zone a few px inland so the two don't fight over the same pixels. The
|
|
102
|
+
* system still owns the outermost `systemEdgeInset` px; when it fires we notice and skip
|
|
103
|
+
* our own animation rather than drawing a second one on top of WebKit's.
|
|
104
|
+
*/
|
|
105
|
+
'inset'
|
|
106
|
+
/**
|
|
107
|
+
* `preventDefault()` the touchstart inside the edge zone, which stops WebKit starting its
|
|
108
|
+
* gesture. Reliable, but it also suppresses the synthetic `click` for touches beginning
|
|
109
|
+
* there — don't use it if you have tappable UI within `swipeEdgeWidth` px of the edge.
|
|
110
|
+
*/
|
|
111
|
+
| 'suppress'
|
|
112
|
+
/** Do nothing, and let both gestures coexist. */
|
|
113
|
+
| 'ignore';
|
|
114
|
+
/**
|
|
115
|
+
* What to do when the page on top has a `canDeactivate` guard.
|
|
116
|
+
*
|
|
117
|
+
* The gesture animates the page away *before* the navigation runs, so a guard that later says
|
|
118
|
+
* no leaves us with nothing to do but snap the page back — a visible, ugly bounce. Guards can
|
|
119
|
+
* also be async, and a touch handler cannot wait for a promise.
|
|
120
|
+
*/
|
|
121
|
+
type GuardPolicy =
|
|
122
|
+
/**
|
|
123
|
+
* **Default.** A route with a `canDeactivate` guard is not swipeable, unless its component
|
|
124
|
+
* implements `ngxCanSwipeBack()` — which is synchronous, so the gesture can simply not start.
|
|
125
|
+
* Your back button still runs the guard normally, and that's where a confirm dialog belongs.
|
|
126
|
+
*/
|
|
127
|
+
'block'
|
|
128
|
+
/**
|
|
129
|
+
* Let the gesture run and take the bounce if the guard refuses. Pick this only when you know
|
|
130
|
+
* your guards effectively always pass.
|
|
131
|
+
*/
|
|
132
|
+
| 'allow';
|
|
133
|
+
interface NgxStackConfig {
|
|
134
|
+
/** Force a platform instead of detecting it. Handy for testing an iOS build on a laptop. */
|
|
135
|
+
platform?: StackPlatformKind | 'auto';
|
|
136
|
+
/**
|
|
137
|
+
* Writing direction. `'auto'` reads the computed `direction` off the host element, so an
|
|
138
|
+
* app that sets `<html dir="rtl">` mirrors everything with no configuration: the transitions
|
|
139
|
+
* come from the other side and the swipe lives on the right edge.
|
|
140
|
+
*/
|
|
141
|
+
direction?: 'auto' | 'ltr' | 'rtl';
|
|
142
|
+
/**
|
|
143
|
+
* The page transition. Pass one function to use it everywhere, or a
|
|
144
|
+
* {@link StackTransitionMap} to vary it by platform.
|
|
145
|
+
*/
|
|
146
|
+
transitions?: StackTransition | StackTransitionMap;
|
|
147
|
+
/**
|
|
148
|
+
* Base duration in ms. Built-in transitions scale this to taste (Android and web run shorter
|
|
149
|
+
* than iOS). A custom transition returns its own duration and can ignore it.
|
|
150
|
+
*/
|
|
151
|
+
duration?: number;
|
|
152
|
+
/**
|
|
153
|
+
* The tabs, as URL path prefixes: `['home', 'search', 'profile']`.
|
|
154
|
+
*
|
|
155
|
+
* Set this and each one gets its own independent stack. Switching tabs unwinds nothing and
|
|
156
|
+
* destroys nothing — the tab you left keeps its pages mounted, exactly where you were, and comes
|
|
157
|
+
* back that way. Leave it unset and there's a single stack, which is the common case.
|
|
158
|
+
*
|
|
159
|
+
* It has to be declared, because nothing in a route config distinguishes "these are siblings you
|
|
160
|
+
* switch between, each keeping its own history" from "these are just different URLs". `/settings`
|
|
161
|
+
* is a tab and `/settings/sheet` is a page inside it; only you know that.
|
|
162
|
+
*
|
|
163
|
+
* It isn't repeated anywhere, though — {@link NgxStackTabs} exposes it as `tabs()`, so the same
|
|
164
|
+
* declaration also renders your tab bar. And a route whose URL doesn't happen to start with its
|
|
165
|
+
* tab's name can say where it belongs with `data: { tab: 'search' }`.
|
|
166
|
+
*/
|
|
167
|
+
tabs?: string[];
|
|
168
|
+
/**
|
|
169
|
+
* Rebuild the pages that *should* have been underneath, when the app opens partway in — a push
|
|
170
|
+
* notification, a shared link, a refresh three screens deep.
|
|
171
|
+
*
|
|
172
|
+
* Without this the router lands you on the detail page with a stack of exactly one: nothing for a
|
|
173
|
+
* swipe to drag into view. (The back *button* is a separate matter and always works — declare
|
|
174
|
+
* `data: { parent: '/inbox' }` on the route.)
|
|
175
|
+
*
|
|
176
|
+
* - `true` — work the ancestors out from the route config: `data: { parent }` where you've
|
|
177
|
+
* declared one, otherwise the URL's own nesting (`/inbox/item/12/notes` → `/inbox/item/12` →
|
|
178
|
+
* `/inbox`).
|
|
179
|
+
* - a function — your own `(url) => parentUrl | null`, applied repeatedly to build a chain. For a
|
|
180
|
+
* URL scheme whose nesting doesn't reflect the screens, or routes behind `loadChildren`, whose
|
|
181
|
+
* children can't be inspected without loading them.
|
|
182
|
+
*
|
|
183
|
+
* Nothing else to set up: the first navigation is intercepted before anything is built, so the
|
|
184
|
+
* deep page is still constructed exactly once — at the end, on top of its ancestors.
|
|
185
|
+
*
|
|
186
|
+
* The cost is real, though: the ancestors are built and **their resolvers run**, for pages the
|
|
187
|
+
* user may never look at. If your list page does something expensive on load, leave this off and
|
|
188
|
+
* let the first back be a tap.
|
|
189
|
+
*/
|
|
190
|
+
deepLinks?: boolean | ((url: string) => string | null);
|
|
191
|
+
/**
|
|
192
|
+
* Cap on how many pages one stack keeps mounted. `0` (the default) means no cap.
|
|
193
|
+
*
|
|
194
|
+
* Pages are cheap but not free, and a stack you can descend forever — a chat, a wiki, a file
|
|
195
|
+
* browser — will happily hold fifty live components. With a cap, pages that fall off the
|
|
196
|
+
* bottom are destroyed. Going back past that horizon still works: the page is rebuilt from
|
|
197
|
+
* its URL and still animates as a *back*, so nothing looks wrong. It just lost its state,
|
|
198
|
+
* which is the trade you asked for.
|
|
199
|
+
*/
|
|
200
|
+
maxDepth?: number;
|
|
201
|
+
/**
|
|
202
|
+
* Move focus into the page that just entered, and announce it to screen readers. Default
|
|
203
|
+
* `true`. Without this, a screen-reader user gets no signal that the page changed and their
|
|
204
|
+
* focus stays on a control that has now slid off the screen.
|
|
205
|
+
*/
|
|
206
|
+
manageFocus?: boolean;
|
|
207
|
+
/** See {@link GuardPolicy}. */
|
|
208
|
+
guardPolicy?: GuardPolicy;
|
|
209
|
+
/**
|
|
210
|
+
* Interactive swipe-to-go-back. `'auto'` arms it on iOS only — Android's back gesture belongs
|
|
211
|
+
* to the OS, and the default web transition is too subtle to scrub usefully.
|
|
212
|
+
*
|
|
213
|
+
* Only the starting value; {@link NgxStackSwipe} changes it at runtime.
|
|
214
|
+
*/
|
|
215
|
+
swipeBack?: boolean | 'auto';
|
|
216
|
+
/** Width of the edge zone, in px, in which a swipe can begin. Mirrored in RTL. */
|
|
217
|
+
swipeEdgeWidth?: number;
|
|
218
|
+
/** Progress in `[0, 1]` past which releasing the finger completes the pop. */
|
|
219
|
+
swipeThreshold?: number;
|
|
220
|
+
/** px/ms. A flick faster than this decides on its own, whatever the progress. */
|
|
221
|
+
swipeVelocityThreshold?: number;
|
|
222
|
+
/** See {@link SystemGesturePolicy}. */
|
|
223
|
+
systemGesture?: SystemGesturePolicy;
|
|
224
|
+
/** With `systemGesture: 'inset'`, how many px at the very edge to concede to the browser. */
|
|
225
|
+
systemEdgeInset?: number;
|
|
226
|
+
/** Also allow dragging with a mouse. For developing the gesture in a desktop browser. */
|
|
227
|
+
swipeWithMouse?: boolean;
|
|
228
|
+
/** Animate the very first page pushed onto an empty stack. */
|
|
229
|
+
animateRoot?: boolean;
|
|
230
|
+
/**
|
|
231
|
+
* Honour `prefers-reduced-motion: reduce` by making transitions instant. Default `true`.
|
|
232
|
+
*
|
|
233
|
+
* The swipe-back keeps its animation regardless, on purpose: motion the user is dragging with
|
|
234
|
+
* their own finger is direct manipulation, which the guidance exempts — and a swipe with no
|
|
235
|
+
* visual response isn't reduced motion, it's a broken gesture.
|
|
236
|
+
*/
|
|
237
|
+
respectReducedMotion?: boolean;
|
|
238
|
+
}
|
|
239
|
+
type ResolvedStackConfig = Required<Omit<NgxStackConfig, 'transitions' | 'tabs'>> & Pick<NgxStackConfig, 'transitions' | 'tabs'>;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Wire up the stack. One call, everything in it.
|
|
243
|
+
*
|
|
244
|
+
* ```ts
|
|
245
|
+
* bootstrapApplication(App, {
|
|
246
|
+
* providers: [
|
|
247
|
+
* provideRouter(routes),
|
|
248
|
+
* provideNgxStack({
|
|
249
|
+
* transitions: { ios: iosTransition, android: androidTransition, web: noneTransition },
|
|
250
|
+
* tabs: ['inbox', 'search'],
|
|
251
|
+
* deepLinks: true, // needs withDisabledInitialNavigation() — see below
|
|
252
|
+
* }),
|
|
253
|
+
* ],
|
|
254
|
+
* });
|
|
255
|
+
* ```
|
|
256
|
+
*
|
|
257
|
+
* Installs {@link NgxStackRouteReuseStrategy}, which is required — Angular's default strategy
|
|
258
|
+
* recycles a component when only the route params change, which would quietly merge `/item/1` and
|
|
259
|
+
* `/item/2` into one page instead of two.
|
|
260
|
+
*/
|
|
261
|
+
declare function provideNgxStack(config?: NgxStackConfig): EnvironmentProviders;
|
|
262
|
+
/** The slice of `@capacitor/app` we need. Structural, so there is no dependency on Capacitor. */
|
|
263
|
+
interface CapacitorAppLike {
|
|
264
|
+
addListener(eventName: 'backButton', listenerFunc: (event: {
|
|
265
|
+
canGoBack: boolean;
|
|
266
|
+
}) => void): Promise<unknown>;
|
|
267
|
+
exitApp(): Promise<void>;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Route the Android hardware back button (and the Android system back gesture, which Capacitor
|
|
271
|
+
* reports through the same event) into the stack.
|
|
272
|
+
*
|
|
273
|
+
* Pass the plugin in rather than having the library import it, so `@capacitor/app` stays out of the
|
|
274
|
+
* dependency graph of anyone shipping only to the web:
|
|
275
|
+
*
|
|
276
|
+
* ```ts
|
|
277
|
+
* import { App } from '@capacitor/app';
|
|
278
|
+
* provideCapacitorBack(App)
|
|
279
|
+
* ```
|
|
280
|
+
*
|
|
281
|
+
* At the root of the stack this calls `exitApp()`, which is what Android users expect — back on the
|
|
282
|
+
* first screen closes the app rather than doing nothing.
|
|
283
|
+
*
|
|
284
|
+
* The event's own `canGoBack` is deliberately ignored. It describes the *webview's* history, and
|
|
285
|
+
* history is a single linear thread while tabs are several stacks — so it says yes when the only
|
|
286
|
+
* thing behind you is a different tab. We ask the stack instead.
|
|
287
|
+
*/
|
|
288
|
+
declare function provideCapacitorBack(app: CapacitorAppLike): EnvironmentProviders;
|
|
289
|
+
/**
|
|
290
|
+
* The same, for Cordova / PhoneGap.
|
|
291
|
+
*
|
|
292
|
+
* Cordova is a WKWebView (iOS) or WebView (Android) with plugins bolted on, so everything else in
|
|
293
|
+
* this library already works there unchanged — the swipe, the transitions, the stacks, the tabs.
|
|
294
|
+
* The one thing that isn't web is the Android hardware back button, which Cordova delivers as a
|
|
295
|
+
* `backbutton` event on `document` once `deviceready` has fired.
|
|
296
|
+
*
|
|
297
|
+
* ```ts
|
|
298
|
+
* provideCordovaBack()
|
|
299
|
+
* ```
|
|
300
|
+
*
|
|
301
|
+
* Nothing to pass in: unlike Capacitor, Cordova's API is a global. At the root of the stack this
|
|
302
|
+
* calls `navigator.app.exitApp()`.
|
|
303
|
+
*/
|
|
304
|
+
declare function provideCordovaBack(): EnvironmentProviders;
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* A page transition that can be either played or scrubbed.
|
|
308
|
+
*
|
|
309
|
+
* This is the whole reason the library exists. A CSS transition or a View Transition is
|
|
310
|
+
* fire-and-forget: it runs to completion on its own clock. A swipe-back is the opposite —
|
|
311
|
+
* the finger owns the clock, the transition has to follow it, and the user may change
|
|
312
|
+
* their mind halfway and drag right back.
|
|
313
|
+
*
|
|
314
|
+
* So a transition is built as a set of paused Web Animations with `fill: 'both'`. Paused at
|
|
315
|
+
* `currentTime = 0` they hold their first keyframe; the gesture then writes `currentTime`
|
|
316
|
+
* directly on every touch move.
|
|
317
|
+
*/
|
|
318
|
+
declare class TransitionPlayer {
|
|
319
|
+
private readonly spec;
|
|
320
|
+
private animations;
|
|
321
|
+
private duration;
|
|
322
|
+
private disposed;
|
|
323
|
+
/**
|
|
324
|
+
* @param interactive Build for scrubbing. Timing easing is forced to linear so that
|
|
325
|
+
* seeking to progress `p` puts the page at exactly `p` of the way across — otherwise
|
|
326
|
+
* the iOS curve makes the page lead and then lag the finger, which is precisely the
|
|
327
|
+
* tell that separates a native-feeling swipe from a web one. The real curve is applied
|
|
328
|
+
* later, by `settle()`.
|
|
329
|
+
*/
|
|
330
|
+
constructor(spec: TransitionSpec, interactive?: boolean);
|
|
331
|
+
/** Where the transition currently sits, in `[0, 1]`. */
|
|
332
|
+
get progress(): number;
|
|
333
|
+
/** Jump to a progress in `[0, 1]`. Called on every touch move during a swipe. */
|
|
334
|
+
seek(progress: number): void;
|
|
335
|
+
/** Run to progress 1 on the transition's own clock. */
|
|
336
|
+
play(): Promise<void>;
|
|
337
|
+
/**
|
|
338
|
+
* Hand the clock back to the browser after a scrub, carrying on to `target`.
|
|
339
|
+
*
|
|
340
|
+
* Deliberately *not* a continuation of the scrub timeline. Re-applying the easing curve
|
|
341
|
+
* to a timeline that is already part-way through would snap the page to `easing(t)`, a
|
|
342
|
+
* visible jump at the exact moment the user lets go. Instead we snapshot what is
|
|
343
|
+
* currently on screen and animate from there to the target with the real curve, which is
|
|
344
|
+
* continuous by construction.
|
|
345
|
+
*
|
|
346
|
+
* @param ms How long the remaining distance should take. Scale this by the distance left
|
|
347
|
+
* so a release near the end is quick and one near the start is not.
|
|
348
|
+
*/
|
|
349
|
+
settle(target: 0 | 1, ms: number, easing?: string): Promise<void>;
|
|
350
|
+
private run;
|
|
351
|
+
private settled;
|
|
352
|
+
/** Snap to the end state with no animation. */
|
|
353
|
+
finish(): void;
|
|
354
|
+
/** Drop the animations, reverting the elements to the styles they had before. */
|
|
355
|
+
destroy(): void;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** One page living on the stack. */
|
|
359
|
+
interface StackEntry {
|
|
360
|
+
readonly id: number;
|
|
361
|
+
/**
|
|
362
|
+
* Serialized URL that owns this page, used to recognise it again on a later navigation.
|
|
363
|
+
* Empty for imperative stacks, which have no URL of their own.
|
|
364
|
+
*/
|
|
365
|
+
url: string;
|
|
366
|
+
/**
|
|
367
|
+
* Which tab's stack this page belongs to, or `''` when the app has no tabs. Pages of
|
|
368
|
+
* inactive tabs stay mounted — that is the entire point of tabs having their own stacks.
|
|
369
|
+
*/
|
|
370
|
+
readonly tab: string;
|
|
371
|
+
/** The wrapper we transform. Not the component's own host element — that lives inside. */
|
|
372
|
+
readonly element: HTMLElement;
|
|
373
|
+
/** The dim overlay shown while this page is covered. */
|
|
374
|
+
readonly scrim: HTMLElement;
|
|
375
|
+
readonly ref: ComponentRef<unknown>;
|
|
376
|
+
route: ActivatedRoute | null;
|
|
377
|
+
/**
|
|
378
|
+
* Child outlet contexts captured when this page was navigated away from, so that any nested
|
|
379
|
+
* `<router-outlet>` inside it comes back alive rather than empty when we return.
|
|
380
|
+
*/
|
|
381
|
+
savedContexts?: Map<string, OutletContext>;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Lifecycle callbacks for a page on the stack.
|
|
385
|
+
*
|
|
386
|
+
* Stacked pages are *not* destroyed when you navigate away from them — that's the point, it's
|
|
387
|
+
* what preserves their state and lets the swipe reveal them instantly. Which also means
|
|
388
|
+
* `ngOnDestroy` no longer means "the user left this page". These hooks do.
|
|
389
|
+
*/
|
|
390
|
+
interface NgxStackPage {
|
|
391
|
+
/** The page is about to become the top of the stack. Fires before the transition. */
|
|
392
|
+
ngxViewWillEnter?(): void;
|
|
393
|
+
/** The page is now the top of the stack and the transition has finished. */
|
|
394
|
+
ngxViewDidEnter?(): void;
|
|
395
|
+
/** The page is about to stop being the top of the stack. */
|
|
396
|
+
ngxViewWillLeave?(): void;
|
|
397
|
+
/** The page is no longer the top of the stack. It stays alive unless it was popped. */
|
|
398
|
+
ngxViewDidLeave?(): void;
|
|
399
|
+
/**
|
|
400
|
+
* Veto the swipe-back gesture while this page is on top. Consulted on every touch, so it can
|
|
401
|
+
* depend on live state — an open modal, a dirty form, an in-flight payment.
|
|
402
|
+
*
|
|
403
|
+
* A veto only. It cannot turn the gesture *on* where `NgxStackSwipe` or the outlet have
|
|
404
|
+
* turned it off. For a page that is simply never swipeable, `data: { swipeBack: false }` on
|
|
405
|
+
* the route says the same thing without any code.
|
|
406
|
+
*
|
|
407
|
+
* This is also how a route with a `canDeactivate` guard opts back into the gesture — see
|
|
408
|
+
* `guardPolicy`.
|
|
409
|
+
*/
|
|
410
|
+
ngxCanSwipeBack?(): boolean;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** A swipe-back in progress: the pages involved, and the transition the finger is scrubbing. */
|
|
414
|
+
interface InteractiveBack {
|
|
415
|
+
readonly player: TransitionPlayer;
|
|
416
|
+
/** The page being uncovered — the one that will be on top if the swipe completes. */
|
|
417
|
+
readonly entering: StackEntry;
|
|
418
|
+
/** The page being dragged off — the current top. */
|
|
419
|
+
readonly leaving: StackEntry;
|
|
420
|
+
}
|
|
421
|
+
/** Emitted around every transition, including the ones a finger is driving. */
|
|
422
|
+
interface StackTransitionEvent {
|
|
423
|
+
direction: StackDirection;
|
|
424
|
+
entering: StackEntry;
|
|
425
|
+
leaving: StackEntry | null;
|
|
426
|
+
/** The tab this happened in, or `''` in a single-stack app. */
|
|
427
|
+
tab: string;
|
|
428
|
+
/** False when the pages simply swapped, e.g. a tab switch or reduced motion. */
|
|
429
|
+
animated: boolean;
|
|
430
|
+
/** True when a swipe-back drove it rather than a navigation. */
|
|
431
|
+
interactive: boolean;
|
|
432
|
+
}
|
|
433
|
+
type StackOp =
|
|
434
|
+
/** Mount `entering` on top of `tab`'s stack. */
|
|
435
|
+
{
|
|
436
|
+
kind: 'push';
|
|
437
|
+
tab: string;
|
|
438
|
+
entering: StackEntry;
|
|
439
|
+
animated: boolean;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Swap the top page for `entering`. What `router.navigate(…, { replaceUrl: true })` means for a
|
|
443
|
+
* stack: history didn't grow, so neither should the stack.
|
|
444
|
+
*/
|
|
445
|
+
| {
|
|
446
|
+
kind: 'replace';
|
|
447
|
+
tab: string;
|
|
448
|
+
entering: StackEntry;
|
|
449
|
+
animated: boolean;
|
|
450
|
+
}
|
|
451
|
+
/** Unwind `tab`'s stack to `toIndex`, destroying everything above it. */
|
|
452
|
+
| {
|
|
453
|
+
kind: 'pop';
|
|
454
|
+
tab: string;
|
|
455
|
+
toIndex: number;
|
|
456
|
+
animated: boolean;
|
|
457
|
+
player?: TransitionPlayer;
|
|
458
|
+
}
|
|
459
|
+
/** Replace *every* stack with this one page. */
|
|
460
|
+
| {
|
|
461
|
+
kind: 'root';
|
|
462
|
+
tab: string;
|
|
463
|
+
entering: StackEntry;
|
|
464
|
+
animated: boolean;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Rebuild a page that fell off the bottom of a capped stack, and make it the new bottom.
|
|
468
|
+
* Everything currently in that stack sat above it, so all of it goes.
|
|
469
|
+
*/
|
|
470
|
+
| {
|
|
471
|
+
kind: 'restore';
|
|
472
|
+
tab: string;
|
|
473
|
+
entering: StackEntry;
|
|
474
|
+
animated: boolean;
|
|
475
|
+
};
|
|
476
|
+
/**
|
|
477
|
+
* Owns the pages of a stack — their DOM, their order, and the transitions between them.
|
|
478
|
+
*
|
|
479
|
+
* Deliberately free of framework plumbing: it knows nothing about the Router or about
|
|
480
|
+
* imperative pushes. Both `NgxStackOutlet` and `NgxStack` drive this same class, which is why
|
|
481
|
+
* a swipe-back behaves identically whether the stack is fed by URLs or by `push()`.
|
|
482
|
+
*
|
|
483
|
+
* With `tabs` configured it holds one stack *per tab* and shows the active one. Tabs are not a
|
|
484
|
+
* feature bolted on top — they're the reason the pages live in a map keyed by tab rather than a
|
|
485
|
+
* flat array. Switching tabs mounts nothing and destroys nothing; it just changes which stack's
|
|
486
|
+
* top page is on screen, so the tab you left is still exactly where you left it.
|
|
487
|
+
*/
|
|
488
|
+
declare class StackController {
|
|
489
|
+
private readonly hostEl;
|
|
490
|
+
private readonly config;
|
|
491
|
+
private readonly platform;
|
|
492
|
+
private readonly stacks;
|
|
493
|
+
private readonly active;
|
|
494
|
+
/** URLs evicted by `maxDepth`, per tab. Remembered so we still know they're *behind* us. */
|
|
495
|
+
private readonly pruned;
|
|
496
|
+
/** The active tab's pages, bottom first. */
|
|
497
|
+
readonly pages: Signal<readonly StackEntry[]>;
|
|
498
|
+
readonly depth: Signal<number>;
|
|
499
|
+
readonly canGoBack: Signal<boolean>;
|
|
500
|
+
readonly activeTab: Signal<string>;
|
|
501
|
+
private readonly _animating;
|
|
502
|
+
readonly animating: Signal<boolean>;
|
|
503
|
+
/** Set by the host so it can re-emit these as component outputs. */
|
|
504
|
+
onTransitionStart?: (event: StackTransitionEvent) => void;
|
|
505
|
+
onTransitionEnd?: (event: StackTransitionEvent) => void;
|
|
506
|
+
private nextId;
|
|
507
|
+
private running;
|
|
508
|
+
private readonly doc;
|
|
509
|
+
constructor(hostEl: HTMLElement, config: ResolvedStackConfig, platform: StackPlatform);
|
|
510
|
+
stackOf(tab: string): readonly StackEntry[];
|
|
511
|
+
top(tab?: string): StackEntry | null;
|
|
512
|
+
at(index: number, tab?: string): StackEntry | null;
|
|
513
|
+
findByUrl(url: string, tab?: string): number;
|
|
514
|
+
/** Was this URL evicted by `maxDepth`? If so it's behind us, not ahead of us. */
|
|
515
|
+
wasPruned(url: string, tab?: string): boolean;
|
|
516
|
+
private hasPrunedPages;
|
|
517
|
+
/** Wrap a freshly created component in a page element and put it in the host. */
|
|
518
|
+
adopt(ref: ComponentRef<unknown>, url: string, route: ActivatedRoute | null, tab?: string): StackEntry;
|
|
519
|
+
/**
|
|
520
|
+
* Apply a stack operation. The stacks are updated *synchronously* so a navigation arriving
|
|
521
|
+
* mid-animation still sees the correct top; only the visuals and the destruction of popped
|
|
522
|
+
* pages wait for the transition.
|
|
523
|
+
*/
|
|
524
|
+
run(op: StackOp): Promise<void>;
|
|
525
|
+
/** Enforce `maxDepth` by dropping pages off the bottom, remembering that they existed. */
|
|
526
|
+
private prune;
|
|
527
|
+
private playerOf;
|
|
528
|
+
private transition;
|
|
529
|
+
/** Tear down a transition: drop popped pages, fix up classes, fire the `did` hooks. */
|
|
530
|
+
private finish;
|
|
531
|
+
/** Jump an in-flight transition straight to its end state. */
|
|
532
|
+
private settle;
|
|
533
|
+
/**
|
|
534
|
+
* Set up a back transition and hand it to the gesture, paused at progress 0.
|
|
535
|
+
*
|
|
536
|
+
* Deliberately fires no lifecycle hooks: a swipe is a *peek* until the user releases, and
|
|
537
|
+
* pages should not be told they entered somewhere the user may drag right back out of. The
|
|
538
|
+
* hooks fire when the pop actually commits, through `run()`.
|
|
539
|
+
*/
|
|
540
|
+
beginInteractiveBack(): InteractiveBack | null;
|
|
541
|
+
/** The user let go without going far enough: run the transition back to where it started. */
|
|
542
|
+
abortInteractiveBack(back: InteractiveBack, ms: number): Promise<void>;
|
|
543
|
+
private buildPlayer;
|
|
544
|
+
/** Which platform's look we're using, honouring a `platform` override in the config. */
|
|
545
|
+
platformKind(): StackPlatformKind;
|
|
546
|
+
/** One function for every platform, a per-platform override, or the built-in. */
|
|
547
|
+
transitionFn(): StackTransition;
|
|
548
|
+
/**
|
|
549
|
+
* Read from the DOM rather than cached, so an app that flips `dir` at runtime — which is what
|
|
550
|
+
* a language switcher does — mirrors on the very next transition without a reload.
|
|
551
|
+
*/
|
|
552
|
+
isRtl(): boolean;
|
|
553
|
+
/**
|
|
554
|
+
* Only gates the *automatic* transitions. A swipe-back still animates: the user is dragging it
|
|
555
|
+
* themselves, and direct manipulation is exempt — a gesture that doesn't visibly follow the
|
|
556
|
+
* finger isn't reduced motion, it's a broken gesture.
|
|
557
|
+
*/
|
|
558
|
+
private prefersReducedMotion;
|
|
559
|
+
private reveal;
|
|
560
|
+
/**
|
|
561
|
+
* The definitive resting state. Exactly one page is visible: the top of the active tab's
|
|
562
|
+
* stack. Everything else — buried pages, and every page of every inactive tab — is hidden and
|
|
563
|
+
* inert but still mounted.
|
|
564
|
+
*/
|
|
565
|
+
private applyStates;
|
|
566
|
+
/**
|
|
567
|
+
* Put focus in the page that just arrived, and say its name out loud.
|
|
568
|
+
*
|
|
569
|
+
* Without this a screen-reader user gets no signal that anything happened, and keyboard focus
|
|
570
|
+
* stays on whatever control they activated — which has now slid off the screen and been marked
|
|
571
|
+
* `inert`, leaving focus nowhere at all.
|
|
572
|
+
*/
|
|
573
|
+
private moveFocus;
|
|
574
|
+
private destroyEntry;
|
|
575
|
+
destroy(): void;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
interface SwipeBackHost {
|
|
579
|
+
readonly hostEl: HTMLElement;
|
|
580
|
+
/**
|
|
581
|
+
* May a swipe start right now? Asked on every touch that lands in the edge zone, so it is free
|
|
582
|
+
* to depend on live state. The single gate: whether the gesture is enabled at all, whether
|
|
583
|
+
* there is anything to go back to, and whether the current page objects.
|
|
584
|
+
*/
|
|
585
|
+
canSwipeBack(): boolean;
|
|
586
|
+
/** Right-to-left, so the gesture lives on the right edge and pulls the other way. */
|
|
587
|
+
isRtl(): boolean;
|
|
588
|
+
/** Build the scrubbable back transition, or return null to refuse. */
|
|
589
|
+
beginSwipeBack(): InteractiveBack | null;
|
|
590
|
+
/** The user let go past the threshold. Finish the animation over `ms`, then pop for real. */
|
|
591
|
+
commitSwipeBack(back: InteractiveBack, ms: number): void;
|
|
592
|
+
/** The user let go short of the threshold. Put everything back over `ms`. */
|
|
593
|
+
abortSwipeBack(back: InteractiveBack, ms: number): void;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* The master switch for swipe-to-go-back.
|
|
598
|
+
*
|
|
599
|
+
* `provideNgxStack({ swipeBack })` only sets the *starting* value — this is what you reach
|
|
600
|
+
* for when the answer changes while the app is running. A modal is open, a map is eating the
|
|
601
|
+
* drag, a form has unsaved edits, a payment is in flight: turn it off, turn it back on after.
|
|
602
|
+
*
|
|
603
|
+
* ```ts
|
|
604
|
+
* const swipe = inject(NgxStackSwipe);
|
|
605
|
+
* swipe.disable();
|
|
606
|
+
* // …later
|
|
607
|
+
* swipe.reset(); // back to whatever the config said
|
|
608
|
+
* ```
|
|
609
|
+
*
|
|
610
|
+
* This is the global layer. Two narrower ones sit on top of it, and either can veto:
|
|
611
|
+
* `<ngx-stack-outlet [swipeBack]="…">` for one stack, and `ngxCanSwipeBack()` or
|
|
612
|
+
* `data: { swipeBack: false }` for one page. See {@link NgxStackPage}.
|
|
613
|
+
*/
|
|
614
|
+
declare class NgxStackSwipe {
|
|
615
|
+
private readonly config;
|
|
616
|
+
private readonly platform;
|
|
617
|
+
private readonly _enabled;
|
|
618
|
+
/** Read it in a template or a computed; it's a signal. */
|
|
619
|
+
readonly enabled: Signal<boolean>;
|
|
620
|
+
enable(): void;
|
|
621
|
+
disable(): void;
|
|
622
|
+
set(enabled: boolean): void;
|
|
623
|
+
/** Go back to what the config asked for, whatever that resolved to on this device. */
|
|
624
|
+
reset(): void;
|
|
625
|
+
private fromConfig;
|
|
626
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<NgxStackSwipe, never>;
|
|
627
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<NgxStackSwipe>;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Everything the two kinds of stack have in common — which is almost everything.
|
|
632
|
+
*
|
|
633
|
+
* `NgxStackOutlet` is fed by the Router and `NgxStack` by `push()`, and that difference is real but
|
|
634
|
+
* shallow: it only decides *where the next page comes from*. Once a page exists, holding it,
|
|
635
|
+
* transitioning to it, dragging it around with a finger and tearing it down are identical jobs, and
|
|
636
|
+
* a swipe-back must behave the same in both. Keeping that in one place is the only way it stays that
|
|
637
|
+
* way.
|
|
638
|
+
*
|
|
639
|
+
* So a subclass supplies three things — how strict to be about guards, whether it overrides the
|
|
640
|
+
* app-wide swipe switch, and what a committed swipe should actually *do* — and inherits the rest.
|
|
641
|
+
*/
|
|
642
|
+
declare abstract class StackHostBase implements SwipeBackHost, OnDestroy {
|
|
643
|
+
/** Fires as a transition begins — including one a finger is about to scrub. */
|
|
644
|
+
readonly transitionStart: i0.OutputEmitterRef<StackTransitionEvent>;
|
|
645
|
+
/** Fires once the pages have settled and any popped page has been destroyed. */
|
|
646
|
+
readonly transitionEnd: i0.OutputEmitterRef<StackTransitionEvent>;
|
|
647
|
+
protected readonly config: ResolvedStackConfig;
|
|
648
|
+
protected readonly platform: ngx_stack.StackPlatform;
|
|
649
|
+
protected readonly swipe: NgxStackSwipe;
|
|
650
|
+
protected readonly viewContainer: ViewContainerRef;
|
|
651
|
+
protected readonly environmentInjector: EnvironmentInjector;
|
|
652
|
+
protected readonly changeDetector: ChangeDetectorRef;
|
|
653
|
+
readonly hostEl: HTMLElement;
|
|
654
|
+
protected readonly controller: StackController;
|
|
655
|
+
private gesture;
|
|
656
|
+
/** The active stack's pages, bottom first. */
|
|
657
|
+
readonly pages: Signal<readonly StackEntry[]>;
|
|
658
|
+
readonly depth: Signal<number>;
|
|
659
|
+
readonly animating: Signal<boolean>;
|
|
660
|
+
constructor();
|
|
661
|
+
/** Arm the gesture. Subclasses call this once they are ready to be swiped. */
|
|
662
|
+
protected startGesture(): void;
|
|
663
|
+
/**
|
|
664
|
+
* Instantiate a page component and hand it to the controller to be wrapped and mounted.
|
|
665
|
+
*
|
|
666
|
+
* The `markForCheck` matters and is easy to lose: both hosts are OnPush with an empty template of
|
|
667
|
+
* their own, so nothing would ever dirty the view and the freshly inserted page would sit there
|
|
668
|
+
* un-checked until something unrelated happened to trigger a pass.
|
|
669
|
+
*/
|
|
670
|
+
protected createPage<T>(component: Type<T>, options: {
|
|
671
|
+
injector: Injector;
|
|
672
|
+
environmentInjector?: EnvironmentInjector;
|
|
673
|
+
}): ComponentRef<T>;
|
|
674
|
+
/**
|
|
675
|
+
* Three layers, narrowest last, each able to veto: the app-wide switch, this stack, and the page
|
|
676
|
+
* currently on top. Re-evaluated on every touch, so all three can change at runtime.
|
|
677
|
+
*/
|
|
678
|
+
canSwipeBack(): boolean;
|
|
679
|
+
isRtl(): boolean;
|
|
680
|
+
beginSwipeBack(): InteractiveBack | null;
|
|
681
|
+
abortSwipeBack(back: InteractiveBack, ms: number): void;
|
|
682
|
+
/** The user let go past the threshold. The animation is done; make the pop real. */
|
|
683
|
+
abstract commitSwipeBack(back: InteractiveBack, ms: number): void;
|
|
684
|
+
/** `null` to defer to the app-wide {@link NgxStackSwipe}. */
|
|
685
|
+
protected abstract swipeBackOverride(): boolean | null;
|
|
686
|
+
/** Only routed pages can carry a `canDeactivate` guard, so only they need a policy for one. */
|
|
687
|
+
protected abstract guardPolicy(): GuardPolicy;
|
|
688
|
+
ngOnDestroy(): void;
|
|
689
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<StackHostBase, never>;
|
|
690
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<StackHostBase, never, never, {}, { "transitionStart": "transitionStart"; "transitionEnd": "transitionEnd"; }, never, never, true, never>;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Where a "back" would actually go. */
|
|
694
|
+
interface BackTarget {
|
|
695
|
+
url: string;
|
|
696
|
+
/**
|
|
697
|
+
* Whether that URL is a page we already have mounted beneath the current one. When it isn't —
|
|
698
|
+
* a cold deep link, where there is nothing underneath — going back has to build it.
|
|
699
|
+
*/
|
|
700
|
+
mounted: boolean;
|
|
701
|
+
}
|
|
702
|
+
/** Implemented by the outlet, so `back()` knows what is actually beneath the current page. */
|
|
703
|
+
interface StackBackTarget {
|
|
704
|
+
backTarget(): BackTarget | null;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Navigation with an explicit stack intent.
|
|
708
|
+
*
|
|
709
|
+
* You don't have to use this for pushing — plain `router.navigate()` works, and the outlet infers
|
|
710
|
+
* the direction by checking whether the target URL is already on the stack. It's for the cases
|
|
711
|
+
* where that inference is wrong, and for going back, which is subtler than it looks.
|
|
712
|
+
*/
|
|
713
|
+
declare class NgxStackNav {
|
|
714
|
+
private readonly router;
|
|
715
|
+
private readonly location;
|
|
716
|
+
private readonly history;
|
|
717
|
+
private target;
|
|
718
|
+
/** @internal Called by `NgxStackOutlet`. */
|
|
719
|
+
registerStack(target: StackBackTarget): void;
|
|
720
|
+
/** @internal */
|
|
721
|
+
unregisterStack(target: StackBackTarget): void;
|
|
722
|
+
/**
|
|
723
|
+
* Is there anywhere to go back to *within the app*?
|
|
724
|
+
*
|
|
725
|
+
* Ask this, not the shell. Capacitor and Cordova both report whether the *webview* can go back,
|
|
726
|
+
* which is a statement about history — and history is a single linear thread while tabs are
|
|
727
|
+
* several stacks, so it says yes when the only thing behind you is a different tab.
|
|
728
|
+
*/
|
|
729
|
+
canGoBack(): boolean;
|
|
730
|
+
/** Push a page on top, even if its URL is already somewhere on the stack. */
|
|
731
|
+
forward(commands: unknown[] | string, extras?: NavigationExtras): Promise<boolean>;
|
|
732
|
+
/**
|
|
733
|
+
* Go back one page. This is what the swipe gesture commits to, and what a back button should
|
|
734
|
+
* call. There are three quite different situations behind that one word, and this picks between
|
|
735
|
+
* them:
|
|
736
|
+
*
|
|
737
|
+
* 1. **There's a page below, and it's also the previous history entry.** The easy case. Use a
|
|
738
|
+
* real `history.back()`, so the URL, the browser's back button and the stack all keep telling
|
|
739
|
+
* the same story.
|
|
740
|
+
*
|
|
741
|
+
* 2. **There's a page below, but history disagrees.** This is what tabs do to you: history is a
|
|
742
|
+
* single linear thread, tabs are several stacks, and switching tabs leaves an unrelated page
|
|
743
|
+
* sitting behind you. `history.back()` would jump sideways out of the stack you're in, so
|
|
744
|
+
* navigate to the page we actually mean instead, replacing the current entry so that popping
|
|
745
|
+
* doesn't inflate history.
|
|
746
|
+
*
|
|
747
|
+
* 3. **There's nothing below at all.** A cold deep link — a push notification, a shared URL, a
|
|
748
|
+
* refresh three screens in. `history.back()` here walks out of the app entirely, which is
|
|
749
|
+
* almost never what the user meant by tapping a back arrow inside it. If the route declares
|
|
750
|
+
* where it sits (`data: { parent: '/inbox' }`) we build that page and animate to it as a back,
|
|
751
|
+
* exactly as though it had been there all along.
|
|
752
|
+
*/
|
|
753
|
+
back(commands?: unknown[] | string, extras?: NavigationExtras): Promise<boolean>;
|
|
754
|
+
/** Throw every stack away and start again from this page. */
|
|
755
|
+
root(commands: unknown[] | string, extras?: NavigationExtras): Promise<boolean>;
|
|
756
|
+
private navigate;
|
|
757
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<NgxStackNav, never>;
|
|
758
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<NgxStackNav>;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* A `<router-outlet>` that keeps a stack instead of a single page.
|
|
763
|
+
*
|
|
764
|
+
* The stock outlet destroys the outgoing component the moment you navigate. That is the right
|
|
765
|
+
* default and completely incompatible with a swipe-back: the page you are swiping back to has to
|
|
766
|
+
* already be on screen, mounted and painted, *before* the navigation that reveals it happens —
|
|
767
|
+
* otherwise there is nothing to drag into view.
|
|
768
|
+
*
|
|
769
|
+
* So pages here are mounted once and stay. Which one you are looking at is decided by where you are
|
|
770
|
+
* in the history, and the direction of each transition is inferred by asking whether the incoming
|
|
771
|
+
* URL is already on the stack (going back) or not (going forward).
|
|
772
|
+
*
|
|
773
|
+
* With `tabs` configured, one outlet holds one stack per tab and shows the active one.
|
|
774
|
+
*/
|
|
775
|
+
declare class NgxStackOutlet extends StackHostBase implements OnInit, OnDestroy, RouterOutletContract, StackBackTarget {
|
|
776
|
+
/** Matches `<router-outlet name>`, for named outlets. */
|
|
777
|
+
readonly name: i0.InputSignal<string>;
|
|
778
|
+
/**
|
|
779
|
+
* Swipe-back for this stack specifically. `null` (the default) defers to `NgxStackSwipe`, the
|
|
780
|
+
* app-wide switch; `true` or `false` overrides it here.
|
|
781
|
+
*/
|
|
782
|
+
readonly swipeBack: i0.InputSignal<boolean | null>;
|
|
783
|
+
private readonly parentContexts;
|
|
784
|
+
private readonly router;
|
|
785
|
+
private readonly nav;
|
|
786
|
+
private readonly document;
|
|
787
|
+
private systemTransition;
|
|
788
|
+
private activatedRouteRef;
|
|
789
|
+
private current;
|
|
790
|
+
private initialized;
|
|
791
|
+
/** A committed swipe, animated to the end, waiting for the router to catch up. */
|
|
792
|
+
private pendingSwipe;
|
|
793
|
+
readonly activateEvents: EventEmitter<unknown>;
|
|
794
|
+
readonly deactivateEvents: EventEmitter<unknown>;
|
|
795
|
+
readonly attachEvents: EventEmitter<unknown>;
|
|
796
|
+
readonly detachEvents: EventEmitter<unknown>;
|
|
797
|
+
readonly activeTab: Signal<string>;
|
|
798
|
+
/**
|
|
799
|
+
* Is there anywhere to go back to? Show your back button on this.
|
|
800
|
+
*
|
|
801
|
+
* True even with a single page on the stack, if that page has a parent it could go up to — which
|
|
802
|
+
* is exactly the cold-deep-link case, where nothing is beneath but obviously something should be.
|
|
803
|
+
*/
|
|
804
|
+
readonly canGoBack: Signal<boolean>;
|
|
805
|
+
constructor();
|
|
806
|
+
ngOnInit(): void;
|
|
807
|
+
get isActivated(): boolean;
|
|
808
|
+
get component(): object | null;
|
|
809
|
+
get activatedRoute(): ActivatedRoute | null;
|
|
810
|
+
get activatedRouteData(): Data;
|
|
811
|
+
activateWith(route: ActivatedRoute, environmentInjector: EnvironmentInjector): void;
|
|
812
|
+
deactivate(): void;
|
|
813
|
+
detach(): ComponentRef<unknown>;
|
|
814
|
+
attach(): void;
|
|
815
|
+
protected swipeBackOverride(): boolean | null;
|
|
816
|
+
protected guardPolicy(): GuardPolicy;
|
|
817
|
+
commitSwipeBack(back: InteractiveBack, ms: number): void;
|
|
818
|
+
/**
|
|
819
|
+
* Where a "back" should actually go.
|
|
820
|
+
*
|
|
821
|
+
* Usually the page beneath the top of the *active* stack — which, with tabs, is emphatically not
|
|
822
|
+
* whatever the browser happens to have behind us in history. When nothing is beneath, we fall back
|
|
823
|
+
* to the page this one sits under. That page isn't mounted, so it has to be built; `mounted` is
|
|
824
|
+
* how {@link NgxStackNav.back} tells the two apart, because only one of them can be served by a
|
|
825
|
+
* plain `history.back()`.
|
|
826
|
+
*/
|
|
827
|
+
backTarget(): BackTarget | null;
|
|
828
|
+
private rollBackPendingSwipe;
|
|
829
|
+
/**
|
|
830
|
+
* Where the top page sits, when nothing is mounted underneath it.
|
|
831
|
+
*
|
|
832
|
+
* Nobody normally has to say: `/inbox/item/12` obviously sits under `/inbox`, and the route config
|
|
833
|
+
* already knows it. `data: { parent }` is only for URLs that don't tell the truth.
|
|
834
|
+
*/
|
|
835
|
+
private parentOfTop;
|
|
836
|
+
private createEntry;
|
|
837
|
+
/** Make `entry` the outlet's current page and give it back its nested outlets. */
|
|
838
|
+
private adopt;
|
|
839
|
+
private urlOf;
|
|
840
|
+
ngOnDestroy(): void;
|
|
841
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<NgxStackOutlet, never>;
|
|
842
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<NgxStackOutlet, "ngx-stack-outlet", ["ngxStackOutlet"], { "name": { "alias": "name"; "required": false; "isSignal": true; }; "swipeBack": { "alias": "swipeBack"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* A self-contained page stack with no URLs — the escape hatch for navigation that has no business
|
|
847
|
+
* being in the address bar.
|
|
848
|
+
*
|
|
849
|
+
* A multi-step filter sheet, a wizard inside a modal, a drill-down in one tab of a tab bar: these
|
|
850
|
+
* are all stacks, they all want the same push/pop transition and the same swipe-back, and none of
|
|
851
|
+
* them should push history entries that the browser's back button then has to unwind one at a time.
|
|
852
|
+
*
|
|
853
|
+
* ```html
|
|
854
|
+
* <ngx-stack [root]="FilterStep1" #filters />
|
|
855
|
+
* ```
|
|
856
|
+
* ```ts
|
|
857
|
+
* filters.push(FilterStep2, { category: 'shoes' });
|
|
858
|
+
* ```
|
|
859
|
+
*
|
|
860
|
+
* Pages reach the stack they are standing on with `inject(NgxStack)`. For anything the user should
|
|
861
|
+
* be able to link to, bookmark or reload, use `<ngx-stack-outlet>` instead.
|
|
862
|
+
*/
|
|
863
|
+
declare class NgxStack extends StackHostBase implements OnInit {
|
|
864
|
+
/** The bottom page. Mounted on init; use `setRoot()` to change it afterwards. */
|
|
865
|
+
readonly root: i0.InputSignal<Type<unknown> | null>;
|
|
866
|
+
/** Inputs for the root page, applied with `setInput`. */
|
|
867
|
+
readonly rootInputs: i0.InputSignal<Record<string, unknown> | undefined>;
|
|
868
|
+
/**
|
|
869
|
+
* Swipe-back for this stack specifically. `null` (the default) defers to `NgxStackSwipe`, the
|
|
870
|
+
* app-wide switch; `true` or `false` overrides it here.
|
|
871
|
+
*/
|
|
872
|
+
readonly swipeBack: i0.InputSignal<boolean | null>;
|
|
873
|
+
/** The stack's own node injector, so pages can `inject(NgxStack)` and push further. */
|
|
874
|
+
private readonly pageInjector;
|
|
875
|
+
readonly canGoBack: Signal<boolean>;
|
|
876
|
+
ngOnInit(): void;
|
|
877
|
+
/** Push a page on top. Resolves when the transition has finished. */
|
|
878
|
+
push<T>(component: Type<T>, inputs?: Record<string, unknown>): Promise<void>;
|
|
879
|
+
/** Pop the top page. A no-op at the root, so it is safe to call blindly. */
|
|
880
|
+
pop(): Promise<void>;
|
|
881
|
+
/** Unwind to the page at `index` (0 is the root), destroying everything above it. */
|
|
882
|
+
popTo(index: number): Promise<void>;
|
|
883
|
+
popToRoot(): Promise<void>;
|
|
884
|
+
/** Throw the stack away and start again from `component`. */
|
|
885
|
+
setRoot<T>(component: Type<T>, inputs?: Record<string, unknown>): Promise<void>;
|
|
886
|
+
protected swipeBackOverride(): boolean | null;
|
|
887
|
+
/** No routes here, so no `canDeactivate` guards to be careful about. */
|
|
888
|
+
protected guardPolicy(): GuardPolicy;
|
|
889
|
+
commitSwipeBack(back: InteractiveBack, ms: number): void;
|
|
890
|
+
private mount;
|
|
891
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<NgxStack, never>;
|
|
892
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<NgxStack, "ngx-stack", ["ngxStack"], { "root": { "alias": "root"; "required": false; "isSignal": true; }; "rootInputs": { "alias": "rootInputs"; "required": false; "isSignal": true; }; "swipeBack": { "alias": "swipeBack"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Remembers where you were in each tab.
|
|
897
|
+
*
|
|
898
|
+
* The stacks themselves are `NgxStackOutlet`'s job — configure `tabs` and it keeps one per tab,
|
|
899
|
+
* mounted and untouched while you're elsewhere. This is the other half: tapping "Search" should
|
|
900
|
+
* take you back to the search result you were reading three screens deep, not dump you at the
|
|
901
|
+
* search tab's front page. So we watch navigations, note the current URL of each tab, and
|
|
902
|
+
* `select()` returns you to it.
|
|
903
|
+
*
|
|
904
|
+
* The tab *bar* is yours to draw — the library ships no visual design. All you need from us:
|
|
905
|
+
*
|
|
906
|
+
* ```ts
|
|
907
|
+
* const tabs = inject(NgxStackTabs);
|
|
908
|
+
* ```
|
|
909
|
+
* ```html
|
|
910
|
+
* @for (tab of ['home', 'search', 'profile']; track tab) {
|
|
911
|
+
* <button [class.active]="tabs.active() === tab" (click)="tabs.select(tab)">{{ tab }}</button>
|
|
912
|
+
* }
|
|
913
|
+
* ```
|
|
914
|
+
*/
|
|
915
|
+
declare class NgxStackTabs {
|
|
916
|
+
private readonly router;
|
|
917
|
+
private readonly config;
|
|
918
|
+
private readonly urls;
|
|
919
|
+
private readonly _active;
|
|
920
|
+
/** The tab currently on screen, or `''` before the first navigation into one. */
|
|
921
|
+
readonly active: Signal<string>;
|
|
922
|
+
/** The tabs you configured, in order. */
|
|
923
|
+
readonly tabs: Signal<readonly string[]>;
|
|
924
|
+
constructor();
|
|
925
|
+
/** The page that actually landed. */
|
|
926
|
+
private leaf;
|
|
927
|
+
/** Where `tab` was last seen, or its front page if it hasn't been visited yet. */
|
|
928
|
+
urlOf(tab: string): string;
|
|
929
|
+
/**
|
|
930
|
+
* Switch to `tab`, landing back on whatever page you last had open there.
|
|
931
|
+
*
|
|
932
|
+
* Tapping the tab you're already on takes you to its root, which is what every native tab bar
|
|
933
|
+
* does — it's the standard way back out of a deep drill-down.
|
|
934
|
+
*/
|
|
935
|
+
select(tab: string): Promise<boolean>;
|
|
936
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<NgxStackTabs, never>;
|
|
937
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<NgxStackTabs>;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Turns off Angular's own route store/detach machinery, because the stack does that job
|
|
942
|
+
* instead — and better, for this purpose: Angular's version stores a detached view and
|
|
943
|
+
* gives it back on request, while the stack keeps every page mounted and on screen, which
|
|
944
|
+
* is what lets a swipe reveal the page underneath before any navigation has happened.
|
|
945
|
+
*
|
|
946
|
+
* The one thing we do care about is `shouldReuseRoute`. Angular's default reuses a route
|
|
947
|
+
* whose config matches, so `/item/1 → /item/2` would recycle the same component instance.
|
|
948
|
+
* On a stack those are two different pages, and going back from one to the other has to
|
|
949
|
+
* work, so a change of params means a new page.
|
|
950
|
+
*/
|
|
951
|
+
declare class NgxStackRouteReuseStrategy implements RouteReuseStrategy {
|
|
952
|
+
shouldDetach(): boolean;
|
|
953
|
+
shouldAttach(): boolean;
|
|
954
|
+
store(): void;
|
|
955
|
+
retrieve(): DetachedRouteHandle | null;
|
|
956
|
+
shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean;
|
|
957
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<NgxStackRouteReuseStrategy, never>;
|
|
958
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<NgxStackRouteReuseStrategy>;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* The iOS navigation-controller push/pop.
|
|
963
|
+
*
|
|
964
|
+
* The incoming page slides the full width of the screen over the outgoing one, which drifts the
|
|
965
|
+
* other way at a third of the speed and dims underneath it — the parallax that makes a UIKit stack
|
|
966
|
+
* feel like sheets of paper rather than slides.
|
|
967
|
+
*
|
|
968
|
+
* The easing is UIKit's own: almost no acceleration, and a very long tail.
|
|
969
|
+
*/
|
|
970
|
+
declare const iosTransition: ngx_stack.StackTransition;
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* The Material push: the incoming page rises and fades in over the outgoing one, which stays where
|
|
974
|
+
* it is. Shorter and flatter than iOS — Material moves things a short distance quickly rather than a
|
|
975
|
+
* long distance smoothly.
|
|
976
|
+
*
|
|
977
|
+
* Deliberately has no horizontal component, which is why `swipeBack: 'auto'` doesn't arm the gesture
|
|
978
|
+
* here: a finger dragging sideways would scrub a page moving vertically. On Android the back gesture
|
|
979
|
+
* belongs to the OS anyway. If you do want an edge swipe, give this platform a `slideTransition()`.
|
|
980
|
+
*/
|
|
981
|
+
declare const androidTransition: ngx_stack.StackTransition;
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* The browser default — the same rise-and-fade as Android, and identical to `androidTransition` down
|
|
985
|
+
* to the numbers.
|
|
986
|
+
*
|
|
987
|
+
* They are still two transitions rather than one, and that is the point: "what a phone does" and
|
|
988
|
+
* "what a browser does" are two separate decisions that merely happen to agree today. Retuning the
|
|
989
|
+
* web here changes nothing on Android, and vice versa. If they were the same object, the first person
|
|
990
|
+
* to want a different feel on the desktop would have to change both.
|
|
991
|
+
*
|
|
992
|
+
* Why Material rather than the iOS slide: a full-width slide says *"this screen came from over
|
|
993
|
+
* there"*, which is true of something you swiped into view and not of something you clicked — and on
|
|
994
|
+
* a wide monitor it is a great many pixels moving for no reason.
|
|
995
|
+
*/
|
|
996
|
+
declare const webTransition: ngx_stack.StackTransition;
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Pages swap instantly.
|
|
1000
|
+
*
|
|
1001
|
+
* Give it to a platform you'd rather not animate — `transitions: { web: noneTransition }` is a
|
|
1002
|
+
* common choice, since a page slide on a desktop monitor mostly just moves a lot of pixels.
|
|
1003
|
+
*
|
|
1004
|
+
* Note this also disarms the swipe on that platform in practice: there are no keyframes for a finger
|
|
1005
|
+
* to scrub, so a drag has nothing to follow.
|
|
1006
|
+
*/
|
|
1007
|
+
declare const noneTransition: StackTransition;
|
|
1008
|
+
|
|
1009
|
+
interface SlideOptions {
|
|
1010
|
+
/** How far the page riding on top travels, as a % of the host's width. */
|
|
1011
|
+
travel: number;
|
|
1012
|
+
/** How far the page underneath drifts the other way. Less than `travel` is what makes a parallax. */
|
|
1013
|
+
parallax: number;
|
|
1014
|
+
/** Peak opacity of the dim overlay on the covered page. `0` to skip it. */
|
|
1015
|
+
scrim: number;
|
|
1016
|
+
/** Fade the travelling page as well as moving it. iOS doesn't; a shorter web slide needs to. */
|
|
1017
|
+
fade: boolean;
|
|
1018
|
+
easing: string;
|
|
1019
|
+
/** Multiplier on the configured duration. */
|
|
1020
|
+
durationScale: number;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* The shape both horizontal transitions share: one page rides in over another, which drifts the
|
|
1024
|
+
* other way more slowly and dims underneath it.
|
|
1025
|
+
*
|
|
1026
|
+
* iOS and web are the same animation with different numbers — a full-width slide with a heavy
|
|
1027
|
+
* parallax, versus a short drift carried mostly by opacity. Writing that twice invites them to
|
|
1028
|
+
* quietly diverge, and the interesting parts here are subtle enough already:
|
|
1029
|
+
*
|
|
1030
|
+
* - `forward` and `back` are the same keyframes with the roles swapped. That symmetry is what makes
|
|
1031
|
+
* a swipe scrubbable at all — the gesture just seeks the `back` spec.
|
|
1032
|
+
* - everything horizontal is signed by `rtl`, so a page pushed "forward" in Arabic arrives from the
|
|
1033
|
+
* left, which is the direction the language reads towards.
|
|
1034
|
+
*/
|
|
1035
|
+
declare function slideTransition(options: SlideOptions): StackTransition;
|
|
1036
|
+
|
|
1037
|
+
interface RiseOptions {
|
|
1038
|
+
/** How far the incoming page rises through, in px. Material moves a short distance, quickly. */
|
|
1039
|
+
travel: number;
|
|
1040
|
+
easing: string;
|
|
1041
|
+
/** Multiplier on the configured duration. */
|
|
1042
|
+
durationScale: number;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* The Material shape: the incoming page rises a short distance and fades in over the outgoing one,
|
|
1046
|
+
* which stays exactly where it is and is simply covered.
|
|
1047
|
+
*
|
|
1048
|
+
* The counterpart to {@link slideTransition}. `androidTransition` and `webTransition` are both
|
|
1049
|
+
* instances of this, with the same numbers — but they remain two separate transitions you can point
|
|
1050
|
+
* at different things, because "what Android does" and "what a browser does" are two decisions that
|
|
1051
|
+
* merely happen to agree today.
|
|
1052
|
+
*
|
|
1053
|
+
* Note there is no horizontal component, and that has a consequence: an edge swipe would drag
|
|
1054
|
+
* sideways while the page moved vertically. That is why `swipeBack: 'auto'` only arms the gesture on
|
|
1055
|
+
* iOS. For a swipe on Android or the web, give that platform a {@link slideTransition} instead.
|
|
1056
|
+
*/
|
|
1057
|
+
declare function riseTransition(options: RiseOptions): StackTransition;
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Where a page sits, when it's the first thing the app opened.
|
|
1061
|
+
*
|
|
1062
|
+
* Two sources, in order:
|
|
1063
|
+
*
|
|
1064
|
+
* 1. **`data: { parent: '/inbox' }`** on the route. The same declaration the back button uses, so
|
|
1065
|
+
* you write it once and it drives both. Necessary whenever the URL doesn't tell the truth about
|
|
1066
|
+
* the hierarchy — flat routes like `/item/42`, or a detail page reachable from two places.
|
|
1067
|
+
*
|
|
1068
|
+
* 2. **The URL's own nesting.** `/inbox/item/12/notes` → `/inbox/item/12` → `/inbox`: drop
|
|
1069
|
+
* segments until what's left is a real page. Costs nothing to set up and is right most of the
|
|
1070
|
+
* time, because most apps already nest their URLs the way their screens nest.
|
|
1071
|
+
*/
|
|
1072
|
+
declare function deriveParentUrl(routes: Routes, url: string): string | null;
|
|
1073
|
+
|
|
1074
|
+
export { NGX_STACK_PLATFORM, NgxStack, NgxStackNav, NgxStackOutlet, NgxStackRouteReuseStrategy, NgxStackSwipe, NgxStackTabs, androidTransition, deriveParentUrl, iosTransition, noneTransition, provideCapacitorBack, provideCordovaBack, provideNgxStack, riseTransition, scrimOf, slideTransition, webTransition };
|
|
1075
|
+
export type { CapacitorAppLike, ElementAnimation, GuardPolicy, NgxStackConfig, NgxStackPage, RiseOptions, SlideOptions, StackDirection, StackEntry, StackPlatform, StackPlatformKind, StackTransition, StackTransitionEvent, StackTransitionMap, SystemGesturePolicy, TransitionContext, TransitionSpec };
|