@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,103 @@
|
|
|
1
|
+
/** Send only replaceable state from an update pass, never user actions. */
|
|
2
|
+
export type NativeUpdateSender = <T>(request: () => Promise<T>, validate?: (result: T) => void) => Promise<T | undefined>;
|
|
3
|
+
|
|
4
|
+
/** Coalesce current-state reads and serialize them across asynchronous native acknowledgements. */
|
|
5
|
+
export const createNativeUpdateQueue = (
|
|
6
|
+
window: Pick<Window, 'requestAnimationFrame' | 'cancelAnimationFrame'>,
|
|
7
|
+
options: {
|
|
8
|
+
update(send: NativeUpdateSender): Promise<void>;
|
|
9
|
+
failed(error: unknown): void | Promise<void>;
|
|
10
|
+
retry(): void;
|
|
11
|
+
reconnect?: boolean;
|
|
12
|
+
},
|
|
13
|
+
) => {
|
|
14
|
+
let frame = 0;
|
|
15
|
+
let dirty = false;
|
|
16
|
+
let running = false;
|
|
17
|
+
let stopped = false;
|
|
18
|
+
let retrying = false;
|
|
19
|
+
let waiting = false;
|
|
20
|
+
let reconciled = false;
|
|
21
|
+
let passFailed = false;
|
|
22
|
+
const request = () => {
|
|
23
|
+
if (stopped) return;
|
|
24
|
+
dirty = true;
|
|
25
|
+
if (!running && !waiting && !frame) frame = window.requestAnimationFrame(() => void run());
|
|
26
|
+
};
|
|
27
|
+
const waitForConnection = (error: unknown) => {
|
|
28
|
+
if (!options.reconnect || reconciled) throw error;
|
|
29
|
+
waiting = true;
|
|
30
|
+
dirty = false;
|
|
31
|
+
};
|
|
32
|
+
const send: NativeUpdateSender = async (operation, validate) => {
|
|
33
|
+
if (stopped || waiting || passFailed) return;
|
|
34
|
+
let result;
|
|
35
|
+
try {
|
|
36
|
+
result = await operation();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (stopped) return;
|
|
39
|
+
passFailed = true;
|
|
40
|
+
if (retrying) waitForConnection(error);
|
|
41
|
+
else {
|
|
42
|
+
retrying = true;
|
|
43
|
+
options.retry();
|
|
44
|
+
request();
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (stopped) return;
|
|
49
|
+
try {
|
|
50
|
+
validate?.(result);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
passFailed = true;
|
|
53
|
+
waitForConnection(error);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
const run = async () => {
|
|
59
|
+
frame = 0;
|
|
60
|
+
if (stopped || waiting) return;
|
|
61
|
+
dirty = false;
|
|
62
|
+
running = true;
|
|
63
|
+
passFailed = false;
|
|
64
|
+
try {
|
|
65
|
+
await options.update(send);
|
|
66
|
+
if (!passFailed) retrying = waiting = reconciled = false;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (stopped) return;
|
|
69
|
+
// Retire the queue before reporting terminal failure; no late update can restart it.
|
|
70
|
+
stopped = true;
|
|
71
|
+
await options.failed(error);
|
|
72
|
+
} finally {
|
|
73
|
+
running = false;
|
|
74
|
+
if (dirty) request();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
request,
|
|
79
|
+
get dirty() {
|
|
80
|
+
return dirty;
|
|
81
|
+
},
|
|
82
|
+
get running() {
|
|
83
|
+
return running;
|
|
84
|
+
},
|
|
85
|
+
get waiting() {
|
|
86
|
+
return waiting;
|
|
87
|
+
},
|
|
88
|
+
reconnected() {
|
|
89
|
+
if (stopped) return;
|
|
90
|
+
if (waiting) {
|
|
91
|
+
reconciled = true;
|
|
92
|
+
retrying = waiting = false;
|
|
93
|
+
}
|
|
94
|
+
request();
|
|
95
|
+
},
|
|
96
|
+
destroy() {
|
|
97
|
+
stopped = true;
|
|
98
|
+
dirty = false;
|
|
99
|
+
window.cancelAnimationFrame(frame);
|
|
100
|
+
frame = 0;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Reflect native state without inventing component-specific input events. */
|
|
2
|
+
export interface NativeValueBinding {
|
|
3
|
+
property?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const applyNativeValues = (target: HTMLElement, values: Record<string, unknown>, bindings: Record<string, NativeValueBinding>) => {
|
|
7
|
+
for (const [key, value] of Object.entries(values)) {
|
|
8
|
+
if (!/^[a-z][a-z0-9-]*$/.test(key)) throw new TypeError('Native value keys must be lowercase attribute names');
|
|
9
|
+
const binding = Object.hasOwn(bindings, key) ? bindings[key] : undefined;
|
|
10
|
+
const property = binding?.property;
|
|
11
|
+
if (
|
|
12
|
+
property &&
|
|
13
|
+
(!/^[a-zA-Z][\w]*$/.test(property) ||
|
|
14
|
+
/^on/i.test(property) ||
|
|
15
|
+
['innerHTML', 'outerHTML', 'textContent', 'innerText', 'style', 'constructor', 'prototype'].includes(property))
|
|
16
|
+
)
|
|
17
|
+
throw new TypeError('Native value binding requires a data property');
|
|
18
|
+
const serialized = JSON.stringify(value);
|
|
19
|
+
if (serialized === undefined) throw new TypeError('Native values must be JSON values');
|
|
20
|
+
if (property) {
|
|
21
|
+
const element = target as unknown as Record<string, unknown>;
|
|
22
|
+
if (JSON.stringify(element[property]) !== serialized) element[property] = value;
|
|
23
|
+
} else {
|
|
24
|
+
const attribute = `data-native-ui-value-${key}`;
|
|
25
|
+
const text = typeof value === 'string' ? value : serialized;
|
|
26
|
+
if (target.getAttribute(attribute) !== text) target.setAttribute(attribute, text);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ListenerHandle } from '../types.js';
|
|
2
|
+
import { boundedListener } from '../native/bridge.js';
|
|
3
|
+
|
|
4
|
+
export type ProjectionDestination = 'native' | 'web' | 'none';
|
|
5
|
+
|
|
6
|
+
/** Notifications from the existing plugin proxy; public plugin methods are never intercepted. */
|
|
7
|
+
export interface ProjectionEvents {
|
|
8
|
+
addListener(name: string, listener: (event: unknown) => void): Promise<ListenerHandle>;
|
|
9
|
+
update?: readonly string[];
|
|
10
|
+
projection?: Partial<Record<ProjectionDestination, readonly string[]>>;
|
|
11
|
+
/** Required when the plugin emits events for multiple independent surfaces. */
|
|
12
|
+
accepts?(event: unknown): boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Subscribe once per event, including events that both update and change destination. */
|
|
16
|
+
export const bindProjectionEvents = async (
|
|
17
|
+
events: ProjectionEvents,
|
|
18
|
+
receive: (destination: ProjectionDestination | undefined, updated: boolean) => void,
|
|
19
|
+
): Promise<ListenerHandle> => {
|
|
20
|
+
const routes = new Map<string, { destination?: ProjectionDestination; updated: boolean }>();
|
|
21
|
+
for (const name of events.update ?? []) routes.set(name, { updated: true });
|
|
22
|
+
for (const destination of ['native', 'web', 'none'] as const) {
|
|
23
|
+
for (const name of events.projection?.[destination] ?? []) {
|
|
24
|
+
const route = routes.get(name) ?? { updated: false };
|
|
25
|
+
if (route.destination && route.destination !== destination)
|
|
26
|
+
throw new TypeError(`Projection event "${name}" has conflicting destinations`);
|
|
27
|
+
routes.set(name, { ...route, destination });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let stopped = false;
|
|
31
|
+
const handles: ListenerHandle[] = [];
|
|
32
|
+
const remove = async () => {
|
|
33
|
+
stopped = true;
|
|
34
|
+
const results = await Promise.allSettled(handles.splice(0).map((handle) => Promise.resolve().then(() => handle.remove())));
|
|
35
|
+
const failure = results.find((result) => result.status === 'rejected');
|
|
36
|
+
if (failure?.status === 'rejected') throw failure.reason;
|
|
37
|
+
};
|
|
38
|
+
// Start all registrations immediately, so one slow bridge reply does not delay other subscriptions.
|
|
39
|
+
const registrations = [...routes].map(async ([name, route]) => {
|
|
40
|
+
const handle = await boundedListener(
|
|
41
|
+
Promise.resolve().then(() =>
|
|
42
|
+
events.addListener(name, (event) => {
|
|
43
|
+
if (!stopped && (!events.accepts || events.accepts(event))) receive(route.destination, route.updated);
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
if (stopped) await handle.remove();
|
|
48
|
+
else handles.push(handle);
|
|
49
|
+
});
|
|
50
|
+
try {
|
|
51
|
+
await Promise.all(registrations);
|
|
52
|
+
return { remove };
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// Disable callbacks immediately. Late successful registrations also remove themselves.
|
|
55
|
+
await remove().catch(() => {});
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getOverlayCoordinator } from '../dom/overlays.js';
|
|
2
|
+
|
|
3
|
+
const key = Symbol.for('@webview-native-ui/engine/projection-pause/v1');
|
|
4
|
+
const createPause = (doc: Document) => {
|
|
5
|
+
const overlays = getOverlayCoordinator(doc);
|
|
6
|
+
const listeners = new Set<() => void>();
|
|
7
|
+
let depth = 0;
|
|
8
|
+
let releaseFrame = 0;
|
|
9
|
+
const paused = () => depth > 0 || releaseFrame !== 0;
|
|
10
|
+
const notify = () => {
|
|
11
|
+
for (const listener of listeners) listener();
|
|
12
|
+
};
|
|
13
|
+
const change = (delta: number) => {
|
|
14
|
+
if (delta < 0 && depth === 0) return;
|
|
15
|
+
const wasPaused = paused();
|
|
16
|
+
depth += delta;
|
|
17
|
+
const win = doc.defaultView;
|
|
18
|
+
if (releaseFrame && win) {
|
|
19
|
+
win.cancelAnimationFrame(releaseFrame);
|
|
20
|
+
releaseFrame = 0;
|
|
21
|
+
}
|
|
22
|
+
if (depth === 0 && win) {
|
|
23
|
+
// Give DOM dismissal a paint opportunity before requesting Native return.
|
|
24
|
+
// Keep all consumers paused meanwhile, including Ionic change callbacks.
|
|
25
|
+
releaseFrame = win.requestAnimationFrame(() => {
|
|
26
|
+
releaseFrame = win.requestAnimationFrame(() => {
|
|
27
|
+
releaseFrame = 0;
|
|
28
|
+
notify();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
} else if (wasPaused !== paused()) notify();
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
// Without a target, only the document-wide request applies; renderers own local coverage.
|
|
35
|
+
isPaused: (target?: HTMLElement) => paused() || (!!target && overlays.isCovered(target)),
|
|
36
|
+
subscribe(listener: () => void) {
|
|
37
|
+
listeners.add(listener);
|
|
38
|
+
const stopOverlays = overlays.subscribe(listener);
|
|
39
|
+
return () => {
|
|
40
|
+
listeners.delete(listener);
|
|
41
|
+
stopOverlays();
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
pause: () => change(1),
|
|
45
|
+
resume: () => change(-1),
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const getProjectionPause = (doc: Document) => {
|
|
50
|
+
const shared = doc as unknown as Record<symbol, ReturnType<typeof createPause>>;
|
|
51
|
+
return (shared[key] ??= createPause(doc));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Request temporary Web projection across this document. Pair each call with resume(). */
|
|
55
|
+
export const pause = (): void => getProjectionPause(document).pause();
|
|
56
|
+
|
|
57
|
+
/** Release one shared pause, regardless of caller; resume exactly once per pause(). */
|
|
58
|
+
export const resume = (): void => getProjectionPause(document).resume();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Placement supplied synchronously before a native view returns from a Web image. */
|
|
2
|
+
export interface ProjectionPlacementBinding {
|
|
3
|
+
readonly projected: boolean;
|
|
4
|
+
dispose(): void;
|
|
5
|
+
}
|
|
6
|
+
interface PlacementState {
|
|
7
|
+
projected: boolean;
|
|
8
|
+
owner?: object;
|
|
9
|
+
invalidate(): void;
|
|
10
|
+
measure(): Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
// The injected runtime and a plugin's bundled helper share the binding.
|
|
13
|
+
const placementKey = Symbol.for('webview-native-ui-engine.projection-placement');
|
|
14
|
+
type PlacementElement = HTMLElement & { [placementKey]?: PlacementState };
|
|
15
|
+
export const bindProjectionPlacement = (
|
|
16
|
+
element: HTMLElement,
|
|
17
|
+
measure: () => Record<string, unknown>,
|
|
18
|
+
invalidate: () => void = () => {},
|
|
19
|
+
): ProjectionPlacementBinding => {
|
|
20
|
+
const host = element as PlacementElement;
|
|
21
|
+
const state: PlacementState = { projected: false, measure, invalidate };
|
|
22
|
+
host[placementKey] = state;
|
|
23
|
+
return {
|
|
24
|
+
get projected() {
|
|
25
|
+
return host[placementKey] === state && state.projected;
|
|
26
|
+
},
|
|
27
|
+
dispose() {
|
|
28
|
+
if (host[placementKey] === state) delete host[placementKey];
|
|
29
|
+
state.projected = false;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
export const projectionPlacement = (element: HTMLElement | undefined): PlacementState | undefined =>
|
|
34
|
+
(element as PlacementElement | undefined)?.[placementKey];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** A fixed Web fallback element. A replacement element requires a new scope. */
|
|
2
|
+
export const createWebProjectionScope = (element: HTMLElement, namespace: string) => {
|
|
3
|
+
if (element.nodeType !== 1 || element.namespaceURI !== 'http://www.w3.org/1999/xhtml' || !element.ownerDocument || !('style' in element))
|
|
4
|
+
throw new TypeError('A Web projection scope requires an HTMLElement fallback');
|
|
5
|
+
if (
|
|
6
|
+
element.matches(
|
|
7
|
+
'img, input, textarea, select, video, audio, canvas, iframe, embed, object, br, hr, area, base, link, meta, source, track, wbr',
|
|
8
|
+
)
|
|
9
|
+
)
|
|
10
|
+
throw new TypeError('A Web projection fallback must be a container element; use the existing fallback container');
|
|
11
|
+
return Object.freeze({ element, namespace });
|
|
12
|
+
};
|
|
13
|
+
export type WebProjectionScope = ReturnType<typeof createWebProjectionScope>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Image presentation only; Native/Web ownership styling stays with the renderer. */
|
|
2
|
+
const hidden = ':is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content])';
|
|
3
|
+
|
|
4
|
+
export const snapshotStyles = `
|
|
5
|
+
@layer native-ui-projection {
|
|
6
|
+
${hidden}, ${hidden} *, ${hidden}::part(native) { visibility: hidden !important; }
|
|
7
|
+
${hidden}::before, ${hidden}::after,
|
|
8
|
+
${hidden} *::before, ${hidden} *::after {
|
|
9
|
+
visibility: hidden !important;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
[data-native-ui-snapshot-visible][data-native-ui-snapshot-positioned] { position:relative!important; }
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
export const snapshotShadowStyles = `
|
|
16
|
+
@layer native-ui-projection {
|
|
17
|
+
:host([data-native-ui-snapshot-content]),
|
|
18
|
+
:host([data-native-ui-snapshot-content])::before,
|
|
19
|
+
:host([data-native-ui-snapshot-content])::after,
|
|
20
|
+
:host([data-native-ui-snapshot-content]) *,
|
|
21
|
+
:host([data-native-ui-snapshot-content]) *::before,
|
|
22
|
+
:host([data-native-ui-snapshot-content]) *::after {
|
|
23
|
+
visibility: hidden !important;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createWebProjectionBatch, type WebProjectionOptions } from './web.js';
|
|
2
|
+
import { createWebProjectionScope, type WebProjectionScope } from './scope.js';
|
|
3
|
+
import type { WebProjectionSource, WebProjectionTransport } from '../types.js';
|
|
4
|
+
|
|
5
|
+
export interface WebProjectionSurfaceOptions extends WebProjectionOptions {
|
|
6
|
+
namespace: string;
|
|
7
|
+
target(source: WebProjectionSource): HTMLElement | undefined;
|
|
8
|
+
/** Only renderer-owned fallback hosts may move into another overlay layer. */
|
|
9
|
+
canRelocate?(target: HTMLElement): boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A renderer supplies placement policy; the Engine owns the complete image handoff. */
|
|
13
|
+
export const createWebProjectionSurface = async (transport: WebProjectionTransport, options: WebProjectionSurfaceOptions) => {
|
|
14
|
+
const scopes = new WeakMap<HTMLElement, WebProjectionScope>();
|
|
15
|
+
const moved = new Map<HTMLElement, { parent: Node; next: ChildNode | null; layer: HTMLElement }>();
|
|
16
|
+
const restorePlacement = () => {
|
|
17
|
+
for (const [element, saved] of [...moved].reverse()) {
|
|
18
|
+
if (element.parentNode === saved.layer && saved.parent.isConnected)
|
|
19
|
+
saved.parent.insertBefore(element, saved.next?.parentNode === saved.parent ? saved.next : null);
|
|
20
|
+
}
|
|
21
|
+
moved.clear();
|
|
22
|
+
};
|
|
23
|
+
const projection = await createWebProjectionBatch(transport, {
|
|
24
|
+
...options,
|
|
25
|
+
wake: () => {
|
|
26
|
+
if (!projection.token && !projection.restoring) restorePlacement();
|
|
27
|
+
options.wake();
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
let latest: WebProjectionSource[] = [];
|
|
31
|
+
const { consider, begin, updated, invalidate, destroy } = projection;
|
|
32
|
+
return Object.assign(projection, {
|
|
33
|
+
consider(revision: number, sources: WebProjectionSource[]) {
|
|
34
|
+
latest = sources;
|
|
35
|
+
consider(
|
|
36
|
+
revision,
|
|
37
|
+
sources.flatMap((source) => {
|
|
38
|
+
const target = options.target(source);
|
|
39
|
+
if (!target?.isConnected || target.ownerDocument !== source.element.ownerDocument) return [];
|
|
40
|
+
let scope = scopes.get(target);
|
|
41
|
+
if (!scope) {
|
|
42
|
+
scope = createWebProjectionScope(target, options.namespace);
|
|
43
|
+
scopes.set(target, scope);
|
|
44
|
+
}
|
|
45
|
+
return [{ ...source, scope }];
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
begin(layer?: HTMLElement) {
|
|
50
|
+
if (layer && !layer.isConnected) return;
|
|
51
|
+
if (layer && !projection.token) {
|
|
52
|
+
for (const source of latest) {
|
|
53
|
+
if (!projection.preparedOwners.includes(source.element)) continue;
|
|
54
|
+
const target = options.target(source);
|
|
55
|
+
if (
|
|
56
|
+
!target?.isConnected ||
|
|
57
|
+
!target.parentNode ||
|
|
58
|
+
target.ownerDocument !== layer.ownerDocument ||
|
|
59
|
+
layer.contains(target) ||
|
|
60
|
+
target.contains(layer) ||
|
|
61
|
+
!options.canRelocate?.(target)
|
|
62
|
+
)
|
|
63
|
+
continue;
|
|
64
|
+
if (!moved.has(target)) moved.set(target, { parent: target.parentNode, next: target.nextSibling, layer });
|
|
65
|
+
layer.append(target);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
begin();
|
|
69
|
+
if (!projection.token) restorePlacement();
|
|
70
|
+
},
|
|
71
|
+
updated(token: string | undefined, ids?: string[], revision?: number) {
|
|
72
|
+
updated(token, ids, revision);
|
|
73
|
+
if (!projection.restoring && !projection.token) restorePlacement();
|
|
74
|
+
},
|
|
75
|
+
invalidate(nativeHidden?: boolean) {
|
|
76
|
+
invalidate(nativeHidden);
|
|
77
|
+
restorePlacement();
|
|
78
|
+
},
|
|
79
|
+
async destroy() {
|
|
80
|
+
try {
|
|
81
|
+
await destroy();
|
|
82
|
+
} finally {
|
|
83
|
+
restorePlacement();
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
export type WebProjectionSurface = Awaited<ReturnType<typeof createWebProjectionSurface>>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { WebProjectionSurface } from './surface.js';
|
|
2
|
+
|
|
3
|
+
/** Owns asynchronous projection creation and disposal for DOM-backed surfaces. */
|
|
4
|
+
export const createWebProjectionSurfaces = <Owner extends Node, Surface extends WebProjectionSurface>(
|
|
5
|
+
options: { ignoreTarget?(element: Element): boolean } = {},
|
|
6
|
+
) => {
|
|
7
|
+
type Entry = { surface?: Surface; ready: Promise<Surface | undefined> };
|
|
8
|
+
const entries = new Map<Owner, Entry>();
|
|
9
|
+
const retiring = new Set<Promise<unknown>>();
|
|
10
|
+
const retire = (disposal: Promise<unknown>) => {
|
|
11
|
+
const pending = disposal.finally(() => retiring.delete(pending));
|
|
12
|
+
retiring.add(pending);
|
|
13
|
+
return pending;
|
|
14
|
+
};
|
|
15
|
+
let stopped = false;
|
|
16
|
+
const values = (): Surface[] => [...entries.values()].flatMap((entry) => (entry.surface ? [entry.surface] : []));
|
|
17
|
+
let doc: Document | undefined;
|
|
18
|
+
let listeners: AbortController | undefined;
|
|
19
|
+
let motionFrame = 0;
|
|
20
|
+
const invalidate = () => values().forEach((surface) => surface.invalidate());
|
|
21
|
+
const changed = (owner?: EventTarget) => values().forEach((surface) => surface.changed(owner));
|
|
22
|
+
const observe = (owner: Owner) => {
|
|
23
|
+
const ownerDocument = owner.nodeType === 9 ? (owner as unknown as Document) : owner.ownerDocument!;
|
|
24
|
+
if (doc && doc !== ownerDocument) throw new TypeError('Projection surfaces must share one Document');
|
|
25
|
+
if (doc) return;
|
|
26
|
+
doc = ownerDocument;
|
|
27
|
+
const win = doc.defaultView!;
|
|
28
|
+
listeners = new win.AbortController();
|
|
29
|
+
const subscription = { signal: listeners.signal };
|
|
30
|
+
win.addEventListener('resize', invalidate, subscription);
|
|
31
|
+
win.addEventListener('orientationchange', invalidate, subscription);
|
|
32
|
+
doc.addEventListener(
|
|
33
|
+
'visibilitychange',
|
|
34
|
+
() => {
|
|
35
|
+
if (doc!.hidden) invalidate();
|
|
36
|
+
},
|
|
37
|
+
subscription,
|
|
38
|
+
);
|
|
39
|
+
const appearance = (event: Event) => {
|
|
40
|
+
const target = event.target instanceof win.Element ? event.target : undefined;
|
|
41
|
+
if (target?.closest('style[data-native-ui-snapshot], [data-native-ui-projection-node]') || (target && options.ignoreTarget?.(target)))
|
|
42
|
+
return;
|
|
43
|
+
if (target?.hasAttribute('data-native-ui-snapshot') && (event as TransitionEvent).pseudoElement) return;
|
|
44
|
+
changed();
|
|
45
|
+
};
|
|
46
|
+
for (const name of ['load', 'transitionrun', 'transitionend', 'transitioncancel', 'animationstart', 'animationend', 'animationcancel'])
|
|
47
|
+
doc.addEventListener(name, appearance, { ...subscription, capture: name === 'load' });
|
|
48
|
+
};
|
|
49
|
+
const followMotion = (moving: ReadonlyMap<HTMLElement, unknown>, target: (source: HTMLElement) => HTMLElement = (source) => source) => {
|
|
50
|
+
if (motionFrame || stopped || !moving.size || !doc?.defaultView) return;
|
|
51
|
+
// Ancestor transforms already carry the image. Only the host's own motion needs rebasing.
|
|
52
|
+
const surfaces = values().filter(
|
|
53
|
+
(surface) =>
|
|
54
|
+
(surface.token || surface.restoring) &&
|
|
55
|
+
(surface.restoring ? surface.visibleOwners : surface.owners).some((owner) => moving.has(target(owner))),
|
|
56
|
+
);
|
|
57
|
+
if (!surfaces.length) return;
|
|
58
|
+
motionFrame = doc.defaultView.requestAnimationFrame(() => {
|
|
59
|
+
motionFrame = 0;
|
|
60
|
+
surfaces.forEach((surface) => surface.moved());
|
|
61
|
+
followMotion(moving, target);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const remove = async (owner: Owner) => {
|
|
65
|
+
const entry = entries.get(owner);
|
|
66
|
+
if (!entry) return;
|
|
67
|
+
entries.delete(owner);
|
|
68
|
+
await retire(entry.surface ? entry.surface.destroy() : entry.ready.catch(() => {}));
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
get: (owner: Owner) => entries.get(owner)?.surface,
|
|
72
|
+
keys: () => entries.keys(),
|
|
73
|
+
values,
|
|
74
|
+
changed,
|
|
75
|
+
invalidate,
|
|
76
|
+
followMotion,
|
|
77
|
+
end: () => values().forEach((surface) => surface.end()),
|
|
78
|
+
consider: (...args: Parameters<WebProjectionSurface['consider']>) => values().forEach((surface) => surface.consider(...args)),
|
|
79
|
+
updated: (...args: Parameters<WebProjectionSurface['updated']>) => values().forEach((surface) => surface.updated(...args)),
|
|
80
|
+
get restoring() {
|
|
81
|
+
return values().some((surface) => surface.restoring);
|
|
82
|
+
},
|
|
83
|
+
get handoffPending() {
|
|
84
|
+
return values().some((surface) => surface.handoffPending);
|
|
85
|
+
},
|
|
86
|
+
ensure(owner: Owner, create: () => Promise<Surface>): Promise<Surface | undefined> {
|
|
87
|
+
if (stopped || !owner.isConnected) return Promise.resolve(undefined);
|
|
88
|
+
observe(owner);
|
|
89
|
+
const existing = entries.get(owner);
|
|
90
|
+
if (existing) return existing.ready;
|
|
91
|
+
const entry: Entry = {
|
|
92
|
+
ready: Promise.resolve()
|
|
93
|
+
.then(create)
|
|
94
|
+
.then(async (surface) => {
|
|
95
|
+
if (stopped || entries.get(owner) !== entry || !owner.isConnected) {
|
|
96
|
+
if (entries.get(owner) === entry) entries.delete(owner);
|
|
97
|
+
await retire(surface.destroy());
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
entry.surface = surface;
|
|
101
|
+
return surface;
|
|
102
|
+
})
|
|
103
|
+
.catch((error) => {
|
|
104
|
+
if (entries.get(owner) === entry) entries.delete(owner);
|
|
105
|
+
throw error;
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
entries.set(owner, entry);
|
|
109
|
+
return entry.ready;
|
|
110
|
+
},
|
|
111
|
+
delete: remove,
|
|
112
|
+
async destroy() {
|
|
113
|
+
stopped = true;
|
|
114
|
+
listeners?.abort();
|
|
115
|
+
if (motionFrame) doc?.defaultView?.cancelAnimationFrame(motionFrame);
|
|
116
|
+
await Promise.all([...retiring, ...[...entries.keys()].map(remove)]);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
};
|