@webview-native-ui/engine 0.0.3 → 0.0.5
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 +1 -0
- package/dist/dom/visibility.js +1 -0
- package/dist/native/session.d.ts +11 -1
- package/dist/native/session.js +77 -8
- package/dist/projection/scope.js +1 -1
- package/dist/projection/styles.d.ts +2 -2
- package/dist/projection/styles.js +20 -9
- package/dist/projection/web.d.ts +3 -1
- package/dist/projection/web.js +273 -176
- package/docs/capacitor-plugin.md +4 -0
- package/docs/cordova-plugin.md +4 -0
- package/docs/projection-containers.md +195 -0
- package/docs/projection.md +28 -4
- package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +31 -14
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +20 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@ The current API is pre-1.0. Use matching, pinned Engine versions across npm and
|
|
|
44
44
|
|
|
45
45
|
## Further reading
|
|
46
46
|
|
|
47
|
+
- [Projection container support](docs/projection-containers.md) — supported HTML and Ionic destinations, sizing conditions and wrapper examples.
|
|
47
48
|
- [Projection behavior and advanced integration](docs/projection.md) — DOM requirements, lifecycle, fallback policies, optional Ionic support and lower-level APIs.
|
|
48
49
|
- [Development and releases](docs/maintenance.md) — repository structure, validation and publishing.
|
|
49
50
|
|
package/dist/dom/visibility.js
CHANGED
|
@@ -13,6 +13,7 @@ export const getUnprojectedVisibility = (element) => {
|
|
|
13
13
|
for (const attribute of Array.from(node.attributes)) {
|
|
14
14
|
if (attribute.name === 'data-native-ui-snapshot-source' ||
|
|
15
15
|
attribute.name === 'data-native-ui-snapshot-prepared' ||
|
|
16
|
+
attribute.name === 'data-native-ui-snapshot-content' ||
|
|
16
17
|
attribute.name.startsWith('data-native-ui-session-'))
|
|
17
18
|
markers.push({ node, name: attribute.name, value: attribute.value });
|
|
18
19
|
}
|
package/dist/native/session.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ProjectionDestination } from '../projection/events.js';
|
|
2
|
-
import type { ScrollPlacement, ListenerHandle, WebProjectionRequest } from '../types.js';
|
|
2
|
+
import type { Frame, ScrollPlacement, ListenerHandle, WebProjectionRequest } from '../types.js';
|
|
3
3
|
/** Serialized by the native library. One registration owns one fixed fallback container. */
|
|
4
4
|
export interface NativeSessionConfiguration {
|
|
5
5
|
id: string;
|
|
@@ -12,6 +12,8 @@ export interface NativeSessionConfiguration {
|
|
|
12
12
|
captureMode?: 'composited' | 'view';
|
|
13
13
|
/** Native registration accepts a final placement in its reveal request. */
|
|
14
14
|
placementHandoff?: boolean;
|
|
15
|
+
/** Android can cover prepared pixels before a Web scroll event arrives. */
|
|
16
|
+
nativeDragHandoff?: boolean;
|
|
15
17
|
receiveListeners?: {
|
|
16
18
|
update?: string[];
|
|
17
19
|
projection?: Partial<Record<ProjectionDestination, string[]>>;
|
|
@@ -29,6 +31,13 @@ export interface NativeSessionBridge {
|
|
|
29
31
|
action: 'track';
|
|
30
32
|
viewportWidth: number;
|
|
31
33
|
scroll?: ScrollPlacement;
|
|
34
|
+
gesture?: {
|
|
35
|
+
viewport: Frame;
|
|
36
|
+
offsetX: number;
|
|
37
|
+
offsetY: number;
|
|
38
|
+
rangeX: number;
|
|
39
|
+
rangeY: number;
|
|
40
|
+
};
|
|
32
41
|
} | {
|
|
33
42
|
action: 'connect';
|
|
34
43
|
} | {
|
|
@@ -37,6 +46,7 @@ export interface NativeSessionBridge {
|
|
|
37
46
|
}): Promise<any>;
|
|
38
47
|
listen(listener: (event: {
|
|
39
48
|
active: boolean;
|
|
49
|
+
precovered?: string;
|
|
40
50
|
}) => void): Promise<ListenerHandle>;
|
|
41
51
|
}
|
|
42
52
|
/** Native startup calls this; application code does not initialize or wrap the plugin. */
|
package/dist/native/session.js
CHANGED
|
@@ -21,6 +21,7 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
21
21
|
const overlays = getOverlayCoordinator(document);
|
|
22
22
|
const effectiveDestination = () => requestedDestination === 'native' && target && overlays.isCovered(target) ? 'web' : requestedDestination;
|
|
23
23
|
let automaticMotion = false;
|
|
24
|
+
let nativeDragging = false;
|
|
24
25
|
let placementPending = false;
|
|
25
26
|
let nativeScroll = false;
|
|
26
27
|
let scrollOwner;
|
|
@@ -51,17 +52,26 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
51
52
|
}
|
|
52
53
|
};
|
|
53
54
|
const bridge = {
|
|
54
|
-
listen: (listener) => nativeBridge.listen(
|
|
55
|
+
listen: (listener) => nativeBridge.listen((event) => {
|
|
56
|
+
listener(event);
|
|
57
|
+
if (!event.active && nativeDragging) {
|
|
58
|
+
nativeDragging = false;
|
|
59
|
+
endAutomaticMotionLater();
|
|
60
|
+
}
|
|
61
|
+
if (event.precovered)
|
|
62
|
+
adoptNativeCover(event.precovered);
|
|
63
|
+
}),
|
|
55
64
|
send: (request) => {
|
|
56
65
|
if (!config.placementHandoff)
|
|
57
66
|
return nativeBridge.send(request);
|
|
58
67
|
const binding = projectionPlacement(target);
|
|
59
68
|
if (request.action === 'cover') {
|
|
69
|
+
const alreadyCovered = coveredToken === request.token && binding?.owner === placementOwner && binding.projected;
|
|
60
70
|
const epoch = ++placementEpoch;
|
|
61
71
|
coveredToken = request.token;
|
|
62
72
|
if (binding) {
|
|
63
73
|
binding.owner = placementOwner;
|
|
64
|
-
binding.projected =
|
|
74
|
+
binding.projected = !!alreadyCovered;
|
|
65
75
|
}
|
|
66
76
|
return nativeBridge
|
|
67
77
|
.send(request)
|
|
@@ -97,7 +107,18 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
97
107
|
: request;
|
|
98
108
|
return nativeBridge
|
|
99
109
|
.send(payload)
|
|
100
|
-
.then((result) => {
|
|
110
|
+
.then(async (result) => {
|
|
111
|
+
// A native drag can race retirement before its notification reaches JS.
|
|
112
|
+
// Keep the prepared underlay until native has applied a fresh return placement.
|
|
113
|
+
if (request.action === 'release' && result?.requiresPlacement) {
|
|
114
|
+
if (binding && (!binding.owner || binding.owner === placementOwner)) {
|
|
115
|
+
result = await nativeBridge.send({ ...request, placement: binding.measure() });
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
await nativeBridge.send({ action: 'visibility', visible: false });
|
|
119
|
+
result = await nativeBridge.send(request);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
101
122
|
if (epoch === placementEpoch &&
|
|
102
123
|
(request.action === 'resume' || releasing || request.action === 'reset' || request.action === 'visibility'))
|
|
103
124
|
clearPlacement();
|
|
@@ -247,6 +268,7 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
247
268
|
const created = await createWebProjection(createWebProjectionScope(target, config.id), bridge, {
|
|
248
269
|
wake,
|
|
249
270
|
captureDelayMs: config.captureDelayMs,
|
|
271
|
+
retainPreparedUntilRelease: config.nativeDragHandoff,
|
|
250
272
|
blocked: () => destination !== 'native' || automaticMotion || (placementPending && !manager?.token),
|
|
251
273
|
revision: () => revision,
|
|
252
274
|
captureRevision: config.captureMode === 'view' ? () => contentRevision : undefined,
|
|
@@ -282,7 +304,23 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
282
304
|
(element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth));
|
|
283
305
|
});
|
|
284
306
|
const scroll = scrollOwner && readScrollPlacement(target, scrollOwner, () => config.id);
|
|
285
|
-
|
|
307
|
+
// Gesture gating needs a hit region, not native compositor scroll eligibility.
|
|
308
|
+
// Rounded/clipped Ionic hosts can reject readScrollPlacement while still scrolling in Web.
|
|
309
|
+
const gesture = config.nativeDragHandoff && config.captureMode === 'view' && scrollOwner
|
|
310
|
+
? {
|
|
311
|
+
viewport: scrollOwner.getBoundingClientRect(),
|
|
312
|
+
offsetX: scrollOwner.scrollLeft,
|
|
313
|
+
offsetY: scrollOwner.scrollTop,
|
|
314
|
+
rangeX: Math.max(0, scrollOwner.scrollWidth - scrollOwner.clientWidth),
|
|
315
|
+
rangeY: Math.max(0, scrollOwner.scrollHeight - scrollOwner.clientHeight),
|
|
316
|
+
}
|
|
317
|
+
: undefined;
|
|
318
|
+
const tracking = await bridge.send({
|
|
319
|
+
action: 'track',
|
|
320
|
+
viewportWidth: innerWidth,
|
|
321
|
+
...(scroll ? { scroll } : {}),
|
|
322
|
+
...(gesture ? { gesture } : {}),
|
|
323
|
+
});
|
|
286
324
|
if (stopped || version !== generation)
|
|
287
325
|
return;
|
|
288
326
|
nativeScroll = tracking?.tracked === true;
|
|
@@ -356,8 +394,13 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
356
394
|
manager?.moved();
|
|
357
395
|
else
|
|
358
396
|
manager?.changed();
|
|
397
|
+
endAutomaticMotionLater();
|
|
398
|
+
};
|
|
399
|
+
function endAutomaticMotionLater() {
|
|
359
400
|
clearTimeout(scrollTimer);
|
|
360
401
|
scrollTimer = setTimeout(() => {
|
|
402
|
+
if (nativeDragging)
|
|
403
|
+
return;
|
|
361
404
|
automaticMotion = false;
|
|
362
405
|
if (stopped || destination !== 'native')
|
|
363
406
|
return;
|
|
@@ -365,7 +408,31 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
365
408
|
if (!manager?.restoring)
|
|
366
409
|
schedule();
|
|
367
410
|
}, 100);
|
|
368
|
-
}
|
|
411
|
+
}
|
|
412
|
+
function adoptNativeCover(token) {
|
|
413
|
+
if (stopped || destination !== 'native' || !target?.isConnected || !manager)
|
|
414
|
+
return;
|
|
415
|
+
nativeDragging = true;
|
|
416
|
+
// A scroll handler may have adopted the same native cover before its notification arrives.
|
|
417
|
+
if (manager.token)
|
|
418
|
+
return;
|
|
419
|
+
if (restoring || manager.restoring)
|
|
420
|
+
generation++;
|
|
421
|
+
coveredToken = token;
|
|
422
|
+
const binding = projectionPlacement(target);
|
|
423
|
+
if (binding) {
|
|
424
|
+
binding.owner = placementOwner;
|
|
425
|
+
binding.projected = true;
|
|
426
|
+
}
|
|
427
|
+
automaticMotion = true;
|
|
428
|
+
beginProjection();
|
|
429
|
+
if (!manager.token) {
|
|
430
|
+
automaticMotion = false;
|
|
431
|
+
void recoverFailedProjection();
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
endAutomaticMotionLater();
|
|
435
|
+
}
|
|
369
436
|
// Listen on composed-tree ancestors themselves: non-composed scroll events inside
|
|
370
437
|
// Ionic's shadow tree never reach document, and Node.contains excludes slotted children.
|
|
371
438
|
const refreshScrollListeners = () => {
|
|
@@ -408,6 +475,7 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
408
475
|
if (stopped || (!force && next === destination))
|
|
409
476
|
return;
|
|
410
477
|
automaticMotion = false;
|
|
478
|
+
nativeDragging = false;
|
|
411
479
|
clearTimeout(scrollTimer);
|
|
412
480
|
destination = next;
|
|
413
481
|
generation++;
|
|
@@ -419,13 +487,13 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
419
487
|
.then(() => {
|
|
420
488
|
if (destination !== 'none' || version !== generation || stopped)
|
|
421
489
|
return;
|
|
422
|
-
manager?.invalidate();
|
|
490
|
+
manager?.invalidate(true);
|
|
423
491
|
setVisibility();
|
|
424
492
|
})
|
|
425
493
|
.catch(() => {
|
|
426
494
|
if (destination !== 'none' || version !== generation || stopped)
|
|
427
495
|
return;
|
|
428
|
-
manager?.invalidate();
|
|
496
|
+
manager?.invalidate(true);
|
|
429
497
|
setVisibility();
|
|
430
498
|
});
|
|
431
499
|
}
|
|
@@ -507,8 +575,9 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
507
575
|
requestedDestination = destination = 'none';
|
|
508
576
|
generation++;
|
|
509
577
|
automaticMotion = false;
|
|
578
|
+
nativeDragging = false;
|
|
510
579
|
clearTimeout(scrollTimer);
|
|
511
|
-
manager?.invalidate();
|
|
580
|
+
manager?.invalidate(true);
|
|
512
581
|
setVisibility();
|
|
513
582
|
},
|
|
514
583
|
};
|
package/dist/projection/scope.js
CHANGED
|
@@ -3,6 +3,6 @@ export const createWebProjectionScope = (element, namespace) => {
|
|
|
3
3
|
if (element.nodeType !== 1 || element.namespaceURI !== 'http://www.w3.org/1999/xhtml' || !element.ownerDocument || !('style' in element))
|
|
4
4
|
throw new TypeError('A Web projection scope requires an HTMLElement fallback');
|
|
5
5
|
if (element.matches('img, input, textarea, select, video, audio, canvas, iframe, embed, object, br, hr, area, base, link, meta, source, track, wbr'))
|
|
6
|
-
throw new TypeError('A Web projection fallback must
|
|
6
|
+
throw new TypeError('A Web projection fallback must be a container element; use the existing fallback container');
|
|
7
7
|
return Object.freeze({ element, namespace });
|
|
8
8
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const
|
|
1
|
+
export declare const snapshotStyles = "\n@layer native-ui-projection {\n:is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content]), :is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content]) *, :is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content])::part(native) { visibility: hidden !important; }\n:is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content])::before, :is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content])::after,\n:is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content]) *::before, :is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content]) *::after {\n visibility: hidden !important;\n}\n}\n[data-native-ui-snapshot-visible] { opacity:1!important;overflow:visible!important; }\n[data-native-ui-snapshot-visible][data-native-ui-snapshot-positioned] { position:relative!important; }\n";
|
|
2
|
+
export declare const snapshotShadowStyles = "\n@layer native-ui-projection {\n:host([data-native-ui-snapshot-content]),\n:host([data-native-ui-snapshot-content])::before,\n:host([data-native-ui-snapshot-content])::after,\n:host([data-native-ui-snapshot-content]) *,\n:host([data-native-ui-snapshot-content]) *::before,\n:host([data-native-ui-snapshot-content]) *::after {\n visibility: hidden !important;\n}\n}\n";
|
|
@@ -1,14 +1,25 @@
|
|
|
1
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])';
|
|
2
3
|
export const snapshotStyles = `
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
visibility:visible!important;opacity:0!important;will-change:opacity!important;
|
|
10
|
-
pointer-events:none!important;image-rendering:pixelated!important;
|
|
4
|
+
@layer native-ui-projection {
|
|
5
|
+
${hidden}, ${hidden} *, ${hidden}::part(native) { visibility: hidden !important; }
|
|
6
|
+
${hidden}::before, ${hidden}::after,
|
|
7
|
+
${hidden} *::before, ${hidden} *::after {
|
|
8
|
+
visibility: hidden !important;
|
|
9
|
+
}
|
|
11
10
|
}
|
|
12
11
|
[data-native-ui-snapshot-visible] { opacity:1!important;overflow:visible!important; }
|
|
13
|
-
[data-native-ui-snapshot-visible][data-native-ui-snapshot
|
|
12
|
+
[data-native-ui-snapshot-visible][data-native-ui-snapshot-positioned] { position:relative!important; }
|
|
13
|
+
`;
|
|
14
|
+
export const snapshotShadowStyles = `
|
|
15
|
+
@layer native-ui-projection {
|
|
16
|
+
:host([data-native-ui-snapshot-content]),
|
|
17
|
+
:host([data-native-ui-snapshot-content])::before,
|
|
18
|
+
:host([data-native-ui-snapshot-content])::after,
|
|
19
|
+
:host([data-native-ui-snapshot-content]) *,
|
|
20
|
+
:host([data-native-ui-snapshot-content]) *::before,
|
|
21
|
+
:host([data-native-ui-snapshot-content]) *::after {
|
|
22
|
+
visibility: hidden !important;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
14
25
|
`;
|
package/dist/projection/web.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface WebProjectionOptions {
|
|
|
6
6
|
events?: ProjectionEvents;
|
|
7
7
|
/** Debounce before preparing replacement pixels, in milliseconds. Defaults to 50; never delays handoff. */
|
|
8
8
|
captureDelayMs?: number;
|
|
9
|
+
/** Preserve an idle underlay while native disarms a possible early drag cover. */
|
|
10
|
+
retainPreparedUntilRelease?: boolean;
|
|
9
11
|
wake(): void;
|
|
10
12
|
blocked(): boolean;
|
|
11
13
|
revision?(): number;
|
|
@@ -35,7 +37,7 @@ declare const createProjectionEngine: (scope: WebProjectionScope, transport: Web
|
|
|
35
37
|
retireRestoration: () => void;
|
|
36
38
|
end: () => void;
|
|
37
39
|
updated: (token: string | undefined, restoredIDs?: string[], revision?: number) => void;
|
|
38
|
-
invalidate: () => void;
|
|
40
|
+
invalidate: (nativeHidden?: boolean) => void;
|
|
39
41
|
destroy(): Promise<void>;
|
|
40
42
|
}>;
|
|
41
43
|
type Engine = Awaited<ReturnType<typeof createProjectionEngine>>;
|