@ng-native/expo 0.3.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/dist/audio.js +2 -2
- package/dist/audio.js.map +1 -1
- 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 +150 -2
- package/dist/expo-ui-components.js +170 -3
- package/dist/expo-ui-components.js.map +1 -1
- package/dist/expo-ui.js +11 -1
- 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/fonts.d.ts +0 -9
- package/dist/fonts.js +19 -14
- package/dist/fonts.js.map +1 -1
- package/dist/glass.d.ts +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.js +7 -1
- package/dist/live-activity.js.map +1 -1
- package/dist/location.d.ts +17 -0
- package/dist/location.js +18 -0
- package/dist/location.js.map +1 -1
- package/dist/native.d.ts +8 -1
- package/dist/native.js +21 -6
- package/dist/native.js.map +1 -1
- package/dist/register-expo-view.d.ts +2 -0
- package/dist/register-expo-view.js +1 -0
- package/dist/register-expo-view.js.map +1 -1
- package/dist/video.js +2 -2
- package/dist/video.js.map +1 -1
- package/dist/widget.d.ts +63 -0
- package/dist/widget.js +98 -0
- package/dist/widget.js.map +1 -0
- package/package.json +6 -2
package/dist/audio.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* // {{ player.state().currentTime }}
|
|
8
8
|
* ```
|
|
9
9
|
*/
|
|
10
|
-
import { expoModule } from "./native.js";
|
|
10
|
+
import { expoModule, unavailable } from "./native.js";
|
|
11
11
|
import { ownPlayer, watchAudioPlayer } from "./player.js";
|
|
12
12
|
/**
|
|
13
13
|
* An audio player owned by the current component, released when it is destroyed.
|
|
@@ -18,7 +18,7 @@ import { ownPlayer, watchAudioPlayer } from "./player.js";
|
|
|
18
18
|
export function audioPlayer(source, options = {}) {
|
|
19
19
|
const expo = expoModule('expo-audio', () => require('expo-audio'), ['ios', 'android', 'web']);
|
|
20
20
|
if (!expo)
|
|
21
|
-
throw
|
|
21
|
+
throw unavailable('expo-audio', 'Stand in for the player in the test.');
|
|
22
22
|
const updateInterval = options.timeUpdate === undefined ? undefined : options.timeUpdate * 1000;
|
|
23
23
|
const native = expo.createAudioPlayer(source, { updateInterval });
|
|
24
24
|
return ownPlayer(native, () => watchAudioPlayer(native));
|
package/dist/audio.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio.js","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"audio.js","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAe,MAAM,aAAa,CAAC;AAEvE;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,MAAwC,EACxC,UAAmC,EAAE;IAErC,MAAM,IAAI,GAAG,UAAU,CACrB,YAAY,EACZ,GAAG,EAAE,CAAC,OAAO,CAAC,YAAY,CAAgC,EAC1D,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,CAC1B,CAAC;IACF,IAAI,CAAC,IAAI;QAAE,MAAM,WAAW,CAAC,YAAY,EAAE,sCAAsC,CAAC,CAAC;IACnF,MAAM,cAAc,GAAG,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAChG,MAAM,MAAM,GAAG,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,CAAC,CAAC;IAClE,OAAO,SAAS,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC;AAC3D,CAAC","sourcesContent":["/**\n * `audioPlayer`, an `expo-audio` player owned by the component that makes it.\n *\n * ```ts\n * protected readonly player = audioPlayer(this.source, { timeUpdate: 0.5 });\n *\n * // {{ player.state().currentTime }}\n * ```\n */\nimport { expoModule, unavailable } from './native.ts';\nimport { ownPlayer, watchAudioPlayer, type Player } from './player.ts';\n\n/**\n * An audio player owned by the current component, released when it is destroyed.\n *\n * `expo-audio` has no `timeUpdateEventInterval` property to set after the fact - the interval is\n * an option to `createAudioPlayer` itself, in milliseconds rather than `videoPlayer`'s seconds.\n */\nexport function audioPlayer(\n source: import('expo-audio').AudioSource,\n options: { timeUpdate?: number } = {},\n): Player<import('expo-audio').AudioPlayer> {\n const expo = expoModule(\n 'expo-audio',\n () => require('expo-audio') as typeof import('expo-audio'),\n ['ios', 'android', 'web'],\n );\n if (!expo) throw unavailable('expo-audio', 'Stand in for the player in the test.');\n const updateInterval = options.timeUpdate === undefined ? undefined : options.timeUpdate * 1000;\n const native = expo.createAudioPlayer(source, { updateInterval });\n return ownPlayer(native, () => watchAudioPlayer(native));\n}\n"]}
|
package/dist/community-views.js
CHANGED
|
@@ -22,7 +22,7 @@ export const NATIVE_VIEWS = {
|
|
|
22
22
|
'web-view': ['RNCWebView', { javaScriptEnabled: true, domStorageEnabled: true }],
|
|
23
23
|
/** `@react-native-community/slider`. */
|
|
24
24
|
slider: ['RNCSlider', { maximumValue: 1, minimumValue: 0, step: 0 }],
|
|
25
|
-
/** `@react-native-community/datetimepicker`.
|
|
25
|
+
/** `@react-native-community/datetimepicker`. iOS only: on Android the library has no view. */
|
|
26
26
|
'date-time-picker': ['RNDateTimePicker'],
|
|
27
27
|
/** `@react-native-picker/picker`, whose items are `<picker-item>` children. */
|
|
28
28
|
picker: ['RNCPicker'],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"community-views.js","sourceRoot":"","sources":["../src/community-views.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,gGAAgG;AAChG,MAAM,CAAC,MAAM,YAAY,GAAiE;IACxF,8BAA8B;IAC9B,UAAU,EAAE,CAAC,YAAY,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC;IAChF,wCAAwC;IACxC,MAAM,EAAE,CAAC,WAAW,EAAE,EAAE,YAAY,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACpE,8FAA8F;IAC9F,kBAAkB,EAAE,CAAC,kBAAkB,CAAC;IACxC,+EAA+E;IAC/E,MAAM,EAAE,CAAC,WAAW,CAAC;IACrB,aAAa,EAAE,CAAC,eAAe,CAAC;IAChC;;;;;;OAMG;IACH,mBAAmB,EAAE,CAAC,qBAAqB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAChE,yFAAyF;IACzF,aAAa,EAAE,CAAC,eAAe,CAAC;IAChC,+EAA+E;IAC/E,YAAY,EAAE,CAAC,cAAc,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IAClD,6BAA6B;IAC7B,aAAa,EAAE,CAAC,qBAAqB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACtE;;;;;;;;OAQG;IACH,WAAW,EAAE,CAAC,iBAAiB,CAAC;IAChC,kEAAkE;IAClE,WAAW,EAAE,CAAC,aAAa,CAAC;CAC7B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAG,QAAgD;IACrF,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;QACpC,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,gDAAgD,OAAO,GAAG,CAAC,CAAC;QACxF,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,CAAC;AACH,CAAC","sourcesContent":["/**\n * Native views from the libraries an Expo app already reaches for, by element name.\n *\n * These are not Expo modules, so their Fabric names are their own - what codegen produced from\n * each library's spec - rather than the `ViewManagerAdapter_` names `registerExpoView` derives.\n * They are here because they are in Expo's own bundled module list, which is the list an app\n * actually installs from, and because the alternative is every app looking each name up in a\n * library's generated code.\n *\n * Names and defaults, not components. An Angular component per native view, prop for prop, is a\n * maintenance burden with no end and a second place for every prop to be wrong. An app writes the\n * small component for the one it uses - a selector, and an input per prop it binds, passed through\n * as a host binding - which keeps its template checked; see `ExpoImage` for the shape.\n *\n * A library still has to be installed for its element to commit as anything. This is the\n * JavaScript half; an element whose native side is missing commits as `UnimplementedNativeView`.\n */\nimport { registerViewName } from '@ng-native/fabric';\n\n/** Element name -> the Fabric component name, and the props its React wrapper would default. */\nexport const NATIVE_VIEWS: Readonly<Record<string, [string, Record<string, unknown>?]>> = {\n /** `react-native-webview`. */\n 'web-view': ['RNCWebView', { javaScriptEnabled: true, domStorageEnabled: true }],\n /** `@react-native-community/slider`. */\n slider: ['RNCSlider', { maximumValue: 1, minimumValue: 0, step: 0 }],\n /** `@react-native-community/datetimepicker`.
|
|
1
|
+
{"version":3,"file":"community-views.js","sourceRoot":"","sources":["../src/community-views.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,gGAAgG;AAChG,MAAM,CAAC,MAAM,YAAY,GAAiE;IACxF,8BAA8B;IAC9B,UAAU,EAAE,CAAC,YAAY,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC;IAChF,wCAAwC;IACxC,MAAM,EAAE,CAAC,WAAW,EAAE,EAAE,YAAY,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACpE,8FAA8F;IAC9F,kBAAkB,EAAE,CAAC,kBAAkB,CAAC;IACxC,+EAA+E;IAC/E,MAAM,EAAE,CAAC,WAAW,CAAC;IACrB,aAAa,EAAE,CAAC,eAAe,CAAC;IAChC;;;;;;OAMG;IACH,mBAAmB,EAAE,CAAC,qBAAqB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAChE,yFAAyF;IACzF,aAAa,EAAE,CAAC,eAAe,CAAC;IAChC,+EAA+E;IAC/E,YAAY,EAAE,CAAC,cAAc,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;IAClD,6BAA6B;IAC7B,aAAa,EAAE,CAAC,qBAAqB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACtE;;;;;;;;OAQG;IACH,WAAW,EAAE,CAAC,iBAAiB,CAAC;IAChC,kEAAkE;IAClE,WAAW,EAAE,CAAC,aAAa,CAAC;CAC7B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAG,QAAgD;IACrF,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;QACpC,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,gDAAgD,OAAO,GAAG,CAAC,CAAC;QACxF,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,CAAC;AACH,CAAC","sourcesContent":["/**\n * Native views from the libraries an Expo app already reaches for, by element name.\n *\n * These are not Expo modules, so their Fabric names are their own - what codegen produced from\n * each library's spec - rather than the `ViewManagerAdapter_` names `registerExpoView` derives.\n * They are here because they are in Expo's own bundled module list, which is the list an app\n * actually installs from, and because the alternative is every app looking each name up in a\n * library's generated code.\n *\n * Names and defaults, not components. An Angular component per native view, prop for prop, is a\n * maintenance burden with no end and a second place for every prop to be wrong. An app writes the\n * small component for the one it uses - a selector, and an input per prop it binds, passed through\n * as a host binding - which keeps its template checked; see `ExpoImage` for the shape.\n *\n * A library still has to be installed for its element to commit as anything. This is the\n * JavaScript half; an element whose native side is missing commits as `UnimplementedNativeView`.\n */\nimport { registerViewName } from '@ng-native/fabric';\n\n/** Element name -> the Fabric component name, and the props its React wrapper would default. */\nexport const NATIVE_VIEWS: Readonly<Record<string, [string, Record<string, unknown>?]>> = {\n /** `react-native-webview`. */\n 'web-view': ['RNCWebView', { javaScriptEnabled: true, domStorageEnabled: true }],\n /** `@react-native-community/slider`. */\n slider: ['RNCSlider', { maximumValue: 1, minimumValue: 0, step: 0 }],\n /** `@react-native-community/datetimepicker`. iOS only: on Android the library has no view. */\n 'date-time-picker': ['RNDateTimePicker'],\n /** `@react-native-picker/picker`, whose items are `<picker-item>` children. */\n picker: ['RNCPicker'],\n 'picker-item': ['RNCPickerItem'],\n /**\n * `@react-native-segmented-control/segmented-control`. iOS only.\n *\n * A view manager from before Fabric, run through the interop layer, which only installs the\n * `onChange` block when the prop is set - as React's handler does by being there. Without it\n * the control changes on screen and nothing hears about it.\n */\n 'segmented-control': ['RNCSegmentedControl', { onChange: true }],\n /** `@react-native-masked-view/masked-view`: its `maskElement` is a prop, not a child. */\n 'masked-view': ['RNCMaskedView'],\n /** `react-native-pager-view`, the swipeable pager a tab layout is built on. */\n 'pager-view': ['RNCViewPager', { initialPage: 0 }],\n /** `lottie-react-native`. */\n 'lottie-view': ['LottieAnimationView', { autoPlay: true, loop: true }],\n /**\n * `@shopify/react-native-skia`.\n *\n * The view is real and this name is right, but it is worth being plain: Skia's drawing model is\n * React elements through a reconciler of its own - `<Canvas><Path/></Canvas>` - and none of that\n * is reachable here. What a `<skia-view>` will take is a `picture`, built with Skia's imperative\n * `PictureRecorder` API, which has no React in it. Anything drawn declaratively does not come\n * across, and a component that renders nothing is what that looks like.\n */\n 'skia-view': ['SkiaPictureView'],\n /** `react-native-view-shot`, which captures whatever it wraps. */\n 'view-shot': ['RNCViewShot'],\n};\n\n/**\n * Register several of the known views by element name.\n *\n * ```ts\n * registerNativeViews('web-view', 'slider');\n * ```\n *\n * Named one at a time rather than all at once on purpose: registering a view for a library the\n * app has not installed produces an element that commits as nothing, and finding out at startup\n * which libraries are present is not something JavaScript can do without importing them all.\n */\nexport function registerNativeViews(...elements: readonly (keyof typeof NATIVE_VIEWS)[]): void {\n for (const element of elements) {\n const known = NATIVE_VIEWS[element];\n if (!known) throw new Error(`[angular-native] no native view is known as '${element}'`);\n registerViewName(element, known[0], known[1]);\n }\n}\n"]}
|
package/dist/crypto.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* the data is. So every call without the module throws, naming the package to install.
|
|
21
21
|
*/
|
|
22
22
|
import { InjectionToken, Service, inject } from '@angular/core';
|
|
23
|
-
import { expoModule } from "./native.js";
|
|
23
|
+
import { expoModule, unavailable } from "./native.js";
|
|
24
24
|
import * as i0 from "@angular/core";
|
|
25
25
|
/**
|
|
26
26
|
* The hash algorithms, as `expo-crypto`'s own enum. Importing the enum from the module loads the
|
|
@@ -72,7 +72,7 @@ export class Crypto {
|
|
|
72
72
|
}
|
|
73
73
|
module() {
|
|
74
74
|
if (!this.native)
|
|
75
|
-
throw
|
|
75
|
+
throw unavailable('expo-crypto', 'Provide Crypto.SOURCE with a stand-in.');
|
|
76
76
|
return this.native;
|
|
77
77
|
}
|
|
78
78
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.2.0", ngImport: i0, type: Crypto, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
package/dist/crypto.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAKhE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;;
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAKhE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;;AAiBtD;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,IAAI,EAAE,OAAO;IACb,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,SAAS;IACjB,MAAM,EAAE,SAAS;IACjB,GAAG,EAAE,KAAK;IACV,GAAG,EAAE,KAAK;IACV,GAAG,EAAE,KAAK;CAC8B,CAAC;AAE3C,mGAAmG;AACnG,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,GAAG,EAAE,KAAK;IACV,MAAM,EAAE,QAAQ;CACiB,CAAC;AAGpC,MAAM,OAAO,MAAM;IACjB,wEAAwE;IACxE,MAAM,CAAU,MAAM,GAAG,IAAI,cAAc,CAAsB,6BAA6B,EAAE;QAC9F,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,aAAa,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,aAAa,CAAS,CAAC;KAC/E,CAAC,CAAC;IAEc,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAEhD,kEAAkE;IAClE,UAAU;QACR,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,UAAU,EAAE,CAAC;IACpC,CAAC;IAED,8CAA8C;IAC9C,KAAK,CAAC,YAAY,CAAC,GAAG,IAA+B;QACnD,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAAC,CAAC;IAClD,CAAC;IAED,8DAA8D;IAC9D,KAAK,CAAC,MAAM,CAAC,GAAG,IAAoB;QAClC,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IACvC,CAAC;IAED,yEAAyE;IACzE,WAAW,CAAC,KAAa;QACvB,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAC7C,CAAC;IAED,8BAA8B;IAC9B,KAAK,CAAC,gBAAgB,CAAC,KAAa;QAClC,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAClD,CAAC;IAED,iGAAiG;IACjG,YAAY,CAAuC,KAAQ;QACzD,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC9C,CAAC;IAEO,MAAM;QACZ,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,MAAM,WAAW,CAAC,aAAa,EAAE,wCAAwC,CAAC,CAAC;QAC7F,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;uGAzCU,MAAM;wGAAN,MAAM;;2FAAN,MAAM;kBADlB,OAAO","sourcesContent":["/**\n * `Crypto`, bound to `expo-crypto`: random identifiers, random bytes, and hashes.\n *\n * ```ts\n * import { Crypto, CryptoDigestAlgorithm } from '@ng-native/expo/crypto';\n *\n * private readonly crypto = inject(Crypto);\n *\n * readonly id = this.crypto.randomUUID();\n * async fingerprint(text: string): Promise<string> {\n * return this.crypto.digestString(CryptoDigestAlgorithm.SHA256, text);\n * }\n * ```\n *\n * The module's functions under shorter names, with its own types. Hermes has no Web Crypto, so\n * this is where a UUID or a hash comes from on a device.\n *\n * Unlike the other Expo services, this one is not inert without its module. An empty identifier\n * is a key every record shares, and an empty hash matches every input; neither looks wrong until\n * the data is. So every call without the module throws, naming the package to install.\n */\nimport { InjectionToken, Service, inject } from '@angular/core';\nimport type {\n CryptoDigestAlgorithm as ExpoDigestAlgorithm,\n CryptoEncoding as ExpoEncoding,\n} from 'expo-crypto';\nimport { expoModule, unavailable } from './native.ts';\n\ntype Expo = typeof import('expo-crypto');\n\n/** The module's functions this service calls. The real module is one; a test provides a fake. */\nexport type NativeCrypto = Pick<\n Expo,\n | 'randomUUID'\n | 'digestStringAsync'\n | 'digest'\n | 'getRandomBytes'\n | 'getRandomBytesAsync'\n | 'getRandomValues'\n>;\n\ntype Args<K extends keyof NativeCrypto> = Parameters<NativeCrypto[K]>;\n\n/**\n * The hash algorithms, as `expo-crypto`'s own enum. Importing the enum from the module loads the\n * module, which a test in Node cannot; these are the same strings, typed as the enum.\n */\nexport const CryptoDigestAlgorithm = {\n SHA1: 'SHA-1',\n SHA256: 'SHA-256',\n SHA384: 'SHA-384',\n SHA512: 'SHA-512',\n MD2: 'MD2',\n MD4: 'MD4',\n MD5: 'MD5',\n} as unknown as typeof ExpoDigestAlgorithm;\n\n/** How `digestString` writes its hash, as `expo-crypto`'s own enum: hex unless asked otherwise. */\nexport const CryptoEncoding = {\n HEX: 'hex',\n BASE64: 'base64',\n} as unknown as typeof ExpoEncoding;\n\n@Service()\nexport class Crypto {\n /** Overridden in a test to hand out identifiers that are not random. */\n static readonly SOURCE = new InjectionToken<NativeCrypto | null>('angular-native.cryptoSource', {\n factory: () => expoModule('expo-crypto', () => require('expo-crypto') as Expo),\n });\n\n private readonly native = inject(Crypto.SOURCE);\n\n /** A version 4 UUID, from the platform's secure random source. */\n randomUUID(): string {\n return this.module().randomUUID();\n }\n\n /** The hash of a string, as hex or base64. */\n async digestString(...args: Args<'digestStringAsync'>): Promise<string> {\n return this.module().digestStringAsync(...args);\n }\n\n /** The hash of bytes, as Web Crypto's `digest` answers it. */\n async digest(...args: Args<'digest'>): Promise<ArrayBuffer> {\n return this.module().digest(...args);\n }\n\n /** Cryptographically secure random bytes, between 0 and 1024 of them. */\n randomBytes(count: number): Uint8Array {\n return this.module().getRandomBytes(count);\n }\n\n /** The same, as a promise. */\n async randomBytesAsync(count: number): Promise<Uint8Array> {\n return this.module().getRandomBytesAsync(count);\n }\n\n /** Fills an integer typed array with secure random values and hands it back, as Web Crypto's. */\n randomValues<T extends Args<'getRandomValues'>[0]>(array: T): T {\n return this.module().getRandomValues(array);\n }\n\n private module(): NativeCrypto {\n if (!this.native) throw unavailable('expo-crypto', 'Provide Crypto.SOURCE with a stand-in.');\n return this.native;\n }\n}\n"]}
|
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"]}
|