@webview-native-ui/engine 0.0.5 → 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.
Files changed (47) hide show
  1. package/README.md +19 -12
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +3 -0
  4. package/dist/native/session.d.ts +5 -0
  5. package/dist/native/session.js +83 -14
  6. package/dist/native/updates.d.ts +16 -0
  7. package/dist/native/updates.js +108 -0
  8. package/dist/native/values.d.ts +5 -0
  9. package/dist/native/values.js +27 -0
  10. package/dist/projection/surface.d.ts +35 -0
  11. package/dist/projection/surface.js +81 -0
  12. package/dist/projection/surfaces.d.ts +20 -0
  13. package/dist/projection/surfaces.js +116 -0
  14. package/dist/projection/web.js +22 -12
  15. package/dist/types.d.ts +4 -0
  16. package/docs/README.md +70 -0
  17. package/docs/advanced/README.md +20 -0
  18. package/docs/advanced/android-placement.md +48 -0
  19. package/docs/advanced/native-handoff.md +89 -0
  20. package/docs/advanced/renderer.md +148 -0
  21. package/docs/advanced/update-queue.md +58 -0
  22. package/docs/advanced/web-projection.md +88 -0
  23. package/docs/assets/ios-native-capture-comparison.png +0 -0
  24. package/docs/concepts.md +97 -0
  25. package/docs/configuration/README.md +29 -0
  26. package/docs/configuration/capture.md +27 -0
  27. package/docs/configuration/events.md +35 -0
  28. package/docs/configuration/ionic-overlays.md +65 -0
  29. package/docs/configuration/values.md +125 -0
  30. package/docs/cordova.md +1 -1
  31. package/docs/{capacitor-plugin.md → getting-started/capacitor.md} +45 -47
  32. package/docs/{cordova-plugin.md → getting-started/cordova.md} +32 -15
  33. package/docs/getting-started/initialization.md +79 -0
  34. package/docs/maintenance.md +14 -2
  35. package/docs/{projection-containers.md → reference/containers.md} +6 -2
  36. package/docs/reference/platforms.md +137 -0
  37. package/docs/reference/projection.md +134 -0
  38. package/docs/reference/state.md +39 -0
  39. package/ios/Sources/WebViewNativeUIEngine/ClippedView.swift +12 -12
  40. package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +70 -9
  41. package/ios/Sources/WebViewNativeUIEngine/Frame.swift +10 -10
  42. package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +92 -44
  43. package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -3
  44. package/ios/Sources/WebViewNativeUIEngine/ScrollBindings.swift +3 -0
  45. package/ios/Sources/WebViewNativeUIEngine/ScrollTracker.swift +10 -8
  46. package/package.json +10 -3
  47. package/docs/projection.md +0 -364
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 [projection behavior](docs/projection.md) for the detailed contract and limits.
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
- ## Integration
34
+ ## Choose your starting point
35
35
 
36
- Applications gain these capabilities through plugins that integrate the Engine. Capacitor and Cordova plugin authors add it as a library dependency while retaining their existing native controls and public APIs. The same Engine supports iOS and Android. Ionic and Angular are not required; Ionic overlay coordination is optional.
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
- - [Capacitor plugin guide](docs/capacitor-plugin.md) — install dependencies and add registration, events and teardown to your existing plugin.
39
- - [Cordova plugin guide](docs/cordova-plugin.md) — integrate the same Engine with Cordova's lifecycle and resource handler, without a Capacitor dependency.
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; applications do not need a separate Engine initializer for the base integration. Keep your plugin's normal API and notifications.
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
- The current API is pre-1.0. Use matching, pinned Engine versions across npm and native dependencies. Android placement requires API 23+, with composited snapshots available from API 26+.
52
+ ## Reference and contributing
44
53
 
45
- ## Further reading
46
-
47
- - [Projection container support](docs/projection-containers.md) — supported HTML and Ionic destinations, sizing conditions and wrapper examples.
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';
@@ -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;
@@ -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 = false;
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 || !manager?.restoring || destination !== 'native')
253
+ if (!manager?.restoring || restoring === manager || destination !== 'native')
210
254
  return;
211
- restoring = true;
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 = false;
276
+ if (restoring === current)
277
+ restoring = undefined;
233
278
  wake();
234
279
  });
235
280
  };
236
281
  const reconcile = async () => {
237
- if (stopped || initializing || automaticMotion)
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
- await manager?.destroy();
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 = found;
311
+ target = findTarget();
253
312
  placementPending = false;
254
- sizeObserver.disconnect();
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
+ };