@webview-native-ui/engine 1.0.1 → 1.0.2
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 +14 -0
- package/WebViewNativeUIControls.podspec +17 -0
- package/dist/controls/activation.d.ts +15 -0
- package/dist/controls/activation.js +370 -0
- package/dist/controls/components/index.d.ts +16 -0
- package/dist/controls/components/index.js +38 -0
- package/dist/controls/components/ion-back-button.d.ts +4 -0
- package/dist/controls/components/ion-back-button.js +16 -0
- package/dist/controls/components/ion-button.d.ts +4 -0
- package/dist/controls/components/ion-button.js +26 -0
- package/dist/controls/components/ion-buttons.d.ts +5 -0
- package/dist/controls/components/ion-buttons.js +34 -0
- package/dist/controls/components/ion-fab.d.ts +4 -0
- package/dist/controls/components/ion-fab.js +164 -0
- package/dist/controls/components/ion-menu-button.d.ts +7 -0
- package/dist/controls/components/ion-menu-button.js +41 -0
- package/dist/controls/components/ion-segment.d.ts +5 -0
- package/dist/controls/components/ion-segment.js +24 -0
- package/dist/controls/components/ion-tab-bar.d.ts +5 -0
- package/dist/controls/components/ion-tab-bar.js +49 -0
- package/dist/controls/components/searchable-tabs.d.ts +20 -0
- package/dist/controls/components/searchable-tabs.js +525 -0
- package/dist/controls/definitions.d.ts +360 -0
- package/dist/controls/definitions.js +1 -0
- package/dist/controls/extensions.d.ts +44 -0
- package/dist/controls/extensions.js +164 -0
- package/dist/controls/foldable/fold-state.d.ts +4 -0
- package/dist/controls/foldable/fold-state.js +8 -0
- package/dist/controls/foldable/types.d.ts +9 -0
- package/dist/controls/foldable/types.js +1 -0
- package/dist/controls/html.d.ts +8 -0
- package/dist/controls/html.js +260 -0
- package/dist/controls/index.d.ts +7 -0
- package/dist/controls/index.js +4 -0
- package/dist/controls/ionic.d.ts +1 -0
- package/dist/controls/ionic.js +1 -0
- package/dist/controls/legacy-layout.d.ts +7 -0
- package/dist/controls/legacy-layout.js +23 -0
- package/dist/controls/overlays/content.d.ts +2 -0
- package/dist/controls/overlays/content.js +55 -0
- package/dist/controls/overlays/controller.d.ts +7 -0
- package/dist/controls/overlays/controller.js +369 -0
- package/dist/controls/overlays/covered-overlay.d.ts +4 -0
- package/dist/controls/overlays/covered-overlay.js +9 -0
- package/dist/controls/overlays/dialog-animation.d.ts +7 -0
- package/dist/controls/overlays/dialog-animation.js +49 -0
- package/dist/controls/overlays/dismiss-animation.d.ts +2 -0
- package/dist/controls/overlays/dismiss-animation.js +14 -0
- package/dist/controls/overlays/focus.d.ts +2 -0
- package/dist/controls/overlays/focus.js +67 -0
- package/dist/controls/overlays/popover.d.ts +10 -0
- package/dist/controls/overlays/popover.js +117 -0
- package/dist/controls/overlays/styles.d.ts +9 -0
- package/dist/controls/overlays/styles.js +194 -0
- package/dist/controls/prehide.d.ts +6 -0
- package/dist/controls/prehide.js +280 -0
- package/dist/controls/runtime.d.ts +5 -0
- package/dist/controls/runtime.js +1685 -0
- package/dist/controls/shared/bridge.d.ts +4 -0
- package/dist/controls/shared/bridge.js +15 -0
- package/dist/controls/shared/candidate.d.ts +23 -0
- package/dist/controls/shared/candidate.js +128 -0
- package/dist/controls/shared/clipping.d.ts +1 -0
- package/dist/controls/shared/clipping.js +1 -0
- package/dist/controls/shared/connection.d.ts +21 -0
- package/dist/controls/shared/connection.js +112 -0
- package/dist/controls/shared/control-options.d.ts +4 -0
- package/dist/controls/shared/control-options.js +14 -0
- package/dist/controls/shared/covered-snapshot.d.ts +35 -0
- package/dist/controls/shared/covered-snapshot.js +41 -0
- package/dist/controls/shared/covered-source.d.ts +22 -0
- package/dist/controls/shared/covered-source.js +72 -0
- package/dist/controls/shared/crossfade.d.ts +11 -0
- package/dist/controls/shared/crossfade.js +71 -0
- package/dist/controls/shared/diagnostics.d.ts +2 -0
- package/dist/controls/shared/diagnostics.js +16 -0
- package/dist/controls/shared/dom.d.ts +45 -0
- package/dist/controls/shared/dom.js +258 -0
- package/dist/controls/shared/events.d.ts +12 -0
- package/dist/controls/shared/events.js +13 -0
- package/dist/controls/shared/horizontal-navigation.d.ts +36 -0
- package/dist/controls/shared/horizontal-navigation.js +90 -0
- package/dist/controls/shared/icons.d.ts +5 -0
- package/dist/controls/shared/icons.js +66 -0
- package/dist/controls/shared/modal-animation.d.ts +21 -0
- package/dist/controls/shared/modal-animation.js +97 -0
- package/dist/controls/shared/modal.d.ts +26 -0
- package/dist/controls/shared/modal.js +261 -0
- package/dist/controls/shared/projection-id.d.ts +2 -0
- package/dist/controls/shared/projection-id.js +2 -0
- package/dist/controls/shared/scroll-placement.d.ts +1 -0
- package/dist/controls/shared/scroll-placement.js +1 -0
- package/dist/controls/shared/tab-bar-width.d.ts +7 -0
- package/dist/controls/shared/tab-bar-width.js +72 -0
- package/dist/controls/shared/toolbar-options.d.ts +10 -0
- package/dist/controls/shared/toolbar-options.js +38 -0
- package/dist/controls/shared/transition-identity.d.ts +3 -0
- package/dist/controls/shared/transition-identity.js +64 -0
- package/dist/controls/shared/vertical-bars-geometry.d.ts +8 -0
- package/dist/controls/shared/vertical-bars-geometry.js +82 -0
- package/dist/controls/shared/vertical-navigation.d.ts +5 -0
- package/dist/controls/shared/vertical-navigation.js +39 -0
- package/dist/controls/startup.d.ts +7 -0
- package/dist/controls/startup.js +72 -0
- package/dist/controls/transport.d.ts +10 -0
- package/dist/controls/transport.js +1 -0
- package/dist/controls/vertical-bars-web-search.d.ts +8 -0
- package/dist/controls/vertical-bars-web-search.js +83 -0
- package/dist/controls/vertical-bars-web.d.ts +2 -0
- package/dist/controls/vertical-bars-web.js +609 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/integration/index.d.ts +36 -0
- package/dist/integration/index.js +25 -0
- package/dist/native/session.js +14 -8
- package/dist/projection/pause.d.ts +10 -0
- package/dist/projection/pause.js +58 -0
- package/dist/projection/styles.d.ts +1 -1
- package/dist/projection/styles.js +0 -1
- package/dist/projection/surfaces.js +2 -1
- package/dist/projection/web.js +39 -5
- package/dist/types.d.ts +5 -1
- package/docs/README.md +1 -0
- package/docs/configuration/README.md +3 -1
- package/docs/configuration/ionic-overlays.md +36 -2
- package/docs/configuration/pause-resume.md +51 -0
- package/docs/controls.md +39 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButton.swift +98 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButtons.swift +24 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIComponents.swift +19 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIFab.swift +80 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUISearchController.swift +449 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUISegment.swift +91 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUITabBar.swift +213 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIToolbarTransition.swift +186 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalBars.swift +696 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalSearch.swift +117 -0
- package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUI.swift +180 -0
- package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUIRendering.swift +38 -0
- package/ios/Sources/WebViewNativeUIControls/NativeUIController.swift +801 -0
- package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayController.swift +131 -0
- package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayHost.swift +85 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIControlRendering.swift +21 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICoveredSnapshots.swift +14 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICrossfade.swift +35 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIHost.swift +8 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIScrollTracker.swift +2 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUISnapshot.swift +206 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIVerticalBarsGeometry.swift +93 -0
- package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +2 -2
- package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +1 -1
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -4
- package/package.json +30 -5
- package/src/controls/activation.ts +427 -0
- package/src/controls/components/index.ts +45 -0
- package/src/controls/components/ion-back-button.ts +22 -0
- package/src/controls/components/ion-button.ts +28 -0
- package/src/controls/components/ion-buttons.ts +37 -0
- package/src/controls/components/ion-fab.ts +151 -0
- package/src/controls/components/ion-menu-button.ts +37 -0
- package/src/controls/components/ion-segment.ts +27 -0
- package/src/controls/components/ion-tab-bar.ts +55 -0
- package/src/controls/components/searchable-tabs.ts +534 -0
- package/src/controls/definitions.ts +338 -0
- package/src/controls/extensions.ts +198 -0
- package/src/controls/foldable/fold-state.ts +10 -0
- package/src/controls/foldable/types.ts +4 -0
- package/src/controls/html.ts +239 -0
- package/src/controls/index.ts +7 -0
- package/src/controls/ionic.ts +1 -0
- package/src/controls/legacy-layout.ts +21 -0
- package/src/controls/overlays/content.ts +51 -0
- package/src/controls/overlays/controller.ts +394 -0
- package/src/controls/overlays/covered-overlay.ts +11 -0
- package/src/controls/overlays/dialog-animation.ts +53 -0
- package/src/controls/overlays/dismiss-animation.ts +15 -0
- package/src/controls/overlays/focus.ts +64 -0
- package/src/controls/overlays/popover.ts +116 -0
- package/src/controls/overlays/styles.ts +195 -0
- package/src/controls/prehide.ts +285 -0
- package/src/controls/runtime.ts +1731 -0
- package/src/controls/shared/bridge.ts +18 -0
- package/src/controls/shared/candidate.ts +163 -0
- package/src/controls/shared/clipping.ts +1 -0
- package/src/controls/shared/connection.ts +105 -0
- package/src/controls/shared/control-options.ts +20 -0
- package/src/controls/shared/covered-snapshot.ts +52 -0
- package/src/controls/shared/covered-source.ts +78 -0
- package/src/controls/shared/crossfade.ts +70 -0
- package/src/controls/shared/diagnostics.ts +13 -0
- package/src/controls/shared/dom.ts +310 -0
- package/src/controls/shared/events.ts +14 -0
- package/src/controls/shared/horizontal-navigation.ts +106 -0
- package/src/controls/shared/icons.ts +64 -0
- package/src/controls/shared/modal-animation.ts +109 -0
- package/src/controls/shared/modal.ts +286 -0
- package/src/controls/shared/projection-id.ts +2 -0
- package/src/controls/shared/scroll-placement.ts +1 -0
- package/src/controls/shared/tab-bar-width.ts +66 -0
- package/src/controls/shared/toolbar-options.ts +51 -0
- package/src/controls/shared/transition-identity.ts +67 -0
- package/src/controls/shared/vertical-bars-geometry.ts +92 -0
- package/src/controls/shared/vertical-navigation.ts +34 -0
- package/src/controls/startup.ts +74 -0
- package/src/controls/transport.ts +11 -0
- package/src/controls/vertical-bars-web-search.ts +86 -0
- package/src/controls/vertical-bars-web.ts +674 -0
- package/src/dom/geometry.ts +24 -0
- package/src/dom/overlays.ts +196 -0
- package/src/dom/scroll-bindings.ts +66 -0
- package/src/dom/scroll-placement.ts +92 -0
- package/src/dom/visibility.ts +29 -0
- package/src/index.ts +19 -0
- package/src/integration/index.ts +64 -0
- package/src/ionic.ts +34 -0
- package/src/native/bootstrap.ts +132 -0
- package/src/native/bridge.ts +18 -0
- package/src/native/resource-url.ts +11 -0
- package/src/native/session.ts +708 -0
- package/src/native/updates.ts +103 -0
- package/src/native/values.ts +29 -0
- package/src/projection/events.ts +58 -0
- package/src/projection/pause.ts +58 -0
- package/src/projection/placement.ts +34 -0
- package/src/projection/scope.ts +13 -0
- package/src/projection/styles.ts +26 -0
- package/src/projection/surface.ts +88 -0
- package/src/projection/surfaces.ts +119 -0
- package/src/projection/web.ts +1013 -0
- package/src/types.ts +58 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function* projectionAncestors(element: HTMLElement): Generator<HTMLElement> {
|
|
2
|
+
let current: HTMLElement | null = element;
|
|
3
|
+
while (current) {
|
|
4
|
+
const root = current.getRootNode();
|
|
5
|
+
current = current.assignedSlot ?? current.parentElement ?? (root instanceof ShadowRoot ? (root.host as HTMLElement) : null);
|
|
6
|
+
if (current) yield current;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const fitsClip = (element: HTMLElement): boolean => {
|
|
11
|
+
const rect = element.getBoundingClientRect();
|
|
12
|
+
for (const ancestor of projectionAncestors(element)) {
|
|
13
|
+
const style = getComputedStyle(ancestor);
|
|
14
|
+
if (style.transform !== 'none' && !new DOMMatrixReadOnly(style.transform).isIdentity) return false;
|
|
15
|
+
const bounds = ancestor.getBoundingClientRect();
|
|
16
|
+
const left = bounds.left + ancestor.clientLeft;
|
|
17
|
+
const top = bounds.top + ancestor.clientTop;
|
|
18
|
+
if (/^(hidden|clip|scroll|auto)$/.test(style.overflowX) && (rect.left < left - 1 || rect.right > left + ancestor.clientWidth + 1))
|
|
19
|
+
return false;
|
|
20
|
+
if (/^(hidden|clip|scroll|auto)$/.test(style.overflowY) && (rect.top < top - 1 || rect.bottom > top + ancestor.clientHeight + 1))
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/** Shared by npm consumers and the runtime injected from the native library. */
|
|
2
|
+
export type OverlayPhase = 'willPresent' | 'didPresent' | 'willDismiss' | 'didDismiss' | 'change';
|
|
3
|
+
export interface OverlayChange {
|
|
4
|
+
element?: HTMLElement;
|
|
5
|
+
phase: OverlayPhase;
|
|
6
|
+
event?: Event;
|
|
7
|
+
}
|
|
8
|
+
export interface OverlayDefinition {
|
|
9
|
+
selector: string;
|
|
10
|
+
events: Partial<Record<OverlayPhase, string>>;
|
|
11
|
+
}
|
|
12
|
+
export interface OverlayOptions {
|
|
13
|
+
/** Additional overlay components; Ionic defaults remain enabled. */
|
|
14
|
+
additional?: readonly OverlayDefinition[];
|
|
15
|
+
exclude?: string;
|
|
16
|
+
/** Return undefined to use the normal DOM/backdrop relationship. */
|
|
17
|
+
covers?: (overlay: HTMLElement, target: HTMLElement) => boolean | undefined;
|
|
18
|
+
}
|
|
19
|
+
export interface OverlayElement extends HTMLElement {
|
|
20
|
+
presented?: boolean;
|
|
21
|
+
overlayIndex?: number;
|
|
22
|
+
showBackdrop?: boolean;
|
|
23
|
+
initialBreakpoint?: number;
|
|
24
|
+
backdropBreakpoint?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Whether a Web overlay covers the underlying surface, independently of its renderer. */
|
|
28
|
+
export const usesOverlayBackdrop = (element: HTMLElement, breakpoint?: number): boolean => {
|
|
29
|
+
if (element.matches('ion-toast, ion-menu')) return false;
|
|
30
|
+
const overlay = element as OverlayElement;
|
|
31
|
+
if (overlay.showBackdrop === false) return false;
|
|
32
|
+
if (!element.matches('ion-modal')) return true;
|
|
33
|
+
const height = breakpoint ?? overlay.initialBreakpoint;
|
|
34
|
+
return height === undefined || height > (overlay.backdropBreakpoint ?? 0);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const key = Symbol.for('@webview-native-ui/engine/overlays/v1');
|
|
38
|
+
const createCoordinator = (doc: Document) => {
|
|
39
|
+
const active = new Set<HTMLElement>();
|
|
40
|
+
const native = new Set<HTMLElement>();
|
|
41
|
+
const listeners = new Set<(change: OverlayChange) => void>();
|
|
42
|
+
const breakpoints = new WeakMap<HTMLElement, number>();
|
|
43
|
+
const dismissed = new WeakSet<HTMLElement>();
|
|
44
|
+
const registrations = new Map<string, () => void>();
|
|
45
|
+
let definitions: readonly OverlayDefinition[] = [];
|
|
46
|
+
let options: OverlayOptions = {};
|
|
47
|
+
let routes = new Map<string, { phase: OverlayPhase; selector: string }>();
|
|
48
|
+
let observer: MutationObserver | undefined;
|
|
49
|
+
const notify = (change: OverlayChange) => {
|
|
50
|
+
for (const listener of [...listeners]) {
|
|
51
|
+
try {
|
|
52
|
+
listener(change);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Native UI overlay listener failed', error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const accept = (element: HTMLElement, phase: OverlayPhase, event?: Event) => {
|
|
59
|
+
if (phase === 'willPresent' || phase === 'didPresent') {
|
|
60
|
+
dismissed.delete(element);
|
|
61
|
+
active.add(element);
|
|
62
|
+
} else if (phase === 'didDismiss') {
|
|
63
|
+
dismissed.add(element);
|
|
64
|
+
active.delete(element);
|
|
65
|
+
native.delete(element);
|
|
66
|
+
breakpoints.delete(element);
|
|
67
|
+
}
|
|
68
|
+
const breakpoint = (event as CustomEvent<{ breakpoint?: number }> | undefined)?.detail?.breakpoint;
|
|
69
|
+
// Zero begins sheet dismissal; retain coverage until DidDismiss retires the surface.
|
|
70
|
+
if (typeof breakpoint === 'number' && breakpoint > 0) breakpoints.set(element, breakpoint);
|
|
71
|
+
notify({ element, phase, event });
|
|
72
|
+
};
|
|
73
|
+
const refresh = (initial = false) => {
|
|
74
|
+
let changed = false;
|
|
75
|
+
for (const element of active)
|
|
76
|
+
if (!element.isConnected || element.ownerDocument !== doc || !definitions.some(({ selector }) => element.matches(selector))) {
|
|
77
|
+
active.delete(element);
|
|
78
|
+
native.delete(element);
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
for (const element of doc.querySelectorAll<OverlayElement>(definitions.map(({ selector }) => selector).join(','))) {
|
|
82
|
+
if (
|
|
83
|
+
!active.has(element) &&
|
|
84
|
+
!dismissed.has(element) &&
|
|
85
|
+
(initial || !element.matches('ion-popover')) &&
|
|
86
|
+
(element.presented || element.classList.contains('show-menu'))
|
|
87
|
+
) {
|
|
88
|
+
active.add(element);
|
|
89
|
+
changed = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (changed) notify({ phase: 'change' });
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
get active(): ReadonlySet<HTMLElement> {
|
|
96
|
+
return active;
|
|
97
|
+
},
|
|
98
|
+
subscribe(listener: (change: OverlayChange) => void) {
|
|
99
|
+
listeners.add(listener);
|
|
100
|
+
return () => {
|
|
101
|
+
listeners.delete(listener);
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
/** Renderer ownership is explicit because a child native window is outside the DOM stack. */
|
|
105
|
+
setNative(element: HTMLElement, value: boolean) {
|
|
106
|
+
if (native.has(element) === value) return;
|
|
107
|
+
if (value) native.add(element);
|
|
108
|
+
else native.delete(element);
|
|
109
|
+
notify({ element, phase: 'change' });
|
|
110
|
+
},
|
|
111
|
+
isExcluded(element: HTMLElement) {
|
|
112
|
+
return !!options.exclude && element.matches(options.exclude);
|
|
113
|
+
},
|
|
114
|
+
isNative(element: HTMLElement) {
|
|
115
|
+
return native.has(element);
|
|
116
|
+
},
|
|
117
|
+
get stack() {
|
|
118
|
+
const stack = [...active].filter((element) => element.isConnected && element.ownerDocument === doc);
|
|
119
|
+
const order = (element: HTMLElement) => {
|
|
120
|
+
const z = Number.parseFloat(doc.defaultView!.getComputedStyle(element).zIndex);
|
|
121
|
+
return Number.isFinite(z) ? z : ((element as OverlayElement).overlayIndex ?? 0);
|
|
122
|
+
};
|
|
123
|
+
stack.sort((a, b) => order(a) - order(b));
|
|
124
|
+
return stack;
|
|
125
|
+
},
|
|
126
|
+
isCovered(target: HTMLElement) {
|
|
127
|
+
const stack = this.stack;
|
|
128
|
+
const owner = stack.reduce((last, element, index) => (element.contains(target) ? index : last), -1);
|
|
129
|
+
return stack.some((element, index) => {
|
|
130
|
+
if (native.has(element) || (options.exclude && element.matches(options.exclude))) return false;
|
|
131
|
+
const override = options.covers?.(element, target);
|
|
132
|
+
if (override !== undefined) return override;
|
|
133
|
+
return (
|
|
134
|
+
index > owner &&
|
|
135
|
+
!element.contains(target) &&
|
|
136
|
+
(element.matches('ion-menu') || usesOverlayBackdrop(element, breakpoints.get(element)))
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
enable(defaults: readonly OverlayDefinition[], config?: OverlayOptions) {
|
|
141
|
+
const nextOptions = { ...options, ...config };
|
|
142
|
+
const nextDefinitions = [...defaults, ...(nextOptions.additional ?? [])];
|
|
143
|
+
if (nextOptions.exclude) doc.querySelector(nextOptions.exclude);
|
|
144
|
+
const nextRoutes = new Map<string, { phase: OverlayPhase; selector: string }>();
|
|
145
|
+
for (const definition of nextDefinitions) {
|
|
146
|
+
doc.querySelector(definition.selector);
|
|
147
|
+
for (const [phase, name] of Object.entries(definition.events)) {
|
|
148
|
+
const existing = nextRoutes.get(name);
|
|
149
|
+
if (existing && existing.phase !== phase) throw new TypeError(`Overlay event "${name}" has conflicting phases`);
|
|
150
|
+
nextRoutes.set(name, {
|
|
151
|
+
phase: phase as OverlayPhase,
|
|
152
|
+
selector: existing ? `${existing.selector}, ${definition.selector}` : definition.selector,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
options = nextOptions;
|
|
157
|
+
definitions = nextDefinitions;
|
|
158
|
+
routes = nextRoutes;
|
|
159
|
+
for (const name of routes.keys()) {
|
|
160
|
+
if (registrations.has(name)) continue;
|
|
161
|
+
const listener = (event: Event) => {
|
|
162
|
+
const route = routes.get(name)!;
|
|
163
|
+
const element = event
|
|
164
|
+
.composedPath()
|
|
165
|
+
.find((node): node is HTMLElement => node instanceof doc.defaultView!.HTMLElement && node.matches(route.selector));
|
|
166
|
+
if (element) accept(element, route.phase, event);
|
|
167
|
+
};
|
|
168
|
+
doc.addEventListener(name, listener, true);
|
|
169
|
+
registrations.set(name, () => doc.removeEventListener(name, listener, true));
|
|
170
|
+
}
|
|
171
|
+
for (const [name, remove] of registrations)
|
|
172
|
+
if (!routes.has(name)) {
|
|
173
|
+
remove();
|
|
174
|
+
registrations.delete(name);
|
|
175
|
+
}
|
|
176
|
+
const initial = !observer;
|
|
177
|
+
if (!observer) {
|
|
178
|
+
observer = new MutationObserver(() => refresh());
|
|
179
|
+
observer.observe(doc.documentElement, { childList: true, subtree: true });
|
|
180
|
+
doc.addEventListener('nativeUIOverlayAdopted', (event) => {
|
|
181
|
+
const element = (event as CustomEvent<HTMLElement>).detail;
|
|
182
|
+
if (element?.ownerDocument === doc) accept(element, 'willPresent', event);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
refresh(initial);
|
|
186
|
+
if (initial || config) notify({ phase: 'change' });
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
export type OverlayCoordinator = ReturnType<typeof createCoordinator>;
|
|
191
|
+
|
|
192
|
+
/** Access is inert: only the explicit framework initializer installs DOM listeners. */
|
|
193
|
+
export const getOverlayCoordinator = (doc: Document): OverlayCoordinator => {
|
|
194
|
+
const shared = doc as unknown as Record<symbol, OverlayCoordinator>;
|
|
195
|
+
return (shared[key] ??= createCoordinator(doc));
|
|
196
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { WebProjectionSource } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/** Coordinate ownership, independent of any plugin's update transport or rendering. */
|
|
4
|
+
export const createScrollBindings = () => {
|
|
5
|
+
let acknowledged: WebProjectionSource[] = [];
|
|
6
|
+
let viewportWidth = 0;
|
|
7
|
+
const motion = new WeakMap<HTMLElement, { version: number; moving: boolean }>();
|
|
8
|
+
const reuse = (candidate: WebProjectionSource, width: number) => {
|
|
9
|
+
const next = candidate.control.scroll;
|
|
10
|
+
if (!candidate.scrollElement || !next || viewportWidth !== width) return false;
|
|
11
|
+
return acknowledged.some((previous) => {
|
|
12
|
+
const current = previous.control.scroll;
|
|
13
|
+
return (
|
|
14
|
+
previous.scrollElement === candidate.scrollElement &&
|
|
15
|
+
current?.id === next.id &&
|
|
16
|
+
current.contentWidth === next.contentWidth &&
|
|
17
|
+
current.contentHeight === next.contentHeight &&
|
|
18
|
+
(['viewport', 'clip'] as const).every((key) =>
|
|
19
|
+
(['x', 'y', 'width', 'height'] as const).every((axis) => current[key][axis] === next[key][axis]),
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
reuse,
|
|
26
|
+
acknowledge(sources: WebProjectionSource[], width: number) {
|
|
27
|
+
acknowledged = sources;
|
|
28
|
+
viewportWidth = width;
|
|
29
|
+
},
|
|
30
|
+
changed(owner: HTMLElement) {
|
|
31
|
+
motion.set(owner, { version: (motion.get(owner)?.version ?? 0) + 1, moving: true });
|
|
32
|
+
},
|
|
33
|
+
settled(owner: HTMLElement) {
|
|
34
|
+
const state = motion.get(owner);
|
|
35
|
+
if (state) state.moving = false;
|
|
36
|
+
},
|
|
37
|
+
eligible(candidate: WebProjectionSource, width: number) {
|
|
38
|
+
return !candidate.scrollElement || !motion.get(candidate.scrollElement)?.moving || reuse(candidate, width);
|
|
39
|
+
},
|
|
40
|
+
acquiring(sources: WebProjectionSource[], width: number) {
|
|
41
|
+
return new Map(
|
|
42
|
+
sources.flatMap((source) =>
|
|
43
|
+
source.scrollElement && !reuse(source, width)
|
|
44
|
+
? [[source.scrollElement, motion.get(source.scrollElement)?.version ?? 0] as const]
|
|
45
|
+
: [],
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
unconfirmed(sources: WebProjectionSource[], acquiring: Map<HTMLElement, number>) {
|
|
50
|
+
return new Set(
|
|
51
|
+
sources.flatMap((source) => {
|
|
52
|
+
const owner = source.scrollElement,
|
|
53
|
+
sample = source.control.scroll;
|
|
54
|
+
return owner &&
|
|
55
|
+
sample &&
|
|
56
|
+
acquiring.has(owner) &&
|
|
57
|
+
(acquiring.get(owner) !== (motion.get(owner)?.version ?? 0) ||
|
|
58
|
+
owner.scrollLeft !== sample.offsetX ||
|
|
59
|
+
owner.scrollTop !== sample.offsetY)
|
|
60
|
+
? [owner]
|
|
61
|
+
: [];
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ScrollPlacement as NativeUIScrollPlacement } from '../types.js';
|
|
2
|
+
type Identify = (element: HTMLElement) => string;
|
|
3
|
+
import { projectionAncestors } from './geometry.js';
|
|
4
|
+
const frame = ({ x, y, width, height }: DOMRect) => ({ x, y, width, height });
|
|
5
|
+
|
|
6
|
+
const viewportFrame = (element: HTMLElement) => {
|
|
7
|
+
const bounds = element.getBoundingClientRect();
|
|
8
|
+
const style = getComputedStyle(element);
|
|
9
|
+
const left = parseFloat(style.borderLeftWidth) || 0;
|
|
10
|
+
const top = parseFloat(style.borderTopWidth) || 0;
|
|
11
|
+
return {
|
|
12
|
+
x: bounds.left + left,
|
|
13
|
+
y: bounds.top + top,
|
|
14
|
+
width: bounds.width - left - (parseFloat(style.borderRightWidth) || 0),
|
|
15
|
+
height: bounds.height - top - (parseFloat(style.borderBottomWidth) || 0),
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Layout-only measurement. Native owns subsequent scroll offsets. No private Ionic APIs. */
|
|
20
|
+
export const readScrollPlacement = (element: HTMLElement, owner: HTMLElement, id: Identify): NativeUIScrollPlacement | undefined => {
|
|
21
|
+
const ancestors = [element, ...projectionAncestors(element)];
|
|
22
|
+
const index = ancestors.indexOf(owner);
|
|
23
|
+
if (index <= 0 || owner === element.ownerDocument.scrollingElement) return;
|
|
24
|
+
const win = element.ownerDocument.defaultView!;
|
|
25
|
+
if (win.visualViewport && (win.visualViewport.scale !== 1 || win.visualViewport.offsetLeft || win.visualViewport.offsetTop)) return;
|
|
26
|
+
let left = 0,
|
|
27
|
+
top = 0,
|
|
28
|
+
right = win.innerWidth,
|
|
29
|
+
bottom = win.innerHeight;
|
|
30
|
+
for (const [i, ancestor] of ancestors.entries()) {
|
|
31
|
+
const style = getComputedStyle(ancestor);
|
|
32
|
+
if (
|
|
33
|
+
(style.transform !== 'none' && !new DOMMatrixReadOnly(style.transform).isIdentity) ||
|
|
34
|
+
(style.zoom && style.zoom !== 'normal' && Number(style.zoom) !== 1) ||
|
|
35
|
+
[style.translate, style.rotate, style.scale, style.perspective].some((value) => value && value !== 'none') ||
|
|
36
|
+
(style.writingMode && style.writingMode !== 'horizontal-tb') ||
|
|
37
|
+
(i < index && /^(fixed|sticky)$/.test(style.position))
|
|
38
|
+
)
|
|
39
|
+
return;
|
|
40
|
+
if (i === 0) continue;
|
|
41
|
+
if (style.scrollbarGutter && style.scrollbarGutter !== 'auto') return;
|
|
42
|
+
const clipsX = /^(auto|scroll|hidden|clip)$/.test(style.overflowX);
|
|
43
|
+
const clipsY = /^(auto|scroll|hidden|clip)$/.test(style.overflowY);
|
|
44
|
+
// Nested moving/clipping coordinates require a separate binding; do not guess.
|
|
45
|
+
if (i < index && (clipsX || clipsY)) return;
|
|
46
|
+
const documentScroll = ancestor === element.ownerDocument.scrollingElement;
|
|
47
|
+
if (
|
|
48
|
+
i > index &&
|
|
49
|
+
(((clipsX || documentScroll) && ancestor.scrollWidth > ancestor.clientWidth) ||
|
|
50
|
+
((clipsY || documentScroll) && ancestor.scrollHeight > ancestor.clientHeight))
|
|
51
|
+
)
|
|
52
|
+
return;
|
|
53
|
+
if (
|
|
54
|
+
(clipsX || clipsY) &&
|
|
55
|
+
[style.borderTopLeftRadius, style.borderTopRightRadius, style.borderBottomLeftRadius, style.borderBottomRightRadius].some(
|
|
56
|
+
(radius) => parseFloat(radius) > 0,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
return;
|
|
60
|
+
if ((style.clipPath && style.clipPath !== 'none') || (style.maskImage && style.maskImage !== 'none')) return;
|
|
61
|
+
if (i >= index) {
|
|
62
|
+
const rect = viewportFrame(ancestor);
|
|
63
|
+
if (clipsX) {
|
|
64
|
+
left = Math.max(left, rect.x);
|
|
65
|
+
right = Math.min(right, rect.x + rect.width);
|
|
66
|
+
}
|
|
67
|
+
if (clipsY) {
|
|
68
|
+
top = Math.max(top, rect.y);
|
|
69
|
+
bottom = Math.min(bottom, rect.y + rect.height);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const style = getComputedStyle(owner);
|
|
74
|
+
if (!/^(auto|scroll)$/.test(style.overflowX) && !/^(auto|scroll)$/.test(style.overflowY)) return;
|
|
75
|
+
const viewport = viewportFrame(owner);
|
|
76
|
+
if (right <= left || bottom <= top || !viewport.width || !viewport.height) return;
|
|
77
|
+
const source = frame(element.getBoundingClientRect());
|
|
78
|
+
return {
|
|
79
|
+
id: id(owner),
|
|
80
|
+
offsetX: owner.scrollLeft,
|
|
81
|
+
offsetY: owner.scrollTop,
|
|
82
|
+
viewport,
|
|
83
|
+
clip: { x: left, y: top, width: right - left, height: bottom - top },
|
|
84
|
+
contentWidth: owner.scrollWidth,
|
|
85
|
+
contentHeight: owner.scrollHeight,
|
|
86
|
+
frame: {
|
|
87
|
+
...source,
|
|
88
|
+
x: source.x - viewport.x + owner.scrollLeft,
|
|
89
|
+
y: source.y - viewport.y + owner.scrollTop,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { projectionAncestors } from './geometry.js';
|
|
2
|
+
|
|
3
|
+
/** Current CSS visibility without the Engine's projection hiding rules.
|
|
4
|
+
* Use when an existing renderer decides whether its DOM source is authored hidden.
|
|
5
|
+
* Projection attributes are restored synchronously before returning; this performs
|
|
6
|
+
* a style read and may notify MutationObservers, so it is not a cached measurement.
|
|
7
|
+
*/
|
|
8
|
+
export const getUnprojectedVisibility = (element: HTMLElement): string => {
|
|
9
|
+
const win = element.ownerDocument.defaultView;
|
|
10
|
+
if (!win) return 'hidden';
|
|
11
|
+
const markers: { node: HTMLElement; name: string; value: string }[] = [];
|
|
12
|
+
for (const node of [element, ...projectionAncestors(element)]) {
|
|
13
|
+
for (const attribute of Array.from(node.attributes)) {
|
|
14
|
+
if (
|
|
15
|
+
attribute.name === 'data-native-ui-snapshot-source' ||
|
|
16
|
+
attribute.name === 'data-native-ui-snapshot-prepared' ||
|
|
17
|
+
attribute.name === 'data-native-ui-snapshot-content' ||
|
|
18
|
+
attribute.name.startsWith('data-native-ui-session-')
|
|
19
|
+
)
|
|
20
|
+
markers.push({ node, name: attribute.name, value: attribute.value });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
for (const { node, name } of markers) node.removeAttribute(name);
|
|
25
|
+
return win.getComputedStyle(element).visibility;
|
|
26
|
+
} finally {
|
|
27
|
+
for (const { node, name, value } of markers) node.setAttribute(name, value);
|
|
28
|
+
}
|
|
29
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './dom/geometry.js';
|
|
3
|
+
export * from './dom/scroll-placement.js';
|
|
4
|
+
export { createWebProjectionScope, type WebProjectionScope } from './projection/scope.js';
|
|
5
|
+
export * from './projection/web.js';
|
|
6
|
+
export * from './dom/scroll-bindings.js';
|
|
7
|
+
export type { ProjectionEvents, ProjectionDestination } from './projection/events.js';
|
|
8
|
+
export { getOverlayCoordinator, usesOverlayBackdrop, type OverlayChange, type OverlayOptions } from './dom/overlays.js';
|
|
9
|
+
export { getUnprojectedVisibility } from './dom/visibility.js';
|
|
10
|
+
|
|
11
|
+
export { bindProjectionPlacement, type ProjectionPlacementBinding } from './projection/placement.js';
|
|
12
|
+
|
|
13
|
+
export { createWebProjectionSurfaces } from './projection/surfaces.js';
|
|
14
|
+
|
|
15
|
+
export * from './projection/surface.js';
|
|
16
|
+
|
|
17
|
+
export { createNativeUpdateQueue, type NativeUpdateSender } from './native/updates.js';
|
|
18
|
+
|
|
19
|
+
export { pause, resume } from './projection/pause.js';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/// <reference lib="es2021.weakref" />
|
|
2
|
+
|
|
3
|
+
export interface NativeUISuspensionOptions {
|
|
4
|
+
/** Navigation-only toolbar handoff; omitted for explicit Web ownership requests. */
|
|
5
|
+
navigation?: boolean;
|
|
6
|
+
/** A swipe whose navigation result has not yet been committed. */
|
|
7
|
+
interactive?: boolean;
|
|
8
|
+
/** Direction supplied by Ionic's navigation animation builder. */
|
|
9
|
+
direction?: 'forward' | 'back' | 'root';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type NativeUITransitionRelease = ((canceled?: boolean) => void) & {
|
|
13
|
+
progress?: (value: number) => void;
|
|
14
|
+
/** The finger lifted; Web will now settle toward its chosen endpoint. */
|
|
15
|
+
finishGesture?: (complete: boolean) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface NativeUIIntegration {
|
|
19
|
+
suspend(scopes: HTMLElement[], options?: NativeUISuspensionOptions): Promise<NativeUITransitionRelease>;
|
|
20
|
+
search?(binding: NativeSearchBinding, active: boolean, focus?: boolean): Promise<boolean>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface NativeSearchBinding {
|
|
24
|
+
tabBar: HTMLElement;
|
|
25
|
+
trigger: HTMLElement;
|
|
26
|
+
footer: HTMLElement;
|
|
27
|
+
active: boolean;
|
|
28
|
+
focused: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const searches = new WeakMap<Document, WeakRef<NativeSearchBinding>[]>();
|
|
32
|
+
|
|
33
|
+
export const registerNativeSearch = (tabBar: HTMLElement, trigger: HTMLElement, footer: HTMLElement): NativeSearchBinding => {
|
|
34
|
+
const binding = { tabBar, trigger, footer, active: false, focused: false };
|
|
35
|
+
const entries = (searches.get(tabBar.ownerDocument) ?? []).filter((entry) => entry.deref() && entry.deref()?.footer !== footer);
|
|
36
|
+
entries.push(new WeakRef(binding));
|
|
37
|
+
searches.set(tabBar.ownerDocument, entries);
|
|
38
|
+
tabBar.ownerDocument.defaultView?.dispatchEvent(new Event('nativeUIRefresh'));
|
|
39
|
+
return binding;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const getNativeSearchBindings = (doc: Document): NativeSearchBinding[] =>
|
|
43
|
+
(searches.get(doc) ?? []).flatMap((entry) => entry.deref() ?? []);
|
|
44
|
+
|
|
45
|
+
export const requestNativeSearch = async (binding: NativeSearchBinding, active: boolean, focus?: boolean): Promise<boolean> =>
|
|
46
|
+
(await runtimes.get(binding.tabBar.ownerDocument)?.search?.(binding, active, focus)) ?? false;
|
|
47
|
+
|
|
48
|
+
const runtimes = new WeakMap<Document, NativeUIIntegration>();
|
|
49
|
+
export const VERTICAL_BARS_TRANSITION_CANCELED = 'iosThemeVerticalBarsTransitionCanceled';
|
|
50
|
+
|
|
51
|
+
export const setNativeUIIntegration = (doc: Document, runtime?: NativeUIIntegration) => {
|
|
52
|
+
if (runtime) runtimes.set(doc, runtime);
|
|
53
|
+
else runtimes.delete(doc);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const hasNativeUIIntegration = (doc: Document): boolean => runtimes.has(doc);
|
|
57
|
+
|
|
58
|
+
export const isNativeUI = (element: HTMLElement) => element.hasAttribute('data-native-ui-projected');
|
|
59
|
+
|
|
60
|
+
export const suspendNativeUI = async (scopes: HTMLElement[], options?: NativeUISuspensionOptions): Promise<NativeUITransitionRelease> =>
|
|
61
|
+
(await runtimes.get(scopes[0]?.ownerDocument)?.suspend(scopes, options)) ?? (() => {});
|
|
62
|
+
|
|
63
|
+
/** Shared geometry used by the theme's transition adapter; no theme import in the plugin. */
|
|
64
|
+
export const nativeWebViewMetrics = { radius: 0 };
|
package/src/ionic.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getOverlayCoordinator, type OverlayDefinition, type OverlayOptions } from './dom/overlays.js';
|
|
2
|
+
export type { OverlayOptions as IonicOverlayOptions } from './dom/overlays.js';
|
|
3
|
+
const defaults: readonly OverlayDefinition[] = [
|
|
4
|
+
...['Modal', 'Popover', 'Alert', 'ActionSheet', 'Loading', 'Picker', 'Toast'].map((name) => ({
|
|
5
|
+
selector: `ion-${name.replace(/[A-Z]/g, (letter, index) => (index ? '-' : '') + letter.toLowerCase())}`,
|
|
6
|
+
events: {
|
|
7
|
+
willPresent: `ion${name}WillPresent`,
|
|
8
|
+
didPresent: `ion${name}DidPresent`,
|
|
9
|
+
willDismiss: `ion${name}WillDismiss`,
|
|
10
|
+
didDismiss: `ion${name}DidDismiss`,
|
|
11
|
+
},
|
|
12
|
+
})),
|
|
13
|
+
{ selector: 'ion-modal', events: { change: 'ionBreakpointDidChange' } },
|
|
14
|
+
{
|
|
15
|
+
selector: 'ion-menu',
|
|
16
|
+
events: { willPresent: 'ionWillOpen', didPresent: 'ionDidOpen', willDismiss: 'ionWillClose', didDismiss: 'ionDidClose' },
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
/** Call once at app startup. Repeated default calls share the existing configuration and monitor. */
|
|
20
|
+
export const enableSupportIonicOverlays = (options?: OverlayOptions, doc: Document | undefined = globalThis.document) => {
|
|
21
|
+
if (!doc) return;
|
|
22
|
+
const coordinator = getOverlayCoordinator(doc);
|
|
23
|
+
coordinator.enable(defaults, options);
|
|
24
|
+
return coordinator;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** The background surface shares the same order used for registered DOM projections. */
|
|
28
|
+
export const ionicModalStack = (doc: Document): HTMLElement[] =>
|
|
29
|
+
getOverlayCoordinator(doc).stack.filter((element) => element.matches('ion-modal'));
|
|
30
|
+
|
|
31
|
+
export const coveredIonicSurface = (overlay: HTMLElement): HTMLElement | undefined => {
|
|
32
|
+
const stack = ionicModalStack(overlay.ownerDocument);
|
|
33
|
+
return overlay.matches('ion-modal') ? stack[stack.indexOf(overlay) - 1] : stack[stack.length - 1];
|
|
34
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { startNativeSession, type NativeSessionConfiguration } from './session.js';
|
|
2
|
+
import type { ListenerHandle } from '../types.js';
|
|
3
|
+
|
|
4
|
+
const sessions = new Map<string, { destroy(): Promise<void>; hide(): void; show(): void }>();
|
|
5
|
+
const pending = new Map<string, { resolve(value: any): void; reject(error: Error): void; timer: ReturnType<typeof setTimeout> }>();
|
|
6
|
+
const interactions = new Map<string, (event: { active: boolean; precovered?: string }) => void>();
|
|
7
|
+
const documentID = Array.from(crypto.getRandomValues(new Uint32Array(4)), (part) => part.toString(16)).join('-');
|
|
8
|
+
const notifications = new Map<string, Map<string, (event: unknown) => void>>();
|
|
9
|
+
/** Cordova plugins forward their existing notifications through NativeUIProjection.receive. */
|
|
10
|
+
export const notify = (id: string, name: string, data: unknown) => notifications.get(id)?.get(name)?.(data);
|
|
11
|
+
let sequence = 0;
|
|
12
|
+
const send = (id: string, request: unknown): Promise<any> =>
|
|
13
|
+
new Promise((resolve, reject) => {
|
|
14
|
+
const key = `${id}:${documentID}:${++sequence}`;
|
|
15
|
+
const timer = setTimeout(() => {
|
|
16
|
+
pending.delete(key);
|
|
17
|
+
reject(new Error('Native UI session timed out'));
|
|
18
|
+
}, 5000);
|
|
19
|
+
pending.set(key, { resolve, reject, timer });
|
|
20
|
+
const message = JSON.stringify({ key, request });
|
|
21
|
+
const host = globalThis as any;
|
|
22
|
+
const channel = host.webkit?.messageHandlers?.[id] ?? host.nativeUIEngine;
|
|
23
|
+
if (!channel) {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
pending.delete(key);
|
|
26
|
+
reject(new Error('Native UI session unavailable'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
channel.postMessage(message);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
pending.delete(key);
|
|
34
|
+
reject(error);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
export const receive = (id: string, key: string | null, value: any, error?: string) => {
|
|
38
|
+
if (key === null) {
|
|
39
|
+
interactions.get(id)?.(value);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const item = pending.get(key);
|
|
43
|
+
if (!item || !key.startsWith(`${id}:`)) return;
|
|
44
|
+
pending.delete(key);
|
|
45
|
+
clearTimeout(item.timer);
|
|
46
|
+
if (error) item.reject(new Error(error));
|
|
47
|
+
else item.resolve(value);
|
|
48
|
+
};
|
|
49
|
+
export const show = (id: string) => {
|
|
50
|
+
sessions.get(id)?.show();
|
|
51
|
+
};
|
|
52
|
+
export const hide = (id: string) => {
|
|
53
|
+
sessions.get(id)?.hide();
|
|
54
|
+
return true;
|
|
55
|
+
};
|
|
56
|
+
export const stop = async (id: string) => {
|
|
57
|
+
const session = sessions.get(id);
|
|
58
|
+
sessions.delete(id);
|
|
59
|
+
notifications.delete(id);
|
|
60
|
+
interactions.delete(id);
|
|
61
|
+
for (const [key, item] of pending) {
|
|
62
|
+
if (!key.startsWith(`${id}:`)) continue;
|
|
63
|
+
pending.delete(key);
|
|
64
|
+
clearTimeout(item.timer);
|
|
65
|
+
item.reject(new Error('Native UI session closed'));
|
|
66
|
+
}
|
|
67
|
+
await session?.destroy();
|
|
68
|
+
};
|
|
69
|
+
export const start = async (config: NativeSessionConfiguration) => {
|
|
70
|
+
if (sessions.has(config.id)) return;
|
|
71
|
+
// Reserve before asynchronous initialization, including duplicate Native init callbacks.
|
|
72
|
+
let actual: Awaited<ReturnType<typeof startNativeSession>> | undefined;
|
|
73
|
+
let cancelled = false;
|
|
74
|
+
let pendingDestination: 'none' | 'native' | undefined;
|
|
75
|
+
const reservation = {
|
|
76
|
+
show: () => {
|
|
77
|
+
pendingDestination = 'native';
|
|
78
|
+
actual?.show();
|
|
79
|
+
},
|
|
80
|
+
hide: () => {
|
|
81
|
+
pendingDestination = 'none';
|
|
82
|
+
actual?.hide();
|
|
83
|
+
},
|
|
84
|
+
destroy: async () => {
|
|
85
|
+
cancelled = true;
|
|
86
|
+
await actual?.destroy();
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
sessions.set(config.id, reservation);
|
|
90
|
+
try {
|
|
91
|
+
if (document.readyState === 'loading')
|
|
92
|
+
await new Promise<void>((resolve) => document.addEventListener('DOMContentLoaded', () => resolve(), { once: true }));
|
|
93
|
+
if (cancelled) return;
|
|
94
|
+
const listeners = new Map<string, (event: unknown) => void>();
|
|
95
|
+
if (config.runtime === 'cordova') notifications.set(config.id, listeners);
|
|
96
|
+
actual = await startNativeSession(
|
|
97
|
+
config,
|
|
98
|
+
{
|
|
99
|
+
send: (request) => send(config.id, request),
|
|
100
|
+
listen: async (listener): Promise<ListenerHandle> => {
|
|
101
|
+
interactions.set(config.id, listener);
|
|
102
|
+
return {
|
|
103
|
+
remove: () => {
|
|
104
|
+
if (interactions.get(config.id) === listener) interactions.delete(config.id);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
config.runtime === 'cordova'
|
|
110
|
+
? {
|
|
111
|
+
addListener: async (name, listener) => {
|
|
112
|
+
listeners.set(name, listener);
|
|
113
|
+
return {
|
|
114
|
+
remove: () => {
|
|
115
|
+
listeners.delete(name);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
: undefined,
|
|
121
|
+
);
|
|
122
|
+
if (pendingDestination === 'none') actual.hide();
|
|
123
|
+
else if (pendingDestination === 'native') actual.show();
|
|
124
|
+
if (cancelled) await actual.destroy();
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (sessions.get(config.id) === reservation) {
|
|
127
|
+
sessions.delete(config.id);
|
|
128
|
+
notifications.delete(config.id);
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
};
|