@webview-native-ui/engine 0.0.0-stage → 0.0.1
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/LICENSE +18 -0
- package/NOTICE +39 -0
- package/README.md +623 -2
- package/WebViewNativeUIEngine.podspec +17 -0
- package/dist/bridge.d.ts +4 -0
- package/dist/bridge.js +15 -0
- package/dist/geometry.d.ts +2 -0
- package/dist/geometry.js +25 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/ionic.d.ts +16 -0
- package/dist/ionic.js +31 -0
- package/dist/native-bootstrap.d.ts +8 -0
- package/dist/native-bootstrap.js +138 -0
- package/dist/native-session.d.ts +48 -0
- package/dist/native-session.js +519 -0
- package/dist/overlays.d.ts +42 -0
- package/dist/overlays.js +181 -0
- package/dist/projection-events.d.ts +12 -0
- package/dist/projection-events.js +44 -0
- package/dist/projection-placement.d.ts +14 -0
- package/dist/projection-placement.js +18 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +11 -0
- package/dist/scroll-bindings.d.ts +11 -0
- package/dist/scroll-bindings.js +55 -0
- package/dist/scroll-placement.d.ts +5 -0
- package/dist/scroll-placement.js +85 -0
- package/dist/snapshot-styles.d.ts +2 -0
- package/dist/snapshot-styles.js +14 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.js +1 -0
- package/dist/visibility.d.ts +6 -0
- package/dist/visibility.js +29 -0
- package/dist/web-projection-scope.d.ts +6 -0
- package/dist/web-projection-scope.js +8 -0
- package/dist/web-projection.d.ts +50 -0
- package/dist/web-projection.js +835 -0
- package/docs/cordova.md +125 -0
- package/ios/Sources/WebViewNativeUIEngine/ClippedView.swift +16 -0
- package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +256 -0
- package/ios/Sources/WebViewNativeUIEngine/Frame.swift +15 -0
- package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +311 -0
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +14 -0
- package/ios/Sources/WebViewNativeUIEngine/ScrollBindings.swift +27 -0
- package/ios/Sources/WebViewNativeUIEngine/ScrollTracker.swift +117 -0
- package/package.json +64 -4
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
3
|
+
Pod::Spec.new do |s|
|
|
4
|
+
s.name = 'WebViewNativeUIEngine'
|
|
5
|
+
s.version = package['version']
|
|
6
|
+
s.summary = 'Native UI infrastructure for Capacitor and Cordova plugin maintainers.'
|
|
7
|
+
s.homepage = 'https://github.com/webview-native-ui/engine'
|
|
8
|
+
s.license = { :type => 'Zlib', :file => 'LICENSE' }
|
|
9
|
+
s.preserve_paths = 'LICENSE', 'NOTICE'
|
|
10
|
+
s.author = { 'rdlabo' => 'sakakibara@rdlabo.jp' }
|
|
11
|
+
s.source = { :git => 'https://github.com/webview-native-ui/engine.git', :tag => "v#{s.version}" }
|
|
12
|
+
s.resource_bundles = { 'WebViewNativeUIEngine' => ['ios/Sources/WebViewNativeUIEngine/Resources/*.js'] }
|
|
13
|
+
s.source_files = 'ios/Sources/WebViewNativeUIEngine/**/*.swift'
|
|
14
|
+
s.ios.deployment_target = '15.0'
|
|
15
|
+
s.swift_version = '5.9'
|
|
16
|
+
s.frameworks = 'UIKit', 'WebKit'
|
|
17
|
+
end
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ListenerHandle as PluginListenerHandle } from './types.js';
|
|
2
|
+
export declare const bounded: <T>(promise: Promise<T>) => Promise<T>;
|
|
3
|
+
/** A timed-out registration still owns a native resource if it eventually succeeds. */
|
|
4
|
+
export declare const boundedListener: (registration: Promise<PluginListenerHandle>) => Promise<PluginListenerHandle>;
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// A failed bridge must not leave the source inaccessible indefinitely.
|
|
2
|
+
export const bounded = (promise) => new Promise((resolve, reject) => {
|
|
3
|
+
const timer = setTimeout(() => reject(new Error('Native UI bridge timed out')), 5000);
|
|
4
|
+
promise.then(resolve, reject).finally(() => clearTimeout(timer));
|
|
5
|
+
});
|
|
6
|
+
/** A timed-out registration still owns a native resource if it eventually succeeds. */
|
|
7
|
+
export const boundedListener = async (registration) => {
|
|
8
|
+
try {
|
|
9
|
+
return await bounded(registration);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
void registration.then((listener) => listener.remove()).catch(() => { });
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
};
|
package/dist/geometry.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function* projectionAncestors(element) {
|
|
2
|
+
let current = element;
|
|
3
|
+
while (current) {
|
|
4
|
+
const root = current.getRootNode();
|
|
5
|
+
current = current.assignedSlot ?? current.parentElement ?? (root instanceof ShadowRoot ? root.host : null);
|
|
6
|
+
if (current)
|
|
7
|
+
yield current;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export const fitsClip = (element) => {
|
|
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)
|
|
15
|
+
return false;
|
|
16
|
+
const bounds = ancestor.getBoundingClientRect();
|
|
17
|
+
const left = bounds.left + ancestor.clientLeft;
|
|
18
|
+
const top = bounds.top + ancestor.clientTop;
|
|
19
|
+
if (/^(hidden|clip|scroll|auto)$/.test(style.overflowX) && (rect.left < left - 1 || rect.right > left + ancestor.clientWidth + 1))
|
|
20
|
+
return false;
|
|
21
|
+
if (/^(hidden|clip|scroll|auto)$/.test(style.overflowY) && (rect.top < top - 1 || rect.bottom > top + ancestor.clientHeight + 1))
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './geometry.js';
|
|
3
|
+
export * from './scroll-placement.js';
|
|
4
|
+
export { createWebProjectionScope, type WebProjectionScope } from './web-projection-scope.js';
|
|
5
|
+
export * from './web-projection.js';
|
|
6
|
+
export * from './scroll-bindings.js';
|
|
7
|
+
export type { ProjectionEvents, ProjectionDestination } from './projection-events.js';
|
|
8
|
+
export { getOverlayCoordinator, usesOverlayBackdrop, type OverlayChange, type OverlayOptions } from './overlays.js';
|
|
9
|
+
export { getUnprojectedVisibility } from './visibility.js';
|
|
10
|
+
export { bindProjectionPlacement, type ProjectionPlacementBinding } from './projection-placement.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './geometry.js';
|
|
3
|
+
export * from './scroll-placement.js';
|
|
4
|
+
export { createWebProjectionScope } from './web-projection-scope.js';
|
|
5
|
+
export * from './web-projection.js';
|
|
6
|
+
export * from './scroll-bindings.js';
|
|
7
|
+
export { getOverlayCoordinator, usesOverlayBackdrop } from './overlays.js';
|
|
8
|
+
export { getUnprojectedVisibility } from './visibility.js';
|
|
9
|
+
export { bindProjectionPlacement } from './projection-placement.js';
|
package/dist/ionic.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type OverlayDefinition, type OverlayOptions } from './overlays.js';
|
|
2
|
+
export type { OverlayOptions as IonicOverlayOptions } from './overlays.js';
|
|
3
|
+
/** Call once at app startup. Repeated default calls share the existing configuration and monitor. */
|
|
4
|
+
export declare const enableSupportIonicOverlays: (options?: OverlayOptions, doc?: Document | undefined) => {
|
|
5
|
+
readonly active: ReadonlySet<HTMLElement>;
|
|
6
|
+
subscribe(listener: (change: import("./overlays.js").OverlayChange) => void): () => void;
|
|
7
|
+
setNative(element: HTMLElement, value: boolean): void;
|
|
8
|
+
isExcluded(element: HTMLElement): boolean;
|
|
9
|
+
isNative(element: HTMLElement): boolean;
|
|
10
|
+
readonly stack: HTMLElement[];
|
|
11
|
+
isCovered(target: HTMLElement): boolean;
|
|
12
|
+
enable(defaults: readonly OverlayDefinition[], config?: OverlayOptions): void;
|
|
13
|
+
} | undefined;
|
|
14
|
+
/** The background surface shares the same order used for registered DOM projections. */
|
|
15
|
+
export declare const ionicModalStack: (doc: Document) => HTMLElement[];
|
|
16
|
+
export declare const coveredIonicSurface: (overlay: HTMLElement) => HTMLElement | undefined;
|
package/dist/ionic.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getOverlayCoordinator } from './overlays.js';
|
|
2
|
+
const defaults = [
|
|
3
|
+
...['Modal', 'Popover', 'Alert', 'ActionSheet', 'Loading', 'Picker', 'Toast'].map((name) => ({
|
|
4
|
+
selector: `ion-${name.replace(/[A-Z]/g, (letter, index) => (index ? '-' : '') + letter.toLowerCase())}`,
|
|
5
|
+
events: {
|
|
6
|
+
willPresent: `ion${name}WillPresent`,
|
|
7
|
+
didPresent: `ion${name}DidPresent`,
|
|
8
|
+
willDismiss: `ion${name}WillDismiss`,
|
|
9
|
+
didDismiss: `ion${name}DidDismiss`,
|
|
10
|
+
},
|
|
11
|
+
})),
|
|
12
|
+
{ selector: 'ion-modal', events: { change: 'ionBreakpointDidChange' } },
|
|
13
|
+
{
|
|
14
|
+
selector: 'ion-menu',
|
|
15
|
+
events: { willPresent: 'ionWillOpen', didPresent: 'ionDidOpen', willDismiss: 'ionWillClose', didDismiss: 'ionDidClose' },
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
/** Call once at app startup. Repeated default calls share the existing configuration and monitor. */
|
|
19
|
+
export const enableSupportIonicOverlays = (options, doc = globalThis.document) => {
|
|
20
|
+
if (!doc)
|
|
21
|
+
return;
|
|
22
|
+
const coordinator = getOverlayCoordinator(doc);
|
|
23
|
+
coordinator.enable(defaults, options);
|
|
24
|
+
return coordinator;
|
|
25
|
+
};
|
|
26
|
+
/** The background surface shares the same order used for registered DOM projections. */
|
|
27
|
+
export const ionicModalStack = (doc) => getOverlayCoordinator(doc).stack.filter((element) => element.matches('ion-modal'));
|
|
28
|
+
export const coveredIonicSurface = (overlay) => {
|
|
29
|
+
const stack = ionicModalStack(overlay.ownerDocument);
|
|
30
|
+
return overlay.matches('ion-modal') ? stack[stack.indexOf(overlay) - 1] : stack[stack.length - 1];
|
|
31
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type NativeSessionConfiguration } from './native-session.js';
|
|
2
|
+
/** Cordova plugins forward their existing notifications through NativeUIProjection.receive. */
|
|
3
|
+
export declare const notify: (id: string, name: string, data: unknown) => void | undefined;
|
|
4
|
+
export declare const receive: (id: string, key: string | null, value: any, error?: string) => void;
|
|
5
|
+
export declare const show: (id: string) => void;
|
|
6
|
+
export declare const hide: (id: string) => boolean;
|
|
7
|
+
export declare const stop: (id: string) => Promise<void>;
|
|
8
|
+
export declare const start: (config: NativeSessionConfiguration) => Promise<void>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { startNativeSession } from './native-session.js';
|
|
2
|
+
const sessions = new Map();
|
|
3
|
+
const pending = new Map();
|
|
4
|
+
const interactions = new Map();
|
|
5
|
+
const documentID = Array.from(crypto.getRandomValues(new Uint32Array(4)), (part) => part.toString(16)).join('-');
|
|
6
|
+
const notifications = new Map();
|
|
7
|
+
/** Cordova plugins forward their existing notifications through NativeUIProjection.receive. */
|
|
8
|
+
export const notify = (id, name, data) => notifications.get(id)?.get(name)?.(data);
|
|
9
|
+
let sequence = 0;
|
|
10
|
+
const send = (id, request) => new Promise((resolve, reject) => {
|
|
11
|
+
const key = `${id}:${documentID}:${++sequence}`;
|
|
12
|
+
const timer = setTimeout(() => {
|
|
13
|
+
pending.delete(key);
|
|
14
|
+
reject(new Error('Native UI session timed out'));
|
|
15
|
+
}, 5000);
|
|
16
|
+
pending.set(key, { resolve, reject, timer });
|
|
17
|
+
const message = JSON.stringify({ key, request });
|
|
18
|
+
const host = globalThis;
|
|
19
|
+
const channel = host.webkit?.messageHandlers?.[id] ?? host[id] ?? host.nativeUIEngine;
|
|
20
|
+
if (!channel) {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
pending.delete(key);
|
|
23
|
+
reject(new Error('Native UI session unavailable'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
channel.postMessage(message);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
pending.delete(key);
|
|
32
|
+
reject(error);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
export const receive = (id, key, value, error) => {
|
|
36
|
+
if (key === null) {
|
|
37
|
+
interactions.get(id)?.(value);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const item = pending.get(key);
|
|
41
|
+
if (!item || !key.startsWith(`${id}:`))
|
|
42
|
+
return;
|
|
43
|
+
pending.delete(key);
|
|
44
|
+
clearTimeout(item.timer);
|
|
45
|
+
if (error)
|
|
46
|
+
item.reject(new Error(error));
|
|
47
|
+
else
|
|
48
|
+
item.resolve(value);
|
|
49
|
+
};
|
|
50
|
+
export const show = (id) => {
|
|
51
|
+
sessions.get(id)?.show();
|
|
52
|
+
};
|
|
53
|
+
export const hide = (id) => {
|
|
54
|
+
sessions.get(id)?.hide();
|
|
55
|
+
return true;
|
|
56
|
+
};
|
|
57
|
+
export const stop = async (id) => {
|
|
58
|
+
const session = sessions.get(id);
|
|
59
|
+
sessions.delete(id);
|
|
60
|
+
notifications.delete(id);
|
|
61
|
+
interactions.delete(id);
|
|
62
|
+
for (const [key, item] of pending) {
|
|
63
|
+
if (!key.startsWith(`${id}:`))
|
|
64
|
+
continue;
|
|
65
|
+
pending.delete(key);
|
|
66
|
+
clearTimeout(item.timer);
|
|
67
|
+
item.reject(new Error('Native UI session closed'));
|
|
68
|
+
}
|
|
69
|
+
await session?.destroy();
|
|
70
|
+
};
|
|
71
|
+
export const start = async (config) => {
|
|
72
|
+
if (sessions.has(config.id))
|
|
73
|
+
return;
|
|
74
|
+
// Reserve before asynchronous initialization, including duplicate Native init callbacks.
|
|
75
|
+
let actual;
|
|
76
|
+
let cancelled = false;
|
|
77
|
+
let pendingDestination;
|
|
78
|
+
const reservation = {
|
|
79
|
+
show: () => {
|
|
80
|
+
pendingDestination = 'native';
|
|
81
|
+
actual?.show();
|
|
82
|
+
},
|
|
83
|
+
hide: () => {
|
|
84
|
+
pendingDestination = 'none';
|
|
85
|
+
actual?.hide();
|
|
86
|
+
},
|
|
87
|
+
destroy: async () => {
|
|
88
|
+
cancelled = true;
|
|
89
|
+
await actual?.destroy();
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
sessions.set(config.id, reservation);
|
|
93
|
+
try {
|
|
94
|
+
if (document.readyState === 'loading')
|
|
95
|
+
await new Promise((resolve) => document.addEventListener('DOMContentLoaded', () => resolve(), { once: true }));
|
|
96
|
+
if (cancelled)
|
|
97
|
+
return;
|
|
98
|
+
const listeners = new Map();
|
|
99
|
+
if (config.runtime === 'cordova')
|
|
100
|
+
notifications.set(config.id, listeners);
|
|
101
|
+
actual = await startNativeSession(config, {
|
|
102
|
+
send: (request) => send(config.id, request),
|
|
103
|
+
listen: async (listener) => {
|
|
104
|
+
interactions.set(config.id, listener);
|
|
105
|
+
return {
|
|
106
|
+
remove: () => {
|
|
107
|
+
if (interactions.get(config.id) === listener)
|
|
108
|
+
interactions.delete(config.id);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
}, config.runtime === 'cordova'
|
|
113
|
+
? {
|
|
114
|
+
addListener: async (name, listener) => {
|
|
115
|
+
listeners.set(name, listener);
|
|
116
|
+
return {
|
|
117
|
+
remove: () => {
|
|
118
|
+
listeners.delete(name);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
: undefined);
|
|
124
|
+
if (pendingDestination === 'none')
|
|
125
|
+
actual.hide();
|
|
126
|
+
else if (pendingDestination === 'native')
|
|
127
|
+
actual.show();
|
|
128
|
+
if (cancelled)
|
|
129
|
+
await actual.destroy();
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (sessions.get(config.id) === reservation) {
|
|
133
|
+
sessions.delete(config.id);
|
|
134
|
+
notifications.delete(config.id);
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type ProjectionDestination } from './projection-events.js';
|
|
2
|
+
import type { ScrollPlacement, ListenerHandle, WebProjectionRequest } from './types.js';
|
|
3
|
+
/** Serialized by the native library. One registration owns one fixed fallback container. */
|
|
4
|
+
export interface NativeSessionConfiguration {
|
|
5
|
+
id: string;
|
|
6
|
+
plugin: string;
|
|
7
|
+
runtime?: 'capacitor' | 'cordova';
|
|
8
|
+
projectionSelector: string;
|
|
9
|
+
/** Recovery while native is requested. Explicit web/none destinations take precedence. */
|
|
10
|
+
failedFallback?: 'web' | 'native';
|
|
11
|
+
captureMode?: 'composited' | 'view';
|
|
12
|
+
/** Native registration accepts a final placement in its reveal request. */
|
|
13
|
+
placementHandoff?: boolean;
|
|
14
|
+
receiveListeners?: {
|
|
15
|
+
update?: string[];
|
|
16
|
+
projection?: Partial<Record<ProjectionDestination, string[]>>;
|
|
17
|
+
match?: Record<string, string | number | boolean>;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface NativeSessionBridge {
|
|
21
|
+
send(request: WebProjectionRequest | {
|
|
22
|
+
action: 'resume';
|
|
23
|
+
token?: string;
|
|
24
|
+
} | {
|
|
25
|
+
action: 'visibility';
|
|
26
|
+
visible: boolean;
|
|
27
|
+
} | {
|
|
28
|
+
action: 'track';
|
|
29
|
+
viewportWidth: number;
|
|
30
|
+
scroll?: ScrollPlacement;
|
|
31
|
+
} | {
|
|
32
|
+
action: 'connect';
|
|
33
|
+
} | {
|
|
34
|
+
action: 'destination';
|
|
35
|
+
destination: ProjectionDestination;
|
|
36
|
+
}): Promise<any>;
|
|
37
|
+
listen(listener: (event: {
|
|
38
|
+
active: boolean;
|
|
39
|
+
}) => void): Promise<ListenerHandle>;
|
|
40
|
+
}
|
|
41
|
+
/** Native startup calls this; application code does not initialize or wrap the plugin. */
|
|
42
|
+
export declare const startNativeSession: (config: NativeSessionConfiguration, nativeBridge: NativeSessionBridge, plugin?: {
|
|
43
|
+
addListener(name: string, listener: (event: unknown) => void): Promise<ListenerHandle>;
|
|
44
|
+
}) => Promise<{
|
|
45
|
+
destroy: () => Promise<void>;
|
|
46
|
+
show: () => void;
|
|
47
|
+
hide: () => void;
|
|
48
|
+
}>;
|