@webview-native-ui/engine 0.0.6 → 0.0.7
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 +19 -12
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/native/session.d.ts +5 -0
- package/dist/native/session.js +83 -14
- package/dist/native/updates.d.ts +16 -0
- package/dist/native/updates.js +108 -0
- package/dist/native/values.d.ts +5 -0
- package/dist/native/values.js +27 -0
- package/dist/projection/surface.d.ts +35 -0
- package/dist/projection/surface.js +81 -0
- package/dist/projection/surfaces.d.ts +20 -0
- package/dist/projection/surfaces.js +116 -0
- package/docs/README.md +70 -0
- package/docs/advanced/README.md +20 -0
- package/docs/advanced/android-placement.md +48 -0
- package/docs/advanced/native-handoff.md +89 -0
- package/docs/advanced/renderer.md +148 -0
- package/docs/advanced/update-queue.md +58 -0
- package/docs/advanced/web-projection.md +88 -0
- package/docs/assets/ios-native-capture-comparison.png +0 -0
- package/docs/concepts.md +97 -0
- package/docs/configuration/README.md +29 -0
- package/docs/configuration/capture.md +27 -0
- package/docs/configuration/events.md +35 -0
- package/docs/configuration/ionic-overlays.md +65 -0
- package/docs/configuration/values.md +125 -0
- package/docs/cordova.md +1 -1
- package/docs/{capacitor-plugin.md → getting-started/capacitor.md} +45 -47
- package/docs/{cordova-plugin.md → getting-started/cordova.md} +32 -15
- package/docs/getting-started/initialization.md +79 -0
- package/docs/maintenance.md +14 -2
- package/docs/{projection-containers.md → reference/containers.md} +6 -2
- package/docs/reference/platforms.md +137 -0
- package/docs/reference/projection.md +134 -0
- package/docs/reference/state.md +39 -0
- package/ios/Sources/WebViewNativeUIEngine/ClippedView.swift +12 -12
- package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +68 -7
- package/ios/Sources/WebViewNativeUIEngine/Frame.swift +10 -10
- package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +74 -39
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -3
- package/ios/Sources/WebViewNativeUIEngine/ScrollBindings.swift +2 -0
- package/ios/Sources/WebViewNativeUIEngine/ScrollTracker.swift +9 -7
- package/package.json +9 -2
- package/docs/projection.md +0 -368
package/README.md
CHANGED
|
@@ -29,24 +29,31 @@ WebView Native UI Engine makes that work reusable. It lets applications combine
|
|
|
29
29
|
|
|
30
30
|
Use the live native view where it belongs, and a prepared visual projection in the DOM when the Web layer needs to own presentation. Restore the interactive native view when it can take over again. An existing DOM container gives the Web projection its place in the application's layout and layer structure.
|
|
31
31
|
|
|
32
|
-
The Engine coordinates this handoff; the integrating renderer still creates and positions its views. A projected image preserves appearance, not live interaction or video. Capture availability and layout support determine when a projection can be used, with an explicit fallback when it cannot. See [
|
|
32
|
+
The Engine coordinates this handoff; the integrating renderer still creates and positions its views. A projected image preserves appearance, not live interaction or video. Capture availability and layout support determine when a projection can be used, with an explicit fallback when it cannot. See [concepts and limits](docs/concepts.md) for the detailed contract and limits.
|
|
33
33
|
|
|
34
|
-
##
|
|
34
|
+
## Choose your starting point
|
|
35
35
|
|
|
36
|
-
Applications gain these capabilities through plugins that integrate the Engine.
|
|
36
|
+
Applications gain these capabilities through plugins that integrate the Engine.
|
|
37
|
+
Plugin authors retain their existing native controls and public APIs; the Engine
|
|
38
|
+
supplies the coordination between native and Web presentation. It supports iOS
|
|
39
|
+
and Android with Capacitor or Cordova. Ionic and Angular are not required.
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
| Start here | Purpose |
|
|
42
|
+
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
43
|
+
| [Documentation](docs/README.md) | Reading order: basic initialization, optional behavior, advanced APIs and reference. |
|
|
44
|
+
| [Concepts and limits](docs/concepts.md) | Decide whether projection fits your component. |
|
|
45
|
+
| [Basic initialization](docs/getting-started/initialization.md) | Minimum setup and required inputs, before framework-specific integration. |
|
|
40
46
|
|
|
41
|
-
Native registration starts the Web runtime;
|
|
47
|
+
Native registration starts the Web runtime; the base integration needs no separate
|
|
48
|
+
application-side Engine initializer. The API is pre-1.0: pin matching Engine
|
|
49
|
+
versions across npm and native dependencies. Android placement requires API 23+,
|
|
50
|
+
with composited snapshots available from API 26+.
|
|
42
51
|
|
|
43
|
-
|
|
52
|
+
## Reference and contributing
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- [Projection behavior and advanced integration](docs/projection.md) — DOM requirements, lifecycle, fallback policies, optional Ionic support and lower-level APIs.
|
|
49
|
-
- [Development and releases](docs/maintenance.md) — repository structure, validation and publishing.
|
|
54
|
+
The [documentation index](docs/README.md) links to configuration, advanced APIs,
|
|
55
|
+
container support and platform contracts. For repository development and publishing,
|
|
56
|
+
see [development and releases](docs/maintenance.md).
|
|
50
57
|
|
|
51
58
|
## License
|
|
52
59
|
|
package/dist/index.d.ts
CHANGED
|
@@ -8,3 +8,6 @@ export type { ProjectionEvents, ProjectionDestination } from './projection/event
|
|
|
8
8
|
export { getOverlayCoordinator, usesOverlayBackdrop, type OverlayChange, type OverlayOptions } from './dom/overlays.js';
|
|
9
9
|
export { getUnprojectedVisibility } from './dom/visibility.js';
|
|
10
10
|
export { bindProjectionPlacement, type ProjectionPlacementBinding } from './projection/placement.js';
|
|
11
|
+
export { createWebProjectionSurfaces } from './projection/surfaces.js';
|
|
12
|
+
export * from './projection/surface.js';
|
|
13
|
+
export { createNativeUpdateQueue, type NativeUpdateSender } from './native/updates.js';
|
package/dist/index.js
CHANGED
|
@@ -7,3 +7,6 @@ export * from './dom/scroll-bindings.js';
|
|
|
7
7
|
export { getOverlayCoordinator, usesOverlayBackdrop } from './dom/overlays.js';
|
|
8
8
|
export { getUnprojectedVisibility } from './dom/visibility.js';
|
|
9
9
|
export { bindProjectionPlacement } from './projection/placement.js';
|
|
10
|
+
export { createWebProjectionSurfaces } from './projection/surfaces.js';
|
|
11
|
+
export * from './projection/surface.js';
|
|
12
|
+
export { createNativeUpdateQueue } from './native/updates.js';
|
package/dist/native/session.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type NativeValueBinding } from './values.js';
|
|
1
2
|
import { type ProjectionDestination } from '../projection/events.js';
|
|
2
3
|
import type { Frame, ScrollPlacement, ListenerHandle, WebProjectionRequest } from '../types.js';
|
|
3
4
|
/** Serialized by the native library. One registration owns one fixed fallback container. */
|
|
@@ -7,6 +8,8 @@ export interface NativeSessionConfiguration {
|
|
|
7
8
|
runtime?: 'capacitor' | 'cordova';
|
|
8
9
|
projectionSelector: string;
|
|
9
10
|
captureDelayMs?: number;
|
|
11
|
+
/** Value reflection is opt-in; native supplies the current values. */
|
|
12
|
+
values?: Record<string, NativeValueBinding>;
|
|
10
13
|
/** Recovery while native is requested. Explicit web/none destinations take precedence. */
|
|
11
14
|
failedFallback?: 'web' | 'native';
|
|
12
15
|
captureMode?: 'composited' | 'view';
|
|
@@ -40,6 +43,8 @@ export interface NativeSessionBridge {
|
|
|
40
43
|
};
|
|
41
44
|
} | {
|
|
42
45
|
action: 'connect';
|
|
46
|
+
} | {
|
|
47
|
+
action: 'values';
|
|
43
48
|
} | {
|
|
44
49
|
action: 'destination';
|
|
45
50
|
destination: ProjectionDestination;
|
package/dist/native/session.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyNativeValues } from './values.js';
|
|
1
2
|
import { projectionPlacement } from '../projection/placement.js';
|
|
2
3
|
import { getOverlayCoordinator } from '../dom/overlays.js';
|
|
3
4
|
import { readScrollPlacement } from '../dom/scroll-placement.js';
|
|
@@ -39,6 +40,46 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
39
40
|
}
|
|
40
41
|
};
|
|
41
42
|
let target;
|
|
43
|
+
let valueRequest = 0;
|
|
44
|
+
let cancelValueWait;
|
|
45
|
+
const retireValueRead = () => {
|
|
46
|
+
valueRequest++;
|
|
47
|
+
cancelValueWait?.();
|
|
48
|
+
cancelValueWait = undefined;
|
|
49
|
+
};
|
|
50
|
+
const refreshValues = async () => {
|
|
51
|
+
if (!config.values || !target || stopped)
|
|
52
|
+
return;
|
|
53
|
+
const element = target;
|
|
54
|
+
retireValueRead();
|
|
55
|
+
const request = valueRequest;
|
|
56
|
+
const current = () => !stopped && request === valueRequest && target === element && findTarget() === element && element.isConnected;
|
|
57
|
+
// Defining a custom element installs its setters. Earlier assignment would shadow them.
|
|
58
|
+
if (element.localName.includes('-') &&
|
|
59
|
+
!element.matches(':defined') &&
|
|
60
|
+
Object.values(config.values).some((binding) => binding.property)) {
|
|
61
|
+
await Promise.race([
|
|
62
|
+
element.ownerDocument.defaultView.customElements.whenDefined(element.localName),
|
|
63
|
+
new Promise((resolve) => {
|
|
64
|
+
cancelValueWait = resolve;
|
|
65
|
+
}),
|
|
66
|
+
]);
|
|
67
|
+
if (!current())
|
|
68
|
+
return;
|
|
69
|
+
cancelValueWait = undefined;
|
|
70
|
+
}
|
|
71
|
+
const result = await nativeBridge.send({ action: 'values' }).catch(() => undefined);
|
|
72
|
+
if (!current())
|
|
73
|
+
return;
|
|
74
|
+
if (result?.values && typeof result.values === 'object' && !Array.isArray(result.values)) {
|
|
75
|
+
try {
|
|
76
|
+
applyNativeValues(element, result.values, config.values);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.warn('[Native UI Engine] Cannot reflect values', error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
42
83
|
const placementOwner = {};
|
|
43
84
|
let coveredToken;
|
|
44
85
|
let placementEpoch = 0;
|
|
@@ -138,7 +179,7 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
138
179
|
let revision = 0;
|
|
139
180
|
let contentRevision = 0;
|
|
140
181
|
let generation = 0;
|
|
141
|
-
let restoring
|
|
182
|
+
let restoring;
|
|
142
183
|
let initializing = false;
|
|
143
184
|
let scheduled = false;
|
|
144
185
|
let reconciling = false;
|
|
@@ -151,6 +192,9 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
151
192
|
return matches.length === 1 && matches[0] instanceof HTMLElement ? matches[0] : undefined;
|
|
152
193
|
};
|
|
153
194
|
const setVisibility = () => {
|
|
195
|
+
const intent = effectiveDestination();
|
|
196
|
+
if (target && target.getAttribute('data-native-ui-destination') !== intent)
|
|
197
|
+
target.setAttribute('data-native-ui-destination', intent);
|
|
154
198
|
if (!target)
|
|
155
199
|
return;
|
|
156
200
|
if (destination === 'web' || (destination === 'native' && placementPending))
|
|
@@ -206,16 +250,16 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
206
250
|
target?.removeAttribute(marker);
|
|
207
251
|
return;
|
|
208
252
|
}
|
|
209
|
-
if (restoring ||
|
|
253
|
+
if (!manager?.restoring || restoring === manager || destination !== 'native')
|
|
210
254
|
return;
|
|
211
|
-
restoring =
|
|
255
|
+
restoring = manager;
|
|
212
256
|
const version = generation;
|
|
213
257
|
const current = manager;
|
|
214
258
|
// The native library acknowledges after the live views have entered a draw pass.
|
|
215
259
|
void bridge
|
|
216
260
|
.send({ action: 'resume' })
|
|
217
261
|
.then(() => {
|
|
218
|
-
if (stopped || version !== generation || current !== manager)
|
|
262
|
+
if (stopped || version !== generation || current !== manager || findTarget() !== target)
|
|
219
263
|
return;
|
|
220
264
|
placementPending = false;
|
|
221
265
|
current.updated(undefined, [sourceID], ++revision);
|
|
@@ -229,33 +273,55 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
229
273
|
void recoverFailedProjection();
|
|
230
274
|
})
|
|
231
275
|
.finally(() => {
|
|
232
|
-
restoring
|
|
276
|
+
if (restoring === current)
|
|
277
|
+
restoring = undefined;
|
|
233
278
|
wake();
|
|
234
279
|
});
|
|
235
280
|
};
|
|
236
281
|
const reconcile = async () => {
|
|
237
|
-
if (stopped || initializing
|
|
238
|
-
return;
|
|
239
|
-
if (manager?.restoring) {
|
|
240
|
-
wake();
|
|
282
|
+
if (stopped || initializing)
|
|
241
283
|
return;
|
|
242
|
-
}
|
|
243
284
|
const found = findTarget();
|
|
244
285
|
if (found !== target) {
|
|
245
286
|
generation++;
|
|
287
|
+
automaticMotion = false;
|
|
288
|
+
nativeDragging = false;
|
|
289
|
+
clearTimeout(scrollTimer);
|
|
246
290
|
clearPlacement();
|
|
247
291
|
target?.removeAttribute(marker);
|
|
248
|
-
|
|
292
|
+
target?.removeAttribute('data-native-ui-destination');
|
|
293
|
+
retireValueRead();
|
|
294
|
+
// A page/DOM replacement retires its old surface even during a drag or draw ACK.
|
|
295
|
+
// Hide before releasing its image so retirement cannot reveal the departed view.
|
|
296
|
+
const previous = manager;
|
|
297
|
+
const previousTarget = target;
|
|
249
298
|
manager = undefined;
|
|
299
|
+
target = undefined;
|
|
300
|
+
sizeObserver.disconnect();
|
|
301
|
+
refreshScrollListeners();
|
|
302
|
+
try {
|
|
303
|
+
if (previousTarget)
|
|
304
|
+
await bridge.send({ action: 'visibility', visible: false });
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
await previous?.destroy();
|
|
308
|
+
}
|
|
250
309
|
if (stopped)
|
|
251
310
|
return;
|
|
252
|
-
target =
|
|
311
|
+
target = findTarget();
|
|
253
312
|
placementPending = false;
|
|
254
|
-
|
|
255
|
-
if (target)
|
|
313
|
+
if (target) {
|
|
256
314
|
sizeObserver.observe(target);
|
|
315
|
+
void refreshValues();
|
|
316
|
+
}
|
|
257
317
|
}
|
|
258
318
|
refreshScrollListeners();
|
|
319
|
+
if (automaticMotion)
|
|
320
|
+
return;
|
|
321
|
+
if (manager?.restoring) {
|
|
322
|
+
wake();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
259
325
|
destination = effectiveDestination();
|
|
260
326
|
if (!target) {
|
|
261
327
|
await bridge.send({ action: 'visibility', visible: false });
|
|
@@ -516,6 +582,7 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
516
582
|
if (stopped)
|
|
517
583
|
return;
|
|
518
584
|
stopped = true;
|
|
585
|
+
retireValueRead();
|
|
519
586
|
clearPlacement();
|
|
520
587
|
generation++;
|
|
521
588
|
stopOverlays();
|
|
@@ -530,6 +597,7 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
530
597
|
scrollAncestors.clear();
|
|
531
598
|
clearTimeout(scrollTimer);
|
|
532
599
|
target?.removeAttribute(marker);
|
|
600
|
+
target?.removeAttribute('data-native-ui-destination');
|
|
533
601
|
style.remove();
|
|
534
602
|
await Promise.allSettled([Promise.resolve(events?.remove()), manager?.destroy()]);
|
|
535
603
|
};
|
|
@@ -552,6 +620,7 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
552
620
|
applyDestination(effectiveDestination(), true);
|
|
553
621
|
}
|
|
554
622
|
if (updated) {
|
|
623
|
+
void refreshValues();
|
|
555
624
|
contentRevision++;
|
|
556
625
|
manager?.changed();
|
|
557
626
|
if (destination === 'native')
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
/** Coalesce current-state reads and serialize them across asynchronous native acknowledgements. */
|
|
4
|
+
export declare const createNativeUpdateQueue: (window: Pick<Window, "requestAnimationFrame" | "cancelAnimationFrame">, options: {
|
|
5
|
+
update(send: NativeUpdateSender): Promise<void>;
|
|
6
|
+
failed(error: unknown): void | Promise<void>;
|
|
7
|
+
retry(): void;
|
|
8
|
+
reconnect?: boolean;
|
|
9
|
+
}) => {
|
|
10
|
+
request: () => void;
|
|
11
|
+
readonly dirty: boolean;
|
|
12
|
+
readonly running: boolean;
|
|
13
|
+
readonly waiting: boolean;
|
|
14
|
+
reconnected(): void;
|
|
15
|
+
destroy(): void;
|
|
16
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Coalesce current-state reads and serialize them across asynchronous native acknowledgements. */
|
|
2
|
+
export const createNativeUpdateQueue = (window, options) => {
|
|
3
|
+
let frame = 0;
|
|
4
|
+
let dirty = false;
|
|
5
|
+
let running = false;
|
|
6
|
+
let stopped = false;
|
|
7
|
+
let retrying = false;
|
|
8
|
+
let waiting = false;
|
|
9
|
+
let reconciled = false;
|
|
10
|
+
let passFailed = false;
|
|
11
|
+
const request = () => {
|
|
12
|
+
if (stopped)
|
|
13
|
+
return;
|
|
14
|
+
dirty = true;
|
|
15
|
+
if (!running && !waiting && !frame)
|
|
16
|
+
frame = window.requestAnimationFrame(() => void run());
|
|
17
|
+
};
|
|
18
|
+
const waitForConnection = (error) => {
|
|
19
|
+
if (!options.reconnect || reconciled)
|
|
20
|
+
throw error;
|
|
21
|
+
waiting = true;
|
|
22
|
+
dirty = false;
|
|
23
|
+
};
|
|
24
|
+
const send = async (operation, validate) => {
|
|
25
|
+
if (stopped || waiting || passFailed)
|
|
26
|
+
return;
|
|
27
|
+
let result;
|
|
28
|
+
try {
|
|
29
|
+
result = await operation();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (stopped)
|
|
33
|
+
return;
|
|
34
|
+
passFailed = true;
|
|
35
|
+
if (retrying)
|
|
36
|
+
waitForConnection(error);
|
|
37
|
+
else {
|
|
38
|
+
retrying = true;
|
|
39
|
+
options.retry();
|
|
40
|
+
request();
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (stopped)
|
|
45
|
+
return;
|
|
46
|
+
try {
|
|
47
|
+
validate?.(result);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
passFailed = true;
|
|
51
|
+
waitForConnection(error);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
const run = async () => {
|
|
57
|
+
frame = 0;
|
|
58
|
+
if (stopped || waiting)
|
|
59
|
+
return;
|
|
60
|
+
dirty = false;
|
|
61
|
+
running = true;
|
|
62
|
+
passFailed = false;
|
|
63
|
+
try {
|
|
64
|
+
await options.update(send);
|
|
65
|
+
if (!passFailed)
|
|
66
|
+
retrying = waiting = reconciled = false;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (stopped)
|
|
70
|
+
return;
|
|
71
|
+
// Retire the queue before reporting terminal failure; no late update can restart it.
|
|
72
|
+
stopped = true;
|
|
73
|
+
await options.failed(error);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
running = false;
|
|
77
|
+
if (dirty)
|
|
78
|
+
request();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
request,
|
|
83
|
+
get dirty() {
|
|
84
|
+
return dirty;
|
|
85
|
+
},
|
|
86
|
+
get running() {
|
|
87
|
+
return running;
|
|
88
|
+
},
|
|
89
|
+
get waiting() {
|
|
90
|
+
return waiting;
|
|
91
|
+
},
|
|
92
|
+
reconnected() {
|
|
93
|
+
if (stopped)
|
|
94
|
+
return;
|
|
95
|
+
if (waiting) {
|
|
96
|
+
reconciled = true;
|
|
97
|
+
retrying = waiting = false;
|
|
98
|
+
}
|
|
99
|
+
request();
|
|
100
|
+
},
|
|
101
|
+
destroy() {
|
|
102
|
+
stopped = true;
|
|
103
|
+
dirty = false;
|
|
104
|
+
window.cancelAnimationFrame(frame);
|
|
105
|
+
frame = 0;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Reflect native state without inventing component-specific input events. */
|
|
2
|
+
export interface NativeValueBinding {
|
|
3
|
+
property?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const applyNativeValues: (target: HTMLElement, values: Record<string, unknown>, bindings: Record<string, NativeValueBinding>) => void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const applyNativeValues = (target, values, bindings) => {
|
|
2
|
+
for (const [key, value] of Object.entries(values)) {
|
|
3
|
+
if (!/^[a-z][a-z0-9-]*$/.test(key))
|
|
4
|
+
throw new TypeError('Native value keys must be lowercase attribute names');
|
|
5
|
+
const binding = Object.hasOwn(bindings, key) ? bindings[key] : undefined;
|
|
6
|
+
const property = binding?.property;
|
|
7
|
+
if (property &&
|
|
8
|
+
(!/^[a-zA-Z][\w]*$/.test(property) ||
|
|
9
|
+
/^on/i.test(property) ||
|
|
10
|
+
['innerHTML', 'outerHTML', 'textContent', 'innerText', 'style', 'constructor', 'prototype'].includes(property)))
|
|
11
|
+
throw new TypeError('Native value binding requires a data property');
|
|
12
|
+
const serialized = JSON.stringify(value);
|
|
13
|
+
if (serialized === undefined)
|
|
14
|
+
throw new TypeError('Native values must be JSON values');
|
|
15
|
+
if (property) {
|
|
16
|
+
const element = target;
|
|
17
|
+
if (JSON.stringify(element[property]) !== serialized)
|
|
18
|
+
element[property] = value;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const attribute = `data-native-ui-value-${key}`;
|
|
22
|
+
const text = typeof value === 'string' ? value : serialized;
|
|
23
|
+
if (target.getAttribute(attribute) !== text)
|
|
24
|
+
target.setAttribute(attribute, text);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type WebProjectionOptions } from './web.js';
|
|
2
|
+
import type { WebProjectionSource, WebProjectionTransport } from '../types.js';
|
|
3
|
+
export interface WebProjectionSurfaceOptions extends WebProjectionOptions {
|
|
4
|
+
namespace: string;
|
|
5
|
+
target(source: WebProjectionSource): HTMLElement | undefined;
|
|
6
|
+
/** Only renderer-owned fallback hosts may move into another overlay layer. */
|
|
7
|
+
canRelocate?(target: HTMLElement): boolean;
|
|
8
|
+
}
|
|
9
|
+
/** A renderer supplies placement policy; the Engine owns the complete image handoff. */
|
|
10
|
+
export declare const createWebProjectionSurface: (transport: WebProjectionTransport, options: WebProjectionSurfaceOptions) => Promise<Omit<{
|
|
11
|
+
readonly owners: readonly HTMLElement[];
|
|
12
|
+
readonly visibleOwners: readonly HTMLElement[];
|
|
13
|
+
readonly preparedOwners: readonly HTMLElement[];
|
|
14
|
+
consider: (revision: number, candidates: import("../types.js").ScopedWebProjectionSource[]) => void;
|
|
15
|
+
changed: (scrollOwner?: EventTarget) => void;
|
|
16
|
+
moved: () => void;
|
|
17
|
+
readonly token: string | undefined;
|
|
18
|
+
begin: () => void;
|
|
19
|
+
readonly restoring: boolean;
|
|
20
|
+
readonly handoffPending: boolean;
|
|
21
|
+
retireRestoration: () => void;
|
|
22
|
+
end: () => void;
|
|
23
|
+
updated: (token: string | undefined, restoredIDs?: string[], revision?: number) => void;
|
|
24
|
+
invalidate: (nativeHidden?: boolean) => void;
|
|
25
|
+
destroy(): Promise<void>;
|
|
26
|
+
}, "consider"> & {
|
|
27
|
+
consider(revision: number, sources: import("../types.js").ScopedWebProjectionSource[]): void;
|
|
28
|
+
} & {
|
|
29
|
+
consider(revision: number, sources: WebProjectionSource[]): void;
|
|
30
|
+
begin(layer?: HTMLElement): void;
|
|
31
|
+
updated(token: string | undefined, ids?: string[], revision?: number): void;
|
|
32
|
+
invalidate(nativeHidden?: boolean): void;
|
|
33
|
+
destroy(): Promise<void>;
|
|
34
|
+
}>;
|
|
35
|
+
export type WebProjectionSurface = Awaited<ReturnType<typeof createWebProjectionSurface>>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createWebProjectionBatch } from './web.js';
|
|
2
|
+
import { createWebProjectionScope } from './scope.js';
|
|
3
|
+
/** A renderer supplies placement policy; the Engine owns the complete image handoff. */
|
|
4
|
+
export const createWebProjectionSurface = async (transport, options) => {
|
|
5
|
+
const scopes = new WeakMap();
|
|
6
|
+
const moved = new Map();
|
|
7
|
+
const restorePlacement = () => {
|
|
8
|
+
for (const [element, saved] of [...moved].reverse()) {
|
|
9
|
+
if (element.parentNode === saved.layer && saved.parent.isConnected)
|
|
10
|
+
saved.parent.insertBefore(element, saved.next?.parentNode === saved.parent ? saved.next : null);
|
|
11
|
+
}
|
|
12
|
+
moved.clear();
|
|
13
|
+
};
|
|
14
|
+
const projection = await createWebProjectionBatch(transport, {
|
|
15
|
+
...options,
|
|
16
|
+
wake: () => {
|
|
17
|
+
if (!projection.token && !projection.restoring)
|
|
18
|
+
restorePlacement();
|
|
19
|
+
options.wake();
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
let latest = [];
|
|
23
|
+
const { consider, begin, updated, invalidate, destroy } = projection;
|
|
24
|
+
return Object.assign(projection, {
|
|
25
|
+
consider(revision, sources) {
|
|
26
|
+
latest = sources;
|
|
27
|
+
consider(revision, sources.flatMap((source) => {
|
|
28
|
+
const target = options.target(source);
|
|
29
|
+
if (!target?.isConnected || target.ownerDocument !== source.element.ownerDocument)
|
|
30
|
+
return [];
|
|
31
|
+
let scope = scopes.get(target);
|
|
32
|
+
if (!scope) {
|
|
33
|
+
scope = createWebProjectionScope(target, options.namespace);
|
|
34
|
+
scopes.set(target, scope);
|
|
35
|
+
}
|
|
36
|
+
return [{ ...source, scope }];
|
|
37
|
+
}));
|
|
38
|
+
},
|
|
39
|
+
begin(layer) {
|
|
40
|
+
if (layer && !layer.isConnected)
|
|
41
|
+
return;
|
|
42
|
+
if (layer && !projection.token) {
|
|
43
|
+
for (const source of latest) {
|
|
44
|
+
if (!projection.preparedOwners.includes(source.element))
|
|
45
|
+
continue;
|
|
46
|
+
const target = options.target(source);
|
|
47
|
+
if (!target?.isConnected ||
|
|
48
|
+
!target.parentNode ||
|
|
49
|
+
target.ownerDocument !== layer.ownerDocument ||
|
|
50
|
+
layer.contains(target) ||
|
|
51
|
+
target.contains(layer) ||
|
|
52
|
+
!options.canRelocate?.(target))
|
|
53
|
+
continue;
|
|
54
|
+
if (!moved.has(target))
|
|
55
|
+
moved.set(target, { parent: target.parentNode, next: target.nextSibling, layer });
|
|
56
|
+
layer.append(target);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
begin();
|
|
60
|
+
if (!projection.token)
|
|
61
|
+
restorePlacement();
|
|
62
|
+
},
|
|
63
|
+
updated(token, ids, revision) {
|
|
64
|
+
updated(token, ids, revision);
|
|
65
|
+
if (!projection.restoring && !projection.token)
|
|
66
|
+
restorePlacement();
|
|
67
|
+
},
|
|
68
|
+
invalidate(nativeHidden) {
|
|
69
|
+
invalidate(nativeHidden);
|
|
70
|
+
restorePlacement();
|
|
71
|
+
},
|
|
72
|
+
async destroy() {
|
|
73
|
+
try {
|
|
74
|
+
await destroy();
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
restorePlacement();
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WebProjectionSurface } from './surface.js';
|
|
2
|
+
/** Owns asynchronous projection creation and disposal for DOM-backed surfaces. */
|
|
3
|
+
export declare const createWebProjectionSurfaces: <Owner extends Node, Surface extends WebProjectionSurface>(options?: {
|
|
4
|
+
ignoreTarget?(element: Element): boolean;
|
|
5
|
+
}) => {
|
|
6
|
+
get: (owner: Owner) => Surface | undefined;
|
|
7
|
+
keys: () => MapIterator<Owner>;
|
|
8
|
+
values: () => Surface[];
|
|
9
|
+
changed: (owner?: EventTarget) => void;
|
|
10
|
+
invalidate: () => void;
|
|
11
|
+
followMotion: (moving: ReadonlyMap<HTMLElement, unknown>, target?: (source: HTMLElement) => HTMLElement) => void;
|
|
12
|
+
end: () => void;
|
|
13
|
+
consider: (revision: number, sources: import("../types.js").WebProjectionSource[]) => void;
|
|
14
|
+
updated: (token: string | undefined, restoredIDs?: string[] | undefined, revision?: number | undefined) => void;
|
|
15
|
+
readonly restoring: boolean;
|
|
16
|
+
readonly handoffPending: boolean;
|
|
17
|
+
ensure(owner: Owner, create: () => Promise<Surface>): Promise<Surface | undefined>;
|
|
18
|
+
delete: (owner: Owner) => Promise<void>;
|
|
19
|
+
destroy(): Promise<void>;
|
|
20
|
+
};
|