@ng-native/router 0.5.0 → 0.7.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/README.md +10 -0
- package/dist/file-routes.d.ts +83 -0
- package/dist/file-routes.js +371 -0
- package/dist/file-routes.js.map +1 -0
- package/dist/in-tab.d.ts +20 -0
- package/dist/in-tab.js +13 -0
- package/dist/in-tab.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/markdown-page.d.ts +39 -0
- package/dist/markdown-page.js +56 -0
- package/dist/markdown-page.js.map +1 -0
- package/dist/native-back.d.ts +8 -0
- package/dist/native-back.js +8 -0
- package/dist/native-back.js.map +1 -1
- package/dist/native-navigation.d.ts +11 -1
- package/dist/native-navigation.js +66 -2
- package/dist/native-navigation.js.map +1 -1
- package/dist/native-platform-location.d.ts +5 -0
- package/dist/native-platform-location.js +4 -0
- package/dist/native-platform-location.js.map +1 -1
- package/dist/native-router-link.js +8 -6
- package/dist/native-router-link.js.map +1 -1
- package/dist/native-stack-outlet.d.ts +6 -0
- package/dist/native-stack-outlet.js +25 -2
- package/dist/native-stack-outlet.js.map +1 -1
- package/dist/native-stack-reuse-strategy.d.ts +9 -0
- package/dist/native-stack-reuse-strategy.js +28 -8
- package/dist/native-stack-reuse-strategy.js.map +1 -1
- package/dist/native-tab.d.ts +6 -0
- package/dist/native-tab.js +11 -2
- package/dist/native-tab.js.map +1 -1
- package/dist/native-tabs-outlet.d.ts +29 -1
- package/dist/native-tabs-outlet.js +113 -28
- package/dist/native-tabs-outlet.js.map +1 -1
- package/dist/native-toolbar.d.ts +39 -0
- package/dist/native-toolbar.js +108 -0
- package/dist/native-toolbar.js.map +1 -0
- package/dist/presented-route.d.ts +9 -1
- package/dist/presented-route.js +1 -1
- package/dist/presented-route.js.map +1 -1
- package/dist/provide-native-router.js +86 -2
- package/dist/provide-native-router.js.map +1 -1
- package/dist/route-parking.d.ts +30 -0
- package/dist/route-parking.js +60 -0
- package/dist/route-parking.js.map +1 -0
- package/dist/screens.js +23 -0
- package/dist/screens.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A token `marked.lexer` made, as `<markdown [tokens]>` takes it. Typed here rather than imported
|
|
3
|
+
* from `marked`, so an app's types never need it.
|
|
4
|
+
*/
|
|
5
|
+
interface MarkdownToken {
|
|
6
|
+
type: string;
|
|
7
|
+
raw: string;
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
}
|
|
10
|
+
/** A Markdown page's file, for the component given to `fileRoutes` as `markdownPage`. */
|
|
11
|
+
export interface MarkdownPageFile<Attributes extends Record<string, any> = Record<string, any>> {
|
|
12
|
+
/** Its path from the pages folder: `about.md`, `guides/install.md`. */
|
|
13
|
+
readonly filename: string;
|
|
14
|
+
/** Its file name without `.md`; `index.md` is `''`. */
|
|
15
|
+
readonly slug: string;
|
|
16
|
+
/** The front matter. A date in it is its ISO string. */
|
|
17
|
+
readonly attributes: Attributes;
|
|
18
|
+
/** The Markdown after the front matter. */
|
|
19
|
+
readonly content: string;
|
|
20
|
+
/** `content` lexed by `marked`, for `<markdown [tokens]>`. */
|
|
21
|
+
readonly tokens: MarkdownToken[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The route data key a Markdown page's file is under, for a package that routes `.md` pages itself,
|
|
25
|
+
* as `pageRoutes` from `@ng-native/analog` does: an app reads the file with `injectMarkdownPage()`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const MARKDOWN_PAGE = "ngNativeMarkdownPage";
|
|
28
|
+
/**
|
|
29
|
+
* The file of the Markdown page this component draws, in the component given to `fileRoutes`, or
|
|
30
|
+
* to `pageRoutes` from `@ng-native/analog`, as `markdownPage`:
|
|
31
|
+
* `{ filename, slug, attributes, content, tokens }`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function injectMarkdownPage<Attributes extends Record<string, any> = Record<string, any>>(): MarkdownPageFile<Attributes>;
|
|
34
|
+
/**
|
|
35
|
+
* A Markdown page's file, from the module Metro made of it, or that module's namespace, with
|
|
36
|
+
* `filename` its path from the pages folder. Throws, naming the file, for anything else.
|
|
37
|
+
*/
|
|
38
|
+
export declare function markdownPageFile(filename: string, loaded: unknown): MarkdownPageFile;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `.md` file routed as a page by `fileRoutes`: the file, as Metro made it a module, handed to the
|
|
3
|
+
* component the app gave as `markdownPage` through its route's data.
|
|
4
|
+
*
|
|
5
|
+
* The router does not draw Markdown, and does not import the package that does, so the app's
|
|
6
|
+
* component draws `tokens` with `<markdown>` from `@ng-native/components/markdown`. `pageRoutes`
|
|
7
|
+
* from `@ng-native/analog` routes a `.md` page with this file and this key too, so one
|
|
8
|
+
* `markdownPage` component draws it under either.
|
|
9
|
+
*/
|
|
10
|
+
import { inject } from '@angular/core';
|
|
11
|
+
import { ActivatedRoute } from '@angular/router';
|
|
12
|
+
/**
|
|
13
|
+
* The route data key a Markdown page's file is under, for a package that routes `.md` pages itself,
|
|
14
|
+
* as `pageRoutes` from `@ng-native/analog` does: an app reads the file with `injectMarkdownPage()`.
|
|
15
|
+
*/
|
|
16
|
+
export const MARKDOWN_PAGE = 'ngNativeMarkdownPage';
|
|
17
|
+
/**
|
|
18
|
+
* The file of the Markdown page this component draws, in the component given to `fileRoutes`, or
|
|
19
|
+
* to `pageRoutes` from `@ng-native/analog`, as `markdownPage`:
|
|
20
|
+
* `{ filename, slug, attributes, content, tokens }`.
|
|
21
|
+
*/
|
|
22
|
+
export function injectMarkdownPage() {
|
|
23
|
+
const file = inject(ActivatedRoute).snapshot.data[MARKDOWN_PAGE];
|
|
24
|
+
if (!file) {
|
|
25
|
+
throw new Error('injectMarkdownPage() is for the markdownPage component fileRoutes or pageRoutes routes a ' +
|
|
26
|
+
'.md page to, and this route is not one.');
|
|
27
|
+
}
|
|
28
|
+
return file;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A Markdown page's file, from the module Metro made of it, or that module's namespace, with
|
|
32
|
+
* `filename` its path from the pages folder. Throws, naming the file, for anything else.
|
|
33
|
+
*/
|
|
34
|
+
export function markdownPageFile(filename, loaded) {
|
|
35
|
+
const module = isMarkdownModule(loaded) ? loaded : loaded?.default;
|
|
36
|
+
if (!isMarkdownModule(module)) {
|
|
37
|
+
throw new Error(`${filename} is not the module @ng-native/metro makes of a .md file. Bundle the app with ` +
|
|
38
|
+
"withAngularNative from '@ng-native/metro/config.cjs', and test it with ngNative() from " +
|
|
39
|
+
"'@ng-native/testing/vitest'.");
|
|
40
|
+
}
|
|
41
|
+
const base = filename.split('/').pop().replace(/\.md$/, '');
|
|
42
|
+
return {
|
|
43
|
+
filename,
|
|
44
|
+
slug: base === 'index' ? '' : base,
|
|
45
|
+
attributes: module.attributes,
|
|
46
|
+
content: module.content,
|
|
47
|
+
tokens: module.tokens,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function isMarkdownModule(value) {
|
|
51
|
+
return (typeof value === 'object' &&
|
|
52
|
+
value !== null &&
|
|
53
|
+
typeof value.content === 'string' &&
|
|
54
|
+
Array.isArray(value.tokens));
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=markdown-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-page.js","sourceRoot":"","sources":["../src/markdown-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AA0BjD;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,sBAAsB,CAAC;AAEpD;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAGhC,MAAM,IAAI,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,CACrB,CAAC;IAC3C,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACb,2FAA2F;YACzF,yCAAyC,CAC5C,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB,EAAE,MAAe;IAChE,MAAM,MAAM,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,MAAgC,EAAE,OAAO,CAAC;IAC9F,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,GAAG,QAAQ,+EAA+E;YACxF,yFAAyF;YACzF,8BAA8B,CACjC,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAC7D,OAAO;QACL,QAAQ;QACR,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI;QAClC,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,MAAM,EAAE,MAAM,CAAC,MAAM;KACtB,CAAC;AACJ,CAAC;AAQD,SAAS,gBAAgB,CAAC,KAAc;IACtC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,OAAQ,KAAwB,CAAC,OAAO,KAAK,QAAQ;QACrD,KAAK,CAAC,OAAO,CAAE,KAAwB,CAAC,MAAM,CAAC,CAChD,CAAC;AACJ,CAAC","sourcesContent":["/**\n * A `.md` file routed as a page by `fileRoutes`: the file, as Metro made it a module, handed to the\n * component the app gave as `markdownPage` through its route's data.\n *\n * The router does not draw Markdown, and does not import the package that does, so the app's\n * component draws `tokens` with `<markdown>` from `@ng-native/components/markdown`. `pageRoutes`\n * from `@ng-native/analog` routes a `.md` page with this file and this key too, so one\n * `markdownPage` component draws it under either.\n */\nimport { inject } from '@angular/core';\nimport { ActivatedRoute } from '@angular/router';\n\n/**\n * A token `marked.lexer` made, as `<markdown [tokens]>` takes it. Typed here rather than imported\n * from `marked`, so an app's types never need it.\n */\ninterface MarkdownToken {\n type: string;\n raw: string;\n [key: string]: any;\n}\n\n/** A Markdown page's file, for the component given to `fileRoutes` as `markdownPage`. */\nexport interface MarkdownPageFile<Attributes extends Record<string, any> = Record<string, any>> {\n /** Its path from the pages folder: `about.md`, `guides/install.md`. */\n readonly filename: string;\n /** Its file name without `.md`; `index.md` is `''`. */\n readonly slug: string;\n /** The front matter. A date in it is its ISO string. */\n readonly attributes: Attributes;\n /** The Markdown after the front matter. */\n readonly content: string;\n /** `content` lexed by `marked`, for `<markdown [tokens]>`. */\n readonly tokens: MarkdownToken[];\n}\n\n/**\n * The route data key a Markdown page's file is under, for a package that routes `.md` pages itself,\n * as `pageRoutes` from `@ng-native/analog` does: an app reads the file with `injectMarkdownPage()`.\n */\nexport const MARKDOWN_PAGE = 'ngNativeMarkdownPage';\n\n/**\n * The file of the Markdown page this component draws, in the component given to `fileRoutes`, or\n * to `pageRoutes` from `@ng-native/analog`, as `markdownPage`:\n * `{ filename, slug, attributes, content, tokens }`.\n */\nexport function injectMarkdownPage<\n Attributes extends Record<string, any> = Record<string, any>,\n>(): MarkdownPageFile<Attributes> {\n const file = inject(ActivatedRoute).snapshot.data[MARKDOWN_PAGE] as\n MarkdownPageFile<Attributes> | undefined;\n if (!file) {\n throw new Error(\n 'injectMarkdownPage() is for the markdownPage component fileRoutes or pageRoutes routes a ' +\n '.md page to, and this route is not one.',\n );\n }\n return file;\n}\n\n/**\n * A Markdown page's file, from the module Metro made of it, or that module's namespace, with\n * `filename` its path from the pages folder. Throws, naming the file, for anything else.\n */\nexport function markdownPageFile(filename: string, loaded: unknown): MarkdownPageFile {\n const module = isMarkdownModule(loaded) ? loaded : (loaded as { default?: unknown })?.default;\n if (!isMarkdownModule(module)) {\n throw new Error(\n `${filename} is not the module @ng-native/metro makes of a .md file. Bundle the app with ` +\n \"withAngularNative from '@ng-native/metro/config.cjs', and test it with ngNative() from \" +\n \"'@ng-native/testing/vitest'.\",\n );\n }\n const base = filename.split('/').pop()!.replace(/\\.md$/, '');\n return {\n filename,\n slug: base === 'index' ? '' : base,\n attributes: module.attributes,\n content: module.content,\n tokens: module.tokens,\n };\n}\n\ninterface MarkdownModule {\n readonly attributes: Record<string, unknown>;\n readonly content: string;\n readonly tokens: MarkdownToken[];\n}\n\nfunction isMarkdownModule(value: unknown): value is MarkdownModule {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as MarkdownModule).content === 'string' &&\n Array.isArray((value as MarkdownModule).tokens)\n );\n}\n"]}
|
package/dist/native-back.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface PoppableStack {
|
|
|
8
8
|
popToRoot(): Promise<boolean> | null;
|
|
9
9
|
/** Pop to the screen showing `url`, or null when no screen below the top shows it. */
|
|
10
10
|
popTo(url: string): Promise<boolean> | null;
|
|
11
|
+
/** Take every screen under the top off at once, or false when it is showing only that one. */
|
|
12
|
+
dropUnderTop(): boolean;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* What a tab bar offers `NativeNavigation` for a push into one of its tabs from outside it. A tab
|
|
@@ -43,6 +45,12 @@ export declare class NativeBack {
|
|
|
43
45
|
addStack(stack: PoppableStack): () => void;
|
|
44
46
|
/** Pop the innermost stack in front that is deeper than one screen, to its first. */
|
|
45
47
|
popToRoot(): Promise<boolean>;
|
|
48
|
+
/**
|
|
49
|
+
* Take the screens under the top of the innermost stack in front that has any off it, leaving
|
|
50
|
+
* the top one: a reset to the url already showing, which the router skips. False when no stack
|
|
51
|
+
* in front had any.
|
|
52
|
+
*/
|
|
53
|
+
resetToTop(): boolean;
|
|
46
54
|
/** Pop the innermost stack in front that has a screen at `url` below its top, down to it. */
|
|
47
55
|
popTo(url: string): Promise<boolean>;
|
|
48
56
|
/**
|
package/dist/native-back.js
CHANGED
|
@@ -69,6 +69,14 @@ export class NativeBack {
|
|
|
69
69
|
}
|
|
70
70
|
return Promise.resolve(false);
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Take the screens under the top of the innermost stack in front that has any off it, leaving
|
|
74
|
+
* the top one: a reset to the url already showing, which the router skips. False when no stack
|
|
75
|
+
* in front had any.
|
|
76
|
+
*/
|
|
77
|
+
resetToTop() {
|
|
78
|
+
return [...this.stacks].reverse().some((stack) => stack.showing() && stack.dropUnderTop());
|
|
79
|
+
}
|
|
72
80
|
/** Pop the innermost stack in front that has a screen at `url` below its top, down to it. */
|
|
73
81
|
popTo(url) {
|
|
74
82
|
for (const stack of [...this.stacks].reverse()) {
|
package/dist/native-back.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-back.js","sourceRoot":"","sources":["../src/native-back.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;;
|
|
1
|
+
{"version":3,"file":"native-back.js","sourceRoot":"","sources":["../src/native-back.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Who answers a back: Android's hardware button, and `NativeNavigation.back()`.\n *\n * Every outlet offers an answer, and the one in front takes it. Each answer is also handed to\n * `HardwareBack` directly, where React Native asks the newest first. An outlet always subscribes\n * after the outlet it sits inside, so that order runs from the innermost outlet out, and a\n * screen's own handler, subscribed after its outlet, still outranks it. `back()` asks the outlets\n * in that same order, without the screens' own handlers: those are for the button.\n *\n * Newest first is not enough on its own. A stack in a tab behind the one in front, or under a\n * screen pushed over the whole tab bar, is still subscribed, so each answer first checks that\n * its outlet is showing what the router is showing.\n */\nimport { Service, inject } from '@angular/core';\nimport type { ActivatedRoute, Router } from '@angular/router';\nimport { HardwareBack } from '@ng-native/device';\n\n/** What a stack offers `NativeNavigation` beyond a back: going down it several screens at once. */\nexport interface PoppableStack {\n /** Whether this stack is part of what the router shows now. */\n showing(): boolean;\n /** Pop to its first screen, or null when it is showing only that one. */\n popToRoot(): Promise<boolean> | null;\n /** Pop to the screen showing `url`, or null when no screen below the top shows it. */\n popTo(url: string): Promise<boolean> | null;\n /** Take every screen under the top off at once, or false when it is showing only that one. */\n dropUnderTop(): boolean;\n}\n\n/**\n * What a tab bar offers `NativeNavigation` for a push into one of its tabs from outside it. A tab\n * nobody has opened has no stack yet, and a page pushed into it would be the stack's first\n * screen, with nothing under it to go back to.\n */\nexport interface TabBar {\n /** The url of the tab `url` is a page inside, when that tab has not been opened. */\n unopenedTabOf(url: string): string | null;\n /** Whether `url` is a tab other than the one in front, or a page inside one. */\n behind(url: string): boolean;\n}\n\n@Service()\nexport class NativeBack {\n private readonly hardwareBack = inject(HardwareBack);\n /** Oldest first, as `HardwareBack` holds them. */\n private readonly answers: BackAnswer[] = [];\n /** Oldest first, and so outermost first, as the answers are. */\n private readonly stacks: PoppableStack[] = [];\n\n /** Oldest first. */\n private readonly tabBars: TabBar[] = [];\n\n /** Offer a tab bar's unopened tabs to a push into one. Returns an unsubscribe. */\n addTabBar(bar: TabBar): () => void {\n this.tabBars.push(bar);\n return () => {\n const index = this.tabBars.indexOf(bar);\n if (index !== -1) this.tabBars.splice(index, 1);\n };\n }\n\n /** The url of the unopened tab `url` is a page inside, or null: see `TabBar`. */\n unopenedTabOf(url: string): string | null {\n for (const bar of this.tabBars) {\n const root = bar.unopenedTabOf(url);\n if (root) return root;\n }\n return null;\n }\n\n /** Whether `url` belongs to a tab that is not the one in front. */\n inTabBehind(url: string): boolean {\n return this.tabBars.some((bar) => bar.behind(url));\n }\n\n /**\n * Whether the app's own stack takes a page presented over whatever is showing. Set by the root\n * stack outlet for as long as there is one: see `presented-route.ts`.\n */\n presents = false;\n\n /** Offer a stack for popping several screens at once. Returns an unsubscribe. */\n addStack(stack: PoppableStack): () => void {\n this.stacks.push(stack);\n return () => {\n const index = this.stacks.indexOf(stack);\n if (index !== -1) this.stacks.splice(index, 1);\n };\n }\n\n /** Pop the innermost stack in front that is deeper than one screen, to its first. */\n popToRoot(): Promise<boolean> {\n for (const stack of [...this.stacks].reverse()) {\n if (!stack.showing()) continue;\n const popped = stack.popToRoot();\n if (popped) return popped;\n }\n return Promise.resolve(false);\n }\n\n /**\n * Take the screens under the top of the innermost stack in front that has any off it, leaving\n * the top one: a reset to the url already showing, which the router skips. False when no stack\n * in front had any.\n */\n resetToTop(): boolean {\n return [...this.stacks].reverse().some((stack) => stack.showing() && stack.dropUnderTop());\n }\n\n /** Pop the innermost stack in front that has a screen at `url` below its top, down to it. */\n popTo(url: string): Promise<boolean> {\n for (const stack of [...this.stacks].reverse()) {\n if (!stack.showing()) continue;\n const popped = stack.popTo(url);\n if (popped) return popped;\n }\n return Promise.resolve(false);\n }\n\n /**\n * Offer an outlet's answer to a back. It returns whether it took the back, and false lets the\n * next one out try. Returns an unsubscribe.\n */\n handle(answer: BackAnswer): () => void {\n this.answers.push(answer);\n const stop = this.hardwareBack.handle(() => answer('button'));\n return () => {\n stop();\n const index = this.answers.indexOf(answer);\n if (index !== -1) this.answers.splice(index, 1);\n };\n }\n\n /** Go back as the button would, innermost outlet first. False when none had anywhere to go. */\n back(): boolean {\n return [...this.answers].reverse().some((answer) => answer('app'));\n }\n}\n\n/**\n * An outlet's answer to a back, told where it came from: Android's button, which is the platform's\n * own dismissal and a screen may refuse, or the app's `NativeNavigation.back()`, which it may not.\n */\nexport type BackAnswer = (from: 'button' | 'app') => boolean;\n\n/**\n * Whether a route is part of what the router shows now, rather than a subtree the reuse strategy\n * detached and is keeping for later: a tab behind, or a screen covered by one pushed over it.\n */\nexport function isShowing(router: Router, route: ActivatedRoute | null): boolean {\n if (!route) return false;\n const visit = (node: ActivatedRoute): boolean => node === route || node.children.some(visit);\n return visit(router.routerState.root);\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type NavigationExtras } from '@angular/router';
|
|
1
|
+
import { type Navigation, type NavigationExtras } from '@angular/router';
|
|
2
2
|
import type { ScreenPresentation, StackPresentation } from './screen-presentation.ts';
|
|
3
3
|
/** What `Router` takes: a url string, or the array form with segments and params. */
|
|
4
4
|
export type NavigationCommands = string | readonly unknown[];
|
|
@@ -23,6 +23,16 @@ export interface PresentOptions extends NativeNavigationOptions {
|
|
|
23
23
|
}
|
|
24
24
|
/** Read the intent off a navigation's state, if it carries one. */
|
|
25
25
|
export declare function intentOf(state: unknown): NativeIntent | null;
|
|
26
|
+
/** What a navigation asks of the stack: its own intent, or the one it was redirected from. */
|
|
27
|
+
export declare function navigationIntent(router: object, navigation: Pick<Navigation, 'id' | 'extras'> | null | undefined): NativeIntent | null;
|
|
28
|
+
/**
|
|
29
|
+
* Hand a navigation's intent on to the one its guard redirects it to. The router schedules that
|
|
30
|
+
* navigation as soon as it has reported the cancel, so it takes the next id.
|
|
31
|
+
*
|
|
32
|
+
* A reset to the url already showing, its own or the one a guard sends it to, is one the router
|
|
33
|
+
* skips: no outlet activates anything for it, so the stack is reset here, to the screen on top.
|
|
34
|
+
*/
|
|
35
|
+
export declare function carryIntentAcrossRedirects(): void;
|
|
26
36
|
export declare class NativeNavigation {
|
|
27
37
|
private readonly router;
|
|
28
38
|
private readonly outlets;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* No decorators, so tests can import it at runtime and so it can be provided with `useClass`,
|
|
14
14
|
* exactly as `NativePlatformLocation` and `NativeStackReuseStrategy` are.
|
|
15
15
|
*/
|
|
16
|
-
import { inject } from '@angular/core';
|
|
17
|
-
import { NavigationCancel, NavigationEnd, NavigationError, Router, } from '@angular/router';
|
|
16
|
+
import { DestroyRef, inject } from '@angular/core';
|
|
17
|
+
import { NavigationCancel, NavigationCancellationCode, NavigationEnd, NavigationError, NavigationSkipped, NavigationSkippedCode, Router, } from '@angular/router';
|
|
18
18
|
import { NativeBack } from "./native-back.js";
|
|
19
19
|
import { leavingPresented, presentedCommands } from "./presented-route.js";
|
|
20
20
|
/**
|
|
@@ -31,6 +31,70 @@ export function intentOf(state) {
|
|
|
31
31
|
const intent = state[NATIVE_INTENT];
|
|
32
32
|
return intent && typeof intent === 'object' ? intent : null;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The intent of a navigation a guard redirected, for the navigation the redirect starts.
|
|
36
|
+
*
|
|
37
|
+
* The router starts a new navigation for a guard's `UrlTree`, keeping `replaceUrl` but not
|
|
38
|
+
* `state`, so a reset to a page that sends a signed-out user to sign in would push the sign-in
|
|
39
|
+
* page over the stack it was meant to clear. Held for that one navigation by its id, which no
|
|
40
|
+
* later navigation shares.
|
|
41
|
+
*/
|
|
42
|
+
const redirected = new WeakMap();
|
|
43
|
+
/** What a navigation asks of the stack: its own intent, or the one it was redirected from. */
|
|
44
|
+
export function navigationIntent(router, navigation) {
|
|
45
|
+
if (!navigation)
|
|
46
|
+
return null;
|
|
47
|
+
const own = intentOf(navigation.extras.state);
|
|
48
|
+
if (own)
|
|
49
|
+
return own;
|
|
50
|
+
const carried = redirected.get(router);
|
|
51
|
+
return carried?.id === navigation.id ? carried.intent : null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Hand a navigation's intent on to the one its guard redirects it to. The router schedules that
|
|
55
|
+
* navigation as soon as it has reported the cancel, so it takes the next id.
|
|
56
|
+
*
|
|
57
|
+
* A reset to the url already showing, its own or the one a guard sends it to, is one the router
|
|
58
|
+
* skips: no outlet activates anything for it, so the stack is reset here, to the screen on top.
|
|
59
|
+
*/
|
|
60
|
+
export function carryIntentAcrossRedirects() {
|
|
61
|
+
const router = inject(Router);
|
|
62
|
+
const outlets = inject(NativeBack);
|
|
63
|
+
const events = router.events.subscribe((event) => {
|
|
64
|
+
if (event instanceof NavigationCancel && event.code === NavigationCancellationCode.Redirect) {
|
|
65
|
+
carryPast(router, event.id);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (event instanceof NavigationSkipped &&
|
|
69
|
+
event.code === NavigationSkippedCode.IgnoredSameUrlNavigation &&
|
|
70
|
+
intentAt(router, event.id)?.stack === 'reset') {
|
|
71
|
+
outlets.resetToTop();
|
|
72
|
+
}
|
|
73
|
+
if (ended(event) && redirected.get(router)?.id === event.id)
|
|
74
|
+
redirected.delete(router);
|
|
75
|
+
});
|
|
76
|
+
inject(DestroyRef).onDestroy(() => events.unsubscribe());
|
|
77
|
+
}
|
|
78
|
+
/** The intent of the navigation in progress, when it is the one with `id`. */
|
|
79
|
+
function intentAt(router, id) {
|
|
80
|
+
const navigation = router.currentNavigation();
|
|
81
|
+
return navigation?.id === id ? navigationIntent(router, navigation) : null;
|
|
82
|
+
}
|
|
83
|
+
/** Hold the intent of the navigation a guard redirected for the one the redirect starts. */
|
|
84
|
+
function carryPast(router, id) {
|
|
85
|
+
const intent = intentAt(router, id);
|
|
86
|
+
if (intent)
|
|
87
|
+
redirected.set(router, { id: id + 1, intent });
|
|
88
|
+
else
|
|
89
|
+
redirected.delete(router);
|
|
90
|
+
}
|
|
91
|
+
/** Whether the event is the last a navigation reports. */
|
|
92
|
+
function ended(event) {
|
|
93
|
+
return (event instanceof NavigationEnd ||
|
|
94
|
+
event instanceof NavigationCancel ||
|
|
95
|
+
event instanceof NavigationError ||
|
|
96
|
+
event instanceof NavigationSkipped);
|
|
97
|
+
}
|
|
34
98
|
export class NativeNavigation {
|
|
35
99
|
router = inject(Router);
|
|
36
100
|
outlets = inject(NativeBack);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-navigation.js","sourceRoot":"","sources":["../src/native-navigation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The typed facade over `Router` for the intents a URL cannot express (ADR 0003).\n *\n * Angular's `Router` stays the single source of truth: each of these is a line over\n * `router.navigate`, setting `replaceUrl` and a presentation on `NavigationExtras.state`. Deep\n * links, guards and resolvers keep working because it is still the Router, and because state\n * rides in history a back restores the presentation the screen was opened with.\n *\n * A navigation service that owns the stack was rejected: it either re-implements guards,\n * resolvers and deep linking, or it drifts from the URL, and once the stack is not derived from\n * router state the two can disagree.\n *\n * No decorators, so tests can import it at runtime and so it can be provided with `useClass`,\n * exactly as `NativePlatformLocation` and `NativeStackReuseStrategy` are.\n */\nimport { inject } from '@angular/core';\nimport {\n NavigationCancel,\n NavigationEnd,\n NavigationError,\n Router,\n type NavigationExtras,\n} from '@angular/router';\nimport { NativeBack } from './native-back.ts';\nimport type { ScreenPresentation, StackPresentation } from './screen-presentation.ts';\nimport { leavingPresented, presentedCommands } from './presented-route.ts';\n\n/** What `Router` takes: a url string, or the array form with segments and params. */\nexport type NavigationCommands = string | readonly unknown[];\n\n/**\n * Where the intent rides. `NavigationExtras.state` is the app's own object, so ours sits under\n * one reserved key rather than spread through it.\n */\nexport const NATIVE_INTENT = 'ɵnativeStack';\n\n/** What a navigation asks the outlet to do with the stack, over and above rendering the route. */\nexport interface NativeIntent {\n /** `replace` drops the screen it supersedes; `reset` empties the stack first. */\n readonly stack?: 'push' | 'replace' | 'reset';\n readonly presentation?: ScreenPresentation;\n}\n\nexport interface NativeNavigationOptions extends NavigationExtras {\n /** How the screen arrives: presentation, animation, gesture and sheet props. */\n presentation?: ScreenPresentation;\n}\n\nexport interface PresentOptions extends NativeNavigationOptions {\n /** Shorthand for `presentation.stackPresentation`. Defaults to `modal`. */\n as?: StackPresentation;\n}\n\n/** A url string up to its query or fragment. */\nconst pathOf = (url: string): string => url.split(/[?#]/, 1)[0]!;\n\n/** Read the intent off a navigation's state, if it carries one. */\nexport function intentOf(state: unknown): NativeIntent | null {\n if (!state || typeof state !== 'object') return null;\n const intent = (state as Record<string, unknown>)[NATIVE_INTENT];\n return intent && typeof intent === 'object' ? (intent as NativeIntent) : null;\n}\n\nexport class NativeNavigation {\n private readonly router = inject(Router);\n private readonly outlets = inject(NativeBack);\n\n /**\n * Push a new screen onto the stack. The plain case, and what `nativeRouterLink` does.\n *\n * From a presented screen the new one is presented the same way, over it, since a pushed\n * screen would slide in under it; `presentation` says otherwise, `stackAnimation` and all.\n */\n push(commands: NavigationCommands, options: NativeNavigationOptions = {}): Promise<boolean> {\n return this.go(commands, options, { stack: 'push' });\n }\n\n /**\n * Swap the current screen for another. The history entry is replaced too, so a back from the\n * new screen goes where a back from the old one would have.\n */\n replace(commands: NavigationCommands, options: NavigationExtras = {}): Promise<boolean> {\n return this.go(commands, { ...options, replaceUrl: true }, { stack: 'replace' });\n }\n\n /**\n * Present a screen over the stack rather than pushing into it: a modal, or a sheet.\n *\n * `present('/filters', { as: 'formSheet', presentation: { sheetAllowedDetents: [0.5, 1] } })`.\n *\n * A presented screen is put on screen outside the stack's navigation controller, so it has no\n * native header and no automatic safe-area insets: it needs its own way out, and its own\n * `safe-area-view`. A sheet can also be dragged away, but `fullScreenModal` cannot, and with\n * `gestureEnabled: false` neither can.\n */\n present(commands: NavigationCommands, options: PresentOptions = {}): Promise<boolean> {\n const { as = 'modal', presentation, ...extras } = options;\n const intent: NativeIntent = {\n stack: 'push',\n presentation: { stackPresentation: as, ...presentation },\n };\n // Asked at once, and only waited for when the page is another tab's: finding its route may\n // load a lazy one.\n const over = this.overTheTabInFront(commands, extras);\n if (!over) return this.go(commands, extras, intent);\n return over.then((found) =>\n this.go(found?.commands ?? commands, found?.extras ?? extras, intent),\n );\n }\n\n /**\n * Replace the whole stack with one screen, the way signing out or finishing onboarding does.\n * Every screen below is destroyed, so there is nothing to go back to.\n */\n reset(commands: NavigationCommands, options: NavigationExtras = {}): Promise<boolean> {\n return this.go(commands, { ...options, replaceUrl: true }, { stack: 'reset' });\n }\n\n /**\n * Go back exactly as the Android back button does: pop the stack in front, or at the root of a\n * tab's stack, go to the first tab. Not one entry back through history, which after a trip to\n * another tab is that tab. At the root of the app there is nowhere to go, and this does nothing.\n */\n back(): void {\n // Optional call: a test that stands a plain object in for the router has no such signal.\n if (!this.router.currentNavigation?.()) {\n this.outlets.back();\n return;\n }\n // Asked while a navigation is still putting its screen up: a page that leaves as it appears,\n // from an effect in its constructor. No stack has that screen to pop yet, so the back waits\n // for the navigation, and goes nowhere if the navigation does not arrive.\n if (this.backWaiting) return;\n this.backWaiting = this.router.events.subscribe((event) => {\n const ended = event instanceof NavigationEnd;\n if (!ended && !(event instanceof NavigationCancel) && !(event instanceof NavigationError)) {\n return;\n }\n this.backWaiting?.unsubscribe();\n this.backWaiting = null;\n if (ended) this.outlets.back();\n });\n }\n\n /** The wait of a back asked for mid-navigation, so two asked for in one are one. */\n private backWaiting: { unsubscribe(): void } | null = null;\n\n /**\n * Pop straight back to the screen at `commands`, taking every screen above it off the stack at\n * once - `popToViewController`. The screen is the one already there, with its state, not a new\n * one. Resolves false, doing nothing, when no screen below the top of a stack in front is\n * showing that url; a push is the way to a screen that is not there.\n */\n popTo(commands: NavigationCommands): Promise<boolean> {\n const tree =\n typeof commands === 'string'\n ? this.router.parseUrl(commands)\n : this.router.createUrlTree([...commands]);\n return this.outlets.popTo(this.router.serializeUrl(tree));\n }\n\n /**\n * Pop the stack in front to its first screen - `popToRootViewController`. Inside a tab, that is\n * the tab's first screen; the screens below keep their state. Resolves false at the root already.\n */\n popToRoot(): Promise<boolean> {\n return this.outlets.popToRoot();\n }\n\n private go(\n commands: NavigationCommands,\n options: NativeNavigationOptions,\n intent: NativeIntent,\n ): Promise<boolean> {\n const { presentation, state, ...extras } = options;\n const full: NativeIntent = presentation\n ? { ...intent, presentation: { ...intent.presentation, ...presentation } }\n : intent;\n\n const navigate = () => {\n const list = typeof commands === 'string' ? [pathOf(commands)] : [...commands];\n const all = {\n ...extras,\n ...(typeof commands === 'string' ? this.queryOf(commands, extras) : {}),\n state: { ...(state as object), [NATIVE_INTENT]: full },\n };\n const leaving = leavingPresented(this.router, list, all);\n return leaving ? this.router.navigateByUrl(leaving, all) : this.router.navigate(list, all);\n };\n // A screen stacked on the first one waits for it. Navigating while the router's first\n // navigation is still running - a lazily loaded root waiting on its import - cancels that\n // navigation, and the app opens on this screen with nothing beneath it. A reset replaces the\n // stack anyway, so it goes at once.\n const start = () => this.underTabRoot(commands, extras, full, navigate);\n return intent.stack !== 'reset' && this.firstNavigationRunning()\n ? this.firstNavigation().then(start)\n : start();\n }\n\n /**\n * A push into a tab nobody has opened goes to the tab first, so its stack has the tab's own\n * first screen under the page, and a back from the page lands there: what a tab bar with a\n * navigation controller per tab does. Without it the page is the stack's only screen, and the\n * same push leaves a different stack behind depending on whether the tab was ever tapped. A\n * presented screen goes where it is asked, and a replace or a reset has nothing to keep.\n */\n private underTabRoot(\n commands: NavigationCommands,\n extras: NavigationExtras,\n intent: NativeIntent,\n navigate: () => Promise<boolean>,\n ): Promise<boolean> {\n if (intent.stack !== 'push' || intent.presentation) return navigate();\n const tab = this.outlets.unopenedTabOf(this.pathFor(commands, extras));\n if (!tab) return navigate();\n return this.router.navigateByUrl(tab).then((arrived) => (arrived ? navigate() : false));\n }\n\n /** The path the commands lead to, without its query and fragment. */\n private pathFor(commands: NavigationCommands, extras: NavigationExtras): string {\n const url =\n typeof commands === 'string'\n ? commands\n : this.router.serializeUrl(this.router.createUrlTree([...commands], extras));\n return pathOf(url);\n }\n\n /**\n * A page of a tab that is not in front, presented over the one that is: its url would select\n * its own tab and show it there, so it goes to the root's presented outlet instead, where the\n * app's own stack shows it over everything. See `presented-route.ts`. Null for a page of the\n * tab in front, or of no tab, which is presented where its url puts it.\n */\n private overTheTabInFront(\n commands: NavigationCommands,\n extras: NavigationExtras,\n ): Promise<{ commands: unknown[]; extras: NavigationExtras } | null> | null {\n if (!this.outlets.presents) return null;\n const path = this.pathFor(commands, extras);\n if (!this.outlets.inTabBehind(path)) return null;\n const query = typeof commands === 'string' ? this.queryOf(commands, extras) : {};\n return presentedCommands(this.router, path).then(\n (presented) =>\n presented && { commands: presented, extras: { ...extras, ...query, relativeTo: null } },\n );\n }\n\n /**\n * The query and fragment written into a url string, as the extras `navigate` takes them: it\n * reads a string command as one path segment, and would encode a `?` into it. Extras given\n * alongside win over the ones in the string.\n */\n private queryOf(url: string, extras: NavigationExtras): NavigationExtras {\n const cut = url.search(/[?#]/);\n if (cut < 0) return {};\n const { queryParams, fragment } = this.router.parseUrl(url.slice(cut));\n return {\n queryParams: { ...queryParams, ...extras.queryParams },\n fragment: extras.fragment ?? fragment ?? undefined,\n };\n }\n\n private firstNavigationRunning(): boolean {\n const router = this.router as Partial<Router>;\n return router.navigated === false && !!router.currentNavigation?.();\n }\n\n private firstNavigation(): Promise<void> {\n return new Promise((resolve) => {\n const events = this.router.events.subscribe((event) => {\n if (\n event instanceof NavigationEnd ||\n event instanceof NavigationCancel ||\n event instanceof NavigationError\n ) {\n events.unsubscribe();\n resolve();\n }\n });\n });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"native-navigation.js","sourceRoot":"","sources":["../src/native-navigation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The typed facade over `Router` for the intents a URL cannot express (ADR 0003).\n *\n * Angular's `Router` stays the single source of truth: each of these is a line over\n * `router.navigate`, setting `replaceUrl` and a presentation on `NavigationExtras.state`. Deep\n * links, guards and resolvers keep working because it is still the Router, and because state\n * rides in history a back restores the presentation the screen was opened with.\n *\n * A navigation service that owns the stack was rejected: it either re-implements guards,\n * resolvers and deep linking, or it drifts from the URL, and once the stack is not derived from\n * router state the two can disagree.\n *\n * No decorators, so tests can import it at runtime and so it can be provided with `useClass`,\n * exactly as `NativePlatformLocation` and `NativeStackReuseStrategy` are.\n */\nimport { DestroyRef, inject } from '@angular/core';\nimport {\n NavigationCancel,\n NavigationCancellationCode,\n NavigationEnd,\n NavigationError,\n NavigationSkipped,\n NavigationSkippedCode,\n Router,\n type Navigation,\n type NavigationExtras,\n} from '@angular/router';\nimport { NativeBack } from './native-back.ts';\nimport type { ScreenPresentation, StackPresentation } from './screen-presentation.ts';\nimport { leavingPresented, presentedCommands } from './presented-route.ts';\n\n/** What `Router` takes: a url string, or the array form with segments and params. */\nexport type NavigationCommands = string | readonly unknown[];\n\n/**\n * Where the intent rides. `NavigationExtras.state` is the app's own object, so ours sits under\n * one reserved key rather than spread through it.\n */\nexport const NATIVE_INTENT = 'ɵnativeStack';\n\n/** What a navigation asks the outlet to do with the stack, over and above rendering the route. */\nexport interface NativeIntent {\n /** `replace` drops the screen it supersedes; `reset` empties the stack first. */\n readonly stack?: 'push' | 'replace' | 'reset';\n readonly presentation?: ScreenPresentation;\n}\n\nexport interface NativeNavigationOptions extends NavigationExtras {\n /** How the screen arrives: presentation, animation, gesture and sheet props. */\n presentation?: ScreenPresentation;\n}\n\nexport interface PresentOptions extends NativeNavigationOptions {\n /** Shorthand for `presentation.stackPresentation`. Defaults to `modal`. */\n as?: StackPresentation;\n}\n\n/** A url string up to its query or fragment. */\nconst pathOf = (url: string): string => url.split(/[?#]/, 1)[0]!;\n\n/** Read the intent off a navigation's state, if it carries one. */\nexport function intentOf(state: unknown): NativeIntent | null {\n if (!state || typeof state !== 'object') return null;\n const intent = (state as Record<string, unknown>)[NATIVE_INTENT];\n return intent && typeof intent === 'object' ? (intent as NativeIntent) : null;\n}\n\n/**\n * The intent of a navigation a guard redirected, for the navigation the redirect starts.\n *\n * The router starts a new navigation for a guard's `UrlTree`, keeping `replaceUrl` but not\n * `state`, so a reset to a page that sends a signed-out user to sign in would push the sign-in\n * page over the stack it was meant to clear. Held for that one navigation by its id, which no\n * later navigation shares.\n */\nconst redirected = new WeakMap<object, { readonly id: number; readonly intent: NativeIntent }>();\n\n/** What a navigation asks of the stack: its own intent, or the one it was redirected from. */\nexport function navigationIntent(\n router: object,\n navigation: Pick<Navigation, 'id' | 'extras'> | null | undefined,\n): NativeIntent | null {\n if (!navigation) return null;\n const own = intentOf(navigation.extras.state);\n if (own) return own;\n const carried = redirected.get(router);\n return carried?.id === navigation.id ? carried.intent : null;\n}\n\n/**\n * Hand a navigation's intent on to the one its guard redirects it to. The router schedules that\n * navigation as soon as it has reported the cancel, so it takes the next id.\n *\n * A reset to the url already showing, its own or the one a guard sends it to, is one the router\n * skips: no outlet activates anything for it, so the stack is reset here, to the screen on top.\n */\nexport function carryIntentAcrossRedirects(): void {\n const router = inject(Router);\n const outlets = inject(NativeBack);\n const events = router.events.subscribe((event) => {\n if (event instanceof NavigationCancel && event.code === NavigationCancellationCode.Redirect) {\n carryPast(router, event.id);\n return;\n }\n if (\n event instanceof NavigationSkipped &&\n event.code === NavigationSkippedCode.IgnoredSameUrlNavigation &&\n intentAt(router, event.id)?.stack === 'reset'\n ) {\n outlets.resetToTop();\n }\n if (ended(event) && redirected.get(router)?.id === event.id) redirected.delete(router);\n });\n inject(DestroyRef).onDestroy(() => events.unsubscribe());\n}\n\n/** The intent of the navigation in progress, when it is the one with `id`. */\nfunction intentAt(router: Router, id: number): NativeIntent | null {\n const navigation = router.currentNavigation();\n return navigation?.id === id ? navigationIntent(router, navigation) : null;\n}\n\n/** Hold the intent of the navigation a guard redirected for the one the redirect starts. */\nfunction carryPast(router: Router, id: number): void {\n const intent = intentAt(router, id);\n if (intent) redirected.set(router, { id: id + 1, intent });\n else redirected.delete(router);\n}\n\n/** Whether the event is the last a navigation reports. */\nfunction ended(event: unknown): event is { readonly id: number } {\n return (\n event instanceof NavigationEnd ||\n event instanceof NavigationCancel ||\n event instanceof NavigationError ||\n event instanceof NavigationSkipped\n );\n}\n\nexport class NativeNavigation {\n private readonly router = inject(Router);\n private readonly outlets = inject(NativeBack);\n\n /**\n * Push a new screen onto the stack. The plain case, and what `nativeRouterLink` does.\n *\n * From a presented screen the new one is presented the same way, over it, since a pushed\n * screen would slide in under it; `presentation` says otherwise, `stackAnimation` and all.\n */\n push(commands: NavigationCommands, options: NativeNavigationOptions = {}): Promise<boolean> {\n return this.go(commands, options, { stack: 'push' });\n }\n\n /**\n * Swap the current screen for another. The history entry is replaced too, so a back from the\n * new screen goes where a back from the old one would have.\n */\n replace(commands: NavigationCommands, options: NavigationExtras = {}): Promise<boolean> {\n return this.go(commands, { ...options, replaceUrl: true }, { stack: 'replace' });\n }\n\n /**\n * Present a screen over the stack rather than pushing into it: a modal, or a sheet.\n *\n * `present('/filters', { as: 'formSheet', presentation: { sheetAllowedDetents: [0.5, 1] } })`.\n *\n * A presented screen is put on screen outside the stack's navigation controller, so it has no\n * native header and no automatic safe-area insets: it needs its own way out, and its own\n * `safe-area-view`. A sheet can also be dragged away, but `fullScreenModal` cannot, and with\n * `gestureEnabled: false` neither can.\n */\n present(commands: NavigationCommands, options: PresentOptions = {}): Promise<boolean> {\n const { as = 'modal', presentation, ...extras } = options;\n const intent: NativeIntent = {\n stack: 'push',\n presentation: { stackPresentation: as, ...presentation },\n };\n // Asked at once, and only waited for when the page is another tab's: finding its route may\n // load a lazy one.\n const over = this.overTheTabInFront(commands, extras);\n if (!over) return this.go(commands, extras, intent);\n return over.then((found) =>\n this.go(found?.commands ?? commands, found?.extras ?? extras, intent),\n );\n }\n\n /**\n * Replace the whole stack with one screen, the way signing out or finishing onboarding does.\n * Every screen below is destroyed, so there is nothing to go back to.\n */\n reset(commands: NavigationCommands, options: NavigationExtras = {}): Promise<boolean> {\n return this.go(commands, { ...options, replaceUrl: true }, { stack: 'reset' });\n }\n\n /**\n * Go back exactly as the Android back button does: pop the stack in front, or at the root of a\n * tab's stack, go to the first tab. Not one entry back through history, which after a trip to\n * another tab is that tab. At the root of the app there is nowhere to go, and this does nothing.\n */\n back(): void {\n // Optional call: a test that stands a plain object in for the router has no such signal.\n if (!this.router.currentNavigation?.()) {\n this.outlets.back();\n return;\n }\n // Asked while a navigation is still putting its screen up: a page that leaves as it appears,\n // from an effect in its constructor. No stack has that screen to pop yet, so the back waits\n // for the navigation, and goes nowhere if the navigation does not arrive.\n if (this.backWaiting) return;\n this.backWaiting = this.router.events.subscribe((event) => {\n const ended = event instanceof NavigationEnd;\n if (!ended && !(event instanceof NavigationCancel) && !(event instanceof NavigationError)) {\n return;\n }\n this.backWaiting?.unsubscribe();\n this.backWaiting = null;\n if (ended) this.outlets.back();\n });\n }\n\n /** The wait of a back asked for mid-navigation, so two asked for in one are one. */\n private backWaiting: { unsubscribe(): void } | null = null;\n\n /**\n * Pop straight back to the screen at `commands`, taking every screen above it off the stack at\n * once - `popToViewController`. The screen is the one already there, with its state, not a new\n * one. Resolves false, doing nothing, when no screen below the top of a stack in front is\n * showing that url; a push is the way to a screen that is not there.\n */\n popTo(commands: NavigationCommands): Promise<boolean> {\n const tree =\n typeof commands === 'string'\n ? this.router.parseUrl(commands)\n : this.router.createUrlTree([...commands]);\n return this.outlets.popTo(this.router.serializeUrl(tree));\n }\n\n /**\n * Pop the stack in front to its first screen - `popToRootViewController`. Inside a tab, that is\n * the tab's first screen; the screens below keep their state. Resolves false at the root already.\n */\n popToRoot(): Promise<boolean> {\n return this.outlets.popToRoot();\n }\n\n private go(\n commands: NavigationCommands,\n options: NativeNavigationOptions,\n intent: NativeIntent,\n ): Promise<boolean> {\n const { presentation, state, ...extras } = options;\n const full: NativeIntent = presentation\n ? { ...intent, presentation: { ...intent.presentation, ...presentation } }\n : intent;\n\n const navigate = () => {\n const list = typeof commands === 'string' ? [pathOf(commands)] : [...commands];\n const all = {\n ...extras,\n ...(typeof commands === 'string' ? this.queryOf(commands, extras) : {}),\n state: { ...(state as object), [NATIVE_INTENT]: full },\n };\n const leaving = leavingPresented(this.router, list, all);\n return leaving ? this.router.navigateByUrl(leaving, all) : this.router.navigate(list, all);\n };\n // A screen stacked on the first one waits for it. Navigating while the router's first\n // navigation is still running - a lazily loaded root waiting on its import - cancels that\n // navigation, and the app opens on this screen with nothing beneath it. A reset replaces the\n // stack anyway, so it goes at once.\n const start = () => this.underTabRoot(commands, extras, full, navigate);\n return intent.stack !== 'reset' && this.firstNavigationRunning()\n ? this.firstNavigation().then(start)\n : start();\n }\n\n /**\n * A push into a tab nobody has opened goes to the tab first, so its stack has the tab's own\n * first screen under the page, and a back from the page lands there: what a tab bar with a\n * navigation controller per tab does. Without it the page is the stack's only screen, and the\n * same push leaves a different stack behind depending on whether the tab was ever tapped. A\n * presented screen goes where it is asked, and a replace or a reset has nothing to keep.\n */\n private underTabRoot(\n commands: NavigationCommands,\n extras: NavigationExtras,\n intent: NativeIntent,\n navigate: () => Promise<boolean>,\n ): Promise<boolean> {\n if (intent.stack !== 'push' || intent.presentation) return navigate();\n const tab = this.outlets.unopenedTabOf(this.pathFor(commands, extras));\n if (!tab) return navigate();\n return this.router.navigateByUrl(tab).then((arrived) => (arrived ? navigate() : false));\n }\n\n /** The path the commands lead to, without its query and fragment. */\n private pathFor(commands: NavigationCommands, extras: NavigationExtras): string {\n const url =\n typeof commands === 'string'\n ? commands\n : this.router.serializeUrl(this.router.createUrlTree([...commands], extras));\n return pathOf(url);\n }\n\n /**\n * A page of a tab that is not in front, presented over the one that is: its url would select\n * its own tab and show it there, so it goes to the root's presented outlet instead, where the\n * app's own stack shows it over everything. See `presented-route.ts`. Null for a page of the\n * tab in front, or of no tab, which is presented where its url puts it.\n */\n private overTheTabInFront(\n commands: NavigationCommands,\n extras: NavigationExtras,\n ): Promise<{ commands: unknown[]; extras: NavigationExtras } | null> | null {\n if (!this.outlets.presents) return null;\n const path = this.pathFor(commands, extras);\n if (!this.outlets.inTabBehind(path)) return null;\n const query = typeof commands === 'string' ? this.queryOf(commands, extras) : {};\n return presentedCommands(this.router, path).then(\n (presented) =>\n presented && { commands: presented, extras: { ...extras, ...query, relativeTo: null } },\n );\n }\n\n /**\n * The query and fragment written into a url string, as the extras `navigate` takes them: it\n * reads a string command as one path segment, and would encode a `?` into it. Extras given\n * alongside win over the ones in the string.\n */\n private queryOf(url: string, extras: NavigationExtras): NavigationExtras {\n const cut = url.search(/[?#]/);\n if (cut < 0) return {};\n const { queryParams, fragment } = this.router.parseUrl(url.slice(cut));\n return {\n queryParams: { ...queryParams, ...extras.queryParams },\n fragment: extras.fragment ?? fragment ?? undefined,\n };\n }\n\n private firstNavigationRunning(): boolean {\n const router = this.router as Partial<Router>;\n return router.navigated === false && !!router.currentNavigation?.();\n }\n\n private firstNavigation(): Promise<void> {\n return new Promise((resolve) => {\n const events = this.router.events.subscribe((event) => {\n if (\n event instanceof NavigationEnd ||\n event instanceof NavigationCancel ||\n event instanceof NavigationError\n ) {\n events.unsubscribe();\n resolve();\n }\n });\n });\n }\n}\n"]}
|
|
@@ -40,6 +40,11 @@ export declare class NativePlatformLocation implements PlatformLocation {
|
|
|
40
40
|
* stack outlet, which has to know whether going back reaches the screen a native pop revealed.
|
|
41
41
|
*/
|
|
42
42
|
ɵurlAt(offset: number): string | null;
|
|
43
|
+
/** The history up to the page showing, oldest first. For parking it across a reload. */
|
|
44
|
+
ɵpages(): readonly {
|
|
45
|
+
readonly url: string;
|
|
46
|
+
readonly state: unknown;
|
|
47
|
+
}[];
|
|
43
48
|
private get current();
|
|
44
49
|
getBaseHrefFromDOM(): string;
|
|
45
50
|
getState(): unknown;
|
|
@@ -38,6 +38,10 @@ export class NativePlatformLocation {
|
|
|
38
38
|
ɵurlAt(offset) {
|
|
39
39
|
return this.entries[this.cursor + offset]?.url ?? null;
|
|
40
40
|
}
|
|
41
|
+
/** The history up to the page showing, oldest first. For parking it across a reload. */
|
|
42
|
+
ɵpages() {
|
|
43
|
+
return this.entries.slice(0, this.cursor + 1);
|
|
44
|
+
}
|
|
41
45
|
get current() {
|
|
42
46
|
return this.entries[this.cursor];
|
|
43
47
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-platform-location.js","sourceRoot":"","sources":["../src/native-platform-location.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `PlatformLocation` over an in-memory history.\n *\n * Angular's `Location` reads and writes the URL through this. On the web the browser owns the\n * history stack; here nothing does, so we keep it ourselves and the URL tree stays the source of\n * truth in both directions: `router.navigate()` pushes an entry, and a swipe-back reaches the\n * outlet as the screen's `dismissed` event, which usually calls `historyGo(-1)` here and emits\n * `popstate` so the router follows. Not always: see the stack outlet's `popBy` for when the entry\n * behind is another tab.\n *\n * No decorators, so tests can import it at runtime and so it can be provided with `useClass`.\n */\nimport type { LocationChangeListener, PlatformLocation } from '@angular/common';\n/**\n * What the history needs of a deep-link source, which `DeepLinks` from\n * `@ng-native/device` satisfies. Structural rather than the class itself: the router has no\n * use for the rest of it, and a test has no use for a real one.\n */\nexport interface LinkSource {\n /** The path the app was launched with, or null for a normal launch. */\n initialUrl(): string | null;\n /** Links that arrive while the app is running. Returns an unsubscribe. */\n subscribe(listener: (url: string) => void): () => void;\n}\n\ninterface HistoryEntry {\n url: string;\n state: unknown;\n}\n\n/** Split a url into its path, `?search` and `#hash` parts. */\nfunction parse(url: string): { pathname: string; search: string; hash: string } {\n const hashAt = url.indexOf('#');\n const hash = hashAt === -1 ? '' : url.slice(hashAt);\n const withoutHash = hashAt === -1 ? url : url.slice(0, hashAt);\n\n const searchAt = withoutHash.indexOf('?');\n const search = searchAt === -1 ? '' : withoutHash.slice(searchAt);\n const pathname = searchAt === -1 ? withoutHash : withoutHash.slice(0, searchAt);\n\n return { pathname: pathname || '/', search, hash };\n}\n\nexport class NativePlatformLocation implements PlatformLocation {\n private readonly entries: HistoryEntry[] = [{ url: '/', state: null }];\n private cursor = 0;\n\n private readonly popStateListeners = new Set<LocationChangeListener>();\n private readonly hashChangeListeners = new Set<LocationChangeListener>();\n\n /**\n * A deep link is history, not a navigation of its own: the launch url *is* the first entry,\n * and one that arrives later is pushed and announced as a popstate, which is the only thing\n * the router listens to. `links` is null in a test that has no interest in either.\n */\n constructor(links: LinkSource | null = null) {\n const initial = links?.initialUrl();\n if (initial) this.entries[0] = { url: initial, state: null };\n links?.subscribe((url) => this.open(url));\n }\n\n /** Follow a link that arrived while the app was running. */\n open(url: string): void {\n if (url === this.current.url) return;\n this.pushState(null, '', url);\n this.emitPopState();\n }\n\n /**\n * The url of the entry `offset` away from the current one, or null past either end. For the\n * stack outlet, which has to know whether going back reaches the screen a native pop revealed.\n */\n ɵurlAt(offset: number): string | null {\n return this.entries[this.cursor + offset]?.url ?? null;\n }\n\n private get current(): HistoryEntry {\n return this.entries[this.cursor]!;\n }\n\n // --- PlatformLocation ---------------------------------------------------\n\n getBaseHrefFromDOM(): string {\n return '/';\n }\n\n getState(): unknown {\n return this.current.state;\n }\n\n onPopState(fn: LocationChangeListener): VoidFunction {\n this.popStateListeners.add(fn);\n return () => this.popStateListeners.delete(fn);\n }\n\n onHashChange(fn: LocationChangeListener): VoidFunction {\n this.hashChangeListeners.add(fn);\n return () => this.hashChangeListeners.delete(fn);\n }\n\n get href(): string {\n return this.current.url;\n }\n\n /** There is no origin on a device. Angular only ever compares these, never resolves them. */\n get protocol(): string {\n return 'app:';\n }\n\n get hostname(): string {\n return '';\n }\n\n get port(): string {\n return '';\n }\n\n get pathname(): string {\n return parse(this.current.url).pathname;\n }\n\n get search(): string {\n return parse(this.current.url).search;\n }\n\n get hash(): string {\n return parse(this.current.url).hash;\n }\n\n pushState(state: unknown, _title: string, url: string): void {\n // A push after going back discards the forward entries, as a browser does.\n this.entries.length = this.cursor + 1;\n this.entries.push({ url, state });\n this.cursor++;\n }\n\n replaceState(state: unknown, _title: string, url: string): void {\n this.entries[this.cursor] = { url, state };\n }\n\n forward(): void {\n this.historyGo(1);\n }\n\n back(): void {\n this.historyGo(-1);\n }\n\n historyGo(relativePosition = 0): void {\n const target = this.cursor + relativePosition;\n // Out of range is a no-op, matching the browser rather than throwing.\n if (target < 0 || target >= this.entries.length) return;\n this.cursor = target;\n this.emitPopState();\n }\n\n private emitPopState(): void {\n const event = { type: 'popstate', state: this.current.state };\n for (const listener of [...this.popStateListeners]) listener(event);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"native-platform-location.js","sourceRoot":"","sources":["../src/native-platform-location.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `PlatformLocation` over an in-memory history.\n *\n * Angular's `Location` reads and writes the URL through this. On the web the browser owns the\n * history stack; here nothing does, so we keep it ourselves and the URL tree stays the source of\n * truth in both directions: `router.navigate()` pushes an entry, and a swipe-back reaches the\n * outlet as the screen's `dismissed` event, which usually calls `historyGo(-1)` here and emits\n * `popstate` so the router follows. Not always: see the stack outlet's `popBy` for when the entry\n * behind is another tab.\n *\n * No decorators, so tests can import it at runtime and so it can be provided with `useClass`.\n */\nimport type { LocationChangeListener, PlatformLocation } from '@angular/common';\n/**\n * What the history needs of a deep-link source, which `DeepLinks` from\n * `@ng-native/device` satisfies. Structural rather than the class itself: the router has no\n * use for the rest of it, and a test has no use for a real one.\n */\nexport interface LinkSource {\n /** The path the app was launched with, or null for a normal launch. */\n initialUrl(): string | null;\n /** Links that arrive while the app is running. Returns an unsubscribe. */\n subscribe(listener: (url: string) => void): () => void;\n}\n\ninterface HistoryEntry {\n url: string;\n state: unknown;\n}\n\n/** Split a url into its path, `?search` and `#hash` parts. */\nfunction parse(url: string): { pathname: string; search: string; hash: string } {\n const hashAt = url.indexOf('#');\n const hash = hashAt === -1 ? '' : url.slice(hashAt);\n const withoutHash = hashAt === -1 ? url : url.slice(0, hashAt);\n\n const searchAt = withoutHash.indexOf('?');\n const search = searchAt === -1 ? '' : withoutHash.slice(searchAt);\n const pathname = searchAt === -1 ? withoutHash : withoutHash.slice(0, searchAt);\n\n return { pathname: pathname || '/', search, hash };\n}\n\nexport class NativePlatformLocation implements PlatformLocation {\n private readonly entries: HistoryEntry[] = [{ url: '/', state: null }];\n private cursor = 0;\n\n private readonly popStateListeners = new Set<LocationChangeListener>();\n private readonly hashChangeListeners = new Set<LocationChangeListener>();\n\n /**\n * A deep link is history, not a navigation of its own: the launch url *is* the first entry,\n * and one that arrives later is pushed and announced as a popstate, which is the only thing\n * the router listens to. `links` is null in a test that has no interest in either.\n */\n constructor(links: LinkSource | null = null) {\n const initial = links?.initialUrl();\n if (initial) this.entries[0] = { url: initial, state: null };\n links?.subscribe((url) => this.open(url));\n }\n\n /** Follow a link that arrived while the app was running. */\n open(url: string): void {\n if (url === this.current.url) return;\n this.pushState(null, '', url);\n this.emitPopState();\n }\n\n /**\n * The url of the entry `offset` away from the current one, or null past either end. For the\n * stack outlet, which has to know whether going back reaches the screen a native pop revealed.\n */\n ɵurlAt(offset: number): string | null {\n return this.entries[this.cursor + offset]?.url ?? null;\n }\n\n /** The history up to the page showing, oldest first. For parking it across a reload. */\n ɵpages(): readonly { readonly url: string; readonly state: unknown }[] {\n return this.entries.slice(0, this.cursor + 1);\n }\n\n private get current(): HistoryEntry {\n return this.entries[this.cursor]!;\n }\n\n // --- PlatformLocation ---------------------------------------------------\n\n getBaseHrefFromDOM(): string {\n return '/';\n }\n\n getState(): unknown {\n return this.current.state;\n }\n\n onPopState(fn: LocationChangeListener): VoidFunction {\n this.popStateListeners.add(fn);\n return () => this.popStateListeners.delete(fn);\n }\n\n onHashChange(fn: LocationChangeListener): VoidFunction {\n this.hashChangeListeners.add(fn);\n return () => this.hashChangeListeners.delete(fn);\n }\n\n get href(): string {\n return this.current.url;\n }\n\n /** There is no origin on a device. Angular only ever compares these, never resolves them. */\n get protocol(): string {\n return 'app:';\n }\n\n get hostname(): string {\n return '';\n }\n\n get port(): string {\n return '';\n }\n\n get pathname(): string {\n return parse(this.current.url).pathname;\n }\n\n get search(): string {\n return parse(this.current.url).search;\n }\n\n get hash(): string {\n return parse(this.current.url).hash;\n }\n\n pushState(state: unknown, _title: string, url: string): void {\n // A push after going back discards the forward entries, as a browser does.\n this.entries.length = this.cursor + 1;\n this.entries.push({ url, state });\n this.cursor++;\n }\n\n replaceState(state: unknown, _title: string, url: string): void {\n this.entries[this.cursor] = { url, state };\n }\n\n forward(): void {\n this.historyGo(1);\n }\n\n back(): void {\n this.historyGo(-1);\n }\n\n historyGo(relativePosition = 0): void {\n const target = this.cursor + relativePosition;\n // Out of range is a no-op, matching the browser rather than throwing.\n if (target < 0 || target >= this.entries.length) return;\n this.cursor = target;\n this.emitPopState();\n }\n\n private emitPopState(): void {\n const event = { type: 'popstate', state: this.current.state };\n for (const listener of [...this.popStateListeners]) listener(event);\n }\n}\n"]}
|
|
@@ -30,12 +30,14 @@ export class NativeRouterLink {
|
|
|
30
30
|
replaceUrl: this.replace(),
|
|
31
31
|
...own,
|
|
32
32
|
};
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
// The stack reads the intent, as `NativeNavigation` sets it. `replaceUrl` only swaps the
|
|
34
|
+
// history entry: without `replace` the replaced screen stays mounted underneath, and a swipe
|
|
35
|
+
// back lands on a page the history no longer has. So the intent follows `replaceUrl` as the
|
|
36
|
+
// navigation has it, `[extras]` included, not the `replace` input alone. Without `push` a
|
|
37
|
+
// link to a url already on the stack goes back to that screen rather than putting a new one
|
|
38
|
+
// over it.
|
|
39
|
+
const stack = extras.replaceUrl ? 'replace' : 'push';
|
|
40
|
+
extras.state = { ...own?.state, [NATIVE_INTENT]: { stack } };
|
|
39
41
|
void this.router.navigate(Array.isArray(target) ? [...target] : [target], extras);
|
|
40
42
|
}
|
|
41
43
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: NativeRouterLink, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-router-link.js","sourceRoot":"","sources":["../src/native-router-link.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3E,OAAO,EAAE,cAAc,EAAE,MAAM,EAAyB,MAAM,iBAAiB,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;;AAMvD,MAAM,OAAO,gBAAgB;IAC3B,kEAAkE;IACzD,gBAAgB,GAAG,KAAK,CAAC,QAAQ;yFAA+B,CAAC;IACjE,MAAM,GAAG,KAAK;0FAAoB,CAAC;IAE5C,gEAAgE;IACvD,OAAO,GAAG,KAAK,CAAC,KAAK,+EAAI,SAAS,EAAE,gBAAgB,GAAG,CAAC;IAEhD,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACxB,KAAK,GAAG,MAAM,CAAC,cAAc,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAE1D,QAAQ;QAChB,MAAM,MAAM,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACvC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAqB;YAC/B,UAAU,EAAE,IAAI,CAAC,KAAK;YACtB,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE;YAC1B,GAAG,GAAG;SACP,CAAC;QACF,4FAA4F;QAC5F,
|
|
1
|
+
{"version":3,"file":"native-router-link.js","sourceRoot":"","sources":["../src/native-router-link.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3E,OAAO,EAAE,cAAc,EAAE,MAAM,EAAyB,MAAM,iBAAiB,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;;AAMvD,MAAM,OAAO,gBAAgB;IAC3B,kEAAkE;IACzD,gBAAgB,GAAG,KAAK,CAAC,QAAQ;yFAA+B,CAAC;IACjE,MAAM,GAAG,KAAK;0FAAoB,CAAC;IAE5C,gEAAgE;IACvD,OAAO,GAAG,KAAK,CAAC,KAAK,+EAAI,SAAS,EAAE,gBAAgB,GAAG,CAAC;IAEhD,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACxB,KAAK,GAAG,MAAM,CAAC,cAAc,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAE1D,QAAQ;QAChB,MAAM,MAAM,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACvC,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAqB;YAC/B,UAAU,EAAE,IAAI,CAAC,KAAK;YACtB,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE;YAC1B,GAAG,GAAG;SACP,CAAC;QACF,yFAAyF;QACzF,6FAA6F;QAC7F,4FAA4F;QAC5F,0FAA0F;QAC1F,4FAA4F;QAC5F,WAAW;QACX,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;QACrD,MAAM,CAAC,KAAK,GAAG,EAAE,GAAG,GAAG,EAAE,KAAK,EAAE,CAAC,aAAa,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC;QAE7D,KAAK,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;IACpF,CAAC;uGA7BU,gBAAgB;2FAAhB,gBAAgB;;2FAAhB,gBAAgB;kBAJ5B,SAAS;mBAAC;oBACT,QAAQ,EAAE,oBAAoB;oBAC9B,IAAI,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE;iBAClC","sourcesContent":["/**\n * `routerLink` for native.\n *\n * Angular's `RouterLink` host-binds `href` and listens for a DOM click, neither of which exists\n * here. This binds to a `<pressable>`'s `press` output instead.\n *\n * Because Angular resolves outputs across every directive on an element, the host listener below\n * binds to `Pressable`'s `press` output without importing it, so the router package does not\n * depend on the components package.\n */\nimport { Directive, booleanAttribute, inject, input } from '@angular/core';\nimport { ActivatedRoute, Router, type NavigationExtras } from '@angular/router';\nimport { NATIVE_INTENT } from './native-navigation.ts';\n\n@Directive({\n selector: '[nativeRouterLink]',\n host: { '(press)': 'navigate()' },\n})\nexport class NativeRouterLink {\n /** A url string, or the command array `Router.navigate` takes. */\n readonly nativeRouterLink = input.required<string | readonly unknown[]>();\n readonly extras = input<NavigationExtras>();\n\n /** Replace the current screen rather than pushing a new one. */\n readonly replace = input(false, { transform: booleanAttribute });\n\n private readonly router = inject(Router);\n private readonly route = inject(ActivatedRoute, { optional: true });\n\n protected navigate(): void {\n const target = this.nativeRouterLink();\n const own = this.extras();\n const extras: NavigationExtras = {\n relativeTo: this.route,\n replaceUrl: this.replace(),\n ...own,\n };\n // The stack reads the intent, as `NativeNavigation` sets it. `replaceUrl` only swaps the\n // history entry: without `replace` the replaced screen stays mounted underneath, and a swipe\n // back lands on a page the history no longer has. So the intent follows `replaceUrl` as the\n // navigation has it, `[extras]` included, not the `replace` input alone. Without `push` a\n // link to a url already on the stack goes back to that screen rather than putting a new one\n // over it.\n const stack = extras.replaceUrl ? 'replace' : 'push';\n extras.state = { ...own?.state, [NATIVE_INTENT]: { stack } };\n\n void this.router.navigate(Array.isArray(target) ? [...target] : [target], extras);\n }\n}\n"]}
|
|
@@ -144,6 +144,12 @@ export declare class NativeStackOutlet implements RouterOutletContract, OnInit {
|
|
|
144
144
|
private claimPresentation;
|
|
145
145
|
/** Take a screen out for good: its bindings, its component, and its native view. */
|
|
146
146
|
private drop;
|
|
147
|
+
/**
|
|
148
|
+
* Take off the lowest screen presented rather than pushed, and every screen over it, for a tab
|
|
149
|
+
* that has gone behind another: see `IN_TAB`. Answers the url of the screen left on top, or null
|
|
150
|
+
* when nothing was presented.
|
|
151
|
+
*/
|
|
152
|
+
private dismissPresented;
|
|
147
153
|
/** Take one entry out of the stack, wherever it is, and drop it. */
|
|
148
154
|
private remove;
|
|
149
155
|
/** The id of the navigation in progress, when there is a router to say. */
|
|
@@ -22,10 +22,11 @@ import { Location, PlatformLocation } from '@angular/common';
|
|
|
22
22
|
import { SCREEN_IN_FRONT } from '@ng-native/device';
|
|
23
23
|
import { bindRouteInputs } from "./bind-route-inputs.js";
|
|
24
24
|
import { NativeBack, isShowing } from "./native-back.js";
|
|
25
|
-
import {
|
|
25
|
+
import { navigationIntent } from "./native-navigation.js";
|
|
26
26
|
import { NativePlatformLocation } from "./native-platform-location.js";
|
|
27
27
|
import { PRESENTED, withoutPresented } from "./presented-route.js";
|
|
28
28
|
import { markScreenRoute } from "./tab-routes.js";
|
|
29
|
+
import { IN_TAB } from "./in-tab.js";
|
|
29
30
|
import { ownHost } from "./own-host.js";
|
|
30
31
|
import { SCREEN_HEADER } from "./screen-header.js";
|
|
31
32
|
import { HostEngine } from '@ng-native/fabric';
|
|
@@ -108,6 +109,13 @@ export class NativeStackOutlet {
|
|
|
108
109
|
const index = this.entries.findIndex((entry) => this.urlOf(entry) === url);
|
|
109
110
|
return index !== -1 && index < this.entries.length - 1 ? this.popToEntry(index) : null;
|
|
110
111
|
},
|
|
112
|
+
dropUnderTop: () => {
|
|
113
|
+
if (this.entries.length < 2)
|
|
114
|
+
return false;
|
|
115
|
+
for (const entry of this.entries.slice(0, -1))
|
|
116
|
+
this.remove(entry);
|
|
117
|
+
return true;
|
|
118
|
+
},
|
|
111
119
|
});
|
|
112
120
|
// Optional for the same reason as the router: the outlet's own tests stand one in without it.
|
|
113
121
|
const navigations = this.router?.events?.subscribe((event) => {
|
|
@@ -118,7 +126,9 @@ export class NativeStackOutlet {
|
|
|
118
126
|
});
|
|
119
127
|
const unhold = this.holder ? this.holdRefusals(this.holder) : undefined;
|
|
120
128
|
const unpresent = this.takePresentedPages(back);
|
|
129
|
+
const unleave = inject(IN_TAB, { optional: true })?.onLeave(() => this.dismissPresented());
|
|
121
130
|
inject(DestroyRef).onDestroy(() => {
|
|
131
|
+
unleave?.();
|
|
122
132
|
unhold?.();
|
|
123
133
|
unpresent?.();
|
|
124
134
|
unsubscribeBack();
|
|
@@ -435,7 +445,7 @@ export class NativeStackOutlet {
|
|
|
435
445
|
* because the router is mid-navigation for exactly as long as an activation takes.
|
|
436
446
|
*/
|
|
437
447
|
currentIntent() {
|
|
438
|
-
return
|
|
448
|
+
return this.router ? navigationIntent(this.router, this.router.getCurrentNavigation()) : null;
|
|
439
449
|
}
|
|
440
450
|
/**
|
|
441
451
|
* How the screen a navigation activates is put on screen.
|
|
@@ -473,6 +483,19 @@ export class NativeStackOutlet {
|
|
|
473
483
|
entry.ref.destroy();
|
|
474
484
|
this.removeScreen(entry.screen);
|
|
475
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* Take off the lowest screen presented rather than pushed, and every screen over it, for a tab
|
|
488
|
+
* that has gone behind another: see `IN_TAB`. Answers the url of the screen left on top, or null
|
|
489
|
+
* when nothing was presented.
|
|
490
|
+
*/
|
|
491
|
+
dismissPresented() {
|
|
492
|
+
const index = this.entries.findIndex((entry, at) => at > 0 && (entry.presentation?.stackPresentation ?? 'push') !== 'push');
|
|
493
|
+
if (index === -1)
|
|
494
|
+
return null;
|
|
495
|
+
for (const entry of this.entries.splice(index))
|
|
496
|
+
this.drop(entry);
|
|
497
|
+
return this.router ? this.urlOf(this.entries[index - 1]) : null;
|
|
498
|
+
}
|
|
476
499
|
/** Take one entry out of the stack, wherever it is, and drop it. */
|
|
477
500
|
remove(entry) {
|
|
478
501
|
const index = this.entries.indexOf(entry);
|