@ng-native/expo 0.2.0 → 0.4.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 +32 -382
- package/dist/async-storage.d.ts +15 -0
- package/dist/async-storage.js +29 -0
- package/dist/async-storage.js.map +1 -0
- package/dist/audio.d.ts +10 -0
- package/dist/audio.js +26 -0
- package/dist/audio.js.map +1 -0
- package/dist/community-views.js +1 -1
- package/dist/community-views.js.map +1 -1
- package/dist/crypto.js +2 -2
- package/dist/crypto.js.map +1 -1
- package/dist/database.d.ts +18 -3
- package/dist/database.js +26 -7
- package/dist/database.js.map +1 -1
- package/dist/date-time-picker.d.ts +33 -0
- package/dist/date-time-picker.js +77 -0
- package/dist/date-time-picker.js.map +1 -0
- package/dist/dom-component.d.ts +5 -0
- package/dist/dom-component.js +57 -2
- package/dist/dom-component.js.map +1 -1
- package/dist/expo-ui-components.d.ts +292 -12
- package/dist/expo-ui-components.js +531 -41
- package/dist/expo-ui-components.js.map +1 -1
- package/dist/expo-ui.js +19 -6
- package/dist/expo-ui.js.map +1 -1
- package/dist/file-system.d.ts +7 -0
- package/dist/file-system.js +16 -3
- package/dist/file-system.js.map +1 -1
- package/dist/foldable.d.ts +78 -0
- package/dist/foldable.js +87 -0
- package/dist/foldable.js.map +1 -0
- package/dist/fonts.d.ts +3 -9
- package/dist/fonts.js +26 -16
- package/dist/fonts.js.map +1 -1
- package/dist/glass.d.ts +1 -1
- package/dist/image-picker.js +3 -9
- package/dist/image-picker.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/live-activity.d.ts +88 -0
- package/dist/live-activity.js +156 -0
- package/dist/live-activity.js.map +1 -0
- package/dist/location.d.ts +17 -0
- package/dist/location.js +20 -7
- package/dist/location.js.map +1 -1
- package/dist/media-library.js +1 -3
- package/dist/media-library.js.map +1 -1
- package/dist/native.d.ts +12 -2
- package/dist/native.js +81 -10
- package/dist/native.js.map +1 -1
- package/dist/notifications.js +2 -4
- package/dist/notifications.js.map +1 -1
- package/dist/permissions.d.ts +2 -0
- package/dist/permissions.js +6 -0
- package/dist/permissions.js.map +1 -1
- package/dist/player.d.ts +24 -11
- package/dist/player.js +7 -26
- package/dist/player.js.map +1 -1
- package/dist/register-expo-view.d.ts +7 -0
- package/dist/register-expo-view.js +6 -1
- package/dist/register-expo-view.js.map +1 -1
- package/dist/screen-capture.js +2 -4
- package/dist/screen-capture.js.map +1 -1
- package/dist/secure-store.d.ts +15 -0
- package/dist/secure-store.js +30 -0
- package/dist/secure-store.js.map +1 -0
- package/dist/store.d.ts +5 -10
- package/dist/store.js +5 -36
- package/dist/store.js.map +1 -1
- package/dist/tracking.d.ts +1 -1
- package/dist/tracking.js +3 -5
- package/dist/tracking.js.map +1 -1
- package/dist/video.d.ts +5 -0
- package/dist/video.js +21 -0
- package/dist/video.js.map +1 -0
- package/dist/watch.d.ts +103 -0
- package/dist/watch.js +228 -0
- package/dist/watch.js.map +1 -0
- package/dist/widget.d.ts +63 -0
- package/dist/widget.js +98 -0
- package/dist/widget.js.map +1 -0
- package/package.json +53 -4
package/dist/database.d.ts
CHANGED
|
@@ -11,11 +11,20 @@ export interface Migration {
|
|
|
11
11
|
readonly to: number;
|
|
12
12
|
readonly up: (database: NativeDatabase) => Promise<void>;
|
|
13
13
|
}
|
|
14
|
+
export interface DatabaseOptions<T extends NativeDatabase = NativeDatabase> {
|
|
15
|
+
/**
|
|
16
|
+
* Run on every open, before the migrations and outside any transaction: where the settings of a
|
|
17
|
+
* connection go. `PRAGMA foreign_keys = ON` belongs here and nowhere else, since SQLite ignores
|
|
18
|
+
* it inside a transaction, which a migration is, and forgets it when the connection closes.
|
|
19
|
+
*/
|
|
20
|
+
readonly onOpen?: (database: T) => Promise<void> | void;
|
|
21
|
+
}
|
|
14
22
|
export declare class Database<T extends NativeDatabase> {
|
|
15
23
|
private readonly open;
|
|
16
24
|
private readonly migrations;
|
|
25
|
+
private readonly onOpen;
|
|
17
26
|
private opening;
|
|
18
|
-
constructor(open: () => Promise<T>, migrations?: readonly Migration[]);
|
|
27
|
+
constructor(open: () => Promise<T>, migrations?: readonly Migration[], options?: DatabaseOptions<T>);
|
|
19
28
|
/**
|
|
20
29
|
* The database, opened and migrated.
|
|
21
30
|
*
|
|
@@ -31,7 +40,7 @@ export declare class Database<T extends NativeDatabase> {
|
|
|
31
40
|
* is nothing left to release.
|
|
32
41
|
*/
|
|
33
42
|
close(): Promise<void>;
|
|
34
|
-
/** Open and migrate, closing
|
|
43
|
+
/** Open, set the connection up and migrate, closing it again if either fails. */
|
|
35
44
|
private start;
|
|
36
45
|
/**
|
|
37
46
|
* Bring the schema up to date, in order, each step in its own transaction.
|
|
@@ -43,5 +52,11 @@ export declare class Database<T extends NativeDatabase> {
|
|
|
43
52
|
*/
|
|
44
53
|
private migrate;
|
|
45
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Open every `database()` with `open` in place of `expo-sqlite`, until the function this answers
|
|
57
|
+
* is called. For a test, where no native module can load: `open` gives a stand-in, such as
|
|
58
|
+
* `memoryDatabase()` from `@ng-native/testing`, and the migrations and `onOpen` run against it.
|
|
59
|
+
*/
|
|
60
|
+
export declare function openDatabasesWith(open: (name: string) => unknown): () => void;
|
|
46
61
|
/** A database, opened on first use and migrated before the first query sees it. */
|
|
47
|
-
export declare function database(name: string, migrations?: readonly Migration[]): Database<import("expo-sqlite").SQLiteDatabase>;
|
|
62
|
+
export declare function database(name: string, migrations?: readonly Migration[], options?: DatabaseOptions<import('expo-sqlite').SQLiteDatabase>): Database<import("expo-sqlite").SQLiteDatabase>;
|
package/dist/database.js
CHANGED
|
@@ -18,14 +18,16 @@
|
|
|
18
18
|
* and an app usually has exactly one. Opening is deferred until something asks: a database opened
|
|
19
19
|
* at startup is a file handle and a WAL journal for an app that may never read from it.
|
|
20
20
|
*/
|
|
21
|
-
import { expoModule } from "./native.js";
|
|
21
|
+
import { expoModule, unavailable } from "./native.js";
|
|
22
22
|
export class Database {
|
|
23
23
|
open;
|
|
24
24
|
migrations;
|
|
25
|
+
onOpen;
|
|
25
26
|
opening = null;
|
|
26
|
-
constructor(open, migrations = []) {
|
|
27
|
+
constructor(open, migrations = [], options = {}) {
|
|
27
28
|
this.open = open;
|
|
28
29
|
this.migrations = [...migrations].sort((a, b) => a.to - b.to);
|
|
30
|
+
this.onOpen = options.onOpen;
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
33
|
* The database, opened and migrated.
|
|
@@ -59,10 +61,11 @@ export class Database {
|
|
|
59
61
|
const database = await opening?.catch(() => null);
|
|
60
62
|
await database?.closeAsync();
|
|
61
63
|
}
|
|
62
|
-
/** Open and migrate, closing
|
|
64
|
+
/** Open, set the connection up and migrate, closing it again if either fails. */
|
|
63
65
|
async start() {
|
|
64
66
|
const database = await this.open();
|
|
65
67
|
try {
|
|
68
|
+
await this.onOpen?.(database);
|
|
66
69
|
await this.migrate(database);
|
|
67
70
|
}
|
|
68
71
|
catch (error) {
|
|
@@ -96,13 +99,29 @@ export class Database {
|
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
}
|
|
102
|
+
let opener = null;
|
|
103
|
+
/**
|
|
104
|
+
* Open every `database()` with `open` in place of `expo-sqlite`, until the function this answers
|
|
105
|
+
* is called. For a test, where no native module can load: `open` gives a stand-in, such as
|
|
106
|
+
* `memoryDatabase()` from `@ng-native/testing`, and the migrations and `onOpen` run against it.
|
|
107
|
+
*/
|
|
108
|
+
export function openDatabasesWith(open) {
|
|
109
|
+
const before = opener;
|
|
110
|
+
opener = open;
|
|
111
|
+
return () => {
|
|
112
|
+
opener = before;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
99
115
|
/** A database, opened on first use and migrated before the first query sees it. */
|
|
100
|
-
export function database(name, migrations = []) {
|
|
116
|
+
export function database(name, migrations = [], options = {}) {
|
|
101
117
|
return new Database(async () => {
|
|
118
|
+
if (opener)
|
|
119
|
+
return opener(name);
|
|
102
120
|
const expo = expoModule('expo-sqlite', () => require('expo-sqlite'), ['ios', 'android', 'web']);
|
|
103
|
-
if (!expo)
|
|
104
|
-
throw
|
|
121
|
+
if (!expo) {
|
|
122
|
+
throw unavailable('expo-sqlite', 'Point database() at a stand-in with openDatabasesWith(), such as memoryDatabase() from @ng-native/testing.');
|
|
123
|
+
}
|
|
105
124
|
return expo.openDatabaseAsync(name);
|
|
106
|
-
}, migrations);
|
|
125
|
+
}, migrations, options);
|
|
107
126
|
}
|
|
108
127
|
//# sourceMappingURL=database.js.map
|
package/dist/database.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.js","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA0BtD,MAAM,OAAO,QAAQ;IACF,IAAI,CAAmB;IACvB,UAAU,CAAuB;IACjC,MAAM,CAA+B;IAC9C,OAAO,GAAsB,IAAI,CAAC;IAE1C,YACE,IAAsB,EACtB,aAAmC,EAAE,EACrC,UAA8B,EAAE;QAEhC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,UAAU,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;QAC9D,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC/B,CAAC;IAED;;;;;;OAMG;IACH,KAAK;QACH,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC;QACtC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,6FAA6F;QAC7F,2EAA2E;QAC3E,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE;YACjB,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO;gBAAE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpD,CAAC,CAAC,CAAC;QACH,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,KAAK;QACT,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,MAAM,QAAQ,GAAG,MAAM,OAAO,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QAClD,MAAM,QAAQ,EAAE,UAAU,EAAE,CAAC;IAC/B,CAAC;IAED,iFAAiF;IACzE,KAAK,CAAC,KAAK;QACjB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,CAAC;YAC9B,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC/B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,4FAA4F;YAC5F,MAAM,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAC5C,MAAM,KAAK,CAAC;QACd,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED;;;;;;;OAOG;IACK,KAAK,CAAC,OAAO,CAAC,QAAW;QAC/B,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM;YAAE,OAAO;QAEpC,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,aAAa,CAA2B,qBAAqB,CAAC,CAAC;QAC1F,IAAI,OAAO,GAAG,GAAG,EAAE,YAAY,IAAI,CAAC,CAAC;QAErC,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACxC,IAAI,SAAS,CAAC,EAAE,IAAI,OAAO;gBAAE,SAAS;YACtC,MAAM,QAAQ,CAAC,oBAAoB,CAAC,KAAK,IAAI,EAAE;gBAC7C,MAAM,SAAS,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;gBAC7B,MAAM,QAAQ,CAAC,SAAS,CAAC,yBAAyB,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC;YACpE,CAAC,CAAC,CAAC;YACH,OAAO,GAAG,SAAS,CAAC,EAAE,CAAC;QACzB,CAAC;IACH,CAAC;CACF;AAED,IAAI,MAAM,GAAuC,IAAI,CAAC;AAEtD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAA+B;IAC/D,MAAM,MAAM,GAAG,MAAM,CAAC;IACtB,MAAM,GAAG,IAAI,CAAC;IACd,OAAO,GAAG,EAAE;QACV,MAAM,GAAG,MAAM,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,QAAQ,CACtB,IAAY,EACZ,aAAmC,EAAE,EACrC,UAAiE,EAAE;IAEnE,OAAO,IAAI,QAAQ,CACjB,KAAK,IAAI,EAAE;QACT,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC,IAAI,CAAyC,CAAC;QACxE,MAAM,IAAI,GAAG,UAAU,CACrB,aAAa,EACb,GAAG,EAAE,CAAC,OAAO,CAAC,aAAa,CAAiC,EAC5D,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,CAC1B,CAAC;QACF,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,WAAW,CACf,aAAa,EACb,4GAA4G,CAC7G,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC,EACD,UAAU,EACV,OAAO,CACR,CAAC;AACJ,CAAC","sourcesContent":["/**\n * `database()`, bound to `expo-sqlite`.\n *\n * `SQLiteProvider` and `useSQLiteContext` are the documented way in and both are React: a context\n * that opens the database, runs migrations, and closes it when the provider unmounts. The database\n * itself is a plain object with `getAllAsync` and the rest already on it, so what is rebuilt is\n * the *lifecycle* - opened once, migrated before anything queries, closed on request:\n *\n * ```ts\n * export const notes = database('notes.db', [\n * { to: 1, up: (db) => db.execAsync('CREATE TABLE note (id INTEGER PRIMARY KEY, body TEXT)') },\n * ]);\n *\n * const rows = await (await notes.ready()).getAllAsync<Note>('SELECT * FROM note');\n * ```\n *\n * A value rather than a service, because a schema belongs to a feature rather than to an injector,\n * and an app usually has exactly one. Opening is deferred until something asks: a database opened\n * at startup is a file handle and a WAL journal for an app that may never read from it.\n */\nimport { expoModule, unavailable } from './native.ts';\n\n/** Enough of `SQLiteDatabase` to own it. Queries go through the database itself. */\nexport interface NativeDatabase {\n execAsync(source: string): Promise<void>;\n getFirstAsync<T>(source: string, ...params: unknown[]): Promise<T | null>;\n closeAsync(): Promise<void>;\n withTransactionAsync(task: () => Promise<void>): Promise<void>;\n}\n\n/** One step from one schema version to the next. Run in order, each inside a transaction. */\nexport interface Migration {\n /** The version this migration produces. Must be greater than every one before it. */\n readonly to: number;\n readonly up: (database: NativeDatabase) => Promise<void>;\n}\n\nexport interface DatabaseOptions<T extends NativeDatabase = NativeDatabase> {\n /**\n * Run on every open, before the migrations and outside any transaction: where the settings of a\n * connection go. `PRAGMA foreign_keys = ON` belongs here and nowhere else, since SQLite ignores\n * it inside a transaction, which a migration is, and forgets it when the connection closes.\n */\n readonly onOpen?: (database: T) => Promise<void> | void;\n}\n\nexport class Database<T extends NativeDatabase> {\n private readonly open: () => Promise<T>;\n private readonly migrations: readonly Migration[];\n private readonly onOpen: DatabaseOptions<T>['onOpen'];\n private opening: Promise<T> | null = null;\n\n constructor(\n open: () => Promise<T>,\n migrations: readonly Migration[] = [],\n options: DatabaseOptions<T> = {},\n ) {\n this.open = open;\n this.migrations = [...migrations].sort((a, b) => a.to - b.to);\n this.onOpen = options.onOpen;\n }\n\n /**\n * The database, opened and migrated.\n *\n * Awaited at every call site rather than held, because the first caller is the one that pays for\n * opening it and every caller after that is handed the same promise - including the ones that\n * arrive while it is still opening, which is what stops two connections being made at once.\n */\n ready(): Promise<T> {\n if (this.opening) return this.opening;\n const opening = this.start();\n this.opening = opening;\n // A failed open is forgotten once everyone waiting on it has heard, so the next caller tries\n // again rather than being handed the same failure for the life of the app.\n opening.catch(() => {\n if (this.opening === opening) this.opening = null;\n });\n return opening;\n }\n\n /**\n * Close it. The next caller reopens; this is for a sign-out that has to leave nothing behind.\n *\n * Resolves even when the open failed: the connection was already closed by the failure, so there\n * is nothing left to release.\n */\n async close(): Promise<void> {\n const opening = this.opening;\n this.opening = null;\n const database = await opening?.catch(() => null);\n await database?.closeAsync();\n }\n\n /** Open, set the connection up and migrate, closing it again if either fails. */\n private async start(): Promise<T> {\n const database = await this.open();\n try {\n await this.onOpen?.(database);\n await this.migrate(database);\n } catch (error) {\n // The migration's own error is the one worth reporting; a failure to close after it is not.\n await database.closeAsync().catch(() => {});\n throw error;\n }\n return database;\n }\n\n /**\n * Bring the schema up to date, in order, each step in its own transaction.\n *\n * `user_version` is SQLite's own integer for this, so the record of where a database has got to\n * lives in the database rather than in a table this has to create first. It is transactional,\n * so it is set inside the migration's own transaction: the schema change and the record of it\n * commit together or roll back together, and a migration can never be half done and recorded.\n */\n private async migrate(database: T): Promise<void> {\n if (!this.migrations.length) return;\n\n const row = await database.getFirstAsync<{ user_version: number }>('PRAGMA user_version');\n let version = row?.user_version ?? 0;\n\n for (const migration of this.migrations) {\n if (migration.to <= version) continue;\n await database.withTransactionAsync(async () => {\n await migration.up(database);\n await database.execAsync(`PRAGMA user_version = ${migration.to}`);\n });\n version = migration.to;\n }\n }\n}\n\nlet opener: ((name: string) => unknown) | null = null;\n\n/**\n * Open every `database()` with `open` in place of `expo-sqlite`, until the function this answers\n * is called. For a test, where no native module can load: `open` gives a stand-in, such as\n * `memoryDatabase()` from `@ng-native/testing`, and the migrations and `onOpen` run against it.\n */\nexport function openDatabasesWith(open: (name: string) => unknown): () => void {\n const before = opener;\n opener = open;\n return () => {\n opener = before;\n };\n}\n\n/** A database, opened on first use and migrated before the first query sees it. */\nexport function database(\n name: string,\n migrations: readonly Migration[] = [],\n options: DatabaseOptions<import('expo-sqlite').SQLiteDatabase> = {},\n) {\n return new Database<import('expo-sqlite').SQLiteDatabase>(\n async () => {\n if (opener) return opener(name) as import('expo-sqlite').SQLiteDatabase;\n const expo = expoModule(\n 'expo-sqlite',\n () => require('expo-sqlite') as typeof import('expo-sqlite'),\n ['ios', 'android', 'web'],\n );\n if (!expo) {\n throw unavailable(\n 'expo-sqlite',\n 'Point database() at a stand-in with openDatabasesWith(), such as memoryDatabase() from @ng-native/testing.',\n );\n }\n return expo.openDatabaseAsync(name);\n },\n migrations,\n options,\n );\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { NativeSyntheticEvent } from '@ng-native/fabric';
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
/** What the picker sends when the user picks: the moment in milliseconds, and its UTC offset. */
|
|
4
|
+
export type DateTimePickerChangeEvent = NativeSyntheticEvent<{
|
|
5
|
+
readonly timestamp: number;
|
|
6
|
+
readonly utcOffset: number;
|
|
7
|
+
}>;
|
|
8
|
+
export declare class DateTimePicker {
|
|
9
|
+
/** The moment shown, as a `Date` or in milliseconds. */
|
|
10
|
+
readonly date: import("@angular/core").InputSignalWithTransform<number | undefined, number | Date | null | undefined>;
|
|
11
|
+
readonly mode: import("@angular/core").InputSignal<"date" | "time" | "datetime" | "countdown" | undefined>;
|
|
12
|
+
/** How the picker is drawn: wheels, a compact field that opens a popover, or a calendar. */
|
|
13
|
+
readonly displayIOS: import("@angular/core").InputSignal<"default" | "spinner" | "compact" | "inline" | undefined>;
|
|
14
|
+
readonly minimumDate: import("@angular/core").InputSignalWithTransform<number | undefined, number | Date | null | undefined>;
|
|
15
|
+
readonly maximumDate: import("@angular/core").InputSignalWithTransform<number | undefined, number | Date | null | undefined>;
|
|
16
|
+
/** The step between the minutes offered. One that divides sixty. */
|
|
17
|
+
readonly minuteInterval: import("@angular/core").InputSignalWithTransform<number | undefined, unknown>;
|
|
18
|
+
/** A locale identifier, `en_GB`, for the picker's own text and order. The device's by default. */
|
|
19
|
+
readonly locale: import("@angular/core").InputSignal<string | undefined>;
|
|
20
|
+
/** An IANA time zone, `Europe/London`, the picker shows the moment in. */
|
|
21
|
+
readonly timeZoneName: import("@angular/core").InputSignal<string | undefined>;
|
|
22
|
+
readonly timeZoneOffsetInMinutes: import("@angular/core").InputSignalWithTransform<number | undefined, unknown>;
|
|
23
|
+
readonly accentColor: import("@angular/core").InputSignal<string | undefined>;
|
|
24
|
+
/** The wheels' text colour, for `displayIOS="spinner"`. */
|
|
25
|
+
readonly textColor: import("@angular/core").InputSignal<string | undefined>;
|
|
26
|
+
readonly themeVariant: import("@angular/core").InputSignal<"dark" | "light" | "unspecified" | undefined>;
|
|
27
|
+
readonly enabled: import("@angular/core").InputSignalWithTransform<boolean | undefined, unknown>;
|
|
28
|
+
readonly change: import("@angular/core").OutputEmitterRef<DateTimePickerChangeEvent>;
|
|
29
|
+
/** The compact picker's popover closed. */
|
|
30
|
+
readonly pickerDismiss: import("@angular/core").OutputEmitterRef<NativeSyntheticEvent<null>>;
|
|
31
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<DateTimePicker, never>;
|
|
32
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<DateTimePicker, "date-time-picker", never, { "date": { "alias": "date"; "required": false; "isSignal": true; }; "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "displayIOS": { "alias": "displayIOS"; "required": false; "isSignal": true; }; "minimumDate": { "alias": "minimumDate"; "required": false; "isSignal": true; }; "maximumDate": { "alias": "maximumDate"; "required": false; "isSignal": true; }; "minuteInterval": { "alias": "minuteInterval"; "required": false; "isSignal": true; }; "locale": { "alias": "locale"; "required": false; "isSignal": true; }; "timeZoneName": { "alias": "timeZoneName"; "required": false; "isSignal": true; }; "timeZoneOffsetInMinutes": { "alias": "timeZoneOffsetInMinutes"; "required": false; "isSignal": true; }; "accentColor": { "alias": "accentColor"; "required": false; "isSignal": true; }; "textColor": { "alias": "textColor"; "required": false; "isSignal": true; }; "themeVariant": { "alias": "themeVariant"; "required": false; "isSignal": true; }; "enabled": { "alias": "enabled"; "required": false; "isSignal": true; }; }, { "change": "change"; "pickerDismiss": "pickerDismiss"; }, never, never, true, never>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<date-time-picker>` with its props and its `change` event typed.
|
|
3
|
+
*
|
|
4
|
+
* The element is `NATIVE_VIEWS`' name for `@react-native-community/datetimepicker`, and it is the
|
|
5
|
+
* library's native view rather than its React component, so what it takes is the view's own: the
|
|
6
|
+
* date is `date` in milliseconds where the component's is `value` as a `Date`, the style is
|
|
7
|
+
* `displayIOS`, and a pick is a `change` event carrying a `timestamp`. This claims the element so
|
|
8
|
+
* strict templates know that, and takes a `Date` wherever the view takes milliseconds.
|
|
9
|
+
*
|
|
10
|
+
* iOS only. On Android the library has no view, only a module that opens a dialog, and the element
|
|
11
|
+
* commits as nothing.
|
|
12
|
+
*/
|
|
13
|
+
import { Component, input, output } from '@angular/core';
|
|
14
|
+
import { optionalBoolean, optionalNumber } from "./transforms.js";
|
|
15
|
+
import * as i0 from "@angular/core";
|
|
16
|
+
/** A moment as the view takes it, in milliseconds, from a `Date` or from milliseconds. */
|
|
17
|
+
function milliseconds(value) {
|
|
18
|
+
if (value == null)
|
|
19
|
+
return undefined;
|
|
20
|
+
return typeof value === 'number' ? value : value.getTime();
|
|
21
|
+
}
|
|
22
|
+
export class DateTimePicker {
|
|
23
|
+
/** The moment shown, as a `Date` or in milliseconds. */
|
|
24
|
+
date = input(undefined, { ...(ngDevMode ? { debugName: "date" } : /* istanbul ignore next */ {}), transform: milliseconds });
|
|
25
|
+
mode = input(/* @ts-ignore */
|
|
26
|
+
...(ngDevMode ? [undefined, { debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
27
|
+
/** How the picker is drawn: wheels, a compact field that opens a popover, or a calendar. */
|
|
28
|
+
displayIOS = input(/* @ts-ignore */
|
|
29
|
+
...(ngDevMode ? [undefined, { debugName: "displayIOS" }] : /* istanbul ignore next */ []));
|
|
30
|
+
minimumDate = input(undefined, { ...(ngDevMode ? { debugName: "minimumDate" } : /* istanbul ignore next */ {}), transform: milliseconds });
|
|
31
|
+
maximumDate = input(undefined, { ...(ngDevMode ? { debugName: "maximumDate" } : /* istanbul ignore next */ {}), transform: milliseconds });
|
|
32
|
+
/** The step between the minutes offered. One that divides sixty. */
|
|
33
|
+
minuteInterval = input(undefined, { ...(ngDevMode ? { debugName: "minuteInterval" } : /* istanbul ignore next */ {}), transform: optionalNumber });
|
|
34
|
+
/** A locale identifier, `en_GB`, for the picker's own text and order. The device's by default. */
|
|
35
|
+
locale = input(/* @ts-ignore */
|
|
36
|
+
...(ngDevMode ? [undefined, { debugName: "locale" }] : /* istanbul ignore next */ []));
|
|
37
|
+
/** An IANA time zone, `Europe/London`, the picker shows the moment in. */
|
|
38
|
+
timeZoneName = input(/* @ts-ignore */
|
|
39
|
+
...(ngDevMode ? [undefined, { debugName: "timeZoneName" }] : /* istanbul ignore next */ []));
|
|
40
|
+
timeZoneOffsetInMinutes = input(undefined, { ...(ngDevMode ? { debugName: "timeZoneOffsetInMinutes" } : /* istanbul ignore next */ {}), transform: optionalNumber });
|
|
41
|
+
accentColor = input(/* @ts-ignore */
|
|
42
|
+
...(ngDevMode ? [undefined, { debugName: "accentColor" }] : /* istanbul ignore next */ []));
|
|
43
|
+
/** The wheels' text colour, for `displayIOS="spinner"`. */
|
|
44
|
+
textColor = input(/* @ts-ignore */
|
|
45
|
+
...(ngDevMode ? [undefined, { debugName: "textColor" }] : /* istanbul ignore next */ []));
|
|
46
|
+
themeVariant = input(/* @ts-ignore */
|
|
47
|
+
...(ngDevMode ? [undefined, { debugName: "themeVariant" }] : /* istanbul ignore next */ []));
|
|
48
|
+
enabled = input(undefined, { ...(ngDevMode ? { debugName: "enabled" } : /* istanbul ignore next */ {}), transform: optionalBoolean });
|
|
49
|
+
change = output();
|
|
50
|
+
/** The compact picker's popover closed. */
|
|
51
|
+
pickerDismiss = output();
|
|
52
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: DateTimePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
53
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.2.0", type: DateTimePicker, isStandalone: true, selector: "date-time-picker", inputs: { date: { classPropertyName: "date", publicName: "date", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, displayIOS: { classPropertyName: "displayIOS", publicName: "displayIOS", isSignal: true, isRequired: false, transformFunction: null }, minimumDate: { classPropertyName: "minimumDate", publicName: "minimumDate", isSignal: true, isRequired: false, transformFunction: null }, maximumDate: { classPropertyName: "maximumDate", publicName: "maximumDate", isSignal: true, isRequired: false, transformFunction: null }, minuteInterval: { classPropertyName: "minuteInterval", publicName: "minuteInterval", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, timeZoneName: { classPropertyName: "timeZoneName", publicName: "timeZoneName", isSignal: true, isRequired: false, transformFunction: null }, timeZoneOffsetInMinutes: { classPropertyName: "timeZoneOffsetInMinutes", publicName: "timeZoneOffsetInMinutes", isSignal: true, isRequired: false, transformFunction: null }, accentColor: { classPropertyName: "accentColor", publicName: "accentColor", isSignal: true, isRequired: false, transformFunction: null }, textColor: { classPropertyName: "textColor", publicName: "textColor", isSignal: true, isRequired: false, transformFunction: null }, themeVariant: { classPropertyName: "themeVariant", publicName: "themeVariant", isSignal: true, isRequired: false, transformFunction: null }, enabled: { classPropertyName: "enabled", publicName: "enabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { change: "change", pickerDismiss: "pickerDismiss" }, host: { properties: { "date": "date()", "mode": "mode()", "displayIOS": "displayIOS()", "minimumDate": "minimumDate()", "maximumDate": "maximumDate()", "minuteInterval": "minuteInterval()", "locale": "locale()", "timeZoneName": "timeZoneName()", "timeZoneOffsetInMinutes": "timeZoneOffsetInMinutes()", "accentColor": "accentColor()", "textColor": "textColor()", "themeVariant": "themeVariant()", "enabled": "enabled()" } }, ngImport: i0, template: '', isInline: true });
|
|
54
|
+
}
|
|
55
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: DateTimePicker, decorators: [{
|
|
56
|
+
type: Component,
|
|
57
|
+
args: [{
|
|
58
|
+
selector: 'date-time-picker',
|
|
59
|
+
template: '',
|
|
60
|
+
host: {
|
|
61
|
+
'[date]': 'date()',
|
|
62
|
+
'[mode]': 'mode()',
|
|
63
|
+
'[displayIOS]': 'displayIOS()',
|
|
64
|
+
'[minimumDate]': 'minimumDate()',
|
|
65
|
+
'[maximumDate]': 'maximumDate()',
|
|
66
|
+
'[minuteInterval]': 'minuteInterval()',
|
|
67
|
+
'[locale]': 'locale()',
|
|
68
|
+
'[timeZoneName]': 'timeZoneName()',
|
|
69
|
+
'[timeZoneOffsetInMinutes]': 'timeZoneOffsetInMinutes()',
|
|
70
|
+
'[accentColor]': 'accentColor()',
|
|
71
|
+
'[textColor]': 'textColor()',
|
|
72
|
+
'[themeVariant]': 'themeVariant()',
|
|
73
|
+
'[enabled]': 'enabled()',
|
|
74
|
+
},
|
|
75
|
+
}]
|
|
76
|
+
}], propDecorators: { date: [{ type: i0.Input, args: [{ isSignal: true, alias: "date", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], displayIOS: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayIOS", required: false }] }], minimumDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minimumDate", required: false }] }], maximumDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maximumDate", required: false }] }], minuteInterval: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteInterval", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], timeZoneName: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeZoneName", required: false }] }], timeZoneOffsetInMinutes: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeZoneOffsetInMinutes", required: false }] }], accentColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "accentColor", required: false }] }], textColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "textColor", required: false }] }], themeVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "themeVariant", required: false }] }], enabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "enabled", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], pickerDismiss: [{ type: i0.Output, args: ["pickerDismiss"] }] } });
|
|
77
|
+
//# sourceMappingURL=date-time-picker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date-time-picker.js","sourceRoot":"","sources":["../src/date-time-picker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEzD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;;AAQlE,0FAA0F;AAC1F,SAAS,YAAY,CAAC,KAAuC;IAC3D,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC;IACpC,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;AAC7D,CAAC;AAqBD,MAAM,OAAO,cAAc;IACzB,wDAAwD;IAC/C,IAAI,GAAG,KAAK,CAAC,SAAS,4EAAI,SAAS,EAAE,YAAY,GAAG,CAAC;IACrD,IAAI,GAAG,KAAK;wFAA8C,CAAC;IACpE,4FAA4F;IACnF,UAAU,GAAG,KAAK;8FAAgD,CAAC;IACnE,WAAW,GAAG,KAAK,CAAC,SAAS,mFAAI,SAAS,EAAE,YAAY,GAAG,CAAC;IAC5D,WAAW,GAAG,KAAK,CAAC,SAAS,mFAAI,SAAS,EAAE,YAAY,GAAG,CAAC;IACrE,oEAAoE;IAC3D,cAAc,GAAG,KAAK,CAAS,SAAS,sFAAI,SAAS,EAAE,cAAc,GAAG,CAAC;IAClF,kGAAkG;IACzF,MAAM,GAAG,KAAK;0FAAU,CAAC;IAClC,0EAA0E;IACjE,YAAY,GAAG,KAAK;gGAAU,CAAC;IAC/B,uBAAuB,GAAG,KAAK,CAAS,SAAS,+FAAI,SAAS,EAAE,cAAc,GAAG,CAAC;IAClF,WAAW,GAAG,KAAK;+FAAU,CAAC;IACvC,2DAA2D;IAClD,SAAS,GAAG,KAAK;6FAAU,CAAC;IAC5B,YAAY,GAAG,KAAK;gGAAoC,CAAC;IACzD,OAAO,GAAG,KAAK,CAAU,SAAS,+EAAI,SAAS,EAAE,eAAe,GAAG,CAAC;IACpE,MAAM,GAAG,MAAM,EAA6B,CAAC;IACtD,2CAA2C;IAClC,aAAa,GAAG,MAAM,EAA8B,CAAC;uGAtBnD,cAAc;2FAAd,cAAc,syEAjBf,EAAE;;2FAiBD,cAAc;kBAnB1B,SAAS;mBAAC;oBACT,QAAQ,EAAE,kBAAkB;oBAC5B,QAAQ,EAAE,EAAE;oBACZ,IAAI,EAAE;wBACJ,QAAQ,EAAE,QAAQ;wBAClB,QAAQ,EAAE,QAAQ;wBAClB,cAAc,EAAE,cAAc;wBAC9B,eAAe,EAAE,eAAe;wBAChC,eAAe,EAAE,eAAe;wBAChC,kBAAkB,EAAE,kBAAkB;wBACtC,UAAU,EAAE,UAAU;wBACtB,gBAAgB,EAAE,gBAAgB;wBAClC,2BAA2B,EAAE,2BAA2B;wBACxD,eAAe,EAAE,eAAe;wBAChC,aAAa,EAAE,aAAa;wBAC5B,gBAAgB,EAAE,gBAAgB;wBAClC,WAAW,EAAE,WAAW;qBACzB;iBACF","sourcesContent":["/**\n * `<date-time-picker>` with its props and its `change` event typed.\n *\n * The element is `NATIVE_VIEWS`' name for `@react-native-community/datetimepicker`, and it is the\n * library's native view rather than its React component, so what it takes is the view's own: the\n * date is `date` in milliseconds where the component's is `value` as a `Date`, the style is\n * `displayIOS`, and a pick is a `change` event carrying a `timestamp`. This claims the element so\n * strict templates know that, and takes a `Date` wherever the view takes milliseconds.\n *\n * iOS only. On Android the library has no view, only a module that opens a dialog, and the element\n * commits as nothing.\n */\nimport { Component, input, output } from '@angular/core';\nimport type { NativeSyntheticEvent } from '@ng-native/fabric';\nimport { optionalBoolean, optionalNumber } from './transforms.ts';\n\n/** What the picker sends when the user picks: the moment in milliseconds, and its UTC offset. */\nexport type DateTimePickerChangeEvent = NativeSyntheticEvent<{\n readonly timestamp: number;\n readonly utcOffset: number;\n}>;\n\n/** A moment as the view takes it, in milliseconds, from a `Date` or from milliseconds. */\nfunction milliseconds(value: Date | number | null | undefined): number | undefined {\n if (value == null) return undefined;\n return typeof value === 'number' ? value : value.getTime();\n}\n\n@Component({\n selector: 'date-time-picker',\n template: '',\n host: {\n '[date]': 'date()',\n '[mode]': 'mode()',\n '[displayIOS]': 'displayIOS()',\n '[minimumDate]': 'minimumDate()',\n '[maximumDate]': 'maximumDate()',\n '[minuteInterval]': 'minuteInterval()',\n '[locale]': 'locale()',\n '[timeZoneName]': 'timeZoneName()',\n '[timeZoneOffsetInMinutes]': 'timeZoneOffsetInMinutes()',\n '[accentColor]': 'accentColor()',\n '[textColor]': 'textColor()',\n '[themeVariant]': 'themeVariant()',\n '[enabled]': 'enabled()',\n },\n})\nexport class DateTimePicker {\n /** The moment shown, as a `Date` or in milliseconds. */\n readonly date = input(undefined, { transform: milliseconds });\n readonly mode = input<'date' | 'time' | 'datetime' | 'countdown'>();\n /** How the picker is drawn: wheels, a compact field that opens a popover, or a calendar. */\n readonly displayIOS = input<'default' | 'spinner' | 'compact' | 'inline'>();\n readonly minimumDate = input(undefined, { transform: milliseconds });\n readonly maximumDate = input(undefined, { transform: milliseconds });\n /** The step between the minutes offered. One that divides sixty. */\n readonly minuteInterval = input<number>(undefined, { transform: optionalNumber });\n /** A locale identifier, `en_GB`, for the picker's own text and order. The device's by default. */\n readonly locale = input<string>();\n /** An IANA time zone, `Europe/London`, the picker shows the moment in. */\n readonly timeZoneName = input<string>();\n readonly timeZoneOffsetInMinutes = input<number>(undefined, { transform: optionalNumber });\n readonly accentColor = input<string>();\n /** The wheels' text colour, for `displayIOS=\"spinner\"`. */\n readonly textColor = input<string>();\n readonly themeVariant = input<'dark' | 'light' | 'unspecified'>();\n readonly enabled = input<boolean>(undefined, { transform: optionalBoolean });\n readonly change = output<DateTimePickerChangeEvent>();\n /** The compact picker's popover closed. */\n readonly pickerDismiss = output<NativeSyntheticEvent<null>>();\n}\n"]}
|
package/dist/dom-component.d.ts
CHANGED
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
* - **`outputs`** map the component's outputs, by public name, to handlers. A name the component
|
|
31
31
|
* has no output for is an error naming the ones it does have, as `<ng-native-island>`'s is.
|
|
32
32
|
* - **Errors** the page throws reach the app's `ErrorHandler`.
|
|
33
|
+
* - **Only its own page is heard.** A message from any other page the web view shows, after a link
|
|
34
|
+
* in the content, say, is ignored, as is one that is not JSON. The inputs given before the page
|
|
35
|
+
* loads are the web view's to hand out, though, and it hands them to whatever page it is showing,
|
|
36
|
+
* so they are no place for a secret.
|
|
33
37
|
*/
|
|
34
38
|
import { InjectionToken } from '@angular/core';
|
|
35
39
|
import { type NativeSyntheticEvent } from '@ng-native/fabric';
|
|
@@ -79,6 +83,7 @@ export declare class DomComponent {
|
|
|
79
83
|
constructor();
|
|
80
84
|
protected receive(event: NativeSyntheticEvent<{
|
|
81
85
|
data: string;
|
|
86
|
+
url?: string;
|
|
82
87
|
}>): void;
|
|
83
88
|
private checkOutputs;
|
|
84
89
|
private send;
|
package/dist/dom-component.js
CHANGED
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
* - **`outputs`** map the component's outputs, by public name, to handlers. A name the component
|
|
31
31
|
* has no output for is an error naming the ones it does have, as `<ng-native-island>`'s is.
|
|
32
32
|
* - **Errors** the page throws reach the app's `ErrorHandler`.
|
|
33
|
+
* - **Only its own page is heard.** A message from any other page the web view shows, after a link
|
|
34
|
+
* in the content, say, is ignored, as is one that is not JSON. The inputs given before the page
|
|
35
|
+
* loads are the web view's to hand out, though, and it hands them to whatever page it is showing,
|
|
36
|
+
* so they are no place for a secret.
|
|
33
37
|
*/
|
|
34
38
|
import { Component, ElementRef, InjectionToken, computed, effect, inject, input, untracked, } from '@angular/core';
|
|
35
39
|
import { Engine } from '@ng-native/fabric';
|
|
@@ -92,14 +96,31 @@ export class DomComponent {
|
|
|
92
96
|
});
|
|
93
97
|
}
|
|
94
98
|
receive(event) {
|
|
95
|
-
const
|
|
99
|
+
const { data, url } = event.nativeEvent;
|
|
100
|
+
const source = this.source();
|
|
101
|
+
if (!source || !isOwnPage(url, source.uri)) {
|
|
102
|
+
if (this.debuggable) {
|
|
103
|
+
console.warn(`[angular-native] <dom-component> ${this.name()}: ignored a message from ` +
|
|
104
|
+
`${url || 'an unnamed page'}, which is not its own page.`);
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const message = parse(data);
|
|
109
|
+
if (!message) {
|
|
110
|
+
if (this.debuggable) {
|
|
111
|
+
console.warn(`[angular-native] <dom-component> ${this.name()}: dropped a message that is not JSON.`);
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
96
115
|
if (message.type === 'ready') {
|
|
97
116
|
this.checkOutputs(message.outputs);
|
|
98
117
|
this.ready = true;
|
|
99
118
|
this.send(this.inputs());
|
|
100
119
|
}
|
|
101
120
|
else if (message.type === 'output') {
|
|
102
|
-
this.outputs()
|
|
121
|
+
const outputs = this.outputs();
|
|
122
|
+
if (Object.hasOwn(outputs, message.name))
|
|
123
|
+
outputs[message.name](message.value);
|
|
103
124
|
}
|
|
104
125
|
else if (message.type === 'error') {
|
|
105
126
|
throw new Error(`[angular-native] <dom-component> ${this.name()}: ${message.message}`);
|
|
@@ -136,4 +157,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.2.0", ngImpor
|
|
|
136
157
|
},
|
|
137
158
|
}]
|
|
138
159
|
}], ctorParameters: () => [], propDecorators: { src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: true }] }], inputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputs", required: false }] }], outputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "outputs", required: false }] }] } });
|
|
160
|
+
/**
|
|
161
|
+
* Whether the page a message came from is the one the component loaded, so a page the web view
|
|
162
|
+
* was taken to (by a link in the content, say) cannot fire the app's outputs or ask for its inputs.
|
|
163
|
+
*
|
|
164
|
+
* The web view names the page it is showing, not the frame that posted, so this tells pages apart
|
|
165
|
+
* and not an iframe from the page that holds it. Every message is named on both platforms, from
|
|
166
|
+
* the web view's own URL; one with no URL cannot be placed, so it is not trusted either.
|
|
167
|
+
*
|
|
168
|
+
* - **Served by the dev server**, the page is trusted by origin: that server serves the app itself.
|
|
169
|
+
* - **Loaded from a file** in a release build or an update, the page is trusted by file name. The
|
|
170
|
+
* web view reports the file as the native side resolved it, which differs from the source URI
|
|
171
|
+
* before the name (on iOS the source is `www.bundle/...`, relative to the app), so the page must
|
|
172
|
+
* be a `file:` URL whose file name is the source's, a hash of the component's path.
|
|
173
|
+
*/
|
|
174
|
+
function isOwnPage(url, source) {
|
|
175
|
+
if (!url)
|
|
176
|
+
return false;
|
|
177
|
+
const served = /^https?:\/\/[^/?#]+/i.exec(source)?.[0];
|
|
178
|
+
if (served)
|
|
179
|
+
return url.toLowerCase().startsWith(`${served.toLowerCase()}/`);
|
|
180
|
+
return /^file:/i.test(url) && fileName(url) === fileName(source);
|
|
181
|
+
}
|
|
182
|
+
function fileName(url) {
|
|
183
|
+
return url.split(/[?#]/)[0].split('/').at(-1);
|
|
184
|
+
}
|
|
185
|
+
function parse(data) {
|
|
186
|
+
try {
|
|
187
|
+
const message = JSON.parse(data);
|
|
188
|
+
return typeof message === 'object' && message !== null ? message : undefined;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
139
194
|
//# sourceMappingURL=dom-component.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dom-component.js","sourceRoot":"","sources":["../src/dom-component.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `<dom-component>`: an Angular component rendered by a browser - DOM, CSS, anything a browser\n * runs - in a web view inside a native screen, with its inputs and outputs bound from the app's\n * template. Expo calls this a DOM component; it is not the Web Components standard.\n *\n * ```ts\n * // web/note.ts: an ordinary Angular component for the browser, in the app's own source\n * 'use dom';\n * @Component({ selector: 'app-note', template: '<textarea ...></textarea>', styles: [...] })\n * export class Note {\n * readonly label = input('');\n * readonly sent = output<string>();\n * }\n * export default mountInWebView(Note); // from '@ng-native/web/web-view'\n *\n * // a native screen\n * import note from './web/note.ts';\n * <dom-component [src]=\"note\" class=\"flex-1\"\n * [inputs]=\"{ label: title() }\" [outputs]=\"{ sent: save }\" />\n * ```\n *\n * The component runs in Expo's own web view (`@expo/dom-webview`, which `expo` already installs),\n * so it is a separate JavaScript runtime from the app and only JSON crosses. It is shipped the way\n * Expo ships its `'use dom'` components: the import is a reference to a page that Expo's dev server\n * serves in development and `expo export:embed` bundles into the app for a release build (see\n * `@ng-native/metro`'s `dom-component.cjs`), and the page is found where Expo finds its own.\n *\n * - **`inputs`** go into the page before it loads, and a changed value is sent to it afterwards\n * without reloading. Anything sent before the page is ready is sent again once it is.\n * - **`outputs`** map the component's outputs, by public name, to handlers. A name the component\n * has no output for is an error naming the ones it does have, as `<ng-native-island>`'s is.\n * - **Errors** the page throws reach the app's `ErrorHandler`.\n */\nimport {\n Component,\n ElementRef,\n InjectionToken,\n computed,\n effect,\n inject,\n input,\n untracked,\n} from '@angular/core';\nimport { Engine, type EngineNode, type NativeSyntheticEvent } from '@ng-native/fabric';\nimport { optional } from './native.ts';\nimport { registerExpoView } from './register-expo-view.ts';\n\n/** The element is this package's, so it needs no `registerExpoViews` call of its own. */\nregisterExpoView('dom-component', 'ExpoDomWebViewModule');\n\n/** A handler for one of the DOM component's outputs. */\nexport type DomComponentOutputHandler = (value: never) => void;\n\n/** The functions Expo's web view defines on its view, called with the view's tag as `this`. */\nexport interface WebViewFunctions {\n injectJavaScript(this: { nativeTag: number }, script: string): Promise<void>;\n}\n\n/** Where the pages are, and how to reach the view once it is on screen. */\nexport interface DomComponentSource {\n /** Where Expo keeps DOM components' pages: the dev server, `www.bundle`, or an update. */\n readonly baseUrl: string | null;\n readonly functions: WebViewFunctions | null;\n}\n\n/** What native code gets for `import note from './web/note.ts'`. */\nexport interface DomComponentReference {\n readonly domComponent: string;\n}\n\ntype PageMessage =\n | { type: 'ready'; outputs: string[] }\n | { type: 'output'; name: string; value: unknown }\n | { type: 'error'; message: string };\n\n@Component({\n selector: 'dom-component',\n template: '',\n host: {\n '[source]': 'source()',\n '[injectedJavaScriptObject]': 'page()',\n '[webviewDebuggingEnabled]': 'debuggable',\n '(message)': 'receive($event)',\n },\n})\nexport class DomComponent {\n /** Overridden in a test to load pages without a dev server. */\n static readonly SOURCE = new InjectionToken<DomComponentSource>(\n 'angular-native.domComponentSource',\n {\n factory: () => {\n // Expo's own resolution, so a page is found wherever Expo would look for a DOM\n // component's: the dev server, the app's `www.bundle`, or a downloaded update.\n const base = optional(() => require('expo/src/dom/base') as { getBaseURL(): string });\n const core = optional(\n () => require('expo-modules-core') as typeof import('expo-modules-core'),\n );\n const module = core?.requireOptionalNativeModule<{\n ViewPrototypes?: Record<string, WebViewFunctions>;\n }>('ExpoDomWebViewModule');\n return {\n baseUrl: optional(() => base?.getBaseURL() ?? null),\n functions: module?.ViewPrototypes?.['ExpoDomWebViewModule'] ?? null,\n };\n },\n },\n );\n\n private readonly native = inject(DomComponent.SOURCE);\n private readonly engine = inject(Engine);\n private readonly node = inject<ElementRef<EngineNode>>(ElementRef).nativeElement;\n private ready = false;\n\n /** The DOM component, as imported: `import note from './web/note.ts'`. */\n readonly src = input.required<DomComponentReference>();\n /** Its inputs, by public name. JSON only: this crosses into another runtime. */\n readonly inputs = input<Readonly<Record<string, unknown>>>({});\n /** Handlers for its outputs, by public name. */\n readonly outputs = input<Readonly<Record<string, DomComponentOutputHandler>>>({});\n\n protected readonly debuggable = typeof __DEV__ === 'undefined' || __DEV__;\n\n protected readonly source = computed(() => {\n const { domComponent } = this.src();\n return this.native.baseUrl === null ? null : { uri: `${this.native.baseUrl}/${domComponent}` };\n });\n\n /** The file's name for an error message: the dev page names it, a release page is a hash. */\n private readonly name = computed(() => this.src().domComponent.split('?')[0]!);\n\n /**\n * What the page reads before it loads. The inputs are read untracked: a change to this prop\n * reloads the page, so a changed input is sent to the running page instead.\n */\n protected readonly page = computed(() => {\n this.src();\n return JSON.stringify({ inputs: untracked(this.inputs) });\n });\n\n constructor() {\n effect(() => {\n const inputs = this.inputs();\n if (this.ready) untracked(() => this.send(inputs));\n });\n }\n\n protected receive(event: NativeSyntheticEvent<{ data: string }>): void {\n const message = JSON.parse(event.nativeEvent.data) as PageMessage;\n if (message.type === 'ready') {\n this.checkOutputs(message.outputs);\n this.ready = true;\n this.send(this.inputs());\n } else if (message.type === 'output') {\n this.outputs()[message.name]?.(message.value as never);\n } else if (message.type === 'error') {\n throw new Error(`[angular-native] <dom-component> ${this.name()}: ${message.message}`);\n }\n }\n\n private checkOutputs(declared: readonly string[]): void {\n for (const name of Object.keys(this.outputs())) {\n if (!declared.includes(name)) {\n throw new Error(\n `[angular-native] <dom-component>: ${this.name()} has no output '${name}' ` +\n `(its outputs: ${declared.join(', ') || 'none'}).`,\n );\n }\n }\n }\n\n private send(inputs: Readonly<Record<string, unknown>>): void {\n const nativeTag = this.engine.tagOf(this.node);\n if (!this.native.functions || nativeTag === null) return;\n const message = JSON.stringify({ type: 'inputs', inputs });\n void this.native.functions.injectJavaScript.call(\n { nativeTag },\n `window.__ngNative && window.__ngNative.receive(${message}); true;`,\n );\n }\n}\n\ndeclare const __DEV__: boolean | undefined;\n"]}
|
|
1
|
+
{"version":3,"file":"dom-component.js","sourceRoot":"","sources":["../src/dom-component.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `<dom-component>`: an Angular component rendered by a browser - DOM, CSS, anything a browser\n * runs - in a web view inside a native screen, with its inputs and outputs bound from the app's\n * template. Expo calls this a DOM component; it is not the Web Components standard.\n *\n * ```ts\n * // web/note.ts: an ordinary Angular component for the browser, in the app's own source\n * 'use dom';\n * @Component({ selector: 'app-note', template: '<textarea ...></textarea>', styles: [...] })\n * export class Note {\n * readonly label = input('');\n * readonly sent = output<string>();\n * }\n * export default mountInWebView(Note); // from '@ng-native/web/web-view'\n *\n * // a native screen\n * import note from './web/note.ts';\n * <dom-component [src]=\"note\" class=\"flex-1\"\n * [inputs]=\"{ label: title() }\" [outputs]=\"{ sent: save }\" />\n * ```\n *\n * The component runs in Expo's own web view (`@expo/dom-webview`, which `expo` already installs),\n * so it is a separate JavaScript runtime from the app and only JSON crosses. It is shipped the way\n * Expo ships its `'use dom'` components: the import is a reference to a page that Expo's dev server\n * serves in development and `expo export:embed` bundles into the app for a release build (see\n * `@ng-native/metro`'s `dom-component.cjs`), and the page is found where Expo finds its own.\n *\n * - **`inputs`** go into the page before it loads, and a changed value is sent to it afterwards\n * without reloading. Anything sent before the page is ready is sent again once it is.\n * - **`outputs`** map the component's outputs, by public name, to handlers. A name the component\n * has no output for is an error naming the ones it does have, as `<ng-native-island>`'s is.\n * - **Errors** the page throws reach the app's `ErrorHandler`.\n * - **Only its own page is heard.** A message from any other page the web view shows, after a link\n * in the content, say, is ignored, as is one that is not JSON. The inputs given before the page\n * loads are the web view's to hand out, though, and it hands them to whatever page it is showing,\n * so they are no place for a secret.\n */\nimport {\n Component,\n ElementRef,\n InjectionToken,\n computed,\n effect,\n inject,\n input,\n untracked,\n} from '@angular/core';\nimport { Engine, type EngineNode, type NativeSyntheticEvent } from '@ng-native/fabric';\nimport { optional } from './native.ts';\nimport { registerExpoView } from './register-expo-view.ts';\n\n/** The element is this package's, so it needs no `registerExpoViews` call of its own. */\nregisterExpoView('dom-component', 'ExpoDomWebViewModule');\n\n/** A handler for one of the DOM component's outputs. */\nexport type DomComponentOutputHandler = (value: never) => void;\n\n/** The functions Expo's web view defines on its view, called with the view's tag as `this`. */\nexport interface WebViewFunctions {\n injectJavaScript(this: { nativeTag: number }, script: string): Promise<void>;\n}\n\n/** Where the pages are, and how to reach the view once it is on screen. */\nexport interface DomComponentSource {\n /** Where Expo keeps DOM components' pages: the dev server, `www.bundle`, or an update. */\n readonly baseUrl: string | null;\n readonly functions: WebViewFunctions | null;\n}\n\n/** What native code gets for `import note from './web/note.ts'`. */\nexport interface DomComponentReference {\n readonly domComponent: string;\n}\n\ntype PageMessage =\n | { type: 'ready'; outputs: string[] }\n | { type: 'output'; name: string; value: unknown }\n | { type: 'error'; message: string };\n\n@Component({\n selector: 'dom-component',\n template: '',\n host: {\n '[source]': 'source()',\n '[injectedJavaScriptObject]': 'page()',\n '[webviewDebuggingEnabled]': 'debuggable',\n '(message)': 'receive($event)',\n },\n})\nexport class DomComponent {\n /** Overridden in a test to load pages without a dev server. */\n static readonly SOURCE = new InjectionToken<DomComponentSource>(\n 'angular-native.domComponentSource',\n {\n factory: () => {\n // Expo's own resolution, so a page is found wherever Expo would look for a DOM\n // component's: the dev server, the app's `www.bundle`, or a downloaded update.\n const base = optional(() => require('expo/src/dom/base') as { getBaseURL(): string });\n const core = optional(\n () => require('expo-modules-core') as typeof import('expo-modules-core'),\n );\n const module = core?.requireOptionalNativeModule<{\n ViewPrototypes?: Record<string, WebViewFunctions>;\n }>('ExpoDomWebViewModule');\n return {\n baseUrl: optional(() => base?.getBaseURL() ?? null),\n functions: module?.ViewPrototypes?.['ExpoDomWebViewModule'] ?? null,\n };\n },\n },\n );\n\n private readonly native = inject(DomComponent.SOURCE);\n private readonly engine = inject(Engine);\n private readonly node = inject<ElementRef<EngineNode>>(ElementRef).nativeElement;\n private ready = false;\n\n /** The DOM component, as imported: `import note from './web/note.ts'`. */\n readonly src = input.required<DomComponentReference>();\n /** Its inputs, by public name. JSON only: this crosses into another runtime. */\n readonly inputs = input<Readonly<Record<string, unknown>>>({});\n /** Handlers for its outputs, by public name. */\n readonly outputs = input<Readonly<Record<string, DomComponentOutputHandler>>>({});\n\n protected readonly debuggable = typeof __DEV__ === 'undefined' || __DEV__;\n\n protected readonly source = computed(() => {\n const { domComponent } = this.src();\n return this.native.baseUrl === null ? null : { uri: `${this.native.baseUrl}/${domComponent}` };\n });\n\n /** The file's name for an error message: the dev page names it, a release page is a hash. */\n private readonly name = computed(() => this.src().domComponent.split('?')[0]!);\n\n /**\n * What the page reads before it loads. The inputs are read untracked: a change to this prop\n * reloads the page, so a changed input is sent to the running page instead.\n */\n protected readonly page = computed(() => {\n this.src();\n return JSON.stringify({ inputs: untracked(this.inputs) });\n });\n\n constructor() {\n effect(() => {\n const inputs = this.inputs();\n if (this.ready) untracked(() => this.send(inputs));\n });\n }\n\n protected receive(event: NativeSyntheticEvent<{ data: string; url?: string }>): void {\n const { data, url } = event.nativeEvent;\n const source = this.source();\n if (!source || !isOwnPage(url, source.uri)) {\n if (this.debuggable) {\n console.warn(\n `[angular-native] <dom-component> ${this.name()}: ignored a message from ` +\n `${url || 'an unnamed page'}, which is not its own page.`,\n );\n }\n return;\n }\n const message = parse(data);\n if (!message) {\n if (this.debuggable) {\n console.warn(\n `[angular-native] <dom-component> ${this.name()}: dropped a message that is not JSON.`,\n );\n }\n return;\n }\n if (message.type === 'ready') {\n this.checkOutputs(message.outputs);\n this.ready = true;\n this.send(this.inputs());\n } else if (message.type === 'output') {\n const outputs = this.outputs();\n if (Object.hasOwn(outputs, message.name)) outputs[message.name]!(message.value as never);\n } else if (message.type === 'error') {\n throw new Error(`[angular-native] <dom-component> ${this.name()}: ${message.message}`);\n }\n }\n\n private checkOutputs(declared: readonly string[]): void {\n for (const name of Object.keys(this.outputs())) {\n if (!declared.includes(name)) {\n throw new Error(\n `[angular-native] <dom-component>: ${this.name()} has no output '${name}' ` +\n `(its outputs: ${declared.join(', ') || 'none'}).`,\n );\n }\n }\n }\n\n private send(inputs: Readonly<Record<string, unknown>>): void {\n const nativeTag = this.engine.tagOf(this.node);\n if (!this.native.functions || nativeTag === null) return;\n const message = JSON.stringify({ type: 'inputs', inputs });\n void this.native.functions.injectJavaScript.call(\n { nativeTag },\n `window.__ngNative && window.__ngNative.receive(${message}); true;`,\n );\n }\n}\n\n/**\n * Whether the page a message came from is the one the component loaded, so a page the web view\n * was taken to (by a link in the content, say) cannot fire the app's outputs or ask for its inputs.\n *\n * The web view names the page it is showing, not the frame that posted, so this tells pages apart\n * and not an iframe from the page that holds it. Every message is named on both platforms, from\n * the web view's own URL; one with no URL cannot be placed, so it is not trusted either.\n *\n * - **Served by the dev server**, the page is trusted by origin: that server serves the app itself.\n * - **Loaded from a file** in a release build or an update, the page is trusted by file name. The\n * web view reports the file as the native side resolved it, which differs from the source URI\n * before the name (on iOS the source is `www.bundle/...`, relative to the app), so the page must\n * be a `file:` URL whose file name is the source's, a hash of the component's path.\n */\nfunction isOwnPage(url: string | undefined, source: string): boolean {\n if (!url) return false;\n const served = /^https?:\\/\\/[^/?#]+/i.exec(source)?.[0];\n if (served) return url.toLowerCase().startsWith(`${served.toLowerCase()}/`);\n return /^file:/i.test(url) && fileName(url) === fileName(source);\n}\n\nfunction fileName(url: string): string {\n return url.split(/[?#]/)[0]!.split('/').at(-1)!;\n}\n\nfunction parse(data: string): PageMessage | undefined {\n try {\n const message: unknown = JSON.parse(data);\n return typeof message === 'object' && message !== null ? (message as PageMessage) : undefined;\n } catch {\n return undefined;\n }\n}\n\ndeclare const __DEV__: boolean | undefined;\n"]}
|